From 1989f988a66fe7585aee8eae534ac00a013751e2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 20 Dec 2025 13:04:19 +0300 Subject: [PATCH] admin --- .../lib/api/v2/admin_users_api_v2.dart | 239 ++++++++++++++++++ .../lib/api/v2/admin_users_api_v2.g.dart | 2 + .../lib/api/v2/packs_api_v2.dart | 16 +- 3 files changed, 249 insertions(+), 8 deletions(-) diff --git a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart index e502041..82ebc25 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:drift/drift.dart' as drift; +import 'package:drift_postgres/drift_postgres.dart'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/api/authorize/acl_types.dart'; import 'package:mnemo_cards_backend/api/authorize/access_service.dart'; @@ -7,6 +9,7 @@ import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/user/user_drift_extension.dart'; import 'package:mnemo_cards_backend/user/user_model.dart' show UserModelExtension; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -191,5 +194,241 @@ class AdminUsersApiV2 { } } + /// POST /api/v2/admin/users + /// Create or update a user + @Route.post('/admin/users') + Future upsertUser(Request request) async { + try { + final auth = await _ensureAdmin(request); + if (auth.statusCode != 200) { + return auth; + } + + final body = await request.readAsString(); + if (body.isEmpty) { + return _json( + { + 'error': 'Invalid request body', + 'message': 'Request body cannot be empty', + 'details': 'Please provide user data in the request body.', + }, + statusCode: 400, + ); + } + + final Map jsonData; + try { + jsonData = jsonDecode(body) as Map; + } catch (e) { + return _json( + { + 'error': 'Invalid JSON', + 'message': 'Failed to parse request body as JSON', + 'details': 'The request body must be valid JSON. Error: $e', + }, + statusCode: 400, + ); + } + + final userDto = UserDto.fromJson(jsonData); + final isUpdate = userDto.id != null && userDto.id!.isNotEmpty; + + if (isUpdate) { + // Update existing user + final existingUser = await _db.userDao.getUserById(userDto.id!); + if (existingUser == null) { + return _json( + { + 'error': 'User not found', + 'message': 'The user you are trying to update does not exist', + 'details': 'User with ID "${userDto.id}" was not found. The user may have been deleted or the ID may be incorrect.', + }, + statusCode: 404, + ); + } + + // Build update companion + // Check if purchases field was present in the original JSON + final purchasesPresent = jsonData.containsKey('purchases'); + + final updates = UsersCompanion( + id: drift.Value(userDto.id!), + name: userDto.name != null + ? drift.Value(userDto.name) + : const drift.Value.absent(), + email: userDto.email != null + ? drift.Value(userDto.email) + : const drift.Value.absent(), + telegram: userDto.telegram != null + ? drift.Value(userDto.telegram) + : const drift.Value.absent(), + admin: drift.Value(userDto.admin), + purchases: purchasesPresent + ? drift.Value(userDto.purchases) + : const drift.Value.absent(), + updatedAt: drift.Value(PgDateTime(DateTime.now())), + ); + + await _db.userDao.updateUserPartial(updates); + + // Get updated user + final updatedUser = await _db.userDao.getUserById(userDto.id!); + if (updatedUser == null) { + return _json( + { + 'error': 'Database error', + 'message': 'Failed to retrieve updated user', + 'details': 'User was updated successfully but could not be retrieved from the database.', + }, + statusCode: 500, + ); + } + + final userModel = await updatedUser.toUserModel(); + final dto = await userModel.toDto(); + + return _json({ + 'result': true, + 'user': dto.toJson(), + }); + } else { + // Create new user + // Generate externalUserId if not provided (use email or generate UUID) + final externalUserId = userDto.email?.isNotEmpty == true + ? userDto.email! + : 'admin_created_${DateTime.now().millisecondsSinceEpoch}'; + + // Check if user with this externalUserId already exists + final existingUser = await _db.userDao.getUserByExternalId(externalUserId); + if (existingUser != null) { + return _json( + { + 'error': 'User already exists', + 'message': 'A user with this email already exists', + 'details': 'A user with email "$externalUserId" already exists. Use update instead of create.', + }, + statusCode: 409, + ); + } + + final now = DateTime.now(); + final userCompanion = UsersCompanion.insert( + externalUserId: externalUserId, + name: drift.Value(userDto.name), + email: drift.Value(userDto.email), + telegram: drift.Value(userDto.telegram), + admin: drift.Value(userDto.admin), + purchases: drift.Value(userDto.purchases), + createdAt: drift.Value(PgDateTime(now)), + updatedAt: drift.Value(PgDateTime(now)), + isDeleted: drift.Value(false), + ); + + final userDataCompanion = UserDatasCompanion.insert( + userId: '', // Will be set by createUserWithData + registrationDate: drift.Value(PgDateTime(now)), + ); + + final userId = await _db.userDao.createUserWithData( + user: userCompanion, + userData: userDataCompanion, + ); + + final createdUser = await _db.userDao.getUserById(userId); + if (createdUser == null) { + return _json( + { + 'error': 'Database error', + 'message': 'Failed to retrieve created user', + 'details': 'User was created successfully but could not be retrieved from the database.', + }, + statusCode: 500, + ); + } + + final userModel = await createdUser.toUserModel(); + final dto = await userModel.toDto(); + + return _json({ + 'result': true, + 'user': dto.toJson(), + }); + } + } catch (e, s) { + print('Error in upsertUser: $e\n$s'); + return _json( + { + 'error': 'Internal server error', + 'message': 'Failed to create or update user', + 'details': 'An unexpected error occurred while processing the request. Please try again later or contact support if the problem persists.', + }, + statusCode: 500, + ); + } + } + + /// DELETE /api/v2/admin/users/{userId} + /// Delete a user (soft delete) + @Route.delete('/admin/users/') + Future deleteUser(Request request, String userId) async { + try { + final auth = await _ensureAdmin(request); + if (auth.statusCode != 200) { + return auth; + } + + if (userId.isEmpty) { + return _json( + { + 'error': 'Invalid user ID', + 'message': 'User ID cannot be empty', + 'field': 'userId', + 'details': 'Please provide a valid user ID to delete the user.', + }, + statusCode: 400, + ); + } + + final user = await _db.userDao.getUserById(userId); + if (user == null) { + return _json( + { + 'error': 'User not found', + 'message': 'The user you are trying to delete does not exist', + 'details': 'User with ID "$userId" was not found. The user may have already been deleted.', + }, + statusCode: 404, + ); + } + + if (user.isDeleted) { + return _json( + { + 'error': 'User already deleted', + 'message': 'The user has already been deleted', + 'details': 'User with ID "$userId" has already been deleted.', + }, + statusCode: 409, + ); + } + + await _db.userDao.softDeleteUser(userId); + + return _json({ + 'result': true, + }); + } catch (e, s) { + print('Error in deleteUser: $e\n$s'); + return _json( + { + 'error': 'Internal server error', + 'message': 'Failed to delete user', + 'details': 'An unexpected error occurred while deleting the user. Please try again later or contact support if the problem persists.', + }, + statusCode: 500, + ); + } + } + Router get router => _$AdminUsersApiV2Router(this); } diff --git a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.g.dart b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.g.dart index eb6b6ee..4d249d9 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.g.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.g.dart @@ -10,5 +10,7 @@ Router _$AdminUsersApiV2Router(AdminUsersApiV2 service) { final router = Router(); router.add('GET', r'/admin/users', service.getAllUsers); router.add('GET', r'/admin/users/', service.getUser); + router.add('POST', r'/admin/users', service.upsertUser); + router.add('DELETE', r'/admin/users/', service.deleteUser); return router; } diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 4d4a3b4..394367f 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -314,18 +314,18 @@ class PacksApiV2 { return _notFound('Pack not found'); } - // For authenticated users, check if they already own the pack + // Return pack preview as buy page + final packDto = await _packManager.getPackDto(packId, user); + final packJson = packDto.toJson(); + + // Include purchase status in response if user is authenticated if (user != null) { final isPurchased = await _isPackPurchased(user.id!, packId) || (await _hasSubscriptionAccess(user)); - if (isPurchased) { - return _conflict('Pack already purchased'); - } + packJson['isPurchased'] = isPurchased; } - - // Return pack preview as buy page - final packDto = await _packManager.getPackDto(packId, user); - return _ok(packDto.toJson()); + + return _ok(packJson); } on FormatException catch (_) { return _badRequest('Invalid pack ID'); } on StateError catch (_) {