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
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:
parent
8d3d4cd1f7
commit
1989f988a6
3 changed files with 249 additions and 8 deletions
|
|
@ -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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/<userId>', service.getUser);
|
||||
router.add('POST', r'/admin/users', service.upsertUser);
|
||||
router.add('DELETE', r'/admin/users/<userId>', service.deleteUser);
|
||||
return router;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (_) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue