main
ts 156 lines 9.25 KB
Raw
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, 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 { apiNewPassword, formatTimestamp, isEqualLax, prefix, reactJoin, useIsMobile, wantArray } from './misc'
9 import { Btn, Flex, IconBtn, NetmaskField, propsForModifiedValues } from './mui'
10 import { Account } from './AccountsPage'
11 import { AutoDelete, Delete } from '@mui/icons-material'
12 import { state, useSnapState } from './state'
13 import VfsPathField from './VfsPathField'
14 import { DateTimeField } from './DateTimeField'
15
16 export default function AccountForm({ account, done, groups, addToBar, reload }: {
17 account: Account,
18 groups: string[],
19 done: (username: string, saveBtn?: HTMLButtonElement) => void,
20 reload: () => void,
21 addToBar: ReactNode
22 }) {
23 const { username } = useSnapState()
24 const [values, setValues] = useState<Account & { password?: string, password2?: string }>(account)
25 const [belongsOptions, setBelongOptions] = useState<string[]>([])
26 const isMobile = useIsMobile()
27 useEffect(() => {
28 setValues(account)
29 setBelongOptions(groups.filter(x => x !== account.username ))
30 if (!isMobile)
31 ref.current?.querySelector('input')?.focus()
32 }, [JSON.stringify(account)]) //eslint-disable-line
33 const add = !account.username
34 const { isGroup } = values
35 const ref = useRef<HTMLFormElement>()
36 const { members } = account
37 const pluginAuth = account.plugin?.auth
38 return h(Form, {
39 formRef: ref,
40 values,
41 set(v, k) {
42 setValues(values => ({ ...values, [k]: v }))
43 },
44 barSx: { gap: 2, width: '100%', ...useDialogBarColors() },
45 stickyBar: true,
46 addToBar: [
47 !add && h(IconBtn, {
48 icon: Delete,
49 title: "Delete",
50 confirm: `Delete ${account.username}?`,
51 ...username === account.username && { disabled: true, title: "Cannot delete current account" },
52 onClick: () => apiCall('del_account', { username: account.username }).then(reload)
53 }),
54 h(IconBtn, {
55 icon: AutoDelete,
56 title: `Invalidate past sessions${prefix('\n(already invalidated sessions before ', formatTimestamp(account.invalidated || 0), ')')}`,
57 confirm: `Invalidate all sessions up to now for "${account.username}"?`,
58 doneMessage: true,
59 onClick: () => apiCall('invalidate_sessions', { username: account.username }).then(reload)
60 }),
61 ...wantArray(addToBar),
62 ],
63 fields: [
64 { k: 'username', label: isGroup ? 'Group name' : undefined, autoComplete: 'off', required: true, md: isGroup && !pluginAuth ? 12 : 4,
65 getError: v => v !== account.username && apiCall('get_account', { username: v })
66 .then(got => got?.username === account.username ? "usernames are case-insensitive" : "already used", () => false),
67 },
68 pluginAuth && { k: '', md: 8, comp: h(Alert, { severity: 'info' }, " Authentication handled by a plugin") },
69 !isGroup && !pluginAuth && { k: 'password', xs: 6, md: 4, type: 'password', autoComplete: 'new-password', required: add,
70 label: add ? "Password" : "Change password"
71 },
72 !isGroup && !pluginAuth && { k: 'password2', xs: 6, md: 4, type: 'password', autoComplete: 'new-password', label: 'Repeat password',
73 getError: (x, { values }) => (x||'') !== (values.password||'') && "Enter same password" },
74
75 { k: 'disabled', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Enabled", xs: 12, sm: 6, lg: 4,
76 helperText: values.disabled || values.canLogin !== false ? "Login is prevented if account is disabled, or all its groups are disabled"
77 : h(Box, { sx: { color: 'warning.main' }, component: 'span' } as any, // Box.component has ts problems with h()
78 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
79 : "Login is prevented because all of its groups are disabled")
80 },
81 { k: 'ignore_limits', comp: BoolField, xs: 12, sm: 6, lg: 4,
82 helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
83 { k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: 6, lg: 4,
84 helperText: "To access THIS interface you are using right now",
85 ...!account.admin && account.adminActualAccess && { value: true, disabled: true, helperText: "This permission is inherited. To disable it, act on the groups." },
86 },
87
88 !isGroup && { k: 'require_password_change', comp: BoolField, xs: 12, sm: 6, lg: 4, helperText: "At next login" },
89 { k: 'disable_password_change', label: "Password change", comp: SelectField, xs: 12, sm: 6, lg: isGroup ? 4 : 4,
90 defaultValue: null,
91 options: { [`Default (${values.canChangePassword ? 'Allowed' : 'Disabled'})`]: null, "Allowed": false, "Disabled": true },
92 },
93
94 !members ? null
95 : isGroup && !members.length ? h(Box, {}, "No members")
96 : members.length > 0 && h(Flex, { gap: 0, flexWrap: 'wrap' }, `${members.length} members: `,
97 reactJoin('', account.members?.map(u => h(groups.includes(u) ? 'i' : 'span', {}, u))),
98 h(Btn, {
99 icon: Delete,
100 confirm: `Delete ${account.members.length} accounts?`,
101 onClick: () => apiCall('del_account', { username: account.members }).then(reload),
102 sx: { verticalAlign: 'text-top' }
103 }),
104 ),
105 isGroup && h(Alert, { severity: 'info' }, `To add users to this group, select the user and then click "Inherit"`),
106 { k: 'belongs', comp: MultiSelectField, label: "Inherit from groups", options: belongsOptions, sm: 6, lg: 4,
107 helperText: "Specify groups to inherit permissions from"
108 + (!isGroup ? '' : ". A group can inherit from another group")
109 + (belongsOptions.length ? '' : ". Now disabled because there are no groups to select, create one first.")
110 },
111
112 { k: 'allow_net', comp: NetmaskField, label: "Allowed network address", sm: 6, lg: 4, placeholder: "any address" },
113 !isGroup && { k: 'auto_login_net', comp: NetmaskField, label: "Auto-login by IP address", sm: 6, lg: 4, placeholder: "none" },
114 { k: 'redirect', comp: VfsPathField, placeholder: "no", sm: 6, lg: 4,
115 helperText: "If you want this account to be redirected to a specific folder/address (or even file) at login time" },
116
117 { k: 'expire', label: "Expiration", sm: 6, lg: 4, comp: DateTimeField, toField: x => x && new Date(x),
118 helperText: "When expired, login won't be allowed" },
119 { k: 'days_to_live', sm: 6, lg: 4, comp: NumberField, step: 'any', min: 1/1000, // 10 minutes
120 ...values.expire && { xs: 12, disabled: true, sx: { opacity: .2 } }, helperText: "Used to set expiration on first login" },
121 { k: 'notes', multiline: true, sm: 6, lg: 4 },
122 ],
123 onError: alertDialog,
124 save: {
125 ...propsForModifiedValues(isModifiedConfig(values, account)),
126 async onClick() {
127 const { password='', password2, adminActualAccess, hasPassword, invalidated, canLogin, members, ...withoutPassword } = values
128 const saveBtn = ref.current?.querySelector<HTMLButtonElement>('button.saveBtn') || undefined
129 if (add) {
130 const got = await apiCall('add_account', withoutPassword)
131 if (password)
132 try { await apiNewPassword(values.username, password) }
133 catch(e) {
134 void apiCall('del_account', { username: values.username }) // best effort, don't wait
135 throw e
136 }
137 done(got?.username, saveBtn)
138 return
139 }
140 const got = await apiCall('set_account', {
141 username: account.username,
142 changes: withoutPassword,
143 })
144 if (password)
145 await apiNewPassword(values.username, password)
146 if (account.username === username)
147 state.username = values.username
148 done(got?.username, saveBtn) // username may have been changed, so we pass it back
149 }
150 }
151 })
152 }
153
154 export function isModifiedConfig(a: any, b: any) {
155 return !isEqualLax(a, b, (a,b) => !a && !b || undefined)
156 }