From 28d4bef661cc0f974b84b86d0f56992dcf5e0ebf Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 7 Dec 2025 00:34:40 +0100 Subject: [PATCH] feat: Initialize core database with user, playlist, and session management, and establish initial server and user API handler files. --- bin/api/users_handler.dart | 166 +++++++++++++++++++++++++++++++++++++ bin/database/database.dart | 28 +++++++ bin/server.dart | 8 +- 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 bin/api/users_handler.dart diff --git a/bin/api/users_handler.dart b/bin/api/users_handler.dart new file mode 100644 index 0000000..e653695 --- /dev/null +++ b/bin/api/users_handler.dart @@ -0,0 +1,166 @@ +import 'dart:convert'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; +import '../database/database.dart'; + +class UsersHandler { + final AppDatabase db; + + UsersHandler(this.db); + + Router get router { + final router = Router(); + router.get('/', _getAllUsers); + router.post('/', _createUser); + router.put('//password', _updatePassword); + router.put('/', _updateUser); + router.delete('/', _deleteUser); + return router; + } + + /// Check if requester is admin + bool _isAdmin(Request request) { + final userId = request.context['userId'] as String?; + if (userId == null) return false; + final user = db.findUserById(userId); + return user?.isAdmin ?? false; + } + + /// GET /api/users + Future _getAllUsers(Request request) async { + try { + if (!_isAdmin(request)) { + return Response.forbidden(jsonEncode({'success': false, 'error': 'Admin required'})); + } + + final users = db.getAllUsers(); + return Response.ok(jsonEncode({ + 'success': true, + 'users': users.map((u) => u.toJson()).toList(), + }), headers: {'Content-Type': 'application/json'}); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({'success': false, 'error': e.toString()}), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + /// POST /api/users + Future _createUser(Request request) async { + try { + if (!_isAdmin(request)) { + return Response.forbidden(jsonEncode({'success': false, 'error': 'Admin required'})); + } + + final payload = jsonDecode(await request.readAsString()) as Map; + final username = payload['username'] as String?; + final password = payload['password'] as String?; + final isAdmin = payload['isAdmin'] as bool? ?? false; + + if (username == null || password == null) { + return Response.badRequest(body: jsonEncode({ + 'success': false, + 'error': 'Missing required fields', + }), headers: {'Content-Type': 'application/json'}); + } + + if (db.findUserByUsername(username) != null) { + return Response.badRequest(body: jsonEncode({ + 'success': false, + 'error': 'Username already exists', + }), headers: {'Content-Type': 'application/json'}); + } + + final user = db.createUser(username, password, isAdmin: isAdmin); + return Response.ok(jsonEncode({ + 'success': true, + 'user': user.toJson(), + }), headers: {'Content-Type': 'application/json'}); + + } catch (e) { + return Response.internalServerError( + body: jsonEncode({'success': false, 'error': e.toString()}), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + /// PUT /api/users/:id/password + Future _updatePassword(Request request, String id) async { + try { + if (!_isAdmin(request)) { + return Response.forbidden(jsonEncode({'success': false, 'error': 'Admin required'})); + } + + final payload = jsonDecode(await request.readAsString()) as Map; + final password = payload['password'] as String?; + + if (password == null) { + return Response.badRequest(body: jsonEncode({ + 'success': false, + 'error': 'Missing password', + }), headers: {'Content-Type': 'application/json'}); + } + + db.updateUserPassword(id, password); + return Response.ok(jsonEncode({'success': true}), headers: {'Content-Type': 'application/json'}); + + } catch (e) { + return Response.internalServerError( + body: jsonEncode({'success': false, 'error': e.toString()}), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + /// PUT /api/users/:id (Toggle Admin) + Future _updateUser(Request request, String id) async { + try { + if (!_isAdmin(request)) { + return Response.forbidden(jsonEncode({'success': false, 'error': 'Admin required'})); + } + + final payload = jsonDecode(await request.readAsString()) as Map; + final isAdmin = payload['isAdmin'] as bool?; + + if (isAdmin != null) { + db.updateUserAdminStatus(id, isAdmin); + } + + return Response.ok(jsonEncode({'success': true}), headers: {'Content-Type': 'application/json'}); + + } catch (e) { + return Response.internalServerError( + body: jsonEncode({'success': false, 'error': e.toString()}), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + /// DELETE /api/users/:id + Future _deleteUser(Request request, String id) async { + try { + if (!_isAdmin(request)) { + return Response.forbidden(jsonEncode({'success': false, 'error': 'Admin required'})); + } + + // Prevent deleting self + final currentUserId = request.context['userId'] as String; + if (currentUserId == id) { + return Response.badRequest(body: jsonEncode({ + 'success': false, + 'error': 'Cannot delete yourself', + }), headers: {'Content-Type': 'application/json'}); + } + + db.deleteUser(id); + return Response.ok(jsonEncode({'success': true}), headers: {'Content-Type': 'application/json'}); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({'success': false, 'error': e.toString()}), + headers: {'Content-Type': 'application/json'}, + ); + } + } +} diff --git a/bin/database/database.dart b/bin/database/database.dart index 082521e..6bceacd 100644 --- a/bin/database/database.dart +++ b/bin/database/database.dart @@ -151,6 +151,34 @@ class AppDatabase { ); } + /// Get all users + List getAllUsers() { + final result = _db.select('SELECT * FROM users ORDER BY username ASC'); + return result.map((row) => User.fromMap(row)).toList(); + } + + /// Update user password + void updateUserPassword(String userId, String newPassword) { + final passwordHash = PasswordHasher.hash(newPassword); + _db.execute( + 'UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [passwordHash, userId], + ); + } + + /// Update user administration status + void updateUserAdminStatus(String userId, bool isAdmin) { + _db.execute( + 'UPDATE users SET is_admin = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [isAdmin ? 1 : 0, userId], + ); + } + + /// Delete user + void deleteUser(String userId) { + _db.execute('DELETE FROM users WHERE id = ?', [userId]); + } + // ==================== Sessions ==================== /// Create new session diff --git a/bin/server.dart b/bin/server.dart index 3497938..a752885 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -9,6 +9,7 @@ import 'package:http/http.dart' as http; import 'package:args/args.dart'; import 'database/database.dart'; import 'api/auth_handler.dart'; +import 'api/users_handler.dart'; import 'api/playlists_handler.dart'; import 'middleware/auth_middleware.dart'; @@ -30,6 +31,7 @@ void main(List args) async { // Create API handlers final authHandler = AuthHandler(db); final playlistsHandler = PlaylistsHandler(db); + final usersHandler = UsersHandler(db); // Setup router final apiRouter = Router() @@ -38,7 +40,11 @@ void main(List args) async { // Playlists endpoints (with auth middleware) ..mount('/api/playlists', Pipeline() .addMiddleware(authMiddleware(db)) - .addHandler(playlistsHandler.router.call)); + .addHandler(playlistsHandler.router.call)) + // Users endpoints (with auth middleware) + ..mount('/api/users', Pipeline() + .addMiddleware(authMiddleware(db)) + .addHandler(usersHandler.router.call)); // Create handlers final staticHandler = createStaticHandler(