This commit is contained in:
David Senoner 2025-05-18 13:18:46 +02:00
commit 0738070ce1
287 changed files with 10116 additions and 0 deletions

View file

@ -0,0 +1,8 @@
import { redirect } from "@sveltejs/kit";
export const load = async (event) => {
if (!event.locals.user) {
return redirect(302, "/login");
}
return { user: event.locals.user };
};

View file

@ -0,0 +1,115 @@
<script>
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { ChevronUp, ChevronDown } from "lucide-svelte";
import House from "lucide-svelte/icons/house";
import Inbox from "lucide-svelte/icons/inbox";
import Calendar from "lucide-svelte/icons/calendar";
import Search from "lucide-svelte/icons/search";
import Settings from "lucide-svelte/icons/settings";
import "../../app.css";
let { children, data } = $props();
const items = [
{
title: "Home",
url: "/",
icon: House,
},
{
title: "Inbox",
url: "/",
icon: Inbox,
},
{
title: "Calendar",
url: "/",
icon: Calendar,
},
{
title: "Search",
url: "/",
icon: Search,
},
];
const settings = [
{
title: "Settings",
url: "settings",
icon: Settings,
},
];
</script>
<Sidebar.Provider>
<Sidebar.Root>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Floors</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each items as item (item.title)}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a href={item.url} {...props}>
<item.icon />
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each settings as item (item.title)}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a href={item.url} {...props}>
<item.icon />
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
{...props}
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
{data.user.username}
<ChevronUp class="ml-auto" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content side="top" class="w-[--bits-dropdown-menu-anchor-width]">
<a href="/logout">
<DropdownMenu.Item class="cursor-pointer">Sign out</DropdownMenu.Item>
</a>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu></Sidebar.Footer
>
</Sidebar.Root>
<main>
<Sidebar.Trigger />
{@render children?.()}
</main>
</Sidebar.Provider>

View file

@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View file

@ -0,0 +1,12 @@
import * as auth from "$lib/server/auth.js";
import { fail, redirect } from "@sveltejs/kit";
export async function load(event) {
if (!event.locals.session) {
return fail(401);
}
await auth.invalidateSession(event.locals.session.id);
auth.deleteSessionTokenCookie(event);
redirect(302, "/login");
}

View file

@ -0,0 +1 @@
<h1>Settings Page</h1>

View file

@ -0,0 +1,6 @@
<script>
import "../../app.css";
let { children } = $props();
</script>
{@render children()}

View file

@ -0,0 +1,104 @@
import * as auth from "$lib/server/auth";
import { db } from "$lib/server/db";
import * as table from "$lib/server/db/schema";
import { hash, verify } from "@node-rs/argon2";
import { encodeBase64url } from "@oslojs/encoding";
import { fail, redirect } from "@sveltejs/kit";
import { eq } from "drizzle-orm";
export const load = async (event) => {
if (event.locals.user) {
return redirect(302, "/");
}
return {};
};
export const actions = {
login: async (event) => {
const formData = await event.request.formData();
const username = formData.get("username");
const password = formData.get("password");
if (!validateUsername(username)) {
return fail(400, { message: "Invalid username" });
}
if (!validatePassword(password)) {
return fail(400, { message: "Invalid password" });
}
const results = await db.select().from(table.users).where(eq(table.users.username, username));
const existingUser = results.at(0);
if (!existingUser) {
return fail(400, { message: "Incorrect username or password" });
}
const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
if (!validPassword) {
return fail(400, { message: "Incorrect username or password" });
}
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, existingUser.id);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
return redirect(302, "/");
},
register: async (event) => {
const formData = await event.request.formData();
const username = formData.get("username");
const password = formData.get("password");
if (!validateUsername(username)) {
return fail(400, { message: "Invalid username" });
}
if (!validatePassword(password)) {
return fail(400, { message: "Invalid password" });
}
const userId = generateUserId();
const passwordHash = await hash(password, {
// recommended minimum parameters
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
try {
await db.insert(table.users).values({ id: userId, username, passwordHash });
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, userId);
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
} catch (e) {
return fail(500, { message: "An error has occurred" });
}
return redirect(302, "/");
},
};
function generateUserId() {
// ID with 120 bits of entropy, or about the same as UUID v4.
const bytes = crypto.getRandomValues(new Uint8Array(15));
const id = encodeBase64url(bytes);
return id;
}
function validateUsername(username) {
return (
typeof username === "string" &&
username.length >= 3 &&
username.length <= 31 &&
/^[a-z0-9_-]+$/.test(username)
);
}
function validatePassword(password) {
return typeof password === "string" && password.length >= 6 && password.length <= 255;
}

View file

@ -0,0 +1,75 @@
<script>
import { enhance } from "$app/forms";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
let { form } = $props();
</script>
<div class="form-container">
<h1 class="text-xl">Login/Register</h1>
<form method="post" action="?/login" use:enhance>
<div class="form-group">
<Label for="username">Username</Label>
<Input id="username" name="username" />
</div>
<div class="form-group">
<Label for="password">Password</Label>
<Input id="password" type="password" name="password" />
</div>
<div class="button-group">
<Button type="submit">Login</Button>
<Button type="submit" formaction="?/register" variant="secondary">Register</Button>
</div>
</form>
{#if form?.message}
<p class="error-message">{form.message}</p>
{/if}
</div>
<style>
/* Center the form container */
.form-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
/* Style the form */
form {
width: 100%;
max-width: 400px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
background-color: var(--color-secondary); /* Use secondary color */
}
/* Style form groups */
.form-group {
margin-bottom: 1.5rem;
}
/* Align buttons */
.button-group {
display: flex;
justify-content: space-between;
flex-direction: row-reverse;
}
/* Error message styling */
.error-message {
color: red;
text-align: center;
margin-top: 1rem;
}
/* Heading styling */
h1 {
text-align: center;
margin-bottom: 2rem;
}
</style>