diff --git a/.env b/.env new file mode 100644 index 0000000..c4313a7 --- /dev/null +++ b/.env @@ -0,0 +1,68 @@ +# Copy this file to .env for local development. Do not commit your real .env. +# Heroku: set these in Config Vars; dotenv is a no-op in production. + +# Discord bot +TOKEN=OTkxMDYyNzUxNjMzODgzMTM2.Gk0ZeD.lit-9ZcMkLnkfRG5_sECdkbmIbeQzZVs9NFQGs +MODE=Normal + +# Caching and sweepers +CACHE_MESSAGES_LIMIT=50 +CACHE_MEMBERS_LIMIT=100 +SWEEP_MESSAGES_INTERVAL_SECONDS=300 +SWEEP_MESSAGES_LIFETIME_SECONDS=900 + +# Logging and diagnostics +LOG_LEVEL=info +PRISMA_LOG_QUERIES=0 +MEMORY_LOG_INTERVAL_SECONDS=0 +ENABLE_MEMORY_OPTIMIZER=false + +# Redis connection +REDIS_URL=redis-17965.c323.us-east-1-2.ec2.redns.redis-cloud.com +REDIS_PASS=HnPiQFoWwsBdJY62SiHZSEDmnbgiycZ5 + +AI_API_URL=http://100.120.146.67:3001 + +REDIS_URL_VPS="redis://:zrc8YsnAKt@100.120.146.67:6379" + +# PostgreSQL connection (for guild settings, user settings, and AI conversations) +DATABASE_URL="postgresql://postgres:zrc8YsnAKt@100.120.146.67:5432/amayo_db?schema=public&connection_limit=10&pool_timeout=20" + + +TOPGG=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfdCI6Ijc4Mjc3NTcyMjU2NjcwNTE1MiIsImlkIjoiMzcxNDY4MTI4MjIzNjU3OTg0IiwiaWF0IjoxNzY0NDY1MDc1fQ.NZnYnZ8PUpjS7WAVtAivoST7BtmZoYQdsnJ2nkUSBgs + +# OLD DATABASE +XATA_DB=postgresql://9h2ta0:xau_Jtqyo6gW7f0FCPxaWgVhN30E0s5WEZuz0@us-east-1.sql.xata.sh/amayo:main?sslmode=require +XATA_SHADOW_DB=postgresql://9h2ta0:xau_Jtqyo6gW7f0FCPxaWgVhN30E0s5WEZuz0@us-east-1.sql.xata.sh/amayo:shadow?sslmode=require + +# Appwrite (for reminders, AI conversations, and guild cache) +APPWRITE_PROJECT_NAME=amayo +APPWRITE_ENDPOINT=https://nyc.cloud.appwrite.io/v1 +APPWRITE_PROJECT_ID=68d8c4b2001abc54d3cd +APPWRITE_API_KEY=standard_b123c1dbaaf7d3f99aa81492509d6277da0cb89eaf86bb8c42210bae0bb39c3ce911ddf101b6be2d3af8972d44ae63beb32c269c24eedbbe032a6c4c13a16c7e542be00c06ab8e9c131986729d45b68e25ac35e770442ea5285b7367938105a7b2d0e380e944d3bd6582db2c3311b3c9d84be5227718f795f30e31de0b9e8439 +APPWRITE_DATABASE_ID=68d8cb9a00250607e236 +APPWRITE_COLLECTION_REMINDERS_ID=reminders_id +APPWRITE_COLLECTION_AI_CONVERSATIONS_ID=aiconversation +APPWRITE_COLLECTION_GUILD_CACHE_ID=68e536b505604a12df65 + +# Reminders +REMINDERS_POLL_INTERVAL_SECONDS=30 + +# Google AI / Gemini +GOOGLE_AI_API_KEY=AIzaSyDcqOndCJw02xFs305iQE7KVptBoBH8aPk +GEMINI_API_KEY=AIzaSyDNTHsqpbiYaEpe5AwykSqtgWGjsZcc_RA +GENAI_IMAGE_MODEL= + +# Slash command registration context +# Your Discord Application ID +CLIENT=991062751633883136 +DISCORD_CLIENT_ID=991062751633883136 +DISCORD_CLIENT_SECRET=eFcVIGDsacdFBJYsuly-mmIsKjzI1lj4 +OWNER_ID=327207082203938818 +DISCORD_REDIRECT_URI=https://api.amayo.dev/auth/callback +# Test guild ID (for per-guild command registration) +guildTest=1316592320954630144 + +NODE_ENV="production" + +API_HOST="api.amayo.dev" diff --git a/.env.aiservice.example b/.env.aiservice.example new file mode 100644 index 0000000..370ab18 --- /dev/null +++ b/.env.aiservice.example @@ -0,0 +1,3 @@ +AI_API_URL=http://100.120.146.67:3000 +# AI_API_URL=http://192.168.1.100:3000 # Alternative: Local network +# AI_API_URL=https://your-ai-api.com # Alternative: Public endpoint diff --git a/.gitignore b/.gitignore index 8117e9c..d70a7a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Dependencias node_modules -.env .env.test qodana.yaml diff --git a/AmayoWeb/.env b/AmayoWeb/.env new file mode 100644 index 0000000..957167a --- /dev/null +++ b/AmayoWeb/.env @@ -0,0 +1,4 @@ +VITE_DISCORD_CLIENT_ID=1118356676802900089 +# Desarrollo: http://localhost:3000 +# Producción: https://api.amayo.dev +VITE_API_URL=https://api.amayo.dev diff --git a/AmayoWeb/.gitignore b/AmayoWeb/.gitignore index 1f11616..5a78f8e 100644 --- a/AmayoWeb/.gitignore +++ b/AmayoWeb/.gitignore @@ -15,7 +15,6 @@ coverage *.local # Environment variables -.env .env.local .env.*.local diff --git a/AmayoWeb/FRONTEND_DEPLOYMENT.md b/AmayoWeb/FRONTEND_DEPLOYMENT.md new file mode 100644 index 0000000..0e80be3 --- /dev/null +++ b/AmayoWeb/FRONTEND_DEPLOYMENT.md @@ -0,0 +1,81 @@ +# Deployment Instructions - Frontend Update + +## Problema Resuelto + +El botón de "Login with Discord" estaba usando una ruta relativa `/auth/discord` que en producción se convertía en `https://docs.amayo.dev/auth/discord` (frontend) en lugar de `https://api.amayo.dev/auth/discord` (backend). + +## Cambios Realizados + +### 1. [LoginView.vue](file:///home/shnimlz/amayo/AmayoWeb/src/views/LoginView.vue#L31) +```vue +// ANTES: + + +// DESPUÉS: + + +// Script agregado: +const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000' +const discordAuthUrl = `${apiUrl}/auth/discord` +``` + +### 2. [.env](file:///home/shnimlz/amayo/AmayoWeb/.env) +```bash +# ANTES: +VITE_API_URL=http://localhost:3000 + +# DESPUÉS: +VITE_API_URL=https://api.amayo.dev +``` + +## Pasos para Deployment en Servidor + +```bash +# 1. Ir al directorio del frontend +cd /home/shnimlz/amayo/AmayoWeb + +# 2. Asegurarse de que .env tiene la URL correcta +cat .env +# Debe mostrar: VITE_API_URL=https://api.amayo.dev + +# 3. Instalar dependencias (si es necesario) +npm install + +# 4. Hacer build de producción +npm run build + +# 5. Copiar archivos built al directorio de Nginx +sudo cp -r dist/* /var/www/docs.amayo.dev/ + +# 6. Verificar permisos +sudo chown -R www-data:www-data /var/www/docs.amayo.dev + +# 7. Reiniciar Nginx (opcional) +sudo systemctl reload nginx +``` + +## Verificación + +1. Abre https://docs.amayo.dev en **modo incógnito** +2. Click en "Continue with Discord" +3. Deberías ser redirigido a: `https://api.amayo.dev/auth/discord` +4. Luego a Discord OAuth page +5. Luego de vuelta a: `https://api.amayo.dev/auth/callback` +6. Finalmente redirigido a: `https://docs.amayo.dev/dashboard` +7. Verifica que la sesión persiste al hacer refresh + +## Troubleshooting + +### Si sigue redirigiendo incorrectamente: +1. Verifica que el build usó la variable correcta: + ```bash + grep -r "api.amayo.dev" /var/www/docs.amayo.dev/assets/*.js + ``` + +2. Limpia cache del navegador completamente + +3. Verifica que el archivo `.env` tiene la URL correcta ANTES de hacer `npm run build` + +### Si aparece error de CORS: +- El backend ya tiene configurado CORS para `https://docs.amayo.dev` ✅ +- Verifica en DevTools → Network que las requests tengan `Access-Control-Allow-Origin` header diff --git a/AmayoWeb/api.amayo.dev.conf b/AmayoWeb/api.amayo.dev.conf new file mode 100644 index 0000000..94faa73 --- /dev/null +++ b/AmayoWeb/api.amayo.dev.conf @@ -0,0 +1,237 @@ +# Configuración de Nginx para Backend Seguro + +# /etc/nginx/sites-available/api.amayo.dev + +# Configuración para ocultar la IP del servidor y mejorar la seguridad + +# Rate limiting zones +limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m; +limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=3r/m; +limit_conn_zone $binary_remote_addr zone=conn_limit:10m; + +# Bloquear IPs que no sean de Cloudflare +geo $realip_remote_addr $cloudflare_ip { + default 0; + + # Cloudflare IPv4 (actualizar periódicamente desde https://www.cloudflare.com/ips-v4) + 173.245.48.0/20 1; + 103.21.244.0/22 1; + 103.22.200.0/22 1; + 103.31.4.0/22 1; + 141.101.64.0/18 1; + 108.162.192.0/18 1; + 190.93.240.0/20 1; + 188.114.96.0/20 1; + 197.234.240.0/22 1; + 198.41.128.0/17 1; + 162.158.0.0/15 1; + 104.16.0.0/13 1; + 104.24.0.0/14 1; + 172.64.0.0/13 1; + 131.0.72.0/22 1; + + # Cloudflare IPv6 + 2400:cb00::/32 1; + 2606:4700::/32 1; + 2803:f800::/32 1; + 2405:b500::/32 1; + 2405:8100::/32 1; + 2a06:98c0::/29 1; + 2c0f:f248::/32 1; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name api.amayo.dev; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/api.amayo.dev/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.amayo.dev/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # 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 "same-origin" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always; + + # Ocultar versión de Nginx + server_tokens off; + more_clear_headers Server; + more_clear_headers 'X-Hidden-*'; + + # Logs + access_log /var/log/nginx/api.amayo.dev.access.log combined buffer=32k; + error_log /var/log/nginx/api.amayo.dev.error.log warn; + + # Bloquear acceso directo (solo Cloudflare) + # if ($cloudflare_ip = 0) { + # return 403 "Direct access forbidden"; + # } + + # Validar que viene de Cloudflare verificando headers + # if ($http_cf_connecting_ip = "") { + # return 403 "Missing Cloudflare headers"; + # } + + # Usar la IP real del cliente (desde Cloudflare) + set_real_ip_from 173.245.48.0/20; + set_real_ip_from 103.21.244.0/22; + set_real_ip_from 103.22.200.0/22; + set_real_ip_from 103.31.4.0/22; + set_real_ip_from 141.101.64.0/18; + set_real_ip_from 108.162.192.0/18; + set_real_ip_from 190.93.240.0/20; + set_real_ip_from 188.114.96.0/20; + set_real_ip_from 197.234.240.0/22; + set_real_ip_from 198.41.128.0/17; + set_real_ip_from 162.158.0.0/15; + set_real_ip_from 104.16.0.0/13; + set_real_ip_from 104.24.0.0/14; + set_real_ip_from 172.64.0.0/13; + set_real_ip_from 131.0.72.0/22; + real_ip_header CF-Connecting-IP; + + # Bloquear user agents sospechosos + # if ($http_user_agent ~* (curl|wget|python|scrapy|nikto|nmap|sqlmap)) { + # return 403 "Forbidden user agent"; + # } + + # Rate limiting + location /api/auth { + limit_req zone=auth_limit burst=5 nodelay; + limit_conn conn_limit 5; + + proxy_pass http://localhost:3000; + 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 $http_cf_connecting_ip; + 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 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /api { + limit_req zone=api_limit burst=10 nodelay; + limit_conn conn_limit 10; + + # CORS (solo para dominios permitidos) + if ($http_origin ~* (https://docs\.amayo\.dev|https://amayo\.dev)) { + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Client-Token, X-Requested-With, X-Timestamp' always; + add_header 'Access-Control-Expose-Headers' 'X-Server-Token' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + } + + # Handle preflight + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Client-Token, X-Requested-With, X-Timestamp' always; + add_header 'Access-Control-Max-Age' 86400 always; + add_header 'Content-Type' 'text/plain charset=UTF-8' always; + add_header 'Content-Length' 0 always; + return 204; + } + + proxy_pass http://localhost:3000; + 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 $http_cf_connecting_ip; + 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 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location / { + # Manejar preflight OPTIONS + if ($request_method = 'OPTIONS') { + return 204; + } + + proxy_pass http://localhost:3000; + 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 $http_cf_connecting_ip; + 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 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # CORS headers + add_header Access-Control-Allow-Origin "https://docs.amayo.dev" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Max-Age 1728000 always; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + } + + # Servir el archivo de configuración de la API + location /.well-known/api-config.json { + alias /var/www/api.amayo.dev/.well-known/api-config.json; + add_header Content-Type application/json; + add_header Cache-Control "public, max-age=3600"; + } + + # Bloquear acceso a archivos sensibles + location ~ /\. { + deny all; + return 404; + } + + # Bloquear acceso a archivos de backup + location ~* \.(bak|backup|swp|tmp|log)$ { + deny all; + return 404; + } +} + +# Redirección HTTP a HTTPS +server { + listen 80; + listen [::]:80; + server_name api.amayo.dev; + + # Solo permitir ACME challenge para Let's Encrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirigir todo lo demás a HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} diff --git a/AmayoWeb/nginx-docs.amayo.dev.conf b/AmayoWeb/docs.amayo.dev.conf similarity index 85% rename from AmayoWeb/nginx-docs.amayo.dev.conf rename to AmayoWeb/docs.amayo.dev.conf index dc8eb14..c3ac885 100644 --- a/AmayoWeb/nginx-docs.amayo.dev.conf +++ b/AmayoWeb/docs.amayo.dev.conf @@ -46,8 +46,9 @@ server { add_header Cache-Control "public, immutable"; } - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; + # Security headers - Allow top.gg to embed this page + add_header X-Frame-Options "ALLOW-FROM https://top.gg" always; + add_header Content-Security-Policy "frame-ancestors 'self' https://top.gg https://*.top.gg" 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; diff --git a/AmayoWeb/nginx-api.amayo.dev.conf b/AmayoWeb/nginx-api.amayo.dev.conf index b053508..5a3d661 100644 --- a/AmayoWeb/nginx-api.amayo.dev.conf +++ b/AmayoWeb/nginx-api.amayo.dev.conf @@ -1,63 +1,207 @@ -# Configuración NGINX para api.amayo.dev (Backend Node.js) -# Ubicación: /etc/nginx/sites-available/api.amayo.dev - -server { - listen 80; - listen [::]:80; - server_name api.amayo.dev; - - # Redirigir HTTP a HTTPS - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - http2 on; - server_name api.amayo.dev; - - # Certificados SSL (generados con certbot) - ssl_certificate /etc/letsencrypt/live/api.amayo.dev/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.amayo.dev/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - # Logs - access_log /var/log/nginx/api.amayo.dev.access.log; - error_log /var/log/nginx/api.amayo.dev.error.log; - - # Proxy al servidor Node.js en puerto 3000 (el bot ejecuta server.ts) - location / { - # Manejar preflight OPTIONS - if ($request_method = 'OPTIONS') { - return 204; - } - - proxy_pass http://127.0.0.1:3000; - 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 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # CORS headers - add_header Access-Control-Allow-Origin "https://docs.amayo.dev" always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; - add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; - add_header Access-Control-Allow-Credentials "true" always; - add_header Access-Control-Max-Age 1728000 always; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - } -} +# Configuración de Nginx para Backend Seguro + +# /etc/nginx/sites-available/api.amayo.dev + +# Configuración para ocultar la IP del servidor y mejorar la seguridad + +# Rate limiting zones +limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m; +limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=3r/m; +limit_conn_zone $binary_remote_addr zone=conn_limit:10m; + +# Bloquear IPs que no sean de Cloudflare +geo $realip_remote_addr $cloudflare_ip { + default 0; + + # Cloudflare IPv4 (actualizar periódicamente desde https://www.cloudflare.com/ips-v4) + 173.245.48.0/20 1; + 103.21.244.0/22 1; + 103.22.200.0/22 1; + 103.31.4.0/22 1; + 141.101.64.0/18 1; + 108.162.192.0/18 1; + 190.93.240.0/20 1; + 188.114.96.0/20 1; + 197.234.240.0/22 1; + 198.41.128.0/17 1; + 162.158.0.0/15 1; + 104.16.0.0/13 1; + 104.24.0.0/14 1; + 172.64.0.0/13 1; + 131.0.72.0/22 1; + + # Cloudflare IPv6 + 2400:cb00::/32 1; + 2606:4700::/32 1; + 2803:f800::/32 1; + 2405:b500::/32 1; + 2405:8100::/32 1; + 2a06:98c0::/29 1; + 2c0f:f248::/32 1; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name api.amayo.dev; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/api.amayo.dev/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.amayo.dev/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # 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 "same-origin" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always; + + # Ocultar versión de Nginx + server_tokens off; + more_clear_headers Server; + more_clear_headers 'X-Hidden-*'; + + # Logs + access_log /var/log/nginx/api.amayo.dev.access.log combined buffer=32k; + error_log /var/log/nginx/api.amayo.dev.error.log warn; + + # Bloquear acceso directo (solo Cloudflare) + # if ($cloudflare_ip = 0) { + # return 403 "Direct access forbidden"; + # } + + # Validar que viene de Cloudflare verificando headers + # if ($http_cf_connecting_ip = "") { + # return 403 "Missing Cloudflare headers"; + # } + + # Usar la IP real del cliente (desde Cloudflare) + set_real_ip_from 173.245.48.0/20; + set_real_ip_from 103.21.244.0/22; + set_real_ip_from 103.22.200.0/22; + set_real_ip_from 103.31.4.0/22; + set_real_ip_from 141.101.64.0/18; + set_real_ip_from 108.162.192.0/18; + set_real_ip_from 190.93.240.0/20; + set_real_ip_from 188.114.96.0/20; + set_real_ip_from 197.234.240.0/22; + set_real_ip_from 198.41.128.0/17; + set_real_ip_from 162.158.0.0/15; + set_real_ip_from 104.16.0.0/13; + set_real_ip_from 104.24.0.0/14; + set_real_ip_from 172.64.0.0/13; + set_real_ip_from 131.0.72.0/22; + real_ip_header CF-Connecting-IP; + + # Bloquear user agents sospechosos + # if ($http_user_agent ~* (curl|wget|python|scrapy|nikto|nmap|sqlmap)) { + # return 403 "Forbidden user agent"; + # } + + # Rate limiting + location /api/auth { + limit_req zone=auth_limit burst=5 nodelay; + limit_conn conn_limit 5; + + proxy_pass http://localhost:3000; + 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 $http_cf_connecting_ip; + 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 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /api { + limit_req zone=api_limit burst=10 nodelay; + limit_conn conn_limit 10; + + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header Origin $http_origin; + proxy_set_header CF-Connecting-IP $http_cf_connecting_ip; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + 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 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location / { + proxy_pass http://localhost:3000; + 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 $http_cf_connecting_ip; + 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 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + } + + # Servir el archivo de configuración de la API + location /.well-known/api-config.json { + alias /var/www/api.amayo.dev/.well-known/api-config.json; + add_header Content-Type application/json; + add_header Cache-Control "public, max-age=3600"; + } + + # Bloquear acceso a archivos sensibles + location ~ /\. { + deny all; + return 404; + } + + # Bloquear acceso a archivos de backup + location ~* \.(bak|backup|swp|tmp|log)$ { + deny all; + return 404; + } +} + +# Redirección HTTP a HTTPS +server { + listen 80; + listen [::]:80; + server_name api.amayo.dev; + + # Solo permitir ACME challenge para Let's Encrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirigir todo lo demás a HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} diff --git a/AmayoWeb/public/favicon.ico b/AmayoWeb/public/favicon.ico index a288011..1c9066f 100644 Binary files a/AmayoWeb/public/favicon.ico and b/AmayoWeb/public/favicon.ico differ diff --git a/AmayoWeb/src/App.vue b/AmayoWeb/src/App.vue index d2e73e5..75f14af 100644 --- a/AmayoWeb/src/App.vue +++ b/AmayoWeb/src/App.vue @@ -6,12 +6,8 @@ diff --git a/AmayoWeb/src/assets/ama_elegant.png b/AmayoWeb/src/assets/ama_elegant.png new file mode 100644 index 0000000..1164e80 Binary files /dev/null and b/AmayoWeb/src/assets/ama_elegant.png differ diff --git a/AmayoWeb/src/assets/ama_smug.png b/AmayoWeb/src/assets/ama_smug.png new file mode 100644 index 0000000..6d4b69b Binary files /dev/null and b/AmayoWeb/src/assets/ama_smug.png differ diff --git a/AmayoWeb/src/assets/base.css b/AmayoWeb/src/assets/base.css index e14291b..77ce878 100644 --- a/AmayoWeb/src/assets/base.css +++ b/AmayoWeb/src/assets/base.css @@ -1,53 +1,24 @@ -/* color palette from */ +/* Unity Theme Palette (Black/Red) */ :root { - --vt-c-white: #ffffff; - --vt-c-white-soft: #f8f8f8; - --vt-c-white-mute: #f2f2f2; + /* Core Colors */ + --unity-bg: #0a0a0a; + --unity-sidebar: #111111; + --unity-card: #161616; + --unity-card-hover: #1c1c1c; - --vt-c-black: #181818; - --vt-c-black-soft: #222222; - --vt-c-black-mute: #282828; + /* Accents */ + --accent-red: #ff3b3b; + --accent-red-dim: rgba(255, 59, 59, 0.2); + --accent-gradient: linear-gradient(135deg, #ff3b3b 0%, #8a0000 100%); - --vt-c-indigo: #2c3e50; + /* Text */ + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --text-muted: #666666; - --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); - --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); - --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); - --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); - - --vt-c-text-light-1: var(--vt-c-indigo); - --vt-c-text-light-2: rgba(60, 60, 60, 0.66); - --vt-c-text-dark-1: var(--vt-c-white); - --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); -} - -/* semantic color variables for this project */ -:root { - --color-background: var(--vt-c-white); - --color-background-soft: var(--vt-c-white-soft); - --color-background-mute: var(--vt-c-white-mute); - - --color-border: var(--vt-c-divider-light-2); - --color-border-hover: var(--vt-c-divider-light-1); - - --color-heading: var(--vt-c-text-light-1); - --color-text: var(--vt-c-text-light-1); - - --section-gap: 160px; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--vt-c-black); - --color-background-soft: var(--vt-c-black-soft); - --color-background-mute: var(--vt-c-black-mute); - - --color-border: var(--vt-c-divider-dark-2); - --color-border-hover: var(--vt-c-divider-dark-1); - - --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); - } + /* Fonts */ + --font-heading: 'Inter', sans-serif; + --font-body: 'Inter', sans-serif; } *, @@ -55,31 +26,73 @@ *::after { box-sizing: border-box; margin: 0; + padding: 0; } body { min-height: 100vh; - color: var(--color-text); - background: var(--color-background); - transition: - color 0.5s, - background-color 0.5s; + color: var(--text-primary); + background: var(--unity-bg); + font-family: var(--font-body); line-height: 1.6; - font-family: - Inter, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - Oxygen, - Ubuntu, - Cantarell, - 'Fira Sans', - 'Droid Sans', - 'Helvetica Neue', - sans-serif; - font-size: 15px; - text-rendering: optimizeLegibility; + overflow-x: hidden; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } + +/* Utility Classes */ + +/* Glass/Card Panel */ +.unity-panel { + background: var(--unity-card); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.03); + transition: all 0.3s ease; +} + +.unity-panel:hover { + background: var(--unity-card-hover); + border-color: rgba(255, 255, 255, 0.08); +} + +/* Text Gradients */ +.text-gradient-red { + background: var(--accent-gradient); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.5s ease forwards; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: var(--unity-bg); +} + +::-webkit-scrollbar-thumb { + background: #333; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--accent-red); +} \ No newline at end of file diff --git a/AmayoWeb/src/assets/boost1.png b/AmayoWeb/src/assets/boost1.png new file mode 100644 index 0000000..a51ee41 Binary files /dev/null and b/AmayoWeb/src/assets/boost1.png differ diff --git a/AmayoWeb/src/assets/boost2.png b/AmayoWeb/src/assets/boost2.png new file mode 100644 index 0000000..2c29cb8 Binary files /dev/null and b/AmayoWeb/src/assets/boost2.png differ diff --git a/AmayoWeb/src/assets/favicon.ico b/AmayoWeb/src/assets/favicon.ico index a288011..6d59751 100644 Binary files a/AmayoWeb/src/assets/favicon.ico and b/AmayoWeb/src/assets/favicon.ico differ diff --git a/AmayoWeb/src/assets/main.css b/AmayoWeb/src/assets/main.css index 6c148b2..5a2b283 100644 --- a/AmayoWeb/src/assets/main.css +++ b/AmayoWeb/src/assets/main.css @@ -1,4 +1,26 @@ @import './base.css'; +@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700;800;900&family=Lato:wght@300;400;700;900&display=swap'); + +:root { + --rpg-gold: #d4af37; + --rpg-gold-light: #f9e79f; + --rpg-gold-dark: #8a6e2f; + --rpg-red: #8b0000; + --rpg-red-light: #ff1744; + --rpg-dark: #0a0a0a; + --rpg-panel: rgba(20, 20, 25, 0.85); + --font-header: 'Cinzel', serif; + --font-body: 'Lato', sans-serif; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-body); + background-color: var(--rpg-dark); + color: #fff; + overflow-x: hidden; +} #app { width: 100%; @@ -15,15 +37,9 @@ a, padding: 3px; } -@media (hover: hover) { - a:hover { - background-color: hsla(160, 100%, 37%, 0.2); - } -} - @media (min-width: 1024px) { body { display: flex; place-items: center; } -} +} \ No newline at end of file diff --git a/AmayoWeb/src/components/AnimatedBackground.vue b/AmayoWeb/src/components/AnimatedBackground.vue index 5ff3ccc..c24ec3e 100644 --- a/AmayoWeb/src/components/AnimatedBackground.vue +++ b/AmayoWeb/src/components/AnimatedBackground.vue @@ -1,14 +1,15 @@ diff --git a/AmayoWeb/src/components/CelestialBackground.vue b/AmayoWeb/src/components/CelestialBackground.vue new file mode 100644 index 0000000..4636e09 --- /dev/null +++ b/AmayoWeb/src/components/CelestialBackground.vue @@ -0,0 +1,134 @@ + + + diff --git a/AmayoWeb/src/components/DiscordLoginButton.vue b/AmayoWeb/src/components/DiscordLoginButton.vue deleted file mode 100644 index ca5fddb..0000000 --- a/AmayoWeb/src/components/DiscordLoginButton.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/AmayoWeb/src/components/EmbedBuilder.vue b/AmayoWeb/src/components/EmbedBuilder.vue new file mode 100644 index 0000000..86ff728 --- /dev/null +++ b/AmayoWeb/src/components/EmbedBuilder.vue @@ -0,0 +1,842 @@ + + + + + diff --git a/AmayoWeb/src/components/FantasyButton.vue b/AmayoWeb/src/components/FantasyButton.vue new file mode 100644 index 0000000..52e7b67 --- /dev/null +++ b/AmayoWeb/src/components/FantasyButton.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/AmayoWeb/src/components/FantasyCard.vue b/AmayoWeb/src/components/FantasyCard.vue new file mode 100644 index 0000000..e2405fa --- /dev/null +++ b/AmayoWeb/src/components/FantasyCard.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/AmayoWeb/src/components/FeaturesSection.vue b/AmayoWeb/src/components/FeaturesSection.vue new file mode 100644 index 0000000..ae24aec --- /dev/null +++ b/AmayoWeb/src/components/FeaturesSection.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/AmayoWeb/src/components/FloatingHexagons.vue b/AmayoWeb/src/components/FloatingHexagons.vue new file mode 100644 index 0000000..1c1ceb8 --- /dev/null +++ b/AmayoWeb/src/components/FloatingHexagons.vue @@ -0,0 +1,108 @@ + + + diff --git a/AmayoWeb/src/components/HelloWorld.vue b/AmayoWeb/src/components/HelloWorld.vue deleted file mode 100644 index a7252aa..0000000 --- a/AmayoWeb/src/components/HelloWorld.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/AmayoWeb/src/components/HeroSection.vue b/AmayoWeb/src/components/HeroSection.vue index a92c465..7e211cc 100644 --- a/AmayoWeb/src/components/HeroSection.vue +++ b/AmayoWeb/src/components/HeroSection.vue @@ -2,18 +2,25 @@
+
+ {{ t('hero.newVersion') }} 2.0 +
+

