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 8 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>
62 changes: 62 additions & 0 deletions lib/base/helpers/model_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'dart:math';
import 'package:gocast_mobile/model/user_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,
);
}
}
117 changes: 117 additions & 0 deletions lib/base/networking/apis/auth_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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/user_model.dart';
import 'package:gocast_mobile/routes.dart';
import 'package:shared_preferences/shared_preferences.dart';

class AuthHandler {
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');
print('Current jwt: $jwt');
return;
}
}
GravityDarkLab marked this conversation as resolved.
Show resolved Hide resolved

// TODO: Handle error when no jwt cookie is found
print('No JWT cookie found');
carlobortolan marked this conversation as resolved.
Show resolved Hide resolved
}

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) {
// TODO: Handle network errors
print('Request error: $e');
carlobortolan marked this conversation as resolved.
Show resolved Hide resolved
return;
}

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

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

static Future<void> ssoAuth(BuildContext context) async {
print('ssoAuth started');
// Redirect the user to the Shibboleth login page
print('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)),
onWebViewCreated: (webview.InAppWebViewController controller) {
print('Web view created');
},
onLoadStart: (webview.InAppWebViewController controller, Uri? url) {
print('Page load started: $url');
},
onLoadStop:
(webview.InAppWebViewController controller, Uri? url) async {
print('Page load stopped: $url');

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

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

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

// Close the web view and go back to the app
print('Closing web view and returning to app');
Navigator.pop(context);
}
},
),
),
);
}
carlobortolan marked this conversation as resolved.
Show resolved Hide resolved

// 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());
}
43 changes: 35 additions & 8 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gocast_mobile/model/user_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});
class App extends ConsumerWidget {
const App({super.key});

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

return MaterialApp(
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')),
},
);
}
}
Loading