admin
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-20 13:04:19 +03:00
parent 8d3d4cd1f7
commit 1989f988a6
3 changed files with 249 additions and 8 deletions

View file

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:drift/drift.dart' as drift;
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart'; import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.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/database/database.dart';
import 'package:mnemo_cards_backend/user/user_drift_extension.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_backend/user/user_model.dart' show UserModelExtension;
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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<Response> 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<String, dynamic> jsonData;
try {
jsonData = jsonDecode(body) as Map<String, dynamic>;
} 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/<userId>')
Future<Response> 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); Router get router => _$AdminUsersApiV2Router(this);
} }

View file

@ -10,5 +10,7 @@ Router _$AdminUsersApiV2Router(AdminUsersApiV2 service) {
final router = Router(); final router = Router();
router.add('GET', r'/admin/users', service.getAllUsers); router.add('GET', r'/admin/users', service.getAllUsers);
router.add('GET', r'/admin/users/<userId>', service.getUser); router.add('GET', r'/admin/users/<userId>', service.getUser);
router.add('POST', r'/admin/users', service.upsertUser);
router.add('DELETE', r'/admin/users/<userId>', service.deleteUser);
return router; return router;
} }

View file

@ -314,18 +314,18 @@ class PacksApiV2 {
return _notFound('Pack not found'); 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) { if (user != null) {
final isPurchased = await _isPackPurchased(user.id!, packId) || final isPurchased = await _isPackPurchased(user.id!, packId) ||
(await _hasSubscriptionAccess(user)); (await _hasSubscriptionAccess(user));
if (isPurchased) { packJson['isPurchased'] = isPurchased;
return _conflict('Pack already purchased');
}
} }
// Return pack preview as buy page return _ok(packJson);
final packDto = await _packManager.getPackDto(packId, user);
return _ok(packDto.toJson());
} on FormatException catch (_) { } on FormatException catch (_) {
return _badRequest('Invalid pack ID'); return _badRequest('Invalid pack ID');
} on StateError catch (_) { } on StateError catch (_) {