@valentinkolb/cloud 0.3.1 → 0.5.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 (194) hide show
  1. package/package.json +18 -8
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +119 -47
  4. package/src/_internal/runtime-context.ts +1 -0
  5. package/src/api/accounts-entities.ts +4 -0
  6. package/src/api/admin-core-settings.ts +98 -0
  7. package/src/api/announcements.ts +131 -0
  8. package/src/api/auth/schemas.ts +24 -0
  9. package/src/api/auth.ts +113 -10
  10. package/src/api/index.ts +15 -25
  11. package/src/api/me.ts +203 -14
  12. package/src/api/search/schemas.ts +1 -0
  13. package/src/api/search.ts +62 -8
  14. package/src/config/ssr.ts +2 -9
  15. package/src/contracts/announcements.test.ts +37 -0
  16. package/src/contracts/announcements.ts +121 -0
  17. package/src/contracts/app.ts +4 -0
  18. package/src/contracts/index.ts +3 -2
  19. package/src/contracts/registry.ts +4 -0
  20. package/src/contracts/shared.ts +108 -1
  21. package/src/desktop/index.ts +704 -0
  22. package/src/desktop/solid.tsx +938 -0
  23. package/src/server/api/index.ts +1 -1
  24. package/src/server/api/respond.ts +50 -10
  25. package/src/server/index.ts +44 -38
  26. package/src/server/middleware/auth.ts +98 -9
  27. package/src/server/middleware/index.ts +2 -1
  28. package/src/server/middleware/settings.ts +26 -0
  29. package/src/server/services/access.test.ts +197 -0
  30. package/src/server/services/access.ts +254 -6
  31. package/src/server/services/index.ts +14 -11
  32. package/src/server/services/pagination.ts +22 -0
  33. package/src/server/time.ts +45 -0
  34. package/src/services/account-lifecycle/index.ts +142 -18
  35. package/src/services/accounts/app.ts +658 -170
  36. package/src/services/accounts/authz.test.ts +77 -0
  37. package/src/services/accounts/authz.ts +22 -0
  38. package/src/services/accounts/entities.ts +84 -5
  39. package/src/services/accounts/groups.ts +30 -24
  40. package/src/services/accounts/model.test.ts +30 -0
  41. package/src/services/accounts/switching.test.ts +14 -0
  42. package/src/services/accounts/switching.ts +15 -6
  43. package/src/services/accounts/users.ts +75 -52
  44. package/src/services/announcements/index.test.ts +32 -0
  45. package/src/services/announcements/index.ts +224 -0
  46. package/src/services/audit/index.test.ts +84 -0
  47. package/src/services/audit/index.ts +431 -0
  48. package/src/services/auth-flows/index.ts +9 -2
  49. package/src/services/auth-flows/ipa.ts +0 -2
  50. package/src/services/auth-flows/magic-link.ts +3 -2
  51. package/src/services/auth-flows/password-reset.ts +284 -0
  52. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  53. package/src/services/auth-flows/proxy-return.ts +49 -0
  54. package/src/services/gateway.ts +162 -0
  55. package/src/services/index.ts +44 -2
  56. package/src/services/ipa/effective-groups.test.ts +33 -0
  57. package/src/services/ipa/effective-groups.ts +70 -0
  58. package/src/services/ipa/profile.ts +45 -3
  59. package/src/services/ipa/search.ts +3 -5
  60. package/src/services/ipa/service-account.ts +15 -0
  61. package/src/services/ipa/sync-planning.test.ts +32 -0
  62. package/src/services/ipa/sync-planning.ts +22 -0
  63. package/src/services/ipa/sync.ts +110 -38
  64. package/src/services/oauth-tokens.ts +104 -0
  65. package/src/services/postgres.ts +21 -6
  66. package/src/services/providers/local/auth.test.ts +22 -0
  67. package/src/services/providers/local/auth.ts +46 -3
  68. package/src/services/secrets.ts +10 -0
  69. package/src/services/service-account-credentials.test.ts +210 -0
  70. package/src/services/service-account-credentials.ts +715 -0
  71. package/src/services/service-accounts.ts +188 -0
  72. package/src/services/session/index.ts +7 -8
  73. package/src/services/settings/app.ts +4 -20
  74. package/src/services/settings/defaults.ts +64 -22
  75. package/src/services/settings/store.ts +47 -0
  76. package/src/services/weather/forecast.ts +40 -7
  77. package/src/services/webauthn.test.ts +36 -0
  78. package/src/services/webauthn.ts +384 -0
  79. package/src/shared/icons.ts +391 -100
  80. package/src/shared/index.ts +7 -0
  81. package/src/shared/markdown/extensions/code.ts +38 -1
  82. package/src/shared/markdown/extensions/images.ts +39 -3
  83. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  84. package/src/shared/markdown/extensions/mark.ts +48 -0
  85. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  86. package/src/shared/markdown/extensions/tables.ts +79 -58
  87. package/src/shared/markdown/formula.test.ts +1089 -0
  88. package/src/shared/markdown/formula.ts +1187 -0
  89. package/src/shared/markdown/index.ts +76 -2
  90. package/src/shared/mock-cover.ts +130 -0
  91. package/src/shared/redirect.test.ts +49 -0
  92. package/src/shared/redirect.ts +52 -0
  93. package/src/shared/theme.test.ts +24 -0
  94. package/src/shared/theme.ts +68 -0
  95. package/src/shared/time.ts +13 -0
  96. package/src/ssr/AdminLayout.tsx +7 -3
  97. package/src/ssr/AdminSidebar.tsx +115 -49
  98. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  99. package/src/ssr/Footer.island.tsx +3 -8
  100. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  101. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  102. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  103. package/src/ssr/Layout.tsx +74 -66
  104. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  105. package/src/ssr/LayoutHelp.tsx +266 -0
  106. package/src/ssr/NavMenu.island.tsx +0 -39
  107. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  108. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  109. package/src/ssr/islands/index.ts +13 -0
  110. package/src/styles/base-popover.css +5 -2
  111. package/src/styles/effects.css +87 -6
  112. package/src/styles/global.css +146 -9
  113. package/src/styles/input.css +3 -1
  114. package/src/styles/utilities-buttons.css +133 -27
  115. package/src/styles/utilities-code-display.css +67 -0
  116. package/src/styles/utilities-completion.css +223 -0
  117. package/src/styles/utilities-detail.css +73 -0
  118. package/src/styles/utilities-feedback.css +16 -15
  119. package/src/styles/utilities-layout.css +42 -2
  120. package/src/styles/utilities-markdown-editor.css +472 -0
  121. package/src/styles/utilities-navigation.css +63 -8
  122. package/src/styles/utilities-script.css +84 -0
  123. package/src/styles/utilities-table-tile.css +229 -0
  124. package/src/types/ambient.d.ts +9 -0
  125. package/src/ui/completion/behaviors.test.ts +95 -0
  126. package/src/ui/completion/behaviors.ts +205 -0
  127. package/src/ui/completion/engine.ts +368 -0
  128. package/src/ui/completion/index.ts +40 -0
  129. package/src/ui/completion/overlay.ts +92 -0
  130. package/src/ui/dialog-core.ts +173 -45
  131. package/src/ui/filter/FilterChip.tsx +42 -40
  132. package/src/ui/index.ts +11 -12
  133. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  134. package/src/ui/input/CheckboxCard.tsx +91 -0
  135. package/src/ui/input/Combobox.tsx +375 -0
  136. package/src/ui/input/DatePicker.tsx +846 -0
  137. package/src/ui/input/DateTimeInput.tsx +29 -4
  138. package/src/ui/input/FileDropzone.tsx +116 -0
  139. package/src/ui/input/IconInput.tsx +116 -0
  140. package/src/ui/input/ImageInput.tsx +19 -2
  141. package/src/ui/input/MultiSelectInput.tsx +448 -0
  142. package/src/ui/input/NumberInput.tsx +417 -61
  143. package/src/ui/input/SegmentedControl.tsx +2 -2
  144. package/src/ui/input/Select.tsx +172 -10
  145. package/src/ui/input/Slider.tsx +3 -4
  146. package/src/ui/input/Switch.tsx +3 -2
  147. package/src/ui/input/TemplateEditor.tsx +212 -0
  148. package/src/ui/input/TextInput.tsx +144 -13
  149. package/src/ui/input/index.ts +53 -8
  150. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  151. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  152. package/src/ui/input/markdown/actions.ts +233 -0
  153. package/src/ui/input/markdown/active-formats.ts +94 -0
  154. package/src/ui/input/markdown/behaviors.ts +193 -0
  155. package/src/ui/input/markdown/code-zone.ts +23 -0
  156. package/src/ui/input/markdown/highlight.ts +316 -0
  157. package/src/ui/layout.ts +22 -0
  158. package/src/ui/misc/AppOverview.tsx +105 -0
  159. package/src/ui/misc/AppWorkspace.tsx +607 -0
  160. package/src/ui/misc/Calendar.tsx +1291 -0
  161. package/src/ui/misc/Chart.tsx +162 -0
  162. package/src/ui/misc/CodeDisplay.tsx +54 -0
  163. package/src/ui/misc/ContextMenu.tsx +2 -2
  164. package/src/ui/misc/DataTable.tsx +269 -0
  165. package/src/ui/misc/DockWorkspace.tsx +425 -0
  166. package/src/ui/misc/Docs.tsx +153 -0
  167. package/src/ui/misc/Dropdown.tsx +2 -2
  168. package/src/ui/misc/EntitySearch.tsx +260 -129
  169. package/src/ui/misc/LinkCard.tsx +14 -2
  170. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  171. package/src/ui/misc/Pagination.tsx +31 -12
  172. package/src/ui/misc/PanelDialog.tsx +109 -0
  173. package/src/ui/misc/Panes.tsx +873 -0
  174. package/src/ui/misc/PermissionEditor.tsx +358 -262
  175. package/src/ui/misc/Placeholder.tsx +40 -0
  176. package/src/ui/misc/ProgressBar.tsx +1 -1
  177. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  178. package/src/ui/misc/SettingsModal.tsx +150 -0
  179. package/src/ui/misc/StatCell.tsx +182 -40
  180. package/src/ui/misc/StatGrid.tsx +149 -0
  181. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  182. package/src/ui/misc/code-highlight.ts +213 -0
  183. package/src/ui/misc/index.ts +93 -12
  184. package/src/ui/prompts.tsx +362 -312
  185. package/src/ui/toast.ts +384 -0
  186. package/src/ui/widgets/Widget.tsx +12 -4
  187. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  188. package/src/ui/ipa/GroupView.tsx +0 -36
  189. package/src/ui/ipa/LoginBtn.tsx +0 -16
  190. package/src/ui/ipa/UserView.tsx +0 -58
  191. package/src/ui/ipa/index.ts +0 -4
  192. package/src/ui/navigation.ts +0 -32
  193. package/src/ui/sidebar.tsx +0 -468
  194. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -0,0 +1,431 @@
