admin/accounts: new "require password change" option #562
Massimo Melina committed
Jan 24, 2025 at 00:54 UTC
7205d21e60e6ce64b889e4518be4e20795afc85d
9 files changed
+48
-28
admin/src/AccountForm.ts
+2
-1
@@ -70,11 +70,12 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
70
: "Login is prevented if account is disabled, or all its groups are disabled" },
71
{ k: 'ignore_limits', comp: BoolField, xs: true,
72
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
73
- { k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: 6, lg: 8,
73
+ { k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: 6, lg: 4,
74
helperText: "To access THIS interface you are using right now",
75
...!account.admin && account.adminActualAccess && { value: true, disabled: true, helperText: "This permission is inherited. To disable it, act on the groups." },
76
},
77
{ k: 'disable_password_change', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Allow password change", xs: true },
78
+ { k: 'require_password_change', comp: BoolField, xs: 12, lg: 4, helperText: "At first login" },
79
!members ? null
80
: group && !members.length ? h(Box, {}, "No members")
81
: members.length > 0 && h(Box, {}, `${members.length} members: `,
config.md
+2
-1
@@ -215,7 +215,8 @@ For each account entries, this is the list of properties you can have:
215
- `disabled` prevents using this account. Default is false.
216
- `expire` account won't work once the time has passed this timestamp. Use JSON timestamp syntax. Default is none.
217
- `days_to_live` used to set `expire` on first login. Default is none.
218
-- `disable_password_change` set `true` if you want to forbid password change for users. Default is `false`.
218
+- `disable_password_change` set `true` if you want to forbid password change for this user. Default is `false`.
219
+- `require_password_change` set `true` if you want to require password change for this user. Default is `false`.
220
- `allow_net` a mask of addresses to restrict the access of the account
221
222
### Specify another file
frontend/src/UserPanel.ts
+23
-18
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { useSnapState } from './state'
3
+import { state, useSnapState } from './state'
4
import { createElement as h } from 'react'
5
import { alertDialog, newDialog, promptDialog } from './dialog'
6
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
@@ -27,23 +27,7 @@ export default function showUserPanel() {
27
label: t`Change password`,
28
id: 'change-password',
29
onClickAnimation: false,
30
- async onClick() {
31
- const pwd = await promptDialog(t('enter_pass', "Enter new password"), { type: 'password' })
32
- if (!pwd) return
33
- const check = await promptDialog(t('enter_pass2', "RE-enter same new password"), { type: 'password' })
34
- if (!check) return
35
- if (check !== pwd)
36
- return alertDialog(t('pass2_mismatch', "The second password you entered did not match the first. Procedure aborted."), 'warning')
37
- const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
38
- const res = await createVerifierAndSalt(srp6aNimbusRoutines, snap.username, pwd)
39
- try {
40
- await apiCall('change_my_srp', { salt: String(res.s), verifier: String(res.v) }, { modal: working })
41
- return alertDialog(t('password_changed', "Password changed"))
42
- }
43
- catch(e) {
44
- return alertDialog(e as Error)
45
- }
46
- }
30
+ onClick: changePassword,
31
}),
32
h(Btn, {
33
icon: 'logout',
@@ -59,3 +43,24 @@ export default function showUserPanel() {
43
}
44
})
45
}
46
+
47
+export async function changePassword(required=false) {
48
+ const pwd = await promptDialog(t('enter_pass', "Enter new password"), {
49
+ type: 'password',
50
+ helperText: required && t('required_change_password', "You are required to change your password")
51
+ })
52
+ if (!pwd) return
53
+ const check = await promptDialog(t('enter_pass2', "RE-enter same new password"), { type: 'password' })
54
+ if (!check) return
55
+ if (check !== pwd)
56
+ return alertDialog(t('pass2_mismatch', "The second password you entered did not match the first. Procedure aborted."), 'warning')
57
+ const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
58
+ const res = await createVerifierAndSalt(srp6aNimbusRoutines, state.username, pwd)
59
+ try {
60
+ await apiCall('change_my_srp', { salt: String(res.s), verifier: String(res.v) }, { modal: working })
61
+ return alertDialog(t('password_changed', "Password changed"))
62
+ }
63
+ catch(e) {
64
+ return alertDialog(e as Error)
65
+ }
66
+}
\ No newline at end of file
frontend/src/login.ts
+10
-4
@@ -10,23 +10,22 @@ import {
10
import { createElement as h, Fragment, useEffect, useRef } from 'react'
11
import { reloadList } from './useFetchList'
12
import { Checkbox, CustomCode } from './components'
13
+import { changePassword } from './UserPanel'
14
import i18n from './i18n'
15
const { t, useI18N } = i18n
16
17
async function login(username:string, password:string, extra?: object) {
18
const stopWorking = working()
19
return srpClientSequence(username, password, apiCall, extra).then(res => {
19
- stopWorking()
20
refreshSession(res)
21
state.loginRequired = false
22
return res
23
}, (err: any) => {
24
- stopWorking()
24
throw Error(err.message === 'trust' ? t('login_untrusted', "Login aborted: server identity cannot be trusted")
25
: err.code === HTTP_UNAUTHORIZED ? t('login_bad_credentials', "Invalid credentials")
26
: err.code === HTTP_CONFLICT ? t('login_bad_cookies', "Cookies not working - login failed")
27
: t(err.message || String(err)))
29
- })
28
+ }).finally(stopWorking)
29
}
30
31
const refreshSession = makeSessionRefresher(state)
@@ -147,7 +146,14 @@ export async function loginDialog(closable=true, reloadAfter=true) {
146
}
147
148
export function useAuthorized() {
150
- const { loginRequired } = useSnapState()
149
+ const { loginRequired, username } = useSnapState()
150
+ const last = useRef('')
151
+ useEffect(() => {
152
+ if (last.current === username) return // need to remember because we are not undoing our useEffect
153
+ last.current = username
154
+ if (username && getHFS().session?.requireChangePassword)
155
+ changePassword(true)
156
+ }, [username])
157
useEffect(() => {
158
if (!loginRequired)
159
closeLoginDialog?.()
shared/dialogs.ts
+2
-1
@@ -75,10 +75,11 @@ export function isDescendant(child: Node | null | undefined, parentMatch: Node |
75
76
let waitClosing = Promise.resolve()
77
let ignorePopState = false
78
-function back() {
78
+async function back() {
79
ignorePopState = true
80
let was = history.state
81
history.back()
82
+ await waitClosing
83
return waitClosing = new Promise<void>(res => {
84
const h = setInterval(() => was !== history.state && res() , 10)
85
setTimeout(() => clearTimeout(h), 500)
src/api.auth.ts
+4
-1
@@ -85,6 +85,7 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
85
expandedUsername: expandUsername(username),
86
adminUrl: ctxAdminAccess(ctx) ? ctx.state.revProxyPath + ADMIN_URI : undefined,
87
canChangePassword: canChangePassword(ctx.state.account),
88
+ requireChangePassword: ctx.state.account?.require_password_change,
89
exp: keepSessionAlive.get() ? new Date(Date.now() + sessionDuration.compiled()) : undefined,
90
accountExp: ctx.state.account?.expire,
91
}
@@ -93,7 +94,9 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
94
export const change_my_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
95
const a = ctx.state.account
96
return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
96
- : changeSrpHelper(a, salt, verifier)
97
+ : changeSrpHelper(a, salt, verifier).then(() => {
98
+ delete a.require_password_change
99
+ })
100
}
101
102
function canChangePassword(account: Account | undefined) {
src/langs/hfs-lang-en.json
+2
-1
@@ -181,6 +181,7 @@
181
182
"Calculate": "Calculate",
183
"Creation": "Creation",
184
- "creation": "creation"
184
+ "creation": "creation",
185
+ "required_change_password": "You are required to change your password"
186
}
187
}
src/langs/hfs-lang-it.json
+2
-1
@@ -173,6 +173,7 @@
173
174
"Calculate": "Calcola",
175
"Creation": "Creazione",
176
- "creation": "creazione"
176
+ "creation": "creazione",
177
+ "required_change_password": "È necessario cambiare la password"
178
}
179
}
src/perm.ts
+1
@@ -23,6 +23,7 @@ export interface Account {
23
expire?: Date
24
days_to_live?: number // this is not inherited, but it will affect sub-accounts via 'expire'
25
allow_net?: string
26
+ require_password_change?: boolean
27
}
28
interface Accounts { [username:string]: Account }
29