feat: add notes app
This commit is contained in:
@@ -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' })
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user