Compare commits
7 Commits
deployment
...
04e11e8120
| Author | SHA1 | Date | |
|---|---|---|---|
| 04e11e8120 | |||
| 9ba8c95142 | |||
| a9546dcc63 | |||
| d7b47f2abe | |||
| e29216e895 | |||
| 5eb4d3eef7 | |||
| 66287a84c6 |
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
|
||||||
});
|
});
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
-- ===================================================================
|
||||||
|
-- STEP 1: Enable Required Extensions
|
||||||
|
-- ===================================================================
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
-- Create Users table
|
-- ===================================================================
|
||||||
CREATE TABLE "Users" (
|
-- STEP 2: Create Tables
|
||||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
-- ===================================================================
|
||||||
"orgid" UUID NULL,
|
|
||||||
"username" VARCHAR(100) UNIQUE NOT NULL,
|
CREATE TABLE IF NOT EXISTS public."ChatArchives"
|
||||||
"password" VARCHAR(255) NOT NULL,
|
(
|
||||||
"email" VARCHAR(255) UNIQUE NOT NULL,
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"fname" VARCHAR(100) NOT NULL,
|
"chatId" uuid NOT NULL,
|
||||||
"lname" VARCHAR(100) NOT NULL,
|
"archivedMessages" json NOT NULL,
|
||||||
"token" VARCHAR(255) NULL,
|
"archivedAt" timestamp without time zone NOT NULL,
|
||||||
"TokenExpires" TIMESTAMP NULL,
|
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"phone" VARCHAR(20) NULL,
|
"chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"state" INTEGER NOT NULL DEFAULT 0,
|
"chatName" character varying(255) COLLATE pg_catalog."default",
|
||||||
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"gameId" uuid,
|
||||||
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
participants uuid[] NOT NULL,
|
||||||
"Orglogindate" TIMESTAMP NULL
|
CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create Organizations table
|
CREATE TABLE IF NOT EXISTS public."Chats"
|
||||||
CREATE TABLE "Organizations" (
|
(
|
||||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"name" VARCHAR(255) NOT NULL,
|
type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying,
|
||||||
"contactfname" VARCHAR(100) NOT NULL,
|
name character varying(255) COLLATE pg_catalog."default",
|
||||||
"contactlname" VARCHAR(100) NOT NULL,
|
"gameId" uuid,
|
||||||
"contactphone" VARCHAR(20) NOT NULL,
|
"createdBy" uuid,
|
||||||
"contactemail" VARCHAR(255) NOT NULL,
|
users uuid[] NOT NULL,
|
||||||
"state" INTEGER NOT NULL DEFAULT 0,
|
messages json NOT NULL DEFAULT '[]'::json,
|
||||||
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"lastActivity" timestamp without time zone,
|
||||||
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"url" VARCHAR(500) NULL,
|
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"userinorg" INTEGER NOT NULL DEFAULT 0,
|
state integer NOT NULL DEFAULT 0,
|
||||||
"maxOrganizationalDecks" INTEGER NULL
|
"archiveDate" timestamp without time zone,
|
||||||
|
CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create Decks table
|
CREATE TABLE IF NOT EXISTS public."Contacts"
|
||||||
CREATE TABLE "Decks" (
|
(
|
||||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"name" VARCHAR(255) NOT NULL,
|
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"type" INTEGER NOT NULL,
|
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"user_id" UUID NOT NULL,
|
userid uuid,
|
||||||
"creation_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
type integer NOT NULL,
|
||||||
"cards" JSONB NOT NULL DEFAULT '[]',
|
txt text COLLATE pg_catalog."default" NOT NULL,
|
||||||
"played_number" INTEGER NOT NULL DEFAULT 0,
|
state integer NOT NULL DEFAULT 0,
|
||||||
"ctype" INTEGER NOT NULL DEFAULT 0,
|
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"update_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"state" INTEGER NOT NULL DEFAULT 0,
|
"adminResponse" text COLLATE pg_catalog."default",
|
||||||
"organization_id" UUID NULL
|
"responseDate" timestamp without time zone,
|
||||||
|
"respondedBy" uuid,
|
||||||
|
CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create Chats table
|
CREATE TABLE IF NOT EXISTS public."Decks"
|
||||||
CREATE TABLE "Chats" (
|
(
|
||||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"type" VARCHAR(50) NOT NULL DEFAULT 'direct',
|
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"name" VARCHAR(255) NULL,
|
type integer NOT NULL,
|
||||||
"gameId" UUID NULL,
|
user_id uuid NOT NULL,
|
||||||
"createdBy" UUID NULL,
|
creation_date timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"users" UUID[] NOT NULL,
|
cards json NOT NULL,
|
||||||
"messages" JSONB NOT NULL DEFAULT '[]',
|
played_number integer NOT NULL DEFAULT 0,
|
||||||
"lastActivity" TIMESTAMP NULL,
|
ctype integer NOT NULL DEFAULT 0,
|
||||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
state integer NOT NULL DEFAULT 0,
|
||||||
"state" INTEGER NOT NULL DEFAULT 0,
|
organization_id uuid,
|
||||||
"archiveDate" TIMESTAMP NULL
|
CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create Contacts table
|
CREATE TABLE IF NOT EXISTS public."Games"
|
||||||
CREATE TABLE "Contacts" (
|
(
|
||||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"name" VARCHAR(255) NOT NULL,
|
gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"email" VARCHAR(255) NOT NULL,
|
maxplayers integer NOT NULL,
|
||||||
"userid" UUID NULL,
|
logintype integer NOT NULL DEFAULT 0,
|
||||||
"type" INTEGER NOT NULL,
|
boardsize integer NOT NULL DEFAULT 50,
|
||||||
"txt" TEXT NOT NULL,
|
"createdBy" uuid NOT NULL,
|
||||||
"state" INTEGER NOT NULL DEFAULT 0,
|
organizationid uuid,
|
||||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
decks jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
playerids uuid[] NOT NULL DEFAULT '{}'::uuid[],
|
||||||
"adminResponse" TEXT NULL,
|
"winnerId" uuid,
|
||||||
"responseDate" TIMESTAMP NULL,
|
state integer NOT NULL DEFAULT 0,
|
||||||
"respondedBy" UUID NULL
|
"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 Games table
|
CREATE TABLE IF NOT EXISTS public."Organizations"
|
||||||
CREATE TABLE "Games" (
|
(
|
||||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"gamecode" VARCHAR(10) UNIQUE NOT NULL,
|
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"maxplayers" INTEGER NOT NULL,
|
contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"logintype" INTEGER NOT NULL DEFAULT 0,
|
contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"state" INTEGER NOT NULL DEFAULT 0,
|
contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"playerids" UUID[] NOT NULL DEFAULT '{}',
|
contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||||
"decks" JSONB NOT NULL DEFAULT '[]',
|
state integer NOT NULL DEFAULT 0,
|
||||||
"boardsize" INTEGER NOT NULL DEFAULT 50,
|
regdate timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
url character varying(500) COLLATE pg_catalog."default",
|
||||||
"finishDate" TIMESTAMP NULL,
|
userinorg integer NOT NULL DEFAULT 0,
|
||||||
"winnerid" UUID NULL,
|
"maxOrganizationalDecks" integer,
|
||||||
"createdBy" UUID NOT NULL,
|
CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id)
|
||||||
"organizationid" UUID NULL
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Add Foreign Key Constraints
|
CREATE TABLE IF NOT EXISTS public."Users"
|
||||||
ALTER TABLE "Users"
|
(
|
||||||
ADD CONSTRAINT "FK_Users_Organizations"
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
ALTER TABLE "Decks"
|
CREATE TABLE IF NOT EXISTS public.migrations
|
||||||
ADD CONSTRAINT "FK_Decks_Users"
|
(
|
||||||
FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE CASCADE;
|
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 "Decks"
|
ALTER TABLE IF EXISTS public."Decks"
|
||||||
ADD CONSTRAINT "FK_Decks_Organizations"
|
ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id)
|
||||||
FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
REFERENCES public."Organizations" (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
ALTER TABLE "Contacts"
|
|
||||||
ADD CONSTRAINT "FK_Contacts_Users"
|
|
||||||
FOREIGN KEY ("userid") REFERENCES "Users"("id") ON DELETE SET NULL;
|
|
||||||
|
|
||||||
ALTER TABLE "Contacts"
|
ALTER TABLE IF EXISTS public."Decks"
|
||||||
ADD CONSTRAINT "FK_Contacts_RespondedBy"
|
ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id)
|
||||||
FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") ON DELETE SET NULL;
|
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
ALTER TABLE "Chats"
|
|
||||||
ADD CONSTRAINT "FK_Chats_CreatedBy"
|
|
||||||
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE SET NULL;
|
|
||||||
|
|
||||||
ALTER TABLE "Chats"
|
ALTER TABLE IF EXISTS public."Games"
|
||||||
ADD CONSTRAINT "FK_Chats_Games"
|
ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId")
|
||||||
FOREIGN KEY ("gameId") REFERENCES "Games"("id") ON DELETE SET NULL;
|
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
ALTER TABLE "Games"
|
|
||||||
ADD CONSTRAINT "FK_Games_CreatedBy"
|
|
||||||
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE "Games"
|
ALTER TABLE IF EXISTS public."Games"
|
||||||
ADD CONSTRAINT "FK_Games_Organizations"
|
ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId")
|
||||||
FOREIGN KEY ("organizationid") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
REFERENCES public."Organizations" (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
ALTER TABLE "Games"
|
|
||||||
ADD CONSTRAINT "FK_Games_Winner"
|
|
||||||
FOREIGN KEY ("winnerid") REFERENCES "Users"("id") ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- Create Indexes for Performance
|
ALTER TABLE IF EXISTS public."Games"
|
||||||
CREATE INDEX "IDX_Users_Username" ON "Users" ("username");
|
ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy")
|
||||||
CREATE INDEX "IDX_Users_Email" ON "Users" ("email");
|
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||||
CREATE INDEX "IDX_Users_OrgId" ON "Users" ("orgid");
|
ON UPDATE NO ACTION
|
||||||
CREATE INDEX "IDX_Users_State" ON "Users" ("state");
|
ON DELETE NO ACTION;
|
||||||
|
|
||||||
CREATE INDEX "IDX_Organizations_Name" ON "Organizations" ("name");
|
|
||||||
CREATE INDEX "IDX_Organizations_State" ON "Organizations" ("state");
|
|
||||||
|
|
||||||
CREATE INDEX "IDX_Decks_UserId" ON "Decks" ("user_id");
|
|
||||||
CREATE INDEX "IDX_Decks_Type" ON "Decks" ("type");
|
|
||||||
CREATE INDEX "IDX_Decks_CType" ON "Decks" ("ctype");
|
|
||||||
CREATE INDEX "IDX_Decks_State" ON "Decks" ("state");
|
|
||||||
CREATE INDEX "IDX_Decks_OrganizationId" ON "Decks" ("organization_id");
|
|
||||||
|
|
||||||
CREATE INDEX "IDX_Chats_Type" ON "Chats" ("type");
|
|
||||||
CREATE INDEX "IDX_Chats_State" ON "Chats" ("state");
|
|
||||||
CREATE INDEX "IDX_Chats_GameId" ON "Chats" ("gameId");
|
|
||||||
CREATE INDEX "IDX_Chats_CreatedBy" ON "Chats" ("createdBy");
|
|
||||||
|
|
||||||
CREATE INDEX "IDX_Contacts_Type" ON "Contacts" ("type");
|
|
||||||
CREATE INDEX "IDX_Contacts_State" ON "Contacts" ("state");
|
|
||||||
CREATE INDEX "IDX_Contacts_UserId" ON "Contacts" ("userid");
|
|
||||||
|
|
||||||
CREATE INDEX "IDX_Games_GameCode" ON "Games" ("gamecode");
|
|
||||||
CREATE INDEX "IDX_Games_State" ON "Games" ("state");
|
|
||||||
CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy");
|
|
||||||
CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid");
|
|
||||||
|
|
||||||
-- Create update trigger for updatedate columns
|
|
||||||
CREATE OR REPLACE FUNCTION update_updatedate_column()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.updatedate = CURRENT_TIMESTAMP;
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
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;
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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">A→Z</option>
|
||||||
|
<option value="abc-desc">Z→A</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">A→Z</span> – Név szerint növekvő sorrendben (A-tól Z-ig)</li>
|
||||||
|
<li><span className="font-bold">Z→A</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user