feat: App lock with biometric/PIN
- LockScreen component with PIN pad - SecuritySettings for setup - Biometric (WebAuthn) support - PIN fallback (6 digits) - Auto-lock after 30s in background - Lock on app start if enabled - Settings page integration
This commit is contained in:
75
src/App.vue
75
src/App.vue
@@ -1,16 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import LockScreen from '@/components/LockScreen.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const isLocked = ref(false)
|
||||
const lastActiveTime = ref(Date.now())
|
||||
const LOCK_TIMEOUT = 30000 // 30 seconds of inactivity
|
||||
|
||||
// Check if lock is enabled
|
||||
const lockEnabled = computed(() => {
|
||||
return localStorage.getItem('lockMethod') &&
|
||||
localStorage.getItem('lockMethod') !== 'none' &&
|
||||
authStore.isAuthenticated
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (authStore.hasStoredToken) {
|
||||
await authStore.fetchCurrentUser()
|
||||
}
|
||||
|
||||
// Check if we should show lock screen on app start
|
||||
if (lockEnabled.value) {
|
||||
const lastUnlock = localStorage.getItem('lastUnlockTime')
|
||||
const now = Date.now()
|
||||
|
||||
// Lock if no previous unlock or more than 30 seconds ago
|
||||
if (!lastUnlock || (now - parseInt(lastUnlock)) > LOCK_TIMEOUT) {
|
||||
isLocked.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for visibility changes (app goes to background/foreground)
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// Track user activity
|
||||
document.addEventListener('touchstart', updateLastActive)
|
||||
document.addEventListener('click', updateLastActive)
|
||||
document.addEventListener('keydown', updateLastActive)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
document.removeEventListener('touchstart', updateLastActive)
|
||||
document.removeEventListener('click', updateLastActive)
|
||||
document.removeEventListener('keydown', updateLastActive)
|
||||
})
|
||||
|
||||
function updateLastActive() {
|
||||
lastActiveTime.value = Date.now()
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
// App went to background - record time
|
||||
localStorage.setItem('backgroundTime', String(Date.now()))
|
||||
} else if (document.visibilityState === 'visible') {
|
||||
// App came back to foreground
|
||||
if (lockEnabled.value) {
|
||||
const backgroundTime = localStorage.getItem('backgroundTime')
|
||||
const now = Date.now()
|
||||
|
||||
// Lock if was in background for more than 30 seconds
|
||||
if (backgroundTime && (now - parseInt(backgroundTime)) > LOCK_TIMEOUT) {
|
||||
isLocked.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnlock() {
|
||||
isLocked.value = false
|
||||
localStorage.setItem('lastUnlockTime', String(Date.now()))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
<!-- Lock Screen Overlay -->
|
||||
<LockScreen
|
||||
v-if="isLocked && lockEnabled"
|
||||
@unlocked="handleUnlock"
|
||||
/>
|
||||
|
||||
<!-- Main App -->
|
||||
<router-view v-show="!isLocked || !lockEnabled" />
|
||||
</template>
|
||||
|
||||
201
src/components/LockScreen.vue
Normal file
201
src/components/LockScreen.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const emit = defineEmits<{
|
||||
unlocked: []
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const lockMethod = ref<'biometric' | 'pin' | null>(null)
|
||||
const pin = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const pinDigits = ref<string[]>(['', '', '', '', '', ''])
|
||||
const currentDigit = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
// Get user's preferred lock method
|
||||
lockMethod.value = localStorage.getItem('lockMethod') as 'biometric' | 'pin' | null
|
||||
|
||||
// If biometric is set, try it automatically
|
||||
if (lockMethod.value === 'biometric') {
|
||||
tryBiometric()
|
||||
}
|
||||
})
|
||||
|
||||
async function tryBiometric() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
// Check if WebAuthn is available
|
||||
if (!window.PublicKeyCredential) {
|
||||
throw new Error('Biometrie nicht verfügbar')
|
||||
}
|
||||
|
||||
// Get stored credential ID
|
||||
const credentialId = localStorage.getItem('biometricCredentialId')
|
||||
if (!credentialId) {
|
||||
throw new Error('Keine biometrischen Daten gespeichert')
|
||||
}
|
||||
|
||||
// Request biometric authentication
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: new Uint8Array(32),
|
||||
timeout: 60000,
|
||||
userVerification: 'required',
|
||||
allowCredentials: [{
|
||||
id: Uint8Array.from(atob(credentialId), c => c.charCodeAt(0)),
|
||||
type: 'public-key',
|
||||
transports: ['internal']
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
if (credential) {
|
||||
emit('unlocked')
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Biometric auth failed:', e)
|
||||
error.value = 'Biometrie fehlgeschlagen. Bitte PIN verwenden.'
|
||||
lockMethod.value = 'pin'
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function addDigit(digit: string) {
|
||||
if (currentDigit.value < 6) {
|
||||
pinDigits.value[currentDigit.value] = digit
|
||||
currentDigit.value++
|
||||
|
||||
if (currentDigit.value === 6) {
|
||||
verifyPin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeDigit() {
|
||||
if (currentDigit.value > 0) {
|
||||
currentDigit.value--
|
||||
pinDigits.value[currentDigit.value] = ''
|
||||
}
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function clearPin() {
|
||||
pinDigits.value = ['', '', '', '', '', '']
|
||||
currentDigit.value = 0
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
async function verifyPin() {
|
||||
const enteredPin = pinDigits.value.join('')
|
||||
const storedPin = localStorage.getItem('appPin')
|
||||
|
||||
if (enteredPin === storedPin) {
|
||||
emit('unlocked')
|
||||
} else {
|
||||
error.value = 'Falscher PIN'
|
||||
// Shake animation + clear after delay
|
||||
setTimeout(clearPin, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function switchToPin() {
|
||||
lockMethod.value = 'pin'
|
||||
error.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-gradient-to-br from-purple-600 to-indigo-700 flex flex-col items-center justify-center z-50">
|
||||
<!-- Logo -->
|
||||
<div class="text-6xl mb-4">🔐</div>
|
||||
<h1 class="text-2xl font-bold text-white mb-2">SeCu</h1>
|
||||
<p class="text-white/70 mb-8">Bitte entsperren</p>
|
||||
|
||||
<!-- Biometric -->
|
||||
<div v-if="lockMethod === 'biometric' && !error" class="text-center">
|
||||
<button
|
||||
@click="tryBiometric"
|
||||
:disabled="loading"
|
||||
class="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center text-4xl mb-4 hover:bg-white/30 transition"
|
||||
>
|
||||
<span v-if="loading" class="animate-spin">⏳</span>
|
||||
<span v-else>👆</span>
|
||||
</button>
|
||||
<p class="text-white/70 text-sm">Fingerabdruck oder Face ID</p>
|
||||
<button
|
||||
@click="switchToPin"
|
||||
class="mt-4 text-white/50 text-sm underline"
|
||||
>
|
||||
Stattdessen PIN verwenden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PIN Entry -->
|
||||
<div v-else class="text-center">
|
||||
<!-- PIN Dots -->
|
||||
<div class="flex space-x-3 mb-8">
|
||||
<div
|
||||
v-for="(digit, i) in pinDigits"
|
||||
:key="i"
|
||||
:class="[
|
||||
'w-4 h-4 rounded-full transition-all',
|
||||
digit ? 'bg-white scale-110' : 'bg-white/30',
|
||||
error ? 'animate-shake bg-red-400' : ''
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<p v-if="error" class="text-red-300 text-sm mb-4">{{ error }}</p>
|
||||
|
||||
<!-- Number Pad -->
|
||||
<div class="grid grid-cols-3 gap-4 max-w-xs">
|
||||
<button
|
||||
v-for="n in [1,2,3,4,5,6,7,8,9]"
|
||||
:key="n"
|
||||
@click="addDigit(String(n))"
|
||||
class="w-16 h-16 bg-white/20 rounded-full text-white text-2xl font-semibold hover:bg-white/30 transition"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
<button
|
||||
v-if="lockMethod === 'biometric' || localStorage.getItem('biometricCredentialId')"
|
||||
@click="tryBiometric"
|
||||
class="w-16 h-16 bg-white/20 rounded-full text-white text-2xl hover:bg-white/30 transition"
|
||||
>
|
||||
👆
|
||||
</button>
|
||||
<div v-else class="w-16 h-16"></div>
|
||||
<button
|
||||
@click="addDigit('0')"
|
||||
class="w-16 h-16 bg-white/20 rounded-full text-white text-2xl font-semibold hover:bg-white/30 transition"
|
||||
>
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
@click="removeDigit"
|
||||
class="w-16 h-16 bg-white/10 rounded-full text-white text-xl hover:bg-white/20 transition"
|
||||
>
|
||||
⌫
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
.animate-shake {
|
||||
animation: shake 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
333
src/components/SecuritySettings.vue
Normal file
333
src/components/SecuritySettings.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const step = ref<'menu' | 'setup-pin' | 'confirm-pin' | 'setup-biometric'>('menu')
|
||||
const currentPin = ref('')
|
||||
const confirmPin = ref('')
|
||||
const error = ref('')
|
||||
const pinDigits = ref<string[]>(['', '', '', '', '', ''])
|
||||
const currentDigit = ref(0)
|
||||
|
||||
const hasBiometric = ref(false)
|
||||
const hasPin = ref(false)
|
||||
const hasBiometricSupport = ref(false)
|
||||
const lockMethod = ref<'biometric' | 'pin' | 'none'>('none')
|
||||
|
||||
onMounted(async () => {
|
||||
// Check what's configured
|
||||
hasPin.value = !!localStorage.getItem('appPin')
|
||||
hasBiometric.value = !!localStorage.getItem('biometricCredentialId')
|
||||
lockMethod.value = localStorage.getItem('lockMethod') as any || 'none'
|
||||
|
||||
// Check if device supports biometric
|
||||
if (window.PublicKeyCredential) {
|
||||
try {
|
||||
hasBiometricSupport.value = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
|
||||
} catch {
|
||||
hasBiometricSupport.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function addDigit(digit: string) {
|
||||
if (currentDigit.value < 6) {
|
||||
pinDigits.value[currentDigit.value] = digit
|
||||
currentDigit.value++
|
||||
|
||||
if (currentDigit.value === 6) {
|
||||
if (step.value === 'setup-pin') {
|
||||
currentPin.value = pinDigits.value.join('')
|
||||
clearPinInput()
|
||||
step.value = 'confirm-pin'
|
||||
} else if (step.value === 'confirm-pin') {
|
||||
confirmPin.value = pinDigits.value.join('')
|
||||
verifyPinMatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeDigit() {
|
||||
if (currentDigit.value > 0) {
|
||||
currentDigit.value--
|
||||
pinDigits.value[currentDigit.value] = ''
|
||||
}
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function clearPinInput() {
|
||||
pinDigits.value = ['', '', '', '', '', '']
|
||||
currentDigit.value = 0
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function verifyPinMatch() {
|
||||
if (currentPin.value === confirmPin.value) {
|
||||
localStorage.setItem('appPin', currentPin.value)
|
||||
localStorage.setItem('lockMethod', 'pin')
|
||||
hasPin.value = true
|
||||
lockMethod.value = 'pin'
|
||||
step.value = 'menu'
|
||||
currentPin.value = ''
|
||||
confirmPin.value = ''
|
||||
clearPinInput()
|
||||
} else {
|
||||
error.value = 'PINs stimmen nicht überein'
|
||||
setTimeout(() => {
|
||||
clearPinInput()
|
||||
step.value = 'setup-pin'
|
||||
currentPin.value = ''
|
||||
confirmPin.value = ''
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBiometric() {
|
||||
step.value = 'setup-biometric'
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
if (!window.PublicKeyCredential) {
|
||||
throw new Error('WebAuthn nicht unterstützt')
|
||||
}
|
||||
|
||||
// Generate a random user ID
|
||||
const userId = new Uint8Array(16)
|
||||
crypto.getRandomValues(userId)
|
||||
|
||||
// Create credential
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: {
|
||||
challenge: new Uint8Array(32),
|
||||
rp: {
|
||||
name: 'SeCu',
|
||||
id: window.location.hostname
|
||||
},
|
||||
user: {
|
||||
id: userId,
|
||||
name: localStorage.getItem('userEmail') || 'user',
|
||||
displayName: 'SeCu User'
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ type: 'public-key', alg: -7 }, // ES256
|
||||
{ type: 'public-key', alg: -257 } // RS256
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: 'platform',
|
||||
userVerification: 'required',
|
||||
residentKey: 'preferred'
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: 'none'
|
||||
}
|
||||
}) as PublicKeyCredential
|
||||
|
||||
if (credential) {
|
||||
// Store credential ID
|
||||
const credentialId = btoa(String.fromCharCode(...new Uint8Array(credential.rawId)))
|
||||
localStorage.setItem('biometricCredentialId', credentialId)
|
||||
localStorage.setItem('lockMethod', 'biometric')
|
||||
hasBiometric.value = true
|
||||
lockMethod.value = 'biometric'
|
||||
step.value = 'menu'
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Biometric setup failed:', e)
|
||||
error.value = e.message || 'Biometrie-Einrichtung fehlgeschlagen'
|
||||
step.value = 'menu'
|
||||
}
|
||||
}
|
||||
|
||||
function disableLock() {
|
||||
if (confirm('App-Sperre wirklich deaktivieren?')) {
|
||||
localStorage.removeItem('appPin')
|
||||
localStorage.removeItem('biometricCredentialId')
|
||||
localStorage.removeItem('lockMethod')
|
||||
hasPin.value = false
|
||||
hasBiometric.value = false
|
||||
lockMethod.value = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
function setLockMethod(method: 'biometric' | 'pin') {
|
||||
localStorage.setItem('lockMethod', method)
|
||||
lockMethod.value = method
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 p-6 text-white">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold">🔐 App-Sicherheit</h2>
|
||||
<button @click="emit('close')" class="text-white/70 hover:text-white">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
<!-- Menu -->
|
||||
<div v-if="step === 'menu'" class="space-y-4">
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Schütze deine App mit Fingerabdruck, Face ID oder PIN.
|
||||
</p>
|
||||
|
||||
<!-- Current Status -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-700">Aktueller Schutz:</span>
|
||||
<span :class="[
|
||||
'px-3 py-1 rounded-full text-sm font-medium',
|
||||
lockMethod === 'none' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'
|
||||
]">
|
||||
{{ lockMethod === 'biometric' ? '👆 Biometrie' : lockMethod === 'pin' ? '🔢 PIN' : '❌ Aus' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="space-y-3">
|
||||
<!-- Biometric Option -->
|
||||
<button
|
||||
v-if="hasBiometricSupport"
|
||||
@click="hasBiometric ? setLockMethod('biometric') : setupBiometric()"
|
||||
:class="[
|
||||
'w-full p-4 rounded-lg border-2 text-left transition flex items-center',
|
||||
lockMethod === 'biometric'
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-gray-200 hover:border-purple-300'
|
||||
]"
|
||||
>
|
||||
<span class="text-2xl mr-4">👆</span>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Fingerabdruck / Face ID</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ hasBiometric ? 'Eingerichtet' : 'Jetzt einrichten' }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="lockMethod === 'biometric'" class="text-purple-600">✓</span>
|
||||
</button>
|
||||
|
||||
<!-- PIN Option -->
|
||||
<button
|
||||
@click="hasPin ? setLockMethod('pin') : (step = 'setup-pin')"
|
||||
:class="[
|
||||
'w-full p-4 rounded-lg border-2 text-left transition flex items-center',
|
||||
lockMethod === 'pin'
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-gray-200 hover:border-purple-300'
|
||||
]"
|
||||
>
|
||||
<span class="text-2xl mr-4">🔢</span>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">PIN-Code (6 Ziffern)</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ hasPin ? 'Eingerichtet' : 'Jetzt einrichten' }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="lockMethod === 'pin'" class="text-purple-600">✓</span>
|
||||
</button>
|
||||
|
||||
<!-- Disable -->
|
||||
<button
|
||||
v-if="lockMethod !== 'none'"
|
||||
@click="disableLock"
|
||||
class="w-full p-4 rounded-lg border-2 border-red-200 text-left transition flex items-center hover:border-red-300 hover:bg-red-50"
|
||||
>
|
||||
<span class="text-2xl mr-4">🔓</span>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-red-700">Sperre deaktivieren</div>
|
||||
<div class="text-sm text-gray-500">Nicht empfohlen</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Change PIN -->
|
||||
<button
|
||||
v-if="hasPin"
|
||||
@click="step = 'setup-pin'"
|
||||
class="w-full text-center text-purple-600 text-sm hover:underline mt-4"
|
||||
>
|
||||
PIN ändern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Setup PIN -->
|
||||
<div v-else-if="step === 'setup-pin' || step === 'confirm-pin'" class="text-center">
|
||||
<h3 class="text-lg font-semibold mb-2">
|
||||
{{ step === 'setup-pin' ? 'Neuen PIN eingeben' : 'PIN bestätigen' }}
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm mb-6">6 Ziffern</p>
|
||||
|
||||
<!-- PIN Dots -->
|
||||
<div class="flex justify-center space-x-3 mb-8">
|
||||
<div
|
||||
v-for="(digit, i) in pinDigits"
|
||||
:key="i"
|
||||
:class="[
|
||||
'w-4 h-4 rounded-full transition-all',
|
||||
digit ? 'bg-purple-600 scale-110' : 'bg-gray-300',
|
||||
error ? 'bg-red-400 animate-shake' : ''
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<p v-if="error" class="text-red-500 text-sm mb-4">{{ error }}</p>
|
||||
|
||||
<!-- Number Pad -->
|
||||
<div class="grid grid-cols-3 gap-3 max-w-xs mx-auto">
|
||||
<button
|
||||
v-for="n in [1,2,3,4,5,6,7,8,9]"
|
||||
:key="n"
|
||||
@click="addDigit(String(n))"
|
||||
class="w-14 h-14 bg-gray-100 rounded-full text-xl font-semibold hover:bg-gray-200 transition"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
<button
|
||||
@click="step = 'menu'; clearPinInput()"
|
||||
class="w-14 h-14 text-gray-500 text-sm"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<button
|
||||
@click="addDigit('0')"
|
||||
class="w-14 h-14 bg-gray-100 rounded-full text-xl font-semibold hover:bg-gray-200 transition"
|
||||
>
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
@click="removeDigit"
|
||||
class="w-14 h-14 text-xl hover:bg-gray-100 rounded-full transition"
|
||||
>
|
||||
⌫
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Setup Biometric -->
|
||||
<div v-else-if="step === 'setup-biometric'" class="text-center py-8">
|
||||
<div class="text-6xl mb-4 animate-pulse">👆</div>
|
||||
<h3 class="text-lg font-semibold mb-2">Biometrie einrichten</h3>
|
||||
<p class="text-gray-500 text-sm mb-4">
|
||||
Bitte Fingerabdruck scannen oder Face ID verwenden...
|
||||
</p>
|
||||
<p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
|
||||
<button
|
||||
@click="step = 'menu'"
|
||||
class="mt-4 text-gray-500 text-sm hover:underline"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { api } from '@/api'
|
||||
import SecuritySettings from '@/components/SecuritySettings.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
@@ -11,6 +12,32 @@ const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const message = ref('')
|
||||
const error = ref('')
|
||||
const showSecuritySettings = ref(false)
|
||||
|
||||
// App lock status
|
||||
const lockMethod = ref<string | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
lockMethod.value = localStorage.getItem('lockMethod')
|
||||
// Store email for biometric setup
|
||||
if (authStore.user?.email) {
|
||||
localStorage.setItem('userEmail', authStore.user.email)
|
||||
}
|
||||
})
|
||||
|
||||
const lockStatusText = computed(() => {
|
||||
if (!lockMethod.value || lockMethod.value === 'none') return 'Deaktiviert'
|
||||
if (lockMethod.value === 'biometric') return 'Fingerabdruck / Face ID'
|
||||
if (lockMethod.value === 'pin') return 'PIN-Code'
|
||||
return 'Unbekannt'
|
||||
})
|
||||
|
||||
const lockStatusClass = computed(() => {
|
||||
if (!lockMethod.value || lockMethod.value === 'none') {
|
||||
return 'bg-red-100 text-red-700'
|
||||
}
|
||||
return 'bg-green-100 text-green-700'
|
||||
})
|
||||
|
||||
async function changePassword() {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
@@ -42,6 +69,11 @@ async function changePassword() {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSecurityClose() {
|
||||
showSecuritySettings.value = false
|
||||
lockMethod.value = localStorage.getItem('lockMethod')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,7 +82,7 @@ async function changePassword() {
|
||||
|
||||
<!-- Profile -->
|
||||
<div class="card">
|
||||
<h2 class="text-lg font-semibold mb-4">Profil</h2>
|
||||
<h2 class="text-lg font-semibold mb-4">👤 Profil</h2>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500">Name</label>
|
||||
@@ -67,9 +99,33 @@ async function changePassword() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Security -->
|
||||
<div class="card">
|
||||
<h2 class="text-lg font-semibold mb-4">🔐 App-Sicherheit</h2>
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Schütze deine App mit Fingerabdruck, Face ID oder PIN.
|
||||
Bei jedem Öffnen der App musst du dich verifizieren.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div class="font-medium">App-Sperre</div>
|
||||
<div :class="['inline-block px-2 py-1 rounded text-sm mt-1', lockStatusClass]">
|
||||
{{ lockStatusText }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="showSecuritySettings = true"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Konfigurieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password -->
|
||||
<div class="card">
|
||||
<h2 class="text-lg font-semibold mb-4">Passwort ändern</h2>
|
||||
<h2 class="text-lg font-semibold mb-4">🔑 Passwort ändern</h2>
|
||||
<form @submit.prevent="changePassword" class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Aktuelles Passwort</label>
|
||||
@@ -92,5 +148,11 @@ async function changePassword() {
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Security Settings Modal -->
|
||||
<SecuritySettings
|
||||
v-if="showSecuritySettings"
|
||||
@close="handleSecurityClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user