npm package: @sp-uvb/nuxt
Description: Nuxt module for Universal Verification Broker (UVB) authentication
Author: UVB Team
Latest version: 0.1.0

# @sp-uvb/nuxt

Nuxt 3 module for Universal Verification Broker (UVB) authentication. Provides automatic server middleware, composables for client-side access, and utilities for API routes.

## Installation

```bash
npm install @sp-uvb/nuxt
# or
yarn add @sp-uvb/nuxt
# or
pnpm add @sp-uvb/nuxt
```

## Quick Start

### 1. Add Module to `nuxt.config.ts`

```typescript
export default defineNuxtConfig({
  modules: ['@sp-uvb/nuxt'],

  uvb: {
    tenantId: 'my-tenant',
    uvbUrl: 'http://localhost:8080', // optional
    cookieName: 'uvb_session', // optional
    excludePaths: ['/login', '/api/auth'], // optional
    autoAuth: true, // optional, default: true
  },
});
```

### 2. Use in Pages

```vue
<script setup lang="ts">
const { session, isAuthenticated } = useUvbAuth();
</script>

<template>
  <div>
    <div v-if="isAuthenticated">
      <h1>Welcome {{ session?.userId }}</h1>
      <p>Factors: {{ session?.factorsVerified.join(', ') }}</p>
    </div>
    <div v-else>
      <a href="/login">Login</a>
    </div>
  </div>
</template>
```

### 3. Use in API Routes

```typescript
// server/api/profile.ts
export default defineEventHandler((event) => {
  const session = requireUvbSession(event);

  return {
    userId: session.userId,
    factors: session.factorsVerified,
  };
});
```

## API

### Module Options

Configure in `nuxt.config.ts`:

```typescript
export default defineNuxtConfig({
  uvb: {
    // Required: Your UVB tenant ID
    tenantId: string

    // Optional: UVB server URL (default: 'http://localhost:8080')
    uvbUrl?: string

    // Optional: API key for server-to-server auth
    apiKey?: string

    // Optional: Cookie name for session token (default: 'uvb_session')
    cookieName?: string

    // Optional: Paths to exclude from authentication (default: [])
    excludePaths?: string[]

    // Optional: Enable automatic auth on all routes (default: true)
    autoAuth?: boolean
  }
})
```

### Composables

#### `useUvbSession()`

Get the current UVB session.

```typescript
const session = useUvbSession();

// session.value contains:
// {
//   userId: string
//   tenantId: string
//   sessionId: string
//   factorsVerified: string[]
//   expiresAt: Date
// }
```

#### `useUvbAuth()`

Get authentication state and helpers.

```typescript
const {
  session, // Ref<UvbSession | null>
  isAuthenticated, // ComputedRef<boolean>
  hasFactor, // (factor: string) => boolean
  hasAllFactors, // (factors: string[]) => boolean
  hasAnyFactor, // (factors: string[]) => boolean
} = useUvbAuth();
```

### Server Utilities

#### `getUvbSession(event)`

Get session from event context (returns `undefined` if no session).

```typescript
export default defineEventHandler((event) => {
  const session = getUvbSession(event);

  if (!session) {
    return { error: 'Not authenticated' };
  }

  return { userId: session.userId };
});
```

#### `requireUvbSession(event)`

Require session or throw 401 error.

```typescript
export default defineEventHandler((event) => {
  const session = requireUvbSession(event);

  // If we get here, user is authenticated
  return { userId: session.userId };
});
```

#### `requireFactors(event, factors)`

Require specific MFA factors or throw 403 error.

```typescript
export default defineEventHandler((event) => {
  const session = requireFactors(event, ['totp', 'webauthn']);

  // User has both TOTP and WebAuthn verified
  return { message: 'Admin action completed' };
});
```

#### `hasFactor(event, factor)`

Check if user has specific factor.

```typescript
export default defineEventHandler((event) => {
  const hasTotp = hasFactor(event, 'totp');

  return { hasTotp };
});
```

#### `isOwner(event, resourceUserId)`

Check if current user owns a resource.

```typescript
export default defineEventHandler(async (event) => {
  const post = await getPost(event.context.params.id);

  if (!isOwner(event, post.userId)) {
    throw createError({ statusCode: 403, message: 'Not your post' });
  }

  return post;
});
```

#### `requireOwnership(event, resourceUserId)`

Require ownership or throw 403 error.

```typescript
export default defineEventHandler(async (event) => {
  const post = await getPost(event.context.params.id);
  requireOwnership(event, post.userId);

  // Delete post
  await deletePost(post.id);
  return { success: true };
});
```

## Examples

### Basic Authentication Page

```vue
<!-- pages/profile.vue -->
<script setup lang="ts">
const { session, isAuthenticated } = useUvbAuth();
const router = useRouter();

// Redirect to login if not authenticated
onMounted(() => {
  if (!isAuthenticated.value) {
    router.push('/login');
  }
});
</script>

<template>
  <div v-if="isAuthenticated">
    <h1>Profile</h1>
    <p>User ID: {{ session?.userId }}</p>
    <p>Session ID: {{ session?.sessionId }}</p>
    <p>Verified Factors: {{ session?.factorsVerified.join(', ') }}</p>
  </div>
</template>
```

