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:
@@ -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 0–4:
|
||||
* +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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user