back
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
d122a73c1b
commit
3ead5ed139
3 changed files with 405 additions and 8 deletions
|
|
@ -6,23 +6,17 @@ import 'package:shelf/shelf.dart';
|
|||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
// import 'v2/ads_api_v2.dart'; // disabled
|
||||
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_users_api_v2.dart'; // disabled
|
||||
import 'v2/auth_api_v2.dart';
|
||||
import 'v2/discounts_api_v2.dart';
|
||||
// import 'v2/games_api_v2.dart'; // disabled
|
||||
import 'v2/packs_api_v2.dart';
|
||||
// import 'v2/purchases_api_v2.dart'; // disabled
|
||||
import 'v2/promocodes_api_v2.dart';
|
||||
import 'v2/subscriptions_api_v2.dart';
|
||||
import 'v2/tasks_api_v2.dart';
|
||||
import 'v2/tests_api_v2.dart';
|
||||
// import 'v2/users_api_v2.dart'; // disabled
|
||||
// import 'v2/telegram_bot_api_v2.dart'; // disabled
|
||||
import 'v2/authorize_v2.dart';
|
||||
import 'v2/telegram_bot_auth_middleware.dart';
|
||||
import 'v2/jwt_service.dart';
|
||||
|
|
@ -58,8 +52,6 @@ class MnemoShelf {
|
|||
v2Router.mount('/', getIt.get<AdminPacksApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<PacksApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<TestsApiV2>().router);
|
||||
// v2Router.mount('/', getIt.get<GamesApiV2>().router); // disabled
|
||||
// v2Router.mount('/', getIt.get<PurchasesApiV2>().router); // disabled
|
||||
v2Router.mount('/', getIt.get<PromocodesApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<SubscriptionsApiV2>().router);
|
||||
v2Router.mount('/', getIt.get<DiscountsApiV2>().router);
|
||||
|
|
|
|||
389
mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart
Normal file
389
mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
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/database/database.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/packs/card_pack_drift_extension.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
part 'admin_packs_api_v2.g.dart';
|
||||
|
||||
/// Admin endpoints for managing card packs in API v2.
|
||||
@injectable
|
||||
class AdminPacksApiV2 {
|
||||
final AppDatabase _db;
|
||||
|
||||
AdminPacksApiV2(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/packs
|
||||
/// Get all packs with pagination and search
|
||||
@Route.get('/admin/packs')
|
||||
Future<Response> getPacks(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'] ?? '';
|
||||
final showDisabled = queryParams['showDisabled'] == 'true';
|
||||
|
||||
// Validate pagination
|
||||
if (page < 1) {
|
||||
return _json(
|
||||
{'error': 'Page must be greater than 0'},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
if (limit < 1 || limit > 100) {
|
||||
return _json(
|
||||
{'error': 'Limit must be between 1 and 100'},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
// Get all packs
|
||||
final allPacks = await _db.packDao.getAllPacks(
|
||||
enabledOnly: !showDisabled,
|
||||
orderByField: 'order',
|
||||
);
|
||||
|
||||
// Apply search filter if provided
|
||||
List<CardPack> filteredPacks = allPacks;
|
||||
if (search.isNotEmpty) {
|
||||
final searchTerm = search.toLowerCase();
|
||||
filteredPacks = allPacks.where((pack) {
|
||||
return pack.title.toLowerCase().contains(searchTerm) ||
|
||||
pack.subtitle.toLowerCase().contains(searchTerm) ||
|
||||
pack.id.toLowerCase().contains(searchTerm);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Convert to preview DTOs
|
||||
final previewDtos = await Future.wait(
|
||||
filteredPacks.map((pack) async {
|
||||
final dto = await pack.toPreviewDto(null);
|
||||
return {
|
||||
'id': dto.id,
|
||||
'title': dto.title,
|
||||
'subtitle': dto.subtitle,
|
||||
'color': dto.color,
|
||||
'cover': dto.imageBase64,
|
||||
'cards': dto.cards,
|
||||
'enabled': pack.enabled,
|
||||
'order': pack.order,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Calculate pagination
|
||||
final total = previewDtos.length;
|
||||
final totalPages = (total / limit).ceil();
|
||||
final offset = (page - 1) * limit;
|
||||
final paginatedPacks = previewDtos.skip(offset).take(limit).toList();
|
||||
|
||||
return _json({
|
||||
'items': paginatedPacks,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'totalPages': totalPages,
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in getPacks: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to fetch packs',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/admin/packs/{packId}
|
||||
/// Get pack details by ID for editing
|
||||
@Route.get('/admin/packs/<packId>')
|
||||
Future<Response> getPack(Request request, String packId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
if (pack == null) {
|
||||
return _json(
|
||||
{'error': 'Pack not found'},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
final cards = await _db.packDao.getPackCards(packId);
|
||||
|
||||
// Get cards order - cards are already sorted by order from getPackCards
|
||||
final cardsOrder = cards.map((c) => c.id).toList();
|
||||
|
||||
// Get preview cards
|
||||
final previewCards = await _db.packDao.getPreviewCards(packId);
|
||||
final previewCardIds = previewCards.map((c) => c.id).toList();
|
||||
|
||||
// Create EditCardPackDto
|
||||
final editDto = EditCardPackDto(
|
||||
id: pack.id,
|
||||
title: pack.title,
|
||||
subtitle: pack.subtitle,
|
||||
color: pack.color,
|
||||
cover: pack.cover,
|
||||
size: pack.size,
|
||||
googlePlayId: pack.googlePlayId,
|
||||
rustoreId: pack.rustoreId,
|
||||
appStoreId: pack.appStoreId,
|
||||
price: pack.price,
|
||||
description: pack.description,
|
||||
enabled: pack.enabled,
|
||||
version: pack.version,
|
||||
order: pack.order,
|
||||
addCardIds: cards.map((c) => c.id).toList(),
|
||||
addTestIds: null,
|
||||
removeCardIds: null,
|
||||
removeTestIds: null,
|
||||
cardsOrder: cardsOrder,
|
||||
previewCards: previewCardIds,
|
||||
);
|
||||
|
||||
return _json(editDto.toJson());
|
||||
} catch (e, s) {
|
||||
print('Error in getPack: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to fetch pack',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/v2/admin/packs
|
||||
/// Create or update pack
|
||||
@Route.post('/admin/packs')
|
||||
Future<Response> upsertPack(Request request) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final body = await request.readAsString();
|
||||
final data = json.decode(body) as Map<String, dynamic>;
|
||||
|
||||
final editDto = EditCardPackDto.fromJson(data);
|
||||
|
||||
String packId;
|
||||
if (editDto.id != null) {
|
||||
// Update existing pack
|
||||
packId = editDto.id!;
|
||||
final existing = await _db.packDao.getPackById(packId);
|
||||
if (existing == null) {
|
||||
return _json(
|
||||
{'error': 'Pack not found'},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
await _db.packDao.updatePackPartial(
|
||||
CardPacksCompanion(
|
||||
id: drift.Value(packId),
|
||||
title: editDto.title != null ? drift.Value(editDto.title!) : const drift.Value.absent(),
|
||||
subtitle: editDto.subtitle != null ? drift.Value(editDto.subtitle!) : const drift.Value.absent(),
|
||||
color: editDto.color != null ? drift.Value(editDto.color) : const drift.Value.absent(),
|
||||
cover: editDto.cover != null ? drift.Value(editDto.cover) : const drift.Value.absent(),
|
||||
size: editDto.size != null ? drift.Value(editDto.size!) : const drift.Value.absent(),
|
||||
googlePlayId: editDto.googlePlayId != null ? drift.Value(editDto.googlePlayId) : const drift.Value.absent(),
|
||||
rustoreId: editDto.rustoreId != null ? drift.Value(editDto.rustoreId) : const drift.Value.absent(),
|
||||
appStoreId: editDto.appStoreId != null ? drift.Value(editDto.appStoreId) : const drift.Value.absent(),
|
||||
price: editDto.price != null ? drift.Value(editDto.price) : const drift.Value.absent(),
|
||||
description: editDto.description != null ? drift.Value(editDto.description) : const drift.Value.absent(),
|
||||
enabled: editDto.enabled != null ? drift.Value(editDto.enabled!) : const drift.Value.absent(),
|
||||
version: editDto.version != null ? drift.Value(editDto.version) : const drift.Value.absent(),
|
||||
order: editDto.order != null ? drift.Value(editDto.order!) : const drift.Value.absent(),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Create new pack
|
||||
packId = await _db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: editDto.title ?? '',
|
||||
subtitle: editDto.subtitle ?? '',
|
||||
color: editDto.color != null ? drift.Value(editDto.color) : const drift.Value.absent(),
|
||||
cover: editDto.cover != null ? drift.Value(editDto.cover) : const drift.Value.absent(),
|
||||
size: editDto.size ?? 0,
|
||||
googlePlayId: editDto.googlePlayId != null ? drift.Value(editDto.googlePlayId) : const drift.Value.absent(),
|
||||
rustoreId: editDto.rustoreId != null ? drift.Value(editDto.rustoreId) : const drift.Value.absent(),
|
||||
appStoreId: editDto.appStoreId != null ? drift.Value(editDto.appStoreId) : const drift.Value.absent(),
|
||||
price: editDto.price != null ? drift.Value(editDto.price) : const drift.Value.absent(),
|
||||
description: editDto.description != null ? drift.Value(editDto.description) : const drift.Value.absent(),
|
||||
enabled: drift.Value(editDto.enabled ?? true),
|
||||
version: editDto.version != null ? drift.Value(editDto.version) : const drift.Value.absent(),
|
||||
order: drift.Value(editDto.order ?? 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle card associations if provided
|
||||
if (editDto.addCardIds != null && editDto.addCardIds!.isNotEmpty) {
|
||||
for (final cardId in editDto.addCardIds!) {
|
||||
await _db.packDao.addCardToPack(
|
||||
packId: packId,
|
||||
cardId: cardId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (editDto.removeCardIds != null && editDto.removeCardIds!.isNotEmpty) {
|
||||
for (final cardId in editDto.removeCardIds!) {
|
||||
await _db.packDao.removeCardFromPack(packId, cardId);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cards order if provided
|
||||
if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) {
|
||||
await _db.packDao.updatePackCardsOrder(
|
||||
packId,
|
||||
editDto.cardsOrder!,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle preview cards
|
||||
if (editDto.previewCards != null) {
|
||||
await _db.packDao.setPreviewCards(packId, editDto.previewCards!);
|
||||
}
|
||||
|
||||
final updatedPack = await _db.packDao.getPackById(packId);
|
||||
if (updatedPack == null) {
|
||||
return _json(
|
||||
{'error': 'Failed to retrieve updated pack'},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
|
||||
final updatedCards = await _db.packDao.getPackCards(packId);
|
||||
final cardsOrder = updatedCards.map((c) => c.id).toList();
|
||||
final previewCards = await _db.packDao.getPreviewCards(packId);
|
||||
final previewCardIds = previewCards.map((c) => c.id).toList();
|
||||
|
||||
final updatedDto = EditCardPackDto(
|
||||
id: updatedPack.id,
|
||||
title: updatedPack.title,
|
||||
subtitle: updatedPack.subtitle,
|
||||
color: updatedPack.color,
|
||||
cover: updatedPack.cover,
|
||||
size: updatedPack.size,
|
||||
googlePlayId: updatedPack.googlePlayId,
|
||||
rustoreId: updatedPack.rustoreId,
|
||||
appStoreId: updatedPack.appStoreId,
|
||||
price: updatedPack.price,
|
||||
description: updatedPack.description,
|
||||
enabled: updatedPack.enabled,
|
||||
version: updatedPack.version,
|
||||
order: updatedPack.order,
|
||||
addCardIds: updatedCards.map((c) => c.id).toList(),
|
||||
addTestIds: null,
|
||||
removeCardIds: null,
|
||||
removeTestIds: null,
|
||||
cardsOrder: cardsOrder,
|
||||
previewCards: previewCardIds,
|
||||
);
|
||||
|
||||
return _json({
|
||||
'success': true,
|
||||
'pack': updatedDto.toJson(),
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in upsertPack: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to save pack',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// DELETE /api/v2/admin/packs/{packId}
|
||||
/// Delete pack (soft delete)
|
||||
@Route.delete('/admin/packs/<packId>')
|
||||
Future<Response> deletePack(Request request, String packId) async {
|
||||
try {
|
||||
final auth = await _ensureAdmin(request);
|
||||
if (auth.statusCode != 200) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
if (pack == null) {
|
||||
return _json(
|
||||
{'error': 'Pack not found'},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
await _db.packDao.softDeletePack(packId);
|
||||
|
||||
return _json({
|
||||
'success': true,
|
||||
'message': 'Pack deleted successfully',
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('Error in deletePack: $e\n$s');
|
||||
return _json(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to delete pack',
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Router get router => _$AdminPacksApiV2Router(this);
|
||||
}
|
||||
16
mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.g.dart
Normal file
16
mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.g.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_packs_api_v2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ShelfRouterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Router _$AdminPacksApiV2Router(AdminPacksApiV2 service) {
|
||||
final router = Router();
|
||||
router.add('GET', r'/admin/packs', service.getPacks);
|
||||
router.add('GET', r'/admin/packs/<packId>', service.getPack);
|
||||
router.add('POST', r'/admin/packs', service.upsertPack);
|
||||
router.add('DELETE', r'/admin/packs/<packId>', service.deletePack);
|
||||
return router;
|
||||
}
|
||||
Loading…
Reference in a new issue