### Conditional UI Based on Factors

```vue
<!-- pages/admin.vue -->
<script setup lang="ts">
const { session, hasAllFactors } = useUvbAuth();

const hasAdminAuth = computed(() => hasAllFactors(['totp', 'webauthn']));
</script>

<template>
  <div>
    <div v-if="hasAdminAuth">
      <h1>Admin Panel</h1>
      <button @click="performAdminAction">Delete All Data</button>
    </div>
    <div v-else>
      <p>Additional authentication required</p>
      <a href="/mfa/setup">Setup MFA</a>
    </div>
  </div>
</template>
```

### Protected API Route

```typescript
// server/api/admin/users.ts
export default defineEventHandler(async (event) => {
  // Require authentication
  const session = requireUvbSession(event);

  // Require strong MFA
  requireFactors(event, ['totp', 'webauthn']);

  // Fetch users
  const users = await fetchUsers();
  return users;
});
```

### API Route with Optional Auth

```typescript
// server/api/posts/[id].ts
export default defineEventHandler(async (event) => {
  const postId = event.context.params.id;
  const post = await getPost(postId);

  // Optional: Show extra data if authenticated
  const session = getUvbSession(event);
  if (session) {
    post.isOwner = post.userId === session.userId;
  }

  return post;
});
```

### Conditional MFA Requirements

```typescript
// server/api/transfer.post.ts
export default defineEventHandler(async (event) => {
  const session = requireUvbSession(event);
  const body = await readBody(event);

  // Require strong auth for large transfers
  if (body.amount > 10000) {
    requireFactors(event, ['totp', 'webauthn']);
  } else if (body.amount > 1000) {
    requireFactors(event, ['totp']);
  }

  // Process transfer
  return { success: true };
});
```

### Resource Ownership Check

```typescript
// server/api/posts/[id].delete.ts
export default defineEventHandler(async (event) => {
  const session = requireUvbSession(event);
  const postId = event.context.params.id;
  const post = await getPost(postId);

  // Ensure user owns the post
  requireOwnership(event, post.userId);

  await deletePost(postId);
  return { success: true };
});
```

### Using Environment Variables

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@sp-uvb/nuxt'],

  uvb: {
    tenantId: process.env.UVB_TENANT_ID!,
    uvbUrl: process.env.UVB_URL || 'http://localhost:8080',
    apiKey: process.env.UVB_API_KEY,
    excludePaths: ['/login', '/register', '/api/auth'],
  },
});
```

### Middleware for Protected Routes

```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isAuthenticated } = useUvbAuth();

  if (!isAuthenticated.value && to.path !== '/login') {
    return navigateTo('/login');
  }
});
```

Then use in pages:

```vue
<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
});
</script>

<template>
  <div>Protected Dashboard</div>
</template>
```

### Server Middleware for Specific Routes

```typescript
// server/middleware/admin-auth.ts
export default defineEventHandler((event) => {
  // Only apply to /api/admin/* routes
  if (!event.path.startsWith('/api/admin')) {
    return;
  }

  const session = requireUvbSession(event);
  requireFactors(event, ['totp', 'webauthn']);
});
```

### Fetch with Session Token

```typescript
// composables/useApi.ts
export function useApi() {
  const config = useRuntimeConfig();

  const fetch = async (url: string, options = {}) => {
    const token = useCookie(config.public.uvb.cookieName || 'uvb_session');

    return $fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token.value}`,
      },
    });
  };

  return { fetch };
}
```

### Logout Function

```typescript
// composables/useAuth.ts
export function useAuth() {
  const config = useRuntimeConfig();
  const session = useUvbSession();

  const logout = async () => {
    // Clear session cookie
    const cookie = useCookie(config.public.uvb.cookieName || 'uvb_session');
    cookie.value = null;

    // Clear local session state
    session.value = null;

    // Redirect to login
    await navigateTo('/login');
  };

  return { logout };
}
```

### Disabling Auto Auth

```typescript
// nuxt.config.ts - Disable automatic authentication
export default defineNuxtConfig({
  uvb: {
    tenantId: 'my-tenant',
    autoAuth: false, // Disable automatic middleware
  },
});
```

Then manually protect routes:

```typescript
// server/api/protected.ts
import { getUvbSession } from '#imports';

export default defineEventHandler((event) => {
  // Manually validate session
  const session = getUvbSession(event);
  if (!session) {
    throw createError({ statusCode: 401, message: 'Unauthorized' });
  }

  return { data: 'protected' };
});
```

## TypeScript

This module includes full TypeScript definitions:

```typescript
import type { UvbSession } from '@sp-uvb/nuxt';

// Session type
interface UvbSession {
  userId: string;
  tenantId: string;
  sessionId: string;
  factorsVerified: string[];
  expiresAt: Date;
}
```

## License

MIT