Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

8 user authentication #14

Merged
merged 11 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# The mobile client for [gocast](https://github.com/TUM-Dev/gocast)

This mobile client for [gocast](https://github.com/TUM-Dev/gocast) is currently under development by the [iPraktikum Winter 23/24](https://ase.cit.tum.de/teaching/23w/ipraktikum/) on behalf of the TUM Developers. In order not to influence the grading of the students, we would ask you to refrain from code contributions until **March 2023**. Until then, we look forward to your contributions in our other repositories. Thank you for your understanding!


## Features

- [x] Authentication using internal account
- [x] Authentication using TUM SSO
- [ ] Overview of own and publicly available Lectures
- [ ] Ability to watch lectures (single, multi - view and split - view)
- [ ] Bookmark lectures
- [ ] Automatic notifications if lecture starts
- [ ] Ability to search for lectures
- [ ] Ability to download lectures in a data privacy conform manner (non - exportable and remotely deletable)
- [ ] Ability to answer quizzes and feedback requests

## Config

1. Make sure to have a local [`gocast`](https://github.com/tum-dev/gocast) instance listening on port `8081`.

2. Run `$ flutter run` to start the app.

3. Run `dart fix --apply && dart format ./lib` before commiting new changes.

## Development

| Dependency | Usage | Where to download it |
|------------------------------------------|------------------------------------------|----------------------------------------------|
| `Flutter` (includes the `Dart` compiler) | SDK to develop this app | https://docs.flutter.dev/get-started/install |
| A local instance of [`gocast`](https://github.com/tum-dev/gocast) | API to fetch user data & streams | https://github.com/TUM-Dev/gocast#readme |
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="de.tum.gocast_mobile" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
Expand Down
Binary file added assets/images/logos/tum-logo-blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,15 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>gocast</string>
</array>
</dict>
</array>

</dict>
</plist>
65 changes: 65 additions & 0 deletions lib/base/helpers/model_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'dart:math';
import 'package:gocast_mobile/model/course/bookmark_model.dart';
import 'package:gocast_mobile/model/course/course_model.dart';
import 'package:gocast_mobile/model/user/user_model.dart';
import 'package:gocast_mobile/model/user/user_settings_model.dart';

class ModelGenerator {
static User generateRandomUser() {
final random = Random();
final id = random.nextInt(1000);
final name = 'User$id';
final lastName = 'Last$id';
final email = '[email protected]';
final matriculationNumber = 'M$id';
final lrzId = 'L$id';
final role = random.nextInt(3) + 1;
final password = 'password$id';
final courses = List.generate(
random.nextInt(5) + 1,
(index) => Course(id: index, name: 'Course$index'),
);
final administeredCourses = List.generate(
random.nextInt(3) + 1,
(index) => Course(id: index, name: 'Administered Course$index'),
);
final pinnedCourses = List.generate(
random.nextInt(3) + 1,
(index) => Course(id: index, name: 'Pinned Course$index'),
);
final settings = List.generate(
random.nextInt(3) + 1,
(index) => UserSetting(
id: index,
userId: id,
type: UserSettingType.values[random.nextInt(3)],
value: 'Value$index',
),
);
final bookmarks = List.generate(
random.nextInt(3) + 1,
(index) => Bookmark(
id: index,
userId: id,
title: 'Bookmark$index',
url: 'https://example.com/bookmark$index',
),
);

return User(
id: id,
name: name,
lastName: lastName,
email: email,
matriculationNumber: matriculationNumber,
lrzId: lrzId,
role: role,
password: password,
courses: courses,
administeredCourses: administeredCourses,
pinnedCourses: pinnedCourses,
settings: settings,
bookmarks: bookmarks,
);
}
}
98 changes: 98 additions & 0 deletions lib/base/networking/apis/auth_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as webview;
import 'package:gocast_mobile/base/helpers/model_generator.dart';
import 'package:gocast_mobile/model/token_model.dart';
import 'package:gocast_mobile/model/user/user_model.dart';
import 'package:gocast_mobile/routes.dart';

class AuthHandler {
static Future<void> basicAuth(
String username,
String password,
) async {
const url = Routes.basicLogin;
final cookieJar = CookieJar();
final dio = Dio(
BaseOptions(
followRedirects: false,
validateStatus: (status) {
return status! < 500;
},
),
);
dio.interceptors.add(CookieManager(cookieJar));

final formData = FormData.fromMap({
'username': username,
'password': password,
});

try {
await dio.post(
url,
data: formData,
);
} catch (e) {
// Throw the error so it can be caught and handled in the signIn method
carlobortolan marked this conversation as resolved.
Show resolved Hide resolved
throw Exception('Request error: $e');
}

List<Cookie> cookies = await cookieJar.loadForRequest(Uri.parse(url));

// Save jwt token
await Token.saveToken(cookies);
}

static Future<void> ssoAuth(BuildContext context) async {
debugPrint('ssoAuth started');
// Redirect the user to the Shibboleth login page
debugPrint('Login URL: $Routes.ssoLogin');

// Open the login page in a web view
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => webview.InAppWebView(
initialUrlRequest:
webview.URLRequest(url: Uri.parse(Routes.ssoLogin)),
onLoadStop:
(webview.InAppWebViewController controller, Uri? url) async {
debugPrint('Page load stopped: $url');

try {
final cookieManager = webview.CookieManager.instance();
List<webview.Cookie> cookies =
await cookieManager.getCookies(url: url!);

// Save jwt token
await Token.saveToken(
cookies.map((c) => Cookie(c.name, c.value)).toList(),
);

// Redirect back to app
if (url.toString().startsWith(Routes.ssoRedirect)) {
debugPrint('Redirect URL detected: $url');

// Close the web view and go back to the app
debugPrint('Closing web view and returning to app');
Navigator.pop(context);
}
} catch (e) {
// Throw the error so it can be caught and handled by the caller of ssoAuth
carlobortolan marked this conversation as resolved.
Show resolved Hide resolved
throw Exception('Error in ssoAuth: $e');
}
},
),
),
);
}

// Generate user mock for testing the views until API/v2 is implemented
static Future<User> fetchUser() async {
// TODO: Add GET:/user endpoint in gocast API to fetch current user information
ahmetsenturk marked this conversation as resolved.
Show resolved Hide resolved
return ModelGenerator.generateRandomUser();
}
}
33 changes: 33 additions & 0 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'dart:async';
import 'dart:developer';

import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';

class AppBlocObserver extends BlocObserver {
const AppBlocObserver();

@override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
log('onChange(${bloc.runtimeType}, $change)');
}

@override
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
log('onError(${bloc.runtimeType}, $error, $stackTrace)');
super.onError(bloc, error, stackTrace);
}
}

