From 0190a0497c7d3e2853de81a9e98ad0f65226b78e Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 20:59:25 +0000 Subject: [PATCH] Update dashboard community links to use env vars, add deploy configs - Dashboard community links now use .env values like community page - Removed hardcoded social media URLs from dashboard - Added deploy folder with nginx and systemd service configs - Moved linktree page to public route - Various backend and auth context updates --- backend/package.json | 2 +- backend/src/index.ts | 12 ++- deploy/back-end_nginx.conf | 81 +++++++++++++++++ deploy/front-end_nginx.conf | 90 +++++++++++++++++++ deploy/spanglish-backend.service | 29 ++++++ deploy/spanglish-frontend.service | 28 ++++++ deploy/spanglish_upstreams.conf | 7 ++ frontend/src/app/(public)/dashboard/page.tsx | 90 +++++++++++-------- frontend/src/app/(public)/page.tsx | 2 +- .../src/app/{(public) => }/linktree/page.tsx | 0 frontend/src/context/AuthContext.tsx | 10 ++- 11 files changed, 302 insertions(+), 49 deletions(-) create mode 100644 deploy/back-end_nginx.conf create mode 100644 deploy/front-end_nginx.conf create mode 100644 deploy/spanglish-backend.service create mode 100644 deploy/spanglish-frontend.service create mode 100644 deploy/spanglish_upstreams.conf rename frontend/src/app/{(public) => }/linktree/page.tsx (100%) diff --git a/backend/package.json b/backend/package.json index d39ed96..9bf1856 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js", + "start": "NODE_ENV=production node dist/index.js", "db:generate": "drizzle-kit generate", "db:migrate": "tsx src/db/migrate.ts", "db:studio": "drizzle-kit studio" diff --git a/backend/src/index.ts b/backend/src/index.ts index 402c1b4..19db9d6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -25,10 +25,14 @@ const app = new Hono(); // Middleware app.use('*', logger()); -app.use('*', cors({ - origin: process.env.FRONTEND_URL || 'http://localhost:3002', - credentials: true, -})); + +// CORS: Only enable in development. In production, nginx handles CORS. +if (process.env.NODE_ENV !== 'production') { + app.use('*', cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3002', + credentials: true, + })); +} // OpenAPI specification const openApiSpec = { diff --git a/deploy/back-end_nginx.conf b/deploy/back-end_nginx.conf new file mode 100644 index 0000000..cb39bb3 --- /dev/null +++ b/deploy/back-end_nginx.conf @@ -0,0 +1,81 @@ +# ============================================================ +# Spanglish Community - Backend API +# api.spanglishcommunity.com +# ============================================================ + +server { + listen 80; + server_name api.spanglishcommunity.com; + + # ACME + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + # Force HTTPS + location / { + return 301 https://api.spanglishcommunity.com$request_uri; + } +} + +server { + listen 443 ssl; + http2 on; + + server_name api.spanglishcommunity.com; + + # SSL + ssl_certificate /etc/letsencrypt/live/spanglishcommunity.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/spanglishcommunity.com/privkey.pem; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Security (API) + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + + # Logs + access_log /var/log/nginx/spanglish_api_access.log; + error_log /var/log/nginx/spanglish_api_error.log; + + location / { + limit_req zone=spanglish_api_limit burst=50 nodelay; + + # CORS Configuration + set $cors_origin ""; + if ($http_origin ~* "^https://(www\.)?spanglishcommunity\.com$") { + set $cors_origin $http_origin; + } + + # Handle preflight OPTIONS requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' $cors_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Max-Age' 86400 always; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + # Add CORS headers to all responses + add_header 'Access-Control-Allow-Origin' $cors_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + + proxy_pass http://spanglish_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; + + proxy_read_timeout 300s; + proxy_connect_timeout 300s; + } +} diff --git a/deploy/front-end_nginx.conf b/deploy/front-end_nginx.conf new file mode 100644 index 0000000..3a2771c --- /dev/null +++ b/deploy/front-end_nginx.conf @@ -0,0 +1,90 @@ +# ============================================================ +# Spanglish Community - Frontend +# spanglishcommunity.com / www +# ============================================================ + +server { + listen 80; + server_name spanglishcommunity.com www.spanglishcommunity.com; + + # ACME + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + # Force HTTPS + location / { + return 301 https://spanglishcommunity.com$request_uri; + } +} + +server { + listen 443 ssl; + http2 on; + + server_name spanglishcommunity.com www.spanglishcommunity.com; + + # SSL + ssl_certificate /etc/letsencrypt/live/spanglishcommunity.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/spanglishcommunity.com/privkey.pem; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Security + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Logs + access_log /var/log/nginx/spanglish_frontend_access.log; + error_log /var/log/nginx/spanglish_frontend_error.log; + + # Proxy /api to backend + location /api { + proxy_pass http://spanglish_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; + + proxy_read_timeout 300s; + proxy_connect_timeout 300s; + } + + # Proxy /uploads to backend + location /uploads { + proxy_pass http://spanglish_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; + + # Cache static files + proxy_cache_valid 200 1d; + expires 1d; + add_header Cache-Control "public, immutable"; + } + + # Frontend App + location / { + proxy_pass http://spanglish_frontend; + 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; + + # WebSocket / HMR + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 60s; + proxy_connect_timeout 60s; + } +} diff --git a/deploy/spanglish-backend.service b/deploy/spanglish-backend.service new file mode 100644 index 0000000..5f44099 --- /dev/null +++ b/deploy/spanglish-backend.service @@ -0,0 +1,29 @@ +[Unit] +Description=Spanglish Backend API +Documentation=https://github.com/spanglish/Spanglish +After=network.target + +[Service] +Type=simple +User=spanglish +Group=spanglish +WorkingDirectory=/home/spanglish/Spanglish/backend +Environment=NODE_ENV=production +Environment=PORT=3018 +EnvironmentFile=/home/spanglish/Spanglish/backend/.env +ExecStart=/usr/bin/node dist/index.js +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=spanglish-backend + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=/home/spanglish/Spanglish/backend/uploads /home/spanglish/Spanglish/backend/data + +[Install] +WantedBy=multi-user.target diff --git a/deploy/spanglish-frontend.service b/deploy/spanglish-frontend.service new file mode 100644 index 0000000..adbd442 --- /dev/null +++ b/deploy/spanglish-frontend.service @@ -0,0 +1,28 @@ +[Unit] +Description=Spanglish Frontend (Next.js) +Documentation=https://github.com/spanglish/Spanglish +After=network.target spanglish-backend.service + +[Service] +Type=simple +User=spanglish +Group=spanglish +WorkingDirectory=/home/spanglish/Spanglish/frontend +Environment=NODE_ENV=production +Environment=PORT=3019 +EnvironmentFile=/home/spanglish/Spanglish/frontend/.env +ExecStart=/usr/bin/npx next start -p 3019 +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=spanglish-frontend + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only + +[Install] +WantedBy=multi-user.target diff --git a/deploy/spanglish_upstreams.conf b/deploy/spanglish_upstreams.conf new file mode 100644 index 0000000..26b2462 --- /dev/null +++ b/deploy/spanglish_upstreams.conf @@ -0,0 +1,7 @@ +upstream spanglish_frontend { + server 127.0.0.1:3019; +} + +upstream spanglish_backend { + server 127.0.0.1:3018; +} \ No newline at end of file diff --git a/frontend/src/app/(public)/dashboard/page.tsx b/frontend/src/app/(public)/dashboard/page.tsx index 468f180..6eefb25 100644 --- a/frontend/src/app/(public)/dashboard/page.tsx +++ b/frontend/src/app/(public)/dashboard/page.tsx @@ -9,6 +9,12 @@ import Button from '@/components/ui/Button'; import { dashboardApi, DashboardSummary, NextEventInfo, UserTicket, UserPayment } from '@/lib/api'; import toast from 'react-hot-toast'; import Link from 'next/link'; +import { + socialConfig, + getWhatsAppUrl, + getInstagramUrl, + getTelegramUrl +} from '@/lib/socialLinks'; // Tab components import TicketsTab from './components/TicketsTab'; @@ -364,45 +370,51 @@ function OverviewTab({ {language === 'es' ? 'Comunidad' : 'Community'}
- -
- - - -
- WhatsApp Group -
- -
- - - -
- Instagram -
- -
- - - -
- - {language === 'es' ? 'Comunidad' : 'Community Page'} - - + {getWhatsAppUrl(socialConfig.whatsapp) && ( + +
+ + + +
+ WhatsApp +
+ )} + {getInstagramUrl(socialConfig.instagram) && ( + +
+ + + +
+ Instagram +
+ )} + {getTelegramUrl(socialConfig.telegram) && ( + +
+ + + +
+ Telegram +
+ )}
diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index ede3e1b..6188f9f 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -97,7 +97,7 @@ export default function HomePage() {
Language exchange event { - const res = await fetch('/api/auth/login', { + const res = await fetch(`${API_BASE}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), @@ -82,7 +84,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }; const loginWithGoogle = async (credential: string) => { - const res = await fetch('/api/auth/google', { + const res = await fetch(`${API_BASE}/api/auth/google`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential }), @@ -98,7 +100,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }; const loginWithMagicLink = async (magicToken: string) => { - const res = await fetch('/api/auth/magic-link/verify', { + const res = await fetch(`${API_BASE}/api/auth/magic-link/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: magicToken }), @@ -114,7 +116,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }; const register = async (registerData: RegisterData) => { - const res = await fetch('/api/auth/register', { + const res = await fetch(`${API_BASE}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(registerData),