@valentinkolb/cloud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +69 -0
- package/public/logo.svg +1 -0
- package/scripts/build.ts +113 -0
- package/scripts/preload.ts +73 -0
- package/src/_internal/define-app.ts +399 -0
- package/src/_internal/heartbeat.ts +33 -0
- package/src/_internal/registry.ts +100 -0
- package/src/_internal/runtime-context.ts +38 -0
- package/src/api/accounts-entities.ts +134 -0
- package/src/api/admin-lifecycle.ts +210 -0
- package/src/api/auth/schemas.ts +28 -0
- package/src/api/auth.ts +230 -0
- package/src/api/index.ts +66 -0
- package/src/api/me.ts +206 -0
- package/src/api/search/schemas.ts +43 -0
- package/src/api/search.ts +130 -0
- package/src/clients/core.ts +19 -0
- package/src/config/env.ts +23 -0
- package/src/config/index.ts +6 -0
- package/src/config/ssr.ts +58 -0
- package/src/contracts/app.ts +140 -0
- package/src/contracts/index.ts +5 -0
- package/src/contracts/profile.ts +67 -0
- package/src/contracts/registry.ts +50 -0
- package/src/contracts/settings-types.ts +84 -0
- package/src/contracts/shared.ts +258 -0
- package/src/contracts/widgets.ts +121 -0
- package/src/index.ts +6 -0
- package/src/server/api/index.ts +1 -0
- package/src/server/api/respond.ts +55 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/index.ts +62 -0
- package/src/server/middleware/auth.ts +168 -0
- package/src/server/middleware/index.ts +7 -0
- package/src/server/middleware/middleware.ts +47 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +126 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/validator.ts +35 -0
- package/src/server/services/access.ts +294 -0
- package/src/server/services/freeipa/client.ts +100 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +78 -0
- package/src/server/services/freeipa/tls.ts +48 -0
- package/src/server/services/freeipa/util.ts +60 -0
- package/src/server/services/geo.ts +154 -0
- package/src/server/services/index.ts +28 -0
- package/src/server/services/services.ts +13 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +907 -0
- package/src/services/account-lifecycle/scheduler.ts +347 -0
- package/src/services/account-model.ts +21 -0
- package/src/services/accounts/app.ts +966 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +45 -0
- package/src/services/accounts/entities.ts +529 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +246 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +64 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +491 -0
- package/src/services/accounts/model.ts +135 -0
- package/src/services/accounts/switching.ts +117 -0
- package/src/services/accounts/users.ts +714 -0
- package/src/services/auth-flows/index.ts +6 -0
- package/src/services/auth-flows/ipa.ts +128 -0
- package/src/services/auth-flows/magic-link.ts +119 -0
- package/src/services/freeipa-config.ts +89 -0
- package/src/services/index.ts +46 -0
- package/src/services/ipa/auth.ts +122 -0
- package/src/services/ipa/groups.ts +684 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/profile.ts +90 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/sync.ts +740 -0
- package/src/services/ipa/users.ts +794 -0
- package/src/services/logging/index.ts +294 -0
- package/src/services/notifications/email.ts +123 -0
- package/src/services/notifications/index.ts +413 -0
- package/src/services/postgres.ts +51 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +13 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +255 -0
- package/src/services/session/index.ts +137 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +101 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +824 -0
- package/src/services/settings/index.ts +203 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +179 -0
- package/src/services/settings/templates.ts +10 -0
- package/src/services/weather/forecast.ts +287 -0
- package/src/services/weather/geo.ts +110 -0
- package/src/services/weather/index.ts +99 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +125 -0
- package/src/services/weather/migrate.ts +22 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/shared/account-display.ts +17 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/icons.ts +109 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +58 -0
- package/src/shared/markdown/extensions/images.ts +43 -0
- package/src/shared/markdown/extensions/info-blocks.ts +93 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +34 -0
- package/src/shared/markdown/extensions/tables.ts +88 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/index.ts +97 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/ssr/AdminLayout.tsx +42 -0
- package/src/ssr/AdminSidebar.tsx +95 -0
- package/src/ssr/Footer.island.tsx +62 -0
- package/src/ssr/GlobalSearchDialog.tsx +389 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
- package/src/ssr/Layout.tsx +326 -0
- package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
- package/src/ssr/NavMenu.island.tsx +108 -0
- package/src/ssr/ThemeToggleRail.island.tsx +27 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/SearchBar.island.tsx +77 -0
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/runtime.ts +22 -0
- package/src/styles/base-popover.css +28 -0
- package/src/styles/effects.css +65 -0
- package/src/styles/global.css +133 -0
- package/src/styles/input.css +54 -0
- package/src/styles/tokens.css +35 -0
- package/src/styles/utilities-buttons.css +125 -0
- package/src/styles/utilities-feedback.css +65 -0
- package/src/styles/utilities-layout.css +122 -0
- package/src/styles/utilities-navigation.css +196 -0
- package/src/types/ambient.d.ts +8 -0
- package/src/ui/admin-settings.tsx +148 -0
- package/src/ui/dialog-core.ts +146 -0
- package/src/ui/filter/FilterChip.tsx +196 -0
- package/src/ui/filter/index.ts +2 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/input/Checkbox.tsx +55 -0
- package/src/ui/input/ColorInput.tsx +122 -0
- package/src/ui/input/DateTimeInput.tsx +86 -0
- package/src/ui/input/ImageInput.tsx +170 -0
- package/src/ui/input/NumberInput.tsx +113 -0
- package/src/ui/input/PinInput.tsx +169 -0
- package/src/ui/input/SegmentedControl.tsx +99 -0
- package/src/ui/input/Select.tsx +288 -0
- package/src/ui/input/SelectChip.tsx +61 -0
- package/src/ui/input/Slider.tsx +118 -0
- package/src/ui/input/Switch.tsx +62 -0
- package/src/ui/input/TagsInput.tsx +115 -0
- package/src/ui/input/TextInput.tsx +160 -0
- package/src/ui/input/index.ts +13 -0
- package/src/ui/input/types.ts +42 -0
- package/src/ui/input/util.tsx +105 -0
- package/src/ui/ipa/Avatar.tsx +28 -0
- package/src/ui/ipa/GroupView.tsx +36 -0
- package/src/ui/ipa/LoginBtn.tsx +16 -0
- package/src/ui/ipa/UserView.tsx +58 -0
- package/src/ui/ipa/index.ts +4 -0
- package/src/ui/misc/ContextMenu.tsx +211 -0
- package/src/ui/misc/CopyButton.tsx +28 -0
- package/src/ui/misc/Dropdown.tsx +194 -0
- package/src/ui/misc/EntitySearch.tsx +213 -0
- package/src/ui/misc/Lightbox.tsx +194 -0
- package/src/ui/misc/LinkCard.tsx +34 -0
- package/src/ui/misc/LogEntriesTable.tsx +61 -0
- package/src/ui/misc/MarkdownView.tsx +65 -0
- package/src/ui/misc/Pagination.tsx +51 -0
- package/src/ui/misc/PermissionEditor.tsx +379 -0
- package/src/ui/misc/ProgressBar.tsx +47 -0
- package/src/ui/misc/RemoveBtn.tsx +27 -0
- package/src/ui/misc/StatCell.tsx +90 -0
- package/src/ui/misc/index.ts +18 -0
- package/src/ui/navigation.ts +32 -0
- package/src/ui/prompts.tsx +854 -0
- package/src/ui/sidebar.tsx +468 -0
- package/src/ui/widgets/Widget.tsx +62 -0
- package/src/ui/widgets/WidgetCard.tsx +19 -0
- package/src/ui/widgets/WidgetHero.tsx +39 -0
- package/src/ui/widgets/WidgetList.tsx +84 -0
- package/src/ui/widgets/WidgetPills.tsx +68 -0
- package/src/ui/widgets/WidgetStat.tsx +67 -0
- package/src/ui/widgets/WidgetStatus.tsx +62 -0
- package/src/ui/widgets/index.ts +9 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { sql, type SQL } from "bun";
|
|
2
|
+
import { notifications } from "../notifications";
|
|
3
|
+
import * as settings from "../settings";
|
|
4
|
+
import { renderTemplate } from "../settings/templates";
|
|
5
|
+
import { session } from "../session";
|
|
6
|
+
import { freeipa } from "../../server/services";
|
|
7
|
+
import { providers } from "../providers";
|
|
8
|
+
import { transitionIpaUserToLocal } from "./switching";
|
|
9
|
+
import {
|
|
10
|
+
canPersistStoredAdmin,
|
|
11
|
+
getDefaultAccountExpiry,
|
|
12
|
+
parseManualAccountExpiry,
|
|
13
|
+
resolveEffectiveAdminState,
|
|
14
|
+
resolveAccountExpires,
|
|
15
|
+
resolveStoredAdminState,
|
|
16
|
+
resolveTargetAccountExpiry,
|
|
17
|
+
} from "./model";
|
|
18
|
+
import { buildRoles } from "./authz";
|
|
19
|
+
import { toPgTextArray, toPgUuidArray } from "../postgres";
|
|
20
|
+
import { buildBaseUser, resolveProviderProfile } from "./base-user";
|
|
21
|
+
import {
|
|
22
|
+
managedGroupIdsSubquery,
|
|
23
|
+
managedGroupsNamesSubquery,
|
|
24
|
+
recursiveGroupIdsSubquery,
|
|
25
|
+
recursiveGroupNamesSubquery,
|
|
26
|
+
} from "./group-sql";
|
|
27
|
+
import { getFreeIpaConfig } from "../freeipa-config";
|
|
28
|
+
import type {
|
|
29
|
+
BaseUser,
|
|
30
|
+
MutationResult,
|
|
31
|
+
PaginationResponse,
|
|
32
|
+
Role,
|
|
33
|
+
User,
|
|
34
|
+
UserProfile,
|
|
35
|
+
UserProvider,
|
|
36
|
+
} from "../../contracts/shared";
|
|
37
|
+
import { buildIpaUserData, emptyIpaUserData, userIpaDataColumns, userIpaDataJoin } from "./ipa-data";
|
|
38
|
+
|
|
39
|
+
type DbRow = Record<string, unknown>;
|
|
40
|
+
type UserMutationTarget = BaseUser & { accountExpires: string | null; storedAdmin: boolean };
|
|
41
|
+
|
|
42
|
+
type CreateUserData = {
|
|
43
|
+
provider: UserProvider;
|
|
44
|
+
profile: UserProfile;
|
|
45
|
+
admin?: boolean;
|
|
46
|
+
email: string;
|
|
47
|
+
givenname: string;
|
|
48
|
+
sn: string;
|
|
49
|
+
displayName?: string;
|
|
50
|
+
autoSendNotification?: boolean;
|
|
51
|
+
requestId?: string;
|
|
52
|
+
accountExpires?: string | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type UpdateUserData = {
|
|
56
|
+
givenname?: string;
|
|
57
|
+
sn?: string;
|
|
58
|
+
displayName?: string;
|
|
59
|
+
mail?: string;
|
|
60
|
+
ipa?: {
|
|
61
|
+
phone?: string;
|
|
62
|
+
address?: {
|
|
63
|
+
street?: string;
|
|
64
|
+
postalCode?: string;
|
|
65
|
+
city?: string;
|
|
66
|
+
state?: string;
|
|
67
|
+
};
|
|
68
|
+
sshPublicKeys?: string[];
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getUserRow = async (id: string): Promise<DbRow | null> => {
|
|
73
|
+
const rows = await sql<DbRow[]>`
|
|
74
|
+
SELECT *
|
|
75
|
+
FROM auth.users
|
|
76
|
+
WHERE id = ${id}::uuid
|
|
77
|
+
`;
|
|
78
|
+
return rows[0] ?? null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sendMagicLinkEmail = async (email: string): Promise<void> => {
|
|
82
|
+
const token = await providers.local.auth.createMagicLinkToken({ email, ttlSeconds: 300 });
|
|
83
|
+
const rawAppUrl = await settings.get<string>("app.url");
|
|
84
|
+
const appUrl = rawAppUrl.startsWith("http") ? rawAppUrl : `https://${rawAppUrl}`;
|
|
85
|
+
const magicLink = `${appUrl}/auth/login?token=${token}`;
|
|
86
|
+
const appName = await settings.get<string>("app.name");
|
|
87
|
+
const template = await settings.get<string>("mail.magic_link_login");
|
|
88
|
+
|
|
89
|
+
await notifications.send({
|
|
90
|
+
type: "email",
|
|
91
|
+
recipient: email,
|
|
92
|
+
subject: `${appName} Login Code`,
|
|
93
|
+
rawHtml: renderTemplate(template, {
|
|
94
|
+
TOKEN: token,
|
|
95
|
+
MAGIC_LINK: magicLink,
|
|
96
|
+
APP_NAME: appName,
|
|
97
|
+
}),
|
|
98
|
+
autoSend: true,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const buildUserMutationTarget = (row: DbRow): UserMutationTarget => ({
|
|
103
|
+
...buildBaseUser(row),
|
|
104
|
+
accountExpires: resolveAccountExpires(row)?.toISOString() ?? null,
|
|
105
|
+
storedAdmin: Boolean(row.admin),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const buildUser = (row: DbRow, groupsAdmin: string[]): User => {
|
|
109
|
+
const { provider, profile } = resolveProviderProfile(row);
|
|
110
|
+
const displayName = (row.display_name as string) ?? "";
|
|
111
|
+
const mail = (row.mail as string) ?? null;
|
|
112
|
+
const memberofGroup = (row.member_groups as string[]) ?? [];
|
|
113
|
+
const memberofGroupIds = (row.member_group_ids as string[]) ?? [];
|
|
114
|
+
const manages = (row.manages as string[]) ?? [];
|
|
115
|
+
const managesGroupIds = (row.manages_group_ids as string[]) ?? [];
|
|
116
|
+
const roles = buildRoles({
|
|
117
|
+
provider,
|
|
118
|
+
profile,
|
|
119
|
+
memberofGroup,
|
|
120
|
+
manages,
|
|
121
|
+
admin: resolveEffectiveAdminState({
|
|
122
|
+
provider,
|
|
123
|
+
storedAdmin: Boolean(row.admin),
|
|
124
|
+
memberofGroup,
|
|
125
|
+
groupsAdmin,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
const common = {
|
|
129
|
+
id: row.id as string,
|
|
130
|
+
uid: row.uid as string,
|
|
131
|
+
roles,
|
|
132
|
+
profile,
|
|
133
|
+
givenname: (row.given_name as string) ?? "",
|
|
134
|
+
sn: (row.sn as string) ?? "",
|
|
135
|
+
displayName: displayName || (profile === "guest" && mail ? mail : ""),
|
|
136
|
+
mail,
|
|
137
|
+
accountExpires: resolveAccountExpires(row)?.toISOString() ?? null,
|
|
138
|
+
lastLoginLocal: row.last_login_local ? (row.last_login_local as Date).toISOString() : null,
|
|
139
|
+
memberofGroup,
|
|
140
|
+
memberofGroupIds,
|
|
141
|
+
manages,
|
|
142
|
+
managesGroupIds,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (provider === "ipa") {
|
|
146
|
+
return {
|
|
147
|
+
...common,
|
|
148
|
+
provider: "ipa",
|
|
149
|
+
ipa: buildIpaUserData(row) ?? emptyIpaUserData(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
...common,
|
|
155
|
+
provider: "local",
|
|
156
|
+
ipa: null,
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const get = async (params: { id: string } | { uid: string }): Promise<User | null> => {
|
|
161
|
+
const whereClause = "id" in params ? sql`u.id = ${params.id}` : sql`u.uid = ${params.uid}`;
|
|
162
|
+
const userIdExpr = "id" in params ? sql`${params.id}` : sql`u.id`;
|
|
163
|
+
const rows = await sql<DbRow[]>`
|
|
164
|
+
SELECT u.*,
|
|
165
|
+
${userIpaDataColumns},
|
|
166
|
+
COALESCE(ARRAY(
|
|
167
|
+
SELECT g.name
|
|
168
|
+
FROM auth.user_groups_v2 ug
|
|
169
|
+
JOIN auth.groups g ON g.id = ug.group_id
|
|
170
|
+
WHERE ug.user_id = u.id
|
|
171
|
+
ORDER BY g.name
|
|
172
|
+
), '{}') AS member_groups,
|
|
173
|
+
COALESCE(ARRAY(
|
|
174
|
+
SELECT ug.group_id
|
|
175
|
+
FROM auth.user_groups_v2 ug
|
|
176
|
+
JOIN auth.groups g ON g.id = ug.group_id
|
|
177
|
+
WHERE ug.user_id = u.id
|
|
178
|
+
ORDER BY g.name
|
|
179
|
+
), '{}') AS member_group_ids,
|
|
180
|
+
COALESCE(ARRAY(
|
|
181
|
+
${managedGroupsNamesSubquery(userIdExpr)}
|
|
182
|
+
), '{}') AS manages,
|
|
183
|
+
COALESCE(ARRAY(
|
|
184
|
+
${managedGroupIdsSubquery(userIdExpr)}
|
|
185
|
+
), '{}') AS manages_group_ids
|
|
186
|
+
FROM auth.users u
|
|
187
|
+
${userIpaDataJoin}
|
|
188
|
+
WHERE ${whereClause}
|
|
189
|
+
`;
|
|
190
|
+
if (rows.length === 0) return null;
|
|
191
|
+
const { groupsAdmin } = await getFreeIpaConfig();
|
|
192
|
+
return buildUser(rows[0]!, groupsAdmin);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const getMinimal = async (params: { id: string } | { uid: string }): Promise<UserMutationTarget | null> => {
|
|
196
|
+
const whereClause = "id" in params ? sql`id = ${params.id}` : sql`uid = ${params.uid}`;
|
|
197
|
+
const rows = await sql<DbRow[]>`
|
|
198
|
+
SELECT id, uid, provider, profile, admin, given_name, sn, display_name, mail, account_expires
|
|
199
|
+
FROM auth.users
|
|
200
|
+
WHERE ${whereClause}
|
|
201
|
+
`;
|
|
202
|
+
if (rows.length === 0) return null;
|
|
203
|
+
return buildUserMutationTarget(rows[0]!);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Minimal user lookup by UID. Returns id + roles WITHOUT group-derived roles.
|
|
208
|
+
* IPA admin status from group membership is NOT resolved here.
|
|
209
|
+
* Use the full `get()` for authorization decisions that depend on group-derived admin.
|
|
210
|
+
*/
|
|
211
|
+
export const getByUid = async (params: { uid: string }): Promise<{ id: string; roles: Role[] } | null> => {
|
|
212
|
+
const rows = await sql<DbRow[]>`SELECT id, provider, profile, admin FROM auth.users WHERE uid = ${params.uid}`;
|
|
213
|
+
if (rows.length === 0) return null;
|
|
214
|
+
const { provider, profile } = resolveProviderProfile(rows[0]!);
|
|
215
|
+
const roles = buildRoles({
|
|
216
|
+
provider,
|
|
217
|
+
profile,
|
|
218
|
+
memberofGroup: [],
|
|
219
|
+
manages: [],
|
|
220
|
+
admin: resolveEffectiveAdminState({
|
|
221
|
+
provider,
|
|
222
|
+
storedAdmin: Boolean(rows[0]!.admin),
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
return { id: rows[0]!.id as string, roles };
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const list = async (params: {
|
|
229
|
+
ids?: string[];
|
|
230
|
+
uids?: string[];
|
|
231
|
+
search?: string;
|
|
232
|
+
provider?: UserProvider;
|
|
233
|
+
profile?: UserProfile;
|
|
234
|
+
page?: number;
|
|
235
|
+
perPage?: number;
|
|
236
|
+
}) => {
|
|
237
|
+
const page = params.page ?? 1;
|
|
238
|
+
const perPage = params.perPage ?? 100;
|
|
239
|
+
const offset = (page - 1) * perPage;
|
|
240
|
+
const search = params.search ? `%${freeipa.util.escapeLike(params.search.toLowerCase())}%` : null;
|
|
241
|
+
const ids = params.ids;
|
|
242
|
+
const uids = params.uids;
|
|
243
|
+
|
|
244
|
+
if ((ids && ids.length === 0) || (uids && uids.length === 0)) {
|
|
245
|
+
return {
|
|
246
|
+
users: [],
|
|
247
|
+
total: 0,
|
|
248
|
+
pagination: {
|
|
249
|
+
page,
|
|
250
|
+
per_page: perPage,
|
|
251
|
+
total: 0,
|
|
252
|
+
total_pages: 0,
|
|
253
|
+
has_next: false,
|
|
254
|
+
} satisfies PaginationResponse,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const conditions: SQL.Query<unknown>[] = [sql`TRUE`];
|
|
259
|
+
if (ids) conditions.push(sql`id = ANY(${toPgUuidArray(ids)}::uuid[])`);
|
|
260
|
+
if (uids) conditions.push(sql`uid = ANY(${toPgTextArray(uids)}::text[])`);
|
|
261
|
+
if (params.provider) conditions.push(sql`provider = ${params.provider}`);
|
|
262
|
+
if (params.profile) conditions.push(sql`profile = ${params.profile}`);
|
|
263
|
+
if (search) {
|
|
264
|
+
conditions.push(sql`(
|
|
265
|
+
LOWER(uid) LIKE ${search} ESCAPE '\\' OR
|
|
266
|
+
LOWER(display_name) LIKE ${search} ESCAPE '\\' OR
|
|
267
|
+
LOWER(given_name) LIKE ${search} ESCAPE '\\' OR
|
|
268
|
+
LOWER(sn) LIKE ${search} ESCAPE '\\' OR
|
|
269
|
+
LOWER(mail) LIKE ${search} ESCAPE '\\'
|
|
270
|
+
)`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const where = conditions.reduce((acc, cond) => sql`${acc} AND ${cond}`);
|
|
274
|
+
const countRows = await sql<DbRow[]>`SELECT COUNT(*)::int AS count FROM auth.users WHERE ${where}`;
|
|
275
|
+
const total = (countRows[0]?.count as number) ?? 0;
|
|
276
|
+
const totalPages = Math.ceil(total / perPage);
|
|
277
|
+
const groupsAdmin = (await getFreeIpaConfig()).groupsAdmin;
|
|
278
|
+
const rows = await sql<DbRow[]>`
|
|
279
|
+
SELECT u.*,
|
|
280
|
+
CASE
|
|
281
|
+
WHEN u.provider = 'local' THEN u.admin
|
|
282
|
+
ELSE EXISTS(
|
|
283
|
+
SELECT 1
|
|
284
|
+
FROM auth.user_groups_v2 ug
|
|
285
|
+
JOIN auth.groups g_admin ON g_admin.id = ug.group_id
|
|
286
|
+
WHERE ug.user_id = u.id
|
|
287
|
+
AND g_admin.provider = 'ipa'
|
|
288
|
+
AND g_admin.name = ANY(${toPgTextArray(groupsAdmin)}::text[])
|
|
289
|
+
)
|
|
290
|
+
END AS effective_admin
|
|
291
|
+
FROM auth.users u
|
|
292
|
+
WHERE ${where}
|
|
293
|
+
ORDER BY uid
|
|
294
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
users: rows.map(buildBaseUser),
|
|
299
|
+
total,
|
|
300
|
+
pagination: {
|
|
301
|
+
page,
|
|
302
|
+
per_page: perPage,
|
|
303
|
+
total,
|
|
304
|
+
total_pages: totalPages,
|
|
305
|
+
has_next: page < totalPages,
|
|
306
|
+
} satisfies PaginationResponse,
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const getGroups = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
|
|
311
|
+
if (params.recursive) {
|
|
312
|
+
const rows = await sql<DbRow[]>`${recursiveGroupNamesSubquery(params.id)}`;
|
|
313
|
+
return rows.map((row) => row.name as string);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const rows = await sql<DbRow[]>`
|
|
317
|
+
SELECT g.name
|
|
318
|
+
FROM auth.user_groups_v2 ug
|
|
319
|
+
JOIN auth.groups g ON g.id = ug.group_id
|
|
320
|
+
WHERE ug.user_id = ${params.id}::uuid
|
|
321
|
+
ORDER BY g.name
|
|
322
|
+
`;
|
|
323
|
+
return rows.map((row) => row.name as string);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const getGroupIds = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
|
|
327
|
+
if (params.recursive) {
|
|
328
|
+
const rows = await sql<DbRow[]>`${recursiveGroupIdsSubquery(params.id)}`;
|
|
329
|
+
return rows.map((row) => row.group_id as string);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const rows = await sql<DbRow[]>`
|
|
333
|
+
SELECT ug.group_id
|
|
334
|
+
FROM auth.user_groups_v2 ug
|
|
335
|
+
JOIN auth.groups g ON g.id = ug.group_id
|
|
336
|
+
WHERE ug.user_id = ${params.id}::uuid
|
|
337
|
+
ORDER BY g.name
|
|
338
|
+
`;
|
|
339
|
+
return rows.map((row) => row.group_id as string);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export const getManagedGroups = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
|
|
343
|
+
if (params.recursive === false) {
|
|
344
|
+
const rows = await sql<DbRow[]>`
|
|
345
|
+
SELECT DISTINCT g.name
|
|
346
|
+
FROM auth.groups g
|
|
347
|
+
LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g.id AND gmu.user_id = ${params.id}::uuid
|
|
348
|
+
LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g.id
|
|
349
|
+
LEFT JOIN auth.user_groups_v2 ug ON ug.group_id = gmg.manager_group_id AND ug.user_id = ${params.id}::uuid
|
|
350
|
+
LEFT JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
|
|
351
|
+
WHERE gmu.user_id IS NOT NULL OR (ug.user_id IS NOT NULL AND g_manager.provider = g.provider)
|
|
352
|
+
ORDER BY g.name
|
|
353
|
+
`;
|
|
354
|
+
return rows.map((row) => row.name as string);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const rows = await sql<DbRow[]>`${managedGroupsNamesSubquery(params.id)}`;
|
|
358
|
+
return rows.map((row) => row.name as string);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Same as `getManagedGroups` but returns group IDs. Prefer this for
|
|
363
|
+
* authorization checks — group names are unique only per provider, so
|
|
364
|
+
* comparing by name can authorize a local group based on a same-named
|
|
365
|
+
* IPA group membership (or vice versa).
|
|
366
|
+
*/
|
|
367
|
+
export const getManagedGroupIds = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
|
|
368
|
+
if (params.recursive === false) {
|
|
369
|
+
const rows = await sql<DbRow[]>`
|
|
370
|
+
SELECT DISTINCT g.id
|
|
371
|
+
FROM auth.groups g
|
|
372
|
+
LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g.id AND gmu.user_id = ${params.id}::uuid
|
|
373
|
+
LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g.id
|
|
374
|
+
LEFT JOIN auth.user_groups_v2 ug ON ug.group_id = gmg.manager_group_id AND ug.user_id = ${params.id}::uuid
|
|
375
|
+
LEFT JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
|
|
376
|
+
WHERE gmu.user_id IS NOT NULL OR (ug.user_id IS NOT NULL AND g_manager.provider = g.provider)
|
|
377
|
+
ORDER BY g.id
|
|
378
|
+
`;
|
|
379
|
+
return rows.map((row) => row.id as string);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const rows = await sql<DbRow[]>`${managedGroupIdsSubquery(params.id)}`;
|
|
383
|
+
return rows.map((row) => row.id as string);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export const demoteToGuest = async (params: {
|
|
387
|
+
ipaSession?: string | null;
|
|
388
|
+
id: string;
|
|
389
|
+
actor: { userId: string; uid: string };
|
|
390
|
+
}): Promise<MutationResult<void>> => {
|
|
391
|
+
const user = await getMinimal({ id: params.id });
|
|
392
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
393
|
+
if (user.provider !== "ipa") {
|
|
394
|
+
return { ok: false, error: "Only IPA-backed accounts can be demoted to local guests", status: 400 };
|
|
395
|
+
}
|
|
396
|
+
if (!params.ipaSession) {
|
|
397
|
+
return { ok: false, error: "IPA session required to demote IPA-backed users", status: 401 };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return providers.ipa.users.demoteToGuest({
|
|
401
|
+
ipaSession: params.ipaSession,
|
|
402
|
+
id: params.id,
|
|
403
|
+
actor: params.actor,
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export const create = async (params: {
|
|
408
|
+
ipaSession?: string | null;
|
|
409
|
+
data: CreateUserData;
|
|
410
|
+
}): Promise<MutationResult<{ user: User; temporaryPassword?: string }>> => {
|
|
411
|
+
if (params.data.provider === "local" && params.data.admin && !canPersistStoredAdmin("local", params.data.profile)) {
|
|
412
|
+
return { ok: false, error: "Only local full accounts can be created as admins", status: 400 };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const accountExpires = await resolveTargetAccountExpiry({
|
|
416
|
+
provider: params.data.provider,
|
|
417
|
+
profile: params.data.profile,
|
|
418
|
+
requested: params.data.accountExpires,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (params.data.provider === "local") {
|
|
422
|
+
const created = await providers.local.users.create({
|
|
423
|
+
data: {
|
|
424
|
+
email: params.data.email,
|
|
425
|
+
givenname: params.data.givenname,
|
|
426
|
+
sn: params.data.sn,
|
|
427
|
+
displayName: params.data.displayName,
|
|
428
|
+
},
|
|
429
|
+
profile: params.data.profile,
|
|
430
|
+
accountExpires,
|
|
431
|
+
admin: params.data.admin,
|
|
432
|
+
});
|
|
433
|
+
if (!created.ok) return created;
|
|
434
|
+
const user = await get({ id: created.data.id });
|
|
435
|
+
if (!user) return { ok: false, error: "Created user not found", status: 500 };
|
|
436
|
+
return { ok: true, data: { user } };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!params.ipaSession) {
|
|
440
|
+
return { ok: false, error: "IPA session required to create IPA-backed users", status: 401 };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const created = await providers.ipa.users.create({
|
|
444
|
+
ipaSession: params.ipaSession,
|
|
445
|
+
profile: params.data.profile,
|
|
446
|
+
accountExpires,
|
|
447
|
+
data: {
|
|
448
|
+
email: params.data.email,
|
|
449
|
+
givenname: params.data.givenname,
|
|
450
|
+
sn: params.data.sn,
|
|
451
|
+
displayName: params.data.displayName,
|
|
452
|
+
autoSendNotification: params.data.autoSendNotification,
|
|
453
|
+
requestId: params.data.requestId,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
if (!created.ok) return created;
|
|
457
|
+
const user = await get({ id: created.data.id });
|
|
458
|
+
if (!user) return { ok: false, error: "Created user not found", status: 500 };
|
|
459
|
+
return {
|
|
460
|
+
ok: true,
|
|
461
|
+
data: {
|
|
462
|
+
user,
|
|
463
|
+
temporaryPassword: created.data._temporaryPassword,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export const update = async (params: {
|
|
469
|
+
ipaSession?: string | null;
|
|
470
|
+
id: string;
|
|
471
|
+
data: UpdateUserData;
|
|
472
|
+
}): Promise<MutationResult<void>> => {
|
|
473
|
+
const user = await getMinimal({ id: params.id });
|
|
474
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
475
|
+
|
|
476
|
+
if (user.provider === "ipa") {
|
|
477
|
+
if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA-backed users", status: 401 };
|
|
478
|
+
return providers.ipa.users.update({
|
|
479
|
+
ipaSession: params.ipaSession,
|
|
480
|
+
id: params.id,
|
|
481
|
+
data: params.data,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (params.data.ipa) {
|
|
486
|
+
return { ok: false, error: "IPA-only fields can only be updated for IPA-backed users", status: 400 };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return providers.local.users.update({
|
|
490
|
+
id: params.id,
|
|
491
|
+
data: params.data,
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
export const setProfile = async (params: {
|
|
496
|
+
id: string;
|
|
497
|
+
profile: UserProfile;
|
|
498
|
+
}): Promise<MutationResult<void>> => {
|
|
499
|
+
const user = await getMinimal({ id: params.id });
|
|
500
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
501
|
+
if (user.provider !== "local") {
|
|
502
|
+
return { ok: false, error: "IPA profile is derived from IPA groups and cannot be set directly", status: 400 };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const accountExpires = user.accountExpires ? new Date(user.accountExpires) : await getDefaultAccountExpiry("local", params.profile);
|
|
506
|
+
return providers.local.users.setProfile({
|
|
507
|
+
id: params.id,
|
|
508
|
+
profile: params.profile,
|
|
509
|
+
accountExpires,
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
export const setAdmin = async (params: {
|
|
514
|
+
id: string;
|
|
515
|
+
admin: boolean;
|
|
516
|
+
}): Promise<MutationResult<void>> => {
|
|
517
|
+
const user = await getMinimal({ id: params.id });
|
|
518
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
519
|
+
if (!canPersistStoredAdmin(user.provider, user.profile)) {
|
|
520
|
+
return {
|
|
521
|
+
ok: false,
|
|
522
|
+
error: user.provider === "ipa" ? "FreeIPA admin access is managed through FreeIPA groups" : "Guest accounts cannot be granted admin access",
|
|
523
|
+
status: 400,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return providers.local.users.setAdmin(params);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export const setExpiry = async (params: {
|
|
531
|
+
ipaSession?: string | null;
|
|
532
|
+
id: string;
|
|
533
|
+
expiryDate: string | null;
|
|
534
|
+
}): Promise<MutationResult<void>> => {
|
|
535
|
+
const user = await getMinimal({ id: params.id });
|
|
536
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
537
|
+
|
|
538
|
+
if (user.provider === "ipa") {
|
|
539
|
+
if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA-backed expiry", status: 401 };
|
|
540
|
+
return providers.ipa.users.setExpiry({
|
|
541
|
+
ipaSession: params.ipaSession,
|
|
542
|
+
id: params.id,
|
|
543
|
+
expiryDate: params.expiryDate,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const parsed = parseManualAccountExpiry(params.expiryDate);
|
|
548
|
+
if (!parsed.ok) return { ok: false, error: parsed.error, status: 400 };
|
|
549
|
+
|
|
550
|
+
return providers.local.users.setExpiry({
|
|
551
|
+
id: params.id,
|
|
552
|
+
profile: user.profile,
|
|
553
|
+
accountExpires: parsed.date,
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export const sendLoginLink = async (params: { id: string }): Promise<MutationResult<void>> => {
|
|
558
|
+
const user = await getMinimal({ id: params.id });
|
|
559
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
560
|
+
if (user.provider !== "local") {
|
|
561
|
+
return { ok: false, error: "Login links are only available for local accounts", status: 400 };
|
|
562
|
+
}
|
|
563
|
+
if (!user.mail) return { ok: false, error: "A local account requires an email address to receive a login link", status: 400 };
|
|
564
|
+
|
|
565
|
+
await sendMagicLinkEmail(user.mail);
|
|
566
|
+
return { ok: true, data: undefined };
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
export const createLoginToken = async (params: {
|
|
570
|
+
id: string;
|
|
571
|
+
}): Promise<MutationResult<{ token: string; magicLink: string; expiresInSeconds: number }>> => {
|
|
572
|
+
const user = await getMinimal({ id: params.id });
|
|
573
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
574
|
+
if (user.provider !== "local") {
|
|
575
|
+
return { ok: false, error: "Login tokens are only available for local accounts", status: 400 };
|
|
576
|
+
}
|
|
577
|
+
if (!user.mail) {
|
|
578
|
+
return { ok: false, error: "A local account requires an email address before a login token can be created", status: 400 };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const expiresInSeconds = 300;
|
|
582
|
+
const token = await providers.local.auth.createMagicLinkToken({
|
|
583
|
+
email: user.mail,
|
|
584
|
+
ttlSeconds: expiresInSeconds,
|
|
585
|
+
});
|
|
586
|
+
const rawAppUrl = await settings.get<string>("app.url");
|
|
587
|
+
const appUrl = rawAppUrl.startsWith("http") ? rawAppUrl : `https://${rawAppUrl}`;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
data: {
|
|
592
|
+
token,
|
|
593
|
+
magicLink: `${appUrl}/auth/login?token=${token}`,
|
|
594
|
+
expiresInSeconds,
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
export const resetPassword = async (params: {
|
|
600
|
+
ipaSession?: string | null;
|
|
601
|
+
id: string;
|
|
602
|
+
}): Promise<MutationResult<{ password: string }>> => {
|
|
603
|
+
const user = await getMinimal({ id: params.id });
|
|
604
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
605
|
+
if (user.provider !== "ipa") {
|
|
606
|
+
return { ok: false, error: "Password resets are only available for IPA-backed accounts", status: 400 };
|
|
607
|
+
}
|
|
608
|
+
if (!params.ipaSession) return { ok: false, error: "IPA session required to reset IPA-backed passwords", status: 401 };
|
|
609
|
+
|
|
610
|
+
return providers.ipa.users.resetPassword({
|
|
611
|
+
ipaSession: params.ipaSession,
|
|
612
|
+
id: params.id,
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
export const switchProvider = async (params: {
|
|
617
|
+
ipaSession?: string | null;
|
|
618
|
+
id: string;
|
|
619
|
+
provider: UserProvider;
|
|
620
|
+
}): Promise<MutationResult<void>> => {
|
|
621
|
+
const user = await getMinimal({ id: params.id });
|
|
622
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
623
|
+
const freeIpaConfig = await getFreeIpaConfig();
|
|
624
|
+
|
|
625
|
+
const currentProvider = user.provider;
|
|
626
|
+
const currentProfile = user.profile;
|
|
627
|
+
const currentExpiry = user.accountExpires ? new Date(user.accountExpires) : null;
|
|
628
|
+
|
|
629
|
+
if (currentProvider === params.provider) {
|
|
630
|
+
return { ok: false, error: `Account already uses provider '${params.provider}'`, status: 400 };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!freeIpaConfig.enabled) {
|
|
634
|
+
return { ok: false, error: "FreeIPA is disabled.", status: 400 };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!params.ipaSession) {
|
|
638
|
+
return { ok: false, error: "IPA session required to switch account providers", status: 401 };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (params.provider === "ipa") {
|
|
642
|
+
if (!user.mail) {
|
|
643
|
+
return { ok: false, error: "A local account needs an email address before it can be switched to IPA", status: 400 };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const result = await providers.ipa.users.create({
|
|
647
|
+
ipaSession: params.ipaSession,
|
|
648
|
+
profile: currentProfile,
|
|
649
|
+
accountExpires: currentExpiry,
|
|
650
|
+
data: {
|
|
651
|
+
email: user.mail,
|
|
652
|
+
givenname: user.givenname,
|
|
653
|
+
sn: user.sn,
|
|
654
|
+
displayName: user.displayName || undefined,
|
|
655
|
+
autoSendNotification: false,
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
if (!result.ok) return result;
|
|
659
|
+
return { ok: true, data: undefined };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const response = await freeipa.client.call({
|
|
663
|
+
url: freeIpaConfig.url,
|
|
664
|
+
ipaSession: params.ipaSession,
|
|
665
|
+
method: "user_del",
|
|
666
|
+
args: [user.uid],
|
|
667
|
+
options: {},
|
|
668
|
+
});
|
|
669
|
+
const ipaDeleteMessage = (response.error?.message ?? "").toLowerCase();
|
|
670
|
+
const ipaDeleteNotFound = ipaDeleteMessage.includes("not found") || ipaDeleteMessage.includes("does not exist");
|
|
671
|
+
if (response.error && !ipaDeleteNotFound) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
error: response.error.message ?? "Failed to delete user from FreeIPA",
|
|
675
|
+
status: freeipa.util.mapIpaErrorCode(response.error.code),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
await sql.begin(async (tx) => {
|
|
680
|
+
await transitionIpaUserToLocal({
|
|
681
|
+
userId: params.id,
|
|
682
|
+
targetProfile: currentProfile,
|
|
683
|
+
accountExpires: currentExpiry,
|
|
684
|
+
db: tx,
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
await session.revokeAllForUser(params.id);
|
|
689
|
+
|
|
690
|
+
return { ok: true, data: undefined };
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
export const remove = async (params: {
|
|
694
|
+
ipaSession?: string | null;
|
|
695
|
+
id: string;
|
|
696
|
+
actor: { userId: string; uid: string };
|
|
697
|
+
}): Promise<MutationResult<void>> => {
|
|
698
|
+
const user = await getMinimal({ id: params.id });
|
|
699
|
+
if (!user) return { ok: false, error: "User not found", status: 404 };
|
|
700
|
+
|
|
701
|
+
if (user.provider === "ipa") {
|
|
702
|
+
if (!params.ipaSession) return { ok: false, error: "IPA session required to delete IPA-backed users", status: 401 };
|
|
703
|
+
return providers.ipa.users.remove({
|
|
704
|
+
ipaSession: params.ipaSession,
|
|
705
|
+
id: params.id,
|
|
706
|
+
actor: params.actor,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return providers.local.users.remove({
|
|
711
|
+
id: params.id,
|
|
712
|
+
actor: params.actor,
|
|
713
|
+
});
|
|
714
|
+
};
|