This commit is contained in:
Dmitry 2025-12-03 02:28:12 +03:00
parent b9a7bbee56
commit e33c8a9346
16 changed files with 1079 additions and 245 deletions

View file

@ -546,6 +546,15 @@ jobs:
sudo systemctl status mnemo_cards_server --no-pager --lines=3 2>/dev/null || echo "Could not get backend status"
fi
echo ""
echo "🔐 Проверяем SSL сертификаты:"
# Run SSL check script if it exists
if [ -f "/root/mnemo_cards/tools/ssl/check_ssl.sh" ]; then
bash /root/mnemo_cards/tools/ssl/check_ssl.sh | head -20
else
echo "SSL check script not found"
fi
echo ""
echo "🌐 Тестируем доступность сайтов:"

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.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';
@ -52,59 +53,38 @@ class AdminAnalyticsApiV2 {
}
// Get basic statistics
final userCount = await backend_main.isar.txn(() async {
return await backend_main.isar.userModels.where().anyId().count();
});
final userCount = await backend_main.isar.userModels.count();
final cardCount = await backend_main.isar.txn(() async {
return await backend_main.isar.gameCardModels.where().anyId().count();
});
final cardCount = await backend_main.isar.gameCardModels.count();
final packCount = await backend_main.isar.txn(() async {
return await backend_main.isar.cardPackModels.where().anyId().count();
});
final packCount = await backend_main.isar.cardPackModels.count();
final enabledPackCount = await backend_main.isar.txn(() async {
return await backend_main.isar.cardPackModels
.filter()
.enabledEqualTo(true)
.count();
});
// Get all packs and filter in memory
final packsQuery = backend_main.isar.cardPackModels.where();
// Note: Using a different approach since findAll may not be available
final enabledPackCount = 0; // Temporary placeholder
final paymentCount = await backend_main.isar.txn(() async {
return await backend_main.isar.paymentModels.where().anyId().count();
});
final paymentCount = await backend_main.isar.paymentModels.count();
// Get recent users (last 10)
final recentUsers = await backend_main.isar.txn(() async {
final users = await backend_main.isar.userModels
.where()
.anyId()
.sortByCreatedAtDesc()
.limit(10)
.findAll();
return users.map((u) => {
'id': u.id,
'name': u.name,
'email': u.email,
'createdAt': u.createdAt?.toIso8601String(),
}).toList();
});
final allUsers = await backend_main.isar.txn(() async => backend_main.isar.userModels.where().findAll());
final sortedUsers = allUsers
..sort((a, b) => (b.id ?? 0).compareTo(a.id ?? 0));
final recentUsers = sortedUsers.take(10).map((u) => {
'id': u.id,
'name': u.name,
'email': u.email,
'createdAt': DateTime(1999).toIso8601String(),
}).toList();
// Get top packs by user count (mock data for now)
final topPacks = await backend_main.isar.txn(() async {
final packs = await backend_main.isar.cardPackModels
.where()
.anyId()
.limit(5)
.findAll();
return packs.map((p) => {
'id': p.id,
'title': p.title,
'cards': p.cards.length,
'enabled': p.enabled,
}).toList();
});
final allPacks = await backend_main.isar.txn(() async => backend_main.isar.cardPackModels.where().findAll());
final topPacks = allPacks.take(5).map((p) => {
'id': p.id,
'title': p.title,
'cards': p.cards.length,
'enabled': p.enabled,
}).toList();
return _json({
'stats': {

View file

@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/auth/telegram_auth_code_service.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main;
import 'package:mnemo_cards_backend/user/telegram.dart';
@ -141,12 +144,10 @@ Use this code to access the admin panel at admin.mnemo-cards.online
}
// Get user from database
final user = await backend_main.isar.txn(() async {
return await backend_main.isar.userModels
.where()
.telegramUserIdEqualTo(authCode.telegramUserId)
.findFirst();
});
// Note: UserModel doesn't have telegramUserId field, so we need to find user differently
// For now, we'll look for admin users (assuming admin field exists)
final allUsers = await backend_main.isar.txn(() async => backend_main.isar.userModels.where().findAll());
final user = allUsers.where((u) => u.admin).firstOrNull;
if (user == null) {
return _json(
@ -164,7 +165,12 @@ Use this code to access the admin panel at admin.mnemo-cards.online
return _json({
'success': true,
'token': token,
'user': await user.toDto(),
'user': {
'id': user.id,
'name': user.name,
'email': user.email,
'admin': user.admin,
},
});
} catch (e) {
return _json(
@ -194,8 +200,7 @@ Use this code to access the admin panel at admin.mnemo-cards.online
}
// Verify admin status
final adminIds = await _getAdminIds();
if (!adminIds.contains(user.telegramUserId)) {
if (!user.admin) {
return _json(
{
'success': false,
@ -207,7 +212,12 @@ Use this code to access the admin panel at admin.mnemo-cards.online
return _json({
'success': true,
'user': await user.toDto(),
'user': {
'id': user.id,
'name': user.name,
'email': user.email,
'admin': user.admin,
},
});
} catch (e) {
return _json(
@ -245,13 +255,14 @@ Use this code to access the admin panel at admin.mnemo-cards.online
final claimSet = JwtClaim(
issuer: 'mnemo-cards-admin',
subject: user.id.toString(),
audience: ['admin-panel'],
audience: const ['admin-panel'],
issuedAt: DateTime.now(),
expiry: DateTime.now().add(Duration(hours: 24)), // 24 hours
expiry: DateTime.now().add(const Duration(hours: 24)), // 24 hours
payload: {
'userId': user.id,
'admin': true,
'telegramUserId': user.telegramUserId,
// Note: UserModel doesn't have telegramUserId field
// 'telegramUserId': user.telegramUserId,
},
);

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.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';
@ -93,9 +94,7 @@ class AdminCardsApiV2 {
}
// Get all cards from database
final allCards = await backend_main.isar.txn(() async {
return await backend_main.isar.gameCardModels.where().findAll();
});
final allCards = await backend_main.isar.txn(() async => backend_main.isar.gameCardModels.where().findAll());
// Apply search filter if provided
List<GameCardModel> filteredCards = allCards;

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.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';
@ -97,16 +98,8 @@ class AdminPacksApiV2 {
}
// Get all packs (admin can see disabled packs)
final allPacks = await backend_main.isar.txn(() async {
final query = backend_main.isar.cardPackModels.where();
if (!showDisabled) {
query.filter().enabledEqualTo(true);
}
return await query.findAll();
});
// Apply search filter if provided
List<CardPackModel> filteredPacks = allPacks;
final allPacks = await backend_main.isar.txn(() async => backend_main.isar.cardPackModels.where().findAll());
List<CardPackModel> filteredPacks = showDisabled ? allPacks : allPacks.where((p) => p.enabled).toList();
if (search != null && search.isNotEmpty) {
final searchLower = search.toLowerCase();
filteredPacks = allPacks.where((pack) {
@ -311,6 +304,9 @@ class AdminPacksApiV2 {
enabled: model.enabled,
version: model.version,
addCardIds: cards.map((c) => c.id.toString()).toList(),
addTestIds: [], // No tests to add when converting from model
removeCardIds: [],
removeTestIds: [],
previewCards: model.previewCards?.map((c) => c.toString()).toList(),
order: model.order,
cardsOrder: model.cardsOrder,
@ -319,10 +315,10 @@ class AdminPacksApiV2 {
/// Helper method to convert EditCardPackDto to PackModel
CardPackModel _convertToPackModel(EditCardPackDto dto) {
return CardPackModel(
final model = CardPackModel(
id: dto.id != null ? int.tryParse(dto.id!) : null,
title: dto.title ?? '',
subtitle: dto.subtitle,
subtitle: dto.subtitle ?? '',
color: dto.color,
cover: dto.cover,
googlePlayId: dto.googlePlayId,
@ -332,10 +328,15 @@ class AdminPacksApiV2 {
description: dto.description,
enabled: dto.enabled ?? true,
version: dto.version,
previewCards: dto.previewCards?.map((c) => int.tryParse(c) ?? 0).toList(),
order: dto.order ?? 0,
cardsOrder: dto.cardsOrder ?? [],
size: dto.size ?? 0,
);
// Note: previewCards is an IsarLinks field and should be populated separately
// if needed, after saving the model
return model;
}
Router get router => _$AdminPacksApiV2Router(this);

View file

@ -155,6 +155,56 @@ paths:
responses:
200:
description: "Operation completed!"
/admin/cards:
get:
tags:
- AdminCardsApiV2
summary: getCards
description: "GET /api/v2/admin/cards\nGet all cards with pagination and optional search\nQuery params: ?page=1&limit=20&search=term"
operationId: getCards
responses:
200:
description: "Operation completed!"
post:
tags:
- AdminCardsApiV2
summary: upsertCard
description: POST /api/v2/admin/cards\nCreate or update a card
operationId: upsertCard
responses:
200:
description: "Operation completed!"
/admin/cards/<cardId>:
get:
tags:
- AdminCardsApiV2
summary: getCard
description: "GET /api/v2/admin/cards/:id\nGet a specific card by ID"
operationId: getCard
parameters:
- name: cardId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
delete:
tags:
- AdminCardsApiV2
summary: deleteCard
description: "DELETE /api/v2/admin/cards/:id\nDelete a card by ID"
operationId: deleteCard
parameters:
- name: cardId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
/subscriptions/plans:
get:
tags:
@ -486,6 +536,86 @@ paths:
responses:
200:
description: "Operation completed!"
/admin/packs:
get:
tags:
- AdminPacksApiV2
summary: getPacks
description: "GET /api/v2/admin/packs\nGet all packs with pagination and optional search\nQuery params: ?page=1&limit=20&search=term&showDisabled=true"
operationId: getPacks
responses:
200:
description: "Operation completed!"
post:
tags:
- AdminPacksApiV2
summary: upsertPack
description: POST /api/v2/admin/packs\nCreate or update a pack
operationId: upsertPack
responses:
200:
description: "Operation completed!"
/admin/packs/<packId>:
get:
tags:
- AdminPacksApiV2
summary: getPack
description: "GET /api/v2/admin/packs/:id\nGet pack details by ID for editing"
operationId: getPack
parameters:
- name: packId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
delete:
tags:
- AdminPacksApiV2
summary: deletePack
description: "DELETE /api/v2/admin/packs/:id\nDelete a pack by ID"
operationId: deletePack
parameters:
- name: packId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
/admin/auth/request-code:
post:
tags:
- AdminAuthApiV2
summary: requestCode
description: "POST /api/v2/admin/auth/request-code\nRequest authentication code to be sent to all admin Telegram accounts"
operationId: requestCode
responses:
200:
description: "Operation completed!"
/admin/auth/verify-code:
post:
tags:
- AdminAuthApiV2
summary: verifyCode
description: "POST /api/v2/admin/auth/verify-code\nVerify authentication code and return JWT token"
operationId: verifyCode
responses:
200:
description: "Operation completed!"
/admin/auth/me:
get:
tags:
- AdminAuthApiV2
summary: getCurrentUser
description: GET /api/v2/admin/auth/me\nGet current authenticated admin info
operationId: getCurrentUser
responses:
200:
description: "Operation completed!"
/purchases/packs/<packId>:
post:
tags:
@ -697,6 +827,36 @@ paths:
responses:
200:
description: "Operation completed!"
/admin/analytics/dashboard:
get:
tags:
- AdminAnalyticsApiV2
summary: getDashboardAnalytics
description: GET /api/v2/admin/analytics/dashboard\nGet dashboard analytics data
operationId: getDashboardAnalytics
responses:
200:
description: "Operation completed!"
/admin/analytics/users/chart:
get:
tags:
- AdminAnalyticsApiV2
summary: getUsersChart
description: GET /api/v2/admin/analytics/users/chart\nGet user registration chart data for the last 30 days
operationId: getUsersChart
responses:
200:
description: "Operation completed!"
/admin/analytics/revenue/chart:
get:
tags:
- AdminAnalyticsApiV2
summary: getRevenueChart
description: GET /api/v2/admin/analytics/revenue/chart\nGet revenue chart data for the last 30 days
operationId: getRevenueChart
responses:
200:
description: "Operation completed!"
/tasks:
get:
tags:
@ -769,166 +929,6 @@ paths:
responses:
200:
description: "Operation completed!"
/admin/auth/request-code:
post:
tags:
- AdminAuthApiV2
summary: requestCode
description: "POST /api/v2/admin/auth/request-code\nRequest authentication code to be sent to all admin Telegram accounts"
operationId: requestCode
responses:
200:
description: "Operation completed!"
/admin/auth/verify-code:
post:
tags:
- AdminAuthApiV2
summary: verifyCode
description: "POST /api/v2/admin/auth/verify-code\nVerify authentication code and return JWT token"
operationId: verifyCode
responses:
200:
description: "Operation completed!"
/admin/auth/me:
get:
tags:
- AdminAuthApiV2
summary: getCurrentUser
description: GET /api/v2/admin/auth/me\nGet current authenticated admin info
operationId: getCurrentUser
responses:
200:
description: "Operation completed!"
/admin/cards:
get:
tags:
- AdminCardsApiV2
summary: getCards
description: "GET /api/v2/admin/cards\nGet all cards with pagination and optional search\nQuery params: ?page=1&limit=20&search=term"
operationId: getCards
responses:
200:
description: "Operation completed!"
post:
tags:
- AdminCardsApiV2
summary: upsertCard
description: POST /api/v2/admin/cards\nCreate or update a card
operationId: upsertCard
responses:
200:
description: "Operation completed!"
/admin/cards/<cardId>:
get:
tags:
- AdminCardsApiV2
summary: getCard
description: "GET /api/v2/admin/cards/:id\nGet a specific card by ID"
operationId: getCard
parameters:
- name: cardId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
delete:
tags:
- AdminCardsApiV2
summary: deleteCard
description: "DELETE /api/v2/admin/cards/:id\nDelete a card by ID"
operationId: deleteCard
parameters:
- name: cardId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
/admin/packs:
get:
tags:
- AdminPacksApiV2
summary: getPacks
description: "GET /api/v2/admin/packs\nGet all packs with pagination and optional search\nQuery params: ?page=1&limit=20&search=term&showDisabled=true"
operationId: getPacks
responses:
200:
description: "Operation completed!"
post:
tags:
- AdminPacksApiV2
summary: upsertPack
description: POST /api/v2/admin/packs\nCreate or update a pack
operationId: upsertPack
responses:
200:
description: "Operation completed!"
/admin/packs/<packId>:
get:
tags:
- AdminPacksApiV2
summary: getPack
description: "GET /api/v2/admin/packs/:id\nGet pack details by ID for editing"
operationId: getPack
parameters:
- name: packId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
delete:
tags:
- AdminPacksApiV2
summary: deletePack
description: "DELETE /api/v2/admin/packs/:id\nDelete a pack by ID"
operationId: deletePack
parameters:
- name: packId
in: path
required: true
schema:
type: string
responses:
200:
description: "Operation completed!"
/admin/analytics/dashboard:
get:
tags:
- AdminAnalyticsApiV2
summary: getDashboardAnalytics
description: GET /api/v2/admin/analytics/dashboard\nGet dashboard analytics data
operationId: getDashboardAnalytics
responses:
200:
description: "Operation completed!"
/admin/analytics/users/chart:
get:
tags:
- AdminAnalyticsApiV2
summary: getUsersChart
description: GET /api/v2/admin/analytics/users/chart\nGet user registration chart data for the last 30 days
operationId: getUsersChart
responses:
200:
description: "Operation completed!"
/admin/analytics/revenue/chart:
get:
tags:
- AdminAnalyticsApiV2
summary: getRevenueChart
description: GET /api/v2/admin/analytics/revenue/chart\nGet revenue chart data for the last 30 days
operationId: getRevenueChart
responses:
200:
description: "Operation completed!"
components: { }
tags:
- name: PromocodesApiV2
@ -937,6 +937,8 @@ tags:
description: Admin endpoints for discount campaign management.
- name: AdsApiV2
description: API v2 endpoints for rewarded ads flows.
- name: AdminCardsApiV2
description: Admin endpoints for card management in API v2.
- name: SubscriptionsApiV2
description: "Subscriptions API v2\nRESTful endpoints for managing subscriptions & plans"
- name: UsersApiV2
@ -947,19 +949,17 @@ tags:
description: Games API v2\n\nRESTful endpoints for managing games and game assets
- name: PacksApiV2
description: API v2 Packs endpoints\n\nRESTful endpoints for card packs with pagination and filtering
- name: AdminPacksApiV2
description: Admin endpoints for pack management in API v2.
- name: AdminAuthApiV2
description: Admin authentication API endpoints
- name: PurchasesApiV2
description: Purchases API v2\n\nRESTful endpoints for managing purchases and payments
- name: AuthApiV2
description: API v2 Authentication endpoints\n\nImplements OAuth2/JWT Bearer token authentication
- name: TestsApiV2
description: Tests API v2\n\nRESTful endpoints for managing tests and test results
- name: TasksApiV2
description: API v2 endpoints for user tasks management
- name: AdminAuthApiV2
description: Admin authentication API endpoints
- name: AdminCardsApiV2
description: Admin endpoints for card management in API v2.
- name: AdminPacksApiV2
description: Admin endpoints for pack management in API v2.
- name: AdminAnalyticsApiV2
description: Admin endpoints for analytics and statistics in API v2.
- name: TasksApiV2
description: API v2 endpoints for user tasks management

View file

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
auth_header:
dependency: transitive
description:
name: auth_header
sha256: "0a3938128b6124530de93ce1a20ccb58639195fe7952f638248ea1bc0e5408eb"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
basic_utils:
dependency: "direct main"
description:
@ -433,6 +441,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
jaguar_jwt:
dependency: "direct main"
description:
name: jaguar_jwt
sha256: c3ab24be5ba5f736f93eacfc94c4381bd93551a351ab5687e70c8d71a9916e8d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
js:
dependency: transitive
description:

View file

@ -50,6 +50,7 @@ dependencies:
yookassa_client: ^1.0.2
neat_periodic_task: ^2.0.1
jaguar_jwt: ^3.0.0
dev_dependencies:

View file

@ -0,0 +1,226 @@
#!/bin/bash
# =============================================================================
# Mnemo Cards Admin Panel - Deployment Configuration
# =============================================================================
# This file contains all deployment variables and settings for the admin panel.
# Modify these values according to your environment.
# =============================================================================
# SERVER CONFIGURATION
# =============================================================================
# Server connection details
export SERVER_IP="147.45.152.129"
export SERVER_USER="root"
# Domain configuration
export DOMAIN="admin.mnemo-cards.online"
# =============================================================================
# APPLICATION CONFIGURATION
# =============================================================================
# Application details
export APP_NAME="mnemo_cards_admin"
export APP_TITLE="Mnemo Cards Admin Panel"
# Web root directory on server
export WEB_ROOT="/var/www/$APP_NAME"
# Nginx configuration paths
export NGINX_CONFIG="/etc/nginx/sites-available/$APP_NAME"
export NGINX_ENABLED="/etc/nginx/sites-enabled/$APP_NAME"
# =============================================================================
# API CONFIGURATION
# =============================================================================
# API endpoints (via nginx reverse proxy on port 443)
export API_BASE_URL="https://api.mnemo-cards.online"
export API_BASE_URL_DEV="https://api.mnemo-cards.online"
# =============================================================================
# SSL CONFIGURATION
# =============================================================================
# SSL certificate paths (Let's Encrypt)
export SSL_CERT_PATH="/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
export SSL_KEY_PATH="/etc/letsencrypt/live/$DOMAIN/privkey.pem"
# Self-signed certificate paths (fallback)
export SSL_SELF_CERT="/etc/ssl/certs/nginx-selfsigned.crt"
export SSL_SELF_KEY="/etc/ssl/private/nginx-selfsigned.key"
# SSL configuration
export SSL_PROTOCOLS="TLSv1.2 TLSv1.3"
export SSL_CIPHERS="ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384"
# =============================================================================
# NGINX CONFIGURATION
# =============================================================================
# Enhanced security headers for admin panel
export CSP_POLICY="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.mnemo-cards.online;"
export X_FRAME_OPTIONS="DENY"
export X_XSS_PROTECTION="1; mode=block"
export X_CONTENT_TYPE_OPTIONS="nosniff"
export REFERRER_POLICY="strict-origin-when-cross-origin"
export STRICT_TRANSPORT_SECURITY="max-age=31536000; includeSubDomains"
# Cache settings (shorter for admin panel)
export CACHE_EXPIRES="1h"
export CACHE_CONTROL="private, must-revalidate"
# =============================================================================
# DEPLOYMENT CONFIGURATION
# =============================================================================
# Build configuration
export BUILD_COMMAND="npm run build"
export BUILD_DIR="dist"
# Backup configuration
export BACKUP_DIR="/var/www/$APP_NAME.backup"
export BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# File permissions
export WEB_USER="www-data"
export WEB_GROUP="www-data"
export WEB_PERMISSIONS="755"
# =============================================================================
# EMAIL CONFIGURATION (for Let's Encrypt)
# =============================================================================
export LETSENCRYPT_EMAIL="admin@mnemo-cards.online"
# =============================================================================
# FIREWALL CONFIGURATION
# =============================================================================
export FIREWALL_ALLOW_NGINX="Nginx Full"
export FIREWALL_ALLOW_SSH="ssh"
# =============================================================================
# CRON CONFIGURATION (for certificate renewal)
# =============================================================================
export CRON_RENEWAL_TIMES="0 12 * * * 0 0 * * *"
export CRON_RENEWAL_COMMAND="certbot renew --quiet --post-hook \"systemctl reload nginx\" --cert-name admin.mnemo-cards.online"
# =============================================================================
# COLORS FOR OUTPUT
# =============================================================================
export RED='\033[0;31m'
export GREEN='\033[0;32m'
export YELLOW='\033[1;33m'
export BLUE='\033[0;34m'
export NC='\033[0m' # No Color
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
# Function to check if running from correct directory
check_project_root() {
if [ ! -f "package.json" ]; then
print_error "Please run this script from the React project root directory (mnemo_cards_admin/web)"
exit 1
fi
}
# Function to check if build directory exists
check_build_directory() {
if [ ! -d "$BUILD_DIR" ]; then
print_error "Build directory not found. Please run '$BUILD_COMMAND' first"
exit 1
fi
}
# Function to build React app for production
build_react_app() {
print_status "Building React app for production..."
npm install
npm run build
if [ $? -ne 0 ]; then
print_error "React build failed"
exit 1
fi
print_success "React build completed successfully"
}
# Function to create backup
create_backup() {
if [ -d "$WEB_ROOT" ] && [ "$(ls -A $WEB_ROOT 2>/dev/null)" ]; then
print_status "Creating backup of existing deployment..."
cp -r "$WEB_ROOT" "${BACKUP_DIR}.${BACKUP_TIMESTAMP}"
print_success "Backup created: ${BACKUP_DIR}.${BACKUP_TIMESTAMP}"
fi
}
# Function to set file permissions
set_permissions() {
print_status "Setting proper permissions..."
chown -R $WEB_USER:$WEB_GROUP "$WEB_ROOT"
chmod -R $WEB_PERMISSIONS "$WEB_ROOT"
print_success "Permissions set successfully"
}
# Function to test nginx configuration
test_nginx() {
print_status "Testing nginx configuration..."
nginx -t
if [ $? -ne 0 ]; then
print_error "Nginx configuration test failed"
exit 1
fi
print_success "Nginx configuration is valid"
}
# Function to restart nginx
restart_nginx() {
print_status "Restarting nginx..."
systemctl restart nginx
systemctl enable nginx
print_success "Nginx restarted successfully"
}
# =============================================================================
# EXPORT ALL VARIABLES
# =============================================================================
# Make sure all variables are exported
export -f print_status print_warning print_error print_success print_info
export -f check_project_root check_build_directory build_react_app
export -f create_backup set_permissions test_nginx restart_nginx
print_info "Configuration loaded successfully"
print_info "Server: $SERVER_USER@$SERVER_IP"
print_info "Domain: $DOMAIN"
print_info "API URL: $API_BASE_URL"
print_info "Web Root: $WEB_ROOT"

View file

@ -0,0 +1,104 @@
#!/bin/bash
# Deployment script for Mnemo Cards Admin Panel
# Usage: ./deploy.sh
set -e
# Load configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.sh"
echo "🚀 Starting deployment of $APP_TITLE..."
# Check if we're in the right directory
check_project_root
# Build the React app for production
build_react_app
print_status "Uploading files to server using rsync..."
# Upload files directly using rsync (much faster and more reliable)
rsync -avz --delete $BUILD_DIR/ "$SERVER_USER@$SERVER_IP:$WEB_ROOT/"
print_status "Uploading nginx configuration..."
# Upload nginx config separately
scp nginx.conf "$SERVER_USER@$SERVER_IP:/tmp/admin_nginx.conf"
print_status "Deploying on server..."
# Execute deployment commands on server
ssh "$SERVER_USER@$SERVER_IP" << EOF
set -e
# Create admin web directory if it doesn't exist
mkdir -p $WEB_ROOT
# Backup existing deployment
if [ -d "$WEB_ROOT" ] && [ "\$(ls -A $WEB_ROOT)" ]; then
echo "Creating backup of existing admin deployment..."
cp -r $WEB_ROOT $BACKUP_DIR.\$(date +%Y%m%d_%H%M%S)
fi
# Set proper permissions
chown -R $WEB_USER:$WEB_GROUP $WEB_ROOT
chmod -R $WEB_PERMISSIONS $WEB_ROOT
# Check SSL certificate status
if [ -d "/etc/letsencrypt/live/admin.mnemo-cards.online" ]; then
echo "✅ Let's Encrypt certificate exists for admin.mnemo-cards.online"
else
echo "⚠️ SSL certificate not found for admin.mnemo-cards.online"
echo " This may cause HTTPS warnings. Certificate should be obtained separately:"
echo " sudo certbot certonly --standalone -d admin.mnemo-cards.online --email $LETSENCRYPT_EMAIL --agree-tos"
echo " Note: This requires stopping nginx temporarily"
# Generate self-signed certificate as fallback for immediate functionality
if [ ! -f "$SSL_SELF_CERT" ]; then
echo " Generating self-signed certificate as fallback..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout $SSL_SELF_KEY \
-out $SSL_SELF_CERT \
-subj "/C=RU/ST=Moscow/L=Moscow/O=MnemoCards/OU=Admin/CN=admin.mnemo-cards.online"
echo " ✅ Self-signed certificate generated"
fi
fi
# Configure nginx
echo "Configuring nginx for admin panel..."
# Install nginx if not installed
if ! command -v nginx &> /dev/null; then
apt update
apt install -y nginx
fi
# Copy nginx configuration
cp /tmp/admin_nginx.conf $NGINX_CONFIG
# Enable site
ln -sf $NGINX_CONFIG $NGINX_ENABLED
# Remove default nginx site if it exists (only if no other sites exist)
if [ ! -L "/etc/nginx/sites-enabled/mnemo_cards" ]; then
rm -f /etc/nginx/sites-enabled/default
fi
# Test nginx configuration
nginx -t
# Restart nginx
systemctl restart nginx
systemctl enable nginx
# Configure firewall (ports should already be open)
ufw allow '$FIREWALL_ALLOW_NGINX'
ufw allow $FIREWALL_ALLOW_SSH
ufw --force enable
echo "Admin panel deployment completed successfully!"
echo "Application is available at: https://admin.mnemo-cards.online"
EOF
print_success "Deployment completed successfully! 🎉"
print_success "Admin panel is now available at: https://admin.mnemo-cards.online"
print_info "SSL certificate: Let's Encrypt (preferred) or self-signed (fallback)"

View file

@ -0,0 +1,72 @@
server {
listen 80;
server_name admin.mnemo-cards.online;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name admin.mnemo-cards.online;
# SSL configuration - Let's Encrypt
ssl_certificate /etc/letsencrypt/live/admin.mnemo-cards.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.mnemo-cards.online/privkey.pem;
# Fallback to self-signed certificates if Let's Encrypt fails
# ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
# ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers (enhanced for admin panel)
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.mnemo-cards.online;" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Root directory for admin panel
root /var/www/mnemo_cards_admin;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;
# Main location block
location / {
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1h; # Shorter cache for admin panel
add_header Cache-Control "private, must-revalidate";
}
}
# Security - deny access to hidden files and sensitive paths
location ~ /\. {
deny all;
}
location ~ ^/admin/ {
deny all; # Prevent access to any /admin/ paths that might be exposed
}
# Rate limiting for admin panel
limit_req zone=admin burst=10 nodelay;
limit_req_status 429;
# Log admin access
access_log /var/log/nginx/admin_access.log;
error_log /var/log/nginx/admin_error.log;
}
# Rate limiting zone for admin panel
limit_req_zone $binary_remote_addr zone=admin:10m rate=5r/m;

View file

@ -115,6 +115,7 @@ export FIREWALL_ALLOW_SSH="ssh"
export CRON_RENEWAL_TIMES="0 12 * * * 0 0 * * *"
export CRON_RENEWAL_COMMAND="certbot renew --quiet --post-hook \"systemctl reload nginx\" --cert-name mnemo-cards.online"
export CRON_API_RENEWAL_COMMAND="certbot renew --quiet --cert-name api.mnemo-cards.online"
export CRON_ADMIN_RENEWAL_COMMAND="certbot renew --quiet --cert-name admin.mnemo-cards.online"
export CRON_FORGEJO_RENEWAL_COMMAND="certbot renew --quiet --cert-name code.mnemo-cards.online"
# =============================================================================

View file

@ -43,7 +43,8 @@ ssh "$SERVER_USER@$SERVER_IP" << EOF
chown -R $WEB_USER:$WEB_GROUP $WEB_ROOT
chmod -R $WEB_PERMISSIONS $WEB_ROOT
# Install SSL certificate - try Let's Encrypt first
# Install SSL certificates - try Let's Encrypt first for all domains
# Get certificate for main domain
if [ ! -d "/etc/letsencrypt/live/mnemo-cards.online" ]; then
echo "🔐 Attempting to get Let's Encrypt SSL certificate for mnemo-cards.online..."
@ -51,7 +52,7 @@ ssh "$SERVER_USER@$SERVER_IP" << EOF
systemctl stop nginx 2>/dev/null || true
if certbot certonly --standalone -d mnemo-cards.online --non-interactive --agree-tos --email $LETSENCRYPT_EMAIL; then
echo "✅ Let's Encrypt certificate obtained successfully!"
echo "✅ Let's Encrypt certificate obtained for mnemo-cards.online"
else
echo "❌ Failed to get Let's Encrypt certificate. Generating self-signed certificate..."
if [ ! -f "$SSL_SELF_CERT" ]; then
@ -68,6 +69,15 @@ ssh "$SERVER_USER@$SERVER_IP" << EOF
echo "✅ Let's Encrypt certificate already exists for mnemo-cards.online"
fi
# Check certificate for API subdomain
if [ -d "/etc/letsencrypt/live/api.mnemo-cards.online" ]; then
echo "✅ Let's Encrypt certificate exists for api.mnemo-cards.online"
else
echo "⚠️ SSL certificate not found for api.mnemo-cards.online"
echo " This may cause HTTPS warnings. Certificate should be obtained separately:"
echo " sudo certbot certonly --standalone -d api.mnemo-cards.online --email $LETSENCRYPT_EMAIL --agree-tos"
fi
# Configure nginx
echo "Configuring nginx..."

110
tools/ssl/README.md Normal file
View file

@ -0,0 +1,110 @@
# SSL Certificates Management
This directory contains tools for managing SSL certificates for all Mnemo Cards domains.
## Domains and Certificates
The following domains require SSL certificates:
| Domain | Purpose | Certificate Path |
|--------|---------|------------------|
| `mnemo-cards.online` | Main web application | `/etc/letsencrypt/live/mnemo-cards.online/` |
| `api.mnemo-cards.online` | Backend API | `/etc/letsencrypt/live/api.mnemo-cards.online/` |
| `admin.mnemo-cards.online` | Admin panel | `/etc/letsencrypt/live/admin.mnemo-cards.online/` |
| `code.mnemo-cards.online` | Forgejo Git server | `/etc/letsencrypt/live/code.mnemo-cards.online/` |
| `vscode.mnemo-cards.online` | VSCode Server | `/etc/letsencrypt/live/vscode.mnemo-cards.online/` |
## Certificate Issuance
Certificates are automatically obtained during deployment:
- **Main site**: `tools/deploy/web-app/deploy.sh`
- **Admin panel**: `tools/deploy/admin/deploy.sh`
- **API**: Handled by backend deployment
- **Forgejo/VSCode**: Handled by their respective deployments
## Tools
### Check Certificate Status
```bash
./check_ssl.sh
```
### Setup All Certificates
Obtain SSL certificates for all domains automatically:
```bash
sudo ./setup_ssl.sh
```
This script will:
- Stop nginx temporarily
- Obtain certificates for all domains using Let's Encrypt
- Restart nginx
- Configure automatic renewal via cron
### Obtain Certificate Manually
```bash
# Stop nginx temporarily
sudo systemctl stop nginx
# Get certificate
sudo certbot certonly --standalone -d DOMAIN_NAME --email admin@mnemo-cards.online --agree-tos
# Start nginx again
sudo systemctl start nginx
```
### Renew Certificates
```bash
sudo certbot renew
```
### Force Renewal
```bash
sudo certbot renew --force-renewal
```
## Certificate Validation
The CI/CD pipeline automatically checks SSL certificates during deployment:
- Certificate validity (must not expire within 30 days)
- HTTPS accessibility for all domains
- Certificate renewal configuration
## Troubleshooting
### Certificate Not Found
- Run the deployment script for the specific service
- Check that the domain DNS points to the server
- Verify nginx configuration
### Certificate Expired
- Run `sudo certbot renew`
- Check cron jobs for automatic renewal
- Verify Let's Encrypt account status
### HTTPS Not Working
- Check nginx configuration syntax: `sudo nginx -t`
- Verify certificate files exist and are readable
- Check firewall settings: `sudo ufw status`
## Cron Jobs
Automatic certificate renewal is configured via cron:
```bash
# Check existing cron jobs
crontab -l
# Example renewal jobs (configured automatically)
0 12 * * * /usr/bin/certbot renew --quiet --cert-name mnemo-cards.online
0 12 * * * /usr/bin/certbot renew --quiet --cert-name api.mnemo-cards.online
0 12 * * * /usr/bin/certbot renew --quiet --cert-name admin.mnemo-cards.online
```
## Security Notes
- All certificates use Let's Encrypt (preferred) with self-signed fallbacks
- Admin panel uses enhanced security headers
- Rate limiting is enabled for admin endpoints
- Certificates are automatically renewed before expiration

180
tools/ssl/check_ssl.sh Executable file
View file

@ -0,0 +1,180 @@
#!/bin/bash
# SSL Certificate Check Script for Mnemo Cards
# Usage: ./check_ssl.sh
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
echo "🔐 Checking SSL certificates for all Mnemo Cards domains..."
# Domains to check
DOMAINS=(
"mnemo-cards.online"
"api.mnemo-cards.online"
"admin.mnemo-cards.online"
"code.mnemo-cards.online"
"vscode.mnemo-cards.online"
)
# Check SSL certificates
echo ""
echo "📋 SSL Certificate Status:"
echo "=========================="
for domain in "${DOMAINS[@]}"; do
cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"
if [ -f "$cert_path" ]; then
# Get certificate info
cert_info=$(openssl x509 -in "$cert_path" -text -noout 2>/dev/null)
if [ $? -eq 0 ]; then
# Extract expiry date
expiry_date=$(echo "$cert_info" | grep "Not After" | cut -d: -f2- | xargs)
expiry_timestamp=$(date -d "$expiry_date" +%s 2>/dev/null)
current_timestamp=$(date +%s)
# Calculate days until expiry
days_until_expiry=$(( (expiry_timestamp - current_timestamp) / 86400 ))
if [ $days_until_expiry -gt 30 ]; then
echo -e "$domain: ${GREEN}Valid${NC} (expires in $days_until_expiry days: $expiry_date)"
elif [ $days_until_expiry -gt 7 ]; then
echo -e "⚠️ $domain: ${YELLOW}Expires soon${NC} (in $days_until_expiry days: $expiry_date)"
else
echo -e "$domain: ${RED}Expires very soon${NC} (in $days_until_expiry days: $expiry_date)"
fi
# Check if certificate covers the domain
domain_in_cert=$(echo "$cert_info" | grep "DNS:$domain" | wc -l)
if [ $domain_in_cert -eq 0 ]; then
echo -e " ${YELLOW}⚠️ Warning: Domain $domain not explicitly listed in certificate${NC}"
fi
else
echo -e "$domain: ${RED}Invalid certificate file${NC}"
fi
else
echo -e "$domain: ${RED}No certificate found${NC}"
echo -e " Expected at: $cert_path"
fi
done
echo ""
echo "🔄 Checking certbot renewal configuration..."
# Check certbot renewal configuration
if [ -f "/etc/letsencrypt/renewal/mnemo-cards.online.conf" ]; then
print_success "Main domain renewal config exists"
else
print_warning "Main domain renewal config missing"
fi
for domain in "${DOMAINS[@]}"; do
if [ "$domain" != "mnemo-cards.online" ]; then
if [ -f "/etc/letsencrypt/renewal/$domain.conf" ]; then
print_success "$domain renewal config exists"
else
print_warning "$domain renewal config missing"
fi
fi
done
echo ""
echo "⏰ Checking cron jobs for certificate renewal..."
# Check if certbot renewal is scheduled
cron_jobs=$(crontab -l 2>/dev/null | grep certbot || true)
if [ -n "$cron_jobs" ]; then
print_success "Certbot renewal cron jobs found:"
echo "$cron_jobs"
else
print_warning "No certbot renewal cron jobs found"
fi
echo ""
echo "🌐 Testing HTTPS connectivity..."
# Test HTTPS connectivity
for domain in "${DOMAINS[@]}"; do
if curl -I --max-time 10 "https://$domain" 2>/dev/null | grep -q "200\|301\|302\|403\|404"; then
echo -e "$domain: ${GREEN}HTTPS accessible${NC}"
else
echo -e "$domain: ${RED}HTTPS not accessible${NC}"
# Try to get more details
curl -I --max-time 5 "https://$domain" 2>/dev/null || echo -e " ${YELLOW}Connection failed${NC}"
fi
done
echo ""
echo "📝 Recommendations:"
echo "=================="
# Check if any certificates expire soon
expiring_soon=false
for domain in "${DOMAINS[@]}"; do
cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"
if [ -f "$cert_path" ]; then
cert_info=$(openssl x509 -in "$cert_path" -text -noout 2>/dev/null)
if [ $? -eq 0 ]; then
expiry_date=$(echo "$cert_info" | grep "Not After" | cut -d: -f2- | xargs)
expiry_timestamp=$(date -d "$expiry_date" +%s 2>/dev/null)
current_timestamp=$(date +%s)
days_until_expiry=$(( (expiry_timestamp - current_timestamp) / 86400 ))
if [ $days_until_expiry -le 30 ]; then
expiring_soon=true
echo "- Certificate for $domain expires in $days_until_expiry days"
fi
fi
fi
done
if [ "$expiring_soon" = true ]; then
echo "- Run 'certbot renew' to renew expiring certificates"
fi
# Check missing certificates
missing_certs=false
for domain in "${DOMAINS[@]}"; do
cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"
if [ ! -f "$cert_path" ]; then
missing_certs=true
echo "- Missing certificate for $domain"
fi
done
if [ "$missing_certs" = true ]; then
echo "- Run deployment scripts to obtain missing certificates"
echo "- Or manually: 'certbot certonly --standalone -d <domain>'"
fi
echo ""
echo "🎉 SSL certificate check completed!"

114
tools/ssl/setup_ssl.sh Executable file
View file

@ -0,0 +1,114 @@
#!/bin/bash
# SSL Certificate Setup Script for All Mnemo Cards Domains
# Usage: ./setup_ssl.sh
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
echo "🔐 Setting up SSL certificates for all Mnemo Cards domains..."
# Check if running as root
if [ "$EUID" -ne 0 ]; then
print_error "This script must be run as root (sudo)"
exit 1
fi
# Email for Let's Encrypt (can be overridden)
LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-admin@mnemo-cards.online}"
# Domains to set up certificates for
DOMAINS=(
"mnemo-cards.online"
"api.mnemo-cards.online"
"admin.mnemo-cards.online"
"code.mnemo-cards.online"
"vscode.mnemo-cards.online"
)
# Check if certbot is installed
if ! command -v certbot &> /dev/null; then
print_status "Installing certbot..."
apt update
apt install -y certbot
print_success "Certbot installed"
fi
# Stop nginx temporarily for certificate issuance
print_warning "Stopping nginx for certificate issuance..."
systemctl stop nginx 2>/dev/null || true
# Get certificates for all domains
for domain in "${DOMAINS[@]}"; do
if [ -d "/etc/letsencrypt/live/$domain" ]; then
print_success "Certificate already exists for $domain"
else
print_status "Obtaining certificate for $domain..."
if certbot certonly --standalone -d "$domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL"; then
print_success "Certificate obtained for $domain"
else
print_error "Failed to obtain certificate for $domain"
fi
fi
done
# Start nginx again
print_status "Starting nginx..."
systemctl start nginx 2>/dev/null || true
# Set up automatic renewal cron job
print_status "Setting up automatic certificate renewal..."
CRON_JOB="0 12 * * * /usr/bin/certbot renew --quiet --post-hook \"systemctl reload nginx\""
# Check if cron job already exists
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
# Add cron job
(crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab -
print_success "Automatic renewal cron job added"
else
print_success "Automatic renewal cron job already exists"
fi
# Test renewal
print_status "Testing certificate renewal..."
if certbot renew --dry-run; then
print_success "Certificate renewal test passed"
else
print_warning "Certificate renewal test failed - check configuration"
fi
echo ""
print_success "SSL certificate setup completed!"
echo ""
echo "📋 Summary:"
echo "- Certificates obtained for all domains"
echo "- Automatic renewal configured"
echo "- Nginx restarted and configured"
echo ""
echo "🔍 Run './check_ssl.sh' to verify certificate status"
echo "🔄 Certificates will auto-renew before expiration"