The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftNuxt Module

Authentication

The auto-provided auth pages, $cwa.auth state, protecting routes, and building a custom registration flow.

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/:

PathPurpose
/loginEmail + password login form
/forgot-passwordRequest 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>