feat: add notes app

This commit is contained in:
2026-10-01 19:39:20 +02:00
parent ccb8dd3d15
commit ddbb6f37b5
35 changed files with 706 additions and 52 deletions
@@ -1,14 +0,0 @@
const FAILED_LOGIN_DELAY_MS = 1000
export default defineEventHandler(async (event) => {
const { password } = await readValidBody(event, loginSchema)
if (!(await passwordMatches(event, password))) {
await new Promise(resolve => setTimeout(resolve, FAILED_LOGIN_DELAY_MS))
throw createError({ statusCode: 401, statusMessage: 'Wrong password' })
}
const session = await diarySession(event)
await session.update({ unlocked: true })
return { unlocked: true }
})
@@ -1,4 +0,0 @@
export default defineEventHandler(async (event) => {
const session = await diarySession(event)
return { unlocked: !!session.data.unlocked }
})
@@ -1,11 +0,0 @@
const PUBLIC_ROUTES = ['/api/diary/session', '/api/diary/login']
export default defineEventHandler(async (event) => {
const path = event.path.split('?')[0]!
if (!path.startsWith('/api/diary/') || PUBLIC_ROUTES.includes(path)) return
const session = await diarySession(event)
if (!session.data.unlocked) {
throw createError({ statusCode: 401, statusMessage: 'The diary is locked' })
}
})
-36
View File
@@ -1,36 +0,0 @@
import type { H3Event } from 'h3'
interface DiarySession {
unlocked?: boolean
}
const SESSION_MAX_AGE_S = 60 * 60 * 24 * 30
export function diarySession(event: H3Event) {
const { sessionSecret } = useRuntimeConfig(event)
if (!sessionSecret || sessionSecret.length < 32) {
throw createError({ statusCode: 500, statusMessage: 'NUXT_SESSION_SECRET must be set (at least 32 characters)' })
}
return useSession<DiarySession>(event, {
name: 'diary',
password: sessionSecret,
maxAge: SESSION_MAX_AGE_S,
cookie: { httpOnly: true, sameSite: 'lax', secure: !import.meta.dev }
})
}
async function sha256(text: string): Promise<Uint8Array> {
return new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)))
}
export async function passwordMatches(event: H3Event, guess: string): Promise<boolean> {
const { diaryPassword } = useRuntimeConfig(event)
if (!diaryPassword) {
throw createError({ statusCode: 500, statusMessage: 'NUXT_DIARY_PASSWORD is not set' })
}
const [a, b] = await Promise.all([sha256(guess), sha256(diaryPassword)])
let difference = 0
for (let i = 0; i < a.length; i++) difference |= a[i]! ^ b[i]!
return difference === 0
}
-10
View File
@@ -1,10 +0,0 @@
import type { H3Event } from 'h3'
import type { z } from 'zod'
export async function readValidBody<Schema extends z.ZodType>(event: H3Event, schema: Schema): Promise<z.output<Schema>> {
const result = schema.safeParse(await readBody(event))
if (!result.success) {
throw createError({ statusCode: 400, statusMessage: result.error.issues[0]?.message ?? 'Invalid request' })
}
return result.data
}