diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..0370726 --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +// Force dynamic rendering for all admin routes to avoid build-time errors +// when calling getServerSession() or other header-dependent functions. +export const dynamic = 'force-dynamic' + +export default function AdminRootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ {children} +
+ ) +} diff --git a/app/api/admin/maintenance/route.ts b/app/api/admin/maintenance/route.ts index 9cd9fa9..2f0cac4 100644 --- a/app/api/admin/maintenance/route.ts +++ b/app/api/admin/maintenance/route.ts @@ -1,53 +1,55 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { writeFileSync, readFileSync } from 'fs' -import { join } from 'path' +import { supabase } from '@/lib/supabase' +import { getMaintenanceConfig, updateMaintenanceCache } from '@/lib/maintenance' +import { validateAdminAccess } from '@/lib/auth' export async function POST(request: NextRequest) { try { - // Check if user is admin (you'll need to implement proper auth check) - const session = await getServerSession() - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + // Check if user is admin using the project's validation utility + const { isValid, error: authError } = await validateAdminAccess() + + if (!isValid) { + return NextResponse.json({ error: 'Unauthorized', details: authError }, { status: 401 }) } - const { enabled, message, duration } = await request.json() - - // Read current .env file - const envPath = join(process.cwd(), '.env') - let envContent = readFileSync(envPath, 'utf8') + const { enabled, message, duration, contactEmail } = await request.json() - // Update maintenance mode setting - const maintenanceModeRegex = /MAINTENANCE_MODE=.*/ - if (maintenanceModeRegex.test(envContent)) { - envContent = envContent.replace(maintenanceModeRegex, `MAINTENANCE_MODE=${enabled}`) - } else { - envContent += `\nMAINTENANCE_MODE=${enabled}` + const updateData: any = { + enabled, + message, + estimated_duration: duration, + updated_at: new Date().toISOString() } - - // Update maintenance message if provided - if (message) { - const messageRegex = /MAINTENANCE_MESSAGE=.*/ - if (messageRegex.test(envContent)) { - envContent = envContent.replace(messageRegex, `MAINTENANCE_MESSAGE=${message}`) - } else { - envContent += `\nMAINTENANCE_MESSAGE=${message}` - } + + if (contactEmail) { + updateData.contact_email = contactEmail } - // Update maintenance duration if provided - if (duration) { - const durationRegex = /MAINTENANCE_DURATION=.*/ - if (durationRegex.test(envContent)) { - envContent = envContent.replace(durationRegex, `MAINTENANCE_DURATION=${duration}`) - } else { - envContent += `\nMAINTENANCE_DURATION=${duration}` - } + // Update Database and return the full updated row + const { data, error } = await supabase + .from('maintenance_config') + .upsert({ + id: 1, + ...updateData + }) + .select() // <--- Request the updated data + .single() + + if (error) { + console.error('Error updating maintenance mode:', error) + return NextResponse.json({ error: 'Failed to update configuration' }, { status: 500 }) } - // Write updated .env file - writeFileSync(envPath, envContent) - + // Update Local Cache using the AUTHORITATIVE data from DB + if (data) { + await updateMaintenanceCache({ + enabled: data.enabled, + message: data.message, + estimatedDuration: data.estimated_duration, + contactEmail: data.contact_email + }) + } + return NextResponse.json({ success: true, message: `Maintenance mode ${enabled ? 'enabled' : 'disabled'}` @@ -59,9 +61,6 @@ export async function POST(request: NextRequest) { } export async function GET() { - return NextResponse.json({ - enabled: process.env.MAINTENANCE_MODE === 'true', - message: process.env.MAINTENANCE_MESSAGE || 'We are currently performing scheduled maintenance.', - duration: process.env.MAINTENANCE_DURATION || '1-2 hours' - }) + const config = await getMaintenanceConfig() + return NextResponse.json(config) } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 832d3f6..546ef25 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,10 @@ import type React from "react" import type { Metadata } from "next" import Script from "next/script" +import { headers } from "next/headers" +import { redirect } from "next/navigation" +import { checkMaintenanceRedirection } from "@/lib/maintenance" +import { validateAdminAccess } from "@/lib/auth" import "./globals.css" import { AuthProvider } from "@/components/providers/auth-provider" @@ -26,11 +30,32 @@ export const metadata: Metadata = { description: "Community edition", } -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + // Global Maintenance Check + const isMaintenance = await checkMaintenanceRedirection() + + if (isMaintenance) { + const headersList = await headers() + const pathname = headersList.get("x-current-path") || "" + + // Allow access to maintenance page, api routes, and static assets + const isPublicAsset = pathname.startsWith("/_next") || pathname.includes(".") + const isAllowedRoute = pathname.startsWith("/maintenance") || pathname.startsWith("/api") || pathname.startsWith("/auth") + + if (!isPublicAsset && !isAllowedRoute) { + // Check if user is admin + const { isValid } = await validateAdminAccess() + + if (!isValid) { + redirect("/maintenance") + } + } + } + return ( @@ -48,4 +73,4 @@ export default function RootLayout({ ) -} +} \ No newline at end of file diff --git a/app/maintenance/page.tsx b/app/maintenance/page.tsx index 32699b4..d67e9ab 100644 --- a/app/maintenance/page.tsx +++ b/app/maintenance/page.tsx @@ -1,14 +1,18 @@ import React from "react" -import { Construction, Terminal, AlertTriangle, ExternalLink } from "lucide-react" +import { Terminal, AlertTriangle, ExternalLink } from "lucide-react" +import { getMaintenanceConfig } from "@/lib/maintenance" -export default function MaintenancePage() { +export const dynamic = "force-dynamic" + +export default async function MaintenancePage() { + const config = await getMaintenanceConfig() const charGif = "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZHBvM3p1eG1zZGthNGs5bHkwa3l4Mjc4ZGVzN2RveTNwamw5eHZ1dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/qIMZVXWJHQI0Qu3Pe9/giphy.gif" return (
{/* Background Grid Pattern - Very Roblox Studio */}
{/* Top Warning Bar */} @@ -44,22 +48,38 @@ export default function MaintenancePage() {
- Technical Stats + System Logs
-
    -
  • Status: Repairing
  • -
  • Eta: 60-120 MIN
  • -
  • Build: v2.4.0
  • +
      +
    • + Status: + Maintenance Mode +
    • +
    • + Eta: + {config.estimatedDuration || 'TBD'} +
    • +
    • + Transmission from Admin: + + {config.message} + +
{/* Action Box */}
-

- Need immediate help from the developers? -

+
+

+ Need help? +

+

+ Our support lines are still active during the repair phase. +

+
Contact Support @@ -72,10 +92,10 @@ export default function MaintenancePage() {
- Server Node: 01-B + Secure Node: ACTIVE

- Property of GitMesh CE // 2025 + Property of GitMesh CE // 2026

@@ -84,4 +104,4 @@ export default function MaintenancePage() {
) -} \ No newline at end of file +} diff --git a/lib/maintenance.ts b/lib/maintenance.ts index f7e8af0..5f94118 100644 --- a/lib/maintenance.ts +++ b/lib/maintenance.ts @@ -1,16 +1,118 @@ +import { supabase } from '@/lib/supabase' +import { promises as fs } from 'fs' +import path from 'path' +import os from 'os' + /** * Utility functions for managing maintenance mode */ -export function isMaintenanceMode(): boolean { - return process.env.MAINTENANCE_MODE === 'true' +export interface MaintenanceConfig { + enabled: boolean + message: string + estimatedDuration: string + contactEmail: string +} + +interface CachedConfig extends MaintenanceConfig { + lastUpdated: number } -export function getMaintenanceConfig() { - return { - enabled: isMaintenanceMode(), - message: process.env.MAINTENANCE_MESSAGE || 'We are currently performing scheduled maintenance.', - estimatedDuration: process.env.MAINTENANCE_DURATION || '1-2 hours', - contactEmail: process.env.FROM_EMAIL || 'support@gitmesh.dev' +const CACHE_FILE = path.join(os.tmpdir(), 'gitmesh_maintenance_cache.json') +const CACHE_TTL = 5 * 60 * 1000 // 5 minutes + +/** + * Checks if the site should redirect to maintenance page. + * Uses a file-based cache to avoid hitting the DB on every request. + */ +export async function checkMaintenanceRedirection(): Promise { + try { + // 1. Try to read from cache + const cacheData = await fs.readFile(CACHE_FILE, 'utf-8').catch(() => null) + + if (cacheData) { + const cached: CachedConfig = JSON.parse(cacheData) + const now = Date.now() + + // If cache is fresh (< 5 mins), use it + if (now - cached.lastUpdated < CACHE_TTL) { + return cached.enabled + } + } + + // 2. Cache is missing or stale, fetch fresh data + const config = await getMaintenanceConfig() + + // 3. Update cache + await updateMaintenanceCache(config) + + return config.enabled + + } catch (error) { + console.error('Error in checkMaintenanceRedirection:', error) + // Fail safe: If caching fails, check DB directly to be safe. + return isMaintenanceMode() } -} \ No newline at end of file +} + +/** + * Manually update the cache. + * Useful when admin updates settings to propagate changes immediately (to this instance). + */ +export async function updateMaintenanceCache(config: MaintenanceConfig): Promise { + try { + const newCache: CachedConfig = { + ...config, + lastUpdated: Date.now() + } + await fs.writeFile(CACHE_FILE, JSON.stringify(newCache)) + } catch (error) { + console.error('Failed to update maintenance cache:', error) + } +} + +/** + * Direct check for real-time status (Admin usage) + */ +export async function isMaintenanceMode(): Promise { + const config = await getMaintenanceConfig() + return config.enabled +} + +/** + * Fetch configuration directly from Supabase (Real-time) + */ +export async function getMaintenanceConfig(): Promise { + const defaults: MaintenanceConfig = { + enabled: false, + message: 'We are currently performing scheduled maintenance.', + estimatedDuration: '1-2 hours', + contactEmail: 'support@gitmesh.dev' + } + + try { + const { data, error } = await supabase + .from('maintenance_config') + .select('*') + .eq('id', 1) + .single() + + if (error) { + console.error('Error fetching maintenance config:', error) + return defaults + } + + if (data) { + return { + enabled: data.enabled, + message: data.message, + estimatedDuration: data.estimated_duration, + contactEmail: data.contact_email + } + } + } catch (error) { + console.error('Unexpected error checking maintenance mode:', error) + } + + return defaults +} diff --git a/middleware.ts b/middleware.ts index 0468b7e..1ffb680 100644 --- a/middleware.ts +++ b/middleware.ts @@ -3,13 +3,15 @@ import { NextResponse } from 'next/server' export default withAuth( function middleware(req) { - // For now, disable maintenance mode check to avoid Edge Runtime issues - // You can implement this via environment variables or API routes instead - const isMaintenancePage = req.nextUrl.pathname === '/maintenance' - const isApiRoute = req.nextUrl.pathname.startsWith('/api') - const isAdminRoute = req.nextUrl.pathname.startsWith('/admin') - - // Add any additional middleware logic here + // Add current path to headers for Server Components to use + const requestHeaders = new Headers(req.headers) + requestHeaders.set('x-current-path', req.nextUrl.pathname) + + return NextResponse.next({ + request: { + headers: requestHeaders, + }, + }) }, { callbacks: { diff --git a/supabase_schema.sql b/supabase_schema.sql index 3bf9b1f..51ce6c1 100644 --- a/supabase_schema.sql +++ b/supabase_schema.sql @@ -55,3 +55,37 @@ CREATE TRIGGER update_content_updated_at BEFORE UPDATE ON public.content FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- 6. Maintenance Configuration +-- Singleton table to store global maintenance mode settings +CREATE TABLE IF NOT EXISTS public.maintenance_config ( + id INT PRIMARY KEY DEFAULT 1 CHECK (id = 1), -- Enforce singleton pattern + enabled BOOLEAN DEFAULT FALSE, + message TEXT DEFAULT 'We are currently performing scheduled maintenance.', + estimated_duration TEXT DEFAULT '1-2 hours', + contact_email TEXT DEFAULT 'support@gitmesh.dev', + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Seed the initial configuration if it doesn't exist +INSERT INTO public.maintenance_config (id, enabled) +VALUES (1, false) +ON CONFLICT (id) DO NOTHING; + +-- Security Policies +ALTER TABLE public.maintenance_config ENABLE ROW LEVEL SECURITY; + +-- Allow everyone (including unauthenticated users) to read maintenance status +CREATE POLICY "anon_read_maintenance" ON public.maintenance_config FOR SELECT TO anon USING (true); + +-- Allow authenticated users (admins) to manage it +CREATE POLICY "auth_manage_maintenance" ON public.maintenance_config FOR ALL TO authenticated USING (true) WITH CHECK (true); + +-- Allow service role full access +CREATE POLICY "service_manage_maintenance" ON public.maintenance_config FOR ALL TO service_role USING (true) WITH CHECK (true); + +-- Trigger to update 'updated_at' automatically +CREATE TRIGGER update_maintenance_updated_at +BEFORE UPDATE ON public.maintenance_config +FOR EACH ROW +EXECUTE FUNCTION update_updated_at_column(); \ No newline at end of file