From 155e837041ac3654d0f317db748c487e5f3da1c4 Mon Sep 17 00:00:00 2001 From: achmad Date: Fri, 29 May 2026 16:45:23 +0700 Subject: [PATCH] feat: add admin layout with sidebar and login Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/app/admin/layout.tsx | 63 +++++++++++++++++++++++ backend/src/app/admin/login/page.tsx | 42 +++++++++++++++ backend/src/app/api/admin/check/route.ts | 8 +++ backend/src/app/api/admin/login/route.ts | 14 +++++ backend/src/app/api/admin/logout/route.ts | 7 +++ 5 files changed, 134 insertions(+) create mode 100644 backend/src/app/admin/layout.tsx create mode 100644 backend/src/app/admin/login/page.tsx create mode 100644 backend/src/app/api/admin/check/route.ts create mode 100644 backend/src/app/api/admin/login/route.ts create mode 100644 backend/src/app/api/admin/logout/route.ts diff --git a/backend/src/app/admin/layout.tsx b/backend/src/app/admin/layout.tsx new file mode 100644 index 0000000..4697705 --- /dev/null +++ b/backend/src/app/admin/layout.tsx @@ -0,0 +1,63 @@ +'use client'; +import { usePathname, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +const NAV = [ + { href: '/admin', label: 'Dashboard' }, + { href: '/admin/players', label: 'Players' }, + { href: '/admin/battlepass', label: 'Battle Pass' }, + { href: '/admin/matches', label: 'Matches' }, + { href: '/admin/promocodes', label: 'Promo Codes' }, + { href: '/admin/store', label: 'Store' }, + { href: '/admin/contracts', label: 'Contracts' }, + { href: '/admin/arsenal', label: 'Arsenal' }, +]; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + const [authed, setAuthed] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (pathname === '/admin/login') { + setLoading(false); + return; + } + fetch('/api/admin/check') + .then(r => r.json()) + .then(d => { + if (d.authenticated) setAuthed(true); + else router.push('/admin/login'); + }) + .catch(() => router.push('/admin/login')) + .finally(() => setLoading(false)); + }, [pathname, router]); + + if (loading) return
Loading...
; + if (pathname === '/admin/login') return <>{children}; + if (!authed) return null; + + return ( +
+ +
{children}
+
+ ); +} diff --git a/backend/src/app/admin/login/page.tsx b/backend/src/app/admin/login/page.tsx new file mode 100644 index 0000000..c3c3dbc --- /dev/null +++ b/backend/src/app/admin/login/page.tsx @@ -0,0 +1,42 @@ +'use client'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function LoginPage() { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + const res = await fetch('/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + const data = await res.json(); + if (data.success) router.push('/admin'); + else setError(data.error || 'Login failed'); + }; + + return ( +
+
+

Admin Login

+ {error &&

{error}

} + setPassword(e.target.value)} + placeholder="Password" + className="w-full px-3 py-2 bg-gray-700 rounded mb-4 text-white" + autoFocus + /> + +
+
+ ); +} diff --git a/backend/src/app/api/admin/check/route.ts b/backend/src/app/api/admin/check/route.ts new file mode 100644 index 0000000..2f38224 --- /dev/null +++ b/backend/src/app/api/admin/check/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +export async function GET() { + const store = cookies(); + const authed = store.get('admin_session')?.value === 'authenticated'; + return NextResponse.json({ authenticated: !!authed }); +} diff --git a/backend/src/app/api/admin/login/route.ts b/backend/src/app/api/admin/login/route.ts new file mode 100644 index 0000000..7429ce6 --- /dev/null +++ b/backend/src/app/api/admin/login/route.ts @@ -0,0 +1,14 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + const { password } = await request.json(); + const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; + if (password !== ADMIN_PASSWORD) { + return NextResponse.json({ success: false, error: 'Invalid password' }, { status: 401 }); + } + const response = NextResponse.json({ success: true }); + response.cookies.set('admin_session', 'authenticated', { + httpOnly: true, secure: false, sameSite: 'lax', path: '/admin', maxAge: 86400, + }); + return response; +} diff --git a/backend/src/app/api/admin/logout/route.ts b/backend/src/app/api/admin/logout/route.ts new file mode 100644 index 0000000..bf49cb2 --- /dev/null +++ b/backend/src/app/api/admin/logout/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + const response = NextResponse.json({ success: true }); + response.cookies.delete('admin_session'); + return response; +}