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
+
+
+
+
+
+ );
+}
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 (
+
+ );
+ }
+
+ 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