Skip to content
Open
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
17 changes: 17 additions & 0 deletions app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="admin-layout">
{children}
</div>
)
}
85 changes: 42 additions & 43 deletions app/api/admin/maintenance/route.ts
Original file line number Diff line number Diff line change
@@ -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'}`
Expand All @@ -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)
}
29 changes: 27 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 (
<html lang="en">
<body className={`${onest.variable} font-sans antialiased overflow-x-hidden`}>
Expand All @@ -48,4 +73,4 @@ export default function RootLayout({
</body>
</html>
)
}
}
50 changes: 35 additions & 15 deletions app/maintenance/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-[#E3E3E3] font-mono flex flex-col items-center justify-center p-0 relative overflow-hidden">
{/* Background Grid Pattern - Very Roblox Studio */}
<div className="absolute inset-0 opacity-[0.05] pointer-events-none"
style={{ backgroundImage: `linear-gradient(#000 1px, transparent 1px), linear-gradient(90deg, #000 1px, transparent 1px)`, size: '20px 20px', backgroundSize: '40px 40px' }}
style={{ backgroundImage: `linear-gradient(#000 1px, transparent 1px), linear-gradient(90deg, #000 1px, transparent 1px)`, backgroundSize: '40px 40px' }}
/>

{/* Top Warning Bar */}
Expand Down Expand Up @@ -44,22 +48,38 @@ export default function MaintenancePage() {
<div className="bg-white border-4 border-black p-6 shadow-[8px_8px_0_0_rgba(0,0,0,1)]">
<div className="flex items-center gap-2 border-b-4 border-black pb-2 mb-4">
<Terminal size={20} />
<span className="font-black uppercase">Technical Stats</span>
<span className="font-black uppercase">System Logs</span>
</div>
<ul className="space-y-2 text-sm font-bold uppercase">
<li className="flex justify-between"><span>Status:</span> <span className="text-[#FFB800]">Repairing</span></li>
<li className="flex justify-between"><span>Eta:</span> <span>60-120 MIN</span></li>
<li className="flex justify-between"><span>Build:</span> <span>v2.4.0</span></li>
<ul className="space-y-3 text-sm font-bold uppercase">
<li className="flex justify-between">
<span>Status:</span>
<span className="text-[#FFB800]">Maintenance Mode</span>
</li>
<li className="flex justify-between">
<span>Eta:</span>
<span>{config.estimatedDuration || 'TBD'}</span>
</li>
<li className="flex flex-col gap-2 mt-4 border-t-4 border-black pt-4">
<span className="text-[10px] opacity-50">Transmission from Admin:</span>
<span className="text-sm normal-case leading-tight text-blue-600 bg-blue-50 p-2 border-2 border-dashed border-blue-200">
{config.message}
</span>
</li>
</ul>
</div>

{/* Action Box */}
<div className="bg-[#00A2FF] border-4 border-black p-6 shadow-[8px_8px_0_0_rgba(0,0,0,1)] flex flex-col justify-between">
<p className="font-black text-white uppercase text-sm mb-4 leading-tight">
Need immediate help from the developers?
</p>
<div>
<p className="font-black text-white uppercase text-sm mb-2 leading-tight">
Need help?
</p>
<p className="text-white text-xs font-bold uppercase mb-4 opacity-80">
Our support lines are still active during the repair phase.
</p>
</div>
<a
href="mailto:support@gitmesh.dev"
href={`mailto:${config.contactEmail}`}
className="w-full bg-white border-4 border-black py-3 text-center font-black uppercase text-black hover:bg-yellow-300 transition-colors shadow-[4px_4px_0_0_rgba(0,0,0,1)] active:shadow-none active:translate-x-1 active:translate-y-1 flex items-center justify-center gap-2"
>
Contact Support <ExternalLink size={18} />
Expand All @@ -72,10 +92,10 @@ export default function MaintenancePage() {
<footer className="w-full border-t-4 border-black bg-white p-4 flex flex-col md:flex-row justify-between items-center gap-4 px-10">
<div className="flex items-center gap-4">
<div className="w-4 h-4 bg-[#FF3131] border-2 border-black animate-pulse" />
<span className="font-black text-sm uppercase">Server Node: 01-B</span>
<span className="font-black text-sm uppercase">Secure Node: ACTIVE</span>
</div>
<p className="font-black text-xs uppercase opacity-50">
Property of GitMesh CE // 2025
Property of GitMesh CE // 2026
</p>
</footer>

Expand All @@ -84,4 +104,4 @@ export default function MaintenancePage() {
<div className="absolute bottom-40 right-10 w-16 h-16 bg-[#00E0FF] border-4 border-black rotate-12 hidden md:block" />
</div>
)
}
}
120 changes: 111 additions & 9 deletions lib/maintenance.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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()
}
}
}

/**
* Manually update the cache.
* Useful when admin updates settings to propagate changes immediately (to this instance).
*/
export async function updateMaintenanceCache(config: MaintenanceConfig): Promise<void> {
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<boolean> {
const config = await getMaintenanceConfig()
return config.enabled
}

/**
* Fetch configuration directly from Supabase (Real-time)
*/
export async function getMaintenanceConfig(): Promise<MaintenanceConfig> {
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
}
Loading