api
Some checks failed
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
Backend CI / test (push) Has been cancelled
Backend CI / build (push) Has been cancelled
Some checks failed
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
Backend CI / test (push) Has been cancelled
Backend CI / build (push) Has been cancelled
This commit is contained in:
parent
aec56c2fd8
commit
5a8c55c1ef
6 changed files with 677 additions and 0 deletions
|
|
@ -40,6 +40,8 @@ import '../v2/admin_analytics_api_v2.dart' as _i368;
|
|||
import '../v2/admin_auth_api_v2.dart' as _i483;
|
||||
import '../v2/admin_cards_api_v2.dart' as _i922;
|
||||
import '../v2/admin_packs_api_v2.dart' as _i1015;
|
||||
import '../v2/admin_tests_api_v2.dart' as _i116;
|
||||
import '../v2/admin_users_api_v2.dart' as _i895;
|
||||
import '../v2/auth_api_v2.dart' as _i52;
|
||||
import '../v2/discounts_api_v2.dart' as _i858;
|
||||
import '../v2/jwt_service.dart' as _i108;
|
||||
|
|
@ -112,6 +114,12 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh.factory<_i1015.AdminPacksApiV2>(
|
||||
() => _i1015.AdminPacksApiV2(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.factory<_i116.AdminTestsApiV2>(
|
||||
() => _i116.AdminTestsApiV2(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.factory<_i895.AdminUsersApiV2>(
|
||||
() => _i895.AdminUsersApiV2(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i964.SubscriptionsApiV2>(
|
||||
() => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import 'v2/admin_analytics_api_v2.dart';
|
|||
import 'v2/admin_auth_api_v2.dart';
|
||||
import 'v2/admin_cards_api_v2.dart';
|
||||
import 'v2/admin_packs_api_v2.dart';
|
||||
import 'v2/admin_tests_api_v2.dart';
|
||||
import 'v2/admin_users_api_v2.dart';
|
||||
import 'v2/auth_api_v2.dart';
|
||||
import 'v2/discounts_api_v2.dart';
|
||||
import 'v2/packs_api_v2.dart';
|
||||
|
|
@ -52,6 +54,8 @@ class MnemoShelf {
|
|||
v2Router.mount('/', getIt.get<AdminAnalyticsApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<AdminCardsApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<AdminPacksApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<AdminTestsApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<AdminUsersApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<PacksApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<TestsApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<PromocodesApiV2>().router);
|
||||
|
|
|
|||
440
mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart
Normal file
440
mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
part 'admin_tests_api_v2.g.dart';
|
||||
|
||||
/// Admin endpoints for managing tests in API v2.
|
||||
@injectable
|
||||
class AdminTestsApiV2 {
|
||||
final AppDatabase _db;
|
||||
|
||||
AdminTestsApiV2(this._db);
|
||||
|
||||
Response _json(
|
||||
Object? data, {
|
||||
int statusCode = 200,
|
||||
Map<String, String> headers = const {},
|
||||
}) {
|
||||
return Response(
|
||||
statusCode,
|
||||
body: data == null ? null : jsonEncode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> _ensureAdmin(Request request) async {
|
||||
try {
|
||||
await request.access!
|
||||
.requireAdmin(AdminAction.access, user: request.user);
|
||||
return Response.ok(null);
|
||||
} on AccessDenied catch (e) {
|
||||
return Response(e.status, body: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/tests
|
||||
/// Get all tests with pagination and search
|
||||
@Route.get('/admin/tests')
|
||||
Future<Response> getAllTests(Request request) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final queryParams = request.url.queryParameters;
|
||||
|
||||
// Parse pagination parameters
|
||||
final page = int.tryParse(queryParams['page'] ?? '1') ?? 1;
|
||||
final limit = int.tryParse(queryParams['limit'] ?? '20') ?? 20;
|
||||
final search = queryParams['search'] ?? '';
|
||||
|
||||
// Validate pagination
|
||||
if (page < 1) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Invalid page parameter',
|
||||
'message': 'Page must be greater than 0. Received: $page',
|
||||
'field': 'page',
|
||||
'details': 'Page numbers start from 1. Please provide a valid page number.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
// Limit validation: if limit is invalid, set to default; if > 100, cap at 100
|
||||
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
|
||||
|
||||
// Get all tests
|
||||
final allTests = await _db.testDao.getAllTests();
|
||||
|
||||
// Convert to TestDto format (simplified, without questions for list view)
|
||||
final testDtos = <Map<String, dynamic>>[];
|
||||
for (final test in allTests) {
|
||||
final questions = await _db.testDao.getTestQuestions(test.id);
|
||||
testDtos.add({
|
||||
'id': test.id,
|
||||
'name': test.name,
|
||||
'color': test.color,
|
||||
'cover': test.cover,
|
||||
'version': test.version ?? '1.0',
|
||||
'time': test.time,
|
||||
'timeSubtitle': test.timeSubtitle,
|
||||
'questions': questions.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search filter if provided
|
||||
List<Map<String, dynamic>> filteredTests = testDtos;
|
||||
if (search.isNotEmpty) {
|
||||
final searchTerm = search.toLowerCase();
|
||||
filteredTests = testDtos.where((test) {
|
||||
return (test['name'] as String?)?.toLowerCase().contains(searchTerm) == true ||
|
||||
(test['id'] as String?)?.toLowerCase().contains(searchTerm) == true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
final total = filteredTests.length;
|
||||
final totalPages = (total / validatedLimit).ceil();
|
||||
final offset = (page - 1) * validatedLimit;
|
||||
final paginatedTests = filteredTests.skip(offset).take(validatedLimit).toList();
|
||||
|
||||
return _json({
|
||||
'items': paginatedTests,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': validatedLimit,
|
||||
'totalPages': totalPages,
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in getAllTests: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to retrieve tests',
|
||||
'details': 'An unexpected error occurred while fetching tests. Please try again later or contact support if the problem persists.',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/tests/{testId}
|
||||
/// Get a specific test by ID
|
||||
@Route.get('/admin/tests/<testId>')
|
||||
Future<Response> getTest(Request request, String testId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (testId.isEmpty) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Invalid test ID',
|
||||
'message': 'Test ID cannot be empty',
|
||||
'field': 'testId',
|
||||
'details': 'Please provide a valid test ID to retrieve test details.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
final test = await _db.testDao.getTestById(testId);
|
||||
if (test == null) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Test not found',
|
||||
'message': 'The requested test does not exist or has been deleted',
|
||||
'details': 'Test with ID "$testId" was not found. Please verify the test ID and try again.',
|
||||
},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
// Get questions
|
||||
final questions = await _db.testDao.getTestQuestions(testId);
|
||||
final questionsList = questions.map((q) {
|
||||
final body = json.decode(q.body) as Map<String, dynamic>;
|
||||
return AbstractTestQuestion.fromJson({
|
||||
'questionType': q.questionType,
|
||||
...body,
|
||||
});
|
||||
}).toList();
|
||||
|
||||
return _json({
|
||||
'id': test.id,
|
||||
'name': test.name,
|
||||
'color': test.color,
|
||||
'cover': test.cover,
|
||||
'version': test.version ?? '1.0',
|
||||
'time': test.time,
|
||||
'timeSubtitle': test.timeSubtitle,
|
||||
'questions': questionsList.map((q) => q.toJson()).toList(),
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in getTest: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to retrieve test',
|
||||
'details': 'An unexpected error occurred while fetching the test. Please try again later or contact support if the problem persists.',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/v2/admin/tests
|
||||
/// Create or update a test
|
||||
@Route.post('/admin/tests')
|
||||
Future<Response> upsertTest(Request request) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final bodyString = await request.readAsString();
|
||||
if (bodyString.isEmpty) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Invalid request body',
|
||||
'message': 'Request body is required',
|
||||
'details': 'Please provide test data in the request body.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
final bodyJson = jsonDecode(bodyString) as Map<String, dynamic>;
|
||||
final testId = bodyJson['id'] as String?;
|
||||
final name = bodyJson['name'] as String?;
|
||||
final color = bodyJson['color'] as String?;
|
||||
final cover = bodyJson['cover'] as String?;
|
||||
final version = bodyJson['version'] as String?;
|
||||
final time = bodyJson['time'] as String?;
|
||||
final timeSubtitle = bodyJson['timeSubtitle'] as String?;
|
||||
final questions = bodyJson['questions'] as List<dynamic>?;
|
||||
|
||||
if (name == null || name.isEmpty) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Invalid test data',
|
||||
'message': 'Test name is required',
|
||||
'field': 'name',
|
||||
'details': 'Please provide a valid test name.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
// Create or update test
|
||||
if (testId != null && testId.isNotEmpty) {
|
||||
// Update existing test
|
||||
final existingTest = await _db.testDao.getTestById(testId);
|
||||
if (existingTest == null) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Test not found',
|
||||
'message': 'Test with the provided ID does not exist',
|
||||
'details': 'Cannot update a test that does not exist. Please create a new test instead.',
|
||||
},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
// Update test
|
||||
final testToUpdate = await _db.testDao.getTestById(testId);
|
||||
if (testToUpdate != null) {
|
||||
final updatedTest = testToUpdate.copyWith(
|
||||
name: name,
|
||||
color: color != null ? drift.Value(color) : const drift.Value.absent(),
|
||||
cover: cover != null ? drift.Value(cover) : const drift.Value.absent(),
|
||||
version: version != null ? drift.Value(version) : const drift.Value.absent(),
|
||||
time: time != null ? drift.Value(time) : const drift.Value.absent(),
|
||||
timeSubtitle: timeSubtitle != null ? drift.Value(timeSubtitle) : const drift.Value.absent(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
);
|
||||
await _db.testDao.updateTest(updatedTest);
|
||||
}
|
||||
|
||||
// Update questions if provided
|
||||
if (questions != null) {
|
||||
// Delete existing questions (soft delete)
|
||||
final existingQuestions = await _db.testDao.getTestQuestions(testId);
|
||||
for (final q in existingQuestions) {
|
||||
await _db.testDao.softDeleteTestQuestion(q.id);
|
||||
}
|
||||
// Add new questions
|
||||
for (final q in questions) {
|
||||
final questionJson = q as Map<String, dynamic>;
|
||||
final questionType = questionJson['questionType'] as String? ?? 'multiple_choice';
|
||||
final questionBody = Map<String, dynamic>.from(questionJson);
|
||||
questionBody.remove('questionType');
|
||||
await _db.testDao.createTestQuestion(
|
||||
TestQuestionsCompanion.insert(
|
||||
testId: testId,
|
||||
questionType: questionType,
|
||||
body: jsonEncode(questionBody),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final updatedTest = await _db.testDao.getTestById(testId);
|
||||
return _json({
|
||||
'success': true,
|
||||
'test': updatedTest != null ? {
|
||||
'id': updatedTest.id,
|
||||
'name': updatedTest.name,
|
||||
'color': updatedTest.color,
|
||||
'cover': updatedTest.cover,
|
||||
'version': updatedTest.version,
|
||||
'time': updatedTest.time,
|
||||
'timeSubtitle': updatedTest.timeSubtitle,
|
||||
} : {
|
||||
'id': testId,
|
||||
'name': name,
|
||||
'color': color,
|
||||
'cover': cover,
|
||||
'version': version,
|
||||
'time': time,
|
||||
'timeSubtitle': timeSubtitle,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create new test
|
||||
final newTest = await _db.testDao.createTest(
|
||||
TestsCompanion.insert(
|
||||
name: name,
|
||||
color: color != null ? drift.Value(color) : const drift.Value.absent(),
|
||||
cover: cover != null ? drift.Value(cover) : const drift.Value.absent(),
|
||||
version: version != null ? drift.Value(version) : const drift.Value.absent(),
|
||||
time: time != null ? drift.Value(time) : const drift.Value.absent(),
|
||||
timeSubtitle: timeSubtitle != null ? drift.Value(timeSubtitle) : const drift.Value.absent(),
|
||||
),
|
||||
);
|
||||
|
||||
// Add questions if provided
|
||||
if (questions != null) {
|
||||
for (final q in questions) {
|
||||
final questionJson = q as Map<String, dynamic>;
|
||||
final questionType = questionJson['questionType'] as String? ?? 'multiple_choice';
|
||||
final questionBody = Map<String, dynamic>.from(questionJson);
|
||||
questionBody.remove('questionType');
|
||||
await _db.testDao.createTestQuestion(
|
||||
TestQuestionsCompanion.insert(
|
||||
testId: newTest,
|
||||
questionType: questionType,
|
||||
body: jsonEncode(questionBody),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final createdTest = await _db.testDao.getTestById(newTest);
|
||||
return _json({
|
||||
'success': true,
|
||||
'test': createdTest != null ? {
|
||||
'id': createdTest.id,
|
||||
'name': createdTest.name,
|
||||
'color': createdTest.color,
|
||||
'cover': createdTest.cover,
|
||||
'version': createdTest.version,
|
||||
'time': createdTest.time,
|
||||
'timeSubtitle': createdTest.timeSubtitle,
|
||||
} : {
|
||||
'id': newTest,
|
||||
'name': name,
|
||||
'color': color,
|
||||
'cover': cover,
|
||||
'version': version,
|
||||
'time': time,
|
||||
'timeSubtitle': timeSubtitle,
|
||||
},
|
||||
}, statusCode: 201);
|
||||
}
|
||||
} catch (e, s) {
|
||||
print('Error in upsertTest: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to save test',
|
||||
'details': 'An unexpected error occurred while saving the test. Please try again later or contact support if the problem persists.',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// DELETE /api/v2/admin/tests/{testId}
|
||||
/// Delete a test
|
||||
@Route.delete('/admin/tests/<testId>')
|
||||
Future<Response> deleteTest(Request request, String testId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (testId.isEmpty) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Invalid test ID',
|
||||
'message': 'Test ID cannot be empty',
|
||||
'field': 'testId',
|
||||
'details': 'Please provide a valid test ID to delete the test.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
final test = await _db.testDao.getTestById(testId);
|
||||
if (test == null) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Test not found',
|
||||
'message': 'The test you are trying to delete does not exist',
|
||||
'details': 'Test with ID "$testId" was not found. It may have already been deleted.',
|
||||
},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
// Soft delete test
|
||||
await _db.testDao.softDeleteTest(testId);
|
||||
|
||||
return _json({
|
||||
'success': true,
|
||||
'message': 'Test deleted successfully',
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in deleteTest: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to delete test',
|
||||
'details': 'An unexpected error occurred while deleting the test. Please try again later or contact support if the problem persists.',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Router get router => _$AdminTestsApiV2Router(this);
|
||||
}
|
||||
16
mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.g.dart
Normal file
16
mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.g.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_tests_api_v2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ShelfRouterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Router _$AdminTestsApiV2Router(AdminTestsApiV2 service) {
|
||||
final router = Router();
|
||||
router.add('GET', r'/admin/tests', service.getAllTests);
|
||||
router.add('GET', r'/admin/tests/<testId>', service.getTest);
|
||||
router.add('POST', r'/admin/tests', service.upsertTest);
|
||||
router.add('DELETE', r'/admin/tests/<testId>', service.deleteTest);
|
||||
return router;
|
||||
}
|
||||
195
mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart
Normal file
195
mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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';
|
||||
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:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
part 'admin_users_api_v2.g.dart';
|
||||
|
||||
/// Admin endpoints for managing users in API v2.
|
||||
@injectable
|
||||
class AdminUsersApiV2 {
|
||||
final AppDatabase _db;
|
||||
|
||||
AdminUsersApiV2(this._db);
|
||||
|
||||
Response _json(
|
||||
Object? data, {
|
||||
int statusCode = 200,
|
||||
Map<String, String> headers = const {},
|
||||
}) {
|
||||
return Response(
|
||||
statusCode,
|
||||
body: data == null ? null : jsonEncode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> _ensureAdmin(Request request) async {
|
||||
try {
|
||||
await request.access!
|
||||
.requireAdmin(AdminAction.access, user: request.user);
|
||||
return Response.ok(null);
|
||||
} on AccessDenied catch (e) {
|
||||
return Response(e.status, body: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/users
|
||||
/// Get all users with pagination and search
|
||||
@Route.get('/admin/users')
|
||||
Future<Response> getAllUsers(Request request) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final queryParams = request.url.queryParameters;
|
||||
|
||||
// Parse pagination parameters
|
||||
final page = int.tryParse(queryParams['page'] ?? '1') ?? 1;
|
||||
final limit = int.tryParse(queryParams['limit'] ?? '20') ?? 20;
|
||||
final search = queryParams['search'] ?? '';
|
||||
|
||||
// Validate pagination
|
||||
if (page < 1) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'Invalid page parameter',
|
||||
'message': 'Page must be greater than 0. Received: $page',
|
||||
'field': 'page',
|
||||
'details': 'Page numbers start from 1. Please provide a valid page number.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
// Limit validation: if limit is invalid, set to default; if > 100, cap at 100
|
||||
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
|
||||
|
||||
// Get total count
|
||||
final total = await _db.userDao.countUsers(includeDeleted: false);
|
||||
|
||||
// Calculate offset
|
||||
final offset = (page - 1) * validatedLimit;
|
||||
|
||||
// Get users with pagination
|
||||
final users = await _db.userDao.getAllUsers(
|
||||
limit: validatedLimit,
|
||||
offset: offset,
|
||||
includeDeleted: false,
|
||||
);
|
||||
|
||||
// Convert to DTOs
|
||||
final userDtos = <Map<String, dynamic>>[];
|
||||
for (final user in users) {
|
||||
final userModel = await user.toUserModel();
|
||||
final dto = await userModel.toDto();
|
||||
userDtos.add(dto.toJson());
|
||||
}
|
||||
|
||||
// Apply search filter if provided
|
||||
List<Map<String, dynamic>> filteredUsers = userDtos;
|
||||
if (search.isNotEmpty) {
|
||||
final searchTerm = search.toLowerCase();
|
||||
filteredUsers = userDtos.where((user) {
|
||||
final email = user['email'] as String? ?? '';
|
||||
final name = user['name'] as String? ?? '';
|
||||
final id = user['id'] as String? ?? '';
|
||||
return email.toLowerCase().contains(searchTerm) ||
|
||||
name.toLowerCase().contains(searchTerm) ||
|
||||
id.toLowerCase().contains(searchTerm);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Recalculate total after search
|
||||
final filteredTotal = search.isNotEmpty ? filteredUsers.length : total;
|
||||
final totalPages = (filteredTotal / validatedLimit).ceil();
|
||||
|
||||
// Apply pagination to filtered results if search is active
|
||||
final paginatedUsers = search.isNotEmpty
|
||||
? filteredUsers.skip(offset).take(validatedLimit).toList()
|
||||
: filteredUsers;
|
||||
|
||||
return _json({
|
||||
'items': paginatedUsers,
|
||||
'total': filteredTotal,
|
||||
'page': page,
|
||||
'limit': validatedLimit,
|
||||
'totalPages': totalPages,
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in getAllUsers: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to retrieve users',
|
||||
'details': 'An unexpected error occurred while fetching users. Please try again later or contact support if the problem persists.',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/users/{userId}
|
||||
/// Get a specific user by ID
|
||||
@Route.get('/admin/users/<userId>')
|
||||
Future<Response> getUser(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 retrieve user details.',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
final user = await _db.userDao.getUserById(userId);
|
||||
if (user == null) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'User not found',
|
||||
'message': 'The requested user does not exist or has been deleted',
|
||||
'details': 'User with ID "$userId" was not found. Please verify the user ID and try again.',
|
||||
},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
final userModel = await user.toUserModel();
|
||||
final dto = await userModel.toDto();
|
||||
|
||||
return _json(dto.toJson());
|
||||
} catch (e, s) {
|
||||
print('Error in getUser: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to retrieve user',
|
||||
'details': 'An unexpected error occurred while fetching the user. Please try again later or contact support if the problem persists.',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Router get router => _$AdminUsersApiV2Router(this);
|
||||
}
|
||||
14
mnemo_cards_backend/lib/api/v2/admin_users_api_v2.g.dart
Normal file
14
mnemo_cards_backend/lib/api/v2/admin_users_api_v2.g.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_users_api_v2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ShelfRouterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Router _$AdminUsersApiV2Router(AdminUsersApiV2 service) {
|
||||
final router = Router();
|
||||
router.add('GET', r'/admin/users', service.getAllUsers);
|
||||
router.add('GET', r'/admin/users/<userId>', service.getUser);
|
||||
return router;
|
||||
}
|
||||
Loading…
Reference in a new issue