Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 77 additions & 45 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,73 +1,105 @@
# --- Core / Environment
APP_ADDITIONAL_ORIGINS=https://admin.example.test
APP_ENV=
APP_ORIGIN=https://example.test
APP_URL=
NEXT_PUBLIC_SITE_URL=

# --- Database
DATABASE_URL=

# --- Upstash Redis (REST)
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# --- Auth (app)
AUTH_SECRET=

CLOUDINARY_CLOUD_NAME=
# --- OAuth: Google
GOOGLE_CLIENT_ID_DEVELOP=
GOOGLE_CLIENT_ID_LOCAL=
GOOGLE_CLIENT_ID_PROD=
GOOGLE_CLIENT_REDIRECT_URI_DEVELOP=
GOOGLE_CLIENT_REDIRECT_URI_LOCAL=
GOOGLE_CLIENT_REDIRECT_URI_PROD=
GOOGLE_CLIENT_SECRET_DEVELOP=
GOOGLE_CLIENT_SECRET_LOCAL=
GOOGLE_CLIENT_SECRET_PROD=

# --- OAuth: GitHub
GITHUB_CLIENT_ID_DEVELOP=
GITHUB_CLIENT_ID_LOCAL=
GITHUB_CLIENT_ID_PROD=
GITHUB_CLIENT_REDIRECT_URI_DEVELOP=
GITHUB_CLIENT_REDIRECT_URI_LOCAL=
GITHUB_CLIENT_REDIRECT_URI_PROD=
GITHUB_CLIENT_SECRET_DEVELOP=
GITHUB_CLIENT_SECRET_LOCAL=
GITHUB_CLIENT_SECRET_PROD=

# --- Cloudinary
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_UPLOAD_FOLDER=

CLOUDINARY_URL=
ENABLE_ADMIN_API=

# --- Payments (Stripe)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
PAYMENTS_ENABLED=
NEXT_PUBLIC_PAYMENTS_ENABLED=
# Options: test, live (defaults to test in development, live in production)
STRIPE_MODE=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

STRIPE_MODE=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# --- Admin / Internal ops
ENABLE_ADMIN_API=
INTERNAL_JANITOR_MIN_INTERVAL_SECONDS=
INTERNAL_JANITOR_SECRET=
JANITOR_URL=

NEXT_PUBLIC_SITE_URL=
NEXT_PUBLIC_SITE_URL=
# --- Quiz
QUIZ_ENCRYPTION_KEY=

# --- Telegram
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_REDIRECT_URI_LOCAL=
GOOGLE_CLIENT_REDIRECT_URI_DEVELOP=
GOOGLE_CLIENT_REDIRECT_URI_PROD=
GITHUB_CLIENT_ID_DEVELOP=
GITHUB_CLIENT_SECRET_DEVELOP=
GITHUB_CLIENT_REDIRECT_URI_DEVELOP=


APP_ENV=

INTERNAL_JANITOR_SECRET=
INTERNAL_JANITOR_MIN_INTERVAL_SECONDS=

QUIZ_ENCRYPTION_KEY=

GMAIL_USER=
GMAIL_APP_PASSWORD=
# --- Email (Gmail SMTP)
EMAIL_FROM=
APP_URL=
GMAIL_APP_PASSWORD=
GMAIL_USER=

# --- Security
CSRF_SECRET=

CHECKOUT_RATE_LIMIT_MAX=
CHECKOUT_RATE_LIMIT_WINDOW_SECONDS=
CHECKOUT_RATE_LIMIT_MAX=10
CHECKOUT_RATE_LIMIT_WINDOW_SECONDS=300

STRIPE_WEBHOOK_INVALID_SIG_RL_MAX=
STRIPE_WEBHOOK_INVALID_SIG_RL_WINDOW_SECONDS=
# Stripe webhook rate limit envs (applied per reason; reason-specific overrides generic).
# Missing signature has its own envs with fallback to generic, then legacy invalid_sig.
STRIPE_WEBHOOK_MISSING_SIG_RL_MAX=30
STRIPE_WEBHOOK_MISSING_SIG_RL_WINDOW_SECONDS=60

