main
ts 99 lines 4.19 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 { ApiError, ApiHandlers } from './apiMiddleware'
4 import {
5 Account, accountCanLoginAdmin, accountHasLoginMethod, accountHasPassword, accounts, addAccount, delAccount, getAccount,
6 updateAccount, accountCanLogin, accountCanChangePassword, normalizeUsername
7 } from './perm'
8 import _ from 'lodash'
9 import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_FOUND } from './const'
10 import { getCurrentUsername, invalidateSessionBefore } from './auth'
11 import { apiAssertTypes, objFromKeys, onlyTruthy, with_ } from './misc'
12 import { pickProps } from './api.vfs'
13
14 function prepareAccount(ac: Account | undefined) {
15 return ac && {
16 ..._.omit(ac, ['password','hashed_password','srp']),
17 username: ac.username, // omit won't copy it because it's a hidden prop
18 hasPassword: accountHasPassword(ac),
19 isGroup: !accountHasLoginMethod(ac),
20 adminActualAccess: accountCanLoginAdmin(ac),
21 canLogin: accountHasLoginMethod(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 => {
26 const ret: string[] = []
27 let news = [ac.username]
28 while (news.length) {
29 news = accounts.filter(a => !ret.includes(a.username) && a.belongs?.some(x => news.includes(x))).map(x => x.username)
30 ret.push(...news)
31 }
32 return _.uniq(ret).sort()
33 })
34 }
35 }
36
37 const ALLOWED_KEYS: (keyof Account)[] = ['admin', 'allow_net', 'auto_login_net', 'belongs', 'days_to_live', 'disable_password_change',
38 'disabled', 'expire', 'ignore_limits', 'notes', 'password', 'redirect', 'require_password_change', 'username']
39
40 export default {
41
42 get_usernames() {
43 return { list: Object.keys(accounts.get()) }
44 },
45
46 get_account({ username }, ctx) {
47 apiAssertTypes({ string_undefined: { username } })
48 return prepareAccount(getAccount(username || getCurrentUsername(ctx)))
49 || new ApiError(HTTP_NOT_FOUND)
50 },
51
52 get_accounts() {
53 return { list: onlyTruthy(Object.values(accounts.get()).map(prepareAccount)) }
54 },
55
56 get_admins() {
57 return { list: _.filter(accounts.get(), accountCanLoginAdmin).map(ac => ac.username) }
58 },
59
60 async set_account({ username, changes }, ctx) {
61 apiAssertTypes({ string: { username } })
62 const acc = getAccount(username)
63 if (!acc)
64 return new ApiError(HTTP_BAD_REQUEST)
65 await updateAccount(acc, pickProps(changes, ALLOWED_KEYS))
66 if (changes.username && ctx.session?.username === normalizeUsername(username)) // update session if necessary
67 ctx.session!.username = normalizeUsername(changes.username)
68 return _.pick(acc, 'username')
69 },
70
71 async add_account({ overwrite, username, ...rest }) {
72 apiAssertTypes({ string: { username } })
73 const existing = getAccount(username)
74 rest = pickProps(rest, ALLOWED_KEYS)
75 if (existing) {
76 if (!overwrite) return new ApiError(HTTP_CONFLICT)
77 await updateAccount(existing, rest)
78 return _.pick(existing, 'username')
79 }
80 const acc = await addAccount(username, rest)
81 return acc ? _.pick(acc, 'username') : new ApiError(HTTP_BAD_REQUEST) // return username because it is normalized
82 },
83
84 del_account({ username }) {
85 apiAssertTypes({ string_array: { username } })
86 if (Array.isArray(username)) {
87 const errors = _.pickBy(objFromKeys(username, u => delAccount(u) ? undefined : HTTP_NOT_FOUND))
88 return _.isEmpty(errors) ? {} : { errors }
89 }
90 return delAccount(username) ? {} : new ApiError(HTTP_NOT_FOUND)
91 },
92
93 invalidate_sessions({ username }) {
94 apiAssertTypes({ string: { username } })
95 invalidateSessionBefore.set(normalizeUsername(username), Date.now())
96 return {}
97 },
98
99 } satisfies ApiHandlers