- {{ displayText }} - | + {{ t('hero.titleStart') }}
+ Companion

-

{{ t('hero.subtitle') }}

+ +

+ {{ t('hero.subtitle') }} +

- -
@@ -22,10 +29,12 @@ {{ stats.servers }}+ {{ t('hero.servers') }}
+
{{ stats.users }}+ {{ t('hero.users') }}
+
{{ stats.commands }}+ {{ t('hero.commands') }} @@ -34,17 +43,93 @@
-
-
🤝
-
{{ t('hero.feature1') }}
-
-
-
🎫
-
{{ t('hero.feature2') }}
-
-
-
⚙️
-
{{ t('hero.feature3') }}
+ +
+
+ # music-commands +
+ + + +
+
+ +
+ +
+
+ User +
+
+
+ User + Today at 4:20 PM +
+
!play Neon Nights
+
+
+
+ + +
+
+ Amayo +
+
+
+ Amayo + BOT + Today at 4:20 PM +
+
+ + + +
+
+
+ +
+
+ Amayo +
+
+
+ Amayo + BOT + Today at 4:20 PM +
+
+
+
🎵
+
+
+
Now Playing
+
Neon Nights - Synthwave Mix
+
+
+
+
+ 1:24 + 3:45 +
+
+
+
+
+
+
+ + +
+ 🔥 + High Quality Audio +
+ +
+ 🛡️ + Advanced Security +
@@ -52,24 +137,11 @@ diff --git a/AmayoWeb/src/components/ServerSelector.vue b/AmayoWeb/src/components/ServerSelector.vue new file mode 100644 index 0000000..1a9c352 --- /dev/null +++ b/AmayoWeb/src/components/ServerSelector.vue @@ -0,0 +1,524 @@ + + + + + diff --git a/AmayoWeb/src/components/TechButton.vue b/AmayoWeb/src/components/TechButton.vue new file mode 100644 index 0000000..1d49244 --- /dev/null +++ b/AmayoWeb/src/components/TechButton.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/AmayoWeb/src/components/TechCard.vue b/AmayoWeb/src/components/TechCard.vue new file mode 100644 index 0000000..058be01 --- /dev/null +++ b/AmayoWeb/src/components/TechCard.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/AmayoWeb/src/components/TheWelcome.vue b/AmayoWeb/src/components/TheWelcome.vue deleted file mode 100644 index 41aaa4a..0000000 --- a/AmayoWeb/src/components/TheWelcome.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/AmayoWeb/src/components/WelcomeItem.vue b/AmayoWeb/src/components/WelcomeItem.vue deleted file mode 100644 index d3bc3a3..0000000 --- a/AmayoWeb/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/AmayoWeb/src/components/docs/HeroSection.vue b/AmayoWeb/src/components/docs/HeroSection.vue deleted file mode 100644 index 41bf5e8..0000000 --- a/AmayoWeb/src/components/docs/HeroSection.vue +++ /dev/null @@ -1,213 +0,0 @@ - - - - - diff --git a/AmayoWeb/src/components/docs/IslandNavbar.vue b/AmayoWeb/src/components/docs/IslandNavbar.vue deleted file mode 100644 index 140ca61..0000000 --- a/AmayoWeb/src/components/docs/IslandNavbar.vue +++ /dev/null @@ -1,340 +0,0 @@ - - - - - diff --git a/AmayoWeb/src/composables/useTheme.js b/AmayoWeb/src/composables/useTheme.js deleted file mode 100644 index 6edeb6b..0000000 --- a/AmayoWeb/src/composables/useTheme.js +++ /dev/null @@ -1,89 +0,0 @@ -import { ref, watch } from 'vue' - -const themes = { - red: { - primary: '#ff1744', - secondary: '#d50000', - accent: '#ff5252', - gradient: 'linear-gradient(135deg, #ff1744, #d50000)', - glow: 'rgba(255, 23, 68, 0.5)', - }, - blue: { - primary: '#2196f3', - secondary: '#1565c0', - accent: '#64b5f6', - gradient: 'linear-gradient(135deg, #2196f3, #1565c0)', - glow: 'rgba(33, 150, 243, 0.5)', - }, - green: { - primary: '#00e676', - secondary: '#00c853', - accent: '#69f0ae', - gradient: 'linear-gradient(135deg, #00e676, #00c853)', - glow: 'rgba(0, 230, 118, 0.5)', - }, - purple: { - primary: '#e040fb', - secondary: '#9c27b0', - accent: '#ea80fc', - gradient: 'linear-gradient(135deg, #e040fb, #9c27b0)', - glow: 'rgba(224, 64, 251, 0.5)', - }, - orange: { - primary: '#ff9100', - secondary: '#ff6d00', - accent: '#ffab40', - gradient: 'linear-gradient(135deg, #ff9100, #ff6d00)', - glow: 'rgba(255, 145, 0, 0.5)', - }, -} - -const currentTheme = ref('red') - -const applyTheme = (themeName) => { - const theme = themes[themeName] - if (!theme) return - - const root = document.documentElement - - // Aplicar variables CSS - root.style.setProperty('--color-primary', theme.primary) - root.style.setProperty('--color-secondary', theme.secondary) - root.style.setProperty('--color-accent', theme.accent) - root.style.setProperty('--gradient-primary', theme.gradient) - root.style.setProperty('--color-glow', theme.glow) - - // Aplicar data attribute para el tema - root.setAttribute('data-theme', themeName) - - console.log('Theme applied:', themeName, theme) -} - -export function useTheme() { - const setTheme = (themeName) => { - if (themes[themeName]) { - currentTheme.value = themeName - applyTheme(themeName) - localStorage.setItem('theme', themeName) - } - } - - const initTheme = () => { - const savedTheme = localStorage.getItem('theme') - if (savedTheme && themes[savedTheme]) { - currentTheme.value = savedTheme - } - applyTheme(currentTheme.value) - } - - watch(currentTheme, (newTheme) => { - applyTheme(newTheme) - }) - - return { - currentTheme, - themes, - setTheme, - initTheme, - } -} diff --git a/AmayoWeb/src/i18n/locales.js b/AmayoWeb/src/i18n/locales.js index 0b875a8..c4cfb64 100644 --- a/AmayoWeb/src/i18n/locales.js +++ b/AmayoWeb/src/i18n/locales.js @@ -3,8 +3,11 @@ export default { navbar: { getStarted: 'Comenzar', dashboard: 'Panel', + premium: 'Premium', }, hero: { + titleStart: 'Tu Compañero', + newVersion: 'Nueva Versión', subtitle: 'Transforma tu servidor de Discord en una experiencia de Ultima Generacion de comandos con nuevas tecnologias.', exploreFeatures: 'Explorar Características', inviteBot: 'Invitar Bot', @@ -15,6 +18,30 @@ export default { feature2: 'Tickets', feature3: 'AutoMod', }, + features: { + title: 'Todo lo que necesitas', + subtitle: 'Amayo viene cargado con todas las funciones para llevar tu servidor al siguiente nivel.', + ai: { + title: 'Inteligencia Artificial', + desc: 'Recomendaciones inteligentes y respuestas potenciadas por IA.' + }, + music: { + title: 'Música de Alta Calidad', + desc: 'Reproduce música sin lag desde Spotify, YouTube, SoundCloud y más con filtros de audio premium.' + }, + alliances: { + title: 'Alianzas', + desc: 'Gestiona alianzas con otros servidores para crecer juntos.' + }, + web: { + title: 'Panel Web', + desc: 'Configura todo desde un panel de control fácil de usar y moderno.' + }, + embedding: { + title: 'Mejor Embedding', + desc: 'Crea mensajes visuales impactantes con nuestro constructor de embeds.' + } + }, hero_docs: { subtitle: 'En esta seccion esta la documentacion oficial para Amayo Bot.', exploreFeatures: 'Ver Comandos', @@ -58,14 +85,32 @@ export default { green: 'Verde', purple: 'Púrpura', orange: 'Naranja', + }, + premium: { + title: 'Elige tu Nivel', + subtitle: 'Desbloquea todo el potencial de Amayo con nuestros planes premium.', + get: 'Obtener', + month: 'Mensual', + personal: 'Uso Personal', + boost1: '1 Boost Server', + boost2: '2 Boost Server', + features: { + volumeBoost: 'Volumen Boost', + betterRecs: 'Mejores Recomendaciones', + playlistLimit: 'Mayor Numero de canciones a importar en la playlist 100>200', + tier1Rewards: 'Recompensas del 1 boost', + } } }, en: { navbar: { getStarted: 'Get Started', dashboard: 'Dashboard', + premium: 'Premium', }, hero: { + titleStart: 'Your Music', + newVersion: 'New Version', subtitle: 'Transform your Discord server into a Next-Gen command experience with cutting-edge technologies.', exploreFeatures: 'Explore Features', inviteBot: 'Invite Bot', @@ -76,7 +121,31 @@ export default { feature2: 'Tickets', feature3: 'AutoMod', }, - hero_docs: { + features: { + title: 'Everything you need', + subtitle: 'Amayo comes loaded with all the features to take your server to the next level.', + ai: { + title: 'Artificial Intelligence', + desc: 'Smart recommendations and AI-powered responses.' + }, + music: { + title: 'High Quality Music', + desc: 'Play lag-free music from Spotify, YouTube, SoundCloud and more with premium audio filters.' + }, + alliances: { + title: 'Alliances', + desc: 'Manage alliances with other servers to grow together.' + }, + web: { + title: 'Web Dashboard', + desc: 'Configure everything from an easy-to-use and modern control panel.' + }, + embedding: { + title: 'Better Embedding', + desc: 'Create stunning visual messages with our embed builder.' + } + }, + hero_docs: { subtitle: 'This section contains the official documentation for Amayo Bot.', exploreFeatures: 'View Commands', inviteBot: 'View Functions', @@ -119,6 +188,21 @@ export default { green: 'Green', purple: 'Purple', orange: 'Orange', + }, + premium: { + title: 'Choose Your Tier', + subtitle: 'Unlock the full potential of Amayo with our premium plans.', + get: 'Get', + month: 'Monthly', + personal: 'Personal Use', + boost1: '1 Boost Server', + boost2: '2 Boost Server', + features: { + volumeBoost: 'Volume Boost', + betterRecs: 'Better Recommendations', + playlistLimit: 'Higher song import limit 100>200', + tier1Rewards: '1 Boost Rewards', + } } } } diff --git a/AmayoWeb/src/router/index.js b/AmayoWeb/src/router/index.js index 6fe8c0e..71c4eaa 100644 --- a/AmayoWeb/src/router/index.js +++ b/AmayoWeb/src/router/index.js @@ -9,11 +9,6 @@ const router = createRouter({ name: 'home', component: () => import('../views/HomeView.vue') }, - { - path: '/docs', - name: 'docs', - component: () => import('../views/DocsView.vue') - }, { path: '/auth/callback', name: 'auth-callback', @@ -28,6 +23,30 @@ const router = createRouter({ path: '/privacy', name: 'privacy', component: () => import('../views/PrivacyPolicy.vue') + }, + { + path: '/premium', + name: 'premium', + component: () => import('../views/PremiumView.vue') + }, + { + path: '/dash/:guildId', + name: 'dashboard', + component: () => import('../views/DashboardView.vue') + }, + { + path: '/dash/:guildId/settings', + name: 'settings', + component: () => import('../views/SettingsView.vue') + }, + { + path: '/login', + name: 'login', + component: () => import('../views/LoginView.vue') + }, + { + path: '/dashboard', + redirect: '/dash/me' // Temporary redirect until guild selection is implemented } ] }) @@ -35,7 +54,7 @@ const router = createRouter({ // Navigation guard para rutas protegidas router.beforeEach((to, from, next) => { const token = localStorage.getItem('authToken') - + if (to.meta.requiresAuth && !token) { next('/') } else { diff --git a/AmayoWeb/src/services/auth.js b/AmayoWeb/src/services/auth.js index 5644fd3..95ad4e6 100644 --- a/AmayoWeb/src/services/auth.js +++ b/AmayoWeb/src/services/auth.js @@ -1,5 +1,5 @@ import axios from 'axios' -import { securityService, rateLimiter } from './security' +import { securityService } from './security' // Inicializar servicio de seguridad await securityService.initialize().catch(err => { @@ -9,11 +9,12 @@ await securityService.initialize().catch(err => { // Crear instancia de axios con configuración de seguridad const createSecureAxios = () => { const instance = axios.create({ - timeout: 10000, // 10 segundos timeout + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', + timeout: 10000, + withCredentials: true, // Importante para enviar cookies de sesión headers: securityService.getSecurityHeaders() }) - // Interceptor para agregar headers de seguridad instance.interceptors.request.use( config => { config.headers = { @@ -25,11 +26,9 @@ const createSecureAxios = () => { error => Promise.reject(error) ) - // Interceptor para validar respuestas instance.interceptors.response.use( response => securityService.validateResponse(response), error => { - // Manejar errores de forma segura if (error.response?.status === 429) { console.error('Rate limit exceeded') } @@ -40,141 +39,47 @@ const createSecureAxios = () => { return instance } -const secureAxios = createSecureAxios() - -// No exponer la URL directamente - usar el servicio de seguridad -const getApiUrl = (path) => { - try { - const baseUrl = securityService.getApiEndpoint() - return `${baseUrl}${path}` - } catch (error) { - console.error('Failed to get API URL:', error) - throw new Error('API service unavailable') - } -} +export const secureAxios = createSecureAxios() export const authService = { - // Redirigir al usuario a Discord OAuth2 - loginWithDiscord() { - // Rate limiting para prevenir abuso - if (!rateLimiter.canMakeRequest('/auth/discord', 'auth')) { - const remainingTime = Math.ceil(rateLimiter.getRemainingTime('/auth/discord', 'auth') / 1000) - throw new Error(`Too many login attempts. Please wait ${remainingTime} seconds.`) - } + // El login se hace directamente con el link href="/auth/discord" en LoginView + // El proxy de Vite redirige /auth/* a localhost:3000 - const clientId = import.meta.env.VITE_DISCORD_CLIENT_ID - if (!clientId) { - throw new Error('Discord client ID not configured') - } - - const redirectUri = import.meta.env.PROD - ? window.location.origin + '/auth/callback' - : 'http://localhost:5173/auth/callback' - - const scope = 'identify guilds' - const state = securityService.generateSessionToken() // CSRF protection - - // Guardar state para validación - sessionStorage.setItem('oauth_state', state) - - const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}&state=${state}` - - window.location.href = authUrl - }, - - // Intercambiar código por token - async handleCallback(code, state) { - // Validar state para prevenir CSRF - const savedState = sessionStorage.getItem('oauth_state') - if (state !== savedState) { - throw new Error('Invalid OAuth state - possible CSRF attack') - } - sessionStorage.removeItem('oauth_state') - - // Rate limiting - if (!rateLimiter.canMakeRequest('/auth/callback', 'auth')) { - throw new Error('Too many authentication attempts') - } - - try { - const response = await secureAxios.post( - getApiUrl('/auth/discord/callback'), - { code, state } - ) - - const { token, user } = response.data - - if (!token || !user) { - throw new Error('Invalid authentication response') - } - - // Guardar token de forma segura - localStorage.setItem('authToken', token) - localStorage.setItem('user', JSON.stringify(user)) - - return { token, user } - } catch (error) { - console.error('Authentication error:', error) - throw new Error('Authentication failed') - } - }, - - // Obtener usuario actual + // Obtener información de sesión actual (del backend via proxy) async getCurrentUser() { - const token = localStorage.getItem('authToken') - if (!token) return null - - // Rate limiting - if (!rateLimiter.canMakeRequest('/auth/me', 'api')) { - throw new Error('Too many requests') - } - try { - const response = await secureAxios.get(getApiUrl('/auth/me')) - return response.data - } catch (error) { - console.error('Error fetching user:', error) - - // Si el token es inválido, hacer logout - if (error.response?.status === 401) { - this.logout() + const response = await secureAxios.get('/api/session') + if (response.data && response.data.user) { + localStorage.setItem('user', JSON.stringify(response.data.user)) + return response.data.user } - + return null + } catch (error) { + console.error('Error fetching session:', error) + return null + } + }, + + // Verificar autenticación (basado en sesión) + isAuthenticated() { + const user = localStorage.getItem('user') + return !!user + }, + + // Obtener usuario cacheado + getCachedUser() { + try { + const user = localStorage.getItem('user') + return user ? JSON.parse(user) : null + } catch { return null } }, // Logout logout() { - localStorage.removeItem('authToken') localStorage.removeItem('user') - securityService.clearSensitiveData() - window.location.href = '/' - }, - - // Verificar si el usuario está autenticado - isAuthenticated() { - const token = localStorage.getItem('authToken') - if (!token) return false - - // Validar que el token no esté expirado (básico) - try { - const payload = JSON.parse(atob(token.split('.')[1])) - const isExpired = payload.exp && payload.exp * 1000 < Date.now() - - if (isExpired) { - this.logout() - return false - } - - return true - } catch { - return !!token // Fallback si no se puede decodificar - } - }, - - // Obtener token - getToken() { - return localStorage.getItem('authToken') + // Redirigir a la ruta de logout que será proxiada + window.location.href = '/auth/logout' } } diff --git a/AmayoWeb/src/services/bot.js b/AmayoWeb/src/services/bot.js index 9147be6..a32f8f7 100644 --- a/AmayoWeb/src/services/bot.js +++ b/AmayoWeb/src/services/bot.js @@ -9,6 +9,7 @@ await securityService.initialize().catch(err => { // Crear instancia de axios con configuración de seguridad const createSecureAxios = () => { const instance = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', timeout: 10000, headers: securityService.getSecurityHeaders() }) @@ -54,15 +55,15 @@ export const botService = { } try { - const response = await secureAxios.get(getApiUrl('/bot/stats')) - + const response = await secureAxios.get(getApiUrl('/api/bot/stats')) + // Cachear los resultados this.cacheStats(response.data) - + return response.data } catch (error) { console.error('Error fetching bot stats:', error) - + // Retornar stats cacheadas si falló la petición return this.getCachedStats() || { servers: 0, @@ -80,11 +81,11 @@ export const botService = { } try { - const response = await secureAxios.get(getApiUrl('/bot/info')) - + const response = await secureAxios.get(getApiUrl('/api/bot/info')) + // Cachear info del bot this.cacheBotInfo(response.data) - + return response.data } catch (error) { console.error('Error fetching bot info:', error) diff --git a/AmayoWeb/src/services/embeds.js b/AmayoWeb/src/services/embeds.js new file mode 100644 index 0000000..0013533 --- /dev/null +++ b/AmayoWeb/src/services/embeds.js @@ -0,0 +1,33 @@ +import { secureAxios } from './auth' + +export const embedsService = { + // Get all embeds for a guild + async getEmbeds(guildId) { + const response = await secureAxios.get(`/api/guilds/${guildId}/embeds`) + return response.data + }, + + // Get a specific embed + async getEmbed(guildId, embedId) { + const response = await secureAxios.get(`/api/guilds/${guildId}/embeds/${embedId}`) + return response.data + }, + + // Create a new embed + async createEmbed(guildId, data) { + const response = await secureAxios.post(`/api/guilds/${guildId}/embeds`, data) + return response.data + }, + + // Update an embed + async updateEmbed(guildId, embedId, data) { + const response = await secureAxios.put(`/api/guilds/${guildId}/embeds/${embedId}`, data) + return response.data + }, + + // Delete an embed + async deleteEmbed(guildId, embedId) { + await secureAxios.delete(`/api/guilds/${guildId}/embeds/${embedId}`) + return true + } +} diff --git a/AmayoWeb/src/services/guilds.js b/AmayoWeb/src/services/guilds.js new file mode 100644 index 0000000..3d7d12e --- /dev/null +++ b/AmayoWeb/src/services/guilds.js @@ -0,0 +1,290 @@ +import axios from 'axios' +import { securityService, rateLimiter } from './security' + +// Crear instancia de axios con configuración de seguridad +const createSecureAxios = () => { + const instance = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', + timeout: 10000, + withCredentials: true, // Importante para enviar cookies + headers: securityService.getSecurityHeaders() + }) + + instance.interceptors.request.use( + config => { + config.headers = { + ...config.headers, + ...securityService.getSecurityHeaders() + } + return config + }, + error => Promise.reject(error) + ) + + instance.interceptors.response.use( + response => securityService.validateResponse(response), + error => { + if (error.response?.status === 429) { + console.error('Rate limit exceeded') + } + return Promise.reject(error) + } + ) + + return instance +} + +const secureAxios = createSecureAxios() + +const getApiUrl = (path) => { + try { + const baseUrl = securityService.getApiEndpoint() + return `${baseUrl}${path}` + } catch (error) { + console.error('Failed to get API URL:', error) + throw new Error('API service unavailable') + } +} + +export const guildService = { + // Obtener servidores del usuario desde la sesión (via proxy) + async getUserGuilds() { + // Rate limiting + if (!rateLimiter.canMakeRequest('/api/session', 'api')) { + return this.getCachedGuilds() || [] + } + + try { + const response = await secureAxios.get('/api/session') + + if (response.data && response.data.guilds) { + this.cacheGuilds(response.data.guilds) + return response.data.guilds + } + + return [] + } catch (error) { + console.error('Error fetching user guilds:', error) + return this.getCachedGuilds() || [] + } + }, + + // Cache system + cacheGuilds(guilds) { + try { + const cacheData = { + data: guilds, + timestamp: Date.now(), + expiresIn: 5 * 60 * 1000 // 5 minutes + } + sessionStorage.setItem('user_guilds_cache', JSON.stringify(cacheData)) + } catch (error) { + console.error('Failed to cache guilds:', error) + } + }, + + getCachedGuilds() { + try { + const cached = sessionStorage.getItem('user_guilds_cache') + if (!cached) return null + + const cacheData = JSON.parse(cached) + const isExpired = Date.now() - cacheData.timestamp > cacheData.expiresIn + + if (isExpired) { + sessionStorage.removeItem('user_guilds_cache') + return null + } + + return cacheData.data + } catch (error) { + console.error('Failed to get cached guilds:', error) + return null + } + }, + + // Get guild statistics + async getGuildStats(guildId) { + if (!rateLimiter.canMakeRequest(`/api/guild/${guildId}/stats`, 'api')) { + return this.getCachedGuildStats(guildId) || null + } + + try { + const response = await secureAxios.get(`/api/guild/${guildId}/stats`) + if (response.data) { + this.cacheGuildStats(guildId, response.data) + return response.data + } + return null + } catch (error) { + console.error('Error fetching guild stats:', error) + return this.getCachedGuildStats(guildId) || null + } + }, + + // Get recent guild actions + async getGuildActions(guildId) { + if (!rateLimiter.canMakeRequest(`/api/guild/${guildId}/actions`, 'api')) { + return this.getCachedGuildActions(guildId) || [] + } + + try { + const response = await secureAxios.get(`/api/guild/${guildId}/actions`) + if (response.data && response.data.actions) { + this.cacheGuildActions(guildId, response.data.actions) + return response.data.actions + } + return [] + } catch (error) { + console.error('Error fetching guild actions:', error) + return this.getCachedGuildActions(guildId) || [] + } + }, + + // Cache guild stats + cacheGuildStats(guildId, stats) { + try { + const cacheData = { + data: stats, + timestamp: Date.now(), + expiresIn: 30 * 1000 // 30 seconds + } + sessionStorage.setItem(`guild_stats_${guildId}`, JSON.stringify(cacheData)) + } catch (error) { + console.error('Failed to cache guild stats:', error) + } + }, + + getCachedGuildStats(guildId) { + try { + const cached = sessionStorage.getItem(`guild_stats_${guildId}`) + if (!cached) return null + + const cacheData = JSON.parse(cached) + const isExpired = Date.now() - cacheData.timestamp > cacheData.expiresIn + + if (isExpired) { + sessionStorage.removeItem(`guild_stats_${guildId}`) + return null + } + + return cacheData.data + } catch (error) { + console.error('Failed to get cached guild stats:', error) + return null + } + }, + + // Cache guild actions + cacheGuildActions(guildId, actions) { + try { + const cacheData = { + data: actions, + timestamp: Date.now(), + expiresIn: 15 * 1000 // 15 seconds + } + sessionStorage.setItem(`guild_actions_${guildId}`, JSON.stringify(cacheData)) + } catch (error) { + console.error('Failed to cache guild actions:', error) + } + }, + + getCachedGuildActions(guildId) { + try { + const cached = sessionStorage.getItem(`guild_actions_${guildId}`) + if (!cached) return null + + const cacheData = JSON.parse(cached) + const isExpired = Date.now() - cacheData.timestamp > cacheData.expiresIn + + if (isExpired) { + sessionStorage.removeItem(`guild_actions_${guildId}`) + return null + } + + return cacheData.data + } catch (error) { + console.error('Failed to get cached guild actions:', error) + return null + } + }, + + // Get guild settings + async getGuildSettings(guildId) { + if (!rateLimiter.canMakeRequest(`/api/guild/${guildId}/settings`, 'api')) { + return this.getCachedGuildSettings(guildId) || null + } + + try { + const response = await secureAxios.get(`/api/guild/${guildId}/settings`) + if (response.data) { + this.cacheGuildSettings(guildId, response.data) + return response.data + } + return null + } catch (error) { + console.error('Error fetching guild settings:', error) + return this.getCachedGuildSettings(guildId) || null + } + }, + + // Update guild settings + async updateGuildSettings(guildId, settings) { + try { + const response = await secureAxios.patch(`/api/guild/${guildId}/settings`, settings) + if (response.data && response.data.settings) { + this.cacheGuildSettings(guildId, response.data.settings) + return response.data + } + return null + } catch (error) { + console.error('Error updating guild settings:', error) + throw error + } + }, + + // Cache guild settings + cacheGuildSettings(guildId, settings) { + try { + const cacheData = { + data: settings, + timestamp: Date.now(), + expiresIn: 60 * 1000 // 1 minute + } + sessionStorage.setItem(`guild_settings_${guildId}`, JSON.stringify(cacheData)) + } catch (error) { + console.error('Failed to cache guild settings:', error) + } + }, + + getCachedGuildSettings(guildId) { + try { + const cached = sessionStorage.getItem(`guild_settings_${guildId}`) + if (!cached) return null + + const cacheData = JSON.parse(cached) + const isExpired = Date.now() - cacheData.timestamp > cacheData.expiresIn + + if (isExpired) { + sessionStorage.removeItem(`guild_settings_${guildId}`) + return null + } + + return cacheData.data + } catch (error) { + console.error('Failed to get cached guild settings:', error) + return null + } + }, + + // Get guild roles + async getGuildRoles(guildId) { + try { + const response = await secureAxios.get(`/api/guild/${guildId}/roles`) + return response.data || [] + } catch (error) { + console.error('Error fetching guild roles:', error) + return [] + } + } +} diff --git a/AmayoWeb/src/services/user.js b/AmayoWeb/src/services/user.js new file mode 100644 index 0000000..cd5d701 --- /dev/null +++ b/AmayoWeb/src/services/user.js @@ -0,0 +1,80 @@ +import { secureAxios } from './auth' + +export const userService = { + // Get user dashboard data + async getUserData() { + try { + const response = await secureAxios.get('/api/user/me') + return response.data + } catch (error) { + console.error('Failed to get user data:', error) + throw error + } + }, + + // Redeem coupon + async redeemCoupon(code) { + try { + const response = await secureAxios.post('/api/user/coupon/redeem', { code }) + return response.data + } catch (error) { + console.error('Failed to redeem coupon:', error) + throw error + } + }, + + // Get user playlists + async getUserPlaylists() { + try { + const response = await secureAxios.get('/api/user/playlists') + return response.data + } catch (error) { + console.error('Failed to get user playlists:', error) + throw error + } + }, + + // Create playlist + async createPlaylist(data) { + try { + const response = await secureAxios.post('/api/user/playlists', data) + return response.data + } catch (error) { + console.error('Failed to create playlist:', error) + throw error + } + }, + + // Get playlist details + async getPlaylistDetails(id) { + try { + const response = await secureAxios.get(`/api/user/playlists/${id}`) + return response.data + } catch (error) { + console.error('Failed to get playlist details:', error) + throw error + } + }, + + // Reorder playlist + async reorderPlaylist(id, tracks) { + try { + const response = await secureAxios.put(`/api/user/playlists/${id}/reorder`, { tracks }) + return response.data + } catch (error) { + console.error('Failed to reorder playlist:', error) + throw error + } + }, + + // Delete playlist + async deletePlaylist(id) { + try { + const response = await secureAxios.delete(`/api/user/playlists/${id}`) + return response.data + } catch (error) { + console.error('Failed to delete playlist:', error) + throw error + } + } +} diff --git a/AmayoWeb/src/views/AuthCallback.vue b/AmayoWeb/src/views/AuthCallback.vue index 215d1de..b6c49eb 100644 --- a/AmayoWeb/src/views/AuthCallback.vue +++ b/AmayoWeb/src/views/AuthCallback.vue @@ -16,28 +16,22 @@ const router = useRouter() const message = ref('Autenticando con Discord...') onMounted(async () => { - const urlParams = new URLSearchParams(window.location.search) - const code = urlParams.get('code') - const error = urlParams.get('error') - - if (error) { - message.value = 'Error en la autenticación' - setTimeout(() => router.push('/'), 2000) - return - } - - if (code) { - try { - await authService.handleCallback(code) + try { + // El backend ya procesó OAuth y estableció la cookie de sesión + // Solo necesitamos obtener los datos del usuario + const user = await authService.getCurrentUser() + + if (user) { message.value = '¡Autenticación exitosa!' - setTimeout(() => router.push('/dashboard'), 1500) - } catch (err) { - message.value = 'Error al procesar la autenticación' - console.error(err) + // Redirigir al selector de servidors + setTimeout(() => router.push('/dash/me'), 500) + } else { + message.value = 'Error: No se pudo obtener la sesión' setTimeout(() => router.push('/'), 2000) } - } else { - message.value = 'Código no encontrado' + } catch (error) { + console.error('Auth callback error:', error) + message.value = 'Error al procesar la autenticación' setTimeout(() => router.push('/'), 2000) } }) diff --git a/AmayoWeb/src/views/DashboardView.vue b/AmayoWeb/src/views/DashboardView.vue new file mode 100644 index 0000000..9259d9c --- /dev/null +++ b/AmayoWeb/src/views/DashboardView.vue @@ -0,0 +1,3401 @@ + + + + + diff --git a/AmayoWeb/src/views/DocsView.vue b/AmayoWeb/src/views/DocsView.vue deleted file mode 100644 index 30fe26e..0000000 --- a/AmayoWeb/src/views/DocsView.vue +++ /dev/null @@ -1,466 +0,0 @@ - - - - - diff --git a/AmayoWeb/src/views/EmbedsView.vue b/AmayoWeb/src/views/EmbedsView.vue new file mode 100644 index 0000000..1539523 --- /dev/null +++ b/AmayoWeb/src/views/EmbedsView.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/AmayoWeb/src/views/HomeView.vue b/AmayoWeb/src/views/HomeView.vue index 1161c07..010ff6c 100644 --- a/AmayoWeb/src/views/HomeView.vue +++ b/AmayoWeb/src/views/HomeView.vue @@ -3,6 +3,7 @@ +
@@ -10,6 +11,7 @@ import AnimatedBackground from '../components/AnimatedBackground.vue' import IslandNavbar from '../components/IslandNavbar.vue' import HeroSection from '../components/HeroSection.vue' +import FeaturesSection from '../components/FeaturesSection.vue' diff --git a/AmayoWeb/src/views/PremiumView.vue b/AmayoWeb/src/views/PremiumView.vue new file mode 100644 index 0000000..9c92c20 --- /dev/null +++ b/AmayoWeb/src/views/PremiumView.vue @@ -0,0 +1,335 @@ + + + + + diff --git a/AmayoWeb/src/views/PrivacyPolicy.vue b/AmayoWeb/src/views/PrivacyPolicy.vue index ac8143d..e542a48 100644 --- a/AmayoWeb/src/views/PrivacyPolicy.vue +++ b/AmayoWeb/src/views/PrivacyPolicy.vue @@ -1,8 +1,7 @@