idle
This commit is contained in:
@@ -1,38 +1,243 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import '../session/session_bloc.dart';
|
||||
import '../session/session_event.dart';
|
||||
import 'auth_event.dart';
|
||||
import 'auth_state.dart';
|
||||
import '../../services/api_client.dart';
|
||||
|
||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
AuthBloc() : super(AuthInitial()) {
|
||||
final ApiClient apiClient;
|
||||
final SessionBloc sessionBloc;
|
||||
|
||||
AuthBloc({required this.apiClient, required this.sessionBloc})
|
||||
: super(AuthInitial()) {
|
||||
on<SignupStarted>(_onSignupStarted);
|
||||
on<RegistrationChallengeRequested>(_onRegistrationChallengeRequested);
|
||||
on<RegistrationResponseSubmitted>(_onRegistrationResponseSubmitted);
|
||||
on<LoginRequested>(_onLoginRequested);
|
||||
on<PasswordLoginRequested>(_onPasswordLoginRequested);
|
||||
on<AuthenticationChallengeRequested>(_onAuthenticationChallengeRequested);
|
||||
on<AuthenticationResponseSubmitted>(_onAuthenticationResponseSubmitted);
|
||||
on<LogoutRequested>(_onLogoutRequested);
|
||||
on<CheckAuthRequested>(_onCheckAuthRequested);
|
||||
}
|
||||
|
||||
void _onLoginRequested(LoginRequested event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
// Redirect to Go auth/login
|
||||
// For web, use window.location or url_launcher
|
||||
// Assume handled in UI
|
||||
emit(const AuthFailure('Redirect to login URL'));
|
||||
Future<void> _onSignupStarted(
|
||||
SignupStarted event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading(message: 'Creating account...'));
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/auth/signup',
|
||||
data: {
|
||||
'username': event.username,
|
||||
'email': event.email,
|
||||
'displayName': event.displayName,
|
||||
'password': event.password ?? '',
|
||||
},
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
final userId = response['userId'];
|
||||
emit(
|
||||
SignupInProgress(
|
||||
username: event.username,
|
||||
email: event.email,
|
||||
displayName: event.displayName,
|
||||
),
|
||||
);
|
||||
|
||||
// Trigger registration challenge request
|
||||
add(RegistrationChallengeRequested(userId: userId));
|
||||
} catch (e) {
|
||||
emit(AuthFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onLogoutRequested(
|
||||
Future<void> _onRegistrationChallengeRequested(
|
||||
RegistrationChallengeRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading(message: 'Getting registration challenge...'));
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/auth/registration-challenge',
|
||||
data: {'userId': event.userId},
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
emit(
|
||||
RegistrationChallengeReceived(
|
||||
userId: event.userId,
|
||||
challenge: response['challenge'],
|
||||
rpName: response['rp']['name'],
|
||||
rpId: response['rp']['id'],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(AuthFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRegistrationResponseSubmitted(
|
||||
RegistrationResponseSubmitted event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading(message: 'Verifying registration...'));
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/auth/registration-verify',
|
||||
data: {
|
||||
'userId': event.userId,
|
||||
'challenge': event.challenge,
|
||||
'credentialId': event.credentialId,
|
||||
'publicKey': event.publicKey,
|
||||
'clientDataJSON': event.clientDataJSON,
|
||||
'attestationObject': event.attestationObject,
|
||||
},
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
final token = response['token'];
|
||||
final user = response['user'];
|
||||
|
||||
sessionBloc.add(SessionStarted(token));
|
||||
|
||||
emit(
|
||||
AuthAuthenticated(
|
||||
token: token,
|
||||
userId: user['id'],
|
||||
username: user['username'],
|
||||
email: user['email'],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(AuthFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoginRequested(
|
||||
LoginRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading(message: 'Requesting authentication challenge...'));
|
||||
try {
|
||||
add(AuthenticationChallengeRequested(username: event.username));
|
||||
} catch (e) {
|
||||
emit(AuthFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAuthenticationChallengeRequested(
|
||||
AuthenticationChallengeRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/auth/authentication-challenge',
|
||||
data: {'username': event.username},
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
final credentialIds =
|
||||
(response['allowCredentials'] as List?)
|
||||
?.map((id) => id.toString())
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
emit(
|
||||
AuthenticationChallengeReceived(
|
||||
challenge: response['challenge'],
|
||||
credentialIds: credentialIds,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(AuthFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAuthenticationResponseSubmitted(
|
||||
AuthenticationResponseSubmitted event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading(message: 'Verifying authentication...'));
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/auth/authentication-verify',
|
||||
data: {
|
||||
'username': event.username,
|
||||
'challenge': event.challenge,
|
||||
'credentialId': event.credentialId,
|
||||
'authenticatorData': event.authenticatorData,
|
||||
'clientDataJSON': event.clientDataJSON,
|
||||
'signature': event.signature,
|
||||
},
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
final token = response['token'];
|
||||
final user = response['user'];
|
||||
|
||||
sessionBloc.add(SessionStarted(token));
|
||||
|
||||
emit(
|
||||
AuthAuthenticated(
|
||||
token: token,
|
||||
userId: user['id'],
|
||||
username: user['username'],
|
||||
email: user['email'],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(AuthFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLogoutRequested(
|
||||
LogoutRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading());
|
||||
// Call logout API
|
||||
emit(AuthLoading(message: 'Logging out...'));
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
sessionBloc.add(SessionExpired());
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
|
||||
void _onCheckAuthRequested(
|
||||
Future<void> _onPasswordLoginRequested(
|
||||
PasswordLoginRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading(message: 'Authenticating...'));
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/auth/password-login',
|
||||
data: {'username': event.username, 'password': event.password},
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
final token = response['token'];
|
||||
final user = response['user'];
|
||||
|
||||
sessionBloc.add(SessionStarted(token));
|
||||
emit(
|
||||
AuthAuthenticated(
|
||||
token: token,
|
||||
userId: user['id'],
|
||||
username: user['username'],
|
||||
email: user['email'],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(AuthFailure('Login failed: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCheckAuthRequested(
|
||||
CheckAuthRequested event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
// Check if token is valid
|
||||
// For now, assume unauthenticated
|
||||
// Check if token is valid in SessionBloc
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,135 @@ import 'package:equatable/equatable.dart';
|
||||
abstract class AuthEvent extends Equatable {
|
||||
const AuthEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
// Signup events
|
||||
class SignupStarted extends AuthEvent {
|
||||
final String username;
|
||||
final String email;
|
||||
final String displayName;
|
||||
final String? password;
|
||||
|
||||
const SignupStarted({
|
||||
required this.username,
|
||||
required this.email,
|
||||
required this.displayName,
|
||||
this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [username, email, displayName, password];
|
||||
}
|
||||
|
||||
class RegistrationChallengeRequested extends AuthEvent {
|
||||
final String userId;
|
||||
|
||||
const RegistrationChallengeRequested({required this.userId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [userId];
|
||||
}
|
||||
|
||||
class RegistrationResponseSubmitted extends AuthEvent {
|
||||
final String userId;
|
||||
final String challenge;
|
||||
final String credentialId;
|
||||
final String publicKey;
|
||||
final String clientDataJSON;
|
||||
final String attestationObject;
|
||||
|
||||
const RegistrationResponseSubmitted({
|
||||
required this.userId,
|
||||
required this.challenge,
|
||||
required this.credentialId,
|
||||
required this.publicKey,
|
||||
required this.clientDataJSON,
|
||||
required this.attestationObject,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
userId,
|
||||
challenge,
|
||||
credentialId,
|
||||
publicKey,
|
||||
clientDataJSON,
|
||||
attestationObject,
|
||||
];
|
||||
}
|
||||
|
||||
// Login events
|
||||
class LoginRequested extends AuthEvent {
|
||||
final String username;
|
||||
|
||||
const LoginRequested({required this.username});
|
||||
|
||||
@override
|
||||
List<Object> get props => [username];
|
||||
}
|
||||
|
||||
class AuthenticationChallengeRequested extends AuthEvent {
|
||||
final String username;
|
||||
|
||||
const AuthenticationChallengeRequested({required this.username});
|
||||
|
||||
@override
|
||||
List<Object> get props => [username];
|
||||
}
|
||||
|
||||
class AuthenticationResponseSubmitted extends AuthEvent {
|
||||
final String username;
|
||||
final String challenge;
|
||||
final String credentialId;
|
||||
final String authenticatorData;
|
||||
final String clientDataJSON;
|
||||
final String signature;
|
||||
|
||||
const AuthenticationResponseSubmitted({
|
||||
required this.username,
|
||||
required this.challenge,
|
||||
required this.credentialId,
|
||||
required this.authenticatorData,
|
||||
required this.clientDataJSON,
|
||||
required this.signature,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
username,
|
||||
challenge,
|
||||
credentialId,
|
||||
authenticatorData,
|
||||
clientDataJSON,
|
||||
signature,
|
||||
];
|
||||
}
|
||||
|
||||
class LogoutRequested extends AuthEvent {
|
||||
const LogoutRequested();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoginRequested extends AuthEvent {}
|
||||
class CheckAuthRequested extends AuthEvent {
|
||||
const CheckAuthRequested();
|
||||
|
||||
class LogoutRequested extends AuthEvent {}
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CheckAuthRequested extends AuthEvent {}
|
||||
class PasswordLoginRequested extends AuthEvent {
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
const PasswordLoginRequested({
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [username, password];
|
||||
}
|
||||
|
||||
@@ -4,25 +4,82 @@ abstract class AuthState extends Equatable {
|
||||
const AuthState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
class AuthLoading extends AuthState {
|
||||
final String? message;
|
||||
|
||||
const AuthLoading({this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class SignupInProgress extends AuthState {
|
||||
final String username;
|
||||
final String email;
|
||||
final String displayName;
|
||||
|
||||
const SignupInProgress({
|
||||
required this.username,
|
||||
required this.email,
|
||||
required this.displayName,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [username, email, displayName];
|
||||
}
|
||||
|
||||
class RegistrationChallengeReceived extends AuthState {
|
||||
final String userId;
|
||||
final String challenge;
|
||||
final String rpName;
|
||||
final String rpId;
|
||||
|
||||
const RegistrationChallengeReceived({
|
||||
required this.userId,
|
||||
required this.challenge,
|
||||
required this.rpName,
|
||||
required this.rpId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [userId, challenge, rpName, rpId];
|
||||
}
|
||||
|
||||
class AuthenticationChallengeReceived extends AuthState {
|
||||
final String challenge;
|
||||
final List<String> credentialIds;
|
||||
|
||||
const AuthenticationChallengeReceived({
|
||||
required this.challenge,
|
||||
required this.credentialIds,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [challenge, credentialIds];
|
||||
}
|
||||
|
||||
class AuthAuthenticated extends AuthState {
|
||||
final String token;
|
||||
final String userId;
|
||||
final String username;
|
||||
final String email;
|
||||
|
||||
const AuthAuthenticated({required this.token, required this.userId});
|
||||
const AuthAuthenticated({
|
||||
required this.token,
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.email,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [token, userId];
|
||||
List<Object> get props => [token, userId, username, email];
|
||||
}
|
||||
|
||||
class AuthUnauthenticated extends AuthState {}
|
||||
|
||||
class AuthFailure extends AuthState {
|
||||
final String error;
|
||||
|
||||
@@ -31,3 +88,10 @@ class AuthFailure extends AuthState {
|
||||
@override
|
||||
List<Object> get props => [error];
|
||||
}
|
||||
|
||||
class AuthUnauthenticated extends AuthState {
|
||||
const AuthUnauthenticated();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
@@ -3,18 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'blocs/auth/auth_bloc.dart';
|
||||
import 'blocs/session/session_bloc.dart';
|
||||
import 'blocs/organization/organization_bloc.dart';
|
||||
import 'blocs/permission/permission_bloc.dart';
|
||||
import 'blocs/file_browser/file_browser_bloc.dart';
|
||||
import 'blocs/upload/upload_bloc.dart';
|
||||
import 'blocs/activity/activity_bloc.dart';
|
||||
import 'repositories/mock_file_repository.dart';
|
||||
import 'services/file_service.dart';
|
||||
import 'services/api_client.dart';
|
||||
import 'services/org_api.dart';
|
||||
import 'services/activity_api.dart';
|
||||
import 'pages/home_page.dart';
|
||||
import 'pages/login_form.dart';
|
||||
import 'pages/signup_form.dart';
|
||||
import 'pages/file_explorer.dart';
|
||||
import 'pages/document_viewer.dart';
|
||||
import 'pages/editor_page.dart';
|
||||
@@ -24,6 +18,7 @@ final GoRouter _router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(path: '/', builder: (context, state) => const HomePage()),
|
||||
GoRoute(path: '/login', builder: (context, state) => const LoginForm()),
|
||||
GoRoute(path: '/signup', builder: (context, state) => const SignupForm()),
|
||||
|
||||
GoRoute(
|
||||
path: '/viewer/:orgId/:fileId',
|
||||
@@ -58,25 +53,11 @@ class MainApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
|
||||
BlocProvider<SessionBloc>(create: (_) => SessionBloc()),
|
||||
BlocProvider<PermissionBloc>(create: (_) => PermissionBloc()),
|
||||
BlocProvider<FileBrowserBloc>(
|
||||
create: (context) => FileBrowserBloc(
|
||||
FileService(ApiClient(context.read<SessionBloc>())),
|
||||
),
|
||||
),
|
||||
BlocProvider<UploadBloc>(
|
||||
create: (_) =>
|
||||
UploadBloc(MockFileRepository()), // keep mock for upload
|
||||
),
|
||||
BlocProvider<OrganizationBloc>(
|
||||
lazy: true,
|
||||
create: (context) => OrganizationBloc(
|
||||
context.read<PermissionBloc>(),
|
||||
context.read<FileBrowserBloc>(),
|
||||
context.read<UploadBloc>(),
|
||||
OrgApi(ApiClient(context.read<SessionBloc>())),
|
||||
BlocProvider<AuthBloc>(
|
||||
create: (context) => AuthBloc(
|
||||
apiClient: ApiClient(context.read<SessionBloc>()),
|
||||
sessionBloc: context.read<SessionBloc>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<ActivityBloc>(
|
||||
|
||||
63
b0esche_cloud/lib/models/credential.dart
Normal file
63
b0esche_cloud/lib/models/credential.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Credential extends Equatable {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String credentialId;
|
||||
final String publicKey;
|
||||
final int signCount;
|
||||
final DateTime createdAt;
|
||||
final DateTime? lastUsedAt;
|
||||
final List<String>? transports;
|
||||
|
||||
const Credential({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.credentialId,
|
||||
required this.publicKey,
|
||||
this.signCount = 0,
|
||||
required this.createdAt,
|
||||
this.lastUsedAt,
|
||||
this.transports,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
credentialId,
|
||||
publicKey,
|
||||
signCount,
|
||||
createdAt,
|
||||
lastUsedAt,
|
||||
transports,
|
||||
];
|
||||
|
||||
factory Credential.fromJson(Map<String, dynamic> json) {
|
||||
return Credential(
|
||||
id: json['id'] as String,
|
||||
userId: json['userId'] as String,
|
||||
credentialId: json['credentialId'] as String,
|
||||
publicKey: json['publicKey'] as String,
|
||||
signCount: json['signCount'] as int? ?? 0,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
lastUsedAt: json['lastUsedAt'] != null
|
||||
? DateTime.parse(json['lastUsedAt'] as String)
|
||||
: null,
|
||||
transports: (json['transports'] as List<dynamic>?)?.cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'userId': userId,
|
||||
'credentialId': credentialId,
|
||||
'publicKey': publicKey,
|
||||
'signCount': signCount,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'lastUsedAt': lastUsedAt?.toIso8601String(),
|
||||
'transports': transports,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,73 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class User extends Equatable {
|
||||
final String id;
|
||||
final String username;
|
||||
final String email;
|
||||
final String? name;
|
||||
final String? displayName;
|
||||
final DateTime createdAt;
|
||||
final DateTime? lastLoginAt;
|
||||
|
||||
const User({required this.email, this.name});
|
||||
const User({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.email,
|
||||
this.displayName,
|
||||
required this.createdAt,
|
||||
this.lastLoginAt,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [email, name];
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
displayName,
|
||||
createdAt,
|
||||
lastLoginAt,
|
||||
];
|
||||
|
||||
User copyWith({String? email, String? name}) {
|
||||
return User(email: email ?? this.email, name: name ?? this.name);
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? username,
|
||||
String? email,
|
||||
String? displayName,
|
||||
DateTime? createdAt,
|
||||
DateTime? lastLoginAt,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
username: username ?? this.username,
|
||||
email: email ?? this.email,
|
||||
displayName: displayName ?? this.displayName,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||
);
|
||||
}
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String,
|
||||
email: json['email'] as String,
|
||||
displayName: json['displayName'] as String?,
|
||||
createdAt: DateTime.parse(
|
||||
json['createdAt'] as String? ?? DateTime.now().toIso8601String(),
|
||||
),
|
||||
lastLoginAt: json['lastLoginAt'] != null
|
||||
? DateTime.parse(json['lastLoginAt'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'username': username,
|
||||
'email': email,
|
||||
'displayName': displayName,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'lastLoginAt': lastLoginAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import '../blocs/file_browser/file_browser_bloc.dart';
|
||||
import '../blocs/file_browser/file_browser_event.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../theme/modern_glass_button.dart';
|
||||
import 'login_form.dart';
|
||||
import 'login_form.dart' show LoginForm;
|
||||
import 'file_explorer.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:math';
|
||||
import '../blocs/auth/auth_bloc.dart';
|
||||
import '../blocs/auth/auth_event.dart';
|
||||
import '../blocs/auth/auth_state.dart';
|
||||
@@ -16,8 +18,55 @@ class LoginForm extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LoginFormState extends State<LoginForm> {
|
||||
final _emailController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _usePasskey = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _generateRandomHex(int bytes) {
|
||||
final random = Random();
|
||||
final values = List<int>.generate(bytes, (i) => random.nextInt(256));
|
||||
return values.map((v) => v.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Future<void> _handleAuthentication(
|
||||
BuildContext context,
|
||||
AuthenticationChallengeReceived state,
|
||||
) async {
|
||||
try {
|
||||
// Simulate WebAuthn authentication by generating fake assertion data
|
||||
// In a real implementation, this would call native WebAuthn APIs
|
||||
final credentialId = state.credentialIds.isNotEmpty
|
||||
? state.credentialIds.first
|
||||
: _generateRandomHex(64);
|
||||
|
||||
if (context.mounted) {
|
||||
context.read<AuthBloc>().add(
|
||||
AuthenticationResponseSubmitted(
|
||||
username: _usernameController.text,
|
||||
challenge: state.challenge,
|
||||
credentialId: credentialId,
|
||||
authenticatorData: _generateRandomHex(37),
|
||||
clientDataJSON:
|
||||
'{"type":"webauthn.get","challenge":"${state.challenge}","origin":"https://b0esche.cloud"}',
|
||||
signature: _generateRandomHex(128),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Authentication failed: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -27,62 +76,69 @@ class _LoginFormState extends State<LoginForm> {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.error)));
|
||||
} else if (state is AuthenticationChallengeReceived) {
|
||||
_handleAuthentication(context, state);
|
||||
} else if (state is AuthAuthenticated) {
|
||||
// Start session
|
||||
context.read<SessionBloc>().add(SessionStarted(state.token));
|
||||
context.go('/');
|
||||
}
|
||||
},
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'sign in',
|
||||
style: TextStyle(fontSize: 24, color: AppTheme.primaryText),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.3),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'sign in',
|
||||
style: TextStyle(fontSize: 24, color: AppTheme.primaryText),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _usernameController,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.text,
|
||||
cursorColor: AppTheme.accentColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'username',
|
||||
hintStyle: TextStyle(color: AppTheme.secondaryText),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.person_outline,
|
||||
color: AppTheme.primaryText,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.primaryText),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _emailController,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
cursorColor: AppTheme.accentColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'email',
|
||||
hintStyle: TextStyle(color: AppTheme.secondaryText),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
color: AppTheme.primaryText,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (!_usePasskey)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.3),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.primaryText),
|
||||
),
|
||||
Divider(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.2),
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
TextField(
|
||||
child: TextField(
|
||||
controller: _passwordController,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
obscuringCharacter: '✱',
|
||||
cursorColor: AppTheme.accentColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'password',
|
||||
@@ -90,37 +146,96 @@ class _LoginFormState extends State<LoginForm> {
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
Icons.lock_outline,
|
||||
color: AppTheme.primaryText,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.primaryText),
|
||||
),
|
||||
),
|
||||
if (!_usePasskey) const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
return ModernGlassButton(
|
||||
isLoading: state is AuthLoading,
|
||||
onPressed: () {
|
||||
if (_usernameController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Username is required'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_usePasskey && _passwordController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Password is required'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_usePasskey) {
|
||||
context.read<AuthBloc>().add(
|
||||
LoginRequested(username: _usernameController.text),
|
||||
);
|
||||
} else {
|
||||
context.read<AuthBloc>().add(
|
||||
PasswordLoginRequested(
|
||||
username: _usernameController.text,
|
||||
password: _passwordController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('sign in'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _usePasskey = !_usePasskey),
|
||||
child: Text(
|
||||
_usePasskey ? 'use password' : 'use passkey',
|
||||
style: TextStyle(
|
||||
color: AppTheme.accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
return ModernGlassButton(
|
||||
isLoading: state is AuthLoading,
|
||||
onPressed: () {
|
||||
context.read<AuthBloc>().add(
|
||||
LoginRequested(
|
||||
// _emailController.text,
|
||||
// _passwordController.text,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('sign in'),
|
||||
);
|
||||
},
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'don\'t have an account?',
|
||||
style: TextStyle(color: AppTheme.secondaryText),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () => context.go('/signup'),
|
||||
child: Text(
|
||||
'create one',
|
||||
style: TextStyle(
|
||||
color: AppTheme.accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
236
b0esche_cloud/lib/pages/signup_form.dart
Normal file
236
b0esche_cloud/lib/pages/signup_form.dart
Normal file
@@ -0,0 +1,236 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:math';
|
||||
import '../blocs/auth/auth_bloc.dart';
|
||||
import '../blocs/auth/auth_event.dart';
|
||||
import '../blocs/auth/auth_state.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../theme/modern_glass_button.dart';
|
||||
|
||||
class SignupForm extends StatefulWidget {
|
||||
const SignupForm({super.key});
|
||||
|
||||
@override
|
||||
State<SignupForm> createState() => _SignupFormState();
|
||||
}
|
||||
|
||||
class _SignupFormState extends State<SignupForm> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _displayNameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_displayNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _generateRandomHex(int bytes) {
|
||||
final random = Random();
|
||||
final values = List<int>.generate(bytes, (i) => random.nextInt(256));
|
||||
return values.map((v) => v.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Future<void> _handleRegistration(
|
||||
BuildContext context,
|
||||
RegistrationChallengeReceived state,
|
||||
) async {
|
||||
try {
|
||||
// Simulate WebAuthn registration by generating fake credential data
|
||||
// In a real implementation, this would call native WebAuthn APIs
|
||||
final credentialId = _generateRandomHex(64);
|
||||
final publicKey = _generateRandomHex(91); // EC2 public key size
|
||||
|
||||
if (context.mounted) {
|
||||
context.read<AuthBloc>().add(
|
||||
RegistrationResponseSubmitted(
|
||||
userId: state.userId,
|
||||
challenge: state.challenge,
|
||||
credentialId: credentialId,
|
||||
publicKey: publicKey,
|
||||
clientDataJSON:
|
||||
'{"type":"webauthn.create","challenge":"${state.challenge}","origin":"https://b0esche.cloud"}',
|
||||
attestationObject: _generateRandomHex(128),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Registration failed: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is AuthFailure) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.error)));
|
||||
} else if (state is RegistrationChallengeReceived) {
|
||||
_handleRegistration(context, state);
|
||||
} else if (state is AuthAuthenticated) {
|
||||
context.go('/');
|
||||
}
|
||||
},
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'create account',
|
||||
style: TextStyle(fontSize: 24, color: AppTheme.primaryText),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryBackground.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.text,
|
||||
cursorColor: AppTheme.accentColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'username',
|
||||
hintStyle: TextStyle(color: AppTheme.secondaryText),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.person_outline,
|
||||
color: AppTheme.primaryText,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.primaryText),
|
||||
),
|
||||
Divider(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.2),
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
cursorColor: AppTheme.accentColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'password',
|
||||
hintStyle: TextStyle(color: AppTheme.secondaryText),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.lock_outline,
|
||||
color: AppTheme.primaryText,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.primaryText),
|
||||
),
|
||||
Divider(
|
||||
color: AppTheme.accentColor.withValues(alpha: 0.2),
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
TextField(
|
||||
controller: _displayNameController,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.text,
|
||||
cursorColor: AppTheme.accentColor,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'display name (optional)',
|
||||
hintStyle: TextStyle(color: AppTheme.secondaryText),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.badge_outlined,
|
||||
color: AppTheme.primaryText,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.primaryText),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
return ModernGlassButton(
|
||||
isLoading: state is AuthLoading,
|
||||
onPressed: () {
|
||||
if (_usernameController.text.isEmpty ||
|
||||
_passwordController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Username and password are required',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.read<AuthBloc>().add(
|
||||
SignupStarted(
|
||||
username: _usernameController.text,
|
||||
email: _usernameController
|
||||
.text, // Use username as email for now
|
||||
displayName: _displayNameController.text,
|
||||
password: _passwordController.text,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('create'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'already have an account?',
|
||||
style: TextStyle(color: AppTheme.secondaryText),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () => context.go('/login'),
|
||||
child: Text(
|
||||
'sign in',
|
||||
style: TextStyle(
|
||||
color: AppTheme.accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,12 @@ class MockAuthRepository implements AuthRepository {
|
||||
Future<User> login(String email, String password) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (email.isNotEmpty && password.isNotEmpty) {
|
||||
return User(email: email);
|
||||
return User(
|
||||
id: 'mock-user-id',
|
||||
username: 'mockuser',
|
||||
email: email,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
} else {
|
||||
throw Exception('Invalid credentials');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import '../models/file_item.dart';
|
||||
import '../models/viewer_session.dart';
|
||||
import '../models/editor_session.dart';
|
||||
import '../models/annotation.dart';
|
||||
import '../repositories/file_repository.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class FileService {
|
||||
|
||||
@@ -37,8 +37,7 @@ dependencies:
|
||||
logger: ^2.0.2
|
||||
|
||||
# Image Handling
|
||||
cached_network_image:
|
||||
^3.3.0
|
||||
cached_network_image: ^3.3.0
|
||||
|
||||
# SVG Support
|
||||
flutter_svg: ^2.0.9
|
||||
|
||||
Reference in New Issue
Block a user