| | |
| | class AuthenticationManager { |
| | constructor() { |
| | this.currentUser = null; |
| | this.sessionTimeout = 15 * 60 * 1000; |
| | this.maxLoginAttempts = 3; |
| | this.lockoutDuration = 30 * 60 * 1000; |
| | this.biometricCredential = null; |
| | |
| | this.init(); |
| | } |
| |
|
| | |
| | init() { |
| | this.setupSessionManagement(); |
| | this.loadStoredCredentials(); |
| | this.checkBiometricSupport(); |
| | } |
| |
|
| | |
| | setupSessionManagement() { |
| | |
| | document.addEventListener('click', () => this.updateLastActivity()); |
| | document.addEventListener('keypress', () => this.updateLastActivity()); |
| | document.addEventListener('touchstart', () => this.updateLastActivity()); |
| |
|
| | |
| | setInterval(() => this.checkSessionExpiry(), 60000); |
| | } |
| |
|
| | |
| | async loginWithPin(phoneNumber, pin) { |
| | try { |
| | |
| | if (this.isAccountLocked(phoneNumber)) { |
| | const lockoutTime = this.getLockoutRemainingTime(phoneNumber); |
| | throw new Error(`الحساب مقفل. المحاولة مرة أخرى خلال ${Math.ceil(lockoutTime / 60000)} دقيقة`); |
| | } |
| |
|
| | |
| | if (!this.validatePhoneNumber(phoneNumber)) { |
| | throw new Error('رقم الهاتف غير صحيح'); |
| | } |
| |
|
| | if (!this.validatePin(pin)) { |
| | throw new Error('رمز PIN يجب أن يكون 4-6 أرقام'); |
| | } |
| |
|
| | |
| | const isValid = await this.verifyCredentials(phoneNumber, pin); |
| | |
| | if (!isValid) { |
| | this.recordFailedAttempt(phoneNumber); |
| | const remainingAttempts = this.getRemainingAttempts(phoneNumber); |
| | |
| | if (remainingAttempts <= 0) { |
| | this.lockAccount(phoneNumber); |
| | throw new Error('تم قفل الحساب بسبب المحاولات الخاطئة المتكررة'); |
| | } |
| | |
| | throw new Error(`بيانات تسجيل الدخول غير صحيحة. المحاولات المتبقية: ${remainingAttempts}`); |
| | } |
| |
|
| | |
| | this.clearFailedAttempts(phoneNumber); |
| | const user = await this.createUserSession(phoneNumber, pin); |
| | |
| | return user; |
| |
|
| | } catch (error) { |
| | console.error('خطأ في تسجيل الدخول:', error); |
| | throw error; |
| | } |
| | } |
| |
|
| | |
| | async loginWithBiometric() { |
| | try { |
| | if (!this.isBiometricSupported()) { |
| | throw new Error('المصادقة البيومترية غير مدعومة في هذا المتصفح'); |
| | } |
| |
|
| | if (!this.biometricCredential) { |
| | throw new Error('لم يتم تسجيل بصمة مسبقاً. يرجى تسجيل الدخول برمز PIN أولاً'); |
| | } |
| |
|
| | |
| | const credential = await navigator.credentials.get({ |
| | publicKey: { |
| | challenge: this.generateChallenge(), |
| | allowCredentials: [{ |
| | type: 'public-key', |
| | id: this.biometricCredential.id |
| | }], |
| | timeout: 60000, |
| | userVerification: 'required' |
| | } |
| | }); |
| |
|
| | if (!credential) { |
| | throw new Error('فشل في التحقق من البصمة'); |
| | } |
| |
|
| | |
| | const savedUser = this.getStoredUser(); |
| | if (!savedUser) { |
| | throw new Error('لم يتم العثور على بيانات المستخدم'); |
| | } |
| |
|
| | const user = await this.createUserSession(savedUser.phone, null, 'biometric'); |
| | return user; |
| |
|
| | } catch (error) { |
| | console.error('خطأ في تسجيل الدخول بالبصمة:', error); |
| | throw error; |
| | } |
| | } |
| |
|
| | |
| | async registerBiometric(phoneNumber) { |
| | try { |
| | if (!this.isBiometricSupported()) { |
| | throw new Error('المصادقة البيومترية غير مدعومة'); |
| | } |
| |
|
| | const credential = await navigator.credentials.create({ |
| | publicKey: { |
| | challenge: this.generateChallenge(), |
| | rp: { |
| | name: 'محفظتي الموحدة', |
| | id: window.location.hostname |
| | }, |
| | user: { |
| | id: new TextEncoder().encode(phoneNumber), |
| | name: phoneNumber, |
| | displayName: `مستخدم ${phoneNumber}` |
| | }, |
| | pubKeyCredParams: [{ |
| | type: 'public-key', |
| | alg: -7 |
| | }], |
| | timeout: 60000, |
| | attestation: 'none', |
| | authenticatorSelection: { |
| | authenticatorAttachment: 'platform', |
| | userVerification: 'required' |
| | } |
| | } |
| | }); |
| |
|
| | if (!credential) { |
| | throw new Error('فشل في تسجيل البصمة'); |
| | } |
| |
|
| | |
| | this.biometricCredential = { |
| | id: credential.rawId, |
| | publicKey: credential.response.publicKey, |
| | phoneNumber: phoneNumber, |
| | registeredAt: new Date().toISOString() |
| | }; |
| |
|
| | this.storeBiometricCredential(); |
| | return true; |
| |
|
| | } catch (error) { |
| | console.error('خطأ في تسجيل البصمة:', error); |
| | throw error; |
| | } |
| | } |
| |
|
| | |
| | async createUserSession(phoneNumber, pin, authMethod = 'pin') { |
| | const user = { |
| | id: this.generateUserId(), |
| | phone: phoneNumber, |
| | name: this.extractNameFromPhone(phoneNumber), |
| | authMethod: authMethod, |
| | loginTime: new Date().toISOString(), |
| | lastActivity: new Date().toISOString(), |
| | sessionId: this.generateSessionId() |
| | }; |
| |
|
| | this.currentUser = user; |
| | this.storeUserSession(user); |
| | this.updateLastActivity(); |
| |
|
| | return user; |
| | } |
| |
|
| | |
| | logout() { |
| | this.currentUser = null; |
| | this.clearUserSession(); |
| | this.clearStoredCredentials(); |
| | |
| | |
| | if (window.app) { |
| | window.app.switchScreen('login'); |
| | } |
| | } |
| |
|
| | |
| | isSessionValid() { |
| | if (!this.currentUser) { |
| | return false; |
| | } |
| |
|
| | const lastActivity = new Date(this.currentUser.lastActivity); |
| | const now = new Date(); |
| | const timeDiff = now.getTime() - lastActivity.getTime(); |
| |
|
| | return timeDiff < this.sessionTimeout; |
| | } |
| |
|
| | |
| | checkSessionExpiry() { |
| | if (this.currentUser && !this.isSessionValid()) { |
| | this.showSessionExpiredDialog(); |
| | } |
| | } |
| |
|
| | |
| | showSessionExpiredDialog() { |
| | if (confirm('انتهت صلاحية الجلسة. هل تريد تسجيل الدخول مرة أخرى؟')) { |
| | this.logout(); |
| | } else { |
| | this.logout(); |
| | } |
| | } |
| |
|
| | |
| | updateLastActivity() { |
| | if (this.currentUser) { |
| | this.currentUser.lastActivity = new Date().toISOString(); |
| | this.storeUserSession(this.currentUser); |
| | } |
| | } |
| |
|
| | |
| | isAccountLocked(phoneNumber) { |
| | const lockData = this.getLockData(phoneNumber); |
| | if (!lockData) return false; |
| |
|
| | const now = new Date().getTime(); |
| | return now < lockData.lockedUntil; |
| | } |
| |
|
| | |
| | getLockoutRemainingTime(phoneNumber) { |
| | const lockData = this.getLockData(phoneNumber); |
| | if (!lockData) return 0; |
| |
|
| | const now = new Date().getTime(); |
| | return Math.max(0, lockData.lockedUntil - now); |
| | } |
| |
|
| | |
| | recordFailedAttempt(phoneNumber) { |
| | const attempts = this.getFailedAttempts(phoneNumber); |
| | attempts.push(new Date().toISOString()); |
| | |
| | localStorage.setItem(`failed_attempts_${phoneNumber}`, JSON.stringify(attempts)); |
| | } |
| |
|
| | |
| | getFailedAttempts(phoneNumber) { |
| | const stored = localStorage.getItem(`failed_attempts_${phoneNumber}`); |
| | return stored ? JSON.parse(stored) : []; |
| | } |
| |
|
| | |
| | getRemainingAttempts(phoneNumber) { |
| | const attempts = this.getFailedAttempts(phoneNumber); |
| | return Math.max(0, this.maxLoginAttempts - attempts.length); |
| | } |
| |
|
| | |
| | lockAccount(phoneNumber) { |
| | const lockData = { |
| | lockedAt: new Date().toISOString(), |
| | lockedUntil: new Date().getTime() + this.lockoutDuration |
| | }; |
| | |
| | localStorage.setItem(`account_lock_${phoneNumber}`, JSON.stringify(lockData)); |
| | } |
| |
|
| | |
| | getLockData(phoneNumber) { |
| | const stored = localStorage.getItem(`account_lock_${phoneNumber}`); |
| | return stored ? JSON.parse(stored) : null; |
| | } |
| |
|
| | |
| | clearFailedAttempts(phoneNumber) { |
| | localStorage.removeItem(`failed_attempts_${phoneNumber}`); |
| | localStorage.removeItem(`account_lock_${phoneNumber}`); |
| | } |
| |
|
| | |
| | validatePhoneNumber(phoneNumber) { |
| | return /^7[0-9]{8}$/.test(phoneNumber); |
| | } |
| |
|
| | |
| | validatePin(pin) { |
| | return /^[0-9]{4,6}$/.test(pin); |
| | } |
| |
|
| | |
| | async verifyCredentials(phoneNumber, pin) { |
| | |
| | await new Promise(resolve => setTimeout(resolve, 1000)); |
| | |
| | |
| | |
| | return this.validatePhoneNumber(phoneNumber) && this.validatePin(pin); |
| | } |
| |
|
| | |
| | isBiometricSupported() { |
| | return window.PublicKeyCredential && |
| | PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable; |
| | } |
| |
|
| | |
| | async checkBiometricSupport() { |
| | if (this.isBiometricSupported()) { |
| | try { |
| | const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); |
| | return available; |
| | } catch (error) { |
| | console.warn('خطأ في فحص دعم المصادقة البيومترية:', error); |
| | return false; |
| | } |
| | } |
| | return false; |
| | } |
| |
|
| | |
| | generateChallenge() { |
| | const array = new Uint8Array(32); |
| | crypto.getRandomValues(array); |
| | return array; |
| | } |
| |
|
| | |
| | generateUserId() { |
| | return 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); |
| | } |
| |
|
| | |
| | generateSessionId() { |
| | return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); |
| | } |
| |
|
| | |
| | extractNameFromPhone(phoneNumber) { |
| | return `مستخدم ${phoneNumber.substr(-4)}`; |
| | } |
| |
|
| | |
| | storeUserSession(user) { |
| | localStorage.setItem('unifiedWallet_session', JSON.stringify(user)); |
| | } |
| |
|
| | |
| | loadStoredSession() { |
| | const stored = localStorage.getItem('unifiedWallet_session'); |
| | if (stored) { |
| | const user = JSON.parse(stored); |
| | if (this.isSessionValid()) { |
| | this.currentUser = user; |
| | return user; |
| | } else { |
| | this.clearUserSession(); |
| | } |
| | } |
| | return null; |
| | } |
| |
|
| | |
| | clearUserSession() { |
| | localStorage.removeItem('unifiedWallet_session'); |
| | } |
| |
|
| | |
| | storeBiometricCredential() { |
| | if (this.biometricCredential) { |
| | localStorage.setItem('unifiedWallet_biometric', JSON.stringify(this.biometricCredential)); |
| | } |
| | } |
| |
|
| | |
| | loadStoredCredentials() { |
| | const stored = localStorage.getItem('unifiedWallet_biometric'); |
| | if (stored) { |
| | this.biometricCredential = JSON.parse(stored); |
| | } |
| | } |
| |
|
| | |
| | clearStoredCredentials() { |
| | localStorage.removeItem('unifiedWallet_biometric'); |
| | this.biometricCredential = null; |
| | } |
| |
|
| | |
| | getStoredUser() { |
| | const stored = localStorage.getItem('unifiedWallet_user'); |
| | return stored ? JSON.parse(stored) : null; |
| | } |
| |
|
| | |
| | getCurrentUser() { |
| | return this.currentUser; |
| | } |
| |
|
| | |
| | isLoggedIn() { |
| | return this.currentUser !== null && this.isSessionValid(); |
| | } |
| | } |
| |
|
| | |
| | window.AuthenticationManager = AuthenticationManager; |
| |
|