@@ -5,26 +5,43 @@ type OnLoginSuccess = (token: string, username: string) => void;
55export function initAuthView ( onLoginSuccess : OnLoginSuccess ) : void {
66 const tabLogin = document . getElementById ( 'tab-login' ) ! as HTMLButtonElement ;
77 const tabRegister = document . getElementById ( 'tab-register' ) ! as HTMLButtonElement ;
8+ const tabRecover = document . getElementById ( 'tab-recover' ) ! as HTMLButtonElement ;
89 const form = document . getElementById ( 'auth-form' ) ! as HTMLFormElement ;
10+ const recoveryForm = document . getElementById ( 'recovery-form' ) ! as HTMLFormElement ;
911 const submitBtn = document . getElementById ( 'auth-submit' ) ! as HTMLButtonElement ;
1012 const messageEl = document . getElementById ( 'auth-message' ) ! ;
1113
1214 let mode : 'login' | 'register' = 'login' ;
1315
16+ function setActiveTab ( tab : HTMLButtonElement ) {
17+ tabLogin . classList . remove ( 'active' ) ;
18+ tabRegister . classList . remove ( 'active' ) ;
19+ tabRecover . classList . remove ( 'active' ) ;
20+ tab . classList . add ( 'active' ) ;
21+ clearMessage ( ) ;
22+ }
23+
1424 tabLogin . addEventListener ( 'click' , ( ) => {
1525 mode = 'login' ;
16- tabLogin . classList . add ( 'active' ) ;
17- tabRegister . classList . remove ( 'active' ) ;
26+ setActiveTab ( tabLogin ) ;
27+ form . classList . remove ( 'hidden' ) ;
28+ recoveryForm . classList . add ( 'hidden' ) ;
1829 submitBtn . textContent = 'Log In' ;
19- clearMessage ( ) ;
2030 } ) ;
2131
2232 tabRegister . addEventListener ( 'click' , ( ) => {
2333 mode = 'register' ;
24- tabRegister . classList . add ( 'active' ) ;
25- tabLogin . classList . remove ( 'active' ) ;
34+ setActiveTab ( tabRegister ) ;
35+ form . classList . remove ( 'hidden' ) ;
36+ recoveryForm . classList . add ( 'hidden' ) ;
2637 submitBtn . textContent = 'Register' ;
27- clearMessage ( ) ;
38+ } ) ;
39+
40+ tabRecover . addEventListener ( 'click' , ( ) => {
41+ setActiveTab ( tabRecover ) ;
42+ form . classList . add ( 'hidden' ) ;
43+ recoveryForm . classList . remove ( 'hidden' ) ;
44+ resetRecoveryForm ( ) ;
2845 } ) ;
2946
3047 form . addEventListener ( 'submit' , async ( e ) => {
@@ -38,8 +55,6 @@ export function initAuthView(onLoginSuccess: OnLoginSuccess): void {
3855 submitBtn . textContent = mode === 'login' ? 'Logging in...' : 'Registering...' ;
3956
4057 try {
41- // OpaqueHttpClient.create() fetches /api/opaque/config and configures
42- // the cipher suite and KSF parameters automatically.
4358 const client = await OpaqueHttpClient . create ( '/api' ) ;
4459
4560 if ( mode === 'register' ) {
@@ -58,6 +73,57 @@ export function initAuthView(onLoginSuccess: OnLoginSuccess): void {
5873 }
5974 } ) ;
6075
76+ // ── Recovery flow ──────────────────────────────────────────────────────────
77+
78+ let recoveryStep : 'start' | 'verify' = 'start' ;
79+
80+ function resetRecoveryForm ( ) {
81+ recoveryStep = 'start' ;
82+ const codeField = document . getElementById ( 'recovery-code-field' ) ! ;
83+ const pwField = document . getElementById ( 'recovery-password-field' ) ! ;
84+ const submitBtn = document . getElementById ( 'recovery-submit' ) ! as HTMLButtonElement ;
85+ codeField . classList . add ( 'hidden' ) ;
86+ pwField . classList . add ( 'hidden' ) ;
87+ submitBtn . textContent = 'Send Recovery Code' ;
88+ ( document . getElementById ( 'recovery-username' ) as HTMLInputElement ) . value = '' ;
89+ ( document . getElementById ( 'recovery-code' ) as HTMLInputElement ) . value = '' ;
90+ ( document . getElementById ( 'recovery-password' ) as HTMLInputElement ) . value = '' ;
91+ }
92+
93+ recoveryForm . addEventListener ( 'submit' , async ( e ) => {
94+ e . preventDefault ( ) ;
95+ clearMessage ( ) ;
96+
97+ const username = ( document . getElementById ( 'recovery-username' ) as HTMLInputElement ) . value . trim ( ) ;
98+ const recoverSubmitBtn = document . getElementById ( 'recovery-submit' ) ! as HTMLButtonElement ;
99+ recoverSubmitBtn . disabled = true ;
100+
101+ try {
102+ const client = await OpaqueHttpClient . create ( '/api' ) ;
103+
104+ if ( recoveryStep === 'start' ) {
105+ await client . recoveryStart ( username ) ;
106+ showMessage ( 'Recovery code sent! Check the server console log for the code.' , 'success' ) ;
107+ recoveryStep = 'verify' ;
108+ document . getElementById ( 'recovery-code-field' ) ! . classList . remove ( 'hidden' ) ;
109+ document . getElementById ( 'recovery-password-field' ) ! . classList . remove ( 'hidden' ) ;
110+ recoverSubmitBtn . textContent = 'Verify & Set New Password' ;
111+ ( document . getElementById ( 'recovery-username' ) as HTMLInputElement ) . readOnly = true ;
112+ } else {
113+ const code = ( document . getElementById ( 'recovery-code' ) as HTMLInputElement ) . value . trim ( ) ;
114+ const newPassword = ( document . getElementById ( 'recovery-password' ) as HTMLInputElement ) . value ;
115+ await client . recoverAndReRegister ( username , code , newPassword ) ;
116+ showMessage ( 'Account recovered! You can now log in with your new password.' , 'success' ) ;
117+ resetRecoveryForm ( ) ;
118+ tabLogin . click ( ) ;
119+ }
120+ } catch ( err : unknown ) {
121+ showMessage ( err instanceof Error ? err . message : String ( err ) , 'error' ) ;
122+ } finally {
123+ recoverSubmitBtn . disabled = false ;
124+ }
125+ } ) ;
126+
61127 function showMessage ( msg : string , type : 'error' | 'success' ) : void {
62128 messageEl . textContent = msg ;
63129 messageEl . className = `message ${ type } ` ;
0 commit comments