1
+ import { sql, type SQL } from "bun";
2
+ import type { PaginationParams, UserProvider } from "../../contracts/shared";
3
+ import { err, fail, ok, paginate, type PageParams, type Paginated, type Result, type ServiceError } from "../../server/services";
4
+ import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
5
+ import { logger } from "../logging";
6
+
7
+ export type AuditOutcome = "allowed" | "denied" | "failed";
8
+ export type AuditActionGroup = "service_accounts";
9
+
10
+ export type AuditActor = {
11
+ userId?: string | null;
12
+ uid?: string | null;
13
+ provider?: UserProvider | string | null;
14
+ roles?: readonly string[] | null;
15
+ };
16
+
17
+ export type AuditTarget = {
18
+ type?: string | null;
19
+ id?: string | null;
20
+ label?: string | null;
21
+ provider?: UserProvider | string | null;
22
+ };
23
+
24
+ export type AuditEvent = {
25
+ id: number;
26
+ createdAt: string;
27
+ action: string;
28
+ outcome: AuditOutcome;
29
+ actor: {
30
+ userId: string | null;
31
+ uid: string | null;
32
+ provider: string | null;
33
+ roles: string[];
34
+ };
35
+ target: {
36
+ type: string | null;
37
+ id: string | null;
38
+ label: string | null;
39
+ provider: string | null;
40
+ };
41
+ reason: string | null;
42
+ errorCode: string | null;
43
+ errorMessage: string | null;
44
+ requestId: string | null;
45
+ metadata: Record<string, unknown>;
46
+ };
47
+
48
+ export type AuditListFilter = {
49
+ search?: string;
50
+ actor?: string;
51
+ target?: string;
52
+ action?: string;
53
+ actionGroup?: AuditActionGroup;
54
+ serviceAccountId?: string;
55
+ outcome?: AuditOutcome;
56
+ provider?: UserProvider | string;
57
+ days?: number;
58
+ };
59
+
60
+ export type SelfServiceAuditActivity = {
61
+ id: number;
62
+ createdAt: string;
63
+ action: string;
64
+ label: string;
65
+ outcome: AuditOutcome;
66
+ context: string | null;
67
+ };
68
+
69
+ export type AuditRecordParams = {
70
+ action: string;
71
+ outcome: AuditOutcome;
72
+ actor?: AuditActor | null;
73
+ target?: AuditTarget | null;
74
+ reason?: string | null;
75
+ error?: Pick<ServiceError, "code" | "message"> | { code?: string | null; message?: string | null } | null;
76
+ requestId?: string | null;
77
+ metadata?: Record<string, unknown> | null;
78
+ };
79
+
80
+ type AuditDb = typeof sql;
81
+
82
+ type DbAuditRow = {
83
+ id: number;
84
+ created_at: Date | string;
85
+ action: string;
86
+ outcome: string;
87
+ actor_user_id: string | null;
88
+ actor_uid: string | null;
89
+ actor_provider: string | null;
90
+ actor_roles: string[] | null;
91
+ target_type: string | null;
92
+ target_id: string | null;
93
+ target_label: string | null;
94
+ target_provider: string | null;
95
+ reason: string | null;
96
+ error_code: string | null;
97
+ error_message: string | null;
98
+ request_id: string | null;
99
+ metadata: Record<string, unknown> | string | null;
100
+ };
101
+
102
+ const log = logger("audit");
103
+ const SENSITIVE_KEY_PATTERN = /(password|secret|token|cookie|authorization|api[_-]?key|private[_-]?key|session|ipa[_-]?session)/i;
104
+ const REDACTED = "[REDACTED]";
105
+ const MAX_STRING_LENGTH = 500;
106
+ const MAX_ARRAY_LENGTH = 50;
107
+ const MAX_DEPTH = 8;
108
+ const SELF_SERVICE_ACTION_LABELS = {
109
+ "accounts.user.change_own_password": "Password changed",
110
+ "accounts.user.remove_self": "Account deleted",
111
+ "accounts.user.update": "Profile updated",
112
+ "accounts.request.create": "Account request submitted",
113
+ "accounts.request.withdraw": "Account request withdrawn",
114
+ "accounts.user.extend_account": "Account extended",
115
+ "service_account_credential.create": "API key created",
116
+ "service_account_credential.revoke": "API key revoked",
117
+ "service_account_credential.authenticate": "API key used",
118
+ "webauthn_credential.create": "Passkey added",
119
+ "webauthn_credential.delete": "Passkey removed",
120
+ "webauthn_credential.authenticate": "Passkey used",
121
+ } as const satisfies Record<string, string>;
122
+ const SELF_SERVICE_ACTIONS = Object.keys(SELF_SERVICE_ACTION_LABELS);
123
+
124
+ const asString = (value: string | null | undefined): string | null => {
125
+ const trimmed = value?.trim();
126
+ return trimmed ? trimmed : null;
127
+ };
128
+
129
+ const asRoleArray = (roles: readonly string[] | null | undefined): string[] => [...new Set((roles ?? []).map((role) => role.trim()).filter(Boolean))];
130
+
131
+ export const sanitizeAuditMetadata = (value: unknown, depth = 0): unknown => {
132
+ if (value == null) return null;
133
+ if (value instanceof Date) return value.toISOString();
134
+ if (typeof value === "string") return value.length > MAX_STRING_LENGTH ? `${value.slice(0, MAX_STRING_LENGTH)}...` : value;
135
+ if (typeof value === "number" || typeof value === "boolean") return value;
136
+ if (typeof value === "bigint") return value.toString();
137
+ if (typeof value !== "object") return null;
138
+ if (depth >= MAX_DEPTH) return "[MAX_DEPTH]";
139
+
140
+ if (Array.isArray(value)) {
141
+ const items = value.slice(0, MAX_ARRAY_LENGTH).map((item) => sanitizeAuditMetadata(item, depth + 1));
142
+ if (value.length > MAX_ARRAY_LENGTH) items.push(`[${value.length - MAX_ARRAY_LENGTH} more]`);
143
+ return items;
144
+ }
145
+
146
+ const out: Record<string, unknown> = {};
147
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
148
+ out[key] = SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : sanitizeAuditMetadata(child, depth + 1);
149
+ }
150
+ return out;
151
+ };
152
+
153
+ export const sanitizeAuditText = (value: string | null | undefined): string | null => {
154
+ const text = asString(value);
155
+ if (!text) return null;
156
+ if (SENSITIVE_KEY_PATTERN.test(text)) return REDACTED;
157
+ return text.length > MAX_STRING_LENGTH ? `${text.slice(0, MAX_STRING_LENGTH)}...` : text;
158
+ };
159
+
160
+ const mapRow = (row: DbAuditRow): AuditEvent => ({
161
+ id: Number(row.id),
162
+ createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
163
+ action: row.action,
164
+ outcome: row.outcome as AuditOutcome,
165
+ actor: {
166
+ userId: row.actor_user_id,
167
+ uid: row.actor_uid,
168
+ provider: row.actor_provider,
169
+ roles: row.actor_roles ?? [],
170
+ },
171
+ target: {
172
+ type: row.target_type,
173
+ id: row.target_id,
174
+ label: row.target_label,
175
+ provider: row.target_provider,
176
+ },
177
+ reason: row.reason,
178
+ errorCode: row.error_code,
179
+ errorMessage: row.error_message,
180
+ requestId: row.request_id,
181
+ metadata: parsePgJsonRecord(row.metadata) ?? {},
182
+ });
183
+
184
+ const mapSelfServiceActivityRow = (row: DbAuditRow): SelfServiceAuditActivity => ({
185
+ id: Number(row.id),
186
+ createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
187
+ action: row.action,
188
+ label: SELF_SERVICE_ACTION_LABELS[row.action as keyof typeof SELF_SERVICE_ACTION_LABELS] ?? row.action,
189
+ outcome: row.outcome as AuditOutcome,
190
+ context: row.target_label ?? null,
191
+ });
192
+
193
+ const outcomeForError = (error: Pick<ServiceError, "status"> | null | undefined): AuditOutcome => {
194
+ if (!error) return "allowed";
195
+ return error.status === 401 || error.status === 403 ? "denied" : "failed";
196
+ };
197
+
198
+ const record = async (params: AuditRecordParams, db: AuditDb = sql): Promise<void> => {
199
+ try {
200
+ const actorRoles = asRoleArray(params.actor?.roles);
201
+ const metadata = (sanitizeAuditMetadata(params.metadata ?? {}) as Record<string, unknown>) ?? {};
202
+ await db`
203
+ INSERT INTO audit.events (
204
+ action,
205
+ outcome,
206
+ actor_user_id,
207
+ actor_uid,
208
+ actor_provider,
209
+ actor_roles,
210
+ target_type,
211
+ target_id,
212
+ target_label,
213
+ target_provider,
214
+ reason,
215
+ error_code,
216
+ error_message,
217
+ request_id,
218
+ metadata
219
+ )
220
+ VALUES (
221
+ ${params.action},
222
+ ${params.outcome},
223
+ ${asString(params.actor?.userId)}::uuid,
224
+ ${asString(params.actor?.uid)},
225
+ ${asString(params.actor?.provider)},
226
+ ${toPgTextArray(actorRoles)}::text[],
227
+ ${asString(params.target?.type)},
228
+ ${asString(params.target?.id)},
229
+ ${asString(params.target?.label)},
230
+ ${asString(params.target?.provider)},
231
+ ${sanitizeAuditText(params.reason)},
232
+ ${asString(params.error?.code ?? null)},
233
+ ${sanitizeAuditText(params.error?.message ?? null)},
234
+ ${asString(params.requestId)},
235
+ ${JSON.stringify(metadata)}::jsonb
236
+ )
237
+ `;
238
+ } catch (error) {
239
+ log.error("Audit write failed", {
240
+ action: params.action,
241
+ outcome: params.outcome,
242
+ error: error instanceof Error ? error.message : String(error),
243
+ });
244
+ throw error;
245
+ }
246
+ };
247
+
248
+ const recordResult = async <T,>(params: {
249
+ action: string;
250
+ actor?: AuditActor | null;
251
+ target?: AuditTarget | null;
252
+ metadata?: Record<string, unknown> | null;
253
+ result: Result<T>;
254
+ db?: AuditDb;
255
+ }): Promise<Result<T>> => {
256
+ await record({
257
+ action: params.action,
258
+ actor: params.actor,
259
+ target: params.target,
260
+ metadata: params.metadata,
261
+ outcome: params.result.ok ? "allowed" : outcomeForError(params.result.error),
262
+ reason: params.result.ok ? null : params.result.error.message,
263
+ error: params.result.ok ? null : params.result.error,
264
+ }, params.db);
265
+ return params.result;
266
+ };
267
+
268
+ /**
269
+ * Use only after an irreversible side effect already happened. Authorization
270
+ * checks and pre-mutation validation must keep using `record`/`recordResult`
271
+ * so missing audit storage can still fail closed before anything changes.
272
+ */
273
+ const recordResultAfterSideEffect = async <T,>(params: {
274
+ action: string;
275
+ actor?: AuditActor | null;
276
+ target?: AuditTarget | null;
277
+ metadata?: Record<string, unknown> | null;
278
+ result: Result<T>;
279
+ }): Promise<Result<T>> => {
280
+ try {
281
+ await recordResult(params);
282
+ } catch (error) {
283
+ log.error("Post-side-effect audit write failed", {
284
+ action: params.action,
285
+ outcome: params.result.ok ? "allowed" : outcomeForError(params.result.error),
286
+ error: error instanceof Error ? error.message : String(error),
287
+ });
288
+ }
289
+ return params.result;
290
+ };
291
+
292
+ const deny = async <T,>(params: {
293
+ action: string;
294
+ actor?: AuditActor | null;
295
+ target?: AuditTarget | null;
296
+ message?: string;
297
+ metadata?: Record<string, unknown> | null;
298
+ }): Promise<Result<T>> => {
299
+ const error = err.forbidden(params.message ?? "Access denied");
300
+ await record({
301
+ action: params.action,
302
+ actor: params.actor,
303
+ target: params.target,
304
+ metadata: params.metadata,
305
+ outcome: "denied",
306
+ reason: error.message,
307
+ error,
308
+ });
309
+ return fail(error);
310
+ };
311
+
312
+ const buildWhere = (filter: AuditListFilter = {}) => {
313
+ const conditions: SQL.Query<unknown>[] = [sql`TRUE`];
314
+ const search = filter.search?.trim();
315
+ const actor = filter.actor?.trim();
316
+ const target = filter.target?.trim();
317
+ const provider = filter.provider?.trim();
318
+ const days = filter.days && Number.isFinite(filter.days) && filter.days > 0 ? Math.min(Math.floor(filter.days), 3650) : null;
319
+
320
+ if (search) {
321
+ const pattern = `%${escapeLikePattern(search)}%`;
322
+ conditions.push(sql`(
323
+ action ILIKE ${pattern} ESCAPE '\\'
324
+ OR COALESCE(actor_user_id::text, '') ILIKE ${pattern} ESCAPE '\\'
325
+ OR COALESCE(actor_uid, '') ILIKE ${pattern} ESCAPE '\\'
326
+ OR COALESCE(target_label, '') ILIKE ${pattern} ESCAPE '\\'
327
+ OR COALESCE(target_id, '') ILIKE ${pattern} ESCAPE '\\'
328
+ OR COALESCE(reason, '') ILIKE ${pattern} ESCAPE '\\'
329
+ OR COALESCE(error_message, '') ILIKE ${pattern} ESCAPE '\\'
330
+ OR metadata::text ILIKE ${pattern} ESCAPE '\\'
331
+ )`);
332
+ }
333
+
334
+ if (actor) {
335
+ const pattern = `%${escapeLikePattern(actor)}%`;
336
+ conditions.push(sql`(actor_user_id::text = ${actor} OR COALESCE(actor_uid, '') ILIKE ${pattern} ESCAPE '\\')`);
337
+ }
338
+ if (target) {
339
+ const pattern = `%${escapeLikePattern(target)}%`;
340
+ conditions.push(sql`(target_id = ${target} OR COALESCE(target_label, '') ILIKE ${pattern} ESCAPE '\\')`);
341
+ }
342
+ if (filter.action?.trim()) conditions.push(sql`action = ${filter.action.trim()}`);
343
+ if (filter.actionGroup === "service_accounts") {
344
+ conditions.push(sql`(
345
+ action LIKE 'service_account%'
346
+ OR target_type IN ('service_account', 'service_account_credential')
347
+ OR metadata ? 'serviceAccountId'
348
+ )`);
349
+ }
350
+ if (filter.serviceAccountId?.trim()) {
351
+ const serviceAccountId = filter.serviceAccountId.trim();
352
+ conditions.push(sql`(
353
+ (target_type = 'service_account' AND target_id = ${serviceAccountId})
354
+ OR metadata->>'serviceAccountId' = ${serviceAccountId}
355
+ )`);
356
+ }
357
+ if (filter.outcome) conditions.push(sql`outcome = ${filter.outcome}`);
358
+ if (provider) conditions.push(sql`(actor_provider = ${provider} OR target_provider = ${provider})`);
359
+ if (days) conditions.push(sql`created_at >= now() - ${`${days} days`}::interval`);
360
+
361
+ return conditions.reduce((acc, condition) => sql`${acc} AND ${condition}`);
362
+ };
363
+
364
+ const list = async (config: {
365
+ pagination?: PageParams;
366
+ filter?: AuditListFilter;
367
+ }): Promise<Paginated<AuditEvent>> => {
368
+ const { page, perPage, offset } = paginate(config.pagination);
369
+ const where = buildWhere(config.filter);
370
+ const [countRows, rows] = await Promise.all([
371
+ sql<{ count: number }[]>`SELECT COUNT(*)::int AS count FROM audit.events WHERE ${where}`,
372
+ sql<DbAuditRow[]>`
373
+ SELECT *
374
+ FROM audit.events
375
+ WHERE ${where}
376
+ ORDER BY created_at DESC, id DESC
377
+ LIMIT ${perPage}
378
+ OFFSET ${offset}
379
+ `,
380
+ ]);
381
+ const total = countRows[0]?.count ?? 0;
382
+ return {
383
+ items: rows.map(mapRow),
384
+ page,
385
+ perPage,
386
+ total,
387
+ hasNext: page * perPage < total,
388
+ };
389
+ };
390
+
391
+ const listSelfServiceActivity = async (config: {
392
+ userId: string;
393
+ pagination?: PageParams;
394
+ days?: number;
395
+ }): Promise<Paginated<SelfServiceAuditActivity>> => {
396
+ const { page, perPage, offset } = paginate(config.pagination);
397
+ const days = config.days && Number.isFinite(config.days) && config.days > 0 ? Math.min(Math.floor(config.days), 365) : 30;
398
+ const where = sql`
399
+ actor_user_id = ${config.userId}::uuid
400
+ AND action = ANY(${toPgTextArray(SELF_SERVICE_ACTIONS)}::text[])
401
+ AND created_at >= now() - ${`${days} days`}::interval
402
+ `;
403
+ const [countRows, rows] = await Promise.all([
404
+ sql<{ count: number }[]>`SELECT COUNT(*)::int AS count FROM audit.events WHERE ${where}`,
405
+ sql<DbAuditRow[]>`
406
+ SELECT *
407
+ FROM audit.events
408
+ WHERE ${where}
409
+ ORDER BY created_at DESC, id DESC
410
+ LIMIT ${perPage}
411
+ OFFSET ${offset}
412
+ `,
413
+ ]);
414
+ const total = countRows[0]?.count ?? 0;
415
+ return {
416
+ items: rows.map(mapSelfServiceActivityRow),
417
+ page,
418
+ perPage,
419
+ total,
420
+ hasNext: page * perPage < total,
421
+ };
422
+ };
423
+
424
+ export const audit = {
425
+ record,
426
+ recordResult,
427
+ recordResultAfterSideEffect,
428
+ deny,
429
+ list,
430
+ listSelfServiceActivity,
431
+ };
@@ -1,6 +1,13 @@
1
1
  import * as ipa from "./ipa";
