- 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
334 lines
11 KiB
Vue
334 lines
11 KiB
Vue
<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>
|