feat: Michess – Initiales Setup auf Basis von chessu

- Rebranding: chessu → Michess (Header, Footer, Metadata, Session-Cookie)
- Stockfish KI-Integration: 6 Schwierigkeitsgrade (Anfänger bis Meister)
- Admin-Panel: Nutzerverwaltung, Sperren/Entsperren, Git-Pull-Update
- Freundessystem: Anfragen, Freundesliste, Nutzersuche
- DB-Schema: role, banned, friend_request, friendship Tabellen
- Docker: NAS-optimiertes docker-compose.yml mit PostgreSQL Healthcheck
- Stockfish binary via apk im Alpine-Image installiert
- .env.example für einfaches Deployment
- scripts/update.sh für manuelles NAS-Update
- Brain.md + Plan.md als Projekt-Gedächtnis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michess
2026-04-13 10:02:52 +02:00
parent 29968f023d
commit 7dac042d73
28 changed files with 1760 additions and 89 deletions

View File

@@ -6,11 +6,13 @@ export const INIT_TABLES = /* sql */ `
CREATE TABLE IF NOT EXISTS "user" (
id SERIAL PRIMARY KEY,
name VARCHAR(128) UNIQUE NOT NULL,
email VARCHAR(128),
email VARCHAR(128) UNIQUE,
password TEXT,
role VARCHAR(16) DEFAULT 'user',
wins INTEGER DEFAULT 0,
losses INTEGER DEFAULT 0,
draws INTEGER DEFAULT 0,
banned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "game" (
@@ -22,7 +24,28 @@ export const INIT_TABLES = /* sql */ `
white_name VARCHAR(32),
black_id INT REFERENCES "user",
black_name VARCHAR(32),
vs_ai BOOLEAN DEFAULT FALSE,
ai_level INTEGER,
started_at TIMESTAMP NOT NULL,
ended_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "friend_request" (
id SERIAL PRIMARY KEY,
from_id INT REFERENCES "user"(id) ON DELETE CASCADE,
to_id INT REFERENCES "user"(id) ON DELETE CASCADE,
status VARCHAR(16) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(from_id, to_id)
);
CREATE TABLE IF NOT EXISTS "friendship" (
id SERIAL PRIMARY KEY,
user_id_1 INT REFERENCES "user"(id) ON DELETE CASCADE,
user_id_2 INT REFERENCES "user"(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id_1, user_id_2)
);
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS role VARCHAR(16) DEFAULT 'user';
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS banned BOOLEAN DEFAULT FALSE;
ALTER TABLE "game" ADD COLUMN IF NOT EXISTS vs_ai BOOLEAN DEFAULT FALSE;
ALTER TABLE "game" ADD COLUMN IF NOT EXISTS ai_level INTEGER;
`;

View File

@@ -8,7 +8,7 @@ export const create = async (user: User, password: string) => {
try {
const res = await db.query(
`INSERT INTO "user"(name, email, password) VALUES($1, $2, $3) RETURNING id, name, email, wins, losses, draws`,
`INSERT INTO "user"(name, email, password) VALUES($1, $2, $3) RETURNING id, name, email, wins, losses, draws, role, banned`,
[user.name, user.email || null, password]
);
return res.rows[0] as User;
@@ -24,11 +24,11 @@ export const findById = async (id: number) => {
}
try {
const res = await db.query(
`SELECT id, name, email, wins, losses, draws FROM "user" WHERE id=$1`,
`SELECT id, name, email, wins, losses, draws, role, banned FROM "user" WHERE id=$1`,
[id]
);
if (res.rowCount) {
return res.rows[0] as User;
return res.rows[0] as User & { banned?: boolean };
} else return null;
} catch (err: unknown) {
console.log(err);
@@ -37,14 +37,13 @@ export const findById = async (id: number) => {
};
export const findByNameEmail = async (user: User, includePassword = false, limit?: number) => {
// if user is not specified, get all users
if (!user) {
try {
const res = await db.query(
`SELECT id, name, email, wins, losses, draws FROM "user" LIMIT $1`,
`SELECT id, name, email, wins, losses, draws, role, banned FROM "user" LIMIT $1`,
[limit ?? 10]
);
return res.rows as (User & { password?: string })[];
return res.rows as (User & { password?: string; banned?: boolean })[];
} catch (err: unknown) {
console.log(err);
return null;
@@ -53,12 +52,25 @@ export const findByNameEmail = async (user: User, includePassword = false, limit
try {
const res = await db.query(
`SELECT id, name, email, wins, losses, draws${
`SELECT id, name, email, wins, losses, draws, role, banned${
includePassword ? `, password` : ""
} FROM "user" WHERE name=$1 OR email=$2 LIMIT $3`,
[user.name, user.email, limit ?? 1]
);
return res.rows as (User & { password?: string })[];
return res.rows as (User & { password?: string; banned?: boolean })[];
} catch (err: unknown) {
console.log(err);
return null;
}
};
export const searchByName = async (query: string, limit = 10) => {
try {
const res = await db.query(
`SELECT id, name, wins, losses, draws FROM "user" WHERE name ILIKE $1 LIMIT $2`,
[`%${query}%`, limit]
);
return res.rows as User[];
} catch (err: unknown) {
console.log(err);
return null;
@@ -71,11 +83,11 @@ export const update = async (id: number, updatedUser: User & { password?: string
}
try {
let query = `UPDATE "user" SET name=$1, email=$2 WHERE id=$3 RETURNING id, name, email, wins, losses, draws`;
let values = [updatedUser.name, updatedUser.email, id];
let query = `UPDATE "user" SET name=$1, email=$2 WHERE id=$3 RETURNING id, name, email, wins, losses, draws, role, banned`;
let values: (string | number | undefined)[] = [updatedUser.name, updatedUser.email, id];
if (updatedUser.password) {
query = `UPDATE "user" SET name=$1, email=$2, password=$3 WHERE id=$4 RETURNING id, name, email, wins, losses, draws`;
query = `UPDATE "user" SET name=$1, email=$2, password=$3 WHERE id=$4 RETURNING id, name, email, wins, losses, draws, role, banned`;
values = [updatedUser.name, updatedUser.email, updatedUser.password, id];
}
const res = await db.query(query, values);
@@ -86,6 +98,58 @@ export const update = async (id: number, updatedUser: User & { password?: string
}
};
export const adminUpdate = async (id: number, fields: { role?: string; banned?: boolean }) => {
try {
const setClauses: string[] = [];
const values: (string | boolean | number)[] = [];
let paramIndex = 1;
if (fields.role !== undefined) {
setClauses.push(`role=$${paramIndex++}`);
values.push(fields.role);
}
if (fields.banned !== undefined) {
setClauses.push(`banned=$${paramIndex++}`);
values.push(fields.banned);
}
if (!setClauses.length) return null;
values.push(id);
const res = await db.query(
`UPDATE "user" SET ${setClauses.join(", ")} WHERE id=$${paramIndex} RETURNING id, name, email, role, banned`,
values
);
return res.rows[0] as User & { banned?: boolean };
} catch (err: unknown) {
console.log(err);
return null;
}
};
export const getAllUsers = async (limit = 50, offset = 0, search?: string) => {
try {
let query = `SELECT id, name, email, wins, losses, draws, role, banned, created_at FROM "user"`;
const values: (string | number)[] = [];
if (search) {
query += ` WHERE name ILIKE $1 OR email ILIKE $1`;
values.push(`%${search}%`);
query += ` ORDER BY created_at DESC LIMIT $${values.length + 1} OFFSET $${values.length + 2}`;
values.push(limit, offset);
} else {
query += ` ORDER BY created_at DESC LIMIT $1 OFFSET $2`;
values.push(limit, offset);
}
const res = await db.query(query, values);
return res.rows as (User & { banned?: boolean; created_at?: string })[];
} catch (err: unknown) {
console.log(err);
return null;
}
};
export const remove = async (id: number) => {
if (id === 0) {
return null;
@@ -106,7 +170,10 @@ const UserModel = {
create,
findById,
findByNameEmail,
searchByName,
update,
adminUpdate,
getAllUsers,
remove
};