{"_rev":"15-c871428c11fa4f14e4fa3916877e8e3f","time":{"created":"2025-12-22T19:11:23.301Z","modified":"2025-12-22T19:11:23.891Z","0.1.0":"2025-12-04T02:57:11.589Z","0.1.1":"2025-12-04T04:08:53.021Z","0.2.0":"2025-12-04T05:05:13.544Z","0.3.1":"2025-12-04T17:26:47.099Z","0.3.2":"2025-12-04T22:09:05.150Z","0.3.3":"2025-12-04T23:10:19.899Z","0.3.4":"2025-12-04T23:45:54.482Z","0.3.5":"2025-12-05T01:52:05.004Z","0.3.6":"2025-12-05T02:03:38.359Z","0.3.8":"2025-12-05T02:10:07.034Z","0.4.0":"2025-12-05T03:12:31.925Z","0.4.1":"2025-12-05T03:28:45.391Z","0.4.2":"2025-12-05T03:37:04.742Z","0.5.0":"2025-12-22T19:11:23.572Z"},"_id":"@artatol-acp/auth-nuxt","name":"@artatol-acp/auth-nuxt","dist-tags":{"latest":"0.5.0"},"versions":{"0.5.0":{"name":"@artatol-acp/auth-nuxt","version":"0.5.0","description":"Nuxt module for Artatol Cloud Platform Authentication with auto-imports, composables, and server middleware","type":"module","main":"./dist/module.mjs","types":"./dist/types.d.mts","exports":{".":{"import":"./dist/module.mjs","types":"./dist/types.d.mts"}},"keywords":["acp","auth","nuxt","nuxt3","authentication","artatol"],"author":{"name":"Artatol"},"license":"MIT","peerDependencies":{"nuxt":"^3.0.0"},"dependencies":{"@artatol-acp/auth-js":"^0.5.0","@nuxt/kit":"^3.0.0","defu":"^6.1.4","jose":"^5.9.6"},"devDependencies":{"@nuxt/module-builder":"^0.8.0","@nuxt/schema":"^3.0.0","typescript":"^5.6.3"},"publishConfig":{"access":"public"},"scripts":{"build":"nuxt-module-build build","dev":"nuxt-module-build build --stub","typecheck":"tsc --noEmit"},"_id":"@artatol-acp/auth-nuxt@0.5.0","_integrity":"sha512-QcG5d0IopE8N1LlBU8QCw3iA2Jjb+K7gODd9IHHTItNvaQ2OPW415vLpm3vzwsJv0RcBJT1yIWn9gCyULCfDkA==","_resolved":"/private/var/folders/9x/wfkj1l0j56z0lywbzrkgrvs80000gn/T/5b7a6c49490b25be98185f0a06645afb/artatol-acp-auth-nuxt-0.5.0.tgz","_from":"file:artatol-acp-auth-nuxt-0.5.0.tgz","_nodeVersion":"20.19.5","_npmVersion":"10.8.2","dist":{"integrity":"sha512-QcG5d0IopE8N1LlBU8QCw3iA2Jjb+K7gODd9IHHTItNvaQ2OPW415vLpm3vzwsJv0RcBJT1yIWn9gCyULCfDkA==","shasum":"d4fb366eff59dbdb37cf9deebdc95eef72b16f65","tarball":"https://registry.npmjs.org/@artatol-acp/auth-nuxt/-/auth-nuxt-0.5.0.tgz","fileCount":31,"unpackedSize":35695,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDDA0FO2bVCVCuCFz7R/iKm6ZaHpAfVHEL0PPTzz4+SWgIhAJK+Np0vhFVAYAwV0m/SYkytM+SGtn4pklxDQmNfslel"}]},"_npmUser":{"name":"charouzek-artatol","email":"martin.charouzek@artatol.com"},"directories":{},"maintainers":[{"name":"charouzek-artatol","email":"martin.charouzek@artatol.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/auth-nuxt_0.5.0_1766430683412_0.6597965184891532"},"_hasShrinkwrap":false}},"maintainers":[{"name":"charouzek-artatol","email":"martin.charouzek@artatol.com"}],"description":"Nuxt module for Artatol Cloud Platform Authentication with auto-imports, composables, and server middleware","keywords":["acp","auth","nuxt","nuxt3","authentication","artatol"],"author":{"name":"Artatol"},"license":"MIT","readme":"# @artatol-acp/auth-nuxt\n\nNuxt module for Artatol Cloud Platform Authentication with auto-imports, composables, server middleware, and automatic token refresh.\n\n## Changelog\n\n### v0.4.2\n\n**Features:**\n- **SSO support for artatol.net**: On `*.artatol.net` subdomains, browser receives `refresh_token` cookie directly from auth server (native SSO). On other domains, SDK extracts it from Set-Cookie header and stores it locally.\n\n### v0.4.1\n\n**Bug Fixes:**\n- **Fixed refresh token extraction**: Auth server returns `refresh_token` only in `Set-Cookie` header, not in JSON body. SDK now correctly extracts it from the response header.\n- **Fixed cookie forwarding**: Refresh and logout endpoints now properly forward the `refresh_token` cookie to the auth server.\n\n### v0.4.0\n\n**Improvements:**\n- Updated to use `@artatol-acp/auth-js` v0.4.0 with improved error handling\n- **Better error messages**: ACPAuthError now always contains proper error message from API\n- **New error helper methods**: `isAuthError()`, `isValidationError()`, `isNetworkError()`\n- `apiKey` is now optional in configuration\n\n## Installation\n\n```bash\nnpm install @artatol-acp/auth-nuxt\n# or\npnpm add @artatol-acp/auth-nuxt\n```\n\n## Prerequisites\n\nBefore using this module, you need to obtain from the ACP AUTH service:\n\n1. **API Key** (required) - Contact your system administrator\n2. **Base URL** (required) - The auth service URL (e.g., `https://sso.artatol.net`)\n3. **JWT Public Key** (optional) - Only needed for local JWT verification with `/api/auth/user`. Download it from:\n   ```bash\n   curl https://sso.artatol.net/public-key\n   ```\n\n   **Note:** Without the public key, you can still use all auth operations (login, register, logout, 2FA, password reset, etc.), but you must use `/api/auth/me` instead of `/api/auth/user` for user verification, which makes an API call to the auth service.\n\n## Setup\n\nAdd the module to your `nuxt.config.ts`:\n\n```typescript\nexport default defineNuxtConfig({\n  modules: ['@artatol-acp/auth-nuxt'],\n  acpAuth: {\n    baseUrl: 'https://sso.artatol.net',\n    apiKey: process.env.ACP_AUTH_API_KEY,\n    jwtPublicKey: `-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----`,\n    enableMiddleware: true,\n    publicPaths: ['/login', '/register', '/forgot-password'],\n    loginPath: '/login',\n  },\n});\n```\n\n## Usage\n\n### Composables\n\n#### `useAuth()`\n\nAccess auth functionality:\n\n```vue\n<script setup>\nconst { login, logout, refresh, register, verifyEmail, resendVerificationEmail, forgotPassword, resetPassword, deleteAccount, client } = useAuth();\n\nconst handleLogin = async () => {\n  const result = await login({\n    email: 'user@example.com',\n    password: 'password123',\n  });\n\n  if ('requiresTwoFactor' in result) {\n    // Handle 2FA\n    console.log(result.tempToken);\n  } else {\n    // Login successful\n    console.log(result.user);\n  }\n};\n</script>\n\n<template>\n  <button @click=\"handleLogin\">Login</button>\n  <button @click=\"logout\">Logout</button>\n</template>\n```\n\n#### `useUser()`\n\nGet current user (fast, local JWT verification). Returns basic user info without 2FA status.\n\n```vue\n<script setup>\nconst { user, isLoading, refresh } = useUser();\n// user: { id: string, email: string } | null\n</script>\n\n<template>\n  <div v-if=\"isLoading\">Loading...</div>\n  <div v-else-if=\"user\">\n    Welcome {{ user.email }}\n  </div>\n  <div v-else>Not logged in</div>\n</template>\n```\n\n#### `useMe()`\n\nGet current user with full data including 2FA status (API call).\n\n```vue\n<script setup>\nconst { user, isLoading, refresh } = useMe();\n// user: { id: string, email: string, twoFactorEnabled: boolean } | null\n</script>\n\n<template>\n  <div v-if=\"isLoading\">Loading...</div>\n  <div v-else-if=\"user\">\n    Welcome {{ user.email }}\n    <span v-if=\"user.twoFactorEnabled\">2FA enabled</span>\n  </div>\n  <div v-else>Not logged in</div>\n</template>\n```\n\n### Server API Routes\n\nThe module automatically adds these API routes:\n\n- `POST /api/auth/login` - Login user\n- `POST /api/auth/logout` - Logout user\n- `POST /api/auth/refresh` - Refresh access token\n- `GET /api/auth/user` - Get current user (verifies locally without API call)\n- `GET /api/auth/me` - Get current user from API (validates token with auth service)\n\n### Middleware\n\nIf `enableMiddleware` is enabled, the module will protect all routes except public paths.\n\nTo disable middleware for a specific route:\n\n```typescript\ndefinePageMeta({\n  middleware: 'guest', // or custom middleware\n});\n```\n\n### Direct Client Access\n\nYou can also use the ACP Auth client directly:\n\n```vue\n<script setup>\nconst { $acpAuth } = useNuxtApp();\n\nconst register = async () => {\n  await $acpAuth.register({\n    email: 'user@example.com',\n    password: 'password123',\n  });\n  // User will receive verification email\n};\n</script>\n```\n\n## Password Requirements\n\nPasswords must meet the following requirements:\n- Minimum 10 characters\n- At least one lowercase letter (a-z)\n- At least one uppercase letter (A-Z)\n- At least one number (0-9)\n\n```vue\n<script setup>\nfunction validatePassword(password: string): string[] {\n  const errors: string[] = [];\n\n  if (password.length < 10) {\n    errors.push('Password must be at least 10 characters');\n  }\n  if (!/[a-z]/.test(password)) {\n    errors.push('Password must contain at least one lowercase letter');\n  }\n  if (!/[A-Z]/.test(password)) {\n    errors.push('Password must contain at least one uppercase letter');\n  }\n  if (!/[0-9]/.test(password)) {\n    errors.push('Password must contain at least one number');\n  }\n\n  return errors;\n}\n</script>\n```\n\n## Email Verification\n\nAfter registration, users must verify their email address before they can log in. The auth service automatically sends a verification email upon registration.\n\n### Verification Flow\n\n1. User registers → receives verification email\n2. User clicks link in email → email is verified\n3. User can now log in\n\n### Using Composables\n\n```vue\n<script setup>\nconst { $acpAuth } = useNuxtApp();\nconst route = useRoute();\n\n// Verify email from token in URL\nconst verifyEmail = async () => {\n  const token = route.query.token as string;\n\n  try {\n    await $acpAuth.verifyEmail({ token });\n    navigateTo('/login?verified=true');\n  } catch (error) {\n    console.error('Verification failed:', error);\n  }\n};\n\n// Resend verification email\nconst resendVerification = async (email: string) => {\n  await $acpAuth.resendVerificationEmail({ email });\n  // Always succeeds to prevent email enumeration\n};\n</script>\n\n<template>\n  <button @click=\"verifyEmail\">Verify Email</button>\n  <button @click=\"resendVerification('user@example.com')\">\n    Resend Verification Email\n  </button>\n</template>\n```\n\n### Handling Unverified Users\n\nWhen an unverified user tries to log in, they will receive an error:\n\n```vue\n<script setup>\nconst { login } = useAuth();\nconst errorMessage = ref('');\n\nconst handleLogin = async (email: string, password: string) => {\n  try {\n    const result = await login({ email, password });\n\n    if ('requiresTwoFactor' in result) {\n      // Handle 2FA\n    } else {\n      navigateTo('/dashboard');\n    }\n  } catch (error: any) {\n    if (error.message?.includes('Email not verified')) {\n      errorMessage.value = 'Please verify your email before logging in';\n    } else {\n      errorMessage.value = 'Login failed';\n    }\n  }\n};\n</script>\n```\n\n### Server API Routes\n\nThe module automatically adds email verification routes:\n\n- `POST /api/auth/verify-email` - Verify user's email address\n- `POST /api/auth/resend-verification` - Resend verification email\n\n## Configuration Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `baseUrl` | string | required | Base URL of ACP AUTH service |\n| `apiKey` | string | required | API key for authenticating your application |\n| `jwtPublicKey` | string | optional | EdDSA public key for JWT verification |\n| `enableMiddleware` | boolean | `true` | Enable auth middleware |\n| `publicPaths` | string[] | `['/login', '/register', '/forgot-password', '/reset-password']` | Paths that don't require auth |\n| `loginPath` | string | `'/login'` | Login page path |\n\n## TypeScript\n\nThe module includes TypeScript types. All composables and the client are fully typed.\n\n## 2FA (Two-Factor Authentication)\n\n### Setup 2FA\n\n```vue\n<script setup>\nconst { $acpAuth } = useNuxtApp();\nconst password = ref('');\nconst qrCodeUrl = ref('');\nconst recoveryCodes = ref([]);\n\nconst setup2FA = async () => {\n  const accessToken = // ... get from state/cookie\n\n  const result = await $acpAuth.setup2FA({ password: password.value }, accessToken);\n  qrCodeUrl.value = result.qrCodeUrl;\n  recoveryCodes.value = result.recoveryCodes;\n};\n</script>\n\n<template>\n  <div>\n    <input v-model=\"password\" type=\"password\" placeholder=\"Your password\" />\n    <button @click=\"setup2FA\">Setup 2FA</button>\n\n    <div v-if=\"qrCodeUrl\">\n      <img :src=\"qrCodeUrl\" alt=\"QR Code\" />\n      <p>Scan this QR code with your authenticator app</p>\n\n      <h3>Recovery Codes</h3>\n      <ul>\n        <li v-for=\"code in recoveryCodes\" :key=\"code\">{{ code }}</li>\n      </ul>\n    </div>\n  </div>\n</template>\n```\n\n### Verify 2FA Setup\n\n```vue\n<script setup>\nconst { $acpAuth } = useNuxtApp();\nconst code = ref('');\n\nconst verify2FA = async () => {\n  const accessToken = // ... get from state/cookie\n\n  await $acpAuth.verify2FA({ code: code.value }, accessToken);\n  // 2FA is now enabled\n  navigateTo('/dashboard');\n};\n</script>\n\n<template>\n  <div>\n    <input v-model=\"code\" placeholder=\"6-digit code\" maxlength=\"6\" />\n    <button @click=\"verify2FA\">Verify & Enable 2FA</button>\n  </div>\n</template>\n```\n\n### Complete 2FA Login Flow\n\n```vue\n<script setup>\nconst { login } = useAuth();\nconst email = ref('');\nconst password = ref('');\nconst requires2FA = ref(false);\nconst tempToken = ref('');\nconst twoFactorCode = ref('');\n\nconst handleLogin = async () => {\n  const result = await login({\n    email: email.value,\n    password: password.value,\n  });\n\n  if ('requiresTwoFactor' in result) {\n    requires2FA.value = true;\n    tempToken.value = result.tempToken;\n  } else {\n    // Login successful\n    navigateTo('/dashboard');\n  }\n};\n\nconst complete2FA = async () => {\n  const { $acpAuth } = useNuxtApp();\n\n  await $acpAuth.verify2FALogin({\n    tempToken: tempToken.value,\n    code: twoFactorCode.value,\n  });\n\n  navigateTo('/dashboard');\n};\n</script>\n\n<template>\n  <div v-if=\"!requires2FA\">\n    <input v-model=\"email\" type=\"email\" placeholder=\"Email\" />\n    <input v-model=\"password\" type=\"password\" placeholder=\"Password\" />\n    <button @click=\"handleLogin\">Login</button>\n  </div>\n\n  <div v-else>\n    <input v-model=\"twoFactorCode\" placeholder=\"6-digit code\" maxlength=\"6\" />\n    <button @click=\"complete2FA\">Verify Code</button>\n  </div>\n</template>\n```\n\n### Disable 2FA\n\n```vue\n<script setup>\nconst { $acpAuth } = useNuxtApp();\nconst password = ref('');\nconst code = ref('');\n\nconst disable2FA = async () => {\n  const accessToken = // ... get from state/cookie\n\n  await $acpAuth.disable2FA({\n    password: password.value,\n    code: code.value,\n  }, accessToken);\n\n  // 2FA is now disabled\n};\n</script>\n\n<template>\n  <div>\n    <input v-model=\"password\" type=\"password\" placeholder=\"Your password\" />\n    <input v-model=\"code\" placeholder=\"Current 2FA code\" maxlength=\"6\" />\n    <button @click=\"disable2FA\">Disable 2FA</button>\n  </div>\n</template>\n```\n\n## Registration\n\n### Register User\n\n```vue\n<script setup>\nconst { register } = useAuth();\nconst email = ref('');\nconst password = ref('');\nconst registered = ref(false);\n\nconst handleRegister = async () => {\n  await register({ email: email.value, password: password.value });\n  // User will receive verification email\n  registered.value = true;\n};\n</script>\n\n<template>\n  <div v-if=\"!registered\">\n    <input v-model=\"email\" type=\"email\" placeholder=\"Email\" />\n    <input v-model=\"password\" type=\"password\" placeholder=\"Password\" />\n    <button @click=\"handleRegister\">Register</button>\n  </div>\n  <div v-else>\n    Check your email to verify your account.\n  </div>\n</template>\n```\n\n## Password Reset\n\n### Forgot Password\n\nRequest a password reset email:\n\n```vue\n<script setup>\nconst { forgotPassword } = useAuth();\nconst email = ref('');\nconst submitted = ref(false);\n\nconst handleForgotPassword = async () => {\n  await forgotPassword(email.value);\n  // Always succeeds to prevent email enumeration\n  submitted.value = true;\n};\n</script>\n\n<template>\n  <div v-if=\"!submitted\">\n    <input v-model=\"email\" type=\"email\" placeholder=\"Email\" />\n    <button @click=\"handleForgotPassword\">Send Reset Link</button>\n  </div>\n  <div v-else>\n    Check your email for a password reset link.\n  </div>\n</template>\n```\n\n### Reset Password\n\nReset password using the token from the email:\n\n```vue\n<script setup>\nconst { resetPassword } = useAuth();\nconst route = useRoute();\nconst newPassword = ref('');\nconst confirmPassword = ref('');\nconst error = ref('');\n\nconst handleResetPassword = async () => {\n  const token = route.query.token as string;\n\n  if (newPassword.value !== confirmPassword.value) {\n    error.value = 'Passwords do not match';\n    return;\n  }\n\n  try {\n    await resetPassword(token, newPassword.value);\n    navigateTo('/login?reset=success');\n  } catch (e) {\n    error.value = 'Invalid or expired reset token';\n  }\n};\n</script>\n\n<template>\n  <div>\n    <div v-if=\"error\" class=\"error\">{{ error }}</div>\n    <input v-model=\"newPassword\" type=\"password\" placeholder=\"New Password\" />\n    <input v-model=\"confirmPassword\" type=\"password\" placeholder=\"Confirm Password\" />\n    <button @click=\"handleResetPassword\">Reset Password</button>\n  </div>\n</template>\n```\n\n## Delete Account\n\nDelete the authenticated user's account:\n\n```vue\n<script setup>\nconst { deleteAccount } = useAuth();\nconst password = ref('');\nconst confirmation = ref('');\nconst error = ref('');\n\nconst handleDeleteAccount = async () => {\n  if (confirmation.value !== 'DELETE') {\n    error.value = 'Please type DELETE to confirm';\n    return;\n  }\n\n  try {\n    const accessToken = // ... get from state/cookie\n    await deleteAccount(password.value, confirmation.value, accessToken);\n    navigateTo('/goodbye');\n  } catch (e) {\n    error.value = 'Failed to delete account. Check your password.';\n  }\n};\n</script>\n\n<template>\n  <div>\n    <div v-if=\"error\" class=\"error\">{{ error }}</div>\n    <input v-model=\"password\" type=\"password\" placeholder=\"Your password\" />\n    <input v-model=\"confirmation\" placeholder=\"Type DELETE to confirm\" />\n    <button @click=\"handleDeleteAccount\">Delete My Account</button>\n  </div>\n</template>\n```\n\n**Note:** The `confirmation` parameter must be the string `\"DELETE\"` to confirm account deletion.\n\n## Health Check\n\nTo check if the auth service is available:\n\n```vue\n<script setup>\nconst { $acpAuth } = useNuxtApp();\nconst healthStatus = ref(null);\n\nonMounted(async () => {\n  const health = await $acpAuth.health();\n  healthStatus.value = health;\n  console.log(health); // { status: 'ok', timestamp: '...' }\n});\n</script>\n\n<template>\n  <div v-if=\"healthStatus\">\n    Auth service: {{ healthStatus.status }}\n  </div>\n</template>\n```\n\n## Automatic Token Refresh\n\nThe Nuxt module automatically refreshes access tokens in the background to maintain seamless user sessions.\n\n### How It Works\n\n1. A client-side plugin runs automatically when your app loads\n2. Every 4 minutes, it calls `/api/auth/refresh` to get a new access token\n3. The refresh happens silently in the background\n4. If refresh fails, the user session is considered expired\n5. Access tokens expire after 5 minutes, so refreshing every 4 minutes ensures tokens never expire during active sessions\n\n### Manual Control\n\nYou can manually control the auto-refresh behavior:\n\n```vue\n<script setup>\nconst { $authRefresh } = useNuxtApp();\n\n// Stop auto-refresh (e.g., when user logs out)\nonUnmounted(() => {\n  $authRefresh.stop();\n});\n\n// Restart auto-refresh (e.g., after login)\nconst handleLogin = async () => {\n  await login({ email: '...', password: '...' });\n  $authRefresh.start();\n};\n</script>\n```\n\n### Manual Refresh\n\nYou can also manually refresh tokens using the `useAuth()` composable:\n\n```vue\n<script setup>\nconst { refresh } = useAuth();\n\n// Manually refresh the token\nconst handleRefresh = async () => {\n  try {\n    await refresh();\n    console.log('Token refreshed successfully');\n  } catch (error) {\n    console.error('Failed to refresh token');\n  }\n};\n</script>\n```\n\n## Error Handling\n\n```vue\n<script setup>\nimport { ACPAuthError } from '@artatol-acp/auth-js';\n\nconst { login } = useAuth();\nconst errorMessage = ref('');\n\nconst handleLogin = async (email: string, password: string) => {\n  try {\n    await login({ email, password });\n    navigateTo('/dashboard');\n  } catch (error) {\n    if (error instanceof ACPAuthError) {\n      console.error('Auth error:', error.message);\n      console.error('Status code:', error.statusCode);\n\n      if (error.statusCode === 401) {\n        errorMessage.value = 'Invalid credentials';\n      } else if (error.statusCode === 403) {\n        errorMessage.value = 'Please verify your email or check if your account is locked';\n      } else if (error.statusCode === 429) {\n        errorMessage.value = 'Too many attempts. Please try again later';\n      } else {\n        errorMessage.value = error.message;\n      }\n    } else {\n      errorMessage.value = 'An unexpected error occurred';\n    }\n  }\n};\n</script>\n\n<template>\n  <div v-if=\"errorMessage\" class=\"error\">\n    {{ errorMessage }}\n  </div>\n</template>\n```\n\n### Common Error Codes\n\n| Status Code | Meaning |\n|-------------|---------|\n| 401 | Unauthorized (invalid credentials or token) |\n| 403 | Forbidden (email not verified, account locked) |\n| 429 | Too Many Requests (rate limited) |\n| 500 | Internal Server Error |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}