Remove unnecessary description and whitespace from nexumi.mdc file
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
---
|
||||
description: Nexumi – verbindliche Projektregeln
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Nexumi – Projektregeln
|
||||
|
||||
## Quelle der Wahrheit
|
||||
|
||||
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
NODE_ENV=development
|
||||
BOT_TOKEN=
|
||||
BOT_CLIENT_ID=
|
||||
BOT_GUILD_ID=
|
||||
DATABASE_URL=postgresql://nexumi:nexumi@postgres:5432/nexumi
|
||||
REDIS_URL=redis://redis:6379
|
||||
LOG_LEVEL=info
|
||||
DEFAULT_LOCALE=de
|
||||
METRICS_TOKEN=
|
||||
SENTRY_DSN=
|
||||
BACKUP_RETENTION_DAYS=14
|
||||
19
.eslintrc.cjs
Normal file
19
.eslintrc.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true
|
||||
},
|
||||
ignorePatterns: ["dist", ".turbo", "node_modules"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error"
|
||||
}
|
||||
};
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.turbo
|
||||
dist
|
||||
coverage
|
||||
.env
|
||||
.env.local
|
||||
pnpm-lock.yaml
|
||||
apps/*/dist
|
||||
apps/*/.next
|
||||
packages/*/dist
|
||||
prisma/migrations
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
||||
25
apps/bot/Dockerfile
Normal file
25
apps/bot/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:22-alpine AS base
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json pnpm-workspace.yaml turbo.json tsconfig.base.json ./
|
||||
COPY apps/bot/package.json apps/bot/package.json
|
||||
COPY packages/shared/package.json packages/shared/package.json
|
||||
RUN pnpm install --frozen-lockfile=false
|
||||
|
||||
FROM deps AS build
|
||||
COPY . .
|
||||
RUN pnpm --filter @nexumi/shared build
|
||||
RUN pnpm --filter @nexumi/bot prisma:generate
|
||||
RUN pnpm --filter @nexumi/bot build
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/bot/dist ./apps/bot/dist
|
||||
COPY --from=build /app/apps/bot/prisma ./apps/bot/prisma
|
||||
COPY --from=build /app/packages/shared/dist ./packages/shared/dist
|
||||
COPY --from=build /app/apps/bot/package.json ./apps/bot/package.json
|
||||
CMD ["node", "apps/bot/dist/index.js"]
|
||||
29
apps/bot/package.json
Normal file
29
apps/bot/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@nexumi/bot",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nexumi/shared": "workspace:*",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"bullmq": "^5.34.0",
|
||||
"discord.js": "^14.17.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"ioredis": "^5.4.2",
|
||||
"pino": "^9.6.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.22.0",
|
||||
"tsx": "^4.19.2"
|
||||
}
|
||||
}
|
||||
81
apps/bot/prisma/schema.prisma
Normal file
81
apps/bot/prisma/schema.prisma
Normal file
@@ -0,0 +1,81 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Guild {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
settings GuildSettings?
|
||||
cases Case[]
|
||||
}
|
||||
|
||||
model GuildSettings {
|
||||
id String @id @default(cuid())
|
||||
guildId String @unique
|
||||
locale String @default("de")
|
||||
moderationEnabled Boolean @default(true)
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
warnings Warning[]
|
||||
notes ModNote[]
|
||||
}
|
||||
|
||||
model Case {
|
||||
id String @id @default(cuid())
|
||||
caseNumber Int
|
||||
guildId String
|
||||
targetUserId String
|
||||
moderatorId String
|
||||
action String
|
||||
reason String?
|
||||
metadata Json?
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
|
||||
warnings Warning[]
|
||||
moderationNote ModNote[]
|
||||
|
||||
@@unique([guildId, caseNumber])
|
||||
@@index([guildId, targetUserId])
|
||||
}
|
||||
|
||||
model Warning {
|
||||
id String @id @default(cuid())
|
||||
guildId String
|
||||
userId String
|
||||
moderatorId String
|
||||
reason String?
|
||||
caseId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
case Case? @relation(fields: [caseId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([guildId, userId])
|
||||
}
|
||||
|
||||
model ModNote {
|
||||
id String @id @default(cuid())
|
||||
guildId String
|
||||
userId String
|
||||
moderatorId String
|
||||
note String
|
||||
caseId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
case Case? @relation(fields: [caseId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([guildId, userId])
|
||||
}
|
||||
3
apps/bot/src/db.ts
Normal file
3
apps/bot/src/db.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
17
apps/bot/src/env.ts
Normal file
17
apps/bot/src/env.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { config } from 'dotenv';
|
||||
import { z } from 'zod';
|
||||
|
||||
config();
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
BOT_TOKEN: z.string().min(1),
|
||||
BOT_CLIENT_ID: z.string().min(1),
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url(),
|
||||
LOG_LEVEL: z.string().default('info'),
|
||||
DEFAULT_LOCALE: z.enum(['de', 'en']).default('de'),
|
||||
BACKUP_RETENTION_DAYS: z.coerce.number().int().positive().default(14)
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(process.env);
|
||||
6
apps/bot/src/logger.ts
Normal file
6
apps/bot/src/logger.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import pino from 'pino';
|
||||
import { env } from './env.js';
|
||||
|
||||
export const logger = pino({
|
||||
level: env.LOG_LEVEL
|
||||
});
|
||||
6
apps/bot/src/redis.ts
Normal file
6
apps/bot/src/redis.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import IORedis from 'ioredis';
|
||||
import { env } from './env.js';
|
||||
|
||||
export const redis = new IORedis(env.REDIS_URL, {
|
||||
maxRetriesPerRequest: null
|
||||
});
|
||||
8
apps/bot/tsconfig.json
Normal file
8
apps/bot/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "prisma"]
|
||||
}
|
||||
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: nexumi
|
||||
POSTGRES_USER: nexumi
|
||||
POSTGRES_PASSWORD: nexumi
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nexumi -d nexumi"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/bot/Dockerfile
|
||||
env_file: .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
webui:
|
||||
image: node:22-alpine
|
||||
working_dir: /app
|
||||
command: sh -c "node -e \"require('http').createServer((_,res)=>res.end('webui scaffold')).listen(3000)\""
|
||||
ports:
|
||||
- "3000:3000"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.nexumi.rule=Host(`nexumi.de`)"
|
||||
- "traefik.http.services.nexumi.loadbalancer.server.port=3000"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
backups:
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "nexumi",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Nexumi monorepo",
|
||||
"packageManager": "pnpm@9.0.0",
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev",
|
||||
"lint": "turbo lint",
|
||||
"test": "turbo test",
|
||||
"typecheck": "turbo typecheck",
|
||||
"prisma:generate": "pnpm --filter @nexumi/bot prisma:generate",
|
||||
"prisma:migrate": "pnpm --filter @nexumi/bot prisma:migrate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"eslint": "^9.16.0",
|
||||
"prettier": "^3.4.1",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
16
packages/shared/package.json
Normal file
16
packages/shared/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nexumi/shared",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
||||
50
packages/shared/src/index.ts
Normal file
50
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const LocaleSchema = z.enum(['de', 'en']);
|
||||
export type Locale = z.infer<typeof LocaleSchema>;
|
||||
|
||||
export const GuildSettingsSchema = z.object({
|
||||
guildId: z.string(),
|
||||
locale: LocaleSchema.default('de'),
|
||||
moderationEnabled: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export const ModerationActionSchema = z.enum([
|
||||
'BAN',
|
||||
'UNBAN',
|
||||
'KICK',
|
||||
'TIMEOUT',
|
||||
'UN_TIMEOUT',
|
||||
'WARN_ADD',
|
||||
'WARN_REMOVE',
|
||||
'WARN_CLEAR',
|
||||
'PURGE',
|
||||
'LOCK',
|
||||
'UNLOCK',
|
||||
'SLOWMODE',
|
||||
'NICK_SET',
|
||||
'NICK_RESET',
|
||||
'CASE_EDIT',
|
||||
'CASE_DELETE',
|
||||
'MODNOTE_ADD'
|
||||
]);
|
||||
|
||||
export type ModerationAction = z.infer<typeof ModerationActionSchema>;
|
||||
|
||||
type Dictionary = Record<string, string>;
|
||||
const de: Dictionary = {
|
||||
'generic.noPermission': 'Du hast keine Berechtigung für diese Aktion.',
|
||||
'moderation.success': 'Aktion erfolgreich ausgeführt.',
|
||||
'moderation.reason.none': 'Kein Grund angegeben'
|
||||
};
|
||||
const en: Dictionary = {
|
||||
'generic.noPermission': 'You do not have permission for this action.',
|
||||
'moderation.success': 'Action executed successfully.',
|
||||
'moderation.reason.none': 'No reason provided'
|
||||
};
|
||||
|
||||
const locales: Record<Locale, Dictionary> = { de, en };
|
||||
|
||||
export function t(locale: Locale, key: string): string {
|
||||
return locales[locale][key] ?? key;
|
||||
}
|
||||
8
packages/shared/tsconfig.json
Normal file
8
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
15
tsconfig.base.json
Normal file
15
tsconfig.base.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
23
turbo.json
Normal file
23
turbo.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^test"],
|
||||
"outputs": ["coverage/**"]
|
||||
},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^typecheck"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user