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

Commit

Permalink
8 user authentication (#14)
Browse files Browse the repository at this point in the history
* Update README.md and dependencies

* Add basic app structure and assets

* Implement first models, viewmodels and api calls needed for user auth

* Implement TUM-SSO authentication and update README.md and

* Clean up auth_handler.dart and update routes

* Update userState and userViewModel to use riverpod and rxdart

* Refactor code to improve readability

* Remove unused import

* Clean up code, refactor models, update error-handling
  • Loading branch information
carlobortolan authored Nov 19, 2023
1 parent c853664 commit 280e0c7
Show file tree
Hide file tree
Showing 21 changed files with 1,023 additions and 22 deletions.
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 = '$name.$lastName@example.com';
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
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
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
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

0 comments on commit 280e0c7

Please sign in to comment.