This commit is contained in:
Tobias Klemp
2025-11-02 20:40:27 +01:00
commit e7bd070b3e
67 changed files with 3205 additions and 0 deletions

12
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,12 @@
<script lang="ts">
import '../app.css'
import favicon from '$lib/assets/favicon.svg'
let { children } = $props()
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children?.()}

15
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,15 @@
<script lang="ts">
async function crawlClevertronik() {
const response = await fetch('/crawl', { method: 'POST' })
const productName = await response.json()
console.log(productName)
}
</script>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the
documentation
</p>
<button onclick={crawlClevertronik}>lets do it</button>

View File

@@ -0,0 +1,22 @@
import { crawlClevertronik } from '$lib/server/crawler/clevertronik'
import { json, error } from '@sveltejs/kit'
import { Effect } from 'effect'
export async function POST() {
try {
const pageContent = await Effect.runPromise(
crawlClevertronik.pipe(Effect.annotateLogs({ route: 'crawl' })),
)
return json(pageContent)
} catch (e) {
if (
e &&
typeof e === 'object' &&
'message' in e &&
typeof e.message === 'string'
) {
return error(500, { message: e.message })
}
return error(500, { message: 'Internal Error' })
}
}

View File

@@ -0,0 +1,2 @@
<a href="/demo/paraglide">paraglide</a>
<a href="/demo/lucia">lucia</a>

View File

@@ -0,0 +1,31 @@
import * as auth from '$lib/server/auth'
import { fail, redirect } from '@sveltejs/kit'
import { getRequestEvent } from '$app/server'
import type { Actions, PageServerLoad } from './$types'
export const load: PageServerLoad = async () => {
const user = requireLogin()
return { user }
}
export const actions: Actions = {
logout: async (event) => {
if (!event.locals.session) {
return fail(401)
}
await auth.invalidateSession(event.locals.session.id)
auth.deleteSessionTokenCookie(event)
return redirect(302, '/demo/lucia/login')
}
}
function requireLogin() {
const { locals } = getRequestEvent()
if (!locals.user) {
return redirect(302, '/demo/lucia/login')
}
return locals.user
}

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { enhance } from '$app/forms'
import type { PageServerData } from './$types'
let { data }: { data: PageServerData } = $props()
</script>
<h1>Hi, {data.user.username}!</h1>
<p>Your user ID is {data.user.id}.</p>
<form method="post" action="?/logout" use:enhance>
<button>Sign out</button>
</form>

View File

@@ -0,0 +1,107 @@
import { hash, verify } from '@node-rs/argon2'
import { encodeBase32LowerCase } from '@oslojs/encoding'
import { fail, redirect } from '@sveltejs/kit'
import { eq } from 'drizzle-orm'
import * as auth from '$lib/server/auth'
import { db } from '$lib/server/db'
import { user } from '$lib/server/db/user'
import type { Actions, PageServerLoad } from './$types'
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
return redirect(302, '/demo/lucia')
}
return {}
}
export const actions: 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 (min 3, max 31 characters, alphanumeric only)'
})
}
if (!validatePassword(password)) {
return fail(400, { message: 'Invalid password (min 6, max 255 characters)' })
}
const results = await db.select().from(user).where(eq(user.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, '/demo/lucia')
},
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(user).values({ id: userId, username, passwordHash })
const sessionToken = auth.generateSessionToken()
const session = await auth.createSession(sessionToken, userId)
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt)
} catch {
return fail(500, { message: 'An error has occurred' })
}
return redirect(302, '/demo/lucia')
}
}
function generateUserId() {
// ID with 120 bits of entropy, or about the same as UUID v4.
const bytes = crypto.getRandomValues(new Uint8Array(15))
const id = encodeBase32LowerCase(bytes)
return id
}
function validateUsername(username: unknown): username is string {
return (
typeof username === 'string' &&
username.length >= 3 &&
username.length <= 31 &&
/^[a-z0-9_-]+$/.test(username)
)
}
function validatePassword(password: unknown): password is string {
return typeof password === 'string' && password.length >= 6 && password.length <= 255
}

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { enhance } from '$app/forms'
import type { ActionData } from './$types'
let { form }: { form: ActionData } = $props()
</script>
<h1>Login/Register</h1>
<form method="post" action="?/login" use:enhance>
<label>
Username
<input
name="username"
class="mt-1 rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
</label>
<label>
Password
<input
type="password"
name="password"
class="mt-1 rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
/>
</label>
<button class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700"
>Login</button
>
<button
formaction="?/register"
class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700"
>Register</button
>
</form>
<p style="color: red">{form?.message ?? ''}</p>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { setLocale } from '$lib/paraglide/runtime'
import { page } from '$app/state'
import { goto } from '$app/navigation'
import { m } from '$lib/paraglide/messages.js'
</script>
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>
<div>
<button onclick={() => setLocale('en')}>en</button>
<button onclick={() => setLocale('es')}>es</button>
<button onclick={() => setLocale('de-de')}>de-de</button>
</div>
<p>
If you use VSCode, install the <a
href="https://marketplace.visualstudio.com/items?itemName=inlang.vs-code-extension"
target="_blank">Sherlock i18n extension</a
> for a better i18n experience.
</p>