wywalenie tej wersji bota

This commit is contained in:
2025-07-21 09:26:45 +02:00
parent 6f5de9a799
commit 5ff1cd9c02
41 changed files with 9 additions and 4884 deletions

0
README.md Normal file
View File

View File

@@ -1,89 +0,0 @@
# 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

View File

@@ -1,37 +0,0 @@
# 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

View File

@@ -1,85 +0,0 @@
<!-- 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
View File

@@ -1,73 +0,0 @@
# 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
View File

@@ -1,150 +0,0 @@
{
"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}"
}
}
]
}

View File

@@ -1,61 +0,0 @@
# 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"]

View File

@@ -1,38 +0,0 @@
# 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"]

View File

@@ -1,102 +0,0 @@
# 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

View File

@@ -1,373 +0,0 @@
# 🎭 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

View File

@@ -1,440 +0,0 @@
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 };

View File

@@ -1,234 +0,0 @@
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 };

View File

@@ -1,43 +0,0 @@
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 };

View File

@@ -1,113 +0,0 @@
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 };

View File

@@ -1,372 +0,0 @@
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', 'http://localhost:3000'],
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 tylko w production
if (process.env.NODE_ENV === 'production') {
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 - tylko w production
if (process.env.NODE_ENV === 'production') {
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 };

View File

@@ -1,24 +0,0 @@
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 };

View File

@@ -1,54 +0,0 @@
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 };

View File

@@ -1,126 +0,0 @@
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

View File

@@ -1,131 +0,0 @@
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

View File

@@ -1,46 +0,0 @@
{
"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"
}

View File

@@ -1,17 +0,0 @@
<!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>

View File

@@ -1,15 +0,0 @@
{
"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"
}

View File

@@ -1,708 +0,0 @@
/* 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;
}
}

View File

@@ -1,31 +0,0 @@
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;

View File

@@ -1,70 +0,0 @@
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;

View File

@@ -1,67 +0,0 @@
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>
);
}

View File

@@ -1,13 +0,0 @@
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;
}

View File

@@ -1,11 +0,0 @@
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>
);

View File

@@ -1,213 +0,0 @@
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;

View File

@@ -1,82 +0,0 @@
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;

View File

@@ -1,303 +0,0 @@
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;

View File

@@ -1,221 +0,0 @@
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;

View File

@@ -1,95 +0,0 @@
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();

View File

@@ -1,124 +0,0 @@
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;
# }
# }

View File

@@ -1,58 +0,0 @@
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;
}

View File

@@ -1,54 +0,0 @@
{
"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"
}
}

View File

@@ -1,43 +0,0 @@
@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

View File

@@ -1,43 +0,0 @@
#!/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 " 🔧 API Backend: http://localhost:3000"
echo " 🎨 React Frontend: http://localhost:3001"
echo " 🗄️ PostgreSQL: localhost:5433"
echo " 📊 pgAdmin: http://localhost:8080 (boratsc@gmail.com / 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"

View File

@@ -1,42 +0,0 @@
#!/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"

View File

@@ -1,62 +0,0 @@
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'
}
};

View File

@@ -13,11 +13,7 @@ Celem projektu jest stworzenie kompleksowego bota Discordowego „Skrzynka Impos
1. Integracja bota z Discord API.
2. Wysyłanie domyślnej wiadomości powitalnej na wskazany kanał #witamy.
3. Panel web do edycji treści i konfiguracji kanału powitalnego.
- **Funkcjonalności docelowe:**
- Obsługa wielu wariantów wiadomości (częściowo dzielona, carouseli, linki, emoji).
- Harmonogramy wysyłki (poranne, wieczorne przypomnienia, rotacje sezonowe).
- Wielojęzyczność.
4. Obsługa wielu wariantów wiadomości (częściowo dzielona, carouseli, linki, emoji).
## 4. Wymagania funkcjonalne
1. **Konfiguracja kanału powitalnego**
@@ -29,32 +25,29 @@ Celem projektu jest stworzenie kompleksowego bota Discordowego „Skrzynka Impos
3. **Publikacja wiadomości**
- Usuwanie / aktualizacja poprzedniej wersji.
- Automatyczne wysyłanie po zapisaniu zmian.
4. **Historia zmian**
- Rejestracja timestamp i użytkownika, który wprowadził modyfikacje.
5. **Uprawnienia użytkowników**
- Role: administrator, edytor, przeglądający.
## 5. Wymagania niefunkcjonalne
- **Wydajność:** Wysłanie i aktualizacja < 500ms.
- **Skalowalność:** Obsługa wielu serwerów jednocześnie.
- **Dostępność:** ≥ 99,5% czasu działania.
- **Bezpieczeństwo:** Autoryzacja użytkowników (JWT / OAuth2), ochrona przed XSS w edytorze.
- **Łatwość utrzymania:** Kod: JavaScript (Node.js), bazy danych SQL (PostgreSQL/MySQL).
- **Bezpieczeństwo:** login i hasło zahashowane, możliwość zmiany hasła,
- Kod: JavaScript (Node.js), bazy danych SQL (PostgreSQL/MySQL).
- Bot uruchamiany przez docker compose (v2)
## 6. Technologia i architektura
- **Backend:** Node.js + Express/Koa.
- **Baza danych:** SQL (PostgreSQL lub MySQL).
- **Frontend panelu web:** React lub Vue.
- **Frontend panelu web:** Prosty web panel html + js.
- **Integracja Discord API:** Biblioteka discord.js.
- **Hosting:** Cloud (Heroku, AWS, Azure).
## 7. Formatowanie wiadomości powitalnej
- Wsparcie Discord Markdown (nagłówki, listy, linki, kanały, wzmianki, emoji).
- Możliwość dzielenia treści na kilka embedów, jeśli przekracza limit znaków (2000).
- Automatyczne dzielenie treści na kilka embedów, jeśli przekracza limit znaków (2000).
- Wbudowane zmienne szablonowe (np. `{{user}}`, `{{server}}`, `{{date}}`).
## 8. Panel web
- **Logowanie:** OAuth2 Discord.
- **Logowanie:** Login i hasło
- **Dashboard:**
- Wybór serwera (lista guilds).
- Konfiguracja kanału powitalnego.
@@ -83,8 +76,3 @@ Zarządzanie kanałem i treścią odbywa się przez panel web po zapisaniu zmian
- Panel web pozwala edytować treść i wybór kanału.
- Przekroczenie limitów Discord (2000 znaków) jest obsłużone.
- Dokumentacja API i instrukcja użytkownika.
## 12. Rozwój w kolejnych etapach
- **Rotacje i harmonogramy:** Zaplanowane wysyłki.
- **Wersje językowe:** Automatyczne wykrywanie preferencji.
- **Analizy:** Statystyki odsłon i reakcji użytkowników.