Skip to content
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
9 changes: 7 additions & 2 deletions lib/src/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class AppModule extends Module {
customTransition: defaultTransition,
guards: [
CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'),
// apparently having a guard in a child route will override the parent's guard so we need it here too
// Important to have this guard last as the order of guards is reversed
AuthGuard(redirectTo: '/auth/'),
],
),
ModuleRoute(
Expand All @@ -74,8 +77,9 @@ class AppModule extends Module {
transition: TransitionType.custom,
customTransition: defaultTransition,
guards: [
CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'),
FeatureGuard([kCalendarPlanFeatureID], redirectTo: '/settings/'),
CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'),
AuthGuard(redirectTo: '/auth/'),
],
),
ModuleRoute(
Expand All @@ -97,14 +101,15 @@ class AppModule extends Module {
customTransition: defaultTransition,
guards: [
CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'),
AuthGuard(redirectTo: '/auth/'),
],
),
],
customTransition: defaultTransition,
transition: TransitionType.custom,
guards: [
AuthGuard(redirectTo: '/auth/'),
HasCoursesGuard(),
AuthGuard(redirectTo: '/auth/'),
],
)
..module(
Expand Down
34 changes: 18 additions & 16 deletions lib/src/app/presentation/screens/not_found_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,24 @@ class NotFoundScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: PaddingAll(),
child: Column(
children: [
ImageMessage(
message: context.t.notFound,
image: Assets.a404,
).expanded(),
Spacing.mediumVertical(),
ElevatedButton(
onPressed: () {
Modular.to.navigate('/dashboard/');
},
child: Text(context.t.notFound_returnHome),
),
],
body: Center(
child: Padding(
padding: PaddingAll(),
child: Column(
children: [
ImageMessage(
message: context.t.notFound,
image: Assets.a404,
).expanded(),
Spacing.mediumVertical(),
ElevatedButton(
onPressed: () {
Modular.to.navigate('/dashboard/');
},
child: Text(context.t.notFound_returnHome),
),
],
),
),
),
);
Expand Down
22 changes: 12 additions & 10 deletions lib/src/app/presentation/widgets/image_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ class ImageMessage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Column(
children: [
image.themed(context).expanded(),
Spacing.mediumVertical(),
Text(
message,
style: context.theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
],
return Center(
child: Column(
children: [
image.themed(context).expanded(),
Spacing.mediumVertical(),
Text(
message,
style: context.theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
],
),
);
}
}
2 changes: 1 addition & 1 deletion lib/src/app/utils/sentry_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension TransactionX on ISentrySpan {
}

/// A mixin that adds transaction tracing to a class.
mixin Tracable on ILoggable {
mixin Tracable on Loggable {
ISentrySpan? _transaction;

/// The parent tracable to attach transactions to.
Expand Down
12 changes: 9 additions & 3 deletions lib/src/auth/presentation/guards/auth_guard.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'package:eduplanner/src/auth/auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:mcquenji_core/mcquenji_core.dart';

/// Guard that checks if the user is authenticated.
///
/// You must import [AuthModule] in the module using this guard.
class AuthGuard extends RouteGuard {
class AuthGuard extends RouteGuard with MiddlewareLogger {
/// Guard that checks if the user is authenticated.
///
/// You must import [AuthModule] in the module using this guard.
Expand All @@ -14,10 +15,15 @@ class AuthGuard extends RouteGuard {
Future<bool> canActivate(String path, ModularRoute route) async {
final auth = Modular.tryGet<AuthRepository>();

if (auth == null) return false;
if (auth == null) {
log('AuthRepository not found');
return false;
}

await auth.ready;

return Modular.get<AuthRepository>().isAuthenticated;
log('Authenticated: ${auth.isAuthenticated}');

return auth.isAuthenticated;
}
}
7 changes: 6 additions & 1 deletion lib/src/auth/presentation/guards/capability_guard.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'dart:async';

import 'package:eduplanner/src/auth/auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:mcquenji_core/mcquenji_core.dart';

/// Guard that checks if the user has the required capabilities.
class CapabilityGuard extends RouteGuard {
class CapabilityGuard extends RouteGuard with MiddlewareLogger {
/// The required capabilities.
final Set<UserCapability> capabilities;

Expand All @@ -19,6 +22,8 @@ class CapabilityGuard extends RouteGuard {

await user.ready;

log('User capabilities: ${user.state.data?.capabilities} ; Required: $capabilities');

if (!user.state.hasData) {
return false;
}
Expand Down
16 changes: 12 additions & 4 deletions lib/src/auth/presentation/repositories/auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AuthRepository extends Repository<AsyncValue<Set<Token>>> with Tracable {
}

@override
FutureOr<void> build(BuildTrigger trigger) async {
FutureOr<void> build(Trigger trigger) async {
if (trigger is! InitialBuildTrigger) return;

if (_state != null && isAuthenticated) {
Expand Down Expand Up @@ -70,7 +70,7 @@ class AuthRepository extends Repository<AsyncValue<Set<Token>>> with Tracable {
}

/// Sign in with [username] and [password].
Future<void> authenticate({
Future<bool> authenticate({
required String username,
required String password,
}) async {
Expand All @@ -94,14 +94,17 @@ class AuthRepository extends Repository<AsyncValue<Set<Token>>> with Tracable {
},
);

if (!state.hasData) return;
if (!state.hasData) return false;

log('Authentication successful');

await _localStorage.write(state.requireData);

return true;
} catch (e) {
transaction.internalError(e);
rethrow;

return false;
} finally {
await transaction.commit();
}
Expand All @@ -128,6 +131,11 @@ class AuthRepository extends Repository<AsyncValue<Set<Token>>> with Tracable {
/// `true` if the user is authenticated.
bool get isAuthenticated => state.hasData && state.requireData.isNotEmpty;

/// Throws a [WaitForDataException] if the user is not authenticated.
void requireAuth() {
if (!isAuthenticated) throw WaitForDataException(AuthRepository);
}

@override
void emit(AsyncValue<Set<Token>> state) {
super.emit(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class EchidnaUserRepository extends UserIdRepository {
}

@override
FutureOr<void> build(BuildTrigger trigger) async {
FutureOr<void> build(Trigger trigger) async {
if (trigger is! UserRepository) return;

final user = waitForData(_user);
Expand Down
8 changes: 3 additions & 5 deletions lib/src/auth/presentation/repositories/user_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,14 @@ class UserRepository extends Repository<AsyncValue<User>> with Tracable {
}

@override
FutureOr<void> build(BuildTrigger trigger) async {
FutureOr<void> build(Trigger trigger) async {
final transaction = startTransaction('loadUsers');

await _auth.ready;

final tokens = waitForData(_auth);

_isHandlingAuthChange = true;

if (tokens.isEmpty) {
if (!_auth.isAuthenticated) {
log('User is unauthenticated');

error(
Expand Down Expand Up @@ -67,7 +65,7 @@ class UserRepository extends Repository<AsyncValue<User>> with Tracable {
distinctId: hash,
properties: {
'capabilities': user.capabilities.map((c) => c.name).toList(),
'vintage': user.vintage,
'vintage': user.vintage?.humanReadable,
'theme': user.themeName,
'optional_tasks_enabled': user.optionalTasksEnabled,
'display_task_count': user.displayTaskCount,
Expand Down
20 changes: 8 additions & 12 deletions lib/src/auth/presentation/screens/login_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,8 @@ class LoginScreen extends StatelessWidget with AdaptiveWidget {
/// Presents an authentication form to the user.
const LoginScreen({super.key});

@override
Widget build(BuildContext context) {
final user = context.watch<UserRepository>();
final auth = context.watch<AuthRepository>();

if (auth.isAuthenticated && user.state.hasData) {
Modular.to.navigate('/dashboard/');
}

return super.build(context);
static void _onLogin() {
Modular.to.navigate('/dashboard/');
}

@override
Expand All @@ -42,7 +34,9 @@ class LoginScreen extends StatelessWidget with AdaptiveWidget {
padding: EdgeInsets.only(right: 150),
child: SizedBox(
width: 350,
child: LoginForm(),
child: LoginForm(
onLogin: _onLogin,
),
),
),
),
Expand All @@ -61,7 +55,9 @@ class LoginScreen extends StatelessWidget with AdaptiveWidget {
spacing: Spacing.mediumSpacing,
children: [
const Spacer(),
const LoginForm(),
const LoginForm(
onLogin: _onLogin,
),
const Spacer(),
Text(
context.t.auth_version(kInstalledRelease.toString()),
Expand Down
15 changes: 9 additions & 6 deletions lib/src/auth/presentation/widgets/login_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import 'package:url_launcher/url_launcher.dart';
/// A form prompting the user to input their credentials.
class LoginForm extends StatefulWidget {
/// A form prompting the user to input their credentials.
const LoginForm({super.key});
const LoginForm({super.key, this.onLogin});

/// Callback to be called when the user logs in successfully.
final void Function()? onLogin;

@override
State<LoginForm> createState() => _LoginFormState();
Expand Down Expand Up @@ -79,17 +82,17 @@ class _LoginFormState extends State<LoginForm> with WidgetsBindingObserver {

final auth = Modular.get<AuthRepository>();

await auth.authenticate(username: username, password: password);
final success = await auth.authenticate(username: username, password: password);

setState(() {
loggingIn = false;
});

if (auth.state.hasError) {
return;
if (success) {
widget.onLogin?.call();
} else {
usernameFocusNode.requestFocus();
}

usernameFocusNode.requestFocus();
}

void togglePasswordVisibility() {
Expand Down
Loading
Loading