Authentication
The CWA Nuxt module wires authentication into your app automatically. The API sets HttpOnly JWT cookies on login; the module reads auth state from those cookies and exposes everything through $cwa.auth.
Pages Provided Automatically
You don't need to create these — the module ships them. Override any by creating the same path in app/pages/:
| Path | Purpose |
|---|---|
/login | Email + password login form |
/forgot-password | Request a password reset email |
/reset-password/[username]/[token] | Set a new password from an email link |
/verify-email/[username]/[token] | Verify email address on registration |
/confirm-new-email/[username]/[newEmail]/[token] | Confirm email address change |
Reading Auth State
const cwa = useCwa()
cwa.auth.signedIn.value // boolean
cwa.auth.isAdmin.value // boolean — ROLE_ADMIN or higher
cwa.auth.user.value // { id, username, emailAddress, roles, ... } or undefined
cwa.auth.roles.value // string[]
cwa.auth.hasRole('ROLE_EDITOR') // boolean
cwa.auth.status.value // 0=SIGNED_OUT, 1=LOADING, 2=SIGNED_IN (CwaAuthStatus enum)
Auth state is only available after client-side hydration. Always wrap auth-dependent UI in <ClientOnly>:
<ClientOnly>
<UserMenu v-if="$cwa.auth.signedIn.value" />
<NuxtLink v-else to="/login">Sign in</NuxtLink>
</ClientOnly>
Signing In Programmatically
const result = await cwa.auth.signIn({ username: 'alice@example.com', password: 'secret' })
if (result instanceof FetchError) {
// result.statusCode === 401 → invalid credentials
error.value = 'Invalid email or password'
} else {
navigateTo('/')
}
Using the Login Composable
For custom login forms, useLogin wraps the full login flow:
import { useLogin } from '#imports'
const { model, submit, loading, error } = useLogin()
// model.username and model.password are reactive
// submit() calls signIn and redirects on success
// error.value is the error message string, or null
<form @submit.prevent="submit">
<input v-model="model.username" type="email" placeholder="Email" />
<input v-model="model.password" type="password" placeholder="Password" />
<p v-if="error" class="text-red-500">{{ error }}</p>
<button :disabled="loading" type="submit">Sign in</button>
</form>
Password Reset
// Step 1: request the reset email
import { useForgotPassword } from '#imports'
const { model, submit, loading, success, error } = useForgotPassword()
// model.username — the user's email or username
// submit() → POST /password/reset/request/{username}
// Step 2: submit the new password (from the link parameters)
import { useResetPassword } from '#imports'
const { model, submit, loading, success, error } = useResetPassword()
// model.username, model.token, model.password, model.passwordConfirm
// submit() → PUT /password/reset/{username}/{token}
Email Verification
// Verify email (from the link)
import { useVerifyEmail } from '#imports'
const { verify, loading, success, error } = useVerifyEmail()
// Call on mount with username and token from route params
onMounted(() => verify(route.params.username, route.params.token))
// Resend the verification email
import { useResendVerifyEmail } from '#imports'
const { resend, loading, success, error } = useResendVerifyEmail()
resend(username)
Protecting Pages
The module provides a auth middleware. Add it to any page that requires a signed-in user:
// app/pages/account.vue
definePageMeta({ middleware: 'auth' })
Unauthenticated users are redirected to /login. After login they are returned to the original page.
For admin-only areas:
definePageMeta({ middleware: 'admin' })
Signing Out
await cwa.auth.signOut()
navigateTo('/login')
User Registration
There is no built-in /register page. Build it yourself:
<script setup lang="ts">
const cwa = useCwa()
const form = reactive({ username: '', emailAddress: '', plainPassword: '' })
const loading = ref(false)
const error = ref<string | null>(null)
async function register() {
loading.value = true
error.value = null
try {
await $fetch(`${cwa.apiUrlBase}/users`, {
method: 'POST',
body: form,
credentials: 'include'
})
navigateTo('/login?registered=1')
} catch (e: any) {
error.value = e?.data?.['hydra:description'] ?? 'Registration failed'
} finally {
loading.value = false
}
}
</script>
After successful registration, the API sends a verification email automatically (if verify_on_register: true in the bundle config).
Customising the Login Page
Create app/pages/login.vue — it takes precedence over the module's built-in page. Use useLogin() inside it to keep the same API wiring:
<!-- app/pages/login.vue -->
<template>
<div class="max-w-sm mx-auto mt-16">
<h1 class="text-2xl font-bold mb-8">Welcome back</h1>
<form @submit.prevent="submit" class="space-y-4">
<input v-model="model.username" type="email" class="input w-full" placeholder="Email" />
<input v-model="model.password" type="password" class="input w-full" placeholder="Password" />
<p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
<button type="submit" :disabled="loading" class="btn-primary w-full">
{{ loading ? 'Signing in...' : 'Sign in' }}
</button>
</form>
<NuxtLink to="/forgot-password" class="text-sm text-gray-500 mt-4 block">
Forgot your password?
</NuxtLink>
</div>
</template>
<script setup lang="ts">
import { useLogin } from '#imports'
definePageMeta({ layout: false }) // or use a minimal layout
const { model, submit, loading, error } = useLogin()
</script>