diff --git a/app/(auth)/login/LoginClient.tsx b/app/(auth)/login/LoginClient.tsx new file mode 100644 index 00000000..9bbfaace --- /dev/null +++ b/app/(auth)/login/LoginClient.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { Alert, Box, Button, Card, CardContent, Stack, Typography } from "@mui/material"; + +export default function LoginClient({ oauthConfigured, startOAuth }: { oauthConfigured: boolean; startOAuth: (formData: FormData) => void }) { + return ( + + + + + + + Caddy Proxy Manager + + + Sign in with your organization's OAuth2 provider to continue. + + + + {oauthConfigured ? ( + + + + ) : ( + + The system administrator needs to configure OAuth2 settings before logins are allowed. If this is a fresh installation, + start with the OAuth setup wizard. + + )} + + + + + ); +} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 81b0045c..3b37162c 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -2,9 +2,10 @@ import { redirect } from "next/navigation"; import { getSession } from "@/src/lib/auth/session"; import { buildAuthorizationUrl } from "@/src/lib/auth/oauth"; import { getOAuthSettings } from "@/src/lib/settings"; +import LoginClient from "./LoginClient"; export default async function LoginPage() { - const session = getSession(); + const session = await getSession(); if (session) { redirect("/"); } @@ -17,69 +18,5 @@ export default async function LoginPage() { redirect(target); } - return ( -
-

Caddy Proxy Manager

-

Sign in with your organization's OAuth2 provider to continue.

- {oauthConfigured ? ( -
- -
- ) : ( -
-

- The system administrator needs to configure OAuth2 settings before logins are allowed. If this is a fresh installation, start - with the{" "} - - OAuth setup wizard - - . -

-
- )} - -
- ); + return ; } diff --git a/app/(dashboard)/DashboardLayoutClient.tsx b/app/(dashboard)/DashboardLayoutClient.tsx new file mode 100644 index 00000000..bb6cd139 --- /dev/null +++ b/app/(dashboard)/DashboardLayoutClient.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { ReactNode } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Box, Button, Divider, List, ListItemButton, ListItemText, Stack, Typography } from "@mui/material"; +import type { UserRecord } from "@/src/lib/auth/session"; + +const NAV_ITEMS = [ + { href: "/", label: "Overview" }, + { href: "/proxy-hosts", label: "Proxy Hosts" }, + { href: "/redirects", label: "Redirects" }, + { href: "/dead-hosts", label: "Dead Hosts" }, + { href: "/streams", label: "Streams" }, + { href: "/access-lists", label: "Access Lists" }, + { href: "/certificates", label: "Certificates" }, + { href: "/settings", label: "Settings" }, + { href: "/audit-log", label: "Audit Log" } +] as const; + +export default function DashboardLayoutClient({ user, children }: { user: UserRecord; children: ReactNode }) { + const pathname = usePathname(); + + return ( + + + + + Caddy Proxy Manager + + + {user.name ?? user.email} + + + + + + + {NAV_ITEMS.map((item) => { + const selected = pathname === item.href; + return ( + + + + ); + })} + + +
+ +
+
+ + + {children} + +
+ ); +} diff --git a/app/(dashboard)/OverviewClient.tsx b/app/(dashboard)/OverviewClient.tsx new file mode 100644 index 00000000..9f0587f3 --- /dev/null +++ b/app/(dashboard)/OverviewClient.tsx @@ -0,0 +1,85 @@ +"use client"; + +import Link from "next/link"; +import { Card, CardActionArea, CardContent, Grid, Paper, Stack, Typography } from "@mui/material"; + +type StatCard = { + label: string; + icon: string; + count: number; + href: string; +}; + +type RecentEvent = { + summary: string; + created_at: string; +}; + +export default function OverviewClient({ + userName, + stats, + recentEvents +}: { + userName: string; + stats: StatCard[]; + recentEvents: RecentEvent[]; +}) { + return ( + + + + Welcome back, {userName} + + + Manage your Caddy reverse proxies, TLS certificates, and services with confidence. + + + + + {stats.map((stat) => ( + + + + + + {stat.icon} + + + {stat.count} + + {stat.label} + + + + + ))} + + + + + Recent Activity + + {recentEvents.length === 0 ? ( + + No activity recorded yet. + + ) : ( + + {recentEvents.map((event, index) => ( + + {event.summary} + + {new Date(event.created_at).toLocaleString()} + + + ))} + + )} + + + ); +} diff --git a/app/(dashboard)/access-lists/AccessListsClient.tsx b/app/(dashboard)/access-lists/AccessListsClient.tsx new file mode 100644 index 00000000..5bf7cf21 --- /dev/null +++ b/app/(dashboard)/access-lists/AccessListsClient.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { + Box, + Button, + Card, + CardContent, + Divider, + IconButton, + List, + ListItem, + ListItemSecondaryAction, + ListItemText, + Stack, + TextField, + Typography +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import type { AccessList } from "@/src/lib/models/access-lists"; +import { + addAccessEntryAction, + createAccessListAction, + deleteAccessEntryAction, + deleteAccessListAction, + updateAccessListAction +} from "./actions"; + +type Props = { + lists: AccessList[]; +}; + +export default function AccessListsClient({ lists }: Props) { + return ( + + + + Access Lists + + Protect proxy hosts with HTTP basic authentication credentials. + + + + {lists.map((list) => ( + + + updateAccessListAction(list.id, formData)} spacing={2}> + + Access List + + + + + + + + + + + + + Accounts + {list.entries.length === 0 ? ( + No credentials configured. + ) : ( + + {list.entries.map((entry) => ( + + + +
+ + + +
+
+
+ ))} +
+ )} +
+ + + + addAccessEntryAction(list.id, formData)} spacing={1.5} direction={{ xs: "column", sm: "row" }}> + + + + +
+
+ ))} +
+ + + + Create access list + + + + + + + + + + + + + + +
+ ); +} diff --git a/app/(dashboard)/access-lists/actions.ts b/app/(dashboard)/access-lists/actions.ts index 57ab8219..3bbf6122 100644 --- a/app/(dashboard)/access-lists/actions.ts +++ b/app/(dashboard)/access-lists/actions.ts @@ -11,7 +11,7 @@ import { } from "@/src/lib/models/access-lists"; export async function createAccessListAction(formData: FormData) { - const { user } = requireUser(); + const { user } = await requireUser(); const rawUsers = String(formData.get("users") ?? ""); const accounts = rawUsers .split("\n") @@ -35,7 +35,7 @@ export async function createAccessListAction(formData: FormData) { } export async function updateAccessListAction(id: number, formData: FormData) { - const { user } = requireUser(); + const { user } = await requireUser(); await updateAccessList( id, { @@ -48,13 +48,13 @@ export async function updateAccessListAction(id: number, formData: FormData) { } export async function deleteAccessListAction(id: number) { - const { user } = requireUser(); + const { user } = await requireUser(); await deleteAccessList(id, user.id); revalidatePath("/access-lists"); } export async function addAccessEntryAction(id: number, formData: FormData) { - const { user } = requireUser(); + const { user } = await requireUser(); await addAccessListEntry( id, { @@ -67,7 +67,7 @@ export async function addAccessEntryAction(id: number, formData: FormData) { } export async function deleteAccessEntryAction(accessListId: number, entryId: number) { - const { user } = requireUser(); + const { user } = await requireUser(); await removeAccessListEntry(accessListId, entryId, user.id); revalidatePath("/access-lists"); } diff --git a/app/(dashboard)/access-lists/page.tsx b/app/(dashboard)/access-lists/page.tsx index c8e84054..5af14275 100644 --- a/app/(dashboard)/access-lists/page.tsx +++ b/app/(dashboard)/access-lists/page.tsx @@ -1,207 +1,7 @@ +import AccessListsClient from "./AccessListsClient"; import { listAccessLists } from "@/src/lib/models/access-lists"; -import { addAccessEntryAction, createAccessListAction, deleteAccessEntryAction, deleteAccessListAction, updateAccessListAction } from "./actions"; export default function AccessListsPage() { const lists = listAccessLists(); - - return ( -
-
-

Access Lists

-

Protect proxy hosts with HTTP basic authentication credentials.

-
- -
- {lists.map((list) => ( -
-
updateAccessListAction(list.id, formData)} className="header"> -
- -