@@ -53,7 +53,7 @@ function SpeedBonusPreview({ max, min }: { max: number; min: number }) {
5353}
5454
5555export default function Settings ( ) {
56- const { token } = useAuth ( ) ;
56+ const { token, isSuperAdmin } = useAuth ( ) ;
5757 const [ cfg , setCfg ] = useState < Partial < AppConfig > | null > ( null ) ;
5858 const [ saved , setSaved ] = useState ( false ) ;
5959 const [ saving , setSaving ] = useState ( false ) ;
@@ -67,6 +67,13 @@ export default function Settings() {
6767 const [ availableAvatars , setAvailableAvatars ] = useState < string [ ] | null > ( null ) ;
6868 const [ avatarsError , setAvatarsError ] = useState < string | null > ( null ) ;
6969
70+ // Super admin: admin management
71+ const [ dbAdmins , setDbAdmins ] = useState <
72+ { id : number ; username : string ; created_at : string ; last_password_change : string | null } [ ]
73+ > ( [ ] ) ;
74+ const [ resetPasswords , setResetPasswords ] = useState < Record < number , string > > ( { } ) ;
75+ const [ resettingId , setResettingId ] = useState < number | null > ( null ) ;
76+
7077 const headers = { Authorization : `Bearer ${ token } ` , 'Content-Type' : 'application/json' } ;
7178
7279 useEffect ( ( ) => {
@@ -93,7 +100,16 @@ export default function Settings() {
93100 setAvailableAvatars ( [ ] ) ;
94101 setAvatarsError ( 'Could not load current avatars.' ) ;
95102 } ) ;
96- } , [ token ] ) ;
103+
104+ if ( isSuperAdmin ) {
105+ fetch ( '/api/admin/admins' , { headers : { Authorization : `Bearer ${ token } ` } } )
106+ . then ( ( r ) => r . json ( ) )
107+ . then ( ( data ) => {
108+ if ( data . admins ) setDbAdmins ( data . admins ) ;
109+ } )
110+ . catch ( ( ) => { } ) ;
111+ }
112+ } , [ token , isSuperAdmin ] ) ;
97113
98114 async function changePassword ( ) {
99115 if ( ! currentPassword ) {
@@ -393,51 +409,124 @@ export default function Settings() {
393409 </ div >
394410
395411 { /* Admin credentials */ }
396- < div className = "card" >
397- < h2 className = "mb-4" > 🔐 Admin Credentials</ h2 >
398- < p className = "text-sm text-muted mb-4" >
399- Current username: < strong style = { { color : 'var(--text)' } } > { adminUsername || '...' } </ strong >
400- </ p >
412+ { isSuperAdmin ? (
413+ < div className = "card" >
414+ < h2 className = "mb-4" > 🔐 Admin Management (Super Admin)</ h2 >
415+ < div className = "alert alert-warn mb-4" style = { { fontSize : '0.85rem' } } >
416+ You are logged in as the super admin. Your password is managed via the
417+ ADMIN_PASSWORD environment variable.
418+ </ div >
401419
402- < div className = "form-group" >
403- < Input
404- label = "Current Password (required to make changes)"
405- type = "password"
406- autoComplete = "off"
407- value = { currentPassword }
408- onChange = { ( e ) => setCurrentPassword ( e . target . value ) }
409- placeholder = "Enter current password"
410- />
411- < Input
412- label = "New Username (leave blank to keep current)"
413- autoComplete = "off"
414- value = { newUsername }
415- onChange = { ( e ) => setNewUsername ( e . target . value ) }
416- placeholder = { adminUsername || 'admin' }
417- />
418- < Input
419- label = "New Password (leave blank to keep current)"
420- type = "password"
421- autoComplete = "new-password"
422- value = { newPassword }
423- onChange = { ( e ) => setNewPassword ( e . target . value ) }
424- placeholder = "Enter new password"
425- noMargin
426- />
427- < button
428- type = "button"
429- onClick = { changePassword }
430- disabled = { changingPassword }
431- className = "btn btn-primary mt-3"
432- >
433- { changingPassword ? 'Updating...' : 'Update Credentials' }
434- </ button >
420+ { dbAdmins . length === 0 ? (
421+ < p className = "text-sm text-muted" > No database admins found.</ p >
422+ ) : (
423+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : 12 } } >
424+ { dbAdmins . map ( ( admin ) => (
425+ < div
426+ key = { admin . id }
427+ style = { {
428+ display : 'flex' ,
429+ alignItems : 'center' ,
430+ gap : 12 ,
431+ padding : '10px 14px' ,
432+ background : 'var(--surface2)' ,
433+ border : '1px solid var(--border)' ,
434+ borderRadius : 'var(--radius-sm)' ,
435+ flexWrap : 'wrap' ,
436+ } }
437+ >
438+ < strong style = { { flex : 1 , minWidth : 100 } } > { admin . username } </ strong >
439+ < Input
440+ label = ""
441+ type = "password"
442+ autoComplete = "new-password"
443+ placeholder = "New password"
444+ value = { resetPasswords [ admin . id ] || '' }
445+ onChange = { ( e ) =>
446+ setResetPasswords ( ( prev ) => ( { ...prev , [ admin . id ] : e . target . value } ) )
447+ }
448+ noMargin
449+ />
450+ < button
451+ type = "button"
452+ className = "btn btn-primary"
453+ disabled = { resettingId === admin . id || ! resetPasswords [ admin . id ] }
454+ onClick = { async ( ) => {
455+ setResettingId ( admin . id ) ;
456+ try {
457+ const res = await fetch ( `/api/admin/admins/${ admin . id } /reset-password` , {
458+ method : 'POST' ,
459+ headers,
460+ body : JSON . stringify ( { newPassword : resetPasswords [ admin . id ] } ) ,
461+ } ) ;
462+ if ( res . ok ) {
463+ alert ( `Password reset for ${ admin . username } ` ) ;
464+ setResetPasswords ( ( prev ) => ( { ...prev , [ admin . id ] : '' } ) ) ;
465+ } else {
466+ const err = await res . json ( ) ;
467+ alert ( err . error || 'Reset failed' ) ;
468+ }
469+ } catch {
470+ alert ( 'Reset failed' ) ;
471+ } finally {
472+ setResettingId ( null ) ;
473+ }
474+ } }
475+ >
476+ { resettingId === admin . id ? 'Resetting...' : 'Reset Password' }
477+ </ button >
478+ </ div >
479+ ) ) }
480+ </ div >
481+ ) }
435482 </ div >
483+ ) : (
484+ < div className = "card" >
485+ < h2 className = "mb-4" > 🔐 Admin Credentials</ h2 >
486+ < p className = "text-sm text-muted mb-4" >
487+ Current username: < strong style = { { color : 'var(--text)' } } > { adminUsername || '...' } </ strong >
488+ </ p >
436489
437- < div className = "alert alert-warn mt-4" style = { { fontSize : '0.85rem' } } >
438- ⚠️ After changing credentials, you'll be logged out and need to log back in.
490+ < div className = "form-group" >
491+ < Input
492+ label = "Current Password (required to make changes)"
493+ type = "password"
494+ autoComplete = "off"
495+ value = { currentPassword }
496+ onChange = { ( e ) => setCurrentPassword ( e . target . value ) }
497+ placeholder = "Enter current password"
498+ />
499+ < Input
500+ label = "New Username (leave blank to keep current)"
501+ autoComplete = "off"
502+ value = { newUsername }
503+ onChange = { ( e ) => setNewUsername ( e . target . value ) }
504+ placeholder = { adminUsername || 'admin' }
505+ />
506+ < Input
507+ label = "New Password (leave blank to keep current)"
508+ type = "password"
509+ autoComplete = "new-password"
510+ value = { newPassword }
511+ onChange = { ( e ) => setNewPassword ( e . target . value ) }
512+ placeholder = "Enter new password"
513+ noMargin
514+ />
515+ < button
516+ type = "button"
517+ onClick = { changePassword }
518+ disabled = { changingPassword }
519+ className = "btn btn-primary mt-3"
520+ >
521+ { changingPassword ? 'Updating...' : 'Update Credentials' }
522+ </ button >
523+ </ div >
524+
525+ < div className = "alert alert-warn mt-4" style = { { fontSize : '0.85rem' } } >
526+ ⚠️ After changing credentials, you'll be logged out and need to log back in.
527+ </ div >
439528 </ div >
440- </ div >
529+ ) }
441530
442531 { /* Avatar / emoji library */ }
443532 < div className = "card" >
0 commit comments