@@ -41,13 +41,38 @@ <h2>Pod</h2>
4141
4242 < h2 > Explore</ h2 >
4343 < div class ="actions ">
44- < a class ="btn btn-secondary " id ="profile-link " href ="# "> Profile</ a >
4544 < a class ="btn btn-secondary " href ="./public/ "> /public/</ a >
45+ < a class ="btn btn-secondary " id ="profile-link " href ="# "> profile</ a >
4646 < a class ="btn btn-secondary " href ="./private/ "> /private/</ a >
4747 < a class ="btn btn-secondary " href ="./inbox/ "> /inbox/</ a >
4848 < a class ="btn btn-secondary " href ="./settings/ "> /settings/</ a >
4949 </ div >
5050
51+ < h2 > Password</ h2 >
52+ < p id ="pw-hint " style ="margin:.25em 0 .5em;color:#666;font-size:.9em "> Climb from rung 1 (default < code > me</ code > ) to rung 2 (your password).</ p >
53+ < button class ="btn btn-secondary " id ="pw-toggle " type ="button "> Change password</ button >
54+ < form id ="pw-form " hidden style ="margin:.75em 0 0;display:grid;gap:.5em;max-width:360px ">
55+ <!-- Hidden username for password managers: present in the form but
56+ not rendered. Standard pattern used by GitHub, Google, etc.
57+ Browsers use this to associate the saved password with an
58+ account name in the credential store. -->
59+ < input type ="text " id ="pw-username " name ="username " autocomplete ="username " value ="me " hidden >
60+ < label style ="display:grid;gap:.2em;font-size:.9em;color:#555 "> Current password
61+ < input type ="password " id ="pw-current " autocomplete ="current-password " required style ="padding:.5em .8em;border:1px solid #ddd;border-radius:6px;font:inherit ">
62+ </ label >
63+ < label style ="display:grid;gap:.2em;font-size:.9em;color:#555 "> New password
64+ < input type ="password " id ="pw-new " autocomplete ="new-password " required minlength ="4 " style ="padding:.5em .8em;border:1px solid #ddd;border-radius:6px;font:inherit ">
65+ </ label >
66+ < label style ="display:grid;gap:.2em;font-size:.9em;color:#555 "> Confirm new password
67+ < input type ="password " id ="pw-confirm " autocomplete ="new-password " required style ="padding:.5em .8em;border:1px solid #ddd;border-radius:6px;font:inherit ">
68+ </ label >
69+ < div class ="actions ">
70+ < button class ="btn " type ="submit " id ="pw-submit "> Update password</ button >
71+ < button class ="btn btn-secondary " type ="button " id ="pw-cancel "> Cancel</ button >
72+ </ div >
73+ < div id ="pw-msg " style ="display:none;padding:.6em .9em;border-radius:8px;font-size:.9em "> </ div >
74+ </ form >
75+
5176 < h2 > Session</ h2 >
5277 < div class ="actions ">
5378 < button class ="btn btn-danger " id ="signout-btn " type ="button "> Sign out</ button >
@@ -110,6 +135,72 @@ <h2>Session</h2>
110135 fail ( 'Sign-out failed: ' + ( e . message || String ( e ) ) ) ;
111136 }
112137 } ) ;
138+
139+ // Change-password flow. POSTs to JSS's PUT /idp/credentials with
140+ // {currentPassword, newPassword}. The endpoint requires the
141+ // session's DPoP-bound bearer token plus re-proof of the current
142+ // password — session.authFetch handles the auth headers, the form
143+ // collects the re-auth proof.
144+ const pwToggle = document . getElementById ( 'pw-toggle' ) ;
145+ const pwForm = document . getElementById ( 'pw-form' ) ;
146+ const pwCancel = document . getElementById ( 'pw-cancel' ) ;
147+ const pwSubmit = document . getElementById ( 'pw-submit' ) ;
148+ const pwMsg = document . getElementById ( 'pw-msg' ) ;
149+ const pwCurrent = document . getElementById ( 'pw-current' ) ;
150+ const pwNew = document . getElementById ( 'pw-new' ) ;
151+ const pwConfirm = document . getElementById ( 'pw-confirm' ) ;
152+
153+ function pwStatus ( text , isError ) {
154+ pwMsg . textContent = text ;
155+ pwMsg . style . display = 'block' ;
156+ pwMsg . style . background = isError ? '#fff0f0' : '#e8f5e9' ;
157+ pwMsg . style . color = isError ? '#a00' : '#1b5e20' ;
158+ }
159+ function pwReset ( ) {
160+ pwForm . reset ( ) ;
161+ pwMsg . style . display = 'none' ;
162+ }
163+
164+ pwToggle . addEventListener ( 'click' , ( ) => {
165+ pwForm . hidden = false ;
166+ pwToggle . hidden = true ;
167+ pwCurrent . focus ( ) ;
168+ } ) ;
169+ pwCancel . addEventListener ( 'click' , ( ) => {
170+ pwForm . hidden = true ;
171+ pwToggle . hidden = false ;
172+ pwReset ( ) ;
173+ } ) ;
174+ pwForm . addEventListener ( 'submit' , async ( ev ) => {
175+ ev . preventDefault ( ) ;
176+ if ( pwNew . value !== pwConfirm . value ) {
177+ pwStatus ( 'New password and confirmation do not match.' , true ) ;
178+ return ;
179+ }
180+ pwSubmit . disabled = true ;
181+ pwStatus ( 'Updating…' ) ;
182+ try {
183+ const res = await session . authFetch ( new URL ( './idp/credentials' , window . location . href ) . href , {
184+ method : 'PUT' ,
185+ headers : { 'Content-Type' : 'application/json' } ,
186+ body : JSON . stringify ( { currentPassword : pwCurrent . value , newPassword : pwNew . value } )
187+ } ) ;
188+ if ( ! res . ok ) {
189+ let detail = '' ;
190+ try {
191+ const j = await res . json ( ) ;
192+ detail = j . error_description || j . error || '' ;
193+ } catch { }
194+ throw new Error ( 'HTTP ' + res . status + ( detail ? ' — ' + detail : '' ) ) ;
195+ }
196+ pwStatus ( 'Password updated. You\'re on rung 2 now.' ) ;
197+ pwCurrent . value = pwNew . value = pwConfirm . value = '' ;
198+ } catch ( e ) {
199+ pwStatus ( 'Change failed: ' + ( e . message || String ( e ) ) , true ) ;
200+ } finally {
201+ pwSubmit . disabled = false ;
202+ }
203+ } ) ;
113204 } else {
114205 signedOutEl . hidden = false ;
115206 }
0 commit comments