feat: authentication

This commit is contained in:
2026-03-25 16:04:40 +01:00
parent a935d61531
commit 5ca59b205e
11 changed files with 256 additions and 7 deletions

View File

@@ -1 +1,3 @@
DATABASE_URL=sqlite.db DATABASE_URL=sqlite.db
ADMIN_USERNAME=admin
ADMIN_PASSWORD=strong_password

7
app/middleware/auth.ts Normal file
View File

@@ -0,0 +1,7 @@
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession();
if (!loggedIn.value) {
return navigateTo("/auth/sign-in");
}
});

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { z } from "zod";
import type { AuthFormField, FormSubmitEvent } from "@nuxt/ui";
definePageMeta({
middleware: () => {
const { loggedIn } = useUserSession();
if (loggedIn.value) return navigateTo("/");
},
});
const fields: AuthFormField[] = [
{ name: "username", type: "text", label: "Username", required: true },
{ name: "password", type: "password", label: "Password", required: true },
];
const schema = z.object({
username: z.string({ error: "Required" }),
password: z.string({ error: "Required" }),
});
type Schema = z.output<typeof schema>;
const { fetch: fetchSession } = useUserSession();
const error = ref<string | null>(null);
const loading = ref(false);
const onSubmit = async (event: FormSubmitEvent<Schema>) => {
error.value = null;
loading.value = true;
try {
await $fetch("/api/auth/sign-in", { method: "POST", body: event.data });
await fetchSession();
await navigateTo("/");
} catch (e) {
error.value = getApiError(e);
} finally {
loading.value = false;
}
};
</script>
<template>
<div class="flex items-center justify-center min-h-screen">
<UPageCard class="w-full max-w-sm">
<UAuthForm
title="Sign In"
:fields="fields"
:schema="schema"
:loading="loading"
@submit="onSubmit"
>
<template v-if="error" #validation>
<UAlert color="error" icon="i-lucide-circle-alert" :title="error" />
</template>
</UAuthForm>
</UPageCard>
</div>
</template>

View File

@@ -1,12 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { LazyLinkModal } from "#components"; import { LazyLinkModal } from "#components";
import type { InternalApi } from "nitropack/types";
type Link = InternalApi["/api/links"]["get"][number]; definePageMeta({ middleware: "auth" });
const toast = useToast(); const toast = useToast();
const overlay = useOverlay(); const overlay = useOverlay();
const { fetch: fetchSession } = useUserSession();
const signOut = async () => {
await $fetch("/api/auth/sign-out", { method: "POST" });
await fetchSession();
await navigateTo("/auth/sign-in");
};
const { data: links, status, refresh } = useLazyFetch("/api/links", { key: "links", server: false, }); const { data: links, status, refresh } = useLazyFetch("/api/links", { key: "links", server: false, });
const route = useRoute(); const route = useRoute();
@@ -64,6 +71,11 @@ const deleteLink = async (link: Link) => {
]" ]"
class="px-2" class="px-2"
/> />
<template #footer>
<UButton variant="ghost" icon="i-lucide-log-out" block @click="signOut">
Sign out
</UButton>
</template>
</UDashboardSidebar> </UDashboardSidebar>
<UDashboardPanel> <UDashboardPanel>

View File

