From 0b8d31e72782c474a52462e9955dc7880dff92cf Mon Sep 17 00:00:00 2001 From: tikaiz Date: Tue, 10 Mar 2026 21:00:43 +0100 Subject: [PATCH] Added jwt authenticaation --- backend_splatournament_manager/package.json | 2 + backend_splatournament_manager/src/app.ts | 17 ++++--- .../src/middlewares/auth-middleware.ts | 21 ++++++++ .../src/services/user-service.ts | 15 +++--- .../lib/providers/auth_provider.dart | 34 ++++++++++++- .../lib/providers/tournament_provider.dart | 25 +++------ .../lib/services/api_client.dart | 45 ++++++++++++++++ .../lib/services/team_service.dart | 51 ++++++++----------- frontend_splatournament_manager/pubspec.yaml | 2 + 9 files changed, 148 insertions(+), 64 deletions(-) create mode 100644 backend_splatournament_manager/src/middlewares/auth-middleware.ts create mode 100644 frontend_splatournament_manager/lib/services/api_client.dart diff --git a/backend_splatournament_manager/package.json b/backend_splatournament_manager/package.json index e5efb1a..1128205 100644 --- a/backend_splatournament_manager/package.json +++ b/backend_splatournament_manager/package.json @@ -13,10 +13,12 @@ "dotenv": "^17.2.3", "ejs": "^3.1.10", "express": "^5.1.0", + "jsonwebtoken": "^9.0.3", "sqlite3": "^5.1.7" }, "devDependencies": { "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.6.1", "@types/sqlite3": "^3.1.11", "nodemon": "^3.1.10", diff --git a/backend_splatournament_manager/src/app.ts b/backend_splatournament_manager/src/app.ts index 1172edf..faffb23 100644 --- a/backend_splatournament_manager/src/app.ts +++ b/backend_splatournament_manager/src/app.ts @@ -5,6 +5,7 @@ import 'dotenv/config'; import {TournamentService} from './services/tournament-service'; import {UserService} from './services/user-service'; import {TeamService} from './services/team-service'; +import {authMiddleware} from './middlewares/auth-middleware'; import router from './middlewares/logger'; import {Database} from 'sqlite3'; import fs from "fs"; @@ -38,7 +39,7 @@ app.get('/tournaments/:id', async (req: Request, res: Response) => { res.send(tournament); }); -app.post('/tournaments', async (req: Request, res: Response) => { +app.post('/tournaments', authMiddleware, async (req: Request, res: Response) => { try { await tournamentService.addTournament(req.body); res.status(201).send(); @@ -48,7 +49,7 @@ app.post('/tournaments', async (req: Request, res: Response) => { } }); -app.put('/tournaments/:id', async (req: Request, res: Response) => { +app.put('/tournaments/:id', authMiddleware, async (req: Request, res: Response) => { try { await tournamentService.updateTournament(+req.params.id, req.body); } catch (err) { @@ -57,7 +58,7 @@ app.put('/tournaments/:id', async (req: Request, res: Response) => { res.status(200).send({message: 'Tournament updated successfully'}); }); -app.delete('/tournaments/:id', async (req: Request, res: Response) => { +app.delete('/tournaments/:id', authMiddleware, async (req: Request, res: Response) => { try { await tournamentService.deleteTournament(+req.params.id); } catch (err) { @@ -79,7 +80,7 @@ app.get('/teams/:id', async (req: Request, res: Response) => { res.send(team); }); -app.post('/teams', async (req: Request, res: Response) => { +app.post('/teams', authMiddleware, async (req: Request, res: Response) => { const {name, tag, description} = req.body; if (!name || !tag) { return res.status(400).send({error: 'name and tag are required'}); @@ -93,7 +94,7 @@ app.post('/teams', async (req: Request, res: Response) => { } }); -app.put('/teams/:id', async (req: Request, res: Response) => { +app.put('/teams/:id', authMiddleware, async (req: Request, res: Response) => { try { await teamService.updateTeam(+req.params.id, req.body); } catch (err) { @@ -102,7 +103,7 @@ app.put('/teams/:id', async (req: Request, res: Response) => { res.status(200).send({message: 'Team updated successfully'}); }); -app.delete('/teams/:id', async (req: Request, res: Response) => { +app.delete('/teams/:id', authMiddleware, async (req: Request, res: Response) => { try { await teamService.deleteTeam(+req.params.id); } catch (err) { @@ -116,7 +117,7 @@ app.get('/tournaments/:id/teams', async (req: Request, res: Response) => { res.send(teams); }); -app.post('/tournaments/:id/teams', async (req: Request, res: Response) => { +app.post('/tournaments/:id/teams', authMiddleware, async (req: Request, res: Response) => { const {teamId} = req.body; if (!teamId) { return res.status(400).send({error: 'teamId is required'}); @@ -132,7 +133,7 @@ app.post('/tournaments/:id/teams', async (req: Request, res: Response) => { } }); -app.delete('/tournaments/:id/teams/:teamId', async (req: Request, res: Response) => { +app.delete('/tournaments/:id/teams/:teamId', authMiddleware, async (req: Request, res: Response) => { try { await teamService.removeTeamFromTournament(+req.params.id, +req.params.teamId); } catch (err) { diff --git a/backend_splatournament_manager/src/middlewares/auth-middleware.ts b/backend_splatournament_manager/src/middlewares/auth-middleware.ts new file mode 100644 index 0000000..8c56058 --- /dev/null +++ b/backend_splatournament_manager/src/middlewares/auth-middleware.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from 'express'; +import * as jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || "key" + +export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).send({ error: 'Unauthorized: No token provided' }); + } + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.verify(token, JWT_SECRET); + // @ts-ignore + req.user = decoded; + + next(); + } catch (err) { + return res.status(401).send({ error: 'Unauthorized: Invalid token' }); + } +}; diff --git a/backend_splatournament_manager/src/services/user-service.ts b/backend_splatournament_manager/src/services/user-service.ts index fb7a5d0..a9cde99 100644 --- a/backend_splatournament_manager/src/services/user-service.ts +++ b/backend_splatournament_manager/src/services/user-service.ts @@ -1,9 +1,12 @@ import { User } from '../models/user'; import { Database } from 'sqlite3'; import * as argon2 from 'argon2'; +import * as jwt from 'jsonwebtoken'; import fs from "fs"; import path from "path"; +const JWT_SECRET = process.env.JWT_SECRET || "key"; + export class UserService { private csvFilename = 'csv/users.csv'; private db: Database; @@ -40,7 +43,7 @@ export class UserService { }); } - register(username: string, password: string): Promise<{ id: number; username: string }> { + register(username: string, password: string): Promise<{ id: number; username: string; token: string }> { return new Promise(async (resolve, reject) => { try { const hash = await argon2.hash(password); @@ -51,7 +54,8 @@ export class UserService { if (err) { return reject(err); } - resolve({ id: this.lastID, username }); + const token = jwt.sign({ id: this.lastID, username }, JWT_SECRET, { expiresIn: '7d' }); + resolve({ id: this.lastID, username, token }); } ); } catch (err) { @@ -60,7 +64,7 @@ export class UserService { }); } - login(username: string, password: string): Promise<{ id: number; username: string }> { + login(username: string, password: string): Promise<{ id: number; username: string; token: string }> { return new Promise((resolve, reject) => { this.db.get( 'SELECT * FROM Users WHERE username = ?', @@ -77,7 +81,8 @@ export class UserService { if (!valid) { return reject(new Error('Invalid password')); } - resolve({ id: user.id, username: user.username }); + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '7d' }); + resolve({ id: user.id, username: user.username, token }); } catch (e) { reject(e); } @@ -86,5 +91,3 @@ export class UserService { }); } } - - diff --git a/frontend_splatournament_manager/lib/providers/auth_provider.dart b/frontend_splatournament_manager/lib/providers/auth_provider.dart index 60add19..2533010 100644 --- a/frontend_splatournament_manager/lib/providers/auth_provider.dart +++ b/frontend_splatournament_manager/lib/providers/auth_provider.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:frontend_splatournament_manager/services/auth_service.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; class AuthProvider extends ChangeNotifier { final AuthService _authService = AuthService(); + final FlutterSecureStorage _storage = const FlutterSecureStorage(); bool _isLoggedIn = false; String? _username; @@ -23,6 +26,12 @@ class AuthProvider extends ChangeNotifier { try { final user = await _authService.login(username, password); + + final token = user['token']; + if (token != null) { + await _storage.write(key: 'jwt_token', value: token); + } + _isLoggedIn = true; _username = user['username']; _userId = user['id']; @@ -44,6 +53,12 @@ class AuthProvider extends ChangeNotifier { try { final user = await _authService.register(username, password); + + final token = user['token']; + if (token != null) { + await _storage.write(key: 'jwt_token', value: token); + } + _isLoggedIn = true; _username = user['username']; _userId = user['id']; @@ -58,17 +73,32 @@ class AuthProvider extends ChangeNotifier { } } - void logout() { + Future logout() async { _isLoggedIn = false; _username = null; _userId = null; _error = null; + await _storage.delete(key: 'jwt_token'); notifyListeners(); } + Future checkAuthStatus() async { + final token = await _storage.read(key: 'jwt_token'); + if (token != null) { + if (JwtDecoder.isExpired(token)) { + await logout(); + } else { + Map decodedToken = JwtDecoder.decode(token); + _isLoggedIn = true; + _userId = decodedToken['id']; + _username = decodedToken['username']; + notifyListeners(); + } + } + } + void clearError() { _error = null; notifyListeners(); } } - diff --git a/frontend_splatournament_manager/lib/providers/tournament_provider.dart b/frontend_splatournament_manager/lib/providers/tournament_provider.dart index 14d4cbc..7cfcc94 100644 --- a/frontend_splatournament_manager/lib/providers/tournament_provider.dart +++ b/frontend_splatournament_manager/lib/providers/tournament_provider.dart @@ -3,12 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:frontend_splatournament_manager/models/tournament.dart'; -import 'package:http/http.dart' as http; - -import '../main.dart'; +import 'package:frontend_splatournament_manager/services/api_client.dart'; class TournamentProvider extends ChangeNotifier { - final String baseUrl = SplatournamentApp.baseUrl; List _availableTournaments = []; Future>? _initialLoadFuture; @@ -16,7 +13,7 @@ class TournamentProvider extends ChangeNotifier { List get availableTournaments => _availableTournaments; Future> _fetchTournaments() async { - final response = await http.get(Uri.parse('$baseUrl/tournaments')); + final response = await ApiClient.get('/tournaments'); if (response.statusCode != HttpStatus.ok) { throw Exception('Failed to load tournaments (${response.statusCode})'); } @@ -48,21 +45,15 @@ class TournamentProvider extends ChangeNotifier { DateTime registrationStartDate, DateTime registrationEndDate, ) async { - final response = await http.post( - Uri.parse('$baseUrl/tournaments'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ + final response = await ApiClient.post( + '/tournaments', + { 'name': name, 'description': description, 'maxTeamAmount': maxTeamAmount, - //weird date formatting - 'registrationStartDate': registrationStartDate.toIso8601String().split( - 'T', - )[0], - 'registrationEndDate': registrationEndDate.toIso8601String().split( - 'T', - )[0], - }), + 'registrationStartDate': registrationStartDate.toIso8601String().split('T')[0], + 'registrationEndDate': registrationEndDate.toIso8601String().split('T')[0], + }, ); if (response.statusCode != HttpStatus.created) { diff --git a/frontend_splatournament_manager/lib/services/api_client.dart b/frontend_splatournament_manager/lib/services/api_client.dart new file mode 100644 index 0000000..ff5967a --- /dev/null +++ b/frontend_splatournament_manager/lib/services/api_client.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:frontend_splatournament_manager/main.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class ApiClient { + static const String baseUrl = SplatournamentApp.baseUrl; + static const _storage = FlutterSecureStorage(); + + static Future> _getHeaders() async { + final token = await _storage.read(key: 'jwt_token'); + return { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + static Future get(String endpoint) async { + final headers = await _getHeaders(); + return await http.get(Uri.parse('$baseUrl$endpoint'), headers: headers); + } + + static Future post(String endpoint, Map body) async { + final headers = await _getHeaders(); + return await http.post( + Uri.parse('$baseUrl$endpoint'), + headers: headers, + body: jsonEncode(body), + ); + } + + static Future put(String endpoint, Map body) async { + final headers = await _getHeaders(); + return await http.put( + Uri.parse('$baseUrl$endpoint'), + headers: headers, + body: jsonEncode(body), + ); + } + + static Future delete(String endpoint) async { + final headers = await _getHeaders(); + return await http.delete(Uri.parse('$baseUrl$endpoint'), headers: headers); + } +} diff --git a/frontend_splatournament_manager/lib/services/team_service.dart b/frontend_splatournament_manager/lib/services/team_service.dart index 566dd3e..2c8dbdf 100644 --- a/frontend_splatournament_manager/lib/services/team_service.dart +++ b/frontend_splatournament_manager/lib/services/team_service.dart @@ -1,15 +1,13 @@ import 'dart:convert'; import 'dart:io'; -import 'package:frontend_splatournament_manager/main.dart'; import 'package:frontend_splatournament_manager/models/team.dart'; -import 'package:http/http.dart' as http; +import 'package:frontend_splatournament_manager/services/api_client.dart'; class TeamService { - final String baseUrl = SplatournamentApp.baseUrl; Future> getAllTeams() async { - final response = await http.get(Uri.parse('$baseUrl/teams')); + final response = await ApiClient.get('/teams'); if (response.statusCode != HttpStatus.ok) { throw Exception('Failed to load teams (${response.statusCode})'); } @@ -18,7 +16,7 @@ class TeamService { } Future getTeamById(int id) async { - final response = await http.get(Uri.parse('$baseUrl/teams/$id')); + final response = await ApiClient.get('/teams/$id'); if (response.statusCode == HttpStatus.notFound) { throw Exception('Team not found'); } @@ -33,10 +31,9 @@ class TeamService { required String tag, String description = '', }) async { - final response = await http.post( - Uri.parse('$baseUrl/teams'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({'name': name, 'tag': tag, 'description': description}), + final response = await ApiClient.post( + '/teams', + {'name': name, 'tag': tag, 'description': description}, ); if (response.statusCode != HttpStatus.created) { final body = json.decode(response.body); @@ -51,14 +48,13 @@ class TeamService { String? tag, String? description, }) async { - final response = await http.put( - Uri.parse('$baseUrl/teams/$id'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - 'name': ?name, - 'tag': ?tag, - 'description': ?description, - }), + final response = await ApiClient.put( + '/teams/$id', + { + if (name != null) 'name': name, + if (tag != null) 'tag': tag, + if (description != null) 'description': description, + }, ); if (response.statusCode != HttpStatus.ok) { final body = json.decode(response.body); @@ -67,7 +63,7 @@ class TeamService { } Future deleteTeam(int id) async { - final response = await http.delete(Uri.parse('$baseUrl/teams/$id')); + final response = await ApiClient.delete('/teams/$id'); if (response.statusCode != HttpStatus.ok) { final body = json.decode(response.body); throw Exception(body['error'] ?? 'Failed to delete team'); @@ -75,9 +71,7 @@ class TeamService { } Future> getTeamsByTournament(int tournamentId) async { - final response = await http.get( - Uri.parse('$baseUrl/tournaments/$tournamentId/teams'), - ); + final response = await ApiClient.get('/tournaments/$tournamentId/teams'); if (response.statusCode != HttpStatus.ok) { throw Exception( 'Failed to load teams for tournament (${response.statusCode})', @@ -88,10 +82,9 @@ class TeamService { } Future registerTeamForTournament(int tournamentId, int teamId) async { - final response = await http.post( - Uri.parse('$baseUrl/tournaments/$tournamentId/teams'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({'teamId': teamId}), + final response = await ApiClient.post( + '/tournaments/$tournamentId/teams', + {'teamId': teamId}, ); if (response.statusCode == 409) { throw Exception('Team is already registered for this tournament'); @@ -103,9 +96,7 @@ class TeamService { } Future removeTeamFromTournament(int tournamentId, int teamId) async { - final response = await http.delete( - Uri.parse('$baseUrl/tournaments/$tournamentId/teams/$teamId'), - ); + final response = await ApiClient.delete('/tournaments/$tournamentId/teams/$teamId'); if (response.statusCode != HttpStatus.ok) { final body = json.decode(response.body); throw Exception(body['error'] ?? 'Failed to remove team from tournament'); @@ -113,9 +104,7 @@ class TeamService { } Future>> getTournamentsByTeam(int teamId) async { - final response = await http.get( - Uri.parse('$baseUrl/teams/$teamId/tournaments'), - ); + final response = await ApiClient.get('/teams/$teamId/tournaments'); if (response.statusCode != HttpStatus.ok) { throw Exception( 'Failed to load tournaments for team (${response.statusCode})', diff --git a/frontend_splatournament_manager/pubspec.yaml b/frontend_splatournament_manager/pubspec.yaml index b1d6df1..55871e1 100644 --- a/frontend_splatournament_manager/pubspec.yaml +++ b/frontend_splatournament_manager/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: provider: ^6.1.5+1 go_router: ^17.1.0 intl: ^0.20.2 + flutter_secure_storage: ^10.0.0 + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: