@valentinkolb/cloud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +69 -0
- package/public/logo.svg +1 -0
- package/scripts/build.ts +113 -0
- package/scripts/preload.ts +73 -0
- package/src/_internal/define-app.ts +399 -0
- package/src/_internal/heartbeat.ts +33 -0
- package/src/_internal/registry.ts +100 -0
- package/src/_internal/runtime-context.ts +38 -0
- package/src/api/accounts-entities.ts +134 -0
- package/src/api/admin-lifecycle.ts +210 -0
- package/src/api/auth/schemas.ts +28 -0
- package/src/api/auth.ts +230 -0
- package/src/api/index.ts +66 -0
- package/src/api/me.ts +206 -0
- package/src/api/search/schemas.ts +43 -0
- package/src/api/search.ts +130 -0
- package/src/clients/core.ts +19 -0
- package/src/config/env.ts +23 -0
- package/src/config/index.ts +6 -0
- package/src/config/ssr.ts +58 -0
- package/src/contracts/app.ts +140 -0
- package/src/contracts/index.ts +5 -0
- package/src/contracts/profile.ts +67 -0
- package/src/contracts/registry.ts +50 -0
- package/src/contracts/settings-types.ts +84 -0
- package/src/contracts/shared.ts +258 -0
- package/src/contracts/widgets.ts +121 -0
- package/src/index.ts +6 -0
- package/src/server/api/index.ts +1 -0
- package/src/server/api/respond.ts +55 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/index.ts +62 -0
- package/src/server/middleware/auth.ts +168 -0
- package/src/server/middleware/index.ts +7 -0
- package/src/server/middleware/middleware.ts +47 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +126 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/validator.ts +35 -0
- package/src/server/services/access.ts +294 -0
- package/src/server/services/freeipa/client.ts +100 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +78 -0
- package/src/server/services/freeipa/tls.ts +48 -0
- package/src/server/services/freeipa/util.ts +60 -0
- package/src/server/services/geo.ts +154 -0
- package/src/server/services/index.ts +28 -0
- package/src/server/services/services.ts +13 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +907 -0
- package/src/services/account-lifecycle/scheduler.ts +347 -0
- package/src/services/account-model.ts +21 -0
- package/src/services/accounts/app.ts +966 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +45 -0
- package/src/services/accounts/entities.ts +529 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +246 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +64 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +491 -0
- package/src/services/accounts/model.ts +135 -0
- package/src/services/accounts/switching.ts +117 -0
- package/src/services/accounts/users.ts +714 -0
- package/src/services/auth-flows/index.ts +6 -0
- package/src/services/auth-flows/ipa.ts +128 -0
- package/src/services/auth-flows/magic-link.ts +119 -0
- package/src/services/freeipa-config.ts +89 -0
- package/src/services/index.ts +46 -0
- package/src/services/ipa/auth.ts +122 -0
- package/src/services/ipa/groups.ts +684 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/profile.ts +90 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/sync.ts +740 -0
- package/src/services/ipa/users.ts +794 -0
- package/src/services/logging/index.ts +294 -0
- package/src/services/notifications/email.ts +123 -0
- package/src/services/notifications/index.ts +413 -0
- package/src/services/postgres.ts +51 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +13 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +255 -0
- package/src/services/session/index.ts +137 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +101 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +824 -0
- package/src/services/settings/index.ts +203 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +179 -0
- package/src/services/settings/templates.ts +10 -0
- package/src/services/weather/forecast.ts +287 -0
- package/src/services/weather/geo.ts +110 -0
- package/src/services/weather/index.ts +99 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +125 -0
- package/src/services/weather/migrate.ts +22 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/shared/account-display.ts +17 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/icons.ts +109 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +58 -0
- package/src/shared/markdown/extensions/images.ts +43 -0
- package/src/shared/markdown/extensions/info-blocks.ts +93 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +34 -0
- package/src/shared/markdown/extensions/tables.ts +88 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/index.ts +97 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/ssr/AdminLayout.tsx +42 -0
- package/src/ssr/AdminSidebar.tsx +95 -0
- package/src/ssr/Footer.island.tsx +62 -0
- package/src/ssr/GlobalSearchDialog.tsx +389 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
- package/src/ssr/Layout.tsx +326 -0
- package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
- package/src/ssr/NavMenu.island.tsx +108 -0
- package/src/ssr/ThemeToggleRail.island.tsx +27 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/SearchBar.island.tsx +77 -0
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/runtime.ts +22 -0
- package/src/styles/base-popover.css +28 -0
- package/src/styles/effects.css +65 -0
- package/src/styles/global.css +133 -0
- package/src/styles/input.css +54 -0
- package/src/styles/tokens.css +35 -0
- package/src/styles/utilities-buttons.css +125 -0
- package/src/styles/utilities-feedback.css +65 -0
- package/src/styles/utilities-layout.css +122 -0
- package/src/styles/utilities-navigation.css +196 -0
- package/src/types/ambient.d.ts +8 -0
- package/src/ui/admin-settings.tsx +148 -0
- package/src/ui/dialog-core.ts +146 -0
- package/src/ui/filter/FilterChip.tsx +196 -0
- package/src/ui/filter/index.ts +2 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/input/Checkbox.tsx +55 -0
- package/src/ui/input/ColorInput.tsx +122 -0
- package/src/ui/input/DateTimeInput.tsx +86 -0
- package/src/ui/input/ImageInput.tsx +170 -0
- package/src/ui/input/NumberInput.tsx +113 -0
- package/src/ui/input/PinInput.tsx +169 -0
- package/src/ui/input/SegmentedControl.tsx +99 -0
- package/src/ui/input/Select.tsx +288 -0
- package/src/ui/input/SelectChip.tsx +61 -0
- package/src/ui/input/Slider.tsx +118 -0
- package/src/ui/input/Switch.tsx +62 -0
- package/src/ui/input/TagsInput.tsx +115 -0
- package/src/ui/input/TextInput.tsx +160 -0
- package/src/ui/input/index.ts +13 -0
- package/src/ui/input/types.ts +42 -0
- package/src/ui/input/util.tsx +105 -0
- package/src/ui/ipa/Avatar.tsx +28 -0
- package/src/ui/ipa/GroupView.tsx +36 -0
- package/src/ui/ipa/LoginBtn.tsx +16 -0
- package/src/ui/ipa/UserView.tsx +58 -0
- package/src/ui/ipa/index.ts +4 -0
- package/src/ui/misc/ContextMenu.tsx +211 -0
- package/src/ui/misc/CopyButton.tsx +28 -0
- package/src/ui/misc/Dropdown.tsx +194 -0
- package/src/ui/misc/EntitySearch.tsx +213 -0
- package/src/ui/misc/Lightbox.tsx +194 -0
- package/src/ui/misc/LinkCard.tsx +34 -0
- package/src/ui/misc/LogEntriesTable.tsx +61 -0
- package/src/ui/misc/MarkdownView.tsx +65 -0
- package/src/ui/misc/Pagination.tsx +51 -0
- package/src/ui/misc/PermissionEditor.tsx +379 -0
- package/src/ui/misc/ProgressBar.tsx +47 -0
- package/src/ui/misc/RemoveBtn.tsx +27 -0
- package/src/ui/misc/StatCell.tsx +90 -0
- package/src/ui/misc/index.ts +18 -0
- package/src/ui/navigation.ts +32 -0
- package/src/ui/prompts.tsx +854 -0
- package/src/ui/sidebar.tsx +468 -0
- package/src/ui/widgets/Widget.tsx +62 -0
- package/src/ui/widgets/WidgetCard.tsx +19 -0
- package/src/ui/widgets/WidgetHero.tsx +39 -0
- package/src/ui/widgets/WidgetList.tsx +84 -0
- package/src/ui/widgets/WidgetPills.tsx +68 -0
- package/src/ui/widgets/WidgetStat.tsx +67 -0
- package/src/ui/widgets/WidgetStatus.tsx +62 -0
- package/src/ui/widgets/index.ts +9 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { job, scheduler } from "@valentinkolb/sync";
|
|
2
|
+
import { logger, logging } from "../logging";
|
|
3
|
+
import { providers } from "../providers";
|
|
4
|
+
import { get as getSetting } from "../settings";
|
|
5
|
+
import { accountLifecycle } from "./index";
|
|
6
|
+
|
|
7
|
+
const log = logger("auth:lifecycle:scheduler");
|
|
8
|
+
const ipaSyncLog = logger("auth:ipa:sync");
|
|
9
|
+
const reminderLog = logger("auth:reminder:daily");
|
|
10
|
+
const guestCleanupLog = logger("auth:guest:cleanup");
|
|
11
|
+
const localUserCleanupLog = logger("auth:local-user:cleanup");
|
|
12
|
+
const auditCleanupLog = logger("auth:lifecycle:audit:cleanup");
|
|
13
|
+
const ipaBackfillLog = logger("auth:ipa:backfill");
|
|
14
|
+
const localUserBackfillLog = logger("auth:local-user:backfill");
|
|
15
|
+
const guestBackfillLog = logger("auth:guest:backfill");
|
|
16
|
+
const logCleanupLog = logger("logging");
|
|
17
|
+
const DEFAULT_IPA_SYNC_CRON = "*/5 * * * *";
|
|
18
|
+
|
|
19
|
+
type JobSummary = {
|
|
20
|
+
scanned: number;
|
|
21
|
+
changed: number;
|
|
22
|
+
skipped: number;
|
|
23
|
+
failed: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const abortedSummary = (): JobSummary => ({ scanned: 0, changed: 0, skipped: 0, failed: 0 });
|
|
27
|
+
|
|
28
|
+
const toDemotionLog = (summary: JobSummary) => ({
|
|
29
|
+
expiredCandidates: summary.scanned,
|
|
30
|
+
demotedToGuest: summary.changed,
|
|
31
|
+
skipped: summary.skipped,
|
|
32
|
+
failed: summary.failed,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const toReminderLog = (summary: JobSummary) => ({
|
|
36
|
+
candidates: summary.scanned,
|
|
37
|
+
sent: summary.changed,
|
|
38
|
+
skipped: summary.skipped,
|
|
39
|
+
failed: summary.failed,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const toCleanupLog = (summary: JobSummary) => ({
|
|
43
|
+
candidates: summary.scanned,
|
|
44
|
+
deleted: summary.changed,
|
|
45
|
+
skipped: summary.skipped,
|
|
46
|
+
failed: summary.failed,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const toBackfillLog = (summary: JobSummary) => ({
|
|
50
|
+
candidates: summary.scanned,
|
|
51
|
+
updated: summary.changed,
|
|
52
|
+
skipped: summary.skipped,
|
|
53
|
+
failed: summary.failed,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const getCronSetting = async (key: string, fallback: string): Promise<string> => {
|
|
57
|
+
const value = String((await getSetting<string>(key)) || "").trim();
|
|
58
|
+
return value.length > 0 ? value : fallback;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getTimezoneSetting = async (): Promise<string> => {
|
|
62
|
+
const value = String((await getSetting<string>("app.timezone")) || "").trim();
|
|
63
|
+
return value.length > 0 ? value : "Europe/Berlin";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retry policy shared by all lifecycle jobs: on transient failure, reschedule
|
|
68
|
+
* up to `maxAttempts - 1` times with exponential backoff from `baseMs`. Beyond
|
|
69
|
+
* that we go terminal and the next cron slot picks up the work.
|
|
70
|
+
*/
|
|
71
|
+
const retryOnError = (cfg: { maxAttempts: number; baseMs: number; maxMs?: number }) =>
|
|
72
|
+
({ ctx }: { ctx: { error?: Error; failureCount: number; reschedule: (cfg: { delayMs: number }) => void; expBackoff: (cfg: { baseMs: number; maxMs?: number }) => number } }) => {
|
|
73
|
+
if (!ctx.error) return;
|
|
74
|
+
if (ctx.failureCount >= cfg.maxAttempts - 1) return;
|
|
75
|
+
ctx.reschedule({ delayMs: ctx.expBackoff({ baseMs: cfg.baseMs, maxMs: cfg.maxMs }) });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ── Jobs ───────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const ipaSyncJob = job<void, JobSummary>({
|
|
81
|
+
id: "auth:ipa:sync",
|
|
82
|
+
defaults: { leaseMs: 120_000 },
|
|
83
|
+
process: async ({ ctx }) => {
|
|
84
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
85
|
+
try {
|
|
86
|
+
await providers.ipa.sync.run();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
ipaSyncLog.error("Sync step failed", { step: "sync", error: error instanceof Error ? error.message : String(error) });
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
await ctx.heartbeat();
|
|
92
|
+
try {
|
|
93
|
+
const summary = await accountLifecycle.demoteExpiredIpaUsers();
|
|
94
|
+
ipaSyncLog.info("Expired IPA demotion complete", toDemotionLog(summary));
|
|
95
|
+
return summary;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
ipaSyncLog.error("Expired IPA demotion step failed", { step: "demote-expired", error: error instanceof Error ? error.message : String(error) });
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const reminderJob = job<void, JobSummary>({
|
|
105
|
+
id: "auth:reminder:daily",
|
|
106
|
+
defaults: { leaseMs: 180_000 },
|
|
107
|
+
process: async ({ ctx }) => {
|
|
108
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
109
|
+
const summary = await accountLifecycle.sendExpiryReminders();
|
|
110
|
+
reminderLog.info("Reminder run complete", toReminderLog(summary));
|
|
111
|
+
return summary;
|
|
112
|
+
},
|
|
113
|
+
after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const guestCleanupJob = job<void, JobSummary>({
|
|
117
|
+
id: "auth:guest:cleanup",
|
|
118
|
+
defaults: { leaseMs: 120_000 },
|
|
119
|
+
process: async ({ ctx }) => {
|
|
120
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
121
|
+
const summary = await accountLifecycle.cleanupExpiredGuests();
|
|
122
|
+
guestCleanupLog.info("Expired guest cleanup complete", toCleanupLog(summary));
|
|
123
|
+
return summary;
|
|
124
|
+
},
|
|
125
|
+
after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const localUserCleanupJob = job<void, JobSummary>({
|
|
129
|
+
id: "auth:local-user:cleanup",
|
|
130
|
+
defaults: { leaseMs: 120_000 },
|
|
131
|
+
process: async ({ ctx }) => {
|
|
132
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
133
|
+
const summary = await accountLifecycle.cleanupExpiredLocalUsers();
|
|
134
|
+
localUserCleanupLog.info("Expired local user cleanup complete", toCleanupLog(summary));
|
|
135
|
+
return summary;
|
|
136
|
+
},
|
|
137
|
+
after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const auditCleanupJob = job<void, JobSummary>({
|
|
141
|
+
id: "auth:lifecycle:audit:cleanup",
|
|
142
|
+
defaults: { leaseMs: 120_000 },
|
|
143
|
+
process: async ({ ctx }) => {
|
|
144
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
145
|
+
const summary = await accountLifecycle.cleanupLifecycleAudit();
|
|
146
|
+
auditCleanupLog.info("Lifecycle audit cleanup complete", toCleanupLog(summary));
|
|
147
|
+
return summary;
|
|
148
|
+
},
|
|
149
|
+
after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const logCleanupJob = job<void, { deleted: number; retentionDays: number }>({
|
|
153
|
+
id: "app:logs:cleanup",
|
|
154
|
+
defaults: { leaseMs: 120_000 },
|
|
155
|
+
process: async ({ ctx }) => {
|
|
156
|
+
if (ctx.signal.aborted) return { deleted: 0, retentionDays: 0 };
|
|
157
|
+
const configured = Number((await getSetting<number | string | null>("logs.retention_days")) ?? 30);
|
|
158
|
+
const retentionDays = Number.isFinite(configured) ? configured : 30;
|
|
159
|
+
const summary = await logging.cleanup(retentionDays);
|
|
160
|
+
logCleanupLog.info("Log cleanup complete", { deleted: summary.deleted, retentionDays });
|
|
161
|
+
return { deleted: summary.deleted, retentionDays };
|
|
162
|
+
},
|
|
163
|
+
after: retryOnError({ maxAttempts: 3, baseMs: 1000 }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const ipaBackfillJob = job<void, JobSummary>({
|
|
167
|
+
id: "auth:ipa:backfill",
|
|
168
|
+
defaults: { leaseMs: 300_000 },
|
|
169
|
+
process: async ({ ctx }) => {
|
|
170
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
171
|
+
const summary = await accountLifecycle.runIpaBackfill();
|
|
172
|
+
ipaBackfillLog.info("IPA expiry backfill complete", toBackfillLog(summary));
|
|
173
|
+
return summary;
|
|
174
|
+
},
|
|
175
|
+
after: retryOnError({ maxAttempts: 2, baseMs: 2000 }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const guestBackfillJob = job<void, JobSummary>({
|
|
179
|
+
id: "auth:guest:backfill",
|
|
180
|
+
defaults: { leaseMs: 300_000 },
|
|
181
|
+
process: async ({ ctx }) => {
|
|
182
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
183
|
+
const summary = await accountLifecycle.runGuestBackfill();
|
|
184
|
+
guestBackfillLog.info("Guest expiry backfill complete", toBackfillLog(summary));
|
|
185
|
+
return summary;
|
|
186
|
+
},
|
|
187
|
+
after: retryOnError({ maxAttempts: 2, baseMs: 2000 }),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const localUserBackfillJob = job<void, JobSummary>({
|
|
191
|
+
id: "auth:local-user:backfill",
|
|
192
|
+
defaults: { leaseMs: 300_000 },
|
|
193
|
+
process: async ({ ctx }) => {
|
|
194
|
+
if (ctx.signal.aborted) return abortedSummary();
|
|
195
|
+
const summary = await accountLifecycle.runLocalUserBackfill();
|
|
196
|
+
localUserBackfillLog.info("Local user expiry backfill complete", toBackfillLog(summary));
|
|
197
|
+
return summary;
|
|
198
|
+
},
|
|
199
|
+
after: retryOnError({ maxAttempts: 2, baseMs: 2000 }),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ── Scheduler ──────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
const lifecycleScheduler = scheduler({ id: "auth-lifecycle" });
|
|
205
|
+
|
|
206
|
+
let started = false;
|
|
207
|
+
let registered = false;
|
|
208
|
+
let registerPromise: Promise<void> | null = null;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Register (or update) a cron-triggered schedule that fans out to the given
|
|
212
|
+
* job. `scheduler.create` is idempotent by id — same cron/tz keeps `nextRunAt`
|
|
213
|
+
* intact; a change resets it. We submit one dispatch per slot using the slot
|
|
214
|
+
* timestamp as idempotency key so misfires don't double-run.
|
|
215
|
+
*/
|
|
216
|
+
const createSchedule = async (config: {
|
|
217
|
+
id: string;
|
|
218
|
+
cron: string;
|
|
219
|
+
tz: string;
|
|
220
|
+
submit: (key: string) => Promise<string>;
|
|
221
|
+
}): Promise<void> => {
|
|
222
|
+
await lifecycleScheduler.create({
|
|
223
|
+
id: config.id,
|
|
224
|
+
cron: config.cron,
|
|
225
|
+
tz: config.tz,
|
|
226
|
+
process: async ({ ctx }) => {
|
|
227
|
+
await config.submit(`slot:${ctx.slotTs}`);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const createScheduleWithFallback = async (config: {
|
|
233
|
+
id: string;
|
|
234
|
+
cron: string;
|
|
235
|
+
fallbackCron: string;
|
|
236
|
+
tz: string;
|
|
237
|
+
submit: (key: string) => Promise<string>;
|
|
238
|
+
settingsKey: string;
|
|
239
|
+
}): Promise<void> => {
|
|
240
|
+
try {
|
|
241
|
+
await createSchedule({ id: config.id, cron: config.cron, tz: config.tz, submit: config.submit });
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (config.cron === config.fallbackCron) throw error;
|
|
244
|
+
log.warn("Invalid configured cron, falling back to default", {
|
|
245
|
+
key: config.settingsKey,
|
|
246
|
+
configuredCron: config.cron,
|
|
247
|
+
fallbackCron: config.fallbackCron,
|
|
248
|
+
timezone: config.tz,
|
|
249
|
+
error: error instanceof Error ? error.message : String(error),
|
|
250
|
+
});
|
|
251
|
+
await createSchedule({ id: config.id, cron: config.fallbackCron, tz: config.tz, submit: config.submit });
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const doRegister = async (): Promise<void> => {
|
|
256
|
+
const [scheduleTz, ipaSyncCron, reminderCron, cleanupCron] = await Promise.all([
|
|
257
|
+
getTimezoneSetting(),
|
|
258
|
+
getCronSetting("freeipa.sync_cron", DEFAULT_IPA_SYNC_CRON),
|
|
259
|
+
getCronSetting("user.account.reminder_cron", "0 9 * * *"),
|
|
260
|
+
getCronSetting("app.cleanup_schedule", "0 4 * * *"),
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
await createScheduleWithFallback({
|
|
264
|
+
id: "auth:ipa:sync",
|
|
265
|
+
cron: ipaSyncCron,
|
|
266
|
+
fallbackCron: DEFAULT_IPA_SYNC_CRON,
|
|
267
|
+
tz: scheduleTz,
|
|
268
|
+
settingsKey: "freeipa.sync_cron",
|
|
269
|
+
submit: (key) => ipaSyncJob.submit({ key }),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await createSchedule({
|
|
273
|
+
id: "auth:reminder:daily",
|
|
274
|
+
cron: reminderCron,
|
|
275
|
+
tz: scheduleTz,
|
|
276
|
+
submit: (key) => reminderJob.submit({ key }),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await createSchedule({
|
|
280
|
+
id: "auth:guest:cleanup",
|
|
281
|
+
cron: cleanupCron,
|
|
282
|
+
tz: scheduleTz,
|
|
283
|
+
submit: (key) => guestCleanupJob.submit({ key }),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await createSchedule({
|
|
287
|
+
id: "auth:local-user:cleanup",
|
|
288
|
+
cron: cleanupCron,
|
|
289
|
+
tz: scheduleTz,
|
|
290
|
+
submit: (key) => localUserCleanupJob.submit({ key }),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await createSchedule({
|
|
294
|
+
id: "auth:lifecycle:audit:cleanup",
|
|
295
|
+
cron: cleanupCron,
|
|
296
|
+
tz: scheduleTz,
|
|
297
|
+
submit: (key) => auditCleanupJob.submit({ key }),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await createSchedule({
|
|
301
|
+
id: "app:logs:cleanup",
|
|
302
|
+
cron: cleanupCron,
|
|
303
|
+
tz: scheduleTz,
|
|
304
|
+
submit: (key) => logCleanupJob.submit({ key }),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
registered = true;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const ensureRegistered = async (): Promise<void> => {
|
|
311
|
+
if (registered) return;
|
|
312
|
+
if (!registerPromise) {
|
|
313
|
+
registerPromise = doRegister().finally(() => {
|
|
314
|
+
registerPromise = null;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
await registerPromise;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const lifecycleJobs = {
|
|
321
|
+
start: async (): Promise<void> => {
|
|
322
|
+
if (!started) {
|
|
323
|
+
lifecycleScheduler.start();
|
|
324
|
+
started = true;
|
|
325
|
+
}
|
|
326
|
+
await ensureRegistered();
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
stop: async (): Promise<void> => {
|
|
330
|
+
if (!started) return;
|
|
331
|
+
await lifecycleScheduler.stop();
|
|
332
|
+
started = false;
|
|
333
|
+
registered = false;
|
|
334
|
+
registerPromise = null;
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
// Manual triggers — use a timestamp key so repeated presses within the same
|
|
338
|
+
// millisecond are deduped but subsequent calls always enqueue a new run.
|
|
339
|
+
submitIpaBackfill: (): Promise<string> => ipaBackfillJob.submit({ key: `manual:${Date.now()}` }),
|
|
340
|
+
submitLocalUserBackfill: (): Promise<string> => localUserBackfillJob.submit({ key: `manual:${Date.now()}` }),
|
|
341
|
+
submitGuestBackfill: (): Promise<string> => guestBackfillJob.submit({ key: `manual:${Date.now()}` }),
|
|
342
|
+
submitReminderRun: (): Promise<string> => reminderJob.submit({ key: `manual:${Date.now()}` }),
|
|
343
|
+
submitIpaSync: (): Promise<string> => ipaSyncJob.submit({ key: `manual:${Date.now()}` }),
|
|
344
|
+
|
|
345
|
+
metrics: () => lifecycleScheduler.metric(),
|
|
346
|
+
listSchedules: async () => lifecycleScheduler.list(),
|
|
347
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
canPersistStoredAdmin,
|
|
3
|
+
calculateIpaProfileFromGroupNames,
|
|
4
|
+
deriveIpaAdminFromGroupNames,
|
|
5
|
+
isGuestProfile,
|
|
6
|
+
isIpaProvider,
|
|
7
|
+
isLocalProvider,
|
|
8
|
+
getConfiguredExpiryDays,
|
|
9
|
+
getDefaultAccountExpiry,
|
|
10
|
+
normalizeManualAccountExpiry,
|
|
11
|
+
parseManualAccountExpiry,
|
|
12
|
+
parseIpaAccountTransitionPolicy,
|
|
13
|
+
parseIpaMatchMode,
|
|
14
|
+
resolveEffectiveAdminState,
|
|
15
|
+
resolveAccountExpires,
|
|
16
|
+
resolveStoredAdminState,
|
|
17
|
+
resolveTargetAccountExpiry,
|
|
18
|
+
type IpaAccountTransitionPolicy,
|
|
19
|
+
type IpaMatchMode,
|
|
20
|
+
} from "./accounts/model";
|
|
21
|
+
export { buildRoles } from "./accounts/authz";
|