Add complete authentication system supporting both web (NPM forward auth) and mobile (OIDC) authentication flows. Web flow: - Check /auth/me on startup to detect NPM forward auth session - Cookies handled by proxy, no Bearer tokens needed Mobile flow: - flutter_appauth for OIDC Authorization Code + PKCE - POST /auth/sync to get user profile and roles - Token storage in SharedPreferences Shared: - Permission system with Domain/Action enums and Role class - PermissionGate and AdminGate widgets for UI permission checks - Route guards redirecting unauthenticated users to login - Login page with platform-specific messaging Platform config: - iOS: CFBundleURLTypes for net.schweitz.tatlock:// - Android: appAuthRedirectScheme, minSdk 23 Docs: - Added Freezed 3.x sealed class documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
111 lines
2.7 KiB
Dart
111 lines
2.7 KiB
Dart
import 'package:flutter/widgets.dart' hide Action;
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'auth_provider.dart';
|
|
import 'permissions.dart';
|
|
|
|
/// A widget that conditionally renders its child based on user permissions.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// PermissionGate(
|
|
/// domain: Domain.controlRoom,
|
|
/// action: Action.admin,
|
|
/// child: DeleteButton(),
|
|
/// fallback: Text('No permission'),
|
|
/// )
|
|
/// ```
|
|
class PermissionGate extends ConsumerWidget {
|
|
const PermissionGate({
|
|
super.key,
|
|
required this.domain,
|
|
required this.action,
|
|
this.category = 'general',
|
|
required this.child,
|
|
this.fallback,
|
|
});
|
|
|
|
/// The domain required for this permission.
|
|
final Domain domain;
|
|
|
|
/// The action level required (viewer, user, editor, admin).
|
|
final Action action;
|
|
|
|
/// Optional category within the domain (defaults to 'general').
|
|
final String category;
|
|
|
|
/// Widget to show when user has permission.
|
|
final Widget child;
|
|
|
|
/// Widget to show when user lacks permission (defaults to empty).
|
|
final Widget? fallback;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final authState = ref.watch(authProvider);
|
|
|
|
final hasPermission = authState.maybeWhen(
|
|
data: (state) => state.hasPermission(domain, action, category: category),
|
|
orElse: () => false,
|
|
);
|
|
|
|
if (hasPermission) {
|
|
return child;
|
|
}
|
|
|
|
return fallback ?? const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
/// A widget that shows its child only if the user is a global admin.
|
|
class AdminGate extends ConsumerWidget {
|
|
const AdminGate({
|
|
super.key,
|
|
required this.child,
|
|
this.fallback,
|
|
});
|
|
|
|
/// Widget to show when user is admin.
|
|
final Widget child;
|
|
|
|
/// Widget to show when user is not admin (defaults to empty).
|
|
final Widget? fallback;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final authState = ref.watch(authProvider);
|
|
|
|
final isAdmin = authState.maybeWhen(
|
|
data: (state) => state.isGlobalAdmin,
|
|
orElse: () => false,
|
|
);
|
|
|
|
if (isAdmin) {
|
|
return child;
|
|
}
|
|
|
|
return fallback ?? const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
/// Extension for checking permissions in code.
|
|
extension PermissionCheck on WidgetRef {
|
|
/// Check if the current user has a specific permission.
|
|
bool hasPermission(Domain domain, Action action, {String category = 'general'}) {
|
|
final authState = read(authProvider);
|
|
return authState.maybeWhen(
|
|
data: (state) => state.hasPermission(domain, action, category: category),
|
|
orElse: () => false,
|
|
);
|
|
}
|
|
|
|
/// Check if the current user is a global admin.
|
|
bool get isGlobalAdmin {
|
|
final authState = read(authProvider);
|
|
return authState.maybeWhen(
|
|
data: (state) => state.isGlobalAdmin,
|
|
orElse: () => false,
|
|
);
|
|
}
|
|
}
|