feat(02-02): frontend auth store, router guard, Login/Register views

- frontend/src/stores/auth.js: useAuthStore with accessToken in memory
  only (never browser storage); login() accepts options.backupCode
- frontend/src/api/client.js: extended with Bearer token injection,
  401 auto-refresh retry, all auth/admin API functions, changePassword
- frontend/src/router/index.js: auth routes added (/login, /register,
  /password-reset, /account, /admin); beforeEach guard redirects
  unauthenticated users to /login with redirect param
- frontend/src/layouts/AuthLayout.vue: centered bare layout for auth pages
- frontend/src/views/auth/LoginView.vue: three-step flow (password, TOTP,
  backup code); "Use a backup code instead" link; UI-SPEC copywriting
- frontend/src/views/auth/RegisterView.vue: registration with
  PasswordStrengthBar; HIBP error display; UI-SPEC copywriting
- frontend/src/components/auth/PasswordStrengthBar.vue: 4-segment bar
- frontend/src/components/ui/AppSpinner.vue: animate-spin SVG spinner
- Stub views: PasswordResetView, NewPasswordView, AccountView, AdminView
- .gitignore: exclude frontend/node_modules, dist, package-lock.json

npm run build exits 0. All acceptance criteria verified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-22 19:45:21 +02:00
parent 1882edfff6
commit 3b7d362600
13 changed files with 1163 additions and 2 deletions
@@ -0,0 +1,68 @@
<template>
<div v-if="password" class="mt-2 space-y-1">
<!-- Strength bar: 4 segments -->
<div class="flex gap-1">
<div
v-for="i in 4"
:key="i"
class="h-1 flex-1 rounded transition-colors duration-200"
:class="i <= strength ? segmentColor : 'bg-gray-200'"
/>
</div>
<!-- Label -->
<div class="flex justify-end">
<span class="text-xs font-semibold" :class="labelColor">{{ label }}</span>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
password: {
type: String,
default: '',
},
})
/**
* Strength score 04:
* +1 if length >= 12
* +1 if contains uppercase
* +1 if contains digit
* +1 if contains special character
* Matches backend AUTH-01 rules.
*/
const strength = computed(() => {
const p = props.password
if (!p) return 0
let score = 0
if (p.length >= 12) score++
if (/[A-Z]/.test(p)) score++
if (/[0-9]/.test(p)) score++
if (/[^A-Za-z0-9]/.test(p)) score++
return score
})
const segmentColor = computed(() => {
if (strength.value === 1) return 'bg-red-500'
if (strength.value === 2) return 'bg-amber-500'
if (strength.value === 3) return 'bg-amber-400'
return 'bg-green-500'
})
const labelColor = computed(() => {
if (strength.value === 1) return 'text-red-500'
if (strength.value === 2) return 'text-amber-500'
if (strength.value === 3) return 'text-amber-400'
return 'text-green-500'
})
const label = computed(() => {
if (strength.value === 1) return 'Too weak'
if (strength.value === 2) return 'Weak'
if (strength.value === 3) return 'Fair'
return 'Strong'
})
</script>
+23
View File
@@ -0,0 +1,23 @@
<template>
<svg
class="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</template>