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 '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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (_) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue