first
This commit is contained in:
commit
0738070ce1
287 changed files with 10116 additions and 0 deletions
8
src/routes/(app)/+layout.server.js
Normal file
8
src/routes/(app)/+layout.server.js
Normal 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 };
|
||||
};
|
115
src/routes/(app)/+layout.svelte
Normal file
115
src/routes/(app)/+layout.svelte
Normal 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>
|
2
src/routes/(app)/+page.svelte
Normal file
2
src/routes/(app)/+page.svelte
Normal 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>
|
12
src/routes/(app)/logout/+page.server.js
Normal file
12
src/routes/(app)/logout/+page.server.js
Normal 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");
|
||||
}
|
1
src/routes/(app)/settings/+page.svelte
Normal file
1
src/routes/(app)/settings/+page.svelte
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>Settings Page</h1>
|
6
src/routes/(auth)/+layout.svelte
Normal file
6
src/routes/(auth)/+layout.svelte
Normal file
|
@ -0,0 +1,6 @@
|
|||
<script>
|
||||
import "../../app.css";
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
104
src/routes/(auth)/login/+page.server.js
Normal file
104
src/routes/(auth)/login/+page.server.js
Normal 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;
|
||||
}
|
75
src/routes/(auth)/login/+page.svelte
Normal file
75
src/routes/(auth)/login/+page.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue