npm package: @sp-uvb/vue
Description: Official Vue.js composables for Universal Verification Broker (UVB)
Author: UVB Team
Latest version: 0.1.0

# @sp-uvb/vue

Official Vue.js composables for Universal Verification Broker (UVB). Provides type-safe, reactive authentication for Vue 3, Nuxt 3, and Quasar applications.

## Features

- 🎯 **Type-Safe Composables** - Full TypeScript support with Vue 3
- 🔄 **Reactive State** - Automatic UI updates on auth state changes
- 💾 **Auto-Persistence** - Session tokens stored in localStorage/sessionStorage
- ⏰ **Session Management** - Automatic expiration handling
- 🔐 **WebAuthn Support** - Built-in helpers for biometric authentication
- 🚀 **SSR Compatible** - Works with Nuxt 3 server-side rendering
- 📦 **Zero Configuration** - Sensible defaults, fully customizable

## Installation

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

## Quick Start

### Vue 3

```vue
<script setup lang="ts">
import { useUVB } from '@sp-uvb/vue';

const { session, isAuthenticated, isLoading, user, verify, logout } = useUVB({
  tenantId: 'your-tenant-id',
  uvbUrl: 'http://localhost:8080',
});

const handleLogin = async () => {
  await verify('totp', '123456');
};
</script>

<template>
  <div>
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="isAuthenticated">
      <p>Welcome, {{ user?.userId }}!</p>
      <button @click="logout">Logout</button>
    </div>
    <div v-else>
      <button @click="handleLogin">Login</button>
    </div>
  </div>
</template>
```

### Nuxt 3

Create a plugin to provide UVB globally:

```ts
// plugins/uvb.ts
import { useUVB } from '@sp-uvb/vue';

export default defineNuxtPlugin(() => {
  const uvb = useUVB({
    tenantId: useRuntimeConfig().public.uvbTenantId,
    uvbUrl: useRuntimeConfig().public.uvbUrl,
  });

  return {
    provide: {
      uvb,
    },
  };
});
```

Use in components:

```vue
<script setup lang="ts">
const { $uvb } = useNuxtApp();

const handleLogin = async () => {
  await $uvb.verify('totp', '123456');
};
</script>

<template>
  <div v-if="$uvb.isAuthenticated.value">
    <p>Welcome, {{ $uvb.user.value?.userId }}!</p>
  </div>
</template>
```

### Quasar

Create a boot file:

```ts
// boot/uvb.ts
import { boot } from 'quasar/wrappers';
import { useUVB } from '@sp-uvb/vue';

export default boot(({ app }) => {
  const uvb = useUVB({
    tenantId: process.env.UVB_TENANT_ID || '',
    uvbUrl: process.env.UVB_URL || 'http://localhost:8080',
  });

  app.config.globalProperties.$uvb = uvb;
});
```

## API Reference

### `useUVB(options)`

Main composable for authentication and session management.

#### Options

```typescript
interface UseUVBOptions {
  tenantId: string; // Required: Your UVB tenant ID
  uvbUrl?: string; // Optional: UVB server URL (default: 'http://localhost:8080')
  apiKey?: string; // Optional: API key for server-to-server auth
  storage?:
    | 'localStorage' // Optional: Storage type (default: 'localStorage')
    | 'sessionStorage'
    | 'memory';
  storageKey?: string; // Optional: Storage key name (default: 'uvb_session_token')
  autoValidate?: boolean; // Optional: Auto-validate on mount (default: true)
}
```

#### Returns

```typescript
interface UseUVBReturn {
  // State
  session: Ref<UVBSession | null>;
  isAuthenticated: Readonly<Ref<boolean>>;
  isLoading: Ref<boolean>;
  error: Ref<Error | null>;

  // Computed
  user: Readonly<
    Ref<{
      userId: string;
      tenantId: string;
      sessionId: string;
      factorsVerified: string[];
      expiresAt: Date;
    } | null>
  >;

  // Methods
  validateSession: (token?: string) => Promise<UVBSession | null>;
  verify: (
    factorType: FactorType,
    factorValue?: string,
    metadata?: Record<string, string>
  ) => Promise<any>;
  logout: () => Promise<void>;
  refreshSession: () => Promise<UVBSession | null>;
  hasFactors: (...factors: FactorType[]) => boolean;
}
```

#### Usage Examples

**Basic Authentication**

```vue
<script setup>
import { useUVB } from '@sp-uvb/vue';

const { verify, isLoading, error } = useUVB({
  tenantId: 'my-tenant',
});

const totpCode = ref('');

const handleVerify = async () => {
  try {
    const result = await verify('totp', totpCode.value);
    if (result.success) {
      console.log('Authenticated!');
    }
  } catch (err) {
    console.error('Verification failed:', err);
  }
};
</script>
```

**Check Verified Factors**

```vue
<script setup>
import { useUVB } from '@sp-uvb/vue';

const { hasFactors, user } = useUVB({ tenantId: 'my-tenant' });

// Check if user has verified both TOTP and WebAuthn
const hasStrongAuth = computed(() => hasFactors('totp', 'webauthn'));
</script>

<template>
  <div v-if="hasStrongAuth">Access to sensitive data granted</div>
</template>
```

**Session Refresh**

```vue
<script setup>
import { useUVB } from '@sp-uvb/vue';
import { onMounted } from 'vue';

const { refreshSession, session } = useUVB({ tenantId: 'my-tenant' });

// Refresh session periodically
onMounted(() => {
  setInterval(
    async () => {
      await refreshSession();
    },
    5 * 60 * 1000
  ); // Every 5 minutes
});
</script>
```

### `useUVBFactor(options)`

Composable for factor enrollment and management.

#### Usage

```vue
<script setup>
import { useUVBFactor } from '@sp-uvb/vue'

const {
  enroll,
  verify,
  listFactors,
  removeFactor,
  enrollmentData,
  isLoading
} = useUVBFactor({
  tenantId: 'my-tenant'
})

// Enroll TOTP
const enrollTOTP = async (userId: string) => {
  const result = await enroll(userId, 'totp')
  if (result.success) {
    console.log('QR Code:', enrollmentData.value?.qrCode)
    console.log('Secret:', enrollmentData.value?.secret)
  }
}

// List user's enrolled factors
const getUserFactors = async (userId: string) => {
  const factors = await listFactors(userId)
  console.log('Enrolled factors:', factors)
}

// Remove a factor
const removeUserFactor = async (userId: string, factorId: string) => {
  await removeFactor(userId, factorId)
}
</script>

<template>
  <div v-if="enrollmentData?.qrCode">
    <img :src="enrollmentData.qrCode" alt="TOTP QR Code" />
    <p>Secret: {{ enrollmentData.secret }}</p>
  </div>
</template>
```

### `useWebAuthn()`

Composable for WebAuthn/FIDO2 authentication.

#### Usage

```vue
<script setup>
import { useWebAuthn } from '@sp-uvb/vue'

const {
  isSupported,
  isLoading,
  error,
  register,
  authenticate
} = useWebAuthn()

// Register a new WebAuthn credential
const registerWebAuthn = async (challenge: string, userName: string, userId: string) => {
  if (!isSupported.value) {
    alert('WebAuthn not supported in this browser')
    return
  }

  try {
    const credential = await register(challenge, userName, userId)
    console.log('Registered credential:', credential)
    // Send credential to your backend for verification
  } catch (err) {
    console.error('Registration failed:', err)
  }
}

// Authenticate with WebAuthn
const authenticateWebAuthn = async (challenge: string) => {
  try {
    const assertion = await authenticate(challenge)
    console.log('Authentication assertion:', assertion)
    // Send assertion to your backend for verification
  } catch (err) {
    console.error('Authentication failed:', err)
  }
}
</script>

<template>
  <div>
    <div v-if="!isSupported">WebAuthn is not supported in your browser</div>
    <button
      v-else
      @click="registerWebAuthn(challenge, 'user@example.com', 'user123')"
      :disabled="isLoading"
    >
      Register Biometric
    </button>
  </div>
</template>
```

## Advanced Examples

### Protected Routes (Vue Router)

```ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { useUVB } from '@sp-uvb/vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/dashboard',
      component: () => import('@/views/Dashboard.vue'),
      meta: { requiresAuth: true },
    },
  ],
});

router.beforeEach((to, from, next) => {
  const { isAuthenticated } = useUVB({ tenantId: 'my-tenant' });

  if (to.meta.requiresAuth && !isAuthenticated.value) {
    next('/login');
  } else {
    next();
  }
});

export default router;
```

### Nuxt 3 Middleware

```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { $uvb } = useNuxtApp();

  if (!$uvb.isAuthenticated.value) {
    return navigateTo('/login');
  }
});
```

Use in pages:

```vue
<script setup>
definePageMeta({
  middleware: 'auth',
});
</script>
```

### Quasar Route Guards

```ts
// router/routes.ts
const routes = [
  {
    path: '/dashboard',
    component: () => import('pages/Dashboard.vue'),
    beforeEnter: (to, from, next) => {
      const { isAuthenticated } = useUVB({ tenantId: 'my-tenant' });
      if (!isAuthenticated.value) {
        next('/login');
      } else {
        next();
      }
    },
  },
];
```

### Multi-Factor Authentication Flow

```vue
<script setup>
import { ref, computed } from 'vue';
import { useUVB, useUVBFactor } from '@sp-uvb/vue';

const { session, verify, hasFactors } = useUVB({ tenantId: 'my-tenant' });
const { enroll, enrollmentData } = useUVBFactor({ tenantId: 'my-tenant' });

const step = (ref < 'login') | 'totp-setup' | 'totp-verify' | ('complete' > 'login');
const email = ref('');
const password = ref('');
const totpCode = ref('');
const userId = ref('');

// Step 1: Email login
const handleEmailLogin = async () => {
  const result = await verify('email_otp', email.value);
  if (result.success) {
    userId.value = result.userId;
    step.value = hasFactors('totp') ? 'totp-verify' : 'totp-setup';
  }
};

// Step 2a: Set up TOTP (first time)
const handleTOTPSetup = async () => {
  await enroll(userId.value, 'totp');
  step.value = 'totp-verify';
};

// Step 2b: Verify TOTP
const handleTOTPVerify = async () => {
  const result = await verify('totp', totpCode.value);
  if (result.success) {
    step.value = 'complete';
  }
};
</script>

<template>
  <div>
    <div v-if="step === 'login'">
      <input v-model="email" placeholder="Email" />
      <button @click="handleEmailLogin">Login</button>
    </div>

    <div v-else-if="step === 'totp-setup'">
      <h3>Set up Two-Factor Authentication</h3>
      <img v-if="enrollmentData?.qrCode" :src="enrollmentData.qrCode" />
      <button @click="handleTOTPSetup">Continue</button>
    </div>

    <div v-else-if="step === 'totp-verify'">
      <input v-model="totpCode" placeholder="6-digit code" />
      <button @click="handleTOTPVerify">Verify</button>
    </div>

    <div v-else-if="step === 'complete'">
      <h3>Welcome! You're authenticated.</h3>
    </div>
  </div>
</template>
```

## TypeScript Support

All composables are fully typed. Import types for better DX:

```typescript
import type { UVBSession, FactorType, UseUVBReturn } from '@sp-uvb/vue';

const uvb: UseUVBReturn = useUVB({ tenantId: 'my-tenant' });
const session: Ref<UVBSession | null> = uvb.session;
```

## SSR Considerations

When using with Nuxt or other SSR frameworks:

1. Storage is automatically disabled during SSR (falls back to memory)
2. Auto-validation only runs on client-side
3. Use `onMounted` for client-only auth logic

```vue
<script setup>
import { onMounted } from 'vue';
import { useUVB } from '@sp-uvb/vue';

const { validateSession } = useUVB({
  tenantId: 'my-tenant',
  autoValidate: false, // Disable auto-validate for manual control
});

onMounted(async () => {
  // Only validate on client-side
  await validateSession();
});
</script>
```

## License

MIT