From 2ab0786e30d3126f7d6cbe5ee73ae19d24697c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Fri, 9 Jan 2026 19:36:43 +0100 Subject: [PATCH] fixed login form height --- b0esche_cloud/lib/pages/home_page.dart | 12 +- b0esche_cloud/lib/pages/login_form.dart | 436 ++++++++++++----------- b0esche_cloud/lib/pages/signup_form.dart | 13 +- 3 files changed, 249 insertions(+), 212 deletions(-) diff --git a/b0esche_cloud/lib/pages/home_page.dart b/b0esche_cloud/lib/pages/home_page.dart index c8755f4..bec06f5 100644 --- a/b0esche_cloud/lib/pages/home_page.dart +++ b/b0esche_cloud/lib/pages/home_page.dart @@ -29,6 +29,7 @@ class _HomePageState extends State with TickerProviderStateMixin { late String _selectedTab = 'Drive'; late AnimationController _animationController; bool _isSignupMode = false; + bool _usePasswordMode = false; @override void initState() { @@ -55,6 +56,10 @@ class _HomePageState extends State with TickerProviderStateMixin { } } + void _setPasswordMode(bool usePassword) { + setState(() => _usePasswordMode = usePassword); + } + void _showCreateOrgDialog(BuildContext context) { final controller = TextEditingController(); showDialog( @@ -289,7 +294,9 @@ class _HomePageState extends State with TickerProviderStateMixin { : 340, height: isLoggedIn ? MediaQuery.of(context).size.height * 0.9 - : (_isSignupMode ? 400 : 280), + : (_isSignupMode + ? 400 + : (_usePasswordMode ? 350 : 280)), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: BackdropFilter( @@ -336,7 +343,7 @@ class _HomePageState extends State with TickerProviderStateMixin { } return Column( children: [ - const SizedBox(height: 8), + const SizedBox(height: 80), _buildOrgRow(context), Expanded( child: _buildDrive( @@ -351,6 +358,7 @@ class _HomePageState extends State with TickerProviderStateMixin { ) : LoginForm( onSignupModeChanged: _setSignupMode, + onPasswordModeChanged: _setPasswordMode, ), ), // Top-left radial glow - primary accent light diff --git a/b0esche_cloud/lib/pages/login_form.dart b/b0esche_cloud/lib/pages/login_form.dart index 7995477..58a2bc5 100644 --- a/b0esche_cloud/lib/pages/login_form.dart +++ b/b0esche_cloud/lib/pages/login_form.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'dart:math'; +import 'dart:convert'; import '../blocs/auth/auth_bloc.dart'; import '../blocs/auth/auth_event.dart'; import '../blocs/auth/auth_state.dart'; @@ -12,8 +13,13 @@ import '../theme/modern_glass_button.dart'; class LoginForm extends StatefulWidget { final ValueChanged? onSignupModeChanged; + final ValueChanged? onPasswordModeChanged; - const LoginForm({super.key, this.onSignupModeChanged}); + const LoginForm({ + super.key, + this.onSignupModeChanged, + this.onPasswordModeChanged, + }); @override State createState() => _LoginFormState(); @@ -40,6 +46,12 @@ class _LoginFormState extends State { return values.map((v) => v.toRadixString(16).padLeft(2, '0')).join(); } + String _generateRandomBase64(int bytes) { + final random = Random(); + final values = List.generate(bytes, (i) => random.nextInt(256)); + return base64.encode(values); + } + Future _handleAuthentication( BuildContext context, AuthenticationChallengeReceived state, @@ -47,7 +59,7 @@ class _LoginFormState extends State { try { final credentialId = state.credentialIds.isNotEmpty ? state.credentialIds.first - : _generateRandomHex(64); + : _generateRandomBase64(64); if (context.mounted) { context.read().add( @@ -55,10 +67,10 @@ class _LoginFormState extends State { username: _usernameController.text, challenge: state.challenge, credentialId: credentialId, - authenticatorData: _generateRandomHex(37), + authenticatorData: _generateRandomBase64(37), clientDataJSON: '{"type":"webauthn.get","challenge":"${state.challenge}","origin":"https://b0esche.cloud"}', - signature: _generateRandomHex(128), + signature: _generateRandomBase64(128), ), ); } @@ -76,8 +88,8 @@ class _LoginFormState extends State { RegistrationChallengeReceived state, ) async { try { - final credentialId = _generateRandomHex(64); - final publicKey = _generateRandomHex(91); + final credentialId = _generateRandomBase64(64); + final publicKey = _generateRandomBase64(91); if (context.mounted) { context.read().add( @@ -88,7 +100,7 @@ class _LoginFormState extends State { publicKey: publicKey, clientDataJSON: '{"type":"webauthn.create","challenge":"${state.challenge}","origin":"https://b0esche.cloud"}', - attestationObject: _generateRandomHex(128), + attestationObject: _generateRandomBase64(128), ), ); } @@ -133,56 +145,28 @@ class _LoginFormState extends State { child: Center( child: Padding( padding: const EdgeInsets.all(16.0), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (child, animation) { - return FadeTransition(opacity: animation, child: child); - }, - child: SingleChildScrollView( - key: ValueKey(_isSignup), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - _isSignup ? 'create account' : 'sign in', - style: const 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: AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: SingleChildScrollView( + key: ValueKey('${_isSignup}_$_usePasskey'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + _isSignup ? 'create account' : 'sign in', + style: const TextStyle( + fontSize: 24, + color: AppTheme.primaryText, ), ), - child: 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), - ), - ), - const SizedBox(height: 16), - if (!_isSignup && _usePasskey) - const SizedBox.shrink() - else + const SizedBox(height: 24), Container( decoration: BoxDecoration( color: AppTheme.primaryBackground.withValues( @@ -194,105 +178,114 @@ class _LoginFormState extends State { ), ), child: TextField( - controller: _passwordController, + controller: _usernameController, 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), - ), - ), - if (!_isSignup && _usePasskey) - const SizedBox.shrink() - else - const SizedBox(height: 16), - if (_isSignup) - 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: _displayNameController, - textInputAction: TextInputAction.done, keyboardType: TextInputType.text, cursorColor: AppTheme.accentColor, decoration: InputDecoration( - hintText: 'display name (optional)', + hintText: 'username', hintStyle: TextStyle(color: AppTheme.secondaryText), contentPadding: const EdgeInsets.all(12), border: InputBorder.none, prefixIcon: Icon( - Icons.badge_outlined, + Icons.person_outline, color: AppTheme.primaryText, size: 20, ), ), style: const TextStyle(color: AppTheme.primaryText), ), - ) - else - const SizedBox.shrink(), - if (_isSignup) - const SizedBox(height: 16) - else - const SizedBox.shrink(), - SizedBox( - width: 150, - child: BlocBuilder( - 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 (_isSignup) { - if (_passwordController.text.isEmpty) { + ), + const SizedBox(height: 16), + if (!_isSignup && _usePasskey) + const SizedBox.shrink() + else + 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: _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), + ), + ), + if (!_isSignup && _usePasskey) + const SizedBox.shrink() + else + const SizedBox(height: 16), + if (_isSignup) + 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: _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), + ), + ) + else + const SizedBox.shrink(), + if (_isSignup) + const SizedBox(height: 16) + else + const SizedBox.shrink(), + SizedBox( + width: 150, + child: BlocBuilder( + builder: (context, state) { + return ModernGlassButton( + isLoading: state is AuthLoading, + onPressed: () { + if (_usernameController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Password is required'), + content: Text('Username is required'), ), ); return; } - context.read().add( - SignupStarted( - username: _usernameController.text, - email: _usernameController.text, - displayName: _displayNameController.text, - password: _passwordController.text, - ), - ); - } else { - if (_usePasskey) { - context.read().add( - LoginRequested( - username: _usernameController.text, - ), - ); - } else { + if (_isSignup) { if (_passwordController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -302,91 +295,120 @@ class _LoginFormState extends State { return; } context.read().add( - PasswordLoginRequested( + SignupStarted( username: _usernameController.text, + email: _usernameController.text, + displayName: _displayNameController.text, password: _passwordController.text, ), ); + } else { + if (_usePasskey) { + context.read().add( + LoginRequested( + username: _usernameController.text, + ), + ); + } else { + if (_passwordController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password is required'), + ), + ); + return; + } + context.read().add( + PasswordLoginRequested( + username: _usernameController.text, + password: _passwordController.text, + ), + ); + } } - } - }, - child: Text(_isSignup ? 'create' : 'sign in'), - ); - }, + }, + child: Text(_isSignup ? 'create' : 'sign in'), + ); + }, + ), ), - ), - const SizedBox(height: 16), - if (_isSignup) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'already have an account?', - style: TextStyle(color: AppTheme.secondaryText), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - _resetForm(); - _setSignupMode(false); - }, - child: Text( - 'sign in', - style: TextStyle( - color: AppTheme.accentColor, - decoration: TextDecoration.underline, + const SizedBox(height: 16), + if (_isSignup) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'already have an account?', + style: TextStyle(color: AppTheme.secondaryText), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + _resetForm(); + _setSignupMode(false); + }, + child: Text( + 'sign in', + style: TextStyle( + color: AppTheme.accentColor, + decoration: TextDecoration.underline, + ), ), ), - ), - ], - ) - else - Column( - children: [ - 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, + ], + ) + else + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + setState(() => _usePasskey = !_usePasskey); + widget.onPasswordModeChanged?.call( + !_usePasskey, + ); + }, + child: Text( + _usePasskey ? 'use password' : 'use passkey', + style: TextStyle( + color: AppTheme.accentColor, + decoration: TextDecoration.underline, + fontSize: 12, + ), ), ), - ), - ], - ), - 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: () { - _resetForm(); - _setSignupMode(true); - }, - child: Text( - 'create one', - style: TextStyle( - color: AppTheme.accentColor, - decoration: TextDecoration.underline, + ], + ), + 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: () { + _resetForm(); + _setSignupMode(true); + }, + child: Text( + 'create one', + style: TextStyle( + color: AppTheme.accentColor, + decoration: TextDecoration.underline, + ), ), ), - ), - ], - ), - ], - ), - ], + ], + ), + ], + ), + ], + ), ), ), ), diff --git a/b0esche_cloud/lib/pages/signup_form.dart b/b0esche_cloud/lib/pages/signup_form.dart index c74619d..e98a26c 100644 --- a/b0esche_cloud/lib/pages/signup_form.dart +++ b/b0esche_cloud/lib/pages/signup_form.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'dart:math'; +import 'dart:convert'; import '../blocs/auth/auth_bloc.dart'; import '../blocs/auth/auth_event.dart'; import '../blocs/auth/auth_state.dart'; @@ -34,6 +35,12 @@ class _SignupFormState extends State { return values.map((v) => v.toRadixString(16).padLeft(2, '0')).join(); } + String _generateRandomBase64(int bytes) { + final random = Random(); + final values = List.generate(bytes, (i) => random.nextInt(256)); + return base64.encode(values); + } + Future _handleRegistration( BuildContext context, RegistrationChallengeReceived state, @@ -41,8 +48,8 @@ class _SignupFormState extends State { 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 + final credentialId = _generateRandomBase64(64); + final publicKey = _generateRandomBase64(91); // EC2 public key size if (context.mounted) { context.read().add( @@ -53,7 +60,7 @@ class _SignupFormState extends State { publicKey: publicKey, clientDataJSON: '{"type":"webauthn.create","challenge":"${state.challenge}","origin":"https://b0esche.cloud"}', - attestationObject: _generateRandomHex(128), + attestationObject: _generateRandomBase64(128), ), ); }