@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.
Files changed (196) hide show
  1. package/package.json +69 -0
  2. package/public/logo.svg +1 -0
  3. package/scripts/build.ts +113 -0
  4. package/scripts/preload.ts +73 -0
  5. package/src/_internal/define-app.ts +399 -0
  6. package/src/_internal/heartbeat.ts +33 -0
  7. package/src/_internal/registry.ts +100 -0
  8. package/src/_internal/runtime-context.ts +38 -0
  9. package/src/api/accounts-entities.ts +134 -0
  10. package/src/api/admin-lifecycle.ts +210 -0
  11. package/src/api/auth/schemas.ts +28 -0
  12. package/src/api/auth.ts +230 -0
  13. package/src/api/index.ts +66 -0
  14. package/src/api/me.ts +206 -0
  15. package/src/api/search/schemas.ts +43 -0
  16. package/src/api/search.ts +130 -0
  17. package/src/clients/core.ts +19 -0
  18. package/src/config/env.ts +23 -0
  19. package/src/config/index.ts +6 -0
  20. package/src/config/ssr.ts +58 -0
  21. package/src/contracts/app.ts +140 -0
  22. package/src/contracts/index.ts +5 -0
  23. package/src/contracts/profile.ts +67 -0
  24. package/src/contracts/registry.ts +50 -0
  25. package/src/contracts/settings-types.ts +84 -0
  26. package/src/contracts/shared.ts +258 -0
  27. package/src/contracts/widgets.ts +121 -0
  28. package/src/index.ts +6 -0
  29. package/src/server/api/index.ts +1 -0
  30. package/src/server/api/respond.ts +55 -0
  31. package/src/server/api-client.ts +54 -0
  32. package/src/server/app-context.ts +39 -0
  33. package/src/server/index.ts +62 -0
  34. package/src/server/middleware/auth.ts +168 -0
  35. package/src/server/middleware/index.ts +7 -0
  36. package/src/server/middleware/middleware.ts +47 -0
  37. package/src/server/middleware/openapi.ts +126 -0
  38. package/src/server/middleware/rate-limit.ts +126 -0
  39. package/src/server/middleware/request-logger.ts +41 -0
  40. package/src/server/middleware/validator.ts +35 -0
  41. package/src/server/services/access.ts +294 -0
  42. package/src/server/services/freeipa/client.ts +100 -0
  43. package/src/server/services/freeipa/index.ts +9 -0
  44. package/src/server/services/freeipa/session.ts +78 -0
  45. package/src/server/services/freeipa/tls.ts +48 -0
  46. package/src/server/services/freeipa/util.ts +60 -0
  47. package/src/server/services/geo.ts +154 -0
  48. package/src/server/services/index.ts +28 -0
  49. package/src/server/services/services.ts +13 -0
  50. package/src/services/account-lifecycle/audit.ts +41 -0
  51. package/src/services/account-lifecycle/index.ts +907 -0
  52. package/src/services/account-lifecycle/scheduler.ts +347 -0
  53. package/src/services/account-model.ts +21 -0
  54. package/src/services/accounts/app.ts +966 -0
  55. package/src/services/accounts/authz.ts +22 -0
  56. package/src/services/accounts/base-group.ts +11 -0
  57. package/src/services/accounts/base-user.ts +45 -0
  58. package/src/services/accounts/entities.ts +529 -0
  59. package/src/services/accounts/group-sql.ts +106 -0
  60. package/src/services/accounts/groups.ts +246 -0
  61. package/src/services/accounts/index.ts +14 -0
  62. package/src/services/accounts/ipa-data.ts +64 -0
  63. package/src/services/accounts/lifecycle.ts +2 -0
  64. package/src/services/accounts/local-groups.ts +491 -0
  65. package/src/services/accounts/model.ts +135 -0
  66. package/src/services/accounts/switching.ts +117 -0
  67. package/src/services/accounts/users.ts +714 -0
  68. package/src/services/auth-flows/index.ts +6 -0
  69. package/src/services/auth-flows/ipa.ts +128 -0
  70. package/src/services/auth-flows/magic-link.ts +119 -0
  71. package/src/services/freeipa-config.ts +89 -0
  72. package/src/services/index.ts +46 -0
  73. package/src/services/ipa/auth.ts +122 -0
  74. package/src/services/ipa/groups.ts +684 -0
  75. package/src/services/ipa/guard.ts +17 -0
  76. package/src/services/ipa/index.ts +17 -0
  77. package/src/services/ipa/profile.ts +90 -0
  78. package/src/services/ipa/search.ts +154 -0
  79. package/src/services/ipa/sync.ts +740 -0
  80. package/src/services/ipa/users.ts +794 -0
  81. package/src/services/logging/index.ts +294 -0
  82. package/src/services/notifications/email.ts +123 -0
  83. package/src/services/notifications/index.ts +413 -0
  84. package/src/services/postgres.ts +51 -0
  85. package/src/services/providers/index.ts +27 -0
  86. package/src/services/providers/local/auth.ts +13 -0
  87. package/src/services/providers/local/index.ts +4 -0
  88. package/src/services/providers/local/users.ts +255 -0
  89. package/src/services/session/index.ts +137 -0
  90. package/src/services/settings/api.ts +61 -0
  91. package/src/services/settings/app.ts +101 -0
  92. package/src/services/settings/crypto.ts +69 -0
  93. package/src/services/settings/defaults.ts +824 -0
  94. package/src/services/settings/index.ts +203 -0
  95. package/src/services/settings/namespace.ts +9 -0
  96. package/src/services/settings/snapshot.ts +49 -0
  97. package/src/services/settings/store.ts +179 -0
  98. package/src/services/settings/templates.ts +10 -0
  99. package/src/services/weather/forecast.ts +287 -0
  100. package/src/services/weather/geo.ts +110 -0
  101. package/src/services/weather/index.ts +99 -0
  102. package/src/services/weather/location.ts +24 -0
  103. package/src/services/weather/locations.ts +125 -0
  104. package/src/services/weather/migrate.ts +22 -0
  105. package/src/services/weather/types.ts +61 -0
  106. package/src/services/weather/ui.ts +50 -0
  107. package/src/shared/account-display.ts +17 -0
  108. package/src/shared/account-session.ts +15 -0
  109. package/src/shared/icons.ts +109 -0
  110. package/src/shared/index.ts +10 -0
  111. package/src/shared/markdown/client.ts +130 -0
  112. package/src/shared/markdown/extensions/code.ts +58 -0
  113. package/src/shared/markdown/extensions/images.ts +43 -0
  114. package/src/shared/markdown/extensions/info-blocks.ts +93 -0
  115. package/src/shared/markdown/extensions/katex.ts +120 -0
  116. package/src/shared/markdown/extensions/links.ts +34 -0
  117. package/src/shared/markdown/extensions/tables.ts +88 -0
  118. package/src/shared/markdown/extensions/task-list.ts +53 -0
  119. package/src/shared/markdown/index.ts +97 -0
  120. package/src/shared/markdown/shared.ts +36 -0
  121. package/src/ssr/AdminLayout.tsx +42 -0
  122. package/src/ssr/AdminSidebar.tsx +95 -0
  123. package/src/ssr/Footer.island.tsx +62 -0
  124. package/src/ssr/GlobalSearchDialog.tsx +389 -0
  125. package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
  126. package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
  127. package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
  128. package/src/ssr/Layout.tsx +326 -0
  129. package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
  130. package/src/ssr/NavMenu.island.tsx +108 -0
  131. package/src/ssr/ThemeToggleRail.island.tsx +27 -0
  132. package/src/ssr/index.ts +5 -0
  133. package/src/ssr/islands/SearchBar.island.tsx +77 -0
  134. package/src/ssr/islands/index.ts +1 -0
  135. package/src/ssr/runtime.ts +22 -0
  136. package/src/styles/base-popover.css +28 -0
  137. package/src/styles/effects.css +65 -0
  138. package/src/styles/global.css +133 -0
  139. package/src/styles/input.css +54 -0
  140. package/src/styles/tokens.css +35 -0
  141. package/src/styles/utilities-buttons.css +125 -0
  142. package/src/styles/utilities-feedback.css +65 -0
  143. package/src/styles/utilities-layout.css +122 -0
  144. package/src/styles/utilities-navigation.css +196 -0
  145. package/src/types/ambient.d.ts +8 -0
  146. package/src/ui/admin-settings.tsx +148 -0
  147. package/src/ui/dialog-core.ts +146 -0
  148. package/src/ui/filter/FilterChip.tsx +196 -0
  149. package/src/ui/filter/index.ts +2 -0
  150. package/src/ui/index.ts +19 -0
  151. package/src/ui/input/Checkbox.tsx +55 -0
  152. package/src/ui/input/ColorInput.tsx +122 -0
  153. package/src/ui/input/DateTimeInput.tsx +86 -0
  154. package/src/ui/input/ImageInput.tsx +170 -0
  155. package/src/ui/input/NumberInput.tsx +113 -0
  156. package/src/ui/input/PinInput.tsx +169 -0
  157. package/src/ui/input/SegmentedControl.tsx +99 -0
  158. package/src/ui/input/Select.tsx +288 -0
  159. package/src/ui/input/SelectChip.tsx +61 -0
  160. package/src/ui/input/Slider.tsx +118 -0
  161. package/src/ui/input/Switch.tsx +62 -0
  162. package/src/ui/input/TagsInput.tsx +115 -0
  163. package/src/ui/input/TextInput.tsx +160 -0
  164. package/src/ui/input/index.ts +13 -0
  165. package/src/ui/input/types.ts +42 -0
  166. package/src/ui/input/util.tsx +105 -0
  167. package/src/ui/ipa/Avatar.tsx +28 -0
  168. package/src/ui/ipa/GroupView.tsx +36 -0
  169. package/src/ui/ipa/LoginBtn.tsx +16 -0
  170. package/src/ui/ipa/UserView.tsx +58 -0
  171. package/src/ui/ipa/index.ts +4 -0
  172. package/src/ui/misc/ContextMenu.tsx +211 -0
  173. package/src/ui/misc/CopyButton.tsx +28 -0
  174. package/src/ui/misc/Dropdown.tsx +194 -0
  175. package/src/ui/misc/EntitySearch.tsx +213 -0
  176. package/src/ui/misc/Lightbox.tsx +194 -0
  177. package/src/ui/misc/LinkCard.tsx +34 -0
  178. package/src/ui/misc/LogEntriesTable.tsx +61 -0
  179. package/src/ui/misc/MarkdownView.tsx +65 -0
  180. package/src/ui/misc/Pagination.tsx +51 -0
  181. package/src/ui/misc/PermissionEditor.tsx +379 -0
  182. package/src/ui/misc/ProgressBar.tsx +47 -0
  183. package/src/ui/misc/RemoveBtn.tsx +27 -0
  184. package/src/ui/misc/StatCell.tsx +90 -0
  185. package/src/ui/misc/index.ts +18 -0
  186. package/src/ui/navigation.ts +32 -0
  187. package/src/ui/prompts.tsx +854 -0
  188. package/src/ui/sidebar.tsx +468 -0
  189. package/src/ui/widgets/Widget.tsx +62 -0
  190. package/src/ui/widgets/WidgetCard.tsx +19 -0
  191. package/src/ui/widgets/WidgetHero.tsx +39 -0
  192. package/src/ui/widgets/WidgetList.tsx +84 -0
  193. package/src/ui/widgets/WidgetPills.tsx +68 -0
  194. package/src/ui/widgets/WidgetStat.tsx +67 -0
  195. package/src/ui/widgets/WidgetStatus.tsx +62 -0
  196. package/src/ui/widgets/index.ts +9 -0
