@samitouri / QOSami-HFS / commits / 0229b68e

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')