@k2b/cloud 0.6.0 → 0.8.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/README.md +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -4,6 +4,7 @@ import type { MutationResult, UserProfile } from "../../contracts/shared";
|
|
|
4
4
|
import { freeipa } from "../../server/services";
|
|
5
5
|
import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
|
|
6
6
|
import { resolveProviderProfile } from "../accounts/base-user";
|
|
7
|
+
import { emailAlreadyUsed, findAccountsByEmail, lockAccountEmail, normalizeAccountEmail } from "../accounts/email-write";
|
|
7
8
|
import { logger } from "../logging";
|
|
8
9
|
import { session } from "../session";
|
|
9
10
|
import * as settings from "../settings";
|
|
@@ -47,34 +48,36 @@ const log = logger("auth:ipa");
|
|
|
47
48
|
* profile patches call `patchUserIpaData` — otherwise SSH keys, uid_number,
|
|
48
49
|
* password expiry, etc. get wiped.
|
|
49
50
|
*/
|
|
50
|
-
const upsertUserIpaData = async (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
51
|
+
const upsertUserIpaData = async (
|
|
52
|
+
params: {
|
|
53
|
+
userId: string;
|
|
54
|
+
uidNumber?: number | null;
|
|
55
|
+
primaryGidNumber?: number | null;
|
|
56
|
+
homeDirectory?: string | null;
|
|
57
|
+
loginShell?: string | null;
|
|
58
|
+
phone?: string | null;
|
|
59
|
+
employeeType?: string | null;
|
|
60
|
+
mobile?: string | null;
|
|
61
|
+
street?: string | null;
|
|
62
|
+
postalCode?: string | null;
|
|
63
|
+
city?: string | null;
|
|
64
|
+
state?: string | null;
|
|
65
|
+
passwordExpires?: Date | null;
|
|
66
|
+
lastLoginIpa?: Date | null;
|
|
67
|
+
syncedAt?: Date | null;
|
|
68
|
+
sshPublicKeys?: string[];
|
|
69
|
+
sshFingerprints?: string[];
|
|
70
|
+
},
|
|
71
|
+
tx: typeof sql,
|
|
72
|
+
) => {
|
|
73
|
+
await mirrorIpaPosix(tx, {
|
|
74
|
+
userId: params.userId,
|
|
75
|
+
uidNumber: params.uidNumber ?? null,
|
|
76
|
+
primaryGidNumber: params.primaryGidNumber ?? null,
|
|
77
|
+
homeDirectory: params.homeDirectory ?? null,
|
|
78
|
+
loginShell: params.loginShell ?? null,
|
|
79
|
+
});
|
|
80
|
+
await tx`
|
|
78
81
|
INSERT INTO auth.user_ipa_data (
|
|
79
82
|
user_id, uid_number, phone, employee_type, mobile, addr_street, addr_postal_code,
|
|
80
83
|
addr_city, addr_state, ipa_password_expires, last_login_ipa, synced_at, ssh_public_keys, ssh_fingerprints
|
|
@@ -110,7 +113,6 @@ const upsertUserIpaData = async (params: {
|
|
|
110
113
|
ssh_public_keys = EXCLUDED.ssh_public_keys,
|
|
111
114
|
ssh_fingerprints = EXCLUDED.ssh_fingerprints
|
|
112
115
|
`;
|
|
113
|
-
});
|
|
114
116
|
};
|
|
115
117
|
|
|
116
118
|
/**
|
|
@@ -119,19 +121,22 @@ const upsertUserIpaData = async (params: {
|
|
|
119
121
|
* If no row exists yet the UPDATE is a no-op; callers that need to guarantee a
|
|
120
122
|
* row must use `upsertUserIpaData` first.
|
|
121
123
|
*/
|
|
122
|
-
const patchUserIpaData = async (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
const patchUserIpaData = async (
|
|
125
|
+
params: {
|
|
126
|
+
userId: string;
|
|
127
|
+
phone?: string | null;
|
|
128
|
+
street?: string | null;
|
|
129
|
+
postalCode?: string | null;
|
|
130
|
+
city?: string | null;
|
|
131
|
+
state?: string | null;
|
|
132
|
+
sshPublicKeys?: string[];
|
|
133
|
+
sshFingerprints?: string[];
|
|
134
|
+
syncedAt?: Date | null;
|
|
135
|
+
},
|
|
136
|
+
tx: typeof sql,
|
|
137
|
+
) => {
|
|
133
138
|
const has = (v: unknown) => v !== undefined;
|
|
134
|
-
await
|
|
139
|
+
await tx`
|
|
135
140
|
UPDATE auth.user_ipa_data SET
|
|
136
141
|
phone = CASE WHEN ${has(params.phone)} THEN ${params.phone ?? null} ELSE phone END,
|
|
137
142
|
addr_street = CASE WHEN ${has(params.street)} THEN ${params.street ?? null} ELSE addr_street END,
|
|
@@ -236,85 +241,91 @@ export const addIpa = async (params: {
|
|
|
236
241
|
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
237
242
|
if (unavailable) return unavailable;
|
|
238
243
|
const { ipaSession, data } = params;
|
|
239
|
-
const {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
244
|
+
const { givenname, sn } = data;
|
|
245
|
+
let promotedUserId: string | undefined;
|
|
246
|
+
const result = await sql.begin(async (tx): Promise<MutationResult<AddIpaResult>> => {
|
|
247
|
+
await lockAccountEmail(tx, data.email);
|
|
248
|
+
const targetProfile = params.profile ?? "user";
|
|
249
|
+
|
|
250
|
+
const displayName = data.displayName || `${givenname} ${sn}`;
|
|
251
|
+
|
|
252
|
+
const matches = await findAccountsByEmail(tx, data.email);
|
|
253
|
+
const locals = matches.filter((row) => row.provider === "local");
|
|
254
|
+
// Preserve an exact legacy match; otherwise promotion must identify one account.
|
|
255
|
+
const local = locals.find((row) => row.mail === data.email) ?? (locals.length === 1 ? locals[0] : undefined);
|
|
256
|
+
if (!local && matches.length > 0) return emailAlreadyUsed();
|
|
257
|
+
const existingLocalRows = local ? [local] : [];
|
|
258
|
+
const email = local?.mail ?? normalizeAccountEmail(data.email);
|
|
259
|
+
|
|
260
|
+
// Use existing guest UID or generate a new one
|
|
261
|
+
let uid: string;
|
|
262
|
+
if (existingLocalRows.length > 0) {
|
|
263
|
+
uid = existingLocalRows[0]!.uid as string;
|
|
264
|
+
} else {
|
|
265
|
+
try {
|
|
266
|
+
const abbrLen = await settings.get<number>("user.abbr_length");
|
|
267
|
+
uid = await generateUniqueAbbreviation(abbrLen);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: e instanceof Error ? e.message : "Failed to generate UID",
|
|
272
|
+
status: 500,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
263
275
|
}
|
|
264
|
-
}
|
|
265
276
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
if ((await uidExists(uid)) && existingLocalRows.length === 0) {
|
|
278
|
+
return { ok: false, error: `UID '${uid}' already exists`, status: 400 };
|
|
279
|
+
}
|
|
269
280
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
const temporaryPassword = generateFreeIpaPassword();
|
|
282
|
+
const accountExpiry = params.accountExpires === undefined ? await calculateAccountExpiration() : params.accountExpires;
|
|
283
|
+
const now = new Date();
|
|
284
|
+
|
|
285
|
+
const ipaOpts: Record<string, unknown> = {
|
|
286
|
+
givenname,
|
|
287
|
+
sn,
|
|
288
|
+
cn: displayName,
|
|
289
|
+
displayname: displayName,
|
|
290
|
+
mail: email,
|
|
291
|
+
userpassword: temporaryPassword,
|
|
292
|
+
};
|
|
293
|
+
if (accountExpiry) {
|
|
294
|
+
ipaOpts.krbprincipalexpiration = freeipa.util.toGeneralizedTime(accountExpiry);
|
|
295
|
+
}
|
|
285
296
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession, method: "user_add", args: [uid], options: ipaOpts });
|
|
298
|
+
if (response.error) {
|
|
299
|
+
const code = response.error.code;
|
|
300
|
+
if (code === 4001)
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: "IPA session expired. Please log in again.",
|
|
304
|
+
status: 401,
|
|
305
|
+
};
|
|
306
|
+
if (code === 4301)
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: "You don't have permission to create users.",
|
|
310
|
+
status: 403,
|
|
311
|
+
};
|
|
296
312
|
return {
|
|
297
313
|
ok: false,
|
|
298
|
-
error:
|
|
299
|
-
status:
|
|
314
|
+
error: response.error.message || "Failed to create user.",
|
|
315
|
+
status: 400,
|
|
300
316
|
};
|
|
301
|
-
|
|
302
|
-
ok: false,
|
|
303
|
-
error: response.error.message || "Failed to create user.",
|
|
304
|
-
status: 400,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
317
|
+
}
|
|
307
318
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
319
|
+
// Extract uidNumber from IPA response
|
|
320
|
+
const ipaResult = response.result?.result as Record<string, unknown> | undefined;
|
|
321
|
+
const uidNumber = ipaResult ? freeipa.util.num(ipaResult.uidnumber) : null;
|
|
311
322
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
323
|
+
let id: string;
|
|
324
|
+
try {
|
|
325
|
+
if (existingLocalRows.length > 0) {
|
|
326
|
+
// Provider switch: update existing local account to IPA user
|
|
327
|
+
const guestId = existingLocalRows[0]!.id as string;
|
|
328
|
+
const updateRows: DbRow[] = await tx`
|
|
318
329
|
UPDATE auth.users SET
|
|
319
330
|
provider = 'ipa',
|
|
320
331
|
profile = ${targetProfile},
|
|
@@ -327,20 +338,23 @@ export const addIpa = async (params: {
|
|
|
327
338
|
WHERE id = ${guestId}
|
|
328
339
|
RETURNING id
|
|
329
340
|
`;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
341
|
+
id = updateRows[0]!.id as string;
|
|
342
|
+
await upsertUserIpaData(
|
|
343
|
+
{
|
|
344
|
+
userId: id,
|
|
345
|
+
uidNumber,
|
|
346
|
+
primaryGidNumber: ipaResult ? freeipa.util.num(ipaResult.gidnumber) : null,
|
|
347
|
+
homeDirectory: ipaResult ? freeipa.util.str(ipaResult.homedirectory) || null : null,
|
|
348
|
+
loginShell: ipaResult ? freeipa.util.str(ipaResult.loginshell) || null : null,
|
|
349
|
+
passwordExpires: now,
|
|
350
|
+
syncedAt: now,
|
|
351
|
+
},
|
|
352
|
+
tx,
|
|
353
|
+
);
|
|
354
|
+
promotedUserId = guestId;
|
|
355
|
+
} else {
|
|
356
|
+
// New user: insert
|
|
357
|
+
const insertRows: DbRow[] = await tx`
|
|
344
358
|
INSERT INTO auth.users (uid, provider, profile, admin, given_name, sn, display_name, mail, account_expires)
|
|
345
359
|
VALUES (${uid}, 'ipa', ${targetProfile}, false, ${givenname}, ${sn}, ${displayName}, ${email}, ${accountExpiry})
|
|
346
360
|
ON CONFLICT (uid) DO UPDATE SET
|
|
@@ -354,38 +368,44 @@ export const addIpa = async (params: {
|
|
|
354
368
|
account_expires = EXCLUDED.account_expires
|
|
355
369
|
RETURNING id
|
|
356
370
|
`;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
371
|
+
id = insertRows[0]!.id as string;
|
|
372
|
+
await upsertUserIpaData(
|
|
373
|
+
{
|
|
374
|
+
userId: id,
|
|
375
|
+
uidNumber,
|
|
376
|
+
primaryGidNumber: ipaResult ? freeipa.util.num(ipaResult.gidnumber) : null,
|
|
377
|
+
homeDirectory: ipaResult ? freeipa.util.str(ipaResult.homedirectory) || null : null,
|
|
378
|
+
loginShell: ipaResult ? freeipa.util.str(ipaResult.loginshell) || null : null,
|
|
379
|
+
passwordExpires: now,
|
|
380
|
+
syncedAt: now,
|
|
381
|
+
},
|
|
382
|
+
tx,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
} catch (dbError) {
|
|
386
|
+
log.error("CRITICAL: FreeIPA user created but local DB update failed. Manual reconciliation needed.", {
|
|
387
|
+
uid,
|
|
388
|
+
email,
|
|
389
|
+
userId: existingLocalRows.length > 0 ? (existingLocalRows[0]!.id as string) : null,
|
|
360
390
|
uidNumber,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
loginShell: ipaResult ? freeipa.util.str(ipaResult.loginshell) || null : null,
|
|
364
|
-
passwordExpires: now,
|
|
365
|
-
syncedAt: now,
|
|
391
|
+
targetProfile,
|
|
392
|
+
error: dbError instanceof Error ? dbError.message : String(dbError),
|
|
366
393
|
});
|
|
394
|
+
throw dbError;
|
|
367
395
|
}
|
|
368
|
-
} catch (dbError) {
|
|
369
|
-
log.error("CRITICAL: FreeIPA user created but local DB update failed. Manual reconciliation needed.", {
|
|
370
|
-
uid,
|
|
371
|
-
email,
|
|
372
|
-
userId: existingLocalRows.length > 0 ? (existingLocalRows[0]!.id as string) : null,
|
|
373
|
-
uidNumber,
|
|
374
|
-
targetProfile,
|
|
375
|
-
error: dbError instanceof Error ? dbError.message : String(dbError),
|
|
376
|
-
});
|
|
377
|
-
throw dbError;
|
|
378
|
-
}
|
|
379
396
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
397
|
+
return {
|
|
398
|
+
ok: true,
|
|
399
|
+
data: {
|
|
400
|
+
id,
|
|
401
|
+
uid,
|
|
402
|
+
accountExpires: accountExpiry ? accountExpiry.toISOString() : null,
|
|
403
|
+
_temporaryPassword: temporaryPassword,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
if (result.ok && promotedUserId) await session.revokeAllForUser(promotedUserId);
|
|
408
|
+
return result;
|
|
389
409
|
};
|
|
390
410
|
|
|
391
411
|
// ==========================
|
|
@@ -400,74 +420,100 @@ export const updateProfile = async (params: {
|
|
|
400
420
|
id: string;
|
|
401
421
|
data: IpaPatchData;
|
|
402
422
|
}): Promise<MutationResult<void>> => {
|
|
403
|
-
const { ipaSession, id
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (provider === "ipa") {
|
|
413
|
-
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
414
|
-
if (unavailable) return unavailable;
|
|
415
|
-
if (!ipaSession) {
|
|
416
|
-
return {
|
|
417
|
-
ok: false,
|
|
418
|
-
error: "IPA session required to update IPA user",
|
|
419
|
-
status: 400,
|
|
420
|
-
};
|
|
423
|
+
const { ipaSession, id } = params;
|
|
424
|
+
const data = { ...params.data };
|
|
425
|
+
return sql.begin(async (tx): Promise<MutationResult<void>> => {
|
|
426
|
+
if (data.mail !== undefined) await lockAccountEmail(tx, data.mail);
|
|
427
|
+
else await tx`LOCK TABLE auth.users IN ROW EXCLUSIVE MODE`;
|
|
428
|
+
|
|
429
|
+
const userRows: DbRow[] = await tx`SELECT uid, provider, profile, mail FROM auth.users WHERE id = ${id} FOR UPDATE`;
|
|
430
|
+
if (userRows.length === 0) {
|
|
431
|
+
return { ok: false, error: "User not found", status: 404 };
|
|
421
432
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (data.ipa?.address?.state !== undefined) ipaOptions.st = data.ipa.address.state || "";
|
|
433
|
-
if (data.ipa?.sshPublicKeys !== undefined) {
|
|
434
|
-
ipaOptions.ipasshpubkey = data.ipa.sshPublicKeys.length > 0 ? data.ipa.sshPublicKeys : "";
|
|
433
|
+
if (data.mail !== undefined) {
|
|
434
|
+
const email = normalizeAccountEmail(data.mail);
|
|
435
|
+
const existingEmail = (userRows[0]!.mail as string | null) ?? "";
|
|
436
|
+
if (email === normalizeAccountEmail(existingEmail)) {
|
|
437
|
+
// Keep the stored spelling: legacy provider/mail uniqueness is case-sensitive.
|
|
438
|
+
delete data.mail;
|
|
439
|
+
} else {
|
|
440
|
+
if (email && (await findAccountsByEmail(tx, email)).some((row) => row.id !== id)) return emailAlreadyUsed();
|
|
441
|
+
data.mail = email;
|
|
442
|
+
}
|
|
435
443
|
}
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
444
|
+
const uid = userRows[0]!.uid as string;
|
|
445
|
+
const { provider } = resolveProviderProfile(userRows[0]!);
|
|
446
|
+
|
|
447
|
+
if (provider === "ipa") {
|
|
448
|
+
const unavailable = await ensureFreeIpaMutationAvailable();
|
|
449
|
+
if (unavailable) return unavailable;
|
|
450
|
+
if (!ipaSession) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
error: "IPA session required to update IPA user",
|
|
454
|
+
status: 400,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const ipaOptions: Record<string, unknown> = {};
|
|
459
|
+
if (data.givenname !== undefined) ipaOptions.givenname = data.givenname;
|
|
460
|
+
if (data.sn !== undefined) ipaOptions.sn = data.sn;
|
|
461
|
+
if (data.displayName !== undefined) ipaOptions.displayname = data.displayName;
|
|
462
|
+
if (data.mail !== undefined) ipaOptions.mail = data.mail || "";
|
|
463
|
+
if (data.ipa?.phone !== undefined) ipaOptions.telephonenumber = data.ipa.phone || "";
|
|
464
|
+
if (data.ipa?.address?.street !== undefined) ipaOptions.street = data.ipa.address.street || "";
|
|
465
|
+
if (data.ipa?.address?.postalCode !== undefined) ipaOptions.postalcode = data.ipa.address.postalCode || "";
|
|
466
|
+
if (data.ipa?.address?.city !== undefined) ipaOptions.l = data.ipa.address.city || "";
|
|
467
|
+
if (data.ipa?.address?.state !== undefined) ipaOptions.st = data.ipa.address.state || "";
|
|
468
|
+
if (data.ipa?.sshPublicKeys !== undefined) {
|
|
469
|
+
ipaOptions.ipasshpubkey = data.ipa.sshPublicKeys.length > 0 ? data.ipa.sshPublicKeys : "";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const response = await freeipa.client.call({
|
|
473
|
+
url: await getIpaUrl(),
|
|
474
|
+
ipaSession,
|
|
475
|
+
method: "user_mod",
|
|
476
|
+
args: [uid],
|
|
477
|
+
options: ipaOptions,
|
|
478
|
+
});
|
|
479
|
+
if (response.error) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
error: response.error.message ?? "Failed to update user in FreeIPA",
|
|
483
|
+
status: freeipa.util.mapIpaErrorCode(response.error.code),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const result = response.result?.result as Record<string, unknown> | undefined;
|
|
488
|
+
// Partial patch: never touch uid_number, password expiry, last-login, or
|
|
489
|
+
// other full-sync fields when the user is only editing profile attributes.
|
|
490
|
+
await patchUserIpaData(
|
|
491
|
+
{
|
|
492
|
+
userId: id,
|
|
493
|
+
phone: data.ipa?.phone,
|
|
494
|
+
street: data.ipa?.address?.street,
|
|
495
|
+
postalCode: data.ipa?.address?.postalCode,
|
|
496
|
+
city: data.ipa?.address?.city,
|
|
497
|
+
state: data.ipa?.address?.state,
|
|
498
|
+
sshPublicKeys:
|
|
499
|
+
data.ipa?.sshPublicKeys !== undefined
|
|
500
|
+
? Array.isArray(result?.ipasshpubkey)
|
|
501
|
+
? (result?.ipasshpubkey as string[])
|
|
502
|
+
: []
|
|
503
|
+
: undefined,
|
|
504
|
+
sshFingerprints:
|
|
505
|
+
data.ipa?.sshPublicKeys !== undefined
|
|
506
|
+
? Array.isArray(result?.sshpubkeyfp)
|
|
507
|
+
? (result?.sshpubkeyfp as string[])
|
|
508
|
+
: []
|
|
509
|
+
: undefined,
|
|
510
|
+
syncedAt: new Date(),
|
|
511
|
+
},
|
|
512
|
+
tx,
|
|
513
|
+
);
|
|
450
514
|
}
|
|
451
515
|
|
|
452
|
-
|
|
453
|
-
// Partial patch: never touch uid_number, password expiry, last-login, or
|
|
454
|
-
// other full-sync fields when the user is only editing profile attributes.
|
|
455
|
-
await patchUserIpaData({
|
|
456
|
-
userId: id,
|
|
457
|
-
phone: data.ipa?.phone,
|
|
458
|
-
street: data.ipa?.address?.street,
|
|
459
|
-
postalCode: data.ipa?.address?.postalCode,
|
|
460
|
-
city: data.ipa?.address?.city,
|
|
461
|
-
state: data.ipa?.address?.state,
|
|
462
|
-
sshPublicKeys:
|
|
463
|
-
data.ipa?.sshPublicKeys !== undefined ? (Array.isArray(result?.ipasshpubkey) ? (result?.ipasshpubkey as string[]) : []) : undefined,
|
|
464
|
-
sshFingerprints:
|
|
465
|
-
data.ipa?.sshPublicKeys !== undefined ? (Array.isArray(result?.sshpubkeyfp) ? (result?.sshpubkeyfp as string[]) : []) : undefined,
|
|
466
|
-
syncedAt: new Date(),
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
await sql`
|
|
516
|
+
await tx`
|
|
471
517
|
UPDATE auth.users
|
|
472
518
|
SET given_name = CASE WHEN ${data.givenname !== undefined} THEN ${data.givenname ?? ""} ELSE given_name END,
|
|
473
519
|
sn = CASE WHEN ${data.sn !== undefined} THEN ${data.sn ?? ""} ELSE sn END,
|
|
@@ -475,11 +521,12 @@ export const updateProfile = async (params: {
|
|
|
475
521
|
WHERE id = ${id}
|
|
476
522
|
`;
|
|
477
523
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
524
|
+
if (data.mail !== undefined) {
|
|
525
|
+
await tx`UPDATE auth.users SET mail = ${data.mail || null} WHERE id = ${id}`;
|
|
526
|
+
}
|
|
481
527
|
|
|
482
|
-
|
|
528
|
+
return { ok: true, data: undefined };
|
|
529
|
+
});
|
|
483
530
|
};
|
|
484
531
|
|
|
485
532
|
// ==========================
|
|
@@ -42,7 +42,7 @@ export const legalConsent = {
|
|
|
42
42
|
groupsAdmin,
|
|
43
43
|
allowPendingLegalConsent: true,
|
|
44
44
|
});
|
|
45
|
-
if (!user || isAccountExpired(user.accountExpires)
|
|
45
|
+
if (!user || isAccountExpired(user.accountExpires)) return null;
|
|
46
46
|
const [row] = await sql<{ pending: boolean }[]>`
|
|
47
47
|
SELECT legal_pending AND NOT EXISTS (SELECT 1 FROM auth.legal_acceptances WHERE user_id = ${user.id}::uuid) AS pending
|
|
48
48
|
FROM auth.session_families WHERE sid = ${claims.sid}::uuid
|