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>
138 lines
3.6 KiB
Dart
138 lines
3.6 KiB
Dart
// Permission system for role-based access control.
|
|
//
|
|
// Roles follow the format: `domain.category:action`
|
|
// - Domain: Feature area (control-room, media, etc.)
|
|
// - Category: Sub-area within domain (default: general)
|
|
// - Action: Permission level (viewer < user < editor < admin)
|
|
|
|
/// Permission domains matching feature areas.
|
|
enum Domain {
|
|
controlRoom('control-room'),
|
|
library('library'),
|
|
media('media'),
|
|
ai('ai'),
|
|
housekeeper('housekeeper'),
|
|
developer('developer'),
|
|
documents('documents'),
|
|
gaming('gaming'),
|
|
admin('admin');
|
|
|
|
const Domain(this.value);
|
|
|
|
/// The API string value for this domain.
|
|
final String value;
|
|
|
|
/// Parse a domain string from API response.
|
|
static Domain? fromString(String value) {
|
|
for (final domain in Domain.values) {
|
|
if (domain.value == value) return domain;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Permission actions in hierarchical order.
|
|
///
|
|
/// Higher actions imply lower ones:
|
|
/// - admin implies editor, user, viewer
|
|
/// - editor implies user, viewer
|
|
/// - user implies viewer
|
|
enum Action {
|
|
viewer(1),
|
|
user(2),
|
|
editor(3),
|
|
admin(4);
|
|
|
|
const Action(this.level);
|
|
|
|
/// Numeric level for comparison (higher = more permissions).
|
|
final int level;
|
|
|
|
/// Check if this action grants at least the required action.
|
|
bool grants(Action required) => level >= required.level;
|
|
|
|
/// Parse an action string from API response.
|
|
static Action? fromString(String value) {
|
|
for (final action in Action.values) {
|
|
if (action.name == value) return action;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// A permission role assigned to a user.
|
|
///
|
|
/// Roles are parsed from the API format: `domain.category:action`
|
|
class Role {
|
|
const Role({
|
|
required this.id,
|
|
required this.name,
|
|
required this.domain,
|
|
required this.category,
|
|
required this.action,
|
|
});
|
|
|
|
/// Unique role ID.
|
|
final String id;
|
|
|
|
/// Full role name (e.g., "control-room.general:admin").
|
|
final String name;
|
|
|
|
/// Permission domain.
|
|
final Domain domain;
|
|
|
|
/// Permission category (usually "general").
|
|
final String category;
|
|
|
|
/// Permission action level.
|
|
final Action action;
|
|
|
|
/// Check if this role grants access for the given domain and action.
|
|
///
|
|
/// Global admin (`admin.general:admin`) grants access to everything.
|
|
/// Otherwise, domain and category must match, and action level must be sufficient.
|
|
bool grants(Domain domain, Action action, {String category = 'general'}) {
|
|
// Global admin override
|
|
if (this.domain == Domain.admin &&
|
|
this.category == 'general' &&
|
|
this.action == Action.admin) {
|
|
return true;
|
|
}
|
|
|
|
// Check domain and category match
|
|
if (this.domain != domain || this.category != category) {
|
|
return false;
|
|
}
|
|
|
|
// Check action hierarchy
|
|
return this.action.grants(action);
|
|
}
|
|
|
|
@override
|
|
String toString() => 'Role($name)';
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is Role && runtimeType == other.runtimeType && id == other.id;
|
|
|
|
@override
|
|
int get hashCode => id.hashCode;
|
|
}
|
|
|
|
/// Extension for checking permissions on a list of roles.
|
|
extension RoleListPermissions on List<Role> {
|
|
/// Check if any role grants the required permission.
|
|
bool hasPermission(Domain domain, Action action, {String category = 'general'}) {
|
|
return any((role) => role.grants(domain, action, category: category));
|
|
}
|
|
|
|
/// Check if any role grants any of the required permissions.
|
|
bool hasAnyPermission(List<(Domain, Action)> permissions) {
|
|
return permissions.any((p) => hasPermission(p.$1, p.$2));
|
|
}
|
|
|
|
/// Check if user is a global admin.
|
|
bool get isGlobalAdmin => hasPermission(Domain.admin, Action.admin);
|
|
}
|