first commit
This commit is contained in:
89
bot/.dockerignore
Normal file
89
bot/.dockerignore
Normal file
@@ -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
|
||||||
37
bot/.env.example
Normal file
37
bot/.env.example
Normal file
@@ -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
|
||||||
85
bot/.github/copilot-instructions.md
vendored
Normal file
85
bot/.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
|
||||||
|
|
||||||
|
# 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
|
||||||
73
bot/.gitignore
vendored
Normal file
73
bot/.gitignore
vendored
Normal file
@@ -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
|
||||||
150
bot/.vscode/tasks.json
vendored
Normal file
150
bot/.vscode/tasks.json
vendored
Normal file
@@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
61
bot/Dockerfile
Normal file
61
bot/Dockerfile
Normal file
@@ -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"]
|
||||||
38
bot/Dockerfile.dev
Normal file
38
bot/Dockerfile.dev
Normal file
@@ -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"]
|
||||||
102
bot/Makefile
Normal file
102
bot/Makefile
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Makefile dla Skrzynka Impostora Bot
|
||||||
|
# Użycie: make <target>
|
||||||
|
|
||||||
|
.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
|
||||||
373
bot/README.md
Normal file
373
bot/README.md
Normal file
@@ -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 <repository-url>
|
||||||
|
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 <channel>` - 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
|
||||||
440
bot/backend/commands/index.js
Normal file
440
bot/backend/commands/index.js
Normal file
@@ -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 };
|
||||||
234
bot/backend/database/DatabaseManager.js
Normal file
234
bot/backend/database/DatabaseManager.js
Normal file
@@ -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 };
|
||||||
43
bot/backend/deploy-commands.js
Normal file
43
bot/backend/deploy-commands.js
Normal file
@@ -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 };
|
||||||
113
bot/backend/index.js
Normal file
113
bot/backend/index.js
Normal file
@@ -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 };
|
||||||
368
bot/backend/web/server.js
Normal file
368
bot/backend/web/server.js
Normal file
@@ -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 };
|
||||||
24
bot/database/migrate.js
Normal file
24
bot/database/migrate.js
Normal file
@@ -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 };
|
||||||
54
bot/database/seed.js
Normal file
54
bot/database/seed.js
Normal file
@@ -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 };
|
||||||
128
bot/docker-compose.dev.yml
Normal file
128
bot/docker-compose.dev.yml
Normal file
@@ -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
|
||||||
131
bot/docker-compose.yml
Normal file
131
bot/docker-compose.yml
Normal file
@@ -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
|
||||||
46
bot/frontend/package.json
Normal file
46
bot/frontend/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
17
bot/frontend/public/index.html
Normal file
17
bot/frontend/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#5865f2" />
|
||||||
|
<meta name="description" content="Panel zarządzania bota Discord - Skrzynka Impostora" />
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<title>Skrzynka Impostora - Panel Administracyjny</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>Musisz włączyć JavaScript aby korzystać z tej aplikacji.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
bot/frontend/public/manifest.json
Normal file
15
bot/frontend/public/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
708
bot/frontend/src/App.css
Normal file
708
bot/frontend/src/App.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
bot/frontend/src/App.js
Normal file
31
bot/frontend/src/App.js
Normal file
@@ -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 (
|
||||||
|
<AuthProvider>
|
||||||
|
<Router>
|
||||||
|
<div className="App">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/" element={<Layout />}>
|
||||||
|
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||||
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="servers" element={<ServerSelect />} />
|
||||||
|
<Route path="servers/:guildId/messages" element={<MessageEditor />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
70
bot/frontend/src/components/Layout.js
Normal file
70
bot/frontend/src/components/Layout.js
Normal file
@@ -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 (
|
||||||
|
<div className="flex">
|
||||||
|
<aside className="sidebar">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-2">🎭 Skrzynka Impostora</h2>
|
||||||
|
<p className="text-gray-300 text-sm">Panel administratora</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="mb-8">
|
||||||
|
<NavLink to="/dashboard" className="nav-link">
|
||||||
|
<Home size={16} className="inline mr-2" />
|
||||||
|
Dashboard
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/servers" className="nav-link">
|
||||||
|
<Server size={16} className="inline mr-2" />
|
||||||
|
Serwery
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<div className="border-t border-gray-600 pt-4">
|
||||||
|
<div className="text-sm text-gray-300 mb-2">
|
||||||
|
Zalogowany jako:
|
||||||
|
</div>
|
||||||
|
<div className="text-white font-medium mb-4">
|
||||||
|
{user?.username}#{user?.discriminator}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="discord-button secondary w-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<LogOut size={16} className="mr-2" />
|
||||||
|
Wyloguj
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="main-content">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
67
bot/frontend/src/hooks/useAuth.js
Normal file
67
bot/frontend/src/hooks/useAuth.js
Normal file
@@ -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 (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
bot/frontend/src/index.css
Normal file
13
bot/frontend/src/index.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
11
bot/frontend/src/index.js
Normal file
11
bot/frontend/src/index.js
Normal file
@@ -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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
213
bot/frontend/src/pages/Dashboard.js
Normal file
213
bot/frontend/src/pages/Dashboard.js
Normal file
@@ -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 <div className="loading">Ładowanie dashboard...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Dashboard</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Przegląd stanu bota i ostatnich aktywności
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-blue-100 rounded-lg mr-4">
|
||||||
|
<Server className="text-blue-600" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Serwery</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalServers}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-green-100 rounded-lg mr-4">
|
||||||
|
<MessageSquare className="text-green-600" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Wiadomości</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalMessages}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-purple-100 rounded-lg mr-4">
|
||||||
|
<Users className="text-purple-600" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Użytkownicy</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalUsers}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-yellow-100 rounded-lg mr-4">
|
||||||
|
<Activity className="text-yellow-600" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Status Bota</p>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${
|
||||||
|
stats.botStatus === 'online' ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}></div>
|
||||||
|
<p className="text-lg font-semibold text-gray-900 capitalize">
|
||||||
|
{stats.botStatus === 'online' ? 'Online' : 'Offline'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="discord-card">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Szybkie akcje</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Link
|
||||||
|
to="/servers"
|
||||||
|
className="block p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Server className="text-blue-600 mr-3" size={20} />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">Zarządzaj serwerami</p>
|
||||||
|
<p className="text-sm text-gray-600">Konfiguruj kanały i wiadomości</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="block p-3 border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MessageSquare className="text-gray-400 mr-3" size={20} />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-500">Szablony wiadomości</p>
|
||||||
|
<p className="text-sm text-gray-400">Wkrótce dostępne</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="block p-3 border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Activity className="text-gray-400 mr-3" size={20} />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-500">Statystyki</p>
|
||||||
|
<p className="text-sm text-gray-400">Wkrótce dostępne</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<div className="discord-card">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ostatnia aktywność</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{recentActivity.length > 0 ? (
|
||||||
|
recentActivity.map((activity) => (
|
||||||
|
<div key={activity.id} className="flex items-start p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{activity.description}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
{activity.server} • {activity.timestamp.toLocaleString('pl-PL')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-center py-4">
|
||||||
|
Brak ostatnich aktywności
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bot Info */}
|
||||||
|
<div className="discord-card mt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Informacje o bocie</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Wersja</p>
|
||||||
|
<p className="font-medium">1.0.0</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Czas działania</p>
|
||||||
|
<p className="font-medium">99.5%</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Ostatnia aktualizacja</p>
|
||||||
|
<p className="font-medium">Dzisiaj</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
82
bot/frontend/src/pages/Login.js
Normal file
82
bot/frontend/src/pages/Login.js
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="max-w-md w-full space-y-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-6xl mb-4">🎭</div>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900">
|
||||||
|
Skrzynka Impostora Bot
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-gray-600">
|
||||||
|
Panel zarządzania wiadomościami powaitalnymi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||||
|
Zaloguj się za pomocą Discord
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-6">
|
||||||
|
Aby zarządzać wiadomościami powaitalnymi, musisz się zalogować przez Discord.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleDiscordLogin}
|
||||||
|
className="discord-button w-full flex items-center justify-center text-lg py-3"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 mr-3"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.010c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
||||||
|
</svg>
|
||||||
|
Zaloguj przez Discord
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center text-sm text-gray-500">
|
||||||
|
<p>
|
||||||
|
Potrzebujesz uprawnień administratora na serwerze Discord,
|
||||||
|
aby zarządzać wiadomościami powaitalnymi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
303
bot/frontend/src/pages/MessageEditor.js
Normal file
303
bot/frontend/src/pages/MessageEditor.js
Normal file
@@ -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, '<h1 class="text-xl font-bold mb-2">$1</h1>')
|
||||||
|
.replace(/^## (.*$)/gim, '<h2 class="text-lg font-bold mb-2">$1</h2>')
|
||||||
|
.replace(/^### (.*$)/gim, '<h3 class="text-base font-bold mb-1">$1</h3>')
|
||||||
|
// Bold
|
||||||
|
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
|
||||||
|
// Italic
|
||||||
|
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
|
||||||
|
// Underline
|
||||||
|
.replace(/__(.*?)__/gim, '<u>$1</u>')
|
||||||
|
// Strikethrough
|
||||||
|
.replace(/~~(.*?)~~/gim, '<del>$1</del>')
|
||||||
|
// Code blocks
|
||||||
|
.replace(/```([\s\S]*?)```/gim, '<pre class="bg-gray-800 text-green-400 p-2 rounded text-sm my-2"><code>$1</code></pre>')
|
||||||
|
// Inline code
|
||||||
|
.replace(/`(.*?)`/gim, '<code class="bg-gray-700 text-gray-200 px-1 rounded text-sm">$1</code>')
|
||||||
|
// Lists
|
||||||
|
.replace(/^\• (.*$)/gim, '<li class="ml-4">• $1</li>')
|
||||||
|
// Line breaks
|
||||||
|
.replace(/\n/gim, '<br>');
|
||||||
|
|
||||||
|
return { __html: html };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="loading">Ładowanie edytora wiadomości...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/servers')}
|
||||||
|
className="discord-button secondary mr-4 flex items-center"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} className="mr-2" />
|
||||||
|
Powrót do serwerów
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Edytor wiadomości</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Edytuj wiadomość powitalną dla serwera
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="success">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Editor Panel */}
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Edytor</h3>
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
||||||
|
<span>{message.content.length}/2000</span>
|
||||||
|
{isDirty && (
|
||||||
|
<span className="text-orange-600 font-medium">
|
||||||
|
• Niezapisane zmiany
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={message.content}
|
||||||
|
onChange={(e) => setMessage({ ...message, content: e.target.value })}
|
||||||
|
className="discord-textarea"
|
||||||
|
placeholder="Wprowadź treść wiadomości powitalnej...
|
||||||
|
|
||||||
|
Możesz używać Discord Markdown:
|
||||||
|
# Nagłówek 1
|
||||||
|
## Nagłówek 2
|
||||||
|
**Pogrubienie**
|
||||||
|
*Kursywa*
|
||||||
|
__Podkreślenie__
|
||||||
|
~~Przekreślenie~~
|
||||||
|
`kod`
|
||||||
|
```
|
||||||
|
blok kodu
|
||||||
|
```
|
||||||
|
• Lista
|
||||||
|
• Punktowana"
|
||||||
|
style={{ height: '400px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={resetMessage}
|
||||||
|
disabled={!isDirty}
|
||||||
|
className="discord-button secondary flex items-center"
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} className="mr-2" />
|
||||||
|
Resetuj
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={saveMessage}
|
||||||
|
disabled={saving || !message.content.trim() || !isDirty}
|
||||||
|
className="discord-button flex items-center"
|
||||||
|
>
|
||||||
|
<Save size={16} className="mr-2" />
|
||||||
|
{saving ? 'Zapisywanie...' : 'Zapisz'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview Panel */}
|
||||||
|
<div className="discord-card">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Podgląd</h3>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPreviewMode('discord')}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
previewMode === 'discord'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPreviewMode('markdown')}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
previewMode === 'markdown'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Markdown
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="preview-area" style={{ height: '400px', overflowY: 'auto' }}>
|
||||||
|
{message.content ? (
|
||||||
|
previewMode === 'discord' ? (
|
||||||
|
<div dangerouslySetInnerHTML={renderDiscordPreview(message.content)} />
|
||||||
|
) : (
|
||||||
|
<ReactMarkdown>{message.content}</ReactMarkdown>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 italic">
|
||||||
|
Wprowadź treść wiadomości, aby zobaczyć podgląd
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tips */}
|
||||||
|
<div className="discord-card mt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
💡 Wskazówki dotyczące formatowania
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Podstawowe formatowanie:</h4>
|
||||||
|
<ul className="space-y-1 text-gray-600">
|
||||||
|
<li><code>**tekst**</code> → <strong>pogrubienie</strong></li>
|
||||||
|
<li><code>*tekst*</code> → <em>kursywa</em></li>
|
||||||
|
<li><code>__tekst__</code> → <u>podkreślenie</u></li>
|
||||||
|
<li><code>~~tekst~~</code> → <del>przekreślenie</del></li>
|
||||||
|
<li><code>`kod`</code> → kod wewnątrz tekstu</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Zaawansowane:</h4>
|
||||||
|
<ul className="space-y-1 text-gray-600">
|
||||||
|
<li><code># Nagłówek</code> → duży nagłówek</li>
|
||||||
|
<li><code>## Nagłówek</code> → średni nagłówek</li>
|
||||||
|
<li><code>### Nagłówek</code> → mały nagłówek</li>
|
||||||
|
<li><code>• Element</code> → lista punktowana</li>
|
||||||
|
<li><code>```kod```</code> → blok kodu</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Character limit warning */}
|
||||||
|
{message.content.length > 1800 && (
|
||||||
|
<div className="discord-card mt-4 border-l-4 border-orange-400 bg-orange-50">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-orange-600 mr-3">⚠️</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-orange-800">
|
||||||
|
Uwaga: Zbliżasz się do limitu znaków
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-orange-700">
|
||||||
|
Discord ogranicza wiadomości do 2000 znaków.
|
||||||
|
Pozostało: {2000 - message.content.length} znaków
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageEditor;
|
||||||
221
bot/frontend/src/pages/ServerSelect.js
Normal file
221
bot/frontend/src/pages/ServerSelect.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
import { Server, MessageSquare, Settings, CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
function ServerSelect() {
|
||||||
|
const [guilds, setGuilds] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadGuilds();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadGuilds = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.getGuilds();
|
||||||
|
setGuilds(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading guilds:', error);
|
||||||
|
setError('Nie udało się załadować listy serwerów');
|
||||||
|
|
||||||
|
// Mock data dla rozwoju
|
||||||
|
setGuilds([
|
||||||
|
{
|
||||||
|
id: '123456789',
|
||||||
|
name: 'Test Server',
|
||||||
|
icon: null,
|
||||||
|
hasBot: true,
|
||||||
|
userPermissions: ['MANAGE_CHANNELS'],
|
||||||
|
memberCount: 150,
|
||||||
|
welcomeChannelConfigured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '987654321',
|
||||||
|
name: 'Drugi Serwer',
|
||||||
|
icon: null,
|
||||||
|
hasBot: false,
|
||||||
|
userPermissions: ['MANAGE_CHANNELS'],
|
||||||
|
memberCount: 50,
|
||||||
|
welcomeChannelConfigured: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '555666777',
|
||||||
|
name: 'Gaming Community',
|
||||||
|
icon: null,
|
||||||
|
hasBot: true,
|
||||||
|
userPermissions: ['ADMINISTRATOR'],
|
||||||
|
memberCount: 300,
|
||||||
|
welcomeChannelConfigured: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerIcon = (guild) => {
|
||||||
|
if (guild.icon) {
|
||||||
|
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerInitials = (name) => {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map(word => word[0])
|
||||||
|
.join('')
|
||||||
|
.substring(0, 2)
|
||||||
|
.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="loading">Ładowanie serwerów...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Twoje serwery</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Wybierz serwer, aby zarządzać wiadomościami powaitalnymi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error mb-6">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{guilds.length === 0 ? (
|
||||||
|
<div className="discord-card text-center">
|
||||||
|
<Server className="mx-auto text-gray-400 mb-4" size={48} />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Brak dostępnych serwerów
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Nie znaleziono serwerów z odpowiednimi uprawnieniami.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Upewnij się, że masz uprawnienia administracyjne na serwerze i że bot jest zaproszony.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{guilds.map((guild) => (
|
||||||
|
<div key={guild.id} className="guild-card">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="guild-icon">
|
||||||
|
{getServerIcon(guild) ? (
|
||||||
|
<img
|
||||||
|
src={getServerIcon(guild)}
|
||||||
|
alt={guild.name}
|
||||||
|
className="w-full h-full rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getServerInitials(guild.name)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900">{guild.name}</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{guild.memberCount || 0} członków
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end space-y-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{guild.hasBot ? (
|
||||||
|
<CheckCircle className="text-green-600" size={16} />
|
||||||
|
) : (
|
||||||
|
<XCircle className="text-red-600" size={16} />
|
||||||
|
)}
|
||||||
|
<span className={`text-xs ml-1 ${
|
||||||
|
guild.hasBot ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}>
|
||||||
|
{guild.hasBot ? 'Bot aktywny' : 'Bot nieaktywny'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{guild.welcomeChannelConfigured && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MessageSquare className="text-blue-600" size={16} />
|
||||||
|
<span className="text-xs text-blue-600 ml-1">
|
||||||
|
Kanał skonfigurowany
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{guild.hasBot ? (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={`/servers/${guild.id}/messages`}
|
||||||
|
className="discord-button w-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<MessageSquare size={16} className="mr-2" />
|
||||||
|
Zarządzaj wiadomościami
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
className="discord-button secondary flex-1 flex items-center justify-center text-sm"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Settings size={14} className="mr-1" />
|
||||||
|
Ustawienia
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
Bot nie jest zaproszony na ten serwer
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="discord-button w-full"
|
||||||
|
onClick={() => {
|
||||||
|
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.REACT_APP_DISCORD_CLIENT_ID}&permissions=2147484672&scope=bot%20applications.commands&guild_id=${guild.id}`;
|
||||||
|
window.open(inviteUrl, '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Zaproś bota
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Uprawnienia: {guild.userPermissions?.includes('ADMINISTRATOR')
|
||||||
|
? 'Administrator'
|
||||||
|
: guild.userPermissions?.join(', ') || 'Brak'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="discord-card mt-8">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Nie widzisz swojego serwera?
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 text-sm text-gray-600">
|
||||||
|
<p>• Upewnij się, że masz uprawnienia administracyjne na serwerze</p>
|
||||||
|
<p>• Sprawdź czy bot jest zaproszony na serwer z odpowiednimi uprawnieniami</p>
|
||||||
|
<p>• Wyloguj się i zaloguj ponownie, aby odświeżyć listę serwerów</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerSelect;
|
||||||
95
bot/frontend/src/services/api.js
Normal file
95
bot/frontend/src/services/api.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
constructor() {
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: process.env.NODE_ENV === 'production'
|
||||||
|
? '/api'
|
||||||
|
: 'http://localhost:3000/api',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInterceptors() {
|
||||||
|
// Request interceptor
|
||||||
|
this.client.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Token expired or invalid
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthToken(token) {
|
||||||
|
if (token) {
|
||||||
|
this.client.defaults.headers.Authorization = `Bearer ${token}`;
|
||||||
|
} else {
|
||||||
|
delete this.client.defaults.headers.Authorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth endpoints
|
||||||
|
async verifyToken() {
|
||||||
|
return this.client.get('/auth/verify');
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
return this.client.post('/auth/logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guild endpoints
|
||||||
|
async getGuilds() {
|
||||||
|
return this.client.get('/guilds');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGuildConfig(guildId) {
|
||||||
|
return this.client.get(`/guilds/${guildId}/config`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message endpoints
|
||||||
|
async getMessage(guildId) {
|
||||||
|
return this.client.get(`/messages/${guildId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveMessage(guildId, content, embedData = null) {
|
||||||
|
return this.client.post(`/messages/${guildId}`, {
|
||||||
|
content,
|
||||||
|
embed_data: embedData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessageRevisions(guildId) {
|
||||||
|
return this.client.get(`/messages/${guildId}/revisions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
async healthCheck() {
|
||||||
|
return this.client.get('/health');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = new ApiService();
|
||||||
124
bot/nginx/conf.d/default.conf
Normal file
124
bot/nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
upstream backend {
|
||||||
|
server bot:3000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect all HTTP traffic to HTTPS
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main server block
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name your-domain.com www.your-domain.com;
|
||||||
|
|
||||||
|
# SSL configuration (uncomment and configure for production)
|
||||||
|
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
# ssl_session_timeout 1d;
|
||||||
|
# ssl_session_cache shared:SSL:50m;
|
||||||
|
# ssl_session_tickets off;
|
||||||
|
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
# ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
|
||||||
|
# Increase max body size for uploads
|
||||||
|
client_max_body_size 10M;
|
||||||
|
|
||||||
|
# API endpoints with rate limiting
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=api burst=20 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# OAuth callback with stricter rate limiting
|
||||||
|
location /api/auth/ {
|
||||||
|
limit_req zone=login burst=5 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files (React app)
|
||||||
|
location / {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
proxy_pass http://backend/api/health;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Development server block (uncomment for development with SSL)
|
||||||
|
# server {
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# server_name localhost;
|
||||||
|
#
|
||||||
|
# # Self-signed certificate for development
|
||||||
|
# ssl_certificate /etc/nginx/ssl/localhost.crt;
|
||||||
|
# ssl_certificate_key /etc/nginx/ssl/localhost.key;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://backend;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection 'upgrade';
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# proxy_cache_bypass $http_upgrade;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
58
bot/nginx/nginx.conf
Normal file
58
bot/nginx/nginx.conf
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log notice;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
use epoll;
|
||||||
|
multi_accept on;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Logging format
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
# Performance optimizations
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 10240;
|
||||||
|
gzip_proxied expired no-cache no-store private must-revalidate auth;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/x-javascript
|
||||||
|
application/xml+rss
|
||||||
|
application/javascript
|
||||||
|
application/json;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||||
|
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||||
|
|
||||||
|
# Include additional configuration files
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
54
bot/package.json
Normal file
54
bot/package.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "skrzynka-impostora-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Discord bot with web panel for welcome messages management",
|
||||||
|
"main": "backend/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node backend/index.js",
|
||||||
|
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
||||||
|
"dev:backend": "nodemon backend/index.js",
|
||||||
|
"dev:frontend": "cd frontend && npm start",
|
||||||
|
"build": "cd frontend && npm run build",
|
||||||
|
"deploy": "node backend/deploy-commands.js",
|
||||||
|
"db:migrate": "node database/migrate.js",
|
||||||
|
"db:seed": "node database/seed.js",
|
||||||
|
"docker:dev": "docker compose -f docker-compose.dev.yml up --build",
|
||||||
|
"docker:dev:down": "docker compose -f docker-compose.dev.yml down",
|
||||||
|
"docker:prod": "docker compose up --build -d",
|
||||||
|
"docker:prod:down": "docker compose down",
|
||||||
|
"docker:build": "docker build -t skrzynka-impostora-bot:latest .",
|
||||||
|
"docker:logs": "docker compose logs -f",
|
||||||
|
"docker:logs:dev": "docker compose -f docker-compose.dev.yml logs -f"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"discord",
|
||||||
|
"bot",
|
||||||
|
"welcome",
|
||||||
|
"panel",
|
||||||
|
"management"
|
||||||
|
],
|
||||||
|
"author": "Skrzynka Impostora Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"discord.js": "^14.14.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"express-validator": "^7.0.1",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"compression": "^1.7.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"@types/node": "^20.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
bot/scripts/dev-start.bat
Normal file
43
bot/scripts/dev-start.bat
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
@echo off
|
||||||
|
REM Skript do uruchamiania środowiska development na Windows
|
||||||
|
|
||||||
|
echo 🚀 Uruchamianie środowiska development...
|
||||||
|
|
||||||
|
REM Sprawdź czy Docker jest uruchomiony
|
||||||
|
docker info >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Docker nie jest uruchomiony. Uruchom Docker Desktop i spróbuj ponownie.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Sprawdź czy plik .env istnieje
|
||||||
|
if not exist .env (
|
||||||
|
echo ⚠️ Plik .env nie istnieje. Kopiuję z .env.example...
|
||||||
|
copy .env.example .env
|
||||||
|
echo 📝 Plik .env został utworzony. Uzupełnij wymagane zmienne przed kontynuowaniem.
|
||||||
|
echo Szczególnie ważne: DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET
|
||||||
|
echo.
|
||||||
|
set /p continue="Czy chcesz kontynuować? (y/N): "
|
||||||
|
if /i not "%continue%"=="y" exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Uruchom docker-compose dla development
|
||||||
|
echo 🐳 Uruchamianie kontenerów...
|
||||||
|
docker compose -f docker-compose.dev.yml up --build -d
|
||||||
|
|
||||||
|
echo ✅ Środowisko development zostało uruchomione!
|
||||||
|
echo.
|
||||||
|
echo 📋 Dostępne usługi:
|
||||||
|
echo 🤖 Bot + Panel Web: http://localhost:3000
|
||||||
|
echo 🗄️ PostgreSQL: localhost:5433
|
||||||
|
echo 📊 pgAdmin: http://localhost:8080 (admin@skrzynka.local / admin)
|
||||||
|
echo 🔄 Redis: localhost:6380
|
||||||
|
echo.
|
||||||
|
echo 📝 Aby zobaczyć logi:
|
||||||
|
echo docker compose -f docker-compose.dev.yml logs -f
|
||||||
|
echo.
|
||||||
|
echo 🛑 Aby zatrzymać:
|
||||||
|
echo docker compose -f docker-compose.dev.yml down
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
42
bot/scripts/dev-start.sh
Normal file
42
bot/scripts/dev-start.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Skript do uruchamiania środowiska development
|
||||||
|
echo "🚀 Uruchamianie środowiska development..."
|
||||||
|
|
||||||
|
# Sprawdź czy Docker jest uruchomiony
|
||||||
|
if ! docker info > /dev/null 2>&1; then
|
||||||
|
echo "❌ Docker nie jest uruchomiony. Uruchom Docker Desktop i spróbuj ponownie."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sprawdź czy plik .env istnieje
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "⚠️ Plik .env nie istnieje. Kopiuję z .env.example..."
|
||||||
|
cp .env.example .env
|
||||||
|
echo "📝 Plik .env został utworzony. Uzupełnij wymagane zmienne przed kontynuowaniem."
|
||||||
|
echo " Szczególnie ważne: DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET"
|
||||||
|
echo ""
|
||||||
|
read -p "Czy chcesz kontynuować? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Uruchom docker-compose dla development
|
||||||
|
echo "🐳 Uruchamianie kontenerów..."
|
||||||
|
docker compose -f docker-compose.dev.yml up --build -d
|
||||||
|
|
||||||
|
echo "✅ Środowisko development zostało uruchomione!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Dostępne usługi:"
|
||||||
|
echo " 🤖 Bot + Panel Web: http://localhost:3000"
|
||||||
|
echo " 🗄️ PostgreSQL: localhost:5433"
|
||||||
|
echo " 📊 pgAdmin: http://localhost:8080 (admin@skrzynka.local / admin)"
|
||||||
|
echo " 🔄 Redis: localhost:6380"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Aby zobaczyć logi:"
|
||||||
|
echo " docker compose -f docker-compose.dev.yml logs -f"
|
||||||
|
echo ""
|
||||||
|
echo "🛑 Aby zatrzymać:"
|
||||||
|
echo " docker compose -f docker-compose.dev.yml down"
|
||||||
42
bot/scripts/prod-deploy.sh
Normal file
42
bot/scripts/prod-deploy.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Skript do deployowania na produkcję
|
||||||
|
echo "🚀 Przygotowywanie do deployowania na produkcję..."
|
||||||
|
|
||||||
|
# Sprawdź czy Docker jest uruchomiony
|
||||||
|
if ! docker info > /dev/null 2>&1; then
|
||||||
|
echo "❌ Docker nie jest uruchomiony."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sprawdź czy wszystkie wymagane zmienne są ustawione
|
||||||
|
if [ -z "$DISCORD_TOKEN" ] || [ -z "$DISCORD_CLIENT_ID" ] || [ -z "$DISCORD_CLIENT_SECRET" ]; then
|
||||||
|
echo "❌ Nie wszystkie wymagane zmienne środowiskowe są ustawione."
|
||||||
|
echo " Wymagane: DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build obrazu
|
||||||
|
echo "🏗️ Budowanie obrazu..."
|
||||||
|
docker build -t skrzynka-impostora-bot:latest .
|
||||||
|
|
||||||
|
# Deploy komend Discord
|
||||||
|
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
|
||||||
|
|
||||||
|
# Uruchom środowisko produkcyjne
|
||||||
|
echo "🚀 Uruchamianie środowiska produkcyjnego..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "✅ Aplikacja została wdrożona!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Dostępne usługi:"
|
||||||
|
echo " 🌐 Aplikacja: http://localhost (przez Nginx)"
|
||||||
|
echo " 🗄️ PostgreSQL: localhost:5432"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Aby zobaczyć logi:"
|
||||||
|
echo " docker compose logs -f"
|
||||||
62
bot/shared/config.js
Normal file
62
bot/shared/config.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Database configuration
|
||||||
|
database: {
|
||||||
|
development: {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: process.env.DB_PORT || 5432,
|
||||||
|
database: process.env.DB_NAME || 'skrzynka_impostora',
|
||||||
|
username: process.env.DB_USER || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: console.log
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
use_env_variable: 'DATABASE_URL',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: false,
|
||||||
|
dialectOptions: {
|
||||||
|
ssl: {
|
||||||
|
require: true,
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bot configuration
|
||||||
|
bot: {
|
||||||
|
token: process.env.DISCORD_TOKEN,
|
||||||
|
clientId: process.env.DISCORD_CLIENT_ID,
|
||||||
|
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||||
|
permissions: {
|
||||||
|
required: [
|
||||||
|
'SEND_MESSAGES',
|
||||||
|
'MANAGE_MESSAGES',
|
||||||
|
'EMBED_LINKS',
|
||||||
|
'READ_MESSAGE_HISTORY',
|
||||||
|
'USE_SLASH_COMMANDS'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Web panel configuration
|
||||||
|
web: {
|
||||||
|
port: process.env.API_PORT || 3000,
|
||||||
|
frontendPort: process.env.WEB_PORT || 3001,
|
||||||
|
jwtSecret: process.env.JWT_SECRET,
|
||||||
|
sessionSecret: process.env.SESSION_SECRET,
|
||||||
|
oauth: {
|
||||||
|
redirectUri: process.env.OAUTH2_REDIRECT_URI
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Application settings
|
||||||
|
app: {
|
||||||
|
name: 'Skrzynka Impostora Bot',
|
||||||
|
version: '1.0.0',
|
||||||
|
environment: process.env.NODE_ENV || 'development',
|
||||||
|
logLevel: process.env.LOG_LEVEL || 'info'
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user