diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 840bd556fc..bfe9633916 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -201,6 +201,7 @@ services:
<<: *catalog-config
DATABASE_URL: postgres://postgres:postgres@catalog:5432/postgres
PEERDB_FLOW_SERVER_HTTP: http://flow_api:8113
+ PEERDB_PASSWORD: peerdb
volumes:
pgdata:
diff --git a/ui/app/api/login/route.ts b/ui/app/api/login/route.ts
new file mode 100644
index 0000000000..9ae9b961e0
--- /dev/null
+++ b/ui/app/api/login/route.ts
@@ -0,0 +1,13 @@
+import { cookies } from 'next/headers';
+
+export async function POST(request: Request) {
+ const { password } = await request.json();
+ if (process.env.PEERDB_PASSWORD !== password) {
+ return new Response(JSON.stringify({ error: 'wrong password' }));
+ }
+ cookies().set('auth', password, {
+ expires: Date.now() + 24 * 60 * 60 * 1000,
+ secure: process.env.PEERDB_SECURE_COOKIES === 'true',
+ });
+ return new Response('{}');
+}
diff --git a/ui/app/api/logout/route.ts b/ui/app/api/logout/route.ts
new file mode 100644
index 0000000000..5faf5a8c84
--- /dev/null
+++ b/ui/app/api/logout/route.ts
@@ -0,0 +1,6 @@
+import { cookies } from 'next/headers';
+
+export async function POST(req: Request) {
+ cookies().delete('auth');
+ return new Response('');
+}
diff --git a/ui/app/login/page.tsx b/ui/app/login/page.tsx
new file mode 100644
index 0000000000..d158978aab
--- /dev/null
+++ b/ui/app/login/page.tsx
@@ -0,0 +1,61 @@
+'use client';
+import Image from 'next/image';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useState } from 'react';
+
+import { Button } from '@/lib/Button';
+import { Layout, LayoutMain } from '@/lib/Layout';
+import { TextField } from '@/lib/TextField';
+
+export default function Login() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [pass, setPass] = useState('');
+ const [error, setError] = useState(() =>
+ searchParams.has('reject') ? 'Authentication failed, please login' : ''
+ );
+ const login = () => {
+ fetch('/api/login', {
+ method: 'POST',
+ body: JSON.stringify({ password: pass }),
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ if (res.error) setError(res.error);
+ else router.push('/');
+ });
+ };
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+ ) =>
+ setPass(e.target.value)
+ }
+ onKeyPress={(e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ login();
+ }
+ }}
+ />
+
+
+
+ );
+}
diff --git a/ui/app/page.tsx b/ui/app/page.tsx
index 4e84221bb7..d89f981887 100644
--- a/ui/app/page.tsx
+++ b/ui/app/page.tsx
@@ -1,10 +1,11 @@
import SidebarComponent from '@/components/SidebarComponent';
import { Header } from '@/lib/Header';
import { Layout, LayoutMain } from '@/lib/Layout';
+import { cookies } from 'next/headers';
export default function Home() {
return (
- }>
+ }>
diff --git a/ui/components/Logout.tsx b/ui/components/Logout.tsx
new file mode 100644
index 0000000000..21c8e8267a
--- /dev/null
+++ b/ui/components/Logout.tsx
@@ -0,0 +1,16 @@
+'use client';
+import { Button } from '@/lib/Button';
+
+export default function Logout() {
+ return (
+
+ );
+}
diff --git a/ui/components/SidebarComponent.tsx b/ui/components/SidebarComponent.tsx
index c60372da8e..c9afc54ca6 100644
--- a/ui/components/SidebarComponent.tsx
+++ b/ui/components/SidebarComponent.tsx
@@ -1,15 +1,15 @@
'use client';
import useTZStore from '@/app/globalstate/time';
+import Logout from '@/components/Logout';
import { BrandLogo } from '@/lib/BrandLogo';
-import { Button } from '@/lib/Button';
import { Icon } from '@/lib/Icon';
import { Label } from '@/lib/Label';
import { RowWithSelect } from '@/lib/Layout';
import { Sidebar, SidebarItem } from '@/lib/Sidebar';
import Link from 'next/link';
-export default function SidebarComponent() {
+export default function SidebarComponent(props: { logout?: boolean }) {
const timezones = ['UTC', 'Local', 'Relative'];
const setZone = useTZStore((state) => state.setZone);
const zone = useTZStore((state) => state.timezone);
@@ -60,7 +60,7 @@ export default function SidebarComponent() {
/>
-
+ {props.logout && }
>
}
bottomLabel={}
diff --git a/ui/middleware.ts b/ui/middleware.ts
new file mode 100644
index 0000000000..3f616b1a7a
--- /dev/null
+++ b/ui/middleware.ts
@@ -0,0 +1,23 @@
+import type { NextRequest } from 'next/server';
+import { NextResponse } from 'next/server';
+
+export default function middleware(req: NextRequest) {
+ if (
+ req.nextUrl.pathname !== '/login' &&
+ req.nextUrl.pathname !== '/api/login' &&
+ req.nextUrl.pathname !== '/api/logout' &&
+ process.env.PEERDB_PASSWORD &&
+ req.cookies.get('auth')?.value !== process.env.PEERDB_PASSWORD
+ ) {
+ req.cookies.delete('auth');
+ return NextResponse.redirect(new URL('/login?reject', req.url));
+ }
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: [
+ // Match everything other than static assets
+ '/((?!_next/static/|images/|favicon.ico$).*)',
+ ],
+};