@@ -0,0 +1,106 @@
1
+ import { sql, type SQLQuery } from "bun";
2
+
3
+ type SqlFragment = SQLQuery;
4
+ type SqlValue = SQLQuery | string;
5
+
6
+ export const recursiveUserGroupsSubquery = (params: { userId: SqlValue; select: SqlFragment }) => sql`
7
+ WITH RECURSIVE user_all_groups AS (
8
+ SELECT ug.group_id, g.provider
9
+ FROM auth.user_groups_v2 ug
10
+ JOIN auth.groups g ON g.id = ug.group_id
11
+ WHERE ug.user_id = ${params.userId}::uuid
12
+ UNION
13
+ SELECT gg.parent_group_id, g_parent.provider
14
+ FROM auth.group_groups_v2 gg
15
+ JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
16
+ JOIN user_all_groups ag ON gg.child_group_id = ag.group_id
17
+ WHERE g_parent.provider = ag.provider
18
+ )
19
+ ${params.select}
20
+ `;
21
+
22
+ export const managedGroupsNamesSubquery = (userId: SqlValue) =>
23
+ recursiveUserGroupsSubquery({
24
+ userId,
25
+ select: sql`
26
+ SELECT DISTINCT g.name
27
+ FROM auth.groups g
28
+ LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g.id AND gmu.user_id = ${userId}::uuid
29
+ LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g.id
30
+ LEFT JOIN user_all_groups ug ON ug.group_id = gmg.manager_group_id AND ug.provider = g.provider
31
+ WHERE gmu.user_id IS NOT NULL OR ug.group_id IS NOT NULL
32
+ ORDER BY g.name
33
+ `,
34
+ });
35
+
36
+ export const managedGroupIdsSubquery = (userId: SqlValue) =>
37
+ recursiveUserGroupsSubquery({
38
+ userId,
39
+ select: sql`
40
+ SELECT managed.id
41
+ FROM (
42
+ SELECT DISTINCT g.id, g.name
43
+ FROM auth.groups g
44
+ LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g.id AND gmu.user_id = ${userId}::uuid
45
+ LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g.id
46
+ LEFT JOIN user_all_groups ug ON ug.group_id = gmg.manager_group_id AND ug.provider = g.provider
47
+ WHERE gmu.user_id IS NOT NULL OR ug.group_id IS NOT NULL
48
+ ) managed
49
+ ORDER BY managed.name
50
+ `,
51
+ });
52
+
53
+ export const recursiveGroupNamesSubquery = (userId: SqlValue) =>
54
+ recursiveUserGroupsSubquery({
55
+ userId,
56
+ select: sql`
57
+ SELECT DISTINCT g.name
58
+ FROM user_all_groups ag
59
+ JOIN auth.groups g ON g.id = ag.group_id
60
+ ORDER BY g.name
61
+ `,
62
+ });
63
+
64
+ export const recursiveGroupIdsSubquery = (userId: SqlValue) =>
65
+ recursiveUserGroupsSubquery({
66
+ userId,
67
+ select: sql`
68
+ SELECT group_ids.group_id
69
+ FROM (
70
+ SELECT DISTINCT g.id AS group_id, g.name
71
+ FROM user_all_groups ag
72
+ JOIN auth.groups g ON g.id = ag.group_id
73
+ ) group_ids
74
+ ORDER BY group_ids.name
75
+ `,
76
+ });
77
+
78
+ export const buildMemberGroupScopeCondition = (params: { userId: SqlValue; groupProvider: SqlFragment }) => sql`
79
+ g.id IN (
80
+ ${recursiveUserGroupsSubquery({
81
+ userId: params.userId,
82
+ select: sql`
83
+ SELECT DISTINCT ug.group_id
84
+ FROM user_all_groups ug
85
+ WHERE ug.provider = ${params.groupProvider}
86
+ `,
87
+ })}
88
+ )
89
+ `;
90
+
91
+ export const buildManagedGroupScopeCondition = (params: { userId: SqlValue; groupProvider: SqlFragment }) => sql`
92
+ g.id IN (
93
+ ${recursiveUserGroupsSubquery({
94
+ userId: params.userId,
95
+ select: sql`
96
+ SELECT DISTINCT g_manage.id
97
+ FROM auth.groups g_manage
98
+ LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g_manage.id AND gmu.user_id = ${params.userId}::uuid
99
+ LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g_manage.id
100
+ LEFT JOIN user_all_groups ug ON ug.group_id = gmg.manager_group_id AND ug.provider = g_manage.provider
101
+ WHERE g_manage.provider = ${params.groupProvider}
102
+ AND (gmu.user_id IS NOT NULL OR ug.group_id IS NOT NULL)
103
+ `,
104
+ })}
105
+ )
106
+ `;
@@ -0,0 +1,246 @@
1
+ import { sql } from "bun";
2
+ import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
3
+ import * as localGroups from "./local-groups";
4
+ import { providers } from "../providers";
5
+ import { freeipa } from "../../server/services";
6
+ import { toPgUuidArray } from "../postgres";
7
+ import { buildBaseGroup } from "./base-group";
8
+ import {
9
+ buildManagedGroupScopeCondition,
10
+ buildMemberGroupScopeCondition,
11
+ } from "./group-sql";
12
+
13
+ type DbRow = Record<string, unknown>;
14
+
15
+ type GroupListScope = "all" | "member" | "managed";
16
+
17
+ const getGroup = async (id: string): Promise<BaseGroup | null> => {
18
+ const [row] = await sql<DbRow[]>`
19
+ SELECT id, provider, name, description, gid_number
20
+ FROM auth.groups
21
+ WHERE id = ${id}::uuid
22
+ `;
23
+ if (!row) return null;
24
+ return buildBaseGroup(row);
25
+ };
26
+
27
+ const listCanonical = async (params: {
28
+ ids?: string[];
29
+ userId?: string;
30
+ scope?: GroupListScope;
31
+ search?: string;
32
+ provider?: UserProvider;
33
+ page?: number;
34
+ perPage?: number;
35
+ }): Promise<{
36
+ groups: BaseGroup[];
37
+ total: number;
38
+ pagination: { page: number; perPage: number; totalPages: number; hasNext: boolean };
39
+ }> => {
40
+ const page = params.page ?? 1;
41
+ const perPage = params.perPage ?? 100;
42
+ const offset = (page - 1) * perPage;
43
+ const pattern = params.search ? `%${freeipa.util.escapeLike(params.search.toLowerCase())}%` : null;
44
+ const ids = params.ids ?? [];
45
+ const scope = params.scope ?? (params.userId ? "member" : "all");
46
+ const scopeUserId = params.userId ?? "00000000-0000-0000-0000-000000000000";
47
+ const idsCondition = ids.length === 0 ? sql`TRUE` : sql`g.id = ANY(${toPgUuidArray(ids)}::uuid[])`;
48
+
49
+ if (params.ids && params.ids.length === 0) {
50
+ return {
51
+ groups: [],
52
+ total: 0,
53
+ pagination: {
54
+ page,
55
+ perPage,
56
+ totalPages: 0,
57
+ hasNext: false,
58
+ },
59
+ };
60
+ }
61
+
62
+ const rows = await sql<DbRow[]>`
63
+ SELECT g.id, g.provider, g.name, g.description, g.gid_number, COUNT(*) OVER() AS total
64
+ FROM auth.groups g
65
+ WHERE (${params.provider ?? null}::text IS NULL OR g.provider = ${params.provider ?? null})
66
+ AND ${idsCondition}
67
+ AND (
68
+ ${scope === "all"} = true
69
+ OR ${params.userId ?? null}::uuid IS NULL
70
+ OR (${scope === "member"} = true AND ${buildMemberGroupScopeCondition({ userId: scopeUserId, groupProvider: sql`g.provider` })})
71
+ OR (${scope === "managed"} = true AND ${buildManagedGroupScopeCondition({ userId: scopeUserId, groupProvider: sql`g.provider` })})
72
+ )
73
+ AND (
74
+ ${pattern}::text IS NULL
75
+ OR LOWER(g.name) LIKE ${pattern} ESCAPE '\\'
76
+ OR LOWER(COALESCE(g.description, '')) LIKE ${pattern} ESCAPE '\\'
77
+ )
78
+ ORDER BY g.name
79
+ LIMIT ${perPage}
80
+ OFFSET ${offset}
81
+ `;
82
+
83
+ const total = rows.length > 0 ? Number((rows[0] as Record<string, unknown>).total) : 0;
84
+ return {
85
+ groups: rows.map(buildBaseGroup),
86
+ total,
87
+ pagination: {
88
+ page,
89
+ perPage,
90
+ totalPages: Math.ceil(total / perPage),
91
+ hasNext: page * perPage < total,
92
+ },
93
+ };
94
+ };
95
+
96
+ export const list = async (params: {
97
+ ids?: string[];
98
+ userId?: string;
99
+ scope?: GroupListScope;
100
+ search?: string;
101
+ provider?: UserProvider;
102
+ page?: number;
103
+ perPage?: number;
104
+ }) => {
105
+ return listCanonical(params);
106
+ };
107
+
108
+ export const get = async (params: { id: string }): Promise<BaseGroup | null> => {
109
+ return getGroup(params.id);
110
+ };
111
+
112
+ export const getMembers = async (params: { id: string; provider?: UserProvider; type?: "user" | "group"; recursive?: boolean }): Promise<GroupMember[]> => {
113
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
114
+ if (provider === "local") return localGroups.getMembers(params);
115
+ if (!provider) return [];
116
+ return providers.ipa.groups.getMembers(params);
117
+ };
118
+
119
+ export const getManagers = async (params: { id: string; provider?: UserProvider; type?: "user" | "group"; recursive?: boolean }): Promise<GroupMember[]> => {
120
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
121
+ if (provider === "local") return localGroups.getManagers(params);
122
+ if (!provider) return [];
123
+ return providers.ipa.groups.getManagers(params);
124
+ };
125
+
126
+ export const getParents = async (params: { id: string; provider?: UserProvider; recursive?: boolean }): Promise<string[]> => {
127
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
128
+ if (provider === "local") return localGroups.getParents(params);
129
+ if (!provider) return [];
130
+ return providers.ipa.groups.getParents(params);
131
+ };
132
+
133
+ export const getManagedGroups = async (params: { id: string; provider?: UserProvider }): Promise<string[]> => {
134
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
135
+ if (provider === "local") return localGroups.getManagedGroups(params);
136
+ if (!provider) return [];
137
+ return providers.ipa.groups.getManagedGroups(params);
138
+ };
139
+
140
+ export const create = async (params: {
141
+ ipaSession?: string | null;
142
+ provider: UserProvider;
143
+ name: string;
144
+ description?: string;
145
+ posix?: boolean;
146
+ }): Promise<MutationResult<BaseGroup>> => {
147
+ if (params.provider === "local") {
148
+ if (params.posix) return { ok: false, error: "Local groups do not support POSIX mode", status: 400 };
149
+ return localGroups.create({ name: params.name, description: params.description });
150
+ }
151
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to create IPA groups", status: 401 };
152
+ return providers.ipa.groups.add({
153
+ ipaSession: params.ipaSession,
154
+ cn: params.name,
155
+ description: params.description,
156
+ posix: params.posix,
157
+ });
158
+ };
159
+
160
+ export const update = async (params: {
161
+ ipaSession?: string | null;
162
+ id: string;
163
+ provider?: UserProvider;
164
+ description: string;
165
+ }): Promise<MutationResult<void>> => {
166
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
167
+ if (provider === "local") return localGroups.update({ id: params.id, description: params.description });
168
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA groups", status: 401 };
169
+ return providers.ipa.groups.update({
170
+ ipaSession: params.ipaSession,
171
+ id: params.id,
172
+ description: params.description,
173
+ });
174
+ };
175
+
176
+ export const remove = async (params: { ipaSession?: string | null; id: string; provider?: UserProvider }): Promise<MutationResult<void>> => {
177
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
178
+ if (provider === "local") return localGroups.remove({ id: params.id });
179
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to delete IPA groups", status: 401 };
180
+ return providers.ipa.groups.remove({
181
+ ipaSession: params.ipaSession,
182
+ id: params.id,
183
+ });
184
+ };
185
+
186
+ export const makePosix = async (params: {
187
+ ipaSession?: string | null;
188
+ id: string;
189
+ provider?: UserProvider;
190
+ }): Promise<MutationResult<{ gidnumber: number | null }>> => {
191
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
192
+ if (provider === "local") return { ok: false, error: "Local groups do not support POSIX mode", status: 400 };
193
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to change IPA groups", status: 401 };
194
+ return providers.ipa.groups.makePosix({
195
+ ipaSession: params.ipaSession,
196
+ id: params.id,
197
+ });
198
+ };
199
+
200
+ export const addMember = async (params: { ipaSession?: string | null; id: string; provider?: UserProvider; user?: string; group?: string }): Promise<MutationResult<void>> => {
201
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
202
+ if (provider === "local") return localGroups.addMember({ id: params.id, user: params.user, group: params.group });
203
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA groups", status: 401 };
204
+ return providers.ipa.groups.addMember({
205
+ ipaSession: params.ipaSession,
206
+ id: params.id,
207
+ user: params.user,
208
+ group: params.group,
209
+ });
210
+ };
211
+
212
+ export const removeMember = async (params: { ipaSession?: string | null; id: string; provider?: UserProvider; user?: string; group?: string }): Promise<MutationResult<void>> => {
213
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
214
+ if (provider === "local") return localGroups.removeMember({ id: params.id, user: params.user, group: params.group });
215
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA groups", status: 401 };
216
+ return providers.ipa.groups.removeMember({
217
+ ipaSession: params.ipaSession,
218
+ id: params.id,
219
+ user: params.user,
220
+ group: params.group,
221
+ });
222
+ };
223
+
224
+ export const addManager = async (params: { ipaSession?: string | null; id: string; provider?: UserProvider; user?: string; group?: string }): Promise<MutationResult<void>> => {
225
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
226
+ if (provider === "local") return localGroups.addManager({ id: params.id, user: params.user, group: params.group });
227
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA groups", status: 401 };
228
+ return providers.ipa.groups.addManager({
229
+ ipaSession: params.ipaSession,
230
+ id: params.id,
231
+ user: params.user,
232
+ group: params.group,
233
+ });
234
+ };
235
+
236
+ export const removeManager = async (params: { ipaSession?: string | null; id: string; provider?: UserProvider; user?: string; group?: string }): Promise<MutationResult<void>> => {
237
+ const provider = params.provider ?? (await getGroup(params.id))?.provider;
238
+ if (provider === "local") return localGroups.removeManager({ id: params.id, user: params.user, group: params.group });
239
+ if (!params.ipaSession) return { ok: false, error: "IPA session required to update IPA groups", status: 401 };
240
+ return providers.ipa.groups.removeManager({
241
+ ipaSession: params.ipaSession,
242
+ id: params.id,
243
+ user: params.user,
244
+ group: params.group,
245
+ });
246
+ };
@@ -0,0 +1,14 @@
1
+ import * as model from "./model";
2
+ import * as authz from "./authz";
3
+ import * as users from "./users";
4
+ import * as groups from "./groups";
5
+ import * as entities from "./entities";
6
+ import * as localGroups from "./local-groups";
7
+ import * as switching from "./switching";
8
+ import * as lifecycle from "./lifecycle";
9
+ import { accountsAppService } from "./app";
10
+
11
+ export { model, authz, users, groups, entities, localGroups, switching, lifecycle };
12
+ export { accountsAppService };
13
+
14
+ export const accounts = { model, authz, users, groups, entities, localGroups, switching, lifecycle, app: accountsAppService } as const;
@@ -0,0 +1,64 @@
1
+ import { sql } from "bun";
2
+ import type { IpaUserData } from "../../contracts/shared";
3
+
4
+ type DbRow = Record<string, unknown>;
5
+
6
+ export const userIpaDataJoin = sql`LEFT JOIN auth.user_ipa_data ui ON ui.user_id = u.id`;
7
+
8
+ export const userIpaDataColumns = sql`
9
+ ui.uid_number AS ipa_uid_number,
10
+ ui.phone AS ipa_phone,
11
+ ui.employee_type AS ipa_employee_type,
12
+ ui.mobile AS ipa_mobile,
13
+ ui.addr_street AS ipa_addr_street,
14
+ ui.addr_postal_code AS ipa_addr_postal_code,
15
+ ui.addr_city AS ipa_addr_city,
16
+ ui.addr_state AS ipa_addr_state,
17
+ ui.ipa_password_expires AS ipa_password_expires,
18
+ ui.last_login_ipa AS ipa_last_login_ipa,
19
+ ui.synced_at AS ipa_synced_at,
20
+ ui.ssh_public_keys AS ipa_ssh_public_keys,
21
+ ui.ssh_fingerprints AS ipa_ssh_fingerprints
22
+ `;
23
+
24
+ export const emptyIpaUserData = (): IpaUserData => ({
25
+ uidNumber: null,
26
+ phone: null,
27
+ employeeType: null,
28
+ mobile: null,
29
+ address: {
30
+ street: null,
31
+ postalCode: null,
32
+ city: null,
33
+ state: null,
34
+ },
35
+ passwordExpires: null,
36
+ lastLoginIpa: null,
37
+ syncedAt: null,
38
+ sshPublicKeys: [],
39
+ sshFingerprints: [],
40
+ });
41
+
42
+ export const buildIpaUserData = (row: DbRow): IpaUserData | null => {
43
+ if (!row.ipa_uid_number && !row.ipa_phone && !row.ipa_password_expires && !row.ipa_synced_at && !row.ipa_ssh_public_keys) {
44
+ return null;
45
+ }
46
+
47
+ return {
48
+ uidNumber: (row.ipa_uid_number as number) ?? null,
49
+ phone: (row.ipa_phone as string) ?? null,
50
+ employeeType: (row.ipa_employee_type as string) ?? null,
51
+ mobile: (row.ipa_mobile as string) ?? null,
52
+ address: {
53
+ street: (row.ipa_addr_street as string) ?? null,
54
+ postalCode: (row.ipa_addr_postal_code as string) ?? null,
55
+ city: (row.ipa_addr_city as string) ?? null,
56
+ state: (row.ipa_addr_state as string) ?? null,
57
+ },
58
+ passwordExpires: row.ipa_password_expires ? (row.ipa_password_expires as Date).toISOString() : null,
59
+ lastLoginIpa: row.ipa_last_login_ipa ? (row.ipa_last_login_ipa as Date).toISOString() : null,
60
+ syncedAt: row.ipa_synced_at ? (row.ipa_synced_at as Date).toISOString() : null,
61
+ sshPublicKeys: (row.ipa_ssh_public_keys as string[]) ?? [],
62
+ sshFingerprints: (row.ipa_ssh_fingerprints as string[]) ?? [],
63
+ };
64
+ };
@@ -0,0 +1,2 @@
1
+ export { accountLifecycle } from "../account-lifecycle";
2
+ export type { AccountLifecycleService } from "../account-lifecycle";