diff --git a/.vscode/launch.json b/.vscode/launch.json index 43020b2..66299b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "type": "chrome", "request": "launch", "name": "client: chrome", - "url": "http://192.168.178.105:3001", + "url": "http://192.168.178.114:3001", "webRoot": "${workspaceFolder}/legalconsenthub" }, { diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt index 0d3bf6a..e6b9246 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt @@ -36,7 +36,7 @@ class SecurityConfig { @Bean fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri("http://192.168.178.105:3001/api/auth/jwks") + return NimbusJwtDecoder.withJwkSetUri("http://192.168.178.114:3001/api/auth/jwks") .jwsAlgorithm(SignatureAlgorithm.ES512).build() } } diff --git a/legalconsenthub/README.md b/legalconsenthub/README.md index f20b1be..8acd148 100644 --- a/legalconsenthub/README.md +++ b/legalconsenthub/README.md @@ -7,8 +7,8 @@ BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET=YOUR_SECRET ``` -2. Generate database schema: `npx @better-auth/cli generate` -3. Migrate schema: `npx @better-auth/cli migrate` +2. Generate database schema: `pnpm dlx @better-auth/cli generate` +3. Migrate schema: `pnpm dlx @better-auth/cli migrate` ## Common errors @@ -31,3 +31,9 @@ rm -fr node_modules; pnpm store prune ``` https://github.com/elizaOS/eliza/pull/665 + +### Unauthorized /token and /organization/list endpoints + +User needs to be logged in to access these endpoints. + +https://www.better-auth.com/docs/plugins/organization#accept-invitation diff --git a/legalconsenthub/better-auth_migrations/2025-03-30T06-50-03.565Z.sql b/legalconsenthub/better-auth_migrations/2025-05-29T08-34-09.998Z.sql similarity index 92% rename from legalconsenthub/better-auth_migrations/2025-03-30T06-50-03.565Z.sql rename to legalconsenthub/better-auth_migrations/2025-05-29T08-34-09.998Z.sql index 2316b7d..7089c34 100644 --- a/legalconsenthub/better-auth_migrations/2025-03-30T06-50-03.565Z.sql +++ b/legalconsenthub/better-auth_migrations/2025-05-29T08-34-09.998Z.sql @@ -6,6 +6,8 @@ create table "account" ("id" text not null primary key, "accountId" text not nul create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date, "updatedAt" date); +create table "jwks" ("id" text not null primary key, "publicKey" text not null, "privateKey" text not null, "createdAt" date not null); + create table "organization" ("id" text not null primary key, "name" text not null, "slug" text not null unique, "logo" text, "createdAt" date not null, "metadata" text); create table "member" ("id" text not null primary key, "organizationId" text not null references "organization" ("id"), "userId" text not null references "user" ("id"), "role" text not null, "createdAt" date not null); diff --git a/legalconsenthub/components/UserMenu.vue b/legalconsenthub/components/UserMenu.vue index 6bfe1c9..39038e2 100644 --- a/legalconsenthub/components/UserMenu.vue +++ b/legalconsenthub/components/UserMenu.vue @@ -178,7 +178,7 @@ const items = computed(() => [ icon: 'i-lucide-log-out', async onSelect(e: Event) { e.preventDefault() - signOut() + await signOut({ redirectTo: '/' }) } } ] diff --git a/legalconsenthub/composables/useAuth.ts b/legalconsenthub/composables/useAuth.ts index 5fa3de9..2884fb4 100644 --- a/legalconsenthub/composables/useAuth.ts +++ b/legalconsenthub/composables/useAuth.ts @@ -1,11 +1,12 @@ // Copied from https://github.com/atinux/nuxthub-better-auth import { defu } from 'defu' -import { createAuthClient } from 'better-auth/client' +import { createAuthClient } from 'better-auth/vue' import type { InferSessionFromClient, InferUserFromClient, ClientOptions } from 'better-auth/client' import { organizationClient, jwtClient } from 'better-auth/client/plugins' import type { RouteLocationRaw } from 'vue-router' import type { UserDto } from '~/.api-client' +import type { RouteLocationNormalizedLoaded } from '#vue-router' interface RuntimeAuthConfig { redirectUserTo: RouteLocationRaw | string @@ -30,6 +31,7 @@ const selectedOrganization = ref<{ export function useAuth() { const url = useRequestURL() + const route = useRoute() const headers = import.meta.server ? useRequestHeaders() : undefined const client = createAuthClient({ @@ -60,18 +62,32 @@ export function useAuth() { user.value = data?.user || null sessionFetching.value = false - // Fetch JWT - workaround for not working extraction of JWT out of session (https://github.com/better-auth/better-auth/issues/1835) - jwt.value = (await client.token()).data?.token ?? null - - // Fetch organization - organizations.value = (await client.organization.list()).data ?? [] - if (!selectedOrganization.value && organizations.value.length > 0) { - selectedOrganization.value = organizations.value[0] + // Only fetch JWT and organizations if we have a session and not on public routes + if (session.value && !isPublicRoute()) { + await fetchJwtAndOrganizations() } return data } + async function fetchJwtAndOrganizations() { + // Fetch JWT + const tokenResult = await client.token() + jwt.value = tokenResult.data?.token ?? null + + // Fetch organization + const orgResult = await client.organization.list({ + fetchOptions: { + headers + } + }) + organizations.value = orgResult.data ?? [] + + if (!selectedOrganization.value && organizations.value.length > 0) { + selectedOrganization.value = organizations.value[0] + } + } + watch( () => selectedOrganization.value, async (newValue) => { @@ -90,6 +106,12 @@ export function useAuth() { }) } + function isPublicRoute(routeToCheck?: RouteLocationNormalizedLoaded) { + const finalRoute = routeToCheck ?? route + const publicRoutes = ['/login', '/signup', '/accept-invitation'] + return publicRoutes.some((path) => finalRoute.path.startsWith(path)) + } + async function signOut({ redirectTo }: { redirectTo?: RouteLocationRaw } = {}) { const res = await client.signOut() if (res.error) { @@ -99,7 +121,7 @@ export function useAuth() { session.value = null user.value = null if (redirectTo) { - await navigateTo(redirectTo) + await navigateTo(redirectTo, { external: true }) } return res } @@ -122,7 +144,9 @@ export function useAuth() { selectedOrganization, options, fetchSession, + fetchJwtAndOrganizations, client, - jwt + jwt, + isPublicRoute } } diff --git a/legalconsenthub/middleware/auth.global.ts b/legalconsenthub/middleware/auth.global.ts index d7ea141..f760ecb 100644 --- a/legalconsenthub/middleware/auth.global.ts +++ b/legalconsenthub/middleware/auth.global.ts @@ -1,6 +1,7 @@ // Copied from https://github.com/atinux/nuxthub-better-auth import { defu } from 'defu' +import type { RouteLocationNormalized } from '#vue-router' type MiddlewareOptions = | false @@ -31,33 +32,44 @@ declare module 'vue-router' { } } -export default defineNuxtRouteMiddleware(async (to) => { - // If auth is disabled, skip middleware +export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) => { + // 1. If auth is disabled, skip middleware if (to.meta?.auth === false) { + console.log('[1] Auth middleware disabled for this route:', to.path) return } - const { loggedIn, options, fetchSession } = useAuth() + const { loggedIn, options, fetchSession, isPublicRoute } = useAuth() const { only, redirectUserTo, redirectGuestTo } = defu(to.meta?.auth, options) - // If guest mode, redirect if authenticated + // 2. If guest mode, redirect if authenticated if (only === 'guest' && loggedIn.value) { - // Avoid infinite redirect + console.log('[2] Guest mode: user is authenticated, redirecting to', redirectUserTo) if (to.path === redirectUserTo) { + console.log('[2.1] Already at redirectUserTo:', redirectUserTo) return } return navigateTo(redirectUserTo) } - // If client-side, fetch session between each navigation + // 3. If client-side, fetch session between each navigation if (import.meta.client) { - await fetchSession() + console.log('[3] Client-side navigation, fetching session') + try { + await fetchSession() + } catch (e) { + console.error(e) + } } - // If not authenticated, redirect to home + + // 4. If not authenticated, redirect to home or guest route if (!loggedIn.value) { - // Avoid infinite redirect - if (to.path === redirectGuestTo) { + if (isPublicRoute(to)) { + console.log('[4] Not authenticated, but route is public:', to.path) + // Continue navigating to the public route return } + // No public route, redirect to guest route + console.log('[4.1] Not authenticated, redirecting to guest route:', redirectGuestTo) return navigateTo(redirectGuestTo) } }) diff --git a/legalconsenthub/package.json b/legalconsenthub/package.json index b8dd421..1df3fd7 100644 --- a/legalconsenthub/package.json +++ b/legalconsenthub/package.json @@ -7,13 +7,14 @@ "dev": "nuxt dev --port 3001 --host", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare && pnpm run fix:bettersqlite", + "postinstall": "nuxt prepare && pnpm run fix:bettersqlite && pnpm run api:generate", "format": "prettier . --write", "type-check": "nuxi typecheck", "lint": "eslint .", "lint:fix": "eslint . --fix", "api:generate": "openapi-generator-cli generate -i ../legalconsenthub-backend/api/legalconsenthub.yml -g typescript-fetch -o .api-client", "fix:bettersqlite": "cd node_modules/better-sqlite3 && pnpm dlx node-gyp rebuild && cd ../..", + "generate:betterauth": "pnpm dlx @better-auth/cli generate --config server/utils/auth.ts", "migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts" }, "dependencies": { @@ -35,5 +36,9 @@ "prettier": "3.5.1", "typescript": "5.7.3", "vue-tsc": "2.2.2" + }, + "volta": { + "node": "22.16.0", + "pnpm": "10.11.0" } } diff --git a/legalconsenthub/plugins/error-handler.ts b/legalconsenthub/plugins/error-handler.ts new file mode 100644 index 0000000..4f24943 --- /dev/null +++ b/legalconsenthub/plugins/error-handler.ts @@ -0,0 +1,9 @@ +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('vue:error', (error, instance, info) => { + console.error('Vue error:', error, 'Instance:', instance, 'Info:', info) + }) + + nuxtApp.hook('app:error', (error) => { + console.error('App error:', error) + }) +}) diff --git a/legalconsenthub/server/utils/auth.ts b/legalconsenthub/server/utils/auth.ts index 791c017..023e88f 100644 --- a/legalconsenthub/server/utils/auth.ts +++ b/legalconsenthub/server/utils/auth.ts @@ -9,7 +9,7 @@ export const auth = betterAuth({ plugins: [ jwt({ jwt: { - issuer: 'http://192.168.178.105:3001', + issuer: 'http://192.168.178.114:3001', expirationTime: '48h' }, jwks: { @@ -22,7 +22,7 @@ export const auth = betterAuth({ organization({ async sendInvitationEmail(data) { console.log('Sending invitation email', data) - const inviteLink = `http://192.168.178.105:3001/accept-invitation/${data.id}` + const inviteLink = `http://192.168.178.114:3001/accept-invitation/${data.id}` await resend.emails.send({ from: 'Acme ', to: data.email,