@@ -910,6 +910,165 @@ async function renderVerifyEmail(token) {
910910 }
911911}
912912
913+ async function renderChangeEmailVerify ( token ) {
914+ const app = $ ( '#app' ) ;
915+
916+ if ( ! token ) {
917+ app . innerHTML = html `
918+ < div class ="auth-page ">
919+ < div class ="auth-card " style ="text-align:center; ">
920+ < div style ="margin:24px 0 16px; ">
921+ < i data-lucide ="x-circle " style ="width:48px;height:48px;color:var(--accent-red); "> </ i >
922+ </ div >
923+ < h1 class ="auth-title "> Invalid Link</ h1 >
924+ < p class ="auth-subtitle "> This email change link is invalid or has expired.</ p >
925+ < div style ="margin-top:24px; ">
926+ < a href ="/login " class ="btn btn-primary "> Sign In</ a >
927+ </ div >
928+ </ div >
929+ </ div >
930+ ` ;
931+ initIcons ( ) ;
932+ return ;
933+ }
934+
935+ app . innerHTML = html `
936+ < div class ="login-page ">
937+ < div class ="login-left ">
938+ < div class ="login-left-top ">
939+ < img src ="https://img.zero-host.org/assets/picto.png " alt ="ZeroHost " />
940+ < span > | Dashboard</ span >
941+ </ div >
942+ </ div >
943+ < div class ="login-right ">
944+ < div class ="login-card ">
945+ < div style ="margin:0 auto 24px;width:fit-content; ">
946+ < span class ="spinner " style ="width:36px;height:36px; "> </ span >
947+ </ div >
948+ < h1 class ="auth-title " style ="text-align:center "> Verifying link...</ h1 >
949+ </ div >
950+ </ div >
951+ </ div >
952+ ` ;
953+ initIcons ( ) ;
954+
955+ try {
956+ const data = await api ( `/auth/change-email/verify?token=${ encodeURIComponent ( token ) } ` ) ;
957+
958+ app . innerHTML = html `
959+ < div class ="login-page ">
960+ < div class ="login-left ">
961+ < div class ="login-left-top ">
962+ < img src ="https://img.zero-host.org/assets/picto.png " alt ="ZeroHost " />
963+ < span > | Dashboard</ span >
964+ </ div >
965+ </ div >
966+ < div class ="login-right ">
967+ < div class ="login-card ">
968+ < h1 class ="auth-title "> Enter verification code</ h1 >
969+ < p class ="auth-subtitle "> A 6-digit code was sent to < strong style ="color:var(--text-primary) "> ${ escapeHtml ( data . pendingEmail ) } </ strong > </ p >
970+ < form id ="change-email-code-form ">
971+ < div class ="auth-error "> </ div >
972+ < div class ="form-group ">
973+ < label for ="change-email-code "> Verification Code</ label >
974+ < input type ="text " id ="change-email-code " placeholder ="000000 " maxlength ="6 " pattern ="[0-9]{6} " inputmode ="numeric " autocomplete ="one-time-code " required style ="text-align:center;font-size:1.4rem;letter-spacing:8px;font-family:'JetBrains Mono',monospace; " />
975+ </ div >
976+ < button type ="submit " class ="btn btn-primary btn-full " id ="change-email-code-btn ">
977+ Confirm
978+ </ button >
979+ </ form >
980+ < button type ="button " class ="btn btn-ghost btn-full " id ="change-email-resend-btn " style ="margin-top:12px;border:1px solid var(--border) ">
981+ Resend code
982+ </ button >
983+ < div class ="auth-footer ">
984+ < a href ="/account/info " id ="change-email-cancel "> Cancel</ a >
985+ </ div >
986+ </ div >
987+ </ div >
988+ </ div >
989+ ` ;
990+
991+ $ ( '#change-email-code-form' ) . addEventListener ( 'submit' , async ( e ) => {
992+ e . preventDefault ( ) ;
993+ const btn = $ ( '#change-email-code-btn' ) ;
994+ const errorEl = $ ( '#change-email-code-form .auth-error' ) ;
995+ const code = $ ( '#change-email-code' ) . value . trim ( ) ;
996+
997+ if ( code . length !== 6 ) {
998+ errorEl . textContent = 'Please enter the 6-digit code' ;
999+ errorEl . classList . add ( 'show' ) ;
1000+ return ;
1001+ }
1002+
1003+ btn . disabled = true ;
1004+ btn . innerHTML = '<span class="spinner"></span>' ;
1005+
1006+ try {
1007+ const result = await api ( '/auth/change-email/confirm' , {
1008+ method : 'POST' ,
1009+ body : JSON . stringify ( { code } ) ,
1010+ } ) ;
1011+ state . token = result . token ;
1012+ state . user = result . user ;
1013+ localStorage . setItem ( 'zh_token' , result . token ) ;
1014+ localStorage . setItem ( 'zh_user' , JSON . stringify ( result . user ) ) ;
1015+ showToast ( 'Email updated successfully' , 'success' ) ;
1016+ navigateTo ( 'account/info' ) ;
1017+ } catch ( err ) {
1018+ errorEl . textContent = err . message ;
1019+ errorEl . classList . add ( 'show' ) ;
1020+ } finally {
1021+ btn . disabled = false ;
1022+ btn . innerHTML = 'Confirm' ;
1023+ }
1024+ } ) ;
1025+
1026+ $ ( '#change-email-resend-btn' ) . addEventListener ( 'click' , async ( ) => {
1027+ const btn = $ ( '#change-email-resend-btn' ) ;
1028+ btn . disabled = true ;
1029+ try {
1030+ await api ( `/auth/change-email/verify?token=${ encodeURIComponent ( token ) } ` ) ;
1031+ showToast ( 'New code sent' , 'success' ) ;
1032+ } catch ( err ) {
1033+ showToast ( err . message , 'error' ) ;
1034+ } finally {
1035+ btn . disabled = false ;
1036+ }
1037+ } ) ;
1038+
1039+ $ ( '#change-email-cancel' ) . addEventListener ( 'click' , ( e ) => {
1040+ e . preventDefault ( ) ;
1041+ navigateTo ( 'account/info' ) ;
1042+ } ) ;
1043+
1044+ initIcons ( ) ;
1045+ } catch ( err ) {
1046+ app . innerHTML = html `
1047+ < div class ="login-page ">
1048+ < div class ="login-left ">
1049+ < div class ="login-left-top ">
1050+ < img src ="https://img.zero-host.org/assets/picto.png " alt ="ZeroHost " />
1051+ < span > | Dashboard</ span >
1052+ </ div >
1053+ </ div >
1054+ < div class ="login-right ">
1055+ < div class ="login-card " style ="text-align:center; ">
1056+ < div style ="margin:0 auto 24px;width:fit-content; ">
1057+ < i data-lucide ="x-circle " style ="width:48px;height:48px;color:var(--accent-red); "> </ i >
1058+ </ div >
1059+ < h1 class ="auth-title "> Link expired</ h1 >
1060+ < p class ="auth-subtitle "> ${ escapeHtml ( err . message ) } </ p >
1061+ < div style ="margin-top:24px; ">
1062+ < a href ="/account/info " class ="btn btn-primary "> Back to Account</ a >
1063+ </ div >
1064+ </ div >
1065+ </ div >
1066+ </ div >
1067+ ` ;
1068+ initIcons ( ) ;
1069+ }
1070+ }
1071+
9131072function renderVerificationSent ( email ) {
9141073 const app = $ ( '#app' ) ;
9151074 app . innerHTML = html `
@@ -1472,6 +1631,13 @@ function navigateTo(page) {
14721631 history . replaceState ( { page : 'verify-email' } , '' , window . location . pathname + window . location . search ) ;
14731632 return ;
14741633 }
1634+ if ( basePage === 'change-email' ) {
1635+ const params = new URLSearchParams ( window . location . search ) ;
1636+ const token = params . get ( 'token' ) ;
1637+ renderChangeEmailVerify ( token ) ;
1638+ history . replaceState ( { page : 'change-email' } , '' , window . location . pathname + window . location . search ) ;
1639+ return ;
1640+ }
14751641
14761642 // Auth guard: require valid token for all other pages
14771643 if ( ! state . token ) {
@@ -3013,21 +3179,13 @@ async function handleChangeEmail(e) {
30133179 showToast ( 'Please fill in all fields' , 'error' ) ;
30143180 return ;
30153181 }
3016- const data = await api ( '/auth/change-email' , {
3182+ await api ( '/auth/change-email' , {
30173183 method : 'POST' ,
30183184 body : JSON . stringify ( { newEmail, password } ) ,
30193185 } ) ;
3020- state . token = data . token ;
3021- state . user = data . user ;
3022- localStorage . setItem ( 'zh_token' , data . token ) ;
3023- localStorage . setItem ( 'zh_user' , JSON . stringify ( data . user ) ) ;
3024- $ ( '#acc-new-email' ) . placeholder = newEmail ;
30253186 $ ( '#acc-new-email' ) . value = '' ;
30263187 $ ( '#acc-email-pw' ) . value = '' ;
3027- showToast ( 'Email updated successfully' , 'success' ) ;
3028-
3029- const sidebarImg = document . querySelector ( '#avatar-container img' ) ;
3030- if ( sidebarImg ) sidebarImg . src = gravatarUrl ( state . user . email , 32 ) ;
3188+ showToast ( 'Confirmation link sent to your current email' , 'success' ) ;
30313189 } catch ( err ) {
30323190 showToast ( err . message , 'error' ) ;
30333191 } finally {
@@ -3588,6 +3746,13 @@ function init() {
35883746 return ;
35893747 }
35903748
3749+ if ( basePage === 'change-email' ) {
3750+ const params = new URLSearchParams ( window . location . search ) ;
3751+ const token = params . get ( 'token' ) ;
3752+ renderChangeEmailVerify ( token ) ;
3753+ return ;
3754+ }
3755+
35913756 if ( basePage === 'login' || basePage === 'signup' ) {
35923757 if ( state . token ) {
35933758 history . replaceState ( { page : 'overview' } , '' , '/' ) ;
0 commit comments