feat: add settings page to analytics-frontend

This commit is contained in:
Usman Baig
2026-01-16 22:37:40 +01:00
parent 63921d5e04
commit e1cf7d4b13
6 changed files with 1343 additions and 0 deletions

40
lib/api/2fa.ts Normal file
View File

@@ -0,0 +1,40 @@
import apiRequest from './client'
export interface Setup2FAResponse {
secret: string
qr_code: string
}
export interface Verify2FAResponse {
message: string
recovery_codes: string[]
}
export interface RegenerateCodesResponse {
recovery_codes: string[]
}
export async function setup2FA(): Promise<Setup2FAResponse> {
return apiRequest<Setup2FAResponse>('/auth/2fa/setup', {
method: 'POST',
})
}
export async function verify2FA(code: string): Promise<Verify2FAResponse> {
return apiRequest<Verify2FAResponse>('/auth/2fa/verify', {
method: 'POST',
body: JSON.stringify({ code }),
})
}
export async function disable2FA(): Promise<void> {
return apiRequest<void>('/auth/2fa/disable', {
method: 'POST',
})
}
export async function regenerateRecoveryCodes(): Promise<RegenerateCodesResponse> {
return apiRequest<RegenerateCodesResponse>('/auth/2fa/recovery', {
method: 'POST',
})
}

60
lib/api/user.ts Normal file
View File

@@ -0,0 +1,60 @@
import apiRequest from './client'
export async function deleteAccount(password: string): Promise<void> {
// This goes to ciphera-auth
return apiRequest<void>('/auth/user', {
method: 'DELETE',
body: JSON.stringify({ password }),
})
}
export interface Session {
id: string
client_ip: string
user_agent: string
created_at: string
expires_at: string
is_current: boolean
}
export async function getUserSessions(): Promise<{ sessions: Session[] }> {
// Hash the current refresh token to identify current session
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null
let currentTokenHash = ''
if (refreshToken) {
// Hash the refresh token using SHA-256
const encoder = new TextEncoder()
const data = encoder.encode(refreshToken)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
currentTokenHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
return apiRequest<{ sessions: Session[] }>('/auth/user/sessions', {
headers: currentTokenHash ? {
'X-Current-Session-Hash': currentTokenHash,
} : undefined,
})
}
export async function revokeSession(sessionId: string): Promise<void> {
return apiRequest<void>(`/auth/user/sessions/${sessionId}`, {
method: 'DELETE',
})
}
export interface UserPreferences {
email_notifications: {
new_file_received: boolean
file_downloaded: boolean
security_alerts: boolean
}
}
export async function updateUserPreferences(preferences: UserPreferences): Promise<void> {
return apiRequest<void>('/auth/user/preferences', {
method: 'PUT',
body: JSON.stringify(preferences),
})
}

133
lib/crypto/password.ts Normal file
View File

@@ -0,0 +1,133 @@
/**
* Password protection utilities using PBKDF2
*/
/**
* Derive a key from password using PBKDF2
* This is used to encrypt the file encryption key when password protection is enabled
*/
export async function deriveKeyFromPassword(
password: string,
salt: Uint8Array
): Promise<CryptoKey> {
// * Import password as key material
const passwordKey = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
)
// * Derive key using PBKDF2
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt as BufferSource,
iterations: 100000, // * High iteration count for security
hash: 'SHA-256',
},
passwordKey,
{
name: 'AES-GCM',
length: 256,
},
false, // not extractable
['encrypt', 'decrypt']
)
}
/**
* Derive an authentication key from password and email (used as salt).
* This ensures the raw password never leaves the client.
*/
export async function deriveAuthKey(
password: string,
email: string
): Promise<string> {
const encoder = new TextEncoder()
// * Import password as key material
const passwordKey = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits']
)
// * Derive bits using PBKDF2
// * We use the email as a deterministic salt for the auth key
const derivedBits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: encoder.encode(email),
iterations: 100000,
hash: 'SHA-256',
},
passwordKey,
256 // 256 bits = 32 bytes
)
// * Convert to hex string
return Array.from(new Uint8Array(derivedBits))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
/**
* Generate a random salt for PBKDF2
*/
export function generateSalt(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(16))
}
/**
* Encrypt a key with a password-derived key
*/
export async function encryptKeyWithPassword(
key: Uint8Array,
password: string
): Promise<{ encrypted: ArrayBuffer; iv: Uint8Array; salt: Uint8Array }> {
const salt = generateSalt()
const derivedKey = await deriveKeyFromPassword(password, salt)
const iv = crypto.getRandomValues(new Uint8Array(12))
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv as BufferSource,
},
derivedKey,
key as BufferSource
)
return {
encrypted,
iv,
salt,
}
}
/**
* Decrypt a key using a password
*/
export async function decryptKeyWithPassword(
encrypted: ArrayBuffer,
iv: Uint8Array,
salt: Uint8Array,
password: string
): Promise<Uint8Array> {
const derivedKey = await deriveKeyFromPassword(password, salt)
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv as BufferSource,
},
derivedKey,
encrypted
)
return new Uint8Array(decrypted)
}