2
2
  import * as magicLink from "./magic-link";
3
+ import * as passwordReset from "./password-reset";
4
+ import * as proxyReturn from "./proxy-return";
3
5
 
4
- export { ipa, magicLink };
6
+ export { ipa, magicLink, passwordReset, proxyReturn };
5
7
 
6
- export const authFlows = { ipa, magicLink } as const;
8
+ export const authFlows = {
9
+ ipa,
10
+ magicLink,
11
+ passwordReset,
12
+ proxyReturn,
13
+ } as const;
@@ -14,7 +14,6 @@ type IpaLoginFailure =
14
14
 
15
15
  type IpaLoginSuccess = {
16
16
  ok: true;
17
- ipaSession: string;
18
17
  userId: string;
19
18
  user: User;
20
19
  };
@@ -103,7 +102,6 @@ export const login = async (params: { username: string; password: string }): Pro
103
102
 
104
103
  return {
105
104
  ok: true,
106
- ipaSession: loginResult.session,
107
105
  userId: userResult.userId,
108
106
  user: userResult.user,
109
107
  };
@@ -5,8 +5,9 @@ import { providers } from "../providers";
5
5
  import * as settings from "../settings";
6
6
  import { renderTemplate } from "../settings/templates";
7
7
  import type { User } from "../../contracts/shared";
8
+ import { createAuthLoginUrl } from "../../shared/redirect";
8
9
 
9
- export const request = async (params: { email: string }): Promise<
10
+ export const request = async (params: { email: string; redirectTo?: string }): Promise<
10
11
  | { ok: true }
11
12
  | { ok: false; status: 400; message: string }
12
13
  > => {
@@ -32,7 +33,7 @@ export const request = async (params: { email: string }): Promise<
32
33
  const token = await providers.local.auth.createMagicLinkToken({ email: params.email, ttlSeconds: 300 });
33
34
  const rawAppUrl = await settings.get<string>("app.url");
34
35
  const appUrl = rawAppUrl.startsWith("http") ? rawAppUrl : `https://${rawAppUrl}`;
35
- const magicLink = `${appUrl}/auth/login?token=${token}`;
36
+ const magicLink = createAuthLoginUrl(appUrl, { token, redirectTo: params.redirectTo });
36
37
 
37
38
  const appName = await settings.get<string>("app.name");
38
39
  const template = await settings.get<string>("mail.magic_link_login");