feat: implement user authentication and admin management

- Add user management API endpoints for listing and creating users.
- Implement login and logout functionality with rate limiting.
- Create UI components for login form, logout button, and animated elements.
- Add session management with JWT and secure cookie handling.
- Seed initial admin user if no users exist.
- Introduce utility functions for password hashing and user authentication.
- Set up proxy middleware for route protection and security headers.
- Create a JSON file for user data storage.
- Add links configuration for external resources.
This commit is contained in:
Anders Böttcher
2026-05-12 14:58:11 +02:00
parent 50566f2a22
commit 0d58fafe4e
34 changed files with 4263 additions and 5012 deletions
+162 -24
View File
@@ -1,6 +1,9 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import { LINKS } from "@/lib/links";
import { getSession } from "@/lib/session";
import LogoutButton from "./components/LogoutButton";
import "./globals.css";
const geistSans = Geist({
@@ -19,32 +22,115 @@ export const metadata: Metadata = {
"Simple, reliable Docker container backups. Share and discover container configs and backup snapshots.",
};
export default function RootLayout({
function GitHubIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" className={className ?? "w-4 h-4"} fill="currentColor" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
);
}
function DockerIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" className={className ?? "w-4 h-4"} fill="currentColor" aria-hidden="true">
<path d="M13.983 11.078h2.119a.186.186 0 0 0 .186-.185V9.006a.186.186 0 0 0-.186-.186h-2.119a.185.185 0 0 0-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 0 0 .186-.186V3.574a.186.186 0 0 0-.186-.185h-2.118a.185.185 0 0 0-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 0 0 .186-.186V6.29a.186.186 0 0 0-.186-.185h-2.118a.185.185 0 0 0-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 0 0 .184-.186V6.29a.185.185 0 0 0-.185-.185H8.1a.185.185 0 0 0-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 0 0 .185-.186V6.29a.185.185 0 0 0-.185-.185H5.136a.186.186 0 0 0-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 0 0 .186-.185V9.006a.186.186 0 0 0-.186-.186h-2.118a.185.185 0 0 0-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 0 0 .184-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.185.185 0 0 0-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 0 0 .185-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.186.186 0 0 0-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.186.186 0 0 0 .184-.185V9.006a.185.185 0 0 0-.184-.186h-2.12a.185.185 0 0 0-.185.186v1.887c0 .102.083.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 0 0-.75.748 11.376 11.376 0 0 0 .692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 0 0 3.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z" />
</svg>
);
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const session = await getSession();
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col bg-[#0b0f1a] text-slate-200">
{/* Nav */}
<header className="sticky top-0 z-50 border-b border-slate-800 bg-[#0b0f1a]/90 backdrop-blur">
<body className="min-h-full flex flex-col" style={{ background: "#060810", color: "#e2e8f0" }}>
{/* ── Global ambient background ── */}
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
<div
className="absolute -top-96 -left-64 w-[900px] h-[900px] rounded-full animate-pulse-slow"
style={{ background: "radial-gradient(circle, rgba(6,182,212,0.07) 0%, transparent 65%)" }}
/>
<div
className="absolute top-1/3 -right-64 w-[700px] h-[700px] rounded-full animate-pulse-slow"
style={{ background: "radial-gradient(circle, rgba(139,92,246,0.07) 0%, transparent 65%)", animationDelay: "2.5s" }}
/>
<div
className="absolute -bottom-48 left-1/4 w-[600px] h-[600px] rounded-full animate-pulse-slow"
style={{ background: "radial-gradient(circle, rgba(6,182,212,0.04) 0%, transparent 65%)", animationDelay: "4s" }}
/>
<div className="absolute inset-0 grid-bg" />
</div>
{/* ── Nav ── */}
<header className="sticky top-0 z-50 glass-heavy">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<Link href="/" className="flex items-center gap-2 font-bold text-lg text-white">
<span className="rounded bg-cyan-500 px-1.5 py-0.5 text-xs font-mono text-slate-900">
.backerup
<Link href="/" className="flex items-center gap-2.5 group">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0"
style={{ background: "linear-gradient(135deg, #22d3ee, #0891b2)" }}
>
<DockerIcon className="w-4 h-4 text-slate-900" />
</div>
<span className="text-base font-bold text-white group-hover:text-cyan-300 transition-colors tracking-tight">
Backerup
</span>
<span>Backerup</span>
</Link>
<nav className="flex items-center gap-6 text-sm text-slate-400">
<Link href="/browse" className="hover:text-white transition-colors">Browse</Link>
<Link href="/share" className="hover:text-white transition-colors">Share</Link>
<nav className="flex items-center gap-1 text-sm font-medium">
<Link
href="/browse"
className="nav-link px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all"
>
Browse
</Link>
<Link
href="/share"
className="rounded-lg bg-cyan-500 px-4 py-1.5 text-sm font-semibold text-slate-900 hover:bg-cyan-400 transition-colors"
className="nav-link px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all"
>
Share
</Link>
<a
href={LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="nav-link px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all flex items-center gap-1.5"
>
<GitHubIcon />
<span className="hidden sm:inline">GitHub</span>
</a>
{session?.role === "admin" && (
<Link
href="/admin"
className="nav-link px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all"
>
Admin
</Link>
)}
{session ? (
<div className="ml-2 flex items-center gap-2">
<span className="hidden sm:block text-xs text-slate-500 px-2">
{session.username}
</span>
<LogoutButton />
</div>
) : (
<Link
href="/login"
className="ml-2 px-4 py-2 rounded-lg font-semibold text-sm text-slate-300 border border-white/10 hover:border-white/20 hover:text-white transition-all"
>
Sign in
</Link>
)}
<Link
href="/share"
className="ml-1 px-4 py-2 rounded-lg font-semibold text-slate-900 transition-all hover:brightness-110 active:scale-95"
style={{ background: "linear-gradient(135deg, #22d3ee, #06b6d4)" }}
>
Upload
</Link>
@@ -52,20 +138,72 @@ export default function RootLayout({
</div>
</header>
{/* Page content */}
{/* ── Page content ── */}
<main className="flex-1">{children}</main>
{/* Footer */}
<footer className="border-t border-slate-800 py-8 text-center text-xs text-slate-600">
<p>
Backerup &mdash; open-source Docker backup.{" "}
<a
href="https://github.com"
className="text-slate-500 hover:text-slate-300 transition-colors"
>
GitHub
</a>
</p>
{/* ── Footer ── */}
<footer className="mt-24 border-t border-white/5">
<div className="mx-auto max-w-6xl px-6 py-16">
<div className="grid gap-12 sm:grid-cols-2 lg:grid-cols-4">
{/* Brand */}
<div className="lg:col-span-2">
<div className="flex items-center gap-2.5 mb-5">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center"
style={{ background: "linear-gradient(135deg, #22d3ee, #0891b2)" }}
>
<DockerIcon className="w-4 h-4 text-slate-900" />
</div>
<span className="font-bold text-white text-lg tracking-tight">Backerup</span>
</div>
<p className="text-sm text-slate-500 max-w-xs leading-relaxed">
Open-source Docker container backup tool. Self-hosted, portable, and reliable.
Archive volumes, restore configs, and share setups with your team.
</p>
<div className="mt-6 flex gap-3">
<a
href={LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/3 text-xs text-slate-400 hover:text-white hover:border-white/20 transition-all"
>
<GitHubIcon />
GitHub
</a>
</div>
</div>
{/* Product */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-4">Product</h3>
<ul className="space-y-3 text-sm text-slate-500">
<li><Link href="/browse" className="hover:text-cyan-400 transition-colors">Browse configs</Link></li>
<li><Link href="/share" className="hover:text-cyan-400 transition-colors">Share a backup</Link></li>
<li><a href={LINKS.docs} className="hover:text-cyan-400 transition-colors">Documentation</a></li>
<li><a href={LINKS.githubReleases} className="hover:text-cyan-400 transition-colors">Changelog</a></li>
</ul>
</div>
{/* Community */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-4">Community</h3>
<ul className="space-y-3 text-sm text-slate-500">
<li><a href={LINKS.github} className="hover:text-cyan-400 transition-colors">GitHub</a></li>
<li><a href={LINKS.githubIssues} className="hover:text-cyan-400 transition-colors">Issues</a></li>
<li><a href={LINKS.githubDiscussions} className="hover:text-cyan-400 transition-colors">Discussions</a></li>
<li><a href={LINKS.githubContributing} className="hover:text-cyan-400 transition-colors">Contributing</a></li>
</ul>
</div>
</div>
<div className="mt-12 pt-8 border-t border-white/5 flex flex-wrap items-center justify-between gap-4 text-xs text-slate-600">
<p>© {new Date().getFullYear()} Backerup Open source under the MIT License.</p>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span>All systems operational</span>
</div>
</div>
</div>
</footer>
</body>
</html>