7 Commits

Author SHA1 Message Date
Donat 04e11e8120 Merge pull request 'bugos userhandling deck handling keszen van' (#106) from admin into main
Reviewed-on: #106
2025-11-26 07:26:21 +00:00
Donat Magda 9ba8c95142 save 2025-11-26 01:42:25 +01:00
Donat Magda a9546dcc63 save 2025-11-26 01:42:07 +01:00
Donat Magda d7b47f2abe save 2025-11-26 01:41:54 +01:00
Donat Magda e29216e895 Update deployment files: fix nginx proxy, update SQL schema, consolidate to single images tar 2025-11-26 00:27:14 +01:00
Donat Magda 5eb4d3eef7 Fix: Use .js extension for entities in production, fix nginx proxy path 2025-11-26 00:05:06 +01:00
Buus 66287a84c6 bugos userhandling deck handling keszen van 2025-11-25 19:45:34 +01:00
22 changed files with 1901 additions and 512 deletions
Binary file not shown.
@@ -1,6 +1,9 @@
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { join } from 'path'; import { join } from 'path';
// Use .js extension in production (compiled) and .ts in development
const ext = __filename.endsWith('.js') ? 'js' : 'ts';
export const AppDataSource = new DataSource({ export const AppDataSource = new DataSource({
type: 'postgres', type: 'postgres',
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
@@ -10,8 +13,8 @@ export const AppDataSource = new DataSource({
database: process.env.DB_NAME || 'serpentrace', database: process.env.DB_NAME || 'serpentrace',
synchronize: false, // Set to false when using migrations synchronize: false, // Set to false when using migrations
logging: process.env.NODE_ENV === 'development', logging: process.env.NODE_ENV === 'development',
entities: [join(__dirname, '../Domain/**/*Aggregate.ts')], entities: [join(__dirname, `../Domain/**/*Aggregate.${ext}`)],
migrations: [join(__dirname, './Migrations/*.ts')], migrations: [join(__dirname, `./Migrations/*.${ext}`)],
migrationsTableName: 'migrations', migrationsTableName: 'migrations',
migrationsRun: false // Let migrations run manually migrationsRun: false // Let migrations run manually
}); });
+3
View File
@@ -38,6 +38,9 @@ RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./ COPY --from=builder /app/package.json ./
# Copy assets directory for email templates and logos
COPY --from=builder /app/assets ./assets
# Create logs directory with proper permissions # Create logs directory with proper permissions
RUN mkdir -p logs && chmod 777 logs RUN mkdir -p logs && chmod 777 logs
+8 -7
View File
@@ -28,12 +28,12 @@ JWT_REFRESH_EXPIRATION=7d
# Email Configuration (SMTP) # Email Configuration (SMTP)
# CHANGE THESE: Configure your email provider # CHANGE THESE: Configure your email provider
EMAIL_HOST=smtp.yourmailprovider.com EMAIL_HOST=mail.serpentrace.hu
EMAIL_PORT=587 EMAIL_PORT=465
EMAIL_SECURE=false EMAIL_SECURE=true
EMAIL_USER=your_email@yourdomain.com EMAIL_USER=noreply@serpentrace.hu
EMAIL_PASS=your_email_password EMAIL_PASS=ZUx720ece&Cin&F{
EMAIL_FROM="SerpentRace <noreply@yourdomain.com>" EMAIL_FROM="SerpentRace <noreply@serpentrace.hu>"
# MinIO Object Storage # MinIO Object Storage
MINIO_ENDPOINT=minio MINIO_ENDPOINT=minio
@@ -45,7 +45,8 @@ MINIO_SECRET_KEY=CHANGE_THIS_MINIO_SECRET_KEY_123!
MINIO_BUCKET_NAME=serpentrace-logs MINIO_BUCKET_NAME=serpentrace-logs
# Application Settings # Application Settings
APP_BASE_URL=http://your-domain.com APP_BASE_URL=https://szesnake.ddc.sze.hu
FRONTEND_URL=https://szesnake.ddc.sze.hu
PORT=3000 PORT=3000
# Chat System Limits # Chat System Limits
@@ -24,9 +24,9 @@ if %errorlevel% neq 0 (
exit /b 1 exit /b 1
) )
REM Check if serpentRaceDocker.tar exists REM Check if serpentrace-images.tar exists
if not exist "serpentRaceDocker.tar" ( if not exist "serpentrace-images.tar" (
echo [ERROR] serpentRaceDocker.tar not found! echo [ERROR] serpentrace-images.tar not found!
echo Please ensure the tar file is in the same directory as this script. echo Please ensure the tar file is in the same directory as this script.
pause pause
exit /b 1 exit /b 1
@@ -40,8 +40,8 @@ if not exist ".env.server" (
exit /b 1 exit /b 1
) )
echo [INFO] Loading Docker images from serpentRaceDocker.tar... echo [INFO] Loading Docker images from serpentrace-images.tar...
docker load -i serpentRaceDocker.tar docker load -i serpentrace-images.tar
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
echo [ERROR] Failed to load Docker images! echo [ERROR] Failed to load Docker images!
pause pause
+5 -5
View File
@@ -20,9 +20,9 @@ if ! command -v docker-compose &> /dev/null; then
exit 1 exit 1
fi fi
# Check if serpentRaceDocker.tar exists # Check if serpentrace-images.tar exists
if [ ! -f "serpentRaceDocker.tar" ]; then if [ ! -f "serpentrace-images.tar" ]; then
echo "[ERROR] serpentRaceDocker.tar not found!" echo "[ERROR] serpentrace-images.tar not found!"
echo "Please ensure the tar file is in the same directory as this script." echo "Please ensure the tar file is in the same directory as this script."
exit 1 exit 1
fi fi
@@ -34,8 +34,8 @@ if [ ! -f ".env.server" ]; then
exit 1 exit 1
fi fi
echo "[INFO] Loading Docker images from serpentRaceDocker.tar..." echo "[INFO] Loading Docker images from serpentrace-images.tar..."
docker load -i serpentRaceDocker.tar docker load -i serpentrace-images.tar
echo "[INFO] Images loaded successfully!" echo "[INFO] Images loaded successfully!"
echo echo
+1 -1
View File
@@ -22,7 +22,7 @@ server {
# API proxy to backend # API proxy to backend
location /api/ { location /api/ {
proxy_pass http://backend:3000/; proxy_pass http://backend:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
+180 -236
View File
@@ -1,236 +1,180 @@
-- SerpentRace Database Schema -- This script was generated by the ERD tool in pgAdmin 4.
-- Generated from TypeORM Entity Aggregates -- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps.
-- This file creates the complete database schema without initial data BEGIN;
-- Enable UUID extension -- ===================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- STEP 1: Enable Required Extensions
-- ===================================================================
-- Create Users table CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE "Users" (
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), -- ===================================================================
"orgid" UUID NULL, -- STEP 2: Create Tables
"username" VARCHAR(100) UNIQUE NOT NULL, -- ===================================================================
"password" VARCHAR(255) NOT NULL,
"email" VARCHAR(255) UNIQUE NOT NULL, CREATE TABLE IF NOT EXISTS public."ChatArchives"
"fname" VARCHAR(100) NOT NULL, (
"lname" VARCHAR(100) NOT NULL, id uuid NOT NULL DEFAULT uuid_generate_v4(),
"token" VARCHAR(255) NULL, "chatId" uuid NOT NULL,
"TokenExpires" TIMESTAMP NULL, "archivedMessages" json NOT NULL,
"phone" VARCHAR(20) NULL, "archivedAt" timestamp without time zone NOT NULL,
"state" INTEGER NOT NULL DEFAULT 0, "createDate" timestamp without time zone NOT NULL DEFAULT now(),
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL,
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "chatName" character varying(255) COLLATE pg_catalog."default",
"Orglogindate" TIMESTAMP NULL "gameId" uuid,
); participants uuid[] NOT NULL,
CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id)
-- Create Organizations table );
CREATE TABLE "Organizations" (
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), CREATE TABLE IF NOT EXISTS public."Chats"
"name" VARCHAR(255) NOT NULL, (
"contactfname" VARCHAR(100) NOT NULL, id uuid NOT NULL DEFAULT uuid_generate_v4(),
"contactlname" VARCHAR(100) NOT NULL, type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying,
"contactphone" VARCHAR(20) NOT NULL, name character varying(255) COLLATE pg_catalog."default",
"contactemail" VARCHAR(255) NOT NULL, "gameId" uuid,
"state" INTEGER NOT NULL DEFAULT 0, "createdBy" uuid,
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, users uuid[] NOT NULL,
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, messages json NOT NULL DEFAULT '[]'::json,
"url" VARCHAR(500) NULL, "lastActivity" timestamp without time zone,
"userinorg" INTEGER NOT NULL DEFAULT 0, "createDate" timestamp without time zone NOT NULL DEFAULT now(),
"maxOrganizationalDecks" INTEGER NULL "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
); state integer NOT NULL DEFAULT 0,
"archiveDate" timestamp without time zone,
-- Create Decks table CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id)
CREATE TABLE "Decks" ( );
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
"name" VARCHAR(255) NOT NULL, CREATE TABLE IF NOT EXISTS public."Contacts"
"type" INTEGER NOT NULL, (
"user_id" UUID NOT NULL, id uuid NOT NULL DEFAULT uuid_generate_v4(),
"creation_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, name character varying(255) COLLATE pg_catalog."default" NOT NULL,
"cards" JSONB NOT NULL DEFAULT '[]', email character varying(255) COLLATE pg_catalog."default" NOT NULL,
"played_number" INTEGER NOT NULL DEFAULT 0, userid uuid,
"ctype" INTEGER NOT NULL DEFAULT 0, type integer NOT NULL,
"update_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, txt text COLLATE pg_catalog."default" NOT NULL,
"state" INTEGER NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
"organization_id" UUID NULL "createDate" timestamp without time zone NOT NULL DEFAULT now(),
); "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"adminResponse" text COLLATE pg_catalog."default",
-- Create Chats table "responseDate" timestamp without time zone,
CREATE TABLE "Chats" ( "respondedBy" uuid,
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id)
"type" VARCHAR(50) NOT NULL DEFAULT 'direct', );
"name" VARCHAR(255) NULL,
"gameId" UUID NULL, CREATE TABLE IF NOT EXISTS public."Decks"
"createdBy" UUID NULL, (
"users" UUID[] NOT NULL, id uuid NOT NULL DEFAULT uuid_generate_v4(),
"messages" JSONB NOT NULL DEFAULT '[]', name character varying(255) COLLATE pg_catalog."default" NOT NULL,
"lastActivity" TIMESTAMP NULL, type integer NOT NULL,
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id uuid NOT NULL,
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, creation_date timestamp without time zone NOT NULL DEFAULT now(),
"state" INTEGER NOT NULL DEFAULT 0, cards json NOT NULL,
"archiveDate" TIMESTAMP NULL played_number integer NOT NULL DEFAULT 0,
); ctype integer NOT NULL DEFAULT 0,
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
-- Create Contacts table state integer NOT NULL DEFAULT 0,
CREATE TABLE "Contacts" ( organization_id uuid,
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id)
"name" VARCHAR(255) NOT NULL, );
"email" VARCHAR(255) NOT NULL,
"userid" UUID NULL, CREATE TABLE IF NOT EXISTS public."Games"
"type" INTEGER NOT NULL, (
"txt" TEXT NOT NULL, id uuid NOT NULL DEFAULT uuid_generate_v4(),
"state" INTEGER NOT NULL DEFAULT 0, gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL,
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, maxplayers integer NOT NULL,
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, logintype integer NOT NULL DEFAULT 0,
"adminResponse" TEXT NULL, boardsize integer NOT NULL DEFAULT 50,
"responseDate" TIMESTAMP NULL, "createdBy" uuid NOT NULL,
"respondedBy" UUID NULL organizationid uuid,
); decks jsonb NOT NULL DEFAULT '[]'::jsonb,
playerids uuid[] NOT NULL DEFAULT '{}'::uuid[],
-- Create Games table "winnerId" uuid,
CREATE TABLE "Games" ( state integer NOT NULL DEFAULT 0,
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), "createDate" timestamp without time zone NOT NULL DEFAULT now(),
"gamecode" VARCHAR(10) UNIQUE NOT NULL, start_date timestamp without time zone,
"maxplayers" INTEGER NOT NULL, "finishDate" timestamp without time zone,
"logintype" INTEGER NOT NULL DEFAULT 0, "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"state" INTEGER NOT NULL DEFAULT 0, "organizationId" uuid,
"playerids" UUID[] NOT NULL DEFAULT '{}', CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id),
"decks" JSONB NOT NULL DEFAULT '[]', CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode)
"boardsize" INTEGER NOT NULL DEFAULT 50, );
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CREATE TABLE IF NOT EXISTS public."Organizations"
"finishDate" TIMESTAMP NULL, (
"winnerid" UUID NULL, id uuid NOT NULL DEFAULT uuid_generate_v4(),
"createdBy" UUID NOT NULL, name character varying(255) COLLATE pg_catalog."default" NOT NULL,
"organizationid" UUID NULL contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL,
); contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL,
contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL,
-- Add Foreign Key Constraints contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL,
ALTER TABLE "Users" state integer NOT NULL DEFAULT 0,
ADD CONSTRAINT "FK_Users_Organizations" regdate timestamp without time zone NOT NULL DEFAULT now(),
FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") ON DELETE SET NULL; "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
url character varying(500) COLLATE pg_catalog."default",
ALTER TABLE "Decks" userinorg integer NOT NULL DEFAULT 0,
ADD CONSTRAINT "FK_Decks_Users" "maxOrganizationalDecks" integer,
FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE CASCADE; CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id)
);
ALTER TABLE "Decks"
ADD CONSTRAINT "FK_Decks_Organizations" CREATE TABLE IF NOT EXISTS public."Users"
FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE SET NULL; (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
ALTER TABLE "Contacts" orgid uuid,
ADD CONSTRAINT "FK_Contacts_Users" username character varying(100) COLLATE pg_catalog."default" NOT NULL,
FOREIGN KEY ("userid") REFERENCES "Users"("id") ON DELETE SET NULL; password character varying(255) COLLATE pg_catalog."default" NOT NULL,
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
ALTER TABLE "Contacts" fname character varying(100) COLLATE pg_catalog."default" NOT NULL,
ADD CONSTRAINT "FK_Contacts_RespondedBy" lname character varying(100) COLLATE pg_catalog."default" NOT NULL,
FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") ON DELETE SET NULL; token character varying(255) COLLATE pg_catalog."default",
"TokenExpires" timestamp without time zone,
ALTER TABLE "Chats" phone character varying(20) COLLATE pg_catalog."default",
ADD CONSTRAINT "FK_Chats_CreatedBy" state integer NOT NULL DEFAULT 0,
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE SET NULL; regdate timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
ALTER TABLE "Chats" "Orglogindate" timestamp without time zone,
ADD CONSTRAINT "FK_Chats_Games" CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id),
FOREIGN KEY ("gameId") REFERENCES "Games"("id") ON DELETE SET NULL; CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email),
CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username)
ALTER TABLE "Games" );
ADD CONSTRAINT "FK_Games_CreatedBy"
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE CASCADE; CREATE TABLE IF NOT EXISTS public.migrations
(
ALTER TABLE "Games" id serial NOT NULL,
ADD CONSTRAINT "FK_Games_Organizations" "timestamp" bigint NOT NULL,
FOREIGN KEY ("organizationid") REFERENCES "Organizations"("id") ON DELETE SET NULL; name character varying COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id)
ALTER TABLE "Games" );
ADD CONSTRAINT "FK_Games_Winner"
FOREIGN KEY ("winnerid") REFERENCES "Users"("id") ON DELETE SET NULL; ALTER TABLE IF EXISTS public."Decks"
ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id)
-- Create Indexes for Performance REFERENCES public."Organizations" (id) MATCH SIMPLE
CREATE INDEX "IDX_Users_Username" ON "Users" ("username"); ON UPDATE NO ACTION
CREATE INDEX "IDX_Users_Email" ON "Users" ("email"); ON DELETE NO ACTION;
CREATE INDEX "IDX_Users_OrgId" ON "Users" ("orgid");
CREATE INDEX "IDX_Users_State" ON "Users" ("state");
ALTER TABLE IF EXISTS public."Decks"
CREATE INDEX "IDX_Organizations_Name" ON "Organizations" ("name"); ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id)
CREATE INDEX "IDX_Organizations_State" ON "Organizations" ("state"); REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION
CREATE INDEX "IDX_Decks_UserId" ON "Decks" ("user_id"); ON DELETE NO ACTION;
CREATE INDEX "IDX_Decks_Type" ON "Decks" ("type");
CREATE INDEX "IDX_Decks_CType" ON "Decks" ("ctype");
CREATE INDEX "IDX_Decks_State" ON "Decks" ("state"); ALTER TABLE IF EXISTS public."Games"
CREATE INDEX "IDX_Decks_OrganizationId" ON "Decks" ("organization_id"); ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId")
REFERENCES public."Users" (id) MATCH SIMPLE
CREATE INDEX "IDX_Chats_Type" ON "Chats" ("type"); ON UPDATE NO ACTION
CREATE INDEX "IDX_Chats_State" ON "Chats" ("state"); ON DELETE NO ACTION;
CREATE INDEX "IDX_Chats_GameId" ON "Chats" ("gameId");
CREATE INDEX "IDX_Chats_CreatedBy" ON "Chats" ("createdBy");
ALTER TABLE IF EXISTS public."Games"
CREATE INDEX "IDX_Contacts_Type" ON "Contacts" ("type"); ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId")
CREATE INDEX "IDX_Contacts_State" ON "Contacts" ("state"); REFERENCES public."Organizations" (id) MATCH SIMPLE
CREATE INDEX "IDX_Contacts_UserId" ON "Contacts" ("userid"); ON UPDATE NO ACTION
ON DELETE NO ACTION;
CREATE INDEX "IDX_Games_GameCode" ON "Games" ("gamecode");
CREATE INDEX "IDX_Games_State" ON "Games" ("state");
CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy"); ALTER TABLE IF EXISTS public."Games"
CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid"); ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy")
REFERENCES public."Users" (id) MATCH SIMPLE
-- Create update trigger for updatedate columns ON UPDATE NO ACTION
CREATE OR REPLACE FUNCTION update_updatedate_column() ON DELETE NO ACTION;
RETURNS TRIGGER AS $$
BEGIN END;
NEW.updatedate = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply update triggers
CREATE TRIGGER update_users_updatedate
BEFORE UPDATE ON "Users"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_organizations_updatedate
BEFORE UPDATE ON "Organizations"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_decks_updatedate
BEFORE UPDATE ON "Decks"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_chats_updatedate
BEFORE UPDATE ON "Chats"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_contacts_updatedate
BEFORE UPDATE ON "Contacts"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_games_updatedate
BEFORE UPDATE ON "Games"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
-- Comments for documentation
COMMENT ON TABLE "Users" IS 'User accounts with authentication and profile information';
COMMENT ON TABLE "Organizations" IS 'Organizations that can have multiple users and premium features';
COMMENT ON TABLE "Decks" IS 'Card decks for the game, can be public, private, or organizational';
COMMENT ON TABLE "Chats" IS 'Chat system supporting direct messages, groups, and game chats';
COMMENT ON TABLE "Contacts" IS 'Contact form submissions and support tickets';
COMMENT ON TABLE "Games" IS 'Game sessions with players, decks, and game state';
-- Enum value comments
COMMENT ON COLUMN "Users"."state" IS '0=REGISTERED_NOT_VERIFIED, 1=VERIFIED_REGULAR, 2=VERIFIED_PREMIUM, 3=SOFT_DELETE, 4=DEACTIVATED, 5=ADMIN';
COMMENT ON COLUMN "Organizations"."state" IS '0=REGISTERED, 1=ACTIVE, 2=SOFT_DELETE';
COMMENT ON COLUMN "Decks"."type" IS '0=LUCK, 1=JOKER, 2=QUESTION';
COMMENT ON COLUMN "Decks"."ctype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION';
COMMENT ON COLUMN "Decks"."state" IS '0=ACTIVE, 1=SOFT_DELETE';
COMMENT ON COLUMN "Chats"."type" IS 'direct, group, game';
COMMENT ON COLUMN "Chats"."state" IS '0=ACTIVE, 1=ARCHIVE, 2=SOFT_DELETE';
COMMENT ON COLUMN "Contacts"."type" IS '0=BUG, 1=PROBLEM, 2=QUESTION, 3=SALES, 4=OTHER';
COMMENT ON COLUMN "Contacts"."state" IS '0=ACTIVE, 1=RESOLVED, 2=SOFT_DELETE';
COMMENT ON COLUMN "Games"."state" IS '0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED';
COMMENT ON COLUMN "Games"."logintype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION';
-- Grant permissions for application user
-- Note: Replace 'serpentrace_app' with your actual application database user
-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO serpentrace_app;
-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO serpentrace_app;
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO serpentrace_app;
+26 -67
View File
@@ -8,31 +8,8 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000"
environment: env_file:
- NODE_ENV=production - .env.server
- PORT=3000
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=serpentrace
- DB_USERNAME=postgres
- DB_PASSWORD=${POSTGRES_PASSWORD}
- REDIS_URL=redis://redis:6379
- REDIS_HOST=redis
- REDIS_PORT=6379
- JWT_SECRET=${JWT_SECRET}
- JWT_EXPIRATION=${JWT_EXPIRATION:-24h}
- JWT_REFRESH_EXPIRATION=${JWT_REFRESH_EXPIRATION:-7d}
- MINIO_ENDPOINT=minio
- MINIO_PORT=9000
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- MINIO_USE_SSL=false
- EMAIL_HOST=${EMAIL_HOST}
- EMAIL_PORT=${EMAIL_PORT}
- EMAIL_SECURE=${EMAIL_SECURE}
- EMAIL_USER=${EMAIL_USER}
- EMAIL_PASS=${EMAIL_PASS}
- EMAIL_FROM=${EMAIL_FROM}
volumes: volumes:
- backend_logs:/app/logs - backend_logs:/app/logs
depends_on: depends_on:
@@ -44,12 +21,7 @@ services:
condition: service_healthy condition: service_healthy
networks: networks:
- serpentrace-network - serpentrace-network
healthcheck: tty: true
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend service using pre-built image # Frontend service using pre-built image
frontend: frontend:
@@ -57,8 +29,10 @@ services:
container_name: serpentrace-frontend container_name: serpentrace-frontend
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "8080:80"
- "443:443" - "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: depends_on:
- backend - backend
networks: networks:
@@ -79,7 +53,7 @@ services:
environment: environment:
POSTGRES_DB: serpentrace POSTGRES_DB: serpentrace
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: "--encoding=UTF-8" POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
@@ -101,7 +75,7 @@ services:
- "6379:6379" - "6379:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} command: redis-server --appendonly yes
networks: networks:
- serpentrace-network - serpentrace-network
healthcheck: healthcheck:
@@ -119,8 +93,8 @@ services:
- "9000:9000" - "9000:9000"
- "9001:9001" - "9001:9001"
environment: environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} MINIO_ROOT_USER: serpentrace
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} MINIO_ROOT_PASSWORD: serpentrace123!
volumes: volumes:
- minio_data:/data - minio_data:/data
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
@@ -132,45 +106,32 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
# Adminer Database Viewer
adminer:
image: adminer:latest
container_name: serpentrace-adminer
restart: unless-stopped
ports:
- "8083:8080" # Access via http://<server-ip>:8083
depends_on:
- postgres
networks:
- serpentrace-network
# Redis Commander for internal administration # Redis Commander
redis-commander: redis-commander:
image: rediscommander/redis-commander:latest image: rediscommander/redis-commander:latest
container_name: serpentrace-redis-commander-dev container_name: serpentrace-redis-commander
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8081:8081" - "8082:8081" # Access via http://<server-ip>:8082
environment: environment:
- REDIS_HOSTS=local:redis:6379 REDIS_HOSTS: local:serpentrace-redis:6379
depends_on: depends_on:
redis: - redis
condition: service_healthy
networks: networks:
- serpentrace-network - serpentrace-network
# Database administration tool for internal administration
pgadmin:
image: dpage/pgadmin4:latest
container_name: serpentrace-pgadmin
restart: unless-stopped
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False'
volumes:
- pgadmin_data:/var/lib/pgadmin
- ./deployment/pgadmin_servers_deployment.json:/pgadmin4/servers.json:ro
depends_on:
postgres:
condition: service_healthy
networks:
- serpentrace-network
volumes: volumes:
postgres_data: postgres_data:
driver: local driver: local
@@ -180,8 +141,6 @@ volumes:
driver: local driver: local
backend_logs: backend_logs:
driver: local driver: local
pgadmin_data:
driver: local
networks: networks:
serpentrace-network: serpentrace-network:
+38 -5
View File
@@ -22,7 +22,7 @@ server {
# API proxy to backend # API proxy to backend
location /api/ { location /api/ {
proxy_pass http://backend:3000/; proxy_pass http://backend:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
@@ -45,10 +45,43 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
# Static assets caching # Adminer Database Viewer proxy
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { location /adminer/ {
expires 1y; proxy_pass http://adminer:8080/;
add_header Cache-Control "public, immutable"; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redis Commander proxy
location /redis/ {
proxy_pass http://redis-commander:8081/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http://redis-commander:8081/ /redis/;
proxy_cache_bypass $http_upgrade;
}
# MinIO Console proxy
location /minio/ {
proxy_pass http://minio:9001/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http://minio:9001/ /minio/;
sub_filter '<base href="/"' '<base href="/minio/"';
sub_filter_types text/html;
proxy_cache_bypass $http_upgrade;
} }
# Health check endpoint # Health check endpoint
+179 -179
View File
@@ -1,180 +1,180 @@
-- This script was generated by the ERD tool in pgAdmin 4. -- This script was generated by the ERD tool in pgAdmin 4.
-- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps. -- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps.
BEGIN; BEGIN;
-- =================================================================== -- ===================================================================
-- STEP 1: Enable Required Extensions -- STEP 1: Enable Required Extensions
-- =================================================================== -- ===================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- =================================================================== -- ===================================================================
-- STEP 2: Create Tables -- STEP 2: Create Tables
-- =================================================================== -- ===================================================================
CREATE TABLE IF NOT EXISTS public."ChatArchives" CREATE TABLE IF NOT EXISTS public."ChatArchives"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
"chatId" uuid NOT NULL, "chatId" uuid NOT NULL,
"archivedMessages" json NOT NULL, "archivedMessages" json NOT NULL,
"archivedAt" timestamp without time zone NOT NULL, "archivedAt" timestamp without time zone NOT NULL,
"createDate" timestamp without time zone NOT NULL DEFAULT now(), "createDate" timestamp without time zone NOT NULL DEFAULT now(),
"chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL, "chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL,
"chatName" character varying(255) COLLATE pg_catalog."default", "chatName" character varying(255) COLLATE pg_catalog."default",
"gameId" uuid, "gameId" uuid,
participants uuid[] NOT NULL, participants uuid[] NOT NULL,
CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id) CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id)
); );
CREATE TABLE IF NOT EXISTS public."Chats" CREATE TABLE IF NOT EXISTS public."Chats"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying, type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying,
name character varying(255) COLLATE pg_catalog."default", name character varying(255) COLLATE pg_catalog."default",
"gameId" uuid, "gameId" uuid,
"createdBy" uuid, "createdBy" uuid,
users uuid[] NOT NULL, users uuid[] NOT NULL,
messages json NOT NULL DEFAULT '[]'::json, messages json NOT NULL DEFAULT '[]'::json,
"lastActivity" timestamp without time zone, "lastActivity" timestamp without time zone,
"createDate" timestamp without time zone NOT NULL DEFAULT now(), "createDate" timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(), "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
state integer NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
"archiveDate" timestamp without time zone, "archiveDate" timestamp without time zone,
CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id) CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id)
); );
CREATE TABLE IF NOT EXISTS public."Contacts" CREATE TABLE IF NOT EXISTS public."Contacts"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(255) COLLATE pg_catalog."default" NOT NULL, name character varying(255) COLLATE pg_catalog."default" NOT NULL,
email character varying(255) COLLATE pg_catalog."default" NOT NULL, email character varying(255) COLLATE pg_catalog."default" NOT NULL,
userid uuid, userid uuid,
type integer NOT NULL, type integer NOT NULL,
txt text COLLATE pg_catalog."default" NOT NULL, txt text COLLATE pg_catalog."default" NOT NULL,
state integer NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
"createDate" timestamp without time zone NOT NULL DEFAULT now(), "createDate" timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(), "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"adminResponse" text COLLATE pg_catalog."default", "adminResponse" text COLLATE pg_catalog."default",
"responseDate" timestamp without time zone, "responseDate" timestamp without time zone,
"respondedBy" uuid, "respondedBy" uuid,
CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id) CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id)
); );
CREATE TABLE IF NOT EXISTS public."Decks" CREATE TABLE IF NOT EXISTS public."Decks"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(255) COLLATE pg_catalog."default" NOT NULL, name character varying(255) COLLATE pg_catalog."default" NOT NULL,
type integer NOT NULL, type integer NOT NULL,
user_id uuid NOT NULL, user_id uuid NOT NULL,
creation_date timestamp without time zone NOT NULL DEFAULT now(), creation_date timestamp without time zone NOT NULL DEFAULT now(),
cards json NOT NULL, cards json NOT NULL,
played_number integer NOT NULL DEFAULT 0, played_number integer NOT NULL DEFAULT 0,
ctype integer NOT NULL DEFAULT 0, ctype integer NOT NULL DEFAULT 0,
"updateDate" timestamp without time zone NOT NULL DEFAULT now(), "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
state integer NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
organization_id uuid, organization_id uuid,
CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id) CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id)
); );
CREATE TABLE IF NOT EXISTS public."Games" CREATE TABLE IF NOT EXISTS public."Games"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL, gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL,
maxplayers integer NOT NULL, maxplayers integer NOT NULL,
logintype integer NOT NULL DEFAULT 0, logintype integer NOT NULL DEFAULT 0,
boardsize integer NOT NULL DEFAULT 50, boardsize integer NOT NULL DEFAULT 50,
"createdBy" uuid NOT NULL, "createdBy" uuid NOT NULL,
organizationid uuid, organizationid uuid,
decks jsonb NOT NULL DEFAULT '[]'::jsonb, decks jsonb NOT NULL DEFAULT '[]'::jsonb,
playerids uuid[] NOT NULL DEFAULT '{}'::uuid[], playerids uuid[] NOT NULL DEFAULT '{}'::uuid[],
"winnerId" uuid, "winnerId" uuid,
state integer NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
"createDate" timestamp without time zone NOT NULL DEFAULT now(), "createDate" timestamp without time zone NOT NULL DEFAULT now(),
start_date timestamp without time zone, start_date timestamp without time zone,
"finishDate" timestamp without time zone, "finishDate" timestamp without time zone,
"updateDate" timestamp without time zone NOT NULL DEFAULT now(), "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"organizationId" uuid, "organizationId" uuid,
CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id), CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id),
CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode) CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode)
); );
CREATE TABLE IF NOT EXISTS public."Organizations" CREATE TABLE IF NOT EXISTS public."Organizations"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(255) COLLATE pg_catalog."default" NOT NULL, name character varying(255) COLLATE pg_catalog."default" NOT NULL,
contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL, contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL,
contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL, contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL,
contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL, contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL,
contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL, contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL,
state integer NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
regdate timestamp without time zone NOT NULL DEFAULT now(), regdate timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(), "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
url character varying(500) COLLATE pg_catalog."default", url character varying(500) COLLATE pg_catalog."default",
userinorg integer NOT NULL DEFAULT 0, userinorg integer NOT NULL DEFAULT 0,
"maxOrganizationalDecks" integer, "maxOrganizationalDecks" integer,
CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id) CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id)
); );
CREATE TABLE IF NOT EXISTS public."Users" CREATE TABLE IF NOT EXISTS public."Users"
( (
id uuid NOT NULL DEFAULT uuid_generate_v4(), id uuid NOT NULL DEFAULT uuid_generate_v4(),
orgid uuid, orgid uuid,
username character varying(100) COLLATE pg_catalog."default" NOT NULL, username character varying(100) COLLATE pg_catalog."default" NOT NULL,
password character varying(255) COLLATE pg_catalog."default" NOT NULL, password character varying(255) COLLATE pg_catalog."default" NOT NULL,
email character varying(255) COLLATE pg_catalog."default" NOT NULL, email character varying(255) COLLATE pg_catalog."default" NOT NULL,
fname character varying(100) COLLATE pg_catalog."default" NOT NULL, fname character varying(100) COLLATE pg_catalog."default" NOT NULL,
lname character varying(100) COLLATE pg_catalog."default" NOT NULL, lname character varying(100) COLLATE pg_catalog."default" NOT NULL,
token character varying(255) COLLATE pg_catalog."default", token character varying(255) COLLATE pg_catalog."default",
"TokenExpires" timestamp without time zone, "TokenExpires" timestamp without time zone,
phone character varying(20) COLLATE pg_catalog."default", phone character varying(20) COLLATE pg_catalog."default",
state integer NOT NULL DEFAULT 0, state integer NOT NULL DEFAULT 0,
regdate timestamp without time zone NOT NULL DEFAULT now(), regdate timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(), "updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"Orglogindate" timestamp without time zone, "Orglogindate" timestamp without time zone,
CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id),
CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email),
CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username) CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username)
); );
CREATE TABLE IF NOT EXISTS public.migrations CREATE TABLE IF NOT EXISTS public.migrations
( (
id serial NOT NULL, id serial NOT NULL,
"timestamp" bigint NOT NULL, "timestamp" bigint NOT NULL,
name character varying COLLATE pg_catalog."default" NOT NULL, name character varying COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id) CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id)
); );
ALTER TABLE IF EXISTS public."Decks" ALTER TABLE IF EXISTS public."Decks"
ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id) ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id)
REFERENCES public."Organizations" (id) MATCH SIMPLE REFERENCES public."Organizations" (id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE NO ACTION; ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Decks" ALTER TABLE IF EXISTS public."Decks"
ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id) ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id)
REFERENCES public."Users" (id) MATCH SIMPLE REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE NO ACTION; ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Games" ALTER TABLE IF EXISTS public."Games"
ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId") ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId")
REFERENCES public."Users" (id) MATCH SIMPLE REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE NO ACTION; ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Games" ALTER TABLE IF EXISTS public."Games"
ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId") ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId")
REFERENCES public."Organizations" (id) MATCH SIMPLE REFERENCES public."Organizations" (id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE NO ACTION; ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Games" ALTER TABLE IF EXISTS public."Games"
ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy") ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy")
REFERENCES public."Users" (id) MATCH SIMPLE REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE NO ACTION; ON DELETE NO ACTION;
END; END;
+1 -1
View File
@@ -22,7 +22,7 @@ server {
# API proxy to backend # API proxy to backend
location /api/ { location /api/ {
proxy_pass http://backend:3000/; proxy_pass http://backend:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
+4
View File
@@ -25,6 +25,7 @@ import ChooseDeck from "./pages/Game/ChooseDeck"
import PlayerSetup from "./pages/Game/PlayerSetup" import PlayerSetup from "./pages/Game/PlayerSetup"
import GameModalsDemo from "./pages/Game/GameModalsDemo" import GameModalsDemo from "./pages/Game/GameModalsDemo"
import { GameWebSocketProvider } from "./contexts/GameWebSocketContext" import { GameWebSocketProvider } from "./contexts/GameWebSocketContext"
import Admin from "./pages/Admin/Admin"
function App() { function App() {
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
@@ -78,6 +79,9 @@ function App() {
<Route path={ROUTES.REPORTS} element={<Reports />} /> <Route path={ROUTES.REPORTS} element={<Reports />} />
<Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} /> <Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} />
<Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} /> <Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} />
<Route path={ROUTES.ADMIN} element={<Admin />} />
</Routes> </Routes>
</Router> </Router>
</GameWebSocketProvider> </GameWebSocketProvider>
+173
View File
@@ -0,0 +1,173 @@
// ...new file...
import { apiClient } from "./userApi"
/**
* Ellenőrzi localStorage.authLevel === "1". Ha nem, redirect a megadott útvonalra.
* Visszatér: true (admin) | false (nem admin, redirect történt)
*/
export function ensureAdminOrRedirect(redirectPath = "/") {
try {
const level = localStorage.getItem("authLevel")
const isAdmin = String(level) === "1"
if (!isAdmin) {
window.location.replace(redirectPath)
return false
}
return true
} catch (err) {
window.location.replace(redirectPath)
return false
}
}
/**
* Admin: paginált paklik lekérése (admin endpoint)
* Visszatér: tömb of decks (raw vagy {decks: []} formátum kezelve)
*/
export async function adminListDecks(from = 0, to = 99) {
if (!ensureAdminOrRedirect("/")) return []
try {
const resp = await apiClient.get(`/admin/decks/page/${from}/${to}`)
const data = resp?.data
return data?.decks || data || []
} catch (err) {
console.error("adminApi.adminListDecks error:", err)
throw err
}
}
/**
* Admin: keresés admin végponton.
* Ha nincs külön kereső végpont, a backend/resp lehet tömb vagy {decks: []}
* Visszatér: tömb of decks
*/
export async function adminSearchDecks(query = "", limit = 100, offset = 0) {
if (!ensureAdminOrRedirect("/")) return []
try {
if (!query) {
// fallback: list page
return await adminListDecks(offset, offset + (limit - 1))
}
// Preferált: admin search endpoint /admin/decks/search/{term}
const resp = await apiClient.get(`/admin/decks/search/${encodeURIComponent(query)}`, {
params: { limit, offset }
})
const data = resp?.data
return data?.decks || data || []
} catch (err) {
console.error("adminApi.adminSearchDecks error:", err)
throw err
}
}
/**
* Admin: paginált felhasználók lekérése
* Visszatér: { users: [], pagination: {...} } vagy tömb of users
*/
export async function adminListUsers(from = 0, to = 99, includeDeleted = false) {
if (!ensureAdminOrRedirect("/")) return { users: [], pagination: { from, to, returned: 0, totalCount: 0, includeDeleted } }
try {
const resp = await apiClient.get(`/admin/users/page/${from}/${to}`, {
params: { includeDeleted }
})
return resp?.data || { users: [] }
} catch (err) {
console.error("adminApi.adminListUsers error:", err)
throw err
}
}
/**
* Admin: Get user by id
*/
export async function adminGetUserById(userId, includeDeleted = false) {
if (!ensureAdminOrRedirect("/")) return null
try {
const resp = await apiClient.get(`/admin/users/${userId}`, { params: { includeDeleted } })
return resp?.data || null
} catch (err) {
console.error("adminApi.adminGetUserById error:", err)
throw err
}
}
/**
* Admin: Search users
*/
export async function adminSearchUsers(query = "", includeDeleted = false, limit = 100, offset = 0) {
if (!ensureAdminOrRedirect("/")) return []
try {
if (!query) {
// fallback: list page
const to = offset + (limit - 1)
const res = await adminListUsers(offset, to, includeDeleted)
// adminListUsers may return object or array
if (Array.isArray(res)) return res
return res.users || []
}
const resp = await apiClient.get(`/admin/users/search/${encodeURIComponent(query)}`, {
params: { includeDeleted, limit, offset }
})
const data = resp?.data
// prefer array but handle wrapped
return data?.users || data || []
} catch (err) {
console.error("adminApi.adminSearchUsers error:", err)
throw err
}
}
/**
* Admin: Update user
*/
export async function adminUpdateUser(userId, payload = {}) {
if (!ensureAdminOrRedirect("/")) return null
try {
const resp = await apiClient.patch(`/admin/users/${userId}`, payload)
return resp?.data || null
} catch (err) {
console.error("adminApi.adminUpdateUser error:", err)
throw err
}
}
/**
* Admin: Deactivate user
*/
export async function adminDeactivateUser(userId) {
if (!ensureAdminOrRedirect("/")) return null
try {
const resp = await apiClient.post(`/admin/users/${userId}/deactivate`)
return resp?.data || null
} catch (err) {
console.error("adminApi.adminDeactivateUser error:", err)
throw err
}
}
/**
* Admin: Delete user
*/
export async function adminDeleteUser(userId) {
if (!ensureAdminOrRedirect("/")) return null
try {
const resp = await apiClient.delete(`/admin/users/${userId}`)
return resp?.data || null
} catch (err) {
console.error("adminApi.adminDeleteUser error:", err)
throw err
}
}
export default {
ensureAdminOrRedirect,
adminListDecks,
adminSearchDecks,
// new user API exports
adminListUsers,
adminGetUserById,
adminSearchUsers,
adminUpdateUser,
adminDeactivateUser,
adminDeleteUser,
}
+56
View File
@@ -107,3 +107,59 @@ export const resetPassword = async (token, newPassword) => {
} }
}; };
// Add: response interceptor + error parser
apiClient.interceptors.response.use(
(resp) => resp,
(error) => {
// Log useful debug info
try {
// eslint-disable-next-line no-console
console.error("API Error:", {
message: error.message,
status: error?.response?.status,
url: error?.config?.url,
data: error?.response?.data,
})
} catch (e) {}
// forward
return Promise.reject(error)
}
)
/**
* Normalize axios error into readable string
* Prefer backend-provided message (resp.data.message), fallback to resp.data or error.message.
*/
export function getApiErrorMessage(error) {
if (!error) return "Ismeretlen hiba"
// axios error with response
const resp = error.response
if (resp) {
const data = resp.data
if (data) {
if (typeof data === "string") return data
if (data.message) return String(data.message)
// sometimes backend sends { errors: [...] }
if (data.errors && Array.isArray(data.errors)) return data.errors.map(e => e.message || JSON.stringify(e)).join("; ")
// fallback to whole payload
try { return JSON.stringify(data) } catch (e) {}
}
return `HTTP ${resp.status} ${resp.statusText || ""}`.trim()
}
// non-response axios error
return error.message || String(error)
}
export default {
login,
register,
getUserStats,
verifyEmail,
getUserProfile,
updateUserProfile,
deleteUserProfile,
forgotPassword,
resetPassword,
apiClient,
}
@@ -2,7 +2,7 @@ import React, { useState } from "react"
import Logo from "../../assets/pictures/Logo" import Logo from "../../assets/pictures/Logo"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // ✅ importáld a navigációs hookot import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // ✅ importáld a navigációs hookot
import { FaSignOutAlt, FaChartBar, FaUser, FaBars } from "react-icons/fa" import { FaSignOutAlt, FaChartBar, FaUser, FaBars, FaUserShield } from "react-icons/fa"
const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10" const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
const navLinkClassPlay = const navLinkClassPlay =
@@ -13,6 +13,9 @@ const Navbar = () => {
const [userMenuOpen, setUserMenuOpen] = useState(false) const [userMenuOpen, setUserMenuOpen] = useState(false)
const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username")) const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username"))
// Új: ellenőrizzük, hogy admin-e (authLevel === "1")
const isAdmin = localStorage.getItem("authLevel") === "1"
// ✅ Használjuk a HandleNavigate hookot // ✅ Használjuk a HandleNavigate hookot
const { goLanding, goAbout, goHome, goLogin, goContacts } = HandleNavigate() const { goLanding, goAbout, goHome, goLogin, goContacts } = HandleNavigate()
@@ -111,6 +114,21 @@ const Navbar = () => {
<FaChartBar className="w-4 h-4" /> <FaChartBar className="w-4 h-4" />
<span className="text-sm">Statisztikák</span> <span className="text-sm">Statisztikák</span>
</button> </button>
{/* Admin gomb a felhasználói lenyílóban, csak adminoknak */}
{isAdmin && (
<button
onClick={() => {
setUserMenuOpen(false)
window.location.href = "/admin"
}}
className="w-full flex items-center gap-3 px-4 py-2 hover:bg-white/10 text-white"
>
<FaUserShield className="w-4 h-4" />
<span className="text-sm">Admin</span>
</button>
)}
<button <button
onClick={() => { onClick={() => {
setUserMenuOpen(false) setUserMenuOpen(false)
@@ -173,11 +191,11 @@ const Navbar = () => {
{menuOpen && ( {menuOpen && (
<> <>
{/* Overlay when menu is open */} {/* Overlay when menu is open */}
<div <div
className="lg:hidden fixed inset-0 bg-black/50 z-40 top-16" className="lg:hidden fixed inset-0 bg-black/50 z-40 top-16"
onClick={() => setMenuOpen(false)} onClick={() => setMenuOpen(false)}
/> />
{/* Side Menu */} {/* Side Menu */}
<div className="lg:hidden fixed top-16 right-0 w-64 bg-emerald-600 shadow-2xl z-50 animate-slide-down rounded-bl-2xl"> <div className="lg:hidden fixed top-16 right-0 w-64 bg-emerald-600 shadow-2xl z-50 animate-slide-down rounded-bl-2xl">
<div className="px-4 py-4 space-y-2 flex flex-col"> <div className="px-4 py-4 space-y-2 flex flex-col">
@@ -201,11 +219,26 @@ const Navbar = () => {
</button> </button>
{isLoggedIn && ( {isLoggedIn && (
<Link to="/decks" onClick={() => setMenuOpen(false)} className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center"> <Link
to="/decks"
onClick={() => setMenuOpen(false)}
className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center"
>
Paklik Paklik
</Link> </Link>
)} )}
{/* Admin gomb csak authLevel === "1" esetén */}
{isAdmin && (
<Link
to="/admin"
onClick={() => setMenuOpen(false)}
className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center"
>
Admin
</Link>
)}
<button <button
onClick={() => { onClick={() => {
goHome() goHome()
@@ -0,0 +1,750 @@
import React, { useState, useEffect } from "react"
import Navbar from "../../components/Navbar/Navbar.jsx"
import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import ButtonGreen from "../../components/Buttons/ButtonGreen.jsx"
import { FaLayerGroup, FaUser, FaFilter, FaCalendarAlt, FaArrowUp, FaArrowDown, FaSortAlphaDown, FaSortAlphaUp, FaQuestionCircle, FaPlus, FaChevronLeft, FaChevronRight } from "react-icons/fa"
import { motion } from "framer-motion"
import {
ensureAdminOrRedirect,
adminListDecks,
adminSearchDecks,
adminListUsers,
adminGetUserById,
adminSearchUsers,
adminUpdateUser,
adminDeactivateUser,
adminDeleteUser
} from "../../api/adminApi"
import { getApiErrorMessage } from "../../api/userApi"
// --- Új importok: navigáció + SearchBox + PopUp + DeckInfoPopUp + deckApi használat ---
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate"
import SearchBox from "../../components/Search/SearchBox"
import PopUp from "../../components/PopUp/PopUp"
import DeckInfoPopUp from "../../components/PopUp/DeckInfoPopUp"
export default function Admin() {
const [active, setActive] = useState("decks") // "decks" | "users"
// --- Új: navigáció hook ---
const { goDeckCreator } = HandleNavigate()
// --- Szűrés / lista állapotok (DeckManager-ről átemelve) ---
const deckTypes = [
{ label: "Luck", color: "var(--color-luck)" },
{ label: "Question", color: "var(--color-question)" },
{ label: "Joker", color: "var(--color-fun)" },
]
const origins = ["Mind", "Vállalati", "Saját"]
const [query, setQuery] = useState("")
const [allDecks, setAllDecks] = useState([]) // összes betöltött pakli (backend)
const [loading, setLoading] = useState(false)
const [selectedType, setSelectedType] = useState("All")
const [selectedOrigin, setSelectedOrigin] = useState("Mind")
const [sortBy, setSortBy] = useState("date-desc")
const [showSortHelp, setShowSortHelp] = useState(false)
const [selectedDeck, setSelectedDeck] = useState(null)
const [itemsPerPage, setItemsPerPage] = useState(20)
const [currentPage, setCurrentPage] = useState(1)
// --- User management states ---
const [users, setUsers] = useState([])
const [userQuery, setUserQuery] = useState("")
const [userLoading, setUserLoading] = useState(false)
const [selectedUser, setSelectedUser] = useState(null)
const [userItemsPerPage, setUserItemsPerPage] = useState(20)
const [userCurrentPage, setUserCurrentPage] = useState(1)
const [userTotalCount, setUserTotalCount] = useState(0)
const [includeDeletedUsers, setIncludeDeletedUsers] = useState(false)
// track latest search to avoid race
const [userSearchToken, setUserSearchToken] = useState(0)
useEffect(() => {
ensureAdminOrRedirect("/")
let mounted = true
const load = async () => {
setLoading(true)
try {
const result = await adminListDecks(0, 99)
if (!mounted) return
// admin endpoints may already return array of deck objects or {decks:[...]}
const rawDecks = Array.isArray(result) ? result : (result.decks || result)
const mapped = (rawDecks || []).map(d => ({
id: d.id,
name: d.name || "",
type: d.type === 2 ? "Question" : d.type === 1 ? "Joker" : "Luck",
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : "",
origin: d.ctype === 2 ? "Vállalati" : d.ctype === 0 ? "Mind" : "Saját",
raw: d
}))
setAllDecks(mapped)
} catch (err) {
console.error("Admin: failed to load decks (admin API)", err)
setAllDecks([])
} finally {
if (mounted) setLoading(false)
}
}
load()
return () => { mounted = false }
}, [])
// Debounced search using adminSearchDecks
useEffect(() => {
const t = setTimeout(async () => {
setLoading(true)
try {
if (!query) {
// if empty, reload full admin list (or keep cached allDecks)
const res = await adminListDecks(0, 99)
const raw = Array.isArray(res) ? res : (res.decks || res)
const mapped = (raw || []).map(d => ({
id: d.id,
name: d.name || "",
type: d.type === 2 ? "Question" : d.type === 1 ? "Joker" : "Luck",
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : "",
origin: d.ctype === 2 ? "Vállalati" : d.ctype === 0 ? "Mind" : "Saját",
raw: d
}))
setAllDecks(mapped)
} else {
const res = await adminSearchDecks(query, 100, 0)
const raw = Array.isArray(res) ? res : (res.decks || res)
const mapped = (raw || []).map(d => ({
id: d.id,
name: d.name || "",
type: d.type === 2 ? "Question" : d.type === 1 ? "Joker" : "Luck",
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : "",
origin: d.ctype === 2 ? "Vállalati" : d.ctype === 0 ? "Mind" : "Saját",
raw: d
}))
setAllDecks(mapped)
}
} catch (err) {
console.error("Admin search error (admin API):", err)
setAllDecks([])
} finally {
setLoading(false)
}
}, 300)
return () => clearTimeout(t)
}, [query])
// Load users when admin panel opened or includeDeleted changes
useEffect(() => {
let mounted = true
const loadUsers = async () => {
if (active !== "users") return
setUserLoading(true)
try {
const res = await adminListUsers(0, 99, includeDeletedUsers)
// res may be { users: [], pagination: {} } or array
const raw = Array.isArray(res) ? res : (res.users || [])
if (!mounted) return
setUsers((raw || []).map(u => ({
id: u.id,
username: u.username || u.name || "",
email: u.email || "",
created: u.creationdate ? new Date(u.creationdate).toLocaleDateString() : "",
deleted: !!u.deleted,
raw: u
})))
// set total if pagination provided
if (!Array.isArray(res) && res.pagination && typeof res.pagination.totalCount === "number") {
setUserTotalCount(res.pagination.totalCount)
} else {
setUserTotalCount((raw || []).length)
}
} catch (err) {
console.error("Admin: failed to load users (admin API)", err)
setUsers([])
setUserTotalCount(0)
} finally {
if (mounted) setUserLoading(false)
}
}
loadUsers()
return () => { mounted = false }
}, [active, includeDeletedUsers])
// Debounced user search
useEffect(() => {
const token = userSearchToken + 1
setUserSearchToken(token)
const t = setTimeout(async () => {
if (active !== "users") return
setUserLoading(true)
try {
if (!userQuery) {
const res = await adminListUsers(0, 99, includeDeletedUsers)
const raw = Array.isArray(res) ? res : (res.users || [])
setUsers((raw || []).map(u => ({
id: u.id,
username: u.username || u.name || "",
email: u.email || "",
created: u.creationdate ? new Date(u.creationdate).toLocaleDateString() : "",
deleted: !!u.deleted,
raw: u
})))
setUserTotalCount(!Array.isArray(res) && res.pagination ? res.pagination.totalCount : (raw || []).length)
} else {
const res = await adminSearchUsers(userQuery, includeDeletedUsers, 100, 0)
const raw = Array.isArray(res) ? res : (res.users || res)
setUsers((raw || []).map(u => ({
id: u.id,
username: u.username || u.name || "",
email: u.email || "",
created: u.creationdate ? new Date(u.creationdate).toLocaleDateString() : "",
deleted: !!u.deleted,
raw: u
})))
setUserTotalCount((raw || []).length)
}
} catch (err) {
console.error("Admin: user search error", err)
setUsers([])
setUserTotalCount(0)
} finally {
setUserLoading(false)
}
}, 300)
return () => clearTimeout(t)
}, [userQuery, active, includeDeletedUsers])
// Filter logic
let filteredDecks = allDecks.filter((deck) => {
const typeMatch = selectedType === "All" || deck.type === selectedType
const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin
const searchMatch = !query || deck.name.toLowerCase().includes(query.toLowerCase())
return typeMatch && originMatch && searchMatch
})
// Sort logic
filteredDecks = [...filteredDecks].sort((a, b) => {
if (sortBy === "date-asc") {
return a.created.localeCompare(b.created)
} else if (sortBy === "date-desc") {
return b.created.localeCompare(a.created)
} else if (sortBy === "abc-asc") {
return a.name.localeCompare(b.name)
} else if (sortBy === "abc-desc") {
return b.name.localeCompare(a.name)
}
return 0
})
// Pagination logic
const totalDecks = filteredDecks.length
const totalPages = Math.max(1, Math.ceil(totalDecks / itemsPerPage))
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const paginatedDecks = filteredDecks.slice(startIndex, endIndex)
// Reset to page 1 when filters change
useEffect(() => {
setCurrentPage(1)
}, [selectedType, selectedOrigin, query, sortBy, itemsPerPage])
const NavButton = ({ id, icon: Icon, label }) => (
<button
onClick={() => setActive(id)}
aria-pressed={active === id}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors text-sm font-medium
${active === id ? "bg-emerald-500 text-white shadow-lg" : "text-gray-200 hover:bg-white/5"}`}
>
<Icon className="w-5 h-5" />
<span>{label}</span>
</button>
)
// --- Teljes Deck handling (DeckManager-ről áthozva) ---
const DeckHandling = () => (
<motion.div
key="decks"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.25 }}
className="space-y-4"
>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold text-white">Deck management</h2>
<ButtonGreen text="Create deck" onClick={goDeckCreator} width="w-36" />
</div>
<div className="mt-4">
<div className="flex gap-2 items-center w-full flex-wrap mb-4">
<SearchBox
value={query}
onChange={(e) => setQuery(e.target.value)}
width={240}
placeholder="Keresés..."
className="mr-4"
/>
<FaFilter style={{ color: "var(--color-success)" }} className="mr-1 sm:mr-2 text-sm sm:text-base" />
<span className="text-[color:var(--color-text)] font-semibold mr-1 sm:mr-2 text-xs sm:text-sm">Típus:</span>
<button
className={`px-2 sm:px-3 py-1 rounded-lg font-medium transition-all duration-200 text-xs sm:text-sm ${
selectedType === "All"
? "bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] border border-[color:var(--color-surface)]"
: "text-[color:var(--color-text)] bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30"
}`}
onClick={() => setSelectedType("All")}
>
Mind
</button>
{deckTypes.map((type) => (
<button
key={type.label}
className={`px-2 sm:px-3 py-1 rounded-lg font-medium transition-all duration-200 ml-1 text-xs sm:text-sm ${
selectedType === type.label
? "text-[color:var(--color-text-inverse)] border border-[color:var(--color-surface)]"
: "text-[color:var(--color-text)] bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30"
}`}
style={selectedType === type.label ? { background: type.color } : undefined}
onClick={() => setSelectedType(type.label)}
>
{type.label === "Luck"
? "Szerencse"
: type.label === "Question"
? "Kérdés"
: type.label === "Joker"
? "Joker"
: type.label}
</button>
))}
<span className="text-[color:var(--color-text)] font-semibold mr-1 sm:mr-2 ml-1 sm:ml-2 text-xs sm:text-sm">Eredet:</span>
<select
className="px-2 sm:px-3 py-1 rounded-lg bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30 text-[color:var(--color-text)] border-none focus:ring-2 focus:ring-[color:var(--color-success)] outline-none text-xs sm:text-sm"
value={selectedOrigin}
onChange={(e) => setSelectedOrigin(e.target.value)}
>
{origins.map((origin) => (
<option key={origin} value={origin} className="bg-zinc-800 text-white">
{origin}
</option>
))}
</select>
<span className="text-[color:var(--color-text)] font-semibold mr-1 sm:mr-2 ml-1 sm:ml-2 flex items-center gap-1 text-xs sm:text-sm">
Rendezés:
<button
type="button"
className="ml-1 text-[color:var(--color-success)] hover:text-[color:var(--color-text)] focus:outline-none"
onClick={() => setShowSortHelp(true)}
aria-label="Rendezési magyarázat megnyitása"
style={{ fontSize: 18, lineHeight: 1 }}
>
<FaQuestionCircle />
</button>
</span>
<select
className="px-2 sm:px-3 py-1 rounded-lg bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30 text-[color:var(--color-text)] border-none focus:ring-2 focus:ring-[color:var(--color-success)] outline-none flex items-center text-xs sm:text-sm"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
aria-label="Rendezés"
>
<option value="date-asc">📅</option>
<option value="date-desc">📅</option>
<option value="abc-asc">AZ</option>
<option value="abc-desc">ZA</option>
</select>
</div>
{/* Items per page selector and count */}
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-6 bg-[color:var(--color-surface)]/60 backdrop-blur-lg rounded-xl px-6 py-3 shadow">
<div className="flex items-center gap-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
Elemek oldalanként:
</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="px-3 py-1.5 rounded-lg bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] focus:ring-2 focus:ring-[color:var(--color-success)] outline-none transition-all duration-200"
>
<option value={20}>20</option>
<option value={30}>30</option>
<option value={40}>40</option>
<option value={50}>50</option>
</select>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
{totalDecks > 0 ? (
<>
{startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli
</>
) : (
<>0 pakli</>
)}
</div>
</div>
{/* Decks Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 sm:gap-6 lg:gap-8 mt-6 sm:mt-8">
{/* Create New Deck card */}
<div
onClick={() => goDeckCreator()}
className="flex flex-col items-center justify-center h-40 sm:h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-xl sm:rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg"
>
<FaPlus style={{ color: "var(--color-success)" }} className="text-4xl sm:text-5xl mb-2" />
<span className="text-[color:var(--color-text)] font-semibold text-sm sm:text-base">Új pakli létrehozása</span>
</div>
{loading && <div className="col-span-full text-center text-[color:var(--color-text-muted)]">Betöltés...</div>}
{!loading && paginatedDecks.length === 0 && <div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek találatok.</div>}
{!loading && paginatedDecks.map((deck) => {
const deckType = deckTypes.find((t) => t.label === deck.type)
const borderColor = deckType ? deckType.color : "var(--color-success)"
return (
<div
key={deck.id}
className="flex flex-col justify-between h-40 sm:h-48 bg-[color:var(--color-card)] rounded-xl sm:rounded-2xl p-4 sm:p-6 shadow-lg border-t-4 hover:scale-105 transition-transform duration-200 cursor-pointer"
style={{ borderTopColor: borderColor }}
onClick={() => setSelectedDeck(deck)}
>
<div>
<span
className="inline-block px-2 sm:px-3 py-1 rounded-full text-[10px] sm:text-xs font-bold mb-2"
style={{
background: deckType?.color,
color: "var(--color-text-inverse)",
}}
>
{deck.type === "Luck"
? "Szerencse"
: deck.type === "Question"
? "Kérdés"
: "Joker"}
</span>
<h2 className="text-xl font-bold text-[color:var(--color-text)] mb-1 truncate">
{deck.name}
</h2>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm mt-2">
Létrehozva: {deck.created}
</div>
</div>
)
})}
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-2 mt-8">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
currentPage === 1
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
}`}
>
<FaChevronLeft />
Előző
</button>
<div className="flex items-center gap-2">
{[...Array(totalPages)].map((_, index) => {
const pageNum = index + 1
if (
pageNum === 1 ||
pageNum === totalPages ||
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
) {
return (
<button
key={pageNum}
onClick={() => setCurrentPage(pageNum)}
className={`w-10 h-10 rounded-lg font-medium transition-all duration-200 ${
currentPage === pageNum
? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] scale-110 shadow-lg'
: 'bg-[color:var(--color-surface)] text-[color:var(--color-text)] hover:bg-[color:var(--color-surface-selected)]'
}`}
>
{pageNum}
</button>
)
} else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) {
return <span key={pageNum} className="text-[color:var(--color-text-muted)]">...</span>
}
return null
})}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
currentPage === totalPages
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
}`}
>
Következő
<FaChevronRight />
</button>
</div>
)}
{/* Sort help popup */}
{showSortHelp && (
<PopUp onClose={() => setShowSortHelp(false)}>
<h2 className="text-lg font-bold mb-4">Rendezési lehetőségek magyarázata</h2>
<ul className="space-y-2 text-[color:var(--color-night)]">
<li><span className="font-bold">📅</span> Dátum szerint növekvő sorrendben (legrégebbi elöl)</li>
<li><span className="font-bold">📅</span> Dátum szerint csökkenő sorrendban (legújabb elöl)</li>
<li><span className="font-bold">AZ</span> Név szerint növekvő sorrendben (A-tól Z-ig)</li>
<li><span className="font-bold">ZA</span> Név szerint csökkenő sorrendben (Z-től A-ig)</li>
</ul>
<div className="mt-6 flex justify-end">
<button className="px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] font-semibold" onClick={() => setShowSortHelp(false)}>Bezárás</button>
</div>
</PopUp>
)}
{/* Deck Info Popup */}
{selectedDeck && <DeckInfoPopUp deck={selectedDeck} onClose={() => setSelectedDeck(null)} />}
</div>
</motion.div>
)
const UserHandlingMock = () => (
<motion.div
key="users"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.25 }}
className="space-y-4"
>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold text-white">User management</h2>
<div className="flex items-center gap-3">
<ButtonGreen text="Invite user" onClick={() => alert("Invite / invite-flow to implement")} width="w-36" />
</div>
</div>
<div className="mt-4">
<div className="flex gap-2 items-center w-full flex-wrap mb-4">
<SearchBox
value={userQuery}
onChange={(e) => setUserQuery(e.target.value)}
width={240}
placeholder="Keresés felhasználók között..."
className="mr-4"
/>
<label className="text-[color:var(--color-text)] font-semibold text-xs sm:text-sm flex items-center gap-2">
<input type="checkbox" checked={includeDeletedUsers} onChange={e => setIncludeDeletedUsers(e.target.checked)} className="mr-1" />
Törölt is
</label>
</div>
<div className="bg-[color:var(--color-surface)]/60 backdrop-blur-lg rounded-xl px-6 py-3 shadow mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex items-center gap-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">Elemek oldalanként:</span>
<select
value={userItemsPerPage}
onChange={(e) => setUserItemsPerPage(Number(e.target.value))}
className="px-3 py-1.5 rounded-lg bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] focus:ring-2 focus:ring-[color:var(--color-success)] outline-none transition-all duration-200"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={30}>30</option>
</select>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
{userTotalCount > 0 ? `${Math.min((userCurrentPage-1)*userItemsPerPage + 1, userTotalCount)}-${Math.min(userCurrentPage*userItemsPerPage, userTotalCount)} / ${userTotalCount} felhasználó` : '0 felhasználó'}
</div>
</div>
{/* User list */}
<div className="grid grid-cols-1 gap-4">
{userLoading && <div className="text-center text-[color:var(--color-text-muted)]">Betöltés...</div>}
{!userLoading && users.length === 0 && <div className="text-center text-[color:var(--color-text-muted)]">Nincsenek találatok.</div>}
{!userLoading && users.slice((userCurrentPage-1)*userItemsPerPage, userCurrentPage*userItemsPerPage).map(u => (
<div key={u.id} className="flex items-center justify-between bg-[color:var(--color-card)] rounded-xl p-4 shadow">
<div>
<div className="font-semibold text-[color:var(--color-text)]">{u.username}</div>
<div className="text-sm text-[color:var(--color-text-muted)]">{u.email}</div>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">Létrehozva: {u.created}</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs ${u.deleted ? 'bg-red-600 text-white' : 'bg-green-600 text-white'}`}>{u.deleted ? 'Törölt' : 'Aktív'}</span>
<button className="px-3 py-1 rounded bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]" onClick={() => setSelectedUser(u)}>Megtekint</button>
</div>
</div>
))}
</div>
{/* Pagination controls for users */}
{ Math.ceil(Math.max(userTotalCount, users.length) / userItemsPerPage) > 1 && (
<div className="flex justify-center items-center gap-2 mt-6">
{(() => {
const totalPages = Math.max(1, Math.ceil(Math.max(userTotalCount, users.length) / userItemsPerPage))
return (
<>
<button onClick={() => setUserCurrentPage(p => Math.max(1, p-1))} className="px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]">Előző</button>
<div className="flex items-center gap-2">
{[...Array(totalPages)].map((_, i) => {
const p = i+1
if (p === 1 || p === totalPages || (p >= userCurrentPage-1 && p <= userCurrentPage+1)) {
return <button key={p} onClick={() => setUserCurrentPage(p)} className={`w-10 h-10 rounded-lg ${userCurrentPage===p ? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]' : 'bg-[color:var(--color-surface)] text-[color:var(--color-text)]'}`}>{p}</button>
}
if (p === userCurrentPage-2 || p === userCurrentPage+2) return <span key={p} className="text-[color:var(--color-text-muted)]">...</span>
return null
})}
</div>
<button onClick={() => setUserCurrentPage(p => Math.min(totalPages, p+1))} className="px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]">Következő</button>
</>
)
})()}
</div>
)}
{/* User details popup */}
{selectedUser && (
<PopUp onClose={() => setSelectedUser(null)}>
<h2 className="text-lg font-bold mb-2">{selectedUser.username}</h2>
<div className="text-sm text-[color:var(--color-text-muted)] mb-4">
Email: {selectedUser.email} <br />
Létrehozva: {selectedUser.created} <br />
Status: {selectedUser.deleted ? 'Törölt' : 'Aktív'}
</div>
<div className="flex gap-2 justify-end">
{!selectedUser.deleted && (
<button
className="px-3 py-2 rounded bg-yellow-600 text-white"
onClick={async () => {
if (!confirm('Biztosan deaktiválod ezt a felhasználót?')) return
try {
const uuid = await resolveUserUuid(selectedUser)
if (!uuid) {
alert("Nem található érvényes userId (UUID) a kiválasztott felhasználónál. Ellenőrizd a backend választ vagy a user objektumot a konzolban.")
console.error("Selected user object:", selectedUser)
return
}
await adminDeactivateUser(uuid)
// reflect change locally
setUsers(curr => curr.map(u => u.id === selectedUser.id ? { ...u, deleted: true } : u))
setSelectedUser(prev => ({ ...prev, deleted: true }))
alert('Felhasználó deaktiválva.')
} catch (err) {
console.error(err)
alert(getApiErrorMessage(err) || 'Hiba a deaktiválás során.')
}
}}
>
Deaktiválás
</button>
)}
<button
className="px-3 py-2 rounded bg-red-600 text-white"
onClick={async () => {
if (!confirm('Végleg törlöd a felhasználót?')) return
try {
const uuid = await resolveUserUuid(selectedUser)
if (!uuid) {
alert("Nem található érvényes userId (UUID) a kiválasztott felhasználónál. Ellenőrizd a backend választ vagy a user objektumot a konzolban.")
console.error("Selected user object:", selectedUser)
return
}
await adminDeleteUser(uuid)
setUsers(curr => curr.filter(u => u.id !== selectedUser.id))
setSelectedUser(null)
alert('Felhasználó törölve.')
} catch (err) {
console.error(err)
alert(getApiErrorMessage(err) || 'Hiba a törlés során.')
}
}}
>
Törlés
</button>
<button className="px-3 py-2 rounded bg-[color:var(--color-surface)]" onClick={() => setSelectedUser(null)}>Bezár</button>
</div>
</PopUp>
)}
</div>
</motion.div>
)
// ---------- NEW: UUID helper + resolver ----------
const isUuid = (val) => {
if (!val || typeof val !== "string") return false
// simple RFC4122 v4-ish UUID check (hex + dashes)
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val.trim())
}
// Try various places on the user object for a UUID, fallback to search by username/email
const resolveUserUuid = async (user) => {
if (!user) return null
const candidates = [
user.id,
user.raw?.id,
user.raw?.userid,
user.raw?.userId,
user.raw?.uuid,
user.raw?.user_id
]
for (const c of candidates) if (isUuid(String(c || ""))) return String(c).trim()
// fallback: try to find canonical user by username or email via adminSearchUsers
const q = user.username || user.email || user.raw?.username || user.raw?.email
if (!q) return null
try {
const res = await adminSearchUsers(q, includeDeletedUsers ? true : false, 10, 0)
const arr = Array.isArray(res) ? res : (res.users || res || [])
if (Array.isArray(arr) && arr.length) {
// try to match by username/email exactly, or take first with valid id
const exact = arr.find(u => (u.username && u.username === user.username) || (u.email && u.email === user.email))
const pick = exact || arr[0]
if (pick && isUuid(pick.id)) return pick.id
}
} catch (err) {
console.error("resolveUserUuid: search failed", err)
}
return null
}
// ---------- end new helpers ----------
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
<div className="fixed inset-0 -z-10 pointer-events-none">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 min-h-[calc(100vh-64px)] mt-[64px] px-4 sm:px-6 py-8">
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-12 gap-6">
{/* Left navigation column */}
<aside className="md:col-span-3 lg:col-span-2 bg-white/5 rounded-2xl p-4 sticky top-[88px] h-fit">
<h3 className="text-lg font-semibold text-white mb-4">Admin panel</h3>
<div className="space-y-3">
<NavButton id="decks" icon={FaLayerGroup} label="Deck handling" />
<NavButton id="users" icon={FaUser} label="User handling" />
</div>
</aside>
{/* Right content column */}
<section className="md:col-span-9 lg:col-span-10">
<div className="bg-white/6 rounded-2xl p-6 min-h-[300px]">
{active === "decks" ? <DeckHandling /> : <UserHandlingMock />}
</div>
</section>
</div>
</main>
<Footer />
</div>
)
}
+6
View File
@@ -36,8 +36,14 @@ export const ROUTES = {
REPORTS: '/report', REPORTS: '/report',
CONTACTS: '/contacts', CONTACTS: '/contacts',
TEST: '/test', TEST: '/test',
// admin
ADMIN: '/admin',
} }
// Helper functions to generate dynamic routes // Helper functions to generate dynamic routes
export const routeHelpers = { export const routeHelpers = {
deckDetails: (deckId) => `/deck/${deckId}`, deckDetails: (deckId) => `/deck/${deckId}`,
+103
View File
@@ -0,0 +1,103 @@
@echo off
REM SerpentRace Production Deployment Script
REM This script loads Docker images and starts the production environment
setlocal EnableDelayedExpansion
echo ===============================================
echo SerpentRace Production Deployment
echo ===============================================
echo.
REM Check if Docker is installed
where docker >nul 2>nul
if %errorlevel% neq 0 (
echo [ERROR] Docker is not installed. Please install Docker first.
pause
exit /b 1
)
where docker-compose >nul 2>nul
if %errorlevel% neq 0 (
echo [ERROR] Docker Compose is not installed. Please install Docker Compose first.
pause
exit /b 1
)
REM Check if serpentrace-images.tar exists
if not exist "serpentrace-images.tar" (
echo [ERROR] serpentrace-images.tar not found!
echo Please ensure the tar file is in the same directory as this script.
pause
exit /b 1
)
REM Check if environment file exists
if not exist ".env.server" (
echo [ERROR] .env.server file not found!
echo Please ensure the environment file is configured.
pause
exit /b 1
)
echo [INFO] Loading Docker images from serpentrace-images.tar...
docker load -i serpentrace-images.tar
if %errorlevel% neq 0 (
echo [ERROR] Failed to load Docker images!
pause
exit /b 1
)
echo [INFO] Images loaded successfully!
echo.
REM Show loaded images
echo [INFO] Loaded images:
docker images | findstr serpentrace
docker images | findstr postgres
docker images | findstr redis
docker images | findstr minio
echo.
echo [WARNING] Before starting the services, please review and update .env.server:
echo - Change all placeholder passwords
echo - Configure email settings
echo - Update domain names
echo - Set strong JWT secret
echo.
echo Press any key to continue with deployment or Ctrl+C to exit...
pause >nul
echo [INFO] Starting production services...
docker-compose -f docker-compose.deploy.yml --env-file .env.server up -d
if %errorlevel% neq 0 (
echo [ERROR] Failed to start services!
pause
exit /b 1
)
echo.
echo ===============================================
echo Deployment Complete!
echo ===============================================
echo.
echo Services are starting up. Please wait a few moments for all services to be ready.
echo.
echo Available services:
echo - Frontend: http://localhost (or your domain)
echo - Backend API: http://localhost:3000
echo - MinIO Console: http://localhost:9001
echo.
echo To check service status: docker-compose -f docker-compose.deploy.yml ps
echo To view logs: docker-compose -f docker-compose.deploy.yml logs -f [service_name]
echo To stop services: docker-compose -f docker-compose.deploy.yml down
echo.
echo IMPORTANT SECURITY NOTES:
echo 1. Change all default passwords in .env.server
echo 2. Configure firewall rules for your server
echo 3. Set up SSL/TLS certificates for HTTPS
echo 4. Configure regular backups
echo 5. Monitor logs and system resources
echo.
pause
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# SerpentRace Production Deployment Script for Linux
# This script loads Docker images and starts the production environment
set -e
echo "==============================================="
echo "SerpentRace Production Deployment"
echo "==============================================="
echo
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "[ERROR] Docker is not installed. Please install Docker first."
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
echo "[ERROR] Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
# Check if serpentrace-images.tar exists
if [ ! -f "serpentrace-images.tar" ]; then
echo "[ERROR] serpentrace-images.tar not found!"
echo "Please ensure the tar file is in the same directory as this script."
exit 1
fi
# Check if environment file exists
if [ ! -f ".env.server" ]; then
echo "[ERROR] .env.server file not found!"
echo "Please ensure the environment file is configured."
exit 1
fi
echo "[INFO] Loading Docker images from serpentrace-images.tar..."
docker load -i serpentrace-images.tar
echo "[INFO] Images loaded successfully!"
echo
# Show loaded images
echo "[INFO] Loaded images:"
docker images | grep -E "(serpentrace|postgres|redis|minio)"
echo
echo "[WARNING] Before starting the services, please review and update .env.server:"
echo " - Change all placeholder passwords"
echo " - Configure email settings"
echo " - Update domain names"
echo " - Set strong JWT secret"
echo
read -p "Press Enter to continue with deployment or Ctrl+C to exit..."
echo "[INFO] Starting production services..."
docker-compose -f docker-compose.deploy.yml --env-file .env.server up -d
echo
echo "==============================================="
echo "Deployment Complete!"
echo "==============================================="
echo
echo "Services are starting up. Please wait a few moments for all services to be ready."
echo
echo "Available services:"
echo " - Frontend: http://localhost (or your domain)"
echo " - Backend API: http://localhost:3000"
echo " - MinIO Console: http://localhost:9001"
echo
echo "To check service status: docker-compose -f docker-compose.deploy.yml ps"
echo "To view logs: docker-compose -f docker-compose.deploy.yml logs -f [service_name]"
echo "To stop services: docker-compose -f docker-compose.deploy.yml down"
echo
echo "IMPORTANT SECURITY NOTES:"
echo "1. Change all default passwords in .env.server"
echo "2. Configure firewall rules for your server"
echo "3. Set up SSL/TLS certificates for HTTPS"
echo "4. Configure regular backups"
echo "5. Monitor logs and system resources"
echo
+60
View File
@@ -0,0 +1,60 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Handle client routing
location / {
try_files $uri $uri/ /index.html;
}
# API proxy to backend
location /api/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# WebSocket support
location /socket.io/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Static assets caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
+180
View File
@@ -0,0 +1,180 @@
-- This script was generated by the ERD tool in pgAdmin 4.
-- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps.
BEGIN;
-- ===================================================================
-- STEP 1: Enable Required Extensions
-- ===================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ===================================================================
-- STEP 2: Create Tables
-- ===================================================================
CREATE TABLE IF NOT EXISTS public."ChatArchives"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
"chatId" uuid NOT NULL,
"archivedMessages" json NOT NULL,
"archivedAt" timestamp without time zone NOT NULL,
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
"chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL,
"chatName" character varying(255) COLLATE pg_catalog."default",
"gameId" uuid,
participants uuid[] NOT NULL,
CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public."Chats"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying,
name character varying(255) COLLATE pg_catalog."default",
"gameId" uuid,
"createdBy" uuid,
users uuid[] NOT NULL,
messages json NOT NULL DEFAULT '[]'::json,
"lastActivity" timestamp without time zone,
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
state integer NOT NULL DEFAULT 0,
"archiveDate" timestamp without time zone,
CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public."Contacts"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
userid uuid,
type integer NOT NULL,
txt text COLLATE pg_catalog."default" NOT NULL,
state integer NOT NULL DEFAULT 0,
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"adminResponse" text COLLATE pg_catalog."default",
"responseDate" timestamp without time zone,
"respondedBy" uuid,
CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public."Decks"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
type integer NOT NULL,
user_id uuid NOT NULL,
creation_date timestamp without time zone NOT NULL DEFAULT now(),
cards json NOT NULL,
played_number integer NOT NULL DEFAULT 0,
ctype integer NOT NULL DEFAULT 0,
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
state integer NOT NULL DEFAULT 0,
organization_id uuid,
CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public."Games"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL,
maxplayers integer NOT NULL,
logintype integer NOT NULL DEFAULT 0,
boardsize integer NOT NULL DEFAULT 50,
"createdBy" uuid NOT NULL,
organizationid uuid,
decks jsonb NOT NULL DEFAULT '[]'::jsonb,
playerids uuid[] NOT NULL DEFAULT '{}'::uuid[],
"winnerId" uuid,
state integer NOT NULL DEFAULT 0,
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
start_date timestamp without time zone,
"finishDate" timestamp without time zone,
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"organizationId" uuid,
CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id),
CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode)
);
CREATE TABLE IF NOT EXISTS public."Organizations"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL,
contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL,
contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL,
contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL,
state integer NOT NULL DEFAULT 0,
regdate timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
url character varying(500) COLLATE pg_catalog."default",
userinorg integer NOT NULL DEFAULT 0,
"maxOrganizationalDecks" integer,
CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public."Users"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
orgid uuid,
username character varying(100) COLLATE pg_catalog."default" NOT NULL,
password character varying(255) COLLATE pg_catalog."default" NOT NULL,
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
fname character varying(100) COLLATE pg_catalog."default" NOT NULL,
lname character varying(100) COLLATE pg_catalog."default" NOT NULL,
token character varying(255) COLLATE pg_catalog."default",
"TokenExpires" timestamp without time zone,
phone character varying(20) COLLATE pg_catalog."default",
state integer NOT NULL DEFAULT 0,
regdate timestamp without time zone NOT NULL DEFAULT now(),
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
"Orglogindate" timestamp without time zone,
CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id),
CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email),
CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username)
);
CREATE TABLE IF NOT EXISTS public.migrations
(
id serial NOT NULL,
"timestamp" bigint NOT NULL,
name character varying COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id)
);
ALTER TABLE IF EXISTS public."Decks"
ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id)
REFERENCES public."Organizations" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Decks"
ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id)
REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Games"
ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId")
REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Games"
ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId")
REFERENCES public."Organizations" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
ALTER TABLE IF EXISTS public."Games"
ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy")
REFERENCES public."Users" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
END;