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