diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx index bed1999..f058e86 100644 --- a/dashboard/src/app/layout.tsx +++ b/dashboard/src/app/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; -import { Sidebar } from '@/components/layout/Sidebar'; import { Providers } from '@/components/Providers'; +import { MainLayout } from '@/components/layout/MainLayout'; const inter = Inter({ subsets: ['latin'] }); @@ -20,12 +20,7 @@ export default function RootLayout({ -
- -
- {children} -
-
+ {children}
diff --git a/dashboard/src/app/login/page.tsx b/dashboard/src/app/login/page.tsx new file mode 100644 index 0000000..194849c --- /dev/null +++ b/dashboard/src/app/login/page.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function LoginPage() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const res = await fetch('/api/v1/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data.error || 'Login failed'); + return; + } + + localStorage.setItem('token', data.token); + router.push('/'); + } catch (err) { + setError('Connection error'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

🐍 OPHION

+

Observability Platform

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded text-white focus:outline-none focus:border-indigo-500" + placeholder="admin@ophion.local" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded text-white focus:outline-none focus:border-indigo-500" + placeholder="••••••••" + required + /> +
+ + +
+
+
+ ); +} diff --git a/dashboard/src/components/AuthGuard.tsx b/dashboard/src/components/AuthGuard.tsx new file mode 100644 index 0000000..06cfd13 --- /dev/null +++ b/dashboard/src/components/AuthGuard.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { isAuthenticated } from '@/lib/auth'; + +export function AuthGuard({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const [checking, setChecking] = useState(true); + + useEffect(() => { + if (pathname === '/login') { + setChecking(false); + return; + } + + if (!isAuthenticated()) { + router.push('/login'); + } else { + setChecking(false); + } + }, [pathname, router]); + + if (checking && pathname !== '/login') { + return ( +
+
Loading...
+
+ ); + } + + return <>{children}; +} diff --git a/dashboard/src/components/layout/MainLayout.tsx b/dashboard/src/components/layout/MainLayout.tsx new file mode 100644 index 0000000..147a1bc --- /dev/null +++ b/dashboard/src/components/layout/MainLayout.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import { Sidebar } from './Sidebar'; +import { AuthGuard } from '../AuthGuard'; + +export function MainLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const isLoginPage = pathname === '/login'; + + if (isLoginPage) { + return <>{children}; + } + + return ( + +
+ +
{children}
+
+
+ ); +} diff --git a/dashboard/src/lib/auth.ts b/dashboard/src/lib/auth.ts new file mode 100644 index 0000000..afe2bfb --- /dev/null +++ b/dashboard/src/lib/auth.ts @@ -0,0 +1,35 @@ +export function getToken(): string | null { + if (typeof window === 'undefined') return null; + return localStorage.getItem('token'); +} + +export function setToken(token: string) { + localStorage.setItem('token', token); +} + +export function removeToken() { + localStorage.removeItem('token'); +} + +export function isAuthenticated(): boolean { + return !!getToken(); +} + +export async function fetchWithAuth(url: string, options: RequestInit = {}) { + const token = getToken(); + const headers = { + ...options.headers, + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + + const res = await fetch(url, { ...options, headers }); + + if (res.status === 401) { + removeToken(); + window.location.href = '/login'; + throw new Error('Unauthorized'); + } + + return res; +} diff --git a/server b/server new file mode 100755 index 0000000..10e750b Binary files /dev/null and b/server differ