RATE_LIMIT_DISABLED=
# Generic Stripe webhook rate limit fallback (applies to missing_sig and invalid_sig).
STRIPE_WEBHOOK_RL_MAX=30
STRIPE_WEBHOOK_RL_WINDOW_SECONDS=60

STRIPE_WEBHOOK_MISSING_SIG_RL_MAX=
STRIPE_WEBHOOK_MISSING_SIG_RL_WINDOW_SECONDS=
# Invalid signature envs (canonical for invalid_sig, legacy fallback for missing_sig).
STRIPE_WEBHOOK_INVALID_SIG_RL_MAX=30
STRIPE_WEBHOOK_INVALID_SIG_RL_WINDOW_SECONDS=60

STRIPE_WEBHOOK_RL_MAX=
STRIPE_WEBHOOK_RL_WINDOW_SECONDS=
# SECURITY: If true, trust Cloudflare's cf-connecting-ip header for rate limiting.
# Enable ONLY when traffic is fronted by Cloudflare (header is set by Cloudflare at the edge).
# Default: false (0). Keep 0 in untrusted environments to avoid IP spoofing.
TRUST_CF_CONNECTING_IP=0

APP_ORIGIN=
APP_ADDITIONAL_ORIGINS=
# SECURITY: If true, trust x-real-ip / x-forwarded-for headers for rate limiting.
# Enable ONLY behind Cloudflare or a trusted reverse proxy that overwrites these headers.
# Default: false (empty/0/false).
TRUST_FORWARDED_HEADERS=0

TRUST_CF_CONNECTING_IP=
TRUST_FORWARDED_HEADERS=
# emergency switch
RATE_LIMIT_DISABLED=0

GROQ_API_KEY=

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
GROQ_API_KEY=
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing trailing newline.

Static analysis flags no blank line at end of file. Most tools and POSIX convention expect a trailing newline.

 GROQ_API_KEY=
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GROQ_API_KEY=
GROQ_API_KEY=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 105-105: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
In `@frontend/.env.example` at line 105, Add a trailing newline to the file so it
ends with a newline character; specifically update the .env.example so the final
line "GROQ_API_KEY=" is followed by a blank line (newline at EOF) to satisfy
POSIX tooling and static analysis.

12 changes: 10 additions & 2 deletions frontend/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';

import { defaultLocale, locales } from '@/i18n/config';
import { AuthTokenPayload } from '@/lib/auth';

import { routing } from './i18n/routing';
Expand Down Expand Up @@ -52,6 +53,13 @@ function isAuthenticated(req: NextRequest): boolean {

const intlMiddleware = createIntlMiddleware(routing);

function resolveLocaleFromPathname(pathname: string): string {
const maybeLocale = pathname.split('/')[1];
return locales.includes(maybeLocale as (typeof locales)[number])
? maybeLocale
: defaultLocale;
}

function authMiddleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const authenticated = isAuthenticated(req);
Expand All @@ -61,7 +69,7 @@ function authMiddleware(req: NextRequest) {

if (pathnameWithoutLocale.startsWith('/dashboard')) {
if (!authenticated) {
const locale = pathname.split('/')[1] || 'en';
const locale = resolveLocaleFromPathname(pathname);
return NextResponse.redirect(new URL(`/${locale}/login`, req.url));
}
}
Expand All @@ -81,7 +89,7 @@ export function proxy(req: NextRequest) {
return NextResponse.redirect(new URL('/en', req.url));
}

const locale = req.nextUrl.pathname.split('/')[1] || 'en';
const locale = resolveLocaleFromPathname(req.nextUrl.pathname);

const authResponse = authMiddleware(req);
if (authResponse) return authResponse;
Expand Down