@@ -1,9 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: [ modules: ['@nuxt/eslint', '@nuxt/ui', 'nuxt-auth-utils'],
'@nuxt/eslint',
'@nuxt/ui'
],
vite: { vite: {
optimizeDeps: { optimizeDeps: {
@@ -19,7 +16,7 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'], css: ['~/assets/css/main.css'],
compatibilityDate: '2025-01-15', compatibilityDate: '2025-01-15',
eslint: { eslint: {
config: { config: {

View File

@@ -20,6 +20,7 @@
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"nuxt-auth-utils": "0.5.29",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },

142
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ importers:
nuxt: nuxt:
specifier: ^4.4.2 specifier: ^4.4.2
version: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0)))(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0))(eslint@10.0.3(jiti@2.6.1))(ioredis@5.10.0)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))(yaml@2.8.2) version: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0)))(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0))(eslint@10.0.3(jiti@2.6.1))(ioredis@5.10.0)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))(yaml@2.8.2)
nuxt-auth-utils:
specifier: 0.5.29
version: 0.5.29(magicast@0.5.2)
tailwindcss: tailwindcss:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1 version: 4.2.1
@@ -54,6 +57,18 @@ importers:
packages: packages:
'@adonisjs/hash@9.1.1':
resolution: {integrity: sha512-ZkRguwjAp4skKvKDdRAfdJ2oqQ0N7p9l3sioyXO1E8o0WcsyDgEpsTQtuVNoIdMiw4sn4gJlmL3nyF4BcK1ZDQ==}
engines: {node: '>=20.6.0'}
peerDependencies:
argon2: ^0.31.2 || ^0.41.0 || ^0.43.0
bcrypt: ^5.1.1 || ^6.0.0
peerDependenciesMeta:
argon2:
optional: true
bcrypt:
optional: true
'@alloc/quick-lru@5.2.0': '@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1485,6 +1500,10 @@ packages:
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -1501,6 +1520,17 @@ packages:
'@poppinss/exception@1.2.3': '@poppinss/exception@1.2.3':
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@poppinss/object-builder@1.1.0':
resolution: {integrity: sha512-FOrOq52l7u8goR5yncX14+k+Ewi5djnrt1JwXeS/FvnwAPOiveFhiczCDuvXdssAwamtrV2hp5Rw9v+n2T7hQg==}
engines: {node: '>=20.6.0'}
'@poppinss/string@1.7.1':
resolution: {integrity: sha512-OrLzv/nGDU6l6dLXIQHe8nbNSWWfuSbpB/TW5nRpZFf49CLuQlIHlSPN9IdSUv2vG+59yGM6LoibsaHn8B8mDw==}
'@poppinss/utils@6.10.1':
resolution: {integrity: sha512-da+MMyeXhBaKtxQiWPfy7+056wk3lVIhioJnXHXkJ2/OHDaZfFcyKHNl1R06sdYO8lIRXcXdoZ6LO2ARmkAREA==}
engines: {node: '>=18.16.0'}
'@remirror/core-constants@3.0.0': '@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -2106,6 +2136,9 @@ packages:
'@types/node@25.5.0': '@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/pluralize@0.0.33':
resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -2687,6 +2720,10 @@ packages:
caniuse-lite@1.0.30001779: caniuse-lite@1.0.30001779:
resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==}
case-anything@3.1.2:
resolution: {integrity: sha512-wljhAjDDIv/hM2FzgJnYQg90AWmZMNtESCjTeLH680qTzdo0nErlCxOmgzgX4ZsZAtIvqHyD87ES8QyriXB+BQ==}
engines: {node: '>=18'}
change-case@5.4.4: change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
@@ -3421,6 +3458,10 @@ packages:
flatted@3.4.1: flatted@3.4.1:
resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==}
flattie@1.1.1:
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
engines: {node: '>=8'}
fontaine@0.8.0: fontaine@0.8.0:
resolution: {integrity: sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg==} resolution: {integrity: sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
@@ -3723,6 +3764,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -4191,6 +4235,23 @@ packages:
nth-check@2.1.1: nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
nuxt-auth-utils@0.5.29:
resolution: {integrity: sha512-aQ9oD8QR51jUCe2BFEsO/G/E0K+XUy8Skjn3hYcNLiqZ9XE4Y3uT/ozBCmAQ+TR3WW6prj8vUR0wQes8W1N0PA==}
peerDependencies:
'@atproto/api': ^0.13.15
'@atproto/oauth-client-node': ^0.2.0
'@simplewebauthn/browser': ^11.0.0
'@simplewebauthn/server': ^11.0.0
peerDependenciesMeta:
'@atproto/api':
optional: true
'@atproto/oauth-client-node':
optional: true
'@simplewebauthn/browser':
optional: true
'@simplewebauthn/server':
optional: true
nuxt@4.4.2: nuxt@4.4.2:
resolution: {integrity: sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==} resolution: {integrity: sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -4209,6 +4270,9 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
oauth4webapi@3.8.5:
resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==}
object-deep-merge@2.0.0: object-deep-merge@2.0.0:
resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==}
@@ -4247,6 +4311,9 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
openid-client@6.8.2:
resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -4760,6 +4827,10 @@ packages:
safe-buffer@5.2.1: safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
sax@1.5.0: sax@1.5.0:
resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
engines: {node: '>=11.0.0'} engines: {node: '>=11.0.0'}
@@ -4771,6 +4842,9 @@ packages:
scule@1.3.0: scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
secure-json-parse@4.1.0:
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
@@ -4837,6 +4911,10 @@ packages:
resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
slugify@1.6.8:
resolution: {integrity: sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==}
engines: {node: '>=8.0.0'}
smob@1.6.1: smob@1.6.1:
resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==} resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -5533,6 +5611,11 @@ packages:
snapshots: snapshots:
'@adonisjs/hash@9.1.1':
dependencies:
'@phc/format': 1.0.0
'@poppinss/utils': 6.10.1
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@antfu/install-pkg@1.1.0': '@antfu/install-pkg@1.1.0':
@@ -7006,6 +7089,8 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-ia32': 2.5.6
'@parcel/watcher-win32-x64': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6
'@phc/format@1.0.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@@ -7023,6 +7108,24 @@ snapshots:
'@poppinss/exception@1.2.3': {} '@poppinss/exception@1.2.3': {}
'@poppinss/object-builder@1.1.0': {}
'@poppinss/string@1.7.1':
dependencies:
'@types/pluralize': 0.0.33
case-anything: 3.1.2
pluralize: 8.0.0
slugify: 1.6.8
'@poppinss/utils@6.10.1':
dependencies:
'@poppinss/exception': 1.2.3
'@poppinss/object-builder': 1.1.0
'@poppinss/string': 1.7.1
flattie: 1.1.1
safe-stable-stringify: 2.5.0
secure-json-parse: 4.1.0
'@remirror/core-constants@3.0.0': {} '@remirror/core-constants@3.0.0': {}
'@rolldown/pluginutils@1.0.0-rc.2': {} '@rolldown/pluginutils@1.0.0-rc.2': {}
@@ -7539,6 +7642,8 @@ snapshots:
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.18.2
'@types/pluralize@0.0.33': {}
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/web-bluetooth@0.0.20': {} '@types/web-bluetooth@0.0.20': {}
@@ -8151,6 +8256,8 @@ snapshots:
caniuse-lite@1.0.30001779: {} caniuse-lite@1.0.30001779: {}
case-anything@3.1.2: {}
change-case@5.4.4: {} change-case@5.4.4: {}
chokidar@4.0.3: chokidar@4.0.3:
@@ -8859,6 +8966,8 @@ snapshots:
flatted@3.4.1: {} flatted@3.4.1: {}
flattie@1.1.1: {}
fontaine@0.8.0: fontaine@0.8.0:
dependencies: dependencies:
'@capsizecss/unpack': 4.0.0 '@capsizecss/unpack': 4.0.0
@@ -9166,6 +9275,8 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
jose@6.2.2: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-tokens@9.0.1: {} js-tokens@9.0.1: {}
@@ -9649,6 +9760,24 @@ snapshots:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
nuxt-auth-utils@0.5.29(magicast@0.5.2):
dependencies:
'@adonisjs/hash': 9.1.1
'@nuxt/kit': 4.4.2(magicast@0.5.2)
defu: 6.1.4
h3: 1.15.6
hookable: 6.1.0
jose: 6.2.2
ofetch: 1.5.1
openid-client: 6.8.2
pathe: 2.0.3
scule: 1.3.0
uncrypto: 0.1.3
transitivePeerDependencies:
- argon2
- bcrypt
- magicast
nuxt@4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0)))(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0))(eslint@10.0.3(jiti@2.6.1))(ioredis@5.10.0)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))(yaml@2.8.2): nuxt@4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0)))(drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(better-sqlite3@12.8.0))(eslint@10.0.3(jiti@2.6.1))(ioredis@5.10.0)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))(yaml@2.8.2):
dependencies: dependencies:
'@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3) '@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3)
@@ -9785,6 +9914,8 @@ snapshots:
pathe: 2.0.3 pathe: 2.0.3
tinyexec: 1.0.4 tinyexec: 1.0.4
oauth4webapi@3.8.5: {}
object-deep-merge@2.0.0: {} object-deep-merge@2.0.0: {}
obug@2.1.1: {} obug@2.1.1: {}
@@ -9826,6 +9957,11 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
openid-client@6.8.2:
dependencies:
jose: 6.2.2
oauth4webapi: 3.8.5
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@@ -10452,6 +10588,8 @@ snapshots:
safe-buffer@5.2.1: {} safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
sax@1.5.0: {} sax@1.5.0: {}
scslre@0.3.0: scslre@0.3.0:
@@ -10462,6 +10600,8 @@ snapshots:
scule@1.3.0: {} scule@1.3.0: {}
secure-json-parse@4.1.0: {}
semver@6.3.1: {} semver@6.3.1: {}
semver@7.7.4: {} semver@7.7.4: {}
@@ -10539,6 +10679,8 @@ snapshots:
slash@5.1.0: {} slash@5.1.0: {}
slugify@1.6.8: {}
smob@1.6.1: {} smob@1.6.1: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}

View File

@@ -0,0 +1,17 @@
import { z } from "zod";
import { env } from "#server/env";
const bodySchema = z.object({
username: z.string(),
password: z.string(),
});
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, bodySchema.parse);
if (body.username !== env.ADMIN_USERNAME || body.password !== env.ADMIN_PASSWORD) {
throw createError({ statusCode: 401, message: "Invalid credentials" });
}
await setUserSession(event, { user: { username: body.username } });
});

View File

@@ -0,0 +1,3 @@
export default defineEventHandler(async (event) => {
await clearUserSession(event);
});

View File

@@ -3,6 +3,8 @@ import { z } from 'zod'
const schema = z.object({ const schema = z.object({
DATABASE_URL: z.string().min(1), DATABASE_URL: z.string().min(1),
ADMIN_USERNAME: z.string().min(1).default("admin"),
ADMIN_PASSWORD: z.string().min(1),
}) })
export const env = schema.parse(process.env) export const env = schema.parse(process.env)

View File

@@ -0,0 +1,7 @@
export default defineEventHandler(async (event) => {
const path = getRequestURL(event).pathname;
if (path.startsWith("/api/") && !path.startsWith("/api/auth/")) {
await requireUserSession(event);
}
});