1- import type { Headers , UploadPublicKeyRequest , BackupBlob } from "@/core/types" ;
1+ import { API_BASE_URL } from "@/core/config" ;
2+ import type { LoginRequest , RegisterRequest , LoginResponse } from "@/core/types" ;
23import { generateX25519KeyPair } from "@/utils/crypto/asymmetric" ;
34import { encodeBlob , encryptBackupWithPassword , decryptBackupWithPassword , decodeBlob } from "@/utils/crypto/backup" ;
45import { b64 , ub64 } from "@/utils/utils" ;
5- import { API_BASE_URL } from "@/core/config" ;
66import { hkdfExtractAndExpand } from "@/utils/crypto/kdf" ;
7+ import { fetchPublicKey , uploadPublicKey , fetchBackupBlob , uploadBackupBlob } from "../crypto" ;
8+ import type { Headers } from "@/core/types" ;
79
810/**
911 * Generates authentication headers for API requests
12+ * @param {string | null } token - Authentication token
1013 * @param {boolean } json - Whether to include JSON content type header
1114 * @returns {Headers } Headers object with authentication and content type
1215 */
@@ -23,61 +26,25 @@ export function getAuthHeaders(token: string | null, json: boolean = true): Head
2326 return headers ;
2427}
2528
26- let currentPublicKey : Uint8Array | null = null ;
27- let currentPrivateKey : Uint8Array | null = null ;
28-
29- async function fetchPublicKey ( token : string ) : Promise < Uint8Array | null > {
30- const headers = getAuthHeaders ( token , true ) ;
31- const res = await fetch ( `${ API_BASE_URL } /crypto/public-key` , { method : "GET" , headers } ) ;
32- if ( ! res . ok ) return null ;
33- const data = await res . json ( ) ;
34- if ( ! data ?. publicKey ) return null ;
35- return ub64 ( data . publicKey ) ;
29+ export interface CheckAuthResponse {
30+ authenticated : boolean ;
31+ username : string ;
32+ admin : boolean ;
3633}
3734
38- async function uploadPublicKey ( publicKey : Uint8Array , token : string ) : Promise < void > {
39- const payload : UploadPublicKeyRequest = {
40- publicKey : b64 ( publicKey )
41- }
42-
43- const headers = getAuthHeaders ( token , true ) ;
44- await fetch ( `${ API_BASE_URL } /crypto/public-key` , {
45- method : "POST" ,
46- headers,
47- body : JSON . stringify ( payload )
48- } ) ;
49- }
50-
51- async function fetchBackupBlob ( token : string ) : Promise < string | null > {
52- const headers = getAuthHeaders ( token , true ) ;
53- const res = await fetch ( `${ API_BASE_URL } /crypto/backup` , {
54- method : "GET" ,
55- headers
56- } ) ;
57- if ( res . ok ) {
58- const response : BackupBlob = await res . json ( ) ;
59- return response . blob ;
60- } else {
61- return null ;
62- }
63- }
64-
65- async function uploadBackupBlob ( blobJson : string , token : string ) : Promise < void > {
66- const payload : BackupBlob = { blob : blobJson }
67-
68- const headers = getAuthHeaders ( token , true ) ;
69- await fetch ( `${ API_BASE_URL } /crypto/backup` , {
70- method : "POST" ,
71- headers,
72- body : JSON . stringify ( payload )
73- } ) ;
35+ export interface LogoutResponse {
36+ status : string ;
37+ message : string ;
7438}
7539
7640export interface UserKeyPairMemory {
7741 publicKey : Uint8Array ;
7842 privateKey : Uint8Array ;
7943}
8044
45+ let currentPublicKey : Uint8Array | null = null ;
46+ let currentPrivateKey : Uint8Array | null = null ;
47+
8148export function getCurrentKeys ( ) : UserKeyPairMemory | null {
8249 if ( currentPublicKey && currentPrivateKey ) return { publicKey : currentPublicKey , privateKey : currentPrivateKey } ;
8350 return null ;
@@ -94,6 +61,72 @@ function saveKeys(
9461 localStorage . setItem ( "privateKey" , encodedPrivateKey ) ;
9562}
9663
64+ /**
65+ * Checks if the current user is authenticated
66+ */
67+ export async function checkAuth ( token : string ) : Promise < CheckAuthResponse > {
68+ const res = await fetch ( `${ API_BASE_URL } /check_auth` , {
69+ headers : getAuthHeaders ( token , true )
70+ } ) ;
71+ if ( ! res . ok ) throw new Error ( "Failed to check auth" ) ;
72+ return await res . json ( ) ;
73+ }
74+
75+ /**
76+ * Logs in a user with username and password
77+ */
78+ export async function login ( request : LoginRequest ) : Promise < LoginResponse > {
79+ const res = await fetch ( `${ API_BASE_URL } /login` , {
80+ method : "POST" ,
81+ headers : getAuthHeaders ( null , true ) ,
82+ body : JSON . stringify ( request )
83+ } ) ;
84+ if ( ! res . ok ) {
85+ const error = await res . json ( ) . catch ( ( ) => ( { detail : "Login failed" } ) ) ;
86+ throw new Error ( error . detail || "Login failed" ) ;
87+ }
88+ return await res . json ( ) ;
89+ }
90+
91+ /**
92+ * Registers a new user
93+ */
94+ export async function register ( request : RegisterRequest ) : Promise < LoginResponse > {
95+ const res = await fetch ( `${ API_BASE_URL } /register` , {
96+ method : "POST" ,
97+ headers : getAuthHeaders ( null , true ) ,
98+ body : JSON . stringify ( request )
99+ } ) ;
100+ if ( ! res . ok ) {
101+ const error = await res . json ( ) . catch ( ( ) => ( { detail : "Registration failed" } ) ) ;
102+ throw new Error ( error . detail || "Registration failed" ) ;
103+ }
104+ return await res . json ( ) ;
105+ }
106+
107+ /**
108+ * Logs out the current user
109+ */
110+ export async function logout ( token : string ) : Promise < LogoutResponse > {
111+ const res = await fetch ( `${ API_BASE_URL } /logout` , {
112+ headers : getAuthHeaders ( token , true )
113+ } ) ;
114+ if ( ! res . ok ) throw new Error ( "Failed to logout" ) ;
115+ return await res . json ( ) ;
116+ }
117+
118+ /**
119+ * Derive a client-side authentication secret so the raw password never leaves the client.
120+ * Uses PBKDF2 (via WebCrypto) + HKDF to produce a stable 32-byte key, then base64.
121+ */
122+ export async function deriveAuthSecret ( username : string , password : string ) : Promise < string > {
123+ // Use per-user salt derived from username; in future we can fetch a server-provided salt
124+ const salt = new TextEncoder ( ) . encode ( `fromchat.user:${ username } ` ) ;
125+ // Derive 32 bytes using HKDF; PBKDF2 already used within importPassword
126+ const derived = await hkdfExtractAndExpand ( new TextEncoder ( ) . encode ( password ) , salt , new TextEncoder ( ) . encode ( "auth-secret" ) , 32 ) ;
127+ return b64 ( derived ) ;
128+ }
129+
97130export async function ensureKeysOnLogin ( password : string , token : string ) : Promise < UserKeyPairMemory > {
98131 // Try to restore from backup
99132 const blobJson = await fetchBackupBlob ( token ) ;
@@ -147,13 +180,41 @@ export function getAuthToken(): string | null {
147180}
148181
149182/**
150- * Derive a client-side authentication secret so the raw password never leaves the client.
151- * Uses PBKDF2 (via WebCrypto) + HKDF to produce a stable 32-byte key, then base64.
183+ * Changes the user's password
152184 */
153- export async function deriveAuthSecret ( username : string , password : string ) : Promise < string > {
154- // Use per-user salt derived from username; in future we can fetch a server-provided salt
155- const salt = new TextEncoder ( ) . encode ( `fromchat.user:${ username } ` ) ;
156- // Derive 32 bytes using HKDF; PBKDF2 already used within importPassword
157- const derived = await hkdfExtractAndExpand ( new TextEncoder ( ) . encode ( password ) , salt , new TextEncoder ( ) . encode ( "auth-secret" ) , 32 ) ;
158- return b64 ( derived ) ;
159- }
185+ export async function changePassword (
186+ token : string ,
187+ username : string ,
188+ currentPassword : string ,
189+ newPassword : string ,
190+ logoutAllExceptCurrent : boolean
191+ ) : Promise < void > {
192+ const currentDerived = await deriveAuthSecret ( username , currentPassword ) ;
193+ const newDerived = await deriveAuthSecret ( username , newPassword ) ;
194+ const res = await fetch ( `${ API_BASE_URL } /change-password` , {
195+ method : "POST" ,
196+ headers : getAuthHeaders ( token , true ) ,
197+ body : JSON . stringify ( {
198+ currentPasswordDerived : currentDerived ,
199+ newPasswordDerived : newDerived ,
200+ logoutAllExceptCurrent
201+ } )
202+ } ) ;
203+ if ( ! res . ok ) throw new Error ( "Failed to change password" ) ;
204+ }
205+
206+ /**
207+ * Deletes the current user's account
208+ */
209+ export async function deleteAccount ( token : string ) : Promise < { status : string ; message : string } > {
210+ const res = await fetch ( `${ API_BASE_URL } /account/delete` , {
211+ method : "POST" ,
212+ headers : getAuthHeaders ( token , true )
213+ } ) ;
214+ if ( ! res . ok ) {
215+ const error = await res . json ( ) . catch ( ( ) => ( { detail : "Failed to delete account" } ) ) ;
216+ throw new Error ( error . detail || "Failed to delete account" ) ;
217+ }
218+ return await res . json ( ) ;
219+ }
220+
0 commit comments