diff --git a/bot/.dockerignore b/bot/.dockerignore new file mode 100644 index 0000000..71eed03 --- /dev/null +++ b/bot/.dockerignore @@ -0,0 +1,89 @@ +# Git +.git +.gitignore + +# Documentation +README.md +*.md + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +frontend/build +dist +build + +# Development tools +.vscode +.idea +*.swp +*.swo + +# Testing +coverage +.nyc_output + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp + +# Database files +*.db +*.sqlite + +# SSL certificates (for security) +ssl/ +*.pem +*.crt +*.key + +# Backup files +*.bak +*.backup diff --git a/bot/.env.example b/bot/.env.example new file mode 100644 index 0000000..f1155a7 --- /dev/null +++ b/bot/.env.example @@ -0,0 +1,37 @@ +# Discord Bot Configuration +DISCORD_TOKEN=your_discord_bot_token_here +DISCORD_CLIENT_ID=your_discord_client_id_here +DISCORD_CLIENT_SECRET=your_discord_client_secret_here + +# Database Configuration +DATABASE_URL=postgresql://username:password@localhost:5432/skrzynka_impostora +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=skrzynka_impostora +DB_USER=username +DB_PASSWORD=password + +# Web Panel Configuration +JWT_SECRET=your_jwt_secret_here +SESSION_SECRET=your_session_secret_here +WEB_PORT=3001 +API_PORT=3000 + +# OAuth2 Configuration +OAUTH2_REDIRECT_URI=http://localhost:3001/auth/discord/callback + +# Environment +NODE_ENV=development + +# Logging +LOG_LEVEL=info + +# Redis Configuration (optional) +REDIS_URL=redis://localhost:6379 + +# Docker specific overrides (uncomment for Docker usage) +# DB_HOST=postgres +# REDIS_URL=redis://redis:6379 + +# Production domain (for OAuth2 and CORS) +# PRODUCTION_DOMAIN=https://your-domain.com diff --git a/bot/.github/copilot-instructions.md b/bot/.github/copilot-instructions.md new file mode 100644 index 0000000..2b2c10d --- /dev/null +++ b/bot/.github/copilot-instructions.md @@ -0,0 +1,85 @@ + + +# Skrzynka Impostora Bot - Instrukcje dla Copilot + +## Opis projektu +To jest projekt Discord Bot o nazwie "Skrzynka Impostora Bot" służący do zarządzania wiadomościami powiatalnymi na serwerach Discord. Projekt składa się z: + +- **Backend**: Node.js + Express z biblioteką discord.js +- **Frontend**: React SPA (Single Page Application) +- **Baza danych**: PostgreSQL +- **Funkcjonalności**: Discord slash commands, panel web OAuth2, edytor wiadomości + +## Struktura projektu +``` +/backend/ - Serwer Node.js i bot Discord + /commands/ - Slash commands dla Discord + /database/ - Zarządzanie bazą danych + /web/ - API serwer dla panelu web +/frontend/ - React aplikacja (panel administracyjny) + /src/components/ - Komponenty React + /src/pages/ - Strony aplikacji + /src/services/ - Serwisy API + /src/hooks/ - React hooks +/database/ - Skrypty migracji i seedowania +/shared/ - Współdzielone utilities +``` + +## Kluczowe technologie +- **Discord.js v14** - Biblioteka Discord API +- **Express.js** - Web framework +- **PostgreSQL** - Baza danych relacyjna +- **React 18** - Frontend framework +- **JWT** - Autoryzacja +- **OAuth2** - Logowanie przez Discord + +## Główne funkcjonalności +1. **Slash Commands**: + - `/skrzynka` - komendy użytkownika + - `/skrzynka-adm` - komendy administracyjne + +2. **Panel Web**: + - Logowanie przez Discord OAuth2 + - Edytor wiadomości z podglądem + - Zarządzanie serwerami + - Historia zmian + +3. **Zarządzanie wiadomościami**: + - Wsparcie Discord Markdown + - Live preview + - Walidacja długości (2000 znaków) + - Automatyczna aktualizacja na Discord + +## Style kodowania +- Używaj ES6+ składni +- Preferuj async/await nad Promise.then() +- Zastosuj destructuring tam gdzie możliwe +- Używaj const/let zamiast var +- Komponenty React jako funkcyjne z hooks +- Nazwy plików: PascalCase dla komponentów, camelCase dla utilities + +## Bezpieczeństwo +- Wszystkie zapytania API wymagają JWT token +- Walidacja danych wejściowych przez express-validator +- Helmet.js dla zabezpieczeń HTTP +- CORS skonfigurowany dla specific origins +- SQL prepared statements (pg library) + +## Środowisko i konfiguracja +- Zmienne środowiskowe w pliku .env +- Development: localhost:3000 (API), localhost:3001 (React) +- Production: buildy React serwowane przez Express + +## Konwencje nazewnictwa +- Tabele bazy: snake_case (np. welcome_messages) +- Kolumny bazy: snake_case (np. guild_id) +- JavaScript: camelCase (np. guildId) +- Komponenty React: PascalCase (np. MessageEditor) +- CSS classes: kebab-case (np. discord-button) + +## Pomocne wskazówki +- Bot wymaga uprawnień MANAGE_CHANNELS do działania +- Discord API ma limit 2000 znaków na wiadomość +- Używaj PostgreSQL JSONB dla embed_data +- Zawsze dodawaj error handling dla Discord API calls +- Frontend używa Tailwind-like CSS classes w App.css diff --git a/bot/.gitignore b/bot/.gitignore new file mode 100644 index 0000000..4818346 --- /dev/null +++ b/bot/.gitignore @@ -0,0 +1,73 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production builds +frontend/build/ +dist/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Database +*.db +*.sqlite +backup_*.sql + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# IDE +.vscode/settings.json +.vscode/launch.json +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp + +# Discord bot specific +config.json +database/data/ + +# Docker +docker-compose.override.yml +.docker/ + +# SSL certificates +ssl/ +*.pem +*.crt +*.key +*.p12 + +# Backup files +*.bak +*.backup diff --git a/bot/.vscode/tasks.json b/bot/.vscode/tasks.json new file mode 100644 index 0000000..07f2605 --- /dev/null +++ b/bot/.vscode/tasks.json @@ -0,0 +1,150 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Development Server", + "type": "shell", + "command": "npm", + "args": [ + "run", + "dev" + ], + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "Docker: Start Development", + "type": "shell", + "command": "docker", + "args": [ + "compose", + "-f", + "docker-compose.dev.yml", + "up", + "--build" + ], + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "Docker: Stop Development", + "type": "shell", + "command": "docker", + "args": [ + "compose", + "-f", + "docker-compose.dev.yml", + "down" + ], + "group": "build", + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "Docker: Build Image", + "type": "shell", + "command": "docker", + "args": [ + "build", + "-t", + "skrzynka-impostora-bot:latest", + "." + ], + "group": "build", + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "Docker: View Logs", + "type": "shell", + "command": "docker", + "args": [ + "compose", + "-f", + "docker-compose.dev.yml", + "logs", + "-f" + ], + "group": "test", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "Deploy Commands to Discord", + "type": "shell", + "command": "npm", + "args": [ + "run", + "deploy" + ], + "group": "build", + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}" + } + } + ] +} \ No newline at end of file diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..79d387e --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,61 @@ +# Multi-stage build dla optymalizacji +FROM node:18-alpine AS base + +# Ustaw workdir +WORKDIR /app + +# Kopiuj package.json dla głównego projektu +COPY package*.json ./ + +# Zainstaluj zależności głównego projektu +RUN npm ci --only=production + +# Stage dla frontendu +FROM node:18-alpine AS frontend-build + +WORKDIR /app/frontend + +# Kopiuj package.json frontendu +COPY frontend/package*.json ./ +RUN npm ci + +# Kopiuj kod frontendu +COPY frontend/ ./ + +# Zbuduj frontend +RUN npm run build + +# Stage dla backend +FROM node:18-alpine AS backend + +WORKDIR /app + +# Kopiuj zależności z base stage +COPY --from=base /app/node_modules ./node_modules +COPY package*.json ./ + +# Kopiuj kod backend +COPY backend/ ./backend/ +COPY database/ ./database/ +COPY shared/ ./shared/ + +# Kopiuj zbudowany frontend +COPY --from=frontend-build /app/frontend/build ./frontend/build + +# Stwórz użytkownika non-root +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nodejs -u 1001 + +# Zmień ownership plików +RUN chown -R nodejs:nodejs /app +USER nodejs + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" + +# Uruchom aplikację +CMD ["node", "backend/index.js"] diff --git a/bot/Dockerfile.dev b/bot/Dockerfile.dev new file mode 100644 index 0000000..d27b50c --- /dev/null +++ b/bot/Dockerfile.dev @@ -0,0 +1,38 @@ +# Development Dockerfile z hot reload +FROM node:18-alpine + +# Zainstaluj nodemon globalnie +RUN npm install -g nodemon concurrently + +# Ustaw workdir +WORKDIR /app + +# Kopiuj package.json files +COPY package*.json ./ +COPY frontend/package*.json ./frontend/ + +# Zainstaluj wszystkie zależności (including dev dependencies) +RUN npm install + +# Zainstaluj zależności frontendu +WORKDIR /app/frontend +RUN npm install + +# Wróć do głównego katalogu +WORKDIR /app + +# Expose ports +EXPOSE 3000 3001 9229 + +# Kopiuj pozostałe pliki (będzie nadpisane przez volume w dev) +COPY . . + +# Stwórz katalogi dla logów +RUN mkdir -p logs + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" || exit 1 + +# Default command (może być nadpisane w docker-compose) +CMD ["npm", "run", "dev"] diff --git a/bot/Makefile b/bot/Makefile new file mode 100644 index 0000000..73a486a --- /dev/null +++ b/bot/Makefile @@ -0,0 +1,102 @@ +# Makefile dla Skrzynka Impostora Bot +# Użycie: make + +.PHONY: help dev dev-down prod prod-down build logs clean deploy-commands + +# Default target +help: ## Pokaż dostępne komendy + @echo "Dostępne komendy dla Skrzynka Impostora Bot:" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +dev: ## Uruchom środowisko development + @echo "🚀 Uruchamianie środowiska development..." + @if [ ! -f .env ]; then \ + echo "⚠️ Kopiuję .env.example do .env"; \ + cp .env.example .env; \ + echo "📝 Uzupełnij zmienne w pliku .env przed kontynuowaniem"; \ + fi + docker compose -f docker-compose.dev.yml up --build -d + @echo "✅ Środowisko uruchomione!" + @echo "📋 Dostępne usługi:" + @echo " 🤖 Bot + Panel: http://localhost:3000" + @echo " 📊 pgAdmin: http://localhost:8080" + +dev-down: ## Zatrzymaj środowisko development + @echo "🛑 Zatrzymywanie środowiska development..." + docker compose -f docker-compose.dev.yml down + +dev-rebuild: ## Przebuduj i uruchom development + @echo "🔄 Przebudowywanie środowiska development..." + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.dev.yml up --build -d + +prod: ## Uruchom środowisko produkcyjne + @echo "🚀 Uruchamianie środowiska produkcyjnego..." + docker compose up --build -d + @echo "✅ Produkcja uruchomiona!" + +prod-down: ## Zatrzymaj środowisko produkcyjne + @echo "🛑 Zatrzymywanie środowiska produkcyjnego..." + docker compose down + +build: ## Zbuduj obraz Docker + @echo "🏗️ Budowanie obrazu Docker..." + docker build -t skrzynka-impostora-bot:latest . + +logs: ## Pokaż logi development + docker compose -f docker-compose.dev.yml logs -f + +logs-prod: ## Pokaż logi produkcji + docker compose logs -f + +clean: ## Wyczyść wszystkie kontenery i volumes + @echo "🧹 Czyszczenie kontenerów i volumes..." + docker compose -f docker-compose.dev.yml down -v + docker compose down -v + docker system prune -f + +clean-all: ## Wyczyść wszystko włącznie z obrazami + @echo "🧹 Czyszczenie wszystkiego..." + docker compose -f docker-compose.dev.yml down -v + docker compose down -v + docker rmi skrzynka-impostora-bot:latest 2>/dev/null || true + docker system prune -af + +deploy-commands: ## Deploy komend Discord (wymaga DISCORD_TOKEN) + @if [ -z "$(DISCORD_TOKEN)" ]; then \ + echo "❌ Brak DISCORD_TOKEN. Użyj: make deploy-commands DISCORD_TOKEN=twoj_token"; \ + exit 1; \ + fi + @echo "📡 Deployowanie komend Discord..." + docker run --rm \ + -e DISCORD_TOKEN="$(DISCORD_TOKEN)" \ + -e DISCORD_CLIENT_ID="$(DISCORD_CLIENT_ID)" \ + skrzynka-impostora-bot:latest \ + node backend/deploy-commands.js + +shell: ## Wejdź do kontenera bota (development) + docker compose -f docker-compose.dev.yml exec bot-dev sh + +shell-prod: ## Wejdź do kontenera bota (produkcja) + docker compose exec bot sh + +db-shell: ## Wejdź do bazy danych PostgreSQL + docker compose -f docker-compose.dev.yml exec postgres-dev psql -U dev_user -d skrzynka_impostora_dev + +backup-db: ## Stwórz backup bazy danych + @echo "💾 Tworzenie backupu bazy danych..." + docker compose -f docker-compose.dev.yml exec postgres-dev pg_dump -U dev_user skrzynka_impostora_dev > backup_$(shell date +%Y%m%d_%H%M%S).sql + @echo "✅ Backup utworzony!" + +status: ## Pokaż status kontenerów + @echo "📊 Status kontenerów development:" + docker compose -f docker-compose.dev.yml ps + @echo "" + @echo "📊 Status kontenerów produkcji:" + docker compose ps + +# Aliasy dla wygody +up: dev ## Alias dla dev +down: dev-down ## Alias dla dev-down +restart: dev-rebuild ## Alias dla dev-rebuild diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..b6a4a09 --- /dev/null +++ b/bot/README.md @@ -0,0 +1,373 @@ +# 🎭 Skrzynka Impostora Bot + +Kompleksowy bot Discord do zarządzania wiadomościami powaitalnymi z panelem web administracyjnym. + +## 📋 Funkcjonalności + +### ✨ Wersja 1.0 +- ✅ Integracja z Discord API (discord.js v14) +- ✅ Slash commands (`/skrzynka`, `/skrzynka-adm`) +- ✅ Panel web do zarządzania wiadomościami +- ✅ Edytor z podglądem Discord Markdown +- ✅ Autoryzacja OAuth2 Discord +- ✅ Historia zmian wiadomości +- ✅ Baza danych PostgreSQL + +### 🚀 Planowane funkcjonalności +- Wielojęzyczność +- Szablony wiadomości +- Harmonogramy wysyłki +- Statystyki i analizy +- System ról użytkowników + +## 🛠️ Technologie + +- **Backend**: Node.js, Express.js, discord.js +- **Frontend**: React 18, React Router +- **Baza danych**: PostgreSQL +- **Autoryzacja**: JWT, OAuth2 Discord +- **Styling**: Custom CSS (Discord-like) + +## 📦 Instalacja + +### Wymagania + +**Dla Docker (Rekomendowane):** +- Docker Desktop >= 4.0 +- Docker Compose v2 +- Konto Discord Developer + +**Dla lokalnego uruchomienia:** +- Node.js >= 18.0.0 +- PostgreSQL >= 12 +- npm >= 8.0.0 +- Konto Discord Developer + +### 1. Sklonuj repozytorium +```bash +git clone +cd skrzynka-impostora-bot +``` + +### 2. Zainstaluj zależności +```bash +# Backend dependencies +npm install + +# Frontend dependencies +cd frontend +npm install +cd .. +``` + +### 3. Konfiguracja Discord Bot + +1. Idź do [Discord Developer Portal](https://discord.com/developers/applications) +2. Utwórz nową aplikację +3. W sekcji "Bot" utwórz bota i skopiuj token +4. W sekcji "OAuth2" dodaj redirect URI: `http://localhost:3001/auth/discord/callback` + +### 4. Konfiguracja bazy danych + +```bash +# Utwórz bazę danych PostgreSQL +createdb skrzynka_impostora + +# Lub w psql: +CREATE DATABASE skrzynka_impostora; +``` + +### 5. Zmienne środowiskowe + +Skopiuj `.env.example` do `.env` i wypełnij: + +```bash +cp .env.example .env +``` + +Edytuj `.env`: +```env +# Discord Bot Configuration +DISCORD_TOKEN=your_discord_bot_token_here +DISCORD_CLIENT_ID=your_discord_client_id_here +DISCORD_CLIENT_SECRET=your_discord_client_secret_here + +# Database Configuration +DATABASE_URL=postgresql://username:password@localhost:5432/skrzynka_impostora +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=skrzynka_impostora +DB_USER=your_username +DB_PASSWORD=your_password + +# Web Panel Configuration +JWT_SECRET=your_secure_random_string_here +SESSION_SECRET=another_secure_random_string +WEB_PORT=3001 +API_PORT=3000 + +# OAuth2 Configuration +OAUTH2_REDIRECT_URI=http://localhost:3001/auth/discord/callback + +# Environment +NODE_ENV=development +``` + +### 6. Inicjalizacja bazy danych + +```bash +# Uruchom migracje +npm run db:migrate + +# Opcjonalnie: dodaj przykładowe dane +npm run db:seed +``` + +### 7. Deploy komend Discord + +```bash +npm run deploy +``` + +## 🚀 Uruchomienie + +### 🐳 Przez Docker (Rekomendowane) + +**Development:** +```bash +# Automatyczny skrypt (Linux/macOS) +./scripts/dev-start.sh + +# Automatyczny skrypt (Windows) +scripts\dev-start.bat + +# Lub ręcznie +docker compose -f docker-compose.dev.yml up --build +``` + +**Produkcja:** +```bash +# Skrypt deploy (Linux/macOS) +./scripts/prod-deploy.sh + +# Lub ręcznie +docker compose up --build -d +``` + +### 💻 Lokalnie (bez Docker) + +### Development (równoczesne uruchomienie backend + frontend) +```bash +npm run dev +``` + +### Osobno Backend i Frontend + +**Backend:** +```bash +npm run dev:backend +``` + +**Frontend:** +```bash +npm run dev:frontend +``` + +### Produkcja +```bash +# Zbuduj frontend +npm run build + +# Uruchom serwer +npm start +``` + +## 📖 Użytkowanie + +### 1. Zaproszenie bota na serwer + +Użyj linku z odpowiednimi uprawnieniami: +``` +https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=2147484672&scope=bot%20applications.commands +``` + +### 2. Konfiguracja kanału powitalnego + +Na serwerze Discord użyj komendy: +``` +/skrzynka-adm set-welcome channel:#witamy +``` + +### 3. Zarządzanie przez panel web + +1. Idź do `http://localhost:3001` (development) +2. Zaloguj się przez Discord +3. Wybierz serwer +4. Edytuj wiadomość powitalną + +### 4. Komendy Discord + +**Komendy użytkownika:** +- `/skrzynka info` - Informacje o bocie +- `/skrzynka help` - Pomoc + +**Komendy administracyjne:** +- `/skrzynka-adm set-welcome ` - Ustaw kanał powitalny +- `/skrzynka-adm welcome` - Wyślij wiadomość powitalną +- `/skrzynka-adm welcome-update` - Zaktualizuj wiadomość +- `/skrzynka-adm status` - Status konfiguracji + +## 🗄️ Struktura bazy danych + +```sql +-- Serwery Discord +guilds (id, name, created_at, updated_at) + +-- Kanały powitalne +welcome_channels (id, guild_id, channel_id, channel_name, is_active, created_at, updated_at) + +-- Wiadomości powitalne +welcome_messages (id, guild_id, content, embed_data, message_id, is_active, created_at, updated_at) + +-- Użytkownicy panelu +users (id, discord_id, username, discriminator, avatar, email, role, is_active, last_login, created_at, updated_at) + +-- Historia zmian +message_revisions (id, message_id, content, embed_data, user_id, revision_number, created_at) + +-- Uprawnienia użytkowników +user_guild_permissions (id, user_id, guild_id, role, granted_by, created_at) +``` + +## 🔧 Development + +### Dostępne skrypty + +```bash +npm run start # Uruchom produkcyjnie +npm run dev # Development (backend + frontend) +npm run dev:backend # Tylko backend +npm run dev:frontend # Tylko frontend +npm run build # Zbuduj frontend +npm run deploy # Deploy komend Discord +npm run db:migrate # Migracja bazy +npm run db:seed # Seed bazy danych + +# Docker skrypty +npm run docker:dev # Uruchom development w Docker +npm run docker:dev:down # Zatrzymaj development Docker +npm run docker:prod # Uruchom produkcję w Docker +npm run docker:build # Zbuduj obraz Docker +npm run docker:logs # Pokaż logi Docker +``` + +### 🐳 Docker Development + +**Dostępne usługi w trybie development:** +- **Bot + API**: http://localhost:3000 +- **Panel Web**: http://localhost:3001 +- **PostgreSQL**: localhost:5433 +- **pgAdmin**: http://localhost:8080 (admin@skrzynka.local / admin) +- **Redis**: localhost:6380 + +**Użyteczne komendy Docker:** +```bash +# Wyświetl logi konkretnej usługi +docker compose -f docker-compose.dev.yml logs -f bot-dev + +# Wejdź do kontenera bota +docker compose -f docker-compose.dev.yml exec bot-dev sh + +# Restart konkretnej usługi +docker compose -f docker-compose.dev.yml restart bot-dev + +# Wyczyść wszystko i rozpocznij od nowa +docker compose -f docker-compose.dev.yml down -v +docker compose -f docker-compose.dev.yml up --build +``` + +### 🔄 Hot Reload + +W trybie development Docker automatycznie: +- Monitoruje zmiany w kodzie (volume mount) +- Restartuje backend przy zmianach (nodemon) +- Przebudowuje frontend na żywo +- Automatycznie stosuje migracje bazy danych + +### Struktura projektu + +``` +├── backend/ # Serwer Node.js +│ ├── index.js # Główny plik bota +│ ├── deploy-commands.js # Deploy slash commands +│ ├── commands/ # Slash commands +│ ├── database/ # Zarządzanie bazą +│ └── web/ # API serwer +├── frontend/ # React aplikacja +│ ├── src/ +│ │ ├── components/ # Komponenty React +│ │ ├── pages/ # Strony aplikacji +│ │ ├── services/ # API client +│ │ └── hooks/ # React hooks +│ └── public/ # Pliki statyczne +├── database/ # Skrypty bazy danych +├── shared/ # Współdzielone utilities +└── .github/ # GitHub konfiguracja +``` + +## 🔒 Bezpieczeństwo + +- JWT tokens dla autoryzacji API +- OAuth2 Discord dla logowania +- Helmet.js dla zabezpieczeń HTTP +- CORS protection +- SQL injection protection (prepared statements) +- Input validation (express-validator) + +## 📊 Monitoring i Logi + +Bot loguje ważne zdarzenia: +- Startup/shutdown +- Command execution +- Database operations +- API requests +- Errors and warnings + +## 🤝 Contribucje + +1. Fork repository +2. Utwórz feature branch (`git checkout -b feature/amazing-feature`) +3. Commit changes (`git commit -m 'Add amazing feature'`) +4. Push branch (`git push origin feature/amazing-feature`) +5. Otwórz Pull Request + +## 📄 Licencja + +MIT License - zobacz plik `LICENSE` + +## 🆘 Pomoc i wsparcie + +- Sprawdź [Issues](../../issues) dla znanych problemów +- Utwórz nowy Issue dla bugów lub feature requests +- Przeczytaj dokumentację Discord.js: https://discord.js.org/ + +## 📈 Roadmap + +### v1.1 - Zarządzanie +- [ ] System ról użytkowników +- [ ] Bulk operations +- [ ] Advanced permissions + +### v1.2 - Funkcjonalności +- [ ] Message templates +- [ ] Scheduled messages +- [ ] Multiple languages + +### v1.3 - Analytics +- [ ] Usage statistics +- [ ] User engagement metrics +- [ ] Performance monitoring + +--- + +Stworzono z ❤️ dla społeczności Discord diff --git a/bot/backend/commands/index.js b/bot/backend/commands/index.js new file mode 100644 index 0000000..5eec057 --- /dev/null +++ b/bot/backend/commands/index.js @@ -0,0 +1,440 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); + +class SlashCommands { + constructor(database) { + this.db = database; + this.commands = this.setupCommands(); + } + + setupCommands() { + return [ + // Komenda informacyjna dla użytkowników + new SlashCommandBuilder() + .setName('skrzynka') + .setDescription('Informacje o bocie Skrzynka Impostora') + .addSubcommand(subcommand => + subcommand + .setName('info') + .setDescription('Pokaż informacje o bocie') + ) + .addSubcommand(subcommand => + subcommand + .setName('help') + .setDescription('Pokaż dostępne komendy') + ), + + // Komendy administracyjne + new SlashCommandBuilder() + .setName('skrzynka-adm') + .setDescription('Komendy administracyjne dla bota Skrzynka Impostora') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels) + .addSubcommand(subcommand => + subcommand + .setName('set-welcome') + .setDescription('Ustaw kanał powitalny') + .addChannelOption(option => + option + .setName('channel') + .setDescription('Kanał na którym będzie wyświetlana wiadomość powitalna') + .setRequired(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('welcome') + .setDescription('Wyślij wiadomość powitalną') + ) + .addSubcommand(subcommand => + subcommand + .setName('welcome-update') + .setDescription('Zaktualizuj wiadomość powitalną z bazy danych') + ) + .addSubcommand(subcommand => + subcommand + .setName('status') + .setDescription('Pokaż status konfiguracji bota') + ) + ]; + } + + getCommandsData() { + return this.commands.map(command => command.toJSON()); + } + + async handleCommand(interaction) { + const { commandName, options } = interaction; + + switch (commandName) { + case 'skrzynka': + await this.handleUserCommands(interaction, options); + break; + case 'skrzynka-adm': + await this.handleAdminCommands(interaction, options); + break; + default: + await interaction.reply({ + content: '❌ Nieznana komenda.', + ephemeral: true + }); + } + } + + async handleUserCommands(interaction, options) { + const subcommand = options.getSubcommand(); + + switch (subcommand) { + case 'info': + await this.showBotInfo(interaction); + break; + case 'help': + await this.showHelp(interaction); + break; + } + } + + async handleAdminCommands(interaction, options) { + const subcommand = options.getSubcommand(); + + // Sprawdź uprawnienia użytkownika + if (!interaction.member.permissions.has(PermissionFlagsBits.ManageChannels)) { + await interaction.reply({ + content: '❌ Nie masz uprawnień do używania komend administracyjnych.', + ephemeral: true + }); + return; + } + + switch (subcommand) { + case 'set-welcome': + await this.setWelcomeChannel(interaction, options); + break; + case 'welcome': + await this.sendWelcomeMessage(interaction); + break; + case 'welcome-update': + await this.updateWelcomeMessage(interaction); + break; + case 'status': + await this.showStatus(interaction); + break; + } + } + + async showBotInfo(interaction) { + const embed = new EmbedBuilder() + .setColor(0x00AE86) + .setTitle('🎭 Skrzynka Impostora Bot') + .setDescription('Bot do zarządzania wiadomościami powaitalnymi na serwerze Discord.') + .addFields( + { name: '📝 Wersja', value: '1.0.0', inline: true }, + { name: '🔧 Panel Web', value: 'Dostępny dla administratorów', inline: true }, + { name: '⚡ Status', value: 'Online', inline: true }, + { name: '💻 Funkcje', value: '• Automatyczne wiadomości powitalne\n• Panel zarządzania web\n• Historia zmian\n• Konfiguracja kanałów', inline: false } + ) + .setThumbnail(interaction.client.user.displayAvatarURL()) + .setFooter({ + text: 'Skrzynka Impostora Bot', + iconURL: interaction.client.user.displayAvatarURL() + }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + } + + async showHelp(interaction) { + const embed = new EmbedBuilder() + .setColor(0x0099FF) + .setTitle('📚 Pomoc - Dostępne komendy') + .addFields( + { + name: '👥 Komendy użytkownika', + value: '`/skrzynka info` - Informacje o bocie\n`/skrzynka help` - Ta pomoc', + inline: false + }, + { + name: '🛠️ Komendy administracyjne', + value: '`/skrzynka-adm set-welcome` - Ustaw kanał powitalny\n`/skrzynka-adm welcome` - Wyślij wiadomość powitalną\n`/skrzynka-adm welcome-update` - Zaktualizuj wiadomość\n`/skrzynka-adm status` - Status konfiguracji', + inline: false + }, + { + name: '🌐 Panel Web', + value: 'Zarządzanie treścią wiadomości dostępne przez panel web.', + inline: false + } + ) + .setFooter({ + text: 'Potrzebujesz pomocy? Skontaktuj się z administratorem serwera.', + iconURL: interaction.client.user.displayAvatarURL() + }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + } + + async setWelcomeChannel(interaction, options) { + const channel = options.getChannel('channel'); + const guildId = interaction.guild.id; + const guildName = interaction.guild.name; + + try { + // Sprawdź czy bot ma uprawnienia do pisania na kanale + if (!channel.permissionsFor(interaction.client.user).has(PermissionFlagsBits.SendMessages)) { + await interaction.reply({ + content: '❌ Bot nie ma uprawnień do pisania na wybranym kanale.', + ephemeral: true + }); + return; + } + + // Dodaj/zaktualizuj serwer w bazie + await this.db.addGuild(guildId, guildName); + + // Ustaw kanał powitalny + await this.db.setWelcomeChannel(guildId, channel.id, channel.name); + + const embed = new EmbedBuilder() + .setColor(0x00FF00) + .setTitle('✅ Kanał powitalny ustawiony') + .setDescription(`Kanał ${channel} został ustawiony jako kanał powitalny.`) + .addFields( + { name: 'Kanał', value: `${channel.name} (${channel.id})`, inline: false }, + { name: 'Następne kroki', value: 'Skonfiguruj treść wiadomości przez panel web lub użyj `/skrzynka-adm welcome` aby wysłać domyślną wiadomość.', inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (error) { + console.error('Błąd podczas ustawiania kanału powitalnego:', error); + await interaction.reply({ + content: '❌ Wystąpił błąd podczas ustawiania kanału powitalnego.', + ephemeral: true + }); + } + } + + async sendWelcomeMessage(interaction) { + const guildId = interaction.guild.id; + + try { + await interaction.deferReply({ ephemeral: true }); + + // Pobierz konfigurację kanału + const welcomeChannelConfig = await this.db.getWelcomeChannel(guildId); + if (!welcomeChannelConfig) { + await interaction.editReply({ + content: '❌ Kanał powitalny nie został skonfigurowany. Użyj `/skrzynka-adm set-welcome` aby go ustawić.' + }); + return; + } + + // Pobierz kanał + const channel = interaction.guild.channels.cache.get(welcomeChannelConfig.channel_id); + if (!channel) { + await interaction.editReply({ + content: '❌ Skonfigurowany kanał powitalny nie istnieje lub bot nie ma do niego dostępu.' + }); + return; + } + + // Pobierz wiadomość z bazy lub użyj domyślnej + let welcomeMessage = await this.db.getWelcomeMessage(guildId); + let messageContent, embedData; + + if (welcomeMessage) { + messageContent = welcomeMessage.content; + embedData = welcomeMessage.embed_data; + } else { + messageContent = this.getDefaultWelcomeMessage(interaction.guild); + embedData = null; + } + + // Wyślij wiadomość + const messageOptions = { content: messageContent }; + if (embedData) { + messageOptions.embeds = [new EmbedBuilder(embedData)]; + } + + const sentMessage = await channel.send(messageOptions); + + // Zapisz lub zaktualizuj wiadomość w bazie + if (welcomeMessage) { + await this.db.updateWelcomeMessage(guildId, messageContent, embedData, sentMessage.id); + } else { + await this.db.saveWelcomeMessage(guildId, messageContent, embedData, sentMessage.id); + } + + const successEmbed = new EmbedBuilder() + .setColor(0x00FF00) + .setTitle('✅ Wiadomość powitalna wysłana') + .setDescription(`Wiadomość została wysłana na kanał ${channel}.`) + .addFields( + { name: 'Kanał', value: `${channel.name}`, inline: true }, + { name: 'ID wiadomości', value: `${sentMessage.id}`, inline: true } + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [successEmbed] }); + + } catch (error) { + console.error('Błąd podczas wysyłania wiadomości powitalnej:', error); + await interaction.editReply({ + content: '❌ Wystąpił błąd podczas wysyłania wiadomości powitalnej.' + }); + } + } + + async updateWelcomeMessage(interaction) { + const guildId = interaction.guild.id; + + try { + await interaction.deferReply({ ephemeral: true }); + + // Pobierz konfigurację i wiadomość + const welcomeChannelConfig = await this.db.getWelcomeChannel(guildId); + const welcomeMessage = await this.db.getWelcomeMessage(guildId); + + if (!welcomeChannelConfig || !welcomeMessage) { + await interaction.editReply({ + content: '❌ Kanał powitalny lub wiadomość nie zostały skonfigurowane.' + }); + return; + } + + const channel = interaction.guild.channels.cache.get(welcomeChannelConfig.channel_id); + if (!channel) { + await interaction.editReply({ + content: '❌ Skonfigurowany kanał powitalny nie istnieje.' + }); + return; + } + + // Znajdź i zaktualizuj wiadomość + if (welcomeMessage.message_id) { + try { + const message = await channel.messages.fetch(welcomeMessage.message_id); + + const messageOptions = { content: welcomeMessage.content }; + if (welcomeMessage.embed_data) { + messageOptions.embeds = [new EmbedBuilder(welcomeMessage.embed_data)]; + } + + await message.edit(messageOptions); + + const successEmbed = new EmbedBuilder() + .setColor(0x00FF00) + .setTitle('✅ Wiadomość zaktualizowana') + .setDescription(`Wiadomość powitalna została zaktualizowana na kanale ${channel}.`) + .setTimestamp(); + + await interaction.editReply({ embeds: [successEmbed] }); + + } catch (fetchError) { + // Jeśli nie można znaleźć wiadomości, wyślij nową + await this.sendWelcomeMessage(interaction); + } + } else { + // Jeśli nie ma ID wiadomości, wyślij nową + await this.sendWelcomeMessage(interaction); + } + + } catch (error) { + console.error('Błąd podczas aktualizacji wiadomości powitalnej:', error); + await interaction.editReply({ + content: '❌ Wystąpił błąd podczas aktualizacji wiadomości powitalnej.' + }); + } + } + + async showStatus(interaction) { + const guildId = interaction.guild.id; + + try { + const welcomeChannelConfig = await this.db.getWelcomeChannel(guildId); + const welcomeMessage = await this.db.getWelcomeMessage(guildId); + + const embed = new EmbedBuilder() + .setColor(0x0099FF) + .setTitle('📊 Status konfiguracji bota') + .setDescription(`Status dla serwera: **${interaction.guild.name}**`); + + if (welcomeChannelConfig) { + const channel = interaction.guild.channels.cache.get(welcomeChannelConfig.channel_id); + embed.addFields({ + name: '📍 Kanał powitalny', + value: channel ? `${channel.name} (${channel.id})` : `❌ Kanał niedostępny (${welcomeChannelConfig.channel_id})`, + inline: false + }); + } else { + embed.addFields({ + name: '📍 Kanał powitalny', + value: '❌ Nie skonfigurowany', + inline: false + }); + } + + if (welcomeMessage) { + embed.addFields( + { + name: '💬 Wiadomość powitalna', + value: '✅ Skonfigurowana w bazie danych', + inline: true + }, + { + name: '🆔 ID wiadomości', + value: welcomeMessage.message_id || 'Brak', + inline: true + }, + { + name: '📅 Ostatnia aktualizacja', + value: new Date(welcomeMessage.updated_at).toLocaleString('pl-PL'), + inline: true + } + ); + } else { + embed.addFields({ + name: '💬 Wiadomość powitalna', + value: '❌ Nie skonfigurowana (będzie użyta domyślna)', + inline: false + }); + } + + embed.setFooter({ + text: 'Użyj panelu web aby zarządzać treścią wiadomości', + iconURL: interaction.client.user.displayAvatarURL() + }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + + } catch (error) { + console.error('Błąd podczas pobierania statusu:', error); + await interaction.reply({ + content: '❌ Wystąpił błąd podczas pobierania statusu.', + ephemeral: true + }); + } + } + + getDefaultWelcomeMessage(guild) { + return `# 🎭 Witamy na serwerze ${guild.name}! + +Miło Cię tutaj widzieć! 👋 + +## 📋 Najważniejsze informacje: +• Przeczytaj regulamin serwera +• Przedstaw się w odpowiednim kanale +• Baw się dobrze i szanuj innych członków + +## 🎮 Funkcje serwera: +• Kanały tematyczne +• System ról +• Eventy i konkursy + +--- +*Ta wiadomość została wygenerowana przez Skrzynka Impostora Bot* +*Administratorzy mogą edytować treść przez panel web*`; + } +} + +module.exports = { SlashCommands }; diff --git a/bot/backend/database/DatabaseManager.js b/bot/backend/database/DatabaseManager.js new file mode 100644 index 0000000..562cc50 --- /dev/null +++ b/bot/backend/database/DatabaseManager.js @@ -0,0 +1,234 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +class DatabaseManager { + constructor() { + this.pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false + }); + } + + async initialize() { + try { + await this.createTables(); + console.log('Tabele bazy danych utworzone pomyślnie'); + } catch (error) { + console.error('Błąd podczas inicjalizacji bazy danych:', error); + throw error; + } + } + + async createTables() { + const createTablesSQL = ` + -- Tabela serwerów Discord + CREATE TABLE IF NOT EXISTS guilds ( + id VARCHAR(20) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Tabela kanałów powitalnych + CREATE TABLE IF NOT EXISTS welcome_channels ( + id SERIAL PRIMARY KEY, + guild_id VARCHAR(20) REFERENCES guilds(id) ON DELETE CASCADE, + channel_id VARCHAR(20) NOT NULL, + channel_name VARCHAR(100), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(guild_id) + ); + + -- Tabela wiadomości powitalnych + CREATE TABLE IF NOT EXISTS welcome_messages ( + id SERIAL PRIMARY KEY, + guild_id VARCHAR(20) REFERENCES guilds(id) ON DELETE CASCADE, + content TEXT NOT NULL, + embed_data JSONB, + message_id VARCHAR(20), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Tabela użytkowników panelu web + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + discord_id VARCHAR(20) UNIQUE NOT NULL, + username VARCHAR(32) NOT NULL, + discriminator VARCHAR(4), + avatar VARCHAR(255), + email VARCHAR(255), + role VARCHAR(20) DEFAULT 'viewer', + is_active BOOLEAN DEFAULT true, + last_login TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Tabela historii wersji wiadomości + CREATE TABLE IF NOT EXISTS message_revisions ( + id SERIAL PRIMARY KEY, + message_id INTEGER REFERENCES welcome_messages(id) ON DELETE CASCADE, + content TEXT NOT NULL, + embed_data JSONB, + user_id INTEGER REFERENCES users(id), + revision_number INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Tabela uprawnień użytkowników do serwerów + CREATE TABLE IF NOT EXISTS user_guild_permissions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + guild_id VARCHAR(20) REFERENCES guilds(id) ON DELETE CASCADE, + role VARCHAR(20) DEFAULT 'viewer', + granted_by INTEGER REFERENCES users(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, guild_id) + ); + + -- Indeksy dla wydajności + CREATE INDEX IF NOT EXISTS idx_guilds_id ON guilds(id); + CREATE INDEX IF NOT EXISTS idx_welcome_channels_guild_id ON welcome_channels(guild_id); + CREATE INDEX IF NOT EXISTS idx_welcome_messages_guild_id ON welcome_messages(guild_id); + CREATE INDEX IF NOT EXISTS idx_users_discord_id ON users(discord_id); + CREATE INDEX IF NOT EXISTS idx_message_revisions_message_id ON message_revisions(message_id); + CREATE INDEX IF NOT EXISTS idx_user_guild_permissions_user_guild ON user_guild_permissions(user_id, guild_id); + `; + + await this.pool.query(createTablesSQL); + } + + // Metody dla zarządzania serwerami + async addGuild(guildId, guildName) { + const query = ` + INSERT INTO guilds (id, name) + VALUES ($1, $2) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + updated_at = CURRENT_TIMESTAMP + `; + await this.pool.query(query, [guildId, guildName]); + } + + async getGuild(guildId) { + const query = 'SELECT * FROM guilds WHERE id = $1'; + const result = await this.pool.query(query, [guildId]); + return result.rows[0]; + } + + // Metody dla kanałów powitalnych + async setWelcomeChannel(guildId, channelId, channelName) { + const query = ` + INSERT INTO welcome_channels (guild_id, channel_id, channel_name) + VALUES ($1, $2, $3) + ON CONFLICT (guild_id) DO UPDATE SET + channel_id = EXCLUDED.channel_id, + channel_name = EXCLUDED.channel_name, + is_active = true, + updated_at = CURRENT_TIMESTAMP + `; + await this.pool.query(query, [guildId, channelId, channelName]); + } + + async getWelcomeChannel(guildId) { + const query = 'SELECT * FROM welcome_channels WHERE guild_id = $1 AND is_active = true'; + const result = await this.pool.query(query, [guildId]); + return result.rows[0]; + } + + // Metody dla wiadomości powitalnych + async saveWelcomeMessage(guildId, content, embedData = null, messageId = null) { + const query = ` + INSERT INTO welcome_messages (guild_id, content, embed_data, message_id) + VALUES ($1, $2, $3, $4) + RETURNING * + `; + const result = await this.pool.query(query, [guildId, content, embedData, messageId]); + return result.rows[0]; + } + + async updateWelcomeMessage(guildId, content, embedData = null, messageId = null) { + const query = ` + UPDATE welcome_messages + SET content = $2, embed_data = $3, message_id = $4, updated_at = CURRENT_TIMESTAMP + WHERE guild_id = $1 AND is_active = true + RETURNING * + `; + const result = await this.pool.query(query, [guildId, content, embedData, messageId]); + return result.rows[0]; + } + + async getWelcomeMessage(guildId) { + const query = 'SELECT * FROM welcome_messages WHERE guild_id = $1 AND is_active = true ORDER BY created_at DESC LIMIT 1'; + const result = await this.pool.query(query, [guildId]); + return result.rows[0]; + } + + // Metody dla użytkowników + async saveUser(discordUser) { + const query = ` + INSERT INTO users (discord_id, username, discriminator, avatar, email) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (discord_id) DO UPDATE SET + username = EXCLUDED.username, + discriminator = EXCLUDED.discriminator, + avatar = EXCLUDED.avatar, + email = EXCLUDED.email, + last_login = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + RETURNING * + `; + const result = await this.pool.query(query, [ + discordUser.id, + discordUser.username, + discordUser.discriminator || '0000', + discordUser.avatar, + discordUser.email + ]); + return result.rows[0]; + } + + async getUser(discordId) { + const query = 'SELECT * FROM users WHERE discord_id = $1'; + const result = await this.pool.query(query, [discordId]); + return result.rows[0]; + } + + // Metody dla historii wersji + async saveMessageRevision(messageId, content, embedData, userId) { + // Pobierz aktualny numer rewizji + const revisionQuery = 'SELECT COALESCE(MAX(revision_number), 0) + 1 as next_revision FROM message_revisions WHERE message_id = $1'; + const revisionResult = await this.pool.query(revisionQuery, [messageId]); + const nextRevision = revisionResult.rows[0].next_revision; + + const query = ` + INSERT INTO message_revisions (message_id, content, embed_data, user_id, revision_number) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + `; + const result = await this.pool.query(query, [messageId, content, embedData, userId, nextRevision]); + return result.rows[0]; + } + + async getMessageRevisions(messageId) { + const query = ` + SELECT mr.*, u.username + FROM message_revisions mr + LEFT JOIN users u ON mr.user_id = u.id + WHERE mr.message_id = $1 + ORDER BY mr.revision_number DESC + `; + const result = await this.pool.query(query, [messageId]); + return result.rows; + } + + async close() { + await this.pool.end(); + } +} + +module.exports = { DatabaseManager }; diff --git a/bot/backend/deploy-commands.js b/bot/backend/deploy-commands.js new file mode 100644 index 0000000..fa2c578 --- /dev/null +++ b/bot/backend/deploy-commands.js @@ -0,0 +1,43 @@ +const { REST, Routes } = require('discord.js'); +const { SlashCommands } = require('./commands'); +require('dotenv').config(); + +async function deployCommands() { + if (!process.env.DISCORD_TOKEN || !process.env.DISCORD_CLIENT_ID) { + console.error('❌ Brak wymaganych zmiennych środowiskowych (DISCORD_TOKEN, DISCORD_CLIENT_ID)'); + process.exit(1); + } + + const commands = new SlashCommands(); + const commandsData = commands.getCommandsData(); + + const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + + try { + console.log(`🚀 Rozpoczęto deploy ${commandsData.length} komend slash...`); + + // Deploy globalnych komend + const data = await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: commandsData } + ); + + console.log(`✅ Pomyślnie zdeployowano ${data.length} komend slash globalnie.`); + + // Wyświetl listę komend + console.log('\n📋 Zdeployowane komendy:'); + data.forEach(command => { + console.log(` • /${command.name} - ${command.description}`); + }); + + } catch (error) { + console.error('❌ Błąd podczas deployowania komend:', error); + } +} + +// Jeśli skrypt jest uruchamiany bezpośrednio +if (require.main === module) { + deployCommands(); +} + +module.exports = { deployCommands }; diff --git a/bot/backend/index.js b/bot/backend/index.js new file mode 100644 index 0000000..f29ffa4 --- /dev/null +++ b/bot/backend/index.js @@ -0,0 +1,113 @@ +const { Client, GatewayIntentBits, REST, Routes, EmbedBuilder } = require('discord.js'); +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); +const compression = require('compression'); +require('dotenv').config(); + +const { DatabaseManager } = require('./database/DatabaseManager'); +const { SlashCommands } = require('./commands'); +const { WebPanel } = require('./web/server'); + +class SkrzynkaImpostoraBot { + constructor() { + this.client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers + ] + }); + + this.db = new DatabaseManager(); + this.commands = new SlashCommands(this.db); + this.webPanel = new WebPanel(this.db); + + this.setupEventHandlers(); + } + + setupEventHandlers() { + this.client.once('ready', () => { + console.log(`✅ Bot zalogowany jako ${this.client.user.tag}`); + this.client.user.setActivity('Zarządzanie wiadomościami powiatalnymi', { type: 'WATCHING' }); + }); + + this.client.on('guildCreate', async (guild) => { + console.log(`Dodano do nowego serwera: ${guild.name} (${guild.id})`); + await this.db.addGuild(guild.id, guild.name); + }); + + this.client.on('interactionCreate', async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + try { + await this.commands.handleCommand(interaction); + } catch (error) { + console.error('Błąd podczas wykonywania komendy:', error); + + const errorEmbed = new EmbedBuilder() + .setColor(0xFF0000) + .setTitle('❌ Błąd') + .setDescription('Wystąpił błąd podczas wykonywania komendy.') + .setTimestamp(); + + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ embeds: [errorEmbed], ephemeral: true }); + } else { + await interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } + }); + + this.client.on('error', console.error); + } + + async start() { + try { + // Inicjalizacja bazy danych + await this.db.initialize(); + console.log('✅ Baza danych zainicjalizowana'); + + // Uruchomienie panelu web + await this.webPanel.start(); + console.log('✅ Panel web uruchomiony'); + + // Logowanie bota + await this.client.login(process.env.DISCORD_TOKEN); + console.log('✅ Bot Discord uruchomiony'); + + } catch (error) { + console.error('❌ Błąd podczas uruchamiania bota:', error); + process.exit(1); + } + } + + async deployCommands() { + const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + + try { + console.log('Rozpoczęto odświeżanie komend slash...'); + + const commands = this.commands.getCommandsData(); + + await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: commands } + ); + + console.log('✅ Komendy slash zostały pomyślnie odświeżone.'); + } catch (error) { + console.error('❌ Błąd podczas odświeżania komend:', error); + } + } +} + +// Uruchomienie bota +if (require.main === module) { + const bot = new SkrzynkaImpostoraBot(); + bot.start(); +} + +module.exports = { SkrzynkaImpostoraBot }; diff --git a/bot/backend/web/server.js b/bot/backend/web/server.js new file mode 100644 index 0000000..9639847 --- /dev/null +++ b/bot/backend/web/server.js @@ -0,0 +1,368 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); +const compression = require('compression'); +const jwt = require('jsonwebtoken'); +const { body, validationResult } = require('express-validator'); +require('dotenv').config(); + +class WebPanel { + constructor(database) { + this.db = database; + this.app = express(); + this.setupMiddleware(); + this.setupRoutes(); + } + + setupMiddleware() { + // Bezpieczeństwo + this.app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], + scriptSrc: ["'self'"], + fontSrc: ["'self'", "https://fonts.gstatic.com"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "https://discord.com", "https://discordapp.com"] + } + } + })); + + // CORS + this.app.use(cors({ + origin: process.env.NODE_ENV === 'production' + ? ['https://your-domain.com'] + : ['http://localhost:3001', 'http://127.0.0.1:3001'], + credentials: true + })); + + // Middleware podstawowe + this.app.use(compression()); + this.app.use(morgan('combined')); + this.app.use(express.json({ limit: '10mb' })); + this.app.use(express.urlencoded({ extended: true, limit: '10mb' })); + + // Serwowanie plików statycznych (dla built React app) + this.app.use(express.static('frontend/build')); + } + + setupRoutes() { + // API Routes + this.app.use('/api/auth', this.createAuthRoutes()); + this.app.use('/api/guilds', this.createGuildRoutes()); + this.app.use('/api/messages', this.createMessageRoutes()); + + // Health check + this.app.get('/api/health', (req, res) => { + res.json({ + status: 'OK', + timestamp: new Date().toISOString(), + version: '1.0.0' + }); + }); + + // Catch all handler dla React Router + this.app.get('*', (req, res) => { + res.sendFile('index.html', { root: 'frontend/build' }); + }); + + // Error handler + this.app.use(this.errorHandler.bind(this)); + } + + createAuthRoutes() { + const router = express.Router(); + + // Discord OAuth2 login + router.get('/discord', (req, res) => { + const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.OAUTH2_REDIRECT_URI)}&response_type=code&scope=identify%20guilds`; + res.redirect(discordAuthUrl); + }); + + // Discord OAuth2 callback + router.get('/discord/callback', async (req, res) => { + const { code } = req.query; + + if (!code) { + return res.status(400).json({ error: 'Brak kodu autoryzacji' }); + } + + try { + // Wymiana kodu na token + const tokenResponse = await fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + client_id: process.env.DISCORD_CLIENT_ID, + client_secret: process.env.DISCORD_CLIENT_SECRET, + code, + grant_type: 'authorization_code', + redirect_uri: process.env.OAUTH2_REDIRECT_URI, + }), + }); + + const tokenData = await tokenResponse.json(); + + if (!tokenData.access_token) { + throw new Error('Nie udało się uzyskać tokenu dostępu'); + } + + // Pobierz dane użytkownika + const userResponse = await fetch('https://discord.com/api/users/@me', { + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + }, + }); + + const userData = await userResponse.json(); + + // Pobierz serwery użytkownika + const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', { + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + }, + }); + + const guildsData = await guildsResponse.json(); + + // Zapisz użytkownika w bazie + const user = await this.db.saveUser(userData); + + // Generuj JWT token + const jwtToken = jwt.sign( + { + userId: user.id, + discordId: user.discord_id, + username: user.username, + role: user.role + }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + // Przekieruj z tokenem + res.redirect(`${process.env.NODE_ENV === 'production' ? 'https://your-domain.com' : 'http://localhost:3001'}/dashboard?token=${jwtToken}`); + + } catch (error) { + console.error('Błąd podczas autoryzacji Discord:', error); + res.status(500).json({ error: 'Błąd podczas autoryzacji' }); + } + }); + + // Wylogowanie + router.post('/logout', (req, res) => { + res.json({ message: 'Wylogowano pomyślnie' }); + }); + + // Weryfikacja tokenu + router.get('/verify', this.authenticateToken, (req, res) => { + res.json({ user: req.user }); + }); + + return router; + } + + createGuildRoutes() { + const router = express.Router(); + + // Pobierz serwery użytkownika + router.get('/', this.authenticateToken, async (req, res) => { + try { + // Tu powinieneś pobrać serwery z Discord API i porównać z bazą + // Na razie zwróć mockowane dane + res.json([ + { + id: '123456789', + name: 'Test Server', + icon: null, + hasBot: true, + userPermissions: ['MANAGE_CHANNELS'] + } + ]); + } catch (error) { + console.error('Błąd podczas pobierania serwerów:', error); + res.status(500).json({ error: 'Błąd podczas pobierania serwerów' }); + } + }); + + // Pobierz konfigurację serwera + router.get('/:guildId/config', this.authenticateToken, async (req, res) => { + try { + const { guildId } = req.params; + + const welcomeChannel = await this.db.getWelcomeChannel(guildId); + const welcomeMessage = await this.db.getWelcomeMessage(guildId); + + res.json({ + welcomeChannel, + welcomeMessage + }); + } catch (error) { + console.error('Błąd podczas pobierania konfiguracji:', error); + res.status(500).json({ error: 'Błąd podczas pobierania konfiguracji' }); + } + }); + + return router; + } + + createMessageRoutes() { + const router = express.Router(); + + // Pobierz wiadomość powitalną + router.get('/:guildId', this.authenticateToken, async (req, res) => { + try { + const { guildId } = req.params; + const message = await this.db.getWelcomeMessage(guildId); + + res.json(message || { + content: this.getDefaultWelcomeMessage(), + embed_data: null + }); + } catch (error) { + console.error('Błąd podczas pobierania wiadomości:', error); + res.status(500).json({ error: 'Błąd podczas pobierania wiadomości' }); + } + }); + + // Zapisz wiadomość powitalną + router.post('/:guildId', + this.authenticateToken, + [ + body('content').notEmpty().withMessage('Treść wiadomości nie może być pusta'), + body('content').isLength({ max: 2000 }).withMessage('Treść wiadomości nie może przekraczać 2000 znaków') + ], + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { guildId } = req.params; + const { content, embed_data } = req.body; + + // Sprawdź czy wiadomość już istnieje + const existingMessage = await this.db.getWelcomeMessage(guildId); + let message; + + if (existingMessage) { + // Zapisz rewizję przed aktualizacją + await this.db.saveMessageRevision( + existingMessage.id, + existingMessage.content, + existingMessage.embed_data, + req.user.userId + ); + + message = await this.db.updateWelcomeMessage(guildId, content, embed_data); + } else { + message = await this.db.saveWelcomeMessage(guildId, content, embed_data); + } + + res.json({ + message: 'Wiadomość została zapisana pomyślnie', + data: message + }); + } catch (error) { + console.error('Błąd podczas zapisywania wiadomości:', error); + res.status(500).json({ error: 'Błąd podczas zapisywania wiadomości' }); + } + } + ); + + // Historia wersji wiadomości + router.get('/:guildId/revisions', this.authenticateToken, async (req, res) => { + try { + const { guildId } = req.params; + const message = await this.db.getWelcomeMessage(guildId); + + if (!message) { + return res.json([]); + } + + const revisions = await this.db.getMessageRevisions(message.id); + res.json(revisions); + } catch (error) { + console.error('Błąd podczas pobierania historii:', error); + res.status(500).json({ error: 'Błąd podczas pobierania historii' }); + } + }); + + return router; + } + + // Middleware autoryzacji + authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Brak tokenu dostępu' }); + } + + jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ error: 'Nieprawidłowy token' }); + } + req.user = user; + next(); + }); + } + + // Error handler + errorHandler(error, req, res, next) { + console.error('Błąd serwera:', error); + + if (error.type === 'entity.parse.failed') { + return res.status(400).json({ error: 'Nieprawidłowy format JSON' }); + } + + res.status(500).json({ + error: 'Wewnętrzny błąd serwera', + ...(process.env.NODE_ENV === 'development' && { details: error.message }) + }); + } + + getDefaultWelcomeMessage() { + return `# 🎭 Witamy na naszym serwerze! + +Miło Cię tutaj widzieć! 👋 + +## 📋 Najważniejsze informacje: +• Przeczytaj regulamin serwera +• Przedstaw się w odpowiednim kanale +• Baw się dobrze i szanuj innych członków + +## 🎮 Funkcje serwera: +• Kanały tematyczne +• System ról +• Eventy i konkursy + +--- +*Ta wiadomość może być edytowana przez panel administracyjny*`; + } + + async start() { + const port = process.env.API_PORT || 3000; + + this.server = this.app.listen(port, () => { + console.log(`🌐 Panel web uruchomiony na porcie ${port}`); + console.log(`📱 URL panelu: http://localhost:${port}`); + }); + + return this.server; + } + + async stop() { + if (this.server) { + this.server.close(); + } + } +} + +module.exports = { WebPanel }; diff --git a/bot/database/migrate.js b/bot/database/migrate.js new file mode 100644 index 0000000..6ef90cc --- /dev/null +++ b/bot/database/migrate.js @@ -0,0 +1,24 @@ +const { DatabaseManager } = require('../backend/database/DatabaseManager'); +require('dotenv').config(); + +async function migrate() { + console.log('🚀 Rozpoczynam migrację bazy danych...'); + + const db = new DatabaseManager(); + + try { + await db.initialize(); + console.log('✅ Migracja zakończona pomyślnie'); + } catch (error) { + console.error('❌ Błąd podczas migracji:', error); + process.exit(1); + } finally { + await db.close(); + } +} + +if (require.main === module) { + migrate(); +} + +module.exports = { migrate }; diff --git a/bot/database/seed.js b/bot/database/seed.js new file mode 100644 index 0000000..c70ae7f --- /dev/null +++ b/bot/database/seed.js @@ -0,0 +1,54 @@ +const { DatabaseManager } = require('../backend/database/DatabaseManager'); +require('dotenv').config(); + +async function seed() { + console.log('🌱 Rozpoczynam seedowanie bazy danych...'); + + const db = new DatabaseManager(); + + try { + await db.initialize(); + + // Dodaj przykładowy serwer + await db.addGuild('123456789', 'Test Server'); + console.log('✅ Dodano przykładowy serwer'); + + // Dodaj przykładowy kanał powitalny + await db.setWelcomeChannel('123456789', '987654321', 'witamy'); + console.log('✅ Dodano przykładowy kanał powitalny'); + + // Dodaj przykładową wiadomość + const defaultMessage = `# 🎭 Witamy na naszym serwerze! + +Miło Cię tutaj widzieć! 👋 + +## 📋 Najważniejsze informacje: +• Przeczytaj regulamin serwera +• Przedstaw się w odpowiednim kanale +• Baw się dobrze i szanuj innych członków + +## 🎮 Funkcje serwera: +• Kanały tematyczne +• System ról +• Eventy i konkursy + +--- +*Ta wiadomość została wygenerowana przez Skrzynka Impostora Bot*`; + + await db.saveWelcomeMessage('123456789', defaultMessage); + console.log('✅ Dodano przykładową wiadomość powitalną'); + + console.log('✅ Seedowanie zakończone pomyślnie'); + } catch (error) { + console.error('❌ Błąd podczas seedowania:', error); + process.exit(1); + } finally { + await db.close(); + } +} + +if (require.main === module) { + seed(); +} + +module.exports = { seed }; diff --git a/bot/docker-compose.dev.yml b/bot/docker-compose.dev.yml new file mode 100644 index 0000000..0e77a07 --- /dev/null +++ b/bot/docker-compose.dev.yml @@ -0,0 +1,128 @@ +version: '3.8' + +services: + # PostgreSQL Database dla development + postgres-dev: + image: postgres:15-alpine + container_name: skrzynka-postgres-dev + restart: unless-stopped + environment: + POSTGRES_DB: skrzynka_impostora_dev + POSTGRES_USER: dev_user + POSTGRES_PASSWORD: dev_password + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_dev_data:/var/lib/postgresql/data + - ./database/init:/docker-entrypoint-initdb.d + ports: + - "5433:5432" # Inny port żeby nie kolidować z lokalnym PostgreSQL + networks: + - skrzynka-dev-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dev_user -d skrzynka_impostora_dev"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis dla development + redis-dev: + image: redis:7-alpine + container_name: skrzynka-redis-dev + restart: unless-stopped + ports: + - "6380:6379" # Inny port + volumes: + - redis_dev_data:/data + networks: + - skrzynka-dev-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + # Bot w trybie development z hot reload + bot-dev: + build: + context: . + dockerfile: Dockerfile.dev + container_name: skrzynka-bot-dev + restart: unless-stopped + environment: + # Database + DATABASE_URL: postgresql://dev_user:dev_password@postgres-dev:5432/skrzynka_impostora_dev + DB_HOST: postgres-dev + DB_PORT: 5432 + DB_NAME: skrzynka_impostora_dev + DB_USER: dev_user + DB_PASSWORD: dev_password + + # Discord Bot + DISCORD_TOKEN: ${DISCORD_TOKEN} + DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID} + DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET} + + # Web Panel + JWT_SECRET: dev_jwt_secret_key + SESSION_SECRET: dev_session_secret_key + WEB_PORT: 3001 + API_PORT: 3000 + + # OAuth2 + OAUTH2_REDIRECT_URI: http://localhost:3001/auth/discord/callback + + # Environment + NODE_ENV: development + LOG_LEVEL: debug + + # Redis + REDIS_URL: redis://redis-dev:6379 + ports: + - "3000:3000" + - "3001:3001" + - "9229:9229" # Node.js debugger port + depends_on: + postgres-dev: + condition: service_healthy + redis-dev: + condition: service_healthy + networks: + - skrzynka-dev-network + volumes: + - .:/app + - /app/node_modules + - /app/frontend/node_modules + - ./logs:/app/logs + command: npm run dev + + # pgAdmin dla zarządzania bazą danych + pgadmin: + image: dpage/pgadmin4:latest + container_name: skrzynka-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@skrzynka.local + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: 'False' + ports: + - "8080:80" + depends_on: + - postgres-dev + networks: + - skrzynka-dev-network + volumes: + - pgadmin_data:/var/lib/pgadmin + +# Volumes +volumes: + postgres_dev_data: + driver: local + redis_dev_data: + driver: local + pgadmin_data: + driver: local + +# Networks +networks: + skrzynka-dev-network: + driver: bridge diff --git a/bot/docker-compose.yml b/bot/docker-compose.yml new file mode 100644 index 0000000..bcccc5a --- /dev/null +++ b/bot/docker-compose.yml @@ -0,0 +1,131 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: skrzynka-postgres + restart: unless-stopped + environment: + POSTGRES_DB: skrzynka_impostora + POSTGRES_USER: skrzynka_user + POSTGRES_PASSWORD: skrzynka_password + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_data:/var/lib/postgresql/data + - ./database/init:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + networks: + - skrzynka-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U skrzynka_user -d skrzynka_impostora"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis for caching (optional) + redis: + image: redis:7-alpine + container_name: skrzynka-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - skrzynka-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + # Discord Bot Application + bot: + build: + context: . + dockerfile: Dockerfile + container_name: skrzynka-bot + restart: unless-stopped + environment: + # Database + DATABASE_URL: postgresql://skrzynka_user:skrzynka_password@postgres:5432/skrzynka_impostora + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: skrzynka_impostora + DB_USER: skrzynka_user + DB_PASSWORD: skrzynka_password + + # Discord Bot (należy ustawić w .env lub przez docker secrets) + DISCORD_TOKEN: ${DISCORD_TOKEN} + DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID} + DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET} + + # Web Panel + JWT_SECRET: ${JWT_SECRET:-super_secret_jwt_key_change_in_production} + SESSION_SECRET: ${SESSION_SECRET:-super_secret_session_key_change_in_production} + WEB_PORT: 3001 + API_PORT: 3000 + + # OAuth2 + OAUTH2_REDIRECT_URI: ${OAUTH2_REDIRECT_URI:-http://localhost:3001/auth/discord/callback} + + # Environment + NODE_ENV: ${NODE_ENV:-production} + LOG_LEVEL: ${LOG_LEVEL:-info} + + # Redis (optional) + REDIS_URL: redis://redis:6379 + ports: + - "3000:3000" + - "3001:3001" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - skrzynka-network + volumes: + - ./logs:/app/logs + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Nginx Reverse Proxy (production) + nginx: + image: nginx:alpine + container_name: skrzynka-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - bot + networks: + - skrzynka-network + profiles: + - production + +# Volumes +volumes: + postgres_data: + driver: local + redis_data: + driver: local + nginx_logs: + driver: local + +# Networks +networks: + skrzynka-network: + driver: bridge diff --git a/bot/frontend/package.json b/bot/frontend/package.json new file mode 100644 index 0000000..44ce8c0 --- /dev/null +++ b/bot/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "skrzynka-impostora-panel", + "version": "1.0.0", + "private": true, + "homepage": ".", + "dependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "react-scripts": "5.0.1", + "axios": "^1.3.4", + "react-markdown": "^8.0.5", + "react-syntax-highlighter": "^15.5.0", + "@monaco-editor/react": "^4.4.6", + "lucide-react": "^0.321.0", + "clsx": "^1.2.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "proxy": "http://localhost:3000" +} diff --git a/bot/frontend/public/index.html b/bot/frontend/public/index.html new file mode 100644 index 0000000..c0e1575 --- /dev/null +++ b/bot/frontend/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + Skrzynka Impostora - Panel Administracyjny + + + +
+ + diff --git a/bot/frontend/public/manifest.json b/bot/frontend/public/manifest.json new file mode 100644 index 0000000..f94f46d --- /dev/null +++ b/bot/frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Skrzynka Impostora", + "name": "Skrzynka Impostora Bot Panel", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#5865f2", + "background_color": "#ffffff" +} diff --git a/bot/frontend/src/App.css b/bot/frontend/src/App.css new file mode 100644 index 0000000..226acbd --- /dev/null +++ b/bot/frontend/src/App.css @@ -0,0 +1,708 @@ +/* Discord-inspired CSS for Skrzynka Impostora Bot Panel */ + +/* CSS Variables */ +:root { + --discord-blue: #5865f2; + --discord-green: #57f287; + --discord-red: #ed4245; + --discord-yellow: #fee75c; + --discord-purple: #eb459e; + + --discord-dark: #2c2f33; + --discord-darker: #23272a; + --discord-light: #36393f; + --discord-lighter: #40444b; + + --text-primary: #ffffff; + --text-secondary: #b9bbbe; + --text-muted: #72767d; + + --background-primary: #36393f; + --background-secondary: #2f3136; + --background-tertiary: #202225; + + --border-color: #202225; + --hover-color: rgba(79, 84, 92, 0.16); +} + +/* Base styles */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: var(--background-primary); + color: var(--text-primary); + line-height: 1.6; +} + +/* Layout */ +.flex { + display: flex; +} + +.min-h-screen { + min-height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 260px; + min-height: 100vh; + background-color: var(--background-secondary); + padding: 1.5rem; + display: flex; + flex-direction: column; + position: fixed; + left: 0; + top: 0; + z-index: 100; +} + +.sidebar h2 { + color: var(--text-primary); + font-size: 1.25rem; + margin: 0 0 0.5rem 0; +} + +.sidebar p { + color: var(--text-secondary); + font-size: 0.875rem; + margin: 0 0 2rem 0; +} + +/* Navigation */ +.nav-link { + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + margin-bottom: 0.25rem; + border-radius: 4px; + color: var(--text-secondary); + text-decoration: none; + transition: all 0.15s ease; +} + +.nav-link:hover { + background-color: var(--hover-color); + color: var(--text-primary); +} + +.nav-link.active { + background-color: var(--discord-blue); + color: white; +} + +/* Main content */ +.main-content { + flex: 1; + margin-left: 260px; + padding: 2rem; + background-color: var(--background-primary); + min-height: 100vh; +} + +/* Cards */ +.discord-card { + background-color: var(--background-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1rem; +} + +.guild-card { + background-color: var(--background-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + transition: all 0.15s ease; +} + +.guild-card:hover { + border-color: var(--discord-blue); + transform: translateY(-2px); +} + +/* Guild icon */ +.guild-icon { + width: 48px; + height: 48px; + border-radius: 50%; + background-color: var(--discord-blue); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + margin-right: 1rem; + font-size: 1rem; +} + +/* Buttons */ +.discord-button { + background-color: var(--discord-blue); + color: white; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.discord-button:hover { + background-color: #4752c4; + transform: translateY(-1px); +} + +.discord-button:disabled { + background-color: var(--text-muted); + cursor: not-allowed; + transform: none; +} + +.discord-button.secondary { + background-color: var(--background-lighter); + color: var(--text-primary); +} + +.discord-button.secondary:hover { + background-color: var(--hover-color); +} + +.discord-button.danger { + background-color: var(--discord-red); +} + +.discord-button.danger:hover { + background-color: #c73e41; +} + +.discord-button.success { + background-color: var(--discord-green); +} + +.discord-button.success:hover { + background-color: #4ac486; +} + +/* Forms */ +.discord-textarea { + width: 100%; + min-height: 200px; + background-color: var(--background-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.75rem; + color: var(--text-primary); + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.875rem; + line-height: 1.5; + resize: vertical; +} + +.discord-textarea:focus { + outline: none; + border-color: var(--discord-blue); +} + +.discord-textarea::placeholder { + color: var(--text-muted); +} + +/* Preview area */ +.preview-area { + background-color: var(--background-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 1rem; + overflow-y: auto; +} + +.preview-area h1 { + font-size: 1.5rem; + font-weight: bold; + margin: 0 0 1rem 0; + color: var(--text-primary); +} + +.preview-area h2 { + font-size: 1.25rem; + font-weight: bold; + margin: 0 0 0.75rem 0; + color: var(--text-primary); +} + +.preview-area h3 { + font-size: 1.125rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + color: var(--text-primary); +} + +.preview-area p { + margin: 0 0 1rem 0; + color: var(--text-secondary); +} + +.preview-area strong { + color: var(--text-primary); + font-weight: bold; +} + +.preview-area em { + font-style: italic; +} + +.preview-area code { + background-color: var(--background-secondary); + color: var(--discord-yellow); + padding: 0.125rem 0.25rem; + border-radius: 3px; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.8em; +} + +.preview-area pre { + background-color: var(--background-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.75rem; + overflow-x: auto; + margin: 1rem 0; +} + +.preview-area pre code { + background: none; + color: var(--discord-green); + padding: 0; +} + +/* Grid system */ +.grid { + display: grid; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +/* Spacing */ +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } +.mb-8 { margin-bottom: 2rem; } +.mt-4 { margin-top: 1rem; } +.mt-6 { margin-top: 1.5rem; } +.mt-8 { margin-top: 2rem; } +.mr-2 { margin-right: 0.5rem; } +.mr-3 { margin-right: 0.75rem; } +.mr-4 { margin-right: 1rem; } +.ml-4 { margin-left: 1rem; } + +/* Text utilities */ +.text-center { text-align: center; } +.text-sm { font-size: 0.875rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } +.text-2xl { font-size: 1.5rem; } +.text-3xl { font-size: 1.875rem; } + +.font-medium { font-weight: 500; } +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } + +.text-gray-900 { color: var(--text-primary); } +.text-gray-600 { color: var(--text-secondary); } +.text-gray-500 { color: var(--text-muted); } + +/* Status indicators */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + color: var(--text-secondary); +} + +.error { + background-color: rgba(237, 66, 69, 0.1); + border: 1px solid var(--discord-red); + border-radius: 4px; + padding: 1rem; + color: var(--discord-red); + margin-bottom: 1rem; +} + +.success { + background-color: rgba(87, 242, 135, 0.1); + border: 1px solid var(--discord-green); + border-radius: 4px; + padding: 1rem; + color: var(--discord-green); + margin-bottom: 1rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .sidebar { + width: 100%; + height: auto; + position: relative; + } + + .main-content { + margin-left: 0; + } + + .grid-cols-2, + .grid-cols-3, + .grid-cols-4 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } +} + +@media (max-width: 1024px) { + .grid-cols-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +/* Animations */ +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.discord-card, +.guild-card { + animation: fade-in 0.3s ease-out; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--background-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +.App { + min-height: 100vh; +} + +/* Discord-like styling */ +.discord-card { + background: #ffffff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + padding: 20px; + margin-bottom: 20px; +} + +.discord-button { + background: #5865f2; + color: white; + border: none; + border-radius: 4px; + padding: 10px 16px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.discord-button:hover { + background: #4752c4; +} + +.discord-button:disabled { + background: #a5a5a5; + cursor: not-allowed; +} + +.discord-button.secondary { + background: #4f545c; +} + +.discord-button.secondary:hover { + background: #5d6269; +} + +.discord-button.danger { + background: #ed4245; +} + +.discord-button.danger:hover { + background: #c03537; +} + +.discord-input { + background: #f2f3f5; + border: 1px solid #e3e5e8; + border-radius: 4px; + padding: 10px; + font-size: 16px; + width: 100%; + box-sizing: border-box; +} + +.discord-input:focus { + outline: none; + border-color: #5865f2; +} + +.discord-textarea { + background: #f2f3f5; + border: 1px solid #e3e5e8; + border-radius: 4px; + padding: 10px; + font-size: 14px; + width: 100%; + min-height: 200px; + resize: vertical; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + box-sizing: border-box; +} + +.discord-textarea:focus { + outline: none; + border-color: #5865f2; +} + +.sidebar { + width: 250px; + background: #2f3136; + color: white; + height: 100vh; + position: fixed; + left: 0; + top: 0; + padding: 20px; + box-sizing: border-box; +} + +.main-content { + margin-left: 250px; + padding: 20px; + min-height: 100vh; +} + +.nav-link { + color: #b9bbbe; + text-decoration: none; + padding: 8px 12px; + border-radius: 4px; + display: block; + margin-bottom: 4px; + transition: background-color 0.2s; +} + +.nav-link:hover { + background: #393c43; + color: white; +} + +.nav-link.active { + background: #5865f2; + color: white; +} + +.preview-area { + background: #36393f; + color: white; + padding: 20px; + border-radius: 8px; + min-height: 200px; + font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.preview-area h1, .preview-area h2, .preview-area h3 { + margin-top: 0; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + font-size: 18px; + color: #666; +} + +.error { + background: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 12px; + border-radius: 4px; + margin-bottom: 16px; +} + +.success { + background: #f0f9ff; + border: 1px solid #bae6fd; + color: #0369a1; + padding: 12px; + border-radius: 4px; + margin-bottom: 16px; +} + +.guild-card { + background: white; + border: 1px solid #e3e5e8; + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.guild-card:hover { + border-color: #5865f2; + box-shadow: 0 2px 8px rgba(88, 101, 242, 0.1); +} + +.guild-icon { + width: 48px; + height: 48px; + border-radius: 50%; + background: #5865f2; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 18px; + margin-right: 12px; +} + +.flex { + display: flex; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.text-sm { + font-size: 14px; +} + +.text-gray-500 { + color: #6b7280; +} + +.text-green-600 { + color: #059669; +} + +.text-red-600 { + color: #dc2626; +} + +.font-medium { + font-weight: 500; +} + +.font-bold { + font-weight: 700; +} + +.mb-2 { + margin-bottom: 8px; +} + +.mb-4 { + margin-bottom: 16px; +} + +.mt-4 { + margin-top: 16px; +} + +.editor-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + height: 600px; +} + +.editor-panel { + display: flex; + flex-direction: column; +} + +.editor-panel h3 { + margin-top: 0; + margin-bottom: 12px; + color: #4f545c; +} + +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + } + + .main-content { + margin-left: 0; + } + + .editor-container { + grid-template-columns: 1fr; + height: auto; + } +} diff --git a/bot/frontend/src/App.js b/bot/frontend/src/App.js new file mode 100644 index 0000000..8b4c356 --- /dev/null +++ b/bot/frontend/src/App.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider } from './hooks/useAuth'; +import Layout from './components/Layout'; +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; +import MessageEditor from './pages/MessageEditor'; +import ServerSelect from './pages/ServerSelect'; +import './App.css'; + +function App() { + return ( + + +
+ + } /> + }> + } /> + } /> + } /> + } /> + + +
+
+
+ ); +} + +export default App; diff --git a/bot/frontend/src/components/Layout.js b/bot/frontend/src/components/Layout.js new file mode 100644 index 0000000..1018181 --- /dev/null +++ b/bot/frontend/src/components/Layout.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; +import { LogOut, Home, Server, Settings } from 'lucide-react'; + +function Layout() { + const { user, logout, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + + React.useEffect(() => { + if (!isAuthenticated) { + navigate('/login'); + } + }, [isAuthenticated, navigate]); + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + if (!isAuthenticated) { + return null; + } + + return ( +
+ + +
+ +
+
+ ); +} + +export default Layout; diff --git a/bot/frontend/src/hooks/useAuth.js b/bot/frontend/src/hooks/useAuth.js new file mode 100644 index 0000000..acec8bd --- /dev/null +++ b/bot/frontend/src/hooks/useAuth.js @@ -0,0 +1,67 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { api } from '../services/api'; + +const AuthContext = createContext(); + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [token, setToken] = useState(localStorage.getItem('auth_token')); + + useEffect(() => { + if (token) { + api.setAuthToken(token); + verifyToken(); + } else { + setLoading(false); + } + }, [token]); + + const verifyToken = async () => { + try { + const response = await api.verifyToken(); + setUser(response.data.user); + } catch (error) { + console.error('Token verification failed:', error); + logout(); + } finally { + setLoading(false); + } + }; + + const login = (authToken) => { + setToken(authToken); + localStorage.setItem('auth_token', authToken); + api.setAuthToken(authToken); + }; + + const logout = () => { + setToken(null); + setUser(null); + localStorage.removeItem('auth_token'); + api.setAuthToken(null); + }; + + const value = { + user, + token, + loading, + login, + logout, + isAuthenticated: !!user + }; + + return ( + + {children} + + ); +} diff --git a/bot/frontend/src/index.css b/bot/frontend/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/bot/frontend/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/bot/frontend/src/index.js b/bot/frontend/src/index.js new file mode 100644 index 0000000..2cb1087 --- /dev/null +++ b/bot/frontend/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/bot/frontend/src/pages/Dashboard.js b/bot/frontend/src/pages/Dashboard.js new file mode 100644 index 0000000..e847c88 --- /dev/null +++ b/bot/frontend/src/pages/Dashboard.js @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../services/api'; +import { Server, MessageSquare, Users, Activity } from 'lucide-react'; + +function Dashboard() { + const [stats, setStats] = useState({ + totalServers: 0, + totalMessages: 0, + totalUsers: 0, + botStatus: 'online' + }); + const [recentActivity, setRecentActivity] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadDashboardData(); + }, []); + + const loadDashboardData = async () => { + try { + // Mock data - w prawdziwej aplikacji pobierz z API + setStats({ + totalServers: 5, + totalMessages: 23, + totalUsers: 1, + botStatus: 'online' + }); + + setRecentActivity([ + { + id: 1, + type: 'message_updated', + server: 'Test Server', + timestamp: new Date(), + description: 'Zaktualizowano wiadomość powitalną' + }, + { + id: 2, + type: 'channel_configured', + server: 'Test Server', + timestamp: new Date(Date.now() - 3600000), + description: 'Skonfigurowano kanał #witamy' + } + ]); + } catch (error) { + console.error('Error loading dashboard data:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Ładowanie dashboard...
; + } + + return ( +
+
+

Dashboard

+

+ Przegląd stanu bota i ostatnich aktywności +

+
+ + {/* Stats Cards */} +
+
+
+
+ +
+
+

Serwery

+

{stats.totalServers}

+
+
+
+ +
+
+
+ +
+
+

Wiadomości

+

{stats.totalMessages}

+
+
+
+ +
+
+
+ +
+
+

Użytkownicy

+

{stats.totalUsers}

+
+
+
+ +
+
+
+ +
+
+

Status Bota

+
+
+

+ {stats.botStatus === 'online' ? 'Online' : 'Offline'} +

+
+
+
+
+
+ +
+ {/* Quick Actions */} +
+

Szybkie akcje

+
+ +
+ +
+

Zarządzaj serwerami

+

Konfiguruj kanały i wiadomości

+
+
+ + +
+
+ +
+

Szablony wiadomości

+

Wkrótce dostępne

+
+
+
+ +
+
+ +
+

Statystyki

+

Wkrótce dostępne

+
+
+
+
+
+ + {/* Recent Activity */} +
+

Ostatnia aktywność

+
+ {recentActivity.length > 0 ? ( + recentActivity.map((activity) => ( +
+
+
+

+ {activity.description} +

+

+ {activity.server} • {activity.timestamp.toLocaleString('pl-PL')} +

+
+
+ )) + ) : ( +

+ Brak ostatnich aktywności +

+ )} +
+
+
+ + {/* Bot Info */} +
+

Informacje o bocie

+
+
+

Wersja

+

1.0.0

+
+
+

Czas działania

+

99.5%

+
+
+

Ostatnia aktualizacja

+

Dzisiaj

+
+
+
+
+ ); +} + +export default Dashboard; diff --git a/bot/frontend/src/pages/Login.js b/bot/frontend/src/pages/Login.js new file mode 100644 index 0000000..aef91d8 --- /dev/null +++ b/bot/frontend/src/pages/Login.js @@ -0,0 +1,82 @@ +import React, { useEffect } from 'react'; +import { useAuth } from '../hooks/useAuth'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +function Login() { + const { login, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + useEffect(() => { + // Sprawdź czy w URL jest token (po przekierowaniu z OAuth) + const token = searchParams.get('token'); + if (token) { + login(token); + navigate('/dashboard'); + return; + } + + // Jeśli już zalogowany, przekieruj na dashboard + if (isAuthenticated) { + navigate('/dashboard'); + } + }, [searchParams, login, navigate, isAuthenticated]); + + const handleDiscordLogin = () => { + const apiUrl = process.env.NODE_ENV === 'production' + ? '/api/auth/discord' + : 'http://localhost:3000/api/auth/discord'; + + window.location.href = apiUrl; + }; + + return ( +
+
+
+
🎭
+

+ Skrzynka Impostora Bot +

+

+ Panel zarządzania wiadomościami powaitalnymi +

+
+ +
+
+

+ Zaloguj się za pomocą Discord +

+

+ Aby zarządzać wiadomościami powaitalnymi, musisz się zalogować przez Discord. +

+ + +
+
+ +
+

+ Potrzebujesz uprawnień administratora na serwerze Discord, + aby zarządzać wiadomościami powaitalnymi. +

+
+
+
+ ); +} + +export default Login; diff --git a/bot/frontend/src/pages/MessageEditor.js b/bot/frontend/src/pages/MessageEditor.js new file mode 100644 index 0000000..1033175 --- /dev/null +++ b/bot/frontend/src/pages/MessageEditor.js @@ -0,0 +1,303 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { api } from '../services/api'; +import { Save, Eye, History, ArrowLeft, RefreshCw } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; + +function MessageEditor() { + const { guildId } = useParams(); + const navigate = useNavigate(); + + const [message, setMessage] = useState({ + content: '', + embed_data: null + }); + const [originalMessage, setOriginalMessage] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [previewMode, setPreviewMode] = useState('discord'); + const [isDirty, setIsDirty] = useState(false); + + useEffect(() => { + loadMessage(); + }, [guildId]); + + useEffect(() => { + if (originalMessage) { + setIsDirty(message.content !== originalMessage.content); + } + }, [message.content, originalMessage]); + + const loadMessage = async () => { + try { + const response = await api.getMessage(guildId); + const loadedMessage = response.data; + + setMessage(loadedMessage); + setOriginalMessage(loadedMessage); + } catch (error) { + console.error('Error loading message:', error); + setError('Nie udało się załadować wiadomości'); + } finally { + setLoading(false); + } + }; + + const saveMessage = async () => { + if (!message.content.trim()) { + setError('Treść wiadomości nie może być pusta'); + return; + } + + if (message.content.length > 2000) { + setError('Treść wiadomości nie może przekraczać 2000 znaków'); + return; + } + + setSaving(true); + setError(null); + + try { + await api.saveMessage(guildId, message.content, message.embed_data); + setSuccess('Wiadomość została zapisana pomyślnie'); + setOriginalMessage({ ...message }); + setIsDirty(false); + + // Ukryj komunikat po 3 sekundach + setTimeout(() => setSuccess(null), 3000); + } catch (error) { + console.error('Error saving message:', error); + setError('Nie udało się zapisać wiadomości'); + } finally { + setSaving(false); + } + }; + + const resetMessage = () => { + if (originalMessage) { + setMessage({ ...originalMessage }); + setIsDirty(false); + } + }; + + const renderDiscordPreview = (content) => { + // Podstawowa konwersja Discord Markdown do HTML + let html = content + // Headers + .replace(/^# (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^### (.*$)/gim, '

$1

') + // Bold + .replace(/\*\*(.*?)\*\*/gim, '$1') + // Italic + .replace(/\*(.*?)\*/gim, '$1') + // Underline + .replace(/__(.*?)__/gim, '$1') + // Strikethrough + .replace(/~~(.*?)~~/gim, '$1') + // Code blocks + .replace(/```([\s\S]*?)```/gim, '
$1
') + // Inline code + .replace(/`(.*?)`/gim, '$1') + // Lists + .replace(/^\• (.*$)/gim, '
  • • $1
  • ') + // Line breaks + .replace(/\n/gim, '
    '); + + return { __html: html }; + }; + + if (loading) { + return
    Ładowanie edytora wiadomości...
    ; + } + + return ( +
    +
    + + +
    +

    Edytor wiadomości

    +

    + Edytuj wiadomość powitalną dla serwera +

    +
    +
    + + {error && ( +
    + {error} +
    + )} + + {success && ( +
    + {success} +
    + )} + +
    + {/* Editor Panel */} +
    +
    +

    Edytor

    +
    + {message.content.length}/2000 + {isDirty && ( + + • Niezapisane zmiany + + )} +
    +
    + +