From 6a25030d903372431b25f9494f4f3f2e1b32999d Mon Sep 17 00:00:00 2001 From: tikaiz Date: Fri, 6 Mar 2026 08:53:41 +0100 Subject: [PATCH] Add Auth service and Login Page --- frontend_splatournament_manager/lib/main.dart | 16 +- .../lib/pages/login_page.dart | 157 ++++++++++++++++++ .../lib/providers/auth_provider.dart | 74 +++++++++ .../lib/services/auth_service.dart | 37 +++++ 4 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 frontend_splatournament_manager/lib/pages/login_page.dart create mode 100644 frontend_splatournament_manager/lib/providers/auth_provider.dart create mode 100644 frontend_splatournament_manager/lib/services/auth_service.dart diff --git a/frontend_splatournament_manager/lib/main.dart b/frontend_splatournament_manager/lib/main.dart index 24eebc2..162844f 100644 --- a/frontend_splatournament_manager/lib/main.dart +++ b/frontend_splatournament_manager/lib/main.dart @@ -1,16 +1,22 @@ import 'package:flutter/material.dart'; import 'package:frontend_splatournament_manager/pages/home_page.dart'; +import 'package:frontend_splatournament_manager/pages/login_page.dart'; import 'package:frontend_splatournament_manager/pages/settings_page.dart'; +import 'package:frontend_splatournament_manager/providers/auth_provider.dart'; import 'package:frontend_splatournament_manager/state_provider.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; void main() { runApp( - ChangeNotifierProvider( - create: (_) => StateProvider(), - child: const SplatournamentApp(), - ),); + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => StateProvider()), + ChangeNotifierProvider(create: (_) => AuthProvider()), + ], + child: const SplatournamentApp(), + ), + ); } class SplatournamentApp extends StatelessWidget { @@ -34,7 +40,9 @@ class SplatournamentApp extends StatelessWidget { } } var routes = GoRouter( + initialLocation: '/login', routes: [ + GoRoute(path: "/login", builder: (context, state) => const LoginPage()), GoRoute(path: "/", builder: (context, state) => HomePage(),routes: [ GoRoute(path: "settings", builder: (context, state) => SettingsPage(),) ]) diff --git a/frontend_splatournament_manager/lib/pages/login_page.dart b/frontend_splatournament_manager/lib/pages/login_page.dart new file mode 100644 index 0000000..fbca0ab --- /dev/null +++ b/frontend_splatournament_manager/lib/pages/login_page.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:frontend_splatournament_manager/providers/auth_provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isRegistering = false; + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + + final authProvider = context.read(); + final username = _usernameController.text.trim(); + final password = _passwordController.text.trim(); + + bool success; + if (_isRegistering) { + success = await authProvider.register(username, password); + } else { + success = await authProvider.login(username, password); + } + + if (success && mounted) { + context.go('/'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + Text( + 'Splatournament Manager', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 32), + TextFormField( + controller: _usernameController, + decoration: const InputDecoration( + labelText: 'Username', + prefixIcon: Icon(Icons.person), + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a username'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Password', + prefixIcon: Icon(Icons.lock), + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a password'; + } + if (_isRegistering && value.trim().length < 4) { + return 'Password must be at least 6 characters'; + } + return null; + }, + onFieldSubmitted: (_) => _submit(), + ), + const SizedBox(height: 8), + Consumer( + builder: (context, auth, _) { + if (auth.error != null) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + auth.error!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + textAlign: TextAlign.center, + ), + ); + } + return const SizedBox.shrink(); + }, + ), + const SizedBox(height: 8), + Consumer( + builder: (context, auth, _) { + return FilledButton( + onPressed: auth.isLoading ? null : _submit, + child: auth.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(_isRegistering ? 'Register' : 'Login'), + ); + }, + ), + const SizedBox(height: 12), + TextButton( + onPressed: () { + setState(() { + _isRegistering = !_isRegistering; + }); + final authProvider = context.read(); + authProvider.clearError(); + }, + child: Text( + _isRegistering + ? 'Already have an account? Login' + : 'Don\'t have an account? Register', + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + diff --git a/frontend_splatournament_manager/lib/providers/auth_provider.dart b/frontend_splatournament_manager/lib/providers/auth_provider.dart new file mode 100644 index 0000000..60add19 --- /dev/null +++ b/frontend_splatournament_manager/lib/providers/auth_provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:frontend_splatournament_manager/services/auth_service.dart'; + +class AuthProvider extends ChangeNotifier { + final AuthService _authService = AuthService(); + + bool _isLoggedIn = false; + String? _username; + int? _userId; + String? _error; + bool _isLoading = false; + + bool get isLoggedIn => _isLoggedIn; + String? get username => _username; + int? get userId => _userId; + String? get error => _error; + bool get isLoading => _isLoading; + + Future login(String username, String password) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final user = await _authService.login(username, password); + _isLoggedIn = true; + _username = user['username']; + _userId = user['id']; + _isLoading = false; + notifyListeners(); + return true; + } catch (e) { + _error = e.toString().replaceFirst('Exception: ', ''); + _isLoading = false; + notifyListeners(); + return false; + } + } + + Future register(String username, String password) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final user = await _authService.register(username, password); + _isLoggedIn = true; + _username = user['username']; + _userId = user['id']; + _isLoading = false; + notifyListeners(); + return true; + } catch (e) { + _error = e.toString().replaceFirst('Exception: ', ''); + _isLoading = false; + notifyListeners(); + return false; + } + } + + void logout() { + _isLoggedIn = false; + _username = null; + _userId = null; + _error = null; + notifyListeners(); + } + + void clearError() { + _error = null; + notifyListeners(); + } +} + diff --git a/frontend_splatournament_manager/lib/services/auth_service.dart b/frontend_splatournament_manager/lib/services/auth_service.dart new file mode 100644 index 0000000..8671870 --- /dev/null +++ b/frontend_splatournament_manager/lib/services/auth_service.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class AuthService { + static const String baseUrl = "http://10.0.2.2:3000"; + + Future> register(String username, String password) async { + final response = await http.post( + Uri.parse('$baseUrl/register'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({'username': username, 'password': password}), + ); + + if (response.statusCode == 201) { + return json.decode(response.body); + } else { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Registration failed'); + } + } + + Future> login(String username, String password) async { + final response = await http.post( + Uri.parse('$baseUrl/login'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({'username': username, 'password': password}), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Login failed'); + } + } +} +