|
|
|
|
|
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; |
|
|
|