Future<void> bootstrap(FutureOr<Widget> Function() builder) async {
FlutterError.onError = (details) {
log(details.exceptionAsString(), stackTrace: details.stack);
};

Bloc.observer = const AppBlocObserver();

// Add cross-flavor configuration here

runApp(await builder());
}
54 changes: 46 additions & 8 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gocast_mobile/model/user/user_state_model.dart';
import 'package:gocast_mobile/viewModels/user_viewmodel.dart';
import 'package:gocast_mobile/views/home_view.dart';
import 'package:gocast_mobile/views/login_view.dart';
import 'package:gocast_mobile/views/welcome_view.dart';

final userViewModel = Provider((ref) => UserViewModel());

final userStateProvider = StreamProvider<UserState>((ref) {
return ref.watch(userViewModel).current.stream;
});

void main() {
runApp(const MyApp());
runApp(
const ProviderScope(
child: App(),
),
);
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();

class App extends ConsumerWidget {
const App({super.key});

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userStateProvider);

if (userState.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
scaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Error: ${userState.error}')),
);
});
}

return MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
title: 'gocast',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff0065bd)),
useMaterial3: true,
),
home: const Scaffold(
body: Center(
child: Text("Hello, iPraktikum! 👋"),
appBarTheme: AppBarTheme(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
),
home: userState.value?.user == null
? const LoginView(key: Key('loginView'))
: const HomeView(key: Key('homeView')),
routes: {
'/welcome': (context) => userState.value?.user == null
? const LoginView(key: Key('loginView'))
: const WelcomeView(key: Key('welcomeView')),
'/home': (context) => userState.value?.user == null
? const LoginView(key: Key('loginView'))
: const HomeView(key: Key('homeView')),
},
);
}
}
13 changes: 13 additions & 0 deletions lib/model/course/bookmark_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Bookmark {
Bookmark({
required this.id,
required this.userId,
required this.title,
required this.url,
});

int id;
int userId;
String title;
String url;
}
9 changes: 9 additions & 0 deletions lib/model/course/course_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Course {
Course({
required this.id,
required this.name,
});

int id;
String name;
}
23 changes: 23 additions & 0 deletions lib/model/token_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/foundation.dart';

// This class will be extended once the API/v2 is implemented
class Token {
static Future<void> saveToken(List<Cookie> cookies) async {
for (var cookie in cookies) {
if (cookie.name == 'jwt') {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('jwt', cookie.value);

// DEBUG: Check cookie
String? jwt = prefs.getString('jwt');
debugPrint('Current jwt: $jwt');
return;
}
}

// Handle error when no jwt cookie is found
throw Exception('No JWT cookie found');
}
}
Loading