ux: admin/accounts: clearer configuration of disable_password_change
Massimo Melina committed
Sep 4, 2025 at 00:37 UTC
0229b68e6185814d4a87ce034e568ccf124ec877
6 files changed
+24
-19
admin/src/AccountForm.ts
+12
-9
@@ -1,12 +1,12 @@
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 { createElement as h, ReactNode, useEffect, useRef, useState } from 'react'
4
-import { BoolField, Form, MultiSelectField, NumberField } from '@hfs/mui-grid-form'
4
+import { BoolField, Form, MultiSelectField, NumberField, SelectField } from '@hfs/mui-grid-form'
5
import { Alert, Box } from '@mui/material'
6
import { apiCall } from './api'
7
import { alertDialog, useDialogBarColors } from './dialog'
8
import { formatTimestamp, isEqualLax, prefix, reactJoin, useIsMobile, wantArray } from './misc'
9
-import { Btn, IconBtn, NetmaskField, propsForModifiedValues } from './mui'
9
+import { Btn, Flex, IconBtn, NetmaskField, propsForModifiedValues, useLogBreakpoint } from './mui'
10
import { Account } from './AccountsPage'
11
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
12
import { AutoDelete, Delete } from '@mui/icons-material'
@@ -68,7 +68,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
68
!isGroup && !pluginAuth && { k: 'password2', xs: 6, md: 4, type: 'password', autoComplete: 'new-password', label: 'Repeat password',
69
getError: (x, { values }) => (x||'') !== (values.password||'') && "Enter same password" },
70
71
- { k: 'disabled', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Enabled", xs: 12, sm: 6, lg: 8,
71
+ { k: 'disabled', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Enabled", xs: 12, sm: 6, lg: 4,
72
helperText: values.disabled || values.canLogin !== false ? "Login is prevented if account is disabled, or all its groups are disabled"
73
: h(Box, { color: 'warning.main', component: 'span' },
74
new Date(account.expire!) < new Date() ? "Login is prevented because account is expired" // use account instead of values, so to use the value currently applied
@@ -76,18 +76,21 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
76
},
77
{ k: 'ignore_limits', comp: BoolField, xs: 12, sm: 6, lg: 4,
78
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
79
-
80
- { k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: isGroup ? 6 : 4, lg: isGroup ? 8 : 4,
79
+ { k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: 6, lg: 4,
80
helperText: "To access THIS interface you are using right now",
81
...!account.admin && account.adminActualAccess && { value: true, disabled: true, helperText: "This permission is inherited. To disable it, act on the groups." },
82
},
84
- { k: 'disable_password_change', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Allow password change", xs: 12, sm: 4 },
85
- !isGroup && { k: 'require_password_change', comp: BoolField, xs: 12, sm: 4, helperText: "At first login" },
83
+ !isGroup && { k: 'require_password_change', comp: BoolField, xs: 12, sm: 6, lg: 6, helperText: "At next login" },
84
+
85
+ { k: 'disable_password_change', label: "Password change", comp: SelectField, xs: 12, sm: 6, lg: isGroup ? 4 : 6,
86
+ defaultValue: null,
87
+ options: { [`Default (${values.canChangePassword ? 'Allowed' : 'Disabled'})`]: null, "Allowed": false, "Disabled": true },
88
+ },
89
90
!members ? null
91
: isGroup && !members.length ? h(Box, {}, "No members")
89
- : members.length > 0 && h(Box, {}, `${members.length} members: `,
90
- reactJoin(', ', account.members?.map(u => h(groups.includes(u) ? 'i' : 'span', {}, u))),
92
+ : members.length > 0 && h(Flex, { gap: 0, flexWrap: 'wrap' }, `${members.length} members: `,
93
+ reactJoin(', ', account.members?.map(u => h(groups.includes(u) ? 'i' : 'span', {}, u))),
94
h(Btn, {
95
icon: Delete,
96
confirm: `Delete ${account.members.length} accounts?`,
admin/src/AccountsPage.ts
+1
@@ -157,6 +157,7 @@ export default function AccountsPage() {
157
adminActualAccess: false,
158
invalidated: undefined,
159
canLogin: true,
160
+ canChangePassword: true,
161
isGroup: sel === 'new-group',
162
members: [],
163
directMembers: [],
mui-grid-form/SelectField.ts
+2
-2
@@ -13,8 +13,8 @@ type SelectOptions<T> = { [label:string]: T } | SelectOption<T>[]
13
type SelectOption<T> = SelectOptionNormalized<T> | (T extends string | number ? T : never)
14
interface SelectOptionNormalized<T> { label?: string, value: T, disabled?: boolean }
15
16
-export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T>) {
17
- const { value, onChange, setApi, options, sx, disabled, afterList, ...rest } = props
16
+export function SelectField<T>(props: FieldProps<T> & CommonSelectProps<T> & { defaultValue?: T }) {
17
+ const { defaultValue, value=defaultValue, onChange, setApi, options, sx, disabled, afterList, ...rest } = props
18
const normalizedOptions = useMemo(() => normalizeOptions(options), [options])
19
const jsonValue = JSON.stringify(value)
20
const currentOption = normalizedOptions?.find(x => JSON.stringify(x.value) === jsonValue)
src/api.accounts.ts
+2
-1
@@ -3,7 +3,7 @@
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import {
5
Account, accountCanLoginAdmin, accountHasPassword, accounts, addAccount, delAccount, getAccount,
6
- changeSrpHelper, updateAccount, accountCanLogin
6
+ changeSrpHelper, updateAccount, accountCanLogin, accountCanChangePassword
7
} from './perm'
8
import _ from 'lodash'
9
import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_FOUND } from './const'
@@ -19,6 +19,7 @@ function prepareAccount(ac: Account | undefined) {
19
isGroup: !ac.plugin?.auth && !accountHasPassword(ac),
20
adminActualAccess: accountCanLoginAdmin(ac),
21
canLogin: accountHasPassword(ac) ? accountCanLogin(ac) : undefined,
22
+ canChangePassword: accountCanChangePassword(ac),
23
invalidated: invalidateSessionBefore.get(ac.username),
24
directMembers: Object.values(accounts.get()).filter(a => a.belongs?.includes(ac.username)).map(x => x.username),
25
members: with_(Object.values(accounts.get()), accounts => {
src/api.auth.ts
+3
-7
@@ -1,7 +1,7 @@
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 {
4
- Account, accountCanLogin, accountIsDisabled, changeSrpHelper, expandUsername, getAccount, getFromAccount
4
+ accountCanLogin, accountIsDisabled, accountCanChangePassword, changeSrpHelper, expandUsername, getAccount
5
} from './perm'
6
import { ApiError, ApiHandler } from './apiMiddleware'
7
import { SRPServerSessionStep1 } from 'tssrp6a'
@@ -112,7 +112,7 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
112
username,
113
expandedUsername: expandUsername(username),
114
adminUrl: ctxAdminAccess(ctx) ? ctx.state.revProxyPath + ADMIN_URI : undefined,
115
- canChangePassword: canChangePassword(ctx.state.account),
115
+ canChangePassword: accountCanChangePassword(ctx.state.account),
116
requireChangePassword: ctx.state.account?.require_password_change,
117
exp: keepSessionAlive.get() ? new Date(Date.now() + sessionDuration.compiled()) : undefined,
118
accountExp: ctx.state.account?.expire,
@@ -121,12 +121,8 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
121
122
export const change_my_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
123
const a = ctx.state.account
124
- return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
124
+ return !a || !accountCanChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
125
: changeSrpHelper(a, salt, verifier).then(() => {
126
delete a.require_password_change
127
})
128
}
129
-
130
-function canChangePassword(account: Account | undefined) {
131
- return account && !getFromAccount(account, a => a.disable_password_change)
132
-}
\ No newline at end of file
src/perm.ts
+4
@@ -209,6 +209,10 @@ export function accountCanLoginAdmin(account: Account) {
209
return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false
210
}
211
212
+export function accountCanChangePassword(account: Account | undefined) {
213
+ return account && !getFromAccount(account, a => a.disable_password_change)
214
+}
215
+
216
export async function changeSrpHelper(account: Account, salt: string, verifier: string) {
217
if (!salt || !verifier)
218
return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')