@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,17 @@
1
+ import { providers } from "../providers";
2
+ import { search } from "./search";
3
+
4
+ export const ipa = {
5
+ auth: providers.ipa.auth,
6
+ users: {
7
+ ...providers.ipa.users,
8
+ addIpa: providers.ipa.users.create,
9
+ delete: providers.ipa.users.remove,
10
+ },
11
+ groups: {
12
+ ...providers.ipa.groups,
13
+ delete: providers.ipa.groups.remove,
14
+ },
15
+ search,
16
+ sync: providers.ipa.sync,
17
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Centralized profile calculation for IPA-backed users.
3
+ *
4
+ * The calculation uses the local mirrored IPA group tree only and treats
5
+ * `auth.groups.id` as the canonical group identity.
6
+ */
7
+
8
+ import { sql } from "bun";
9
+ import { calculateIpaProfileFromGroupNames } from "../account-model";
10
+ import { getFreeIpaConfig } from "../freeipa-config";
11
+
12
+ type DbRow = Record<string, unknown>;
13
+
14
+ /**
15
+ * Get all IPA group names a user belongs to (direct + inherited via parent groups).
16
+ */
17
+ export const getAllUserGroups = async (userId: string): Promise<string[]> => {
18
+ const rows: DbRow[] = await sql`
19
+ WITH RECURSIVE all_groups AS (
20
+ SELECT ug.group_id
21
+ FROM auth.user_groups_v2 ug
22
+ JOIN auth.groups g ON g.id = ug.group_id
23
+ WHERE ug.user_id = ${userId} AND g.provider = 'ipa'
24
+ UNION
25
+ SELECT gg.parent_group_id
26
+ FROM auth.group_groups_v2 gg
27
+ JOIN auth.groups g ON g.id = gg.parent_group_id
28
+ JOIN all_groups ag ON gg.child_group_id = ag.group_id
29
+ WHERE g.provider = 'ipa'
30
+ )
31
+ SELECT DISTINCT g.name
32
+ FROM all_groups ag
33
+ JOIN auth.groups g ON g.id = ag.group_id
34
+ ORDER BY g.name
35
+ `;
36
+
37
+ return rows.map((row) => row.name as string);
38
+ };
39
+
40
+ /**
41
+ * Calculate canonical IPA profile from effective group names. Reads
42
+ * `freeipa.groups.base_ipa_realm` from settings (cache-aside).
43
+ */
44
+ export const calculateIpaProfile = async (memberOfGroups: string[]): Promise<"user" | "guest"> => {
45
+ const config = await getFreeIpaConfig();
46
+ return calculateIpaProfileFromGroupNames(memberOfGroups, config.groupsBaseIpaRealm);
47
+ };
48
+
49
+ /**
50
+ * Calculate canonical IPA profile for a user from the local DB mirror.
51
+ */
52
+ export const calculateIpaProfileFromLocalDb = async (userId: string): Promise<"user" | "guest"> => {
53
+ const groups = await getAllUserGroups(userId);
54
+ return calculateIpaProfile(groups);
55
+ };
56
+
57
+ /**
58
+ * Update one IPA-backed user's canonical profile projection.
59
+ */
60
+ export const updateUserIpaProfile = async (userId: string): Promise<void> => {
61
+ const profile = await calculateIpaProfileFromLocalDb(userId);
62
+ await sql`
63
+ UPDATE auth.users
64
+ SET provider = 'ipa',
65
+ profile = ${profile}
66
+ WHERE id = ${userId} AND provider = 'ipa'
67
+ `;
68
+ };
69
+
70
+ /**
71
+ * Update all IPA-backed users affected by a group hierarchy change.
72
+ */
73
+ export const updateProfileForAffectedUsers = async (groupId: string): Promise<void> => {
74
+ const affectedUsers: DbRow[] = await sql`
75
+ WITH RECURSIVE affected_groups AS (
76
+ SELECT ${groupId}::uuid AS group_id
77
+ UNION
78
+ SELECT gg.child_group_id
79
+ FROM auth.group_groups_v2 gg
80
+ JOIN affected_groups ag ON gg.parent_group_id = ag.group_id
81
+ )
82
+ SELECT DISTINCT ug.user_id
83
+ FROM auth.user_groups_v2 ug
84
+ JOIN affected_groups ag ON ug.group_id = ag.group_id
85
+ `;
86
+
87
+ for (const row of affectedUsers) {
88
+ await updateUserIpaProfile(row.user_id as string);
89
+ }
90
+ };
@@ -0,0 +1,154 @@
1
+ import { sql } from "bun";
2
+ import type { BaseUser, BaseGroup, UserProvider, UserProfile } from "../../contracts/shared";
3
+ import { buildRoles } from "../account-model";
4
+ import { freeipa } from "../../server/services";
5
+ import { toPgTextArray, toPgUuidArray } from "../postgres";
6
+ import { getFreeIpaConfig } from "../freeipa-config";
7
+
8
+ // ==========================
9
+ // Search Options
10
+ // ==========================
11
+
12
+ export type SearchOptions = {
13
+ /** Search users */
14
+ users?: boolean;
15
+ /** Search groups */
16
+ groups?: boolean;
17
+ /** User UUIDs to exclude from results */
18
+ excludeUserIds?: string[];
19
+ /** Group IDs to exclude from results */
20
+ excludeGroups?: string[];
21
+ /** Only return groups the user is a member of */
22
+ onlyUserGroups?: string[];
23
+ /** Only return POSIX groups (have gid_number) */
24
+ onlyPosixGroups?: boolean;
25
+ /** Only return users that are members of these groups */
26
+ usersInGroups?: string[];
27
+ };
28
+
29
+ // ==========================
30
+ // Search (autocomplete for member/manager add dialogs)
31
+ // Returns BaseUser/BaseGroup (no relations needed for autocomplete)
32
+ // ==========================
33
+
34
+ /**
35
+ * Executes a filtered lookup query and returns normalized matches.
36
+ */
37
+ export const search = async (query: string, options: SearchOptions): Promise<{ users: BaseUser[]; groups: BaseGroup[] }> => {
38
+ const q = `%${freeipa.util.escapeLike(query.toLowerCase())}%`;
39
+ let users: BaseUser[] = [];
40
+ let groups: BaseGroup[] = [];
41
+
42
+ // ========== Search Users ==========
43
+ if (options.users) {
44
+ const excludeIds = options.excludeUserIds ?? [];
45
+ const inGroups = options.usersInGroups ?? [];
46
+ const groupsAdmin = (await getFreeIpaConfig()).groupsAdmin;
47
+
48
+ // Build optional WHERE fragments
49
+ const excludeFilter = excludeIds.length > 0 ? sql`AND u.id <> ALL(${toPgUuidArray(excludeIds)}::uuid[])` : sql``;
50
+ const groupFilter =
51
+ inGroups.length > 0
52
+ ? sql`AND EXISTS (
53
+ SELECT 1
54
+ FROM auth.user_groups_v2 ug
55
+ JOIN auth.groups g ON g.id = ug.group_id
56
+ WHERE ug.user_id = u.id
57
+ AND g.provider = 'ipa'
58
+ AND ug.group_id = ANY(${toPgUuidArray(inGroups)}::uuid[])
59
+ )`
60
+ : sql``;
61
+
62
+ const rows = await sql`
63
+ SELECT u.id, u.uid, u.provider, u.profile, u.given_name, u.sn, u.display_name, u.mail,
64
+ EXISTS(
65
+ SELECT 1
66
+ FROM auth.user_groups_v2 ug_admin
67
+ JOIN auth.groups g_admin ON g_admin.id = ug_admin.group_id
68
+ WHERE ug_admin.user_id = u.id
69
+ AND g_admin.provider = 'ipa'
70
+ AND g_admin.name = ANY(${toPgTextArray(groupsAdmin)}::text[])
71
+ ) AS effective_admin
72
+ FROM auth.users u
73
+ WHERE u.provider = 'ipa'
74
+ AND (
75
+ LOWER(u.uid) LIKE ${q} ESCAPE '\\' OR LOWER(u.display_name) LIKE ${q} ESCAPE '\\' OR
76
+ LOWER(u.given_name) LIKE ${q} ESCAPE '\\' OR LOWER(u.sn) LIKE ${q} ESCAPE '\\' OR LOWER(u.mail) LIKE ${q} ESCAPE '\\'
77
+ )
78
+ ${excludeFilter}
79
+ ${groupFilter}
80
+ ORDER BY u.uid
81
+ LIMIT 10
82
+ `;
83
+
84
+ type UserRow = {
85
+ id: string;
86
+ uid: string;
87
+ provider: UserProvider;
88
+ profile: UserProfile;
89
+ effective_admin: boolean | null;
90
+ given_name: string | null;
91
+ sn: string | null;
92
+ display_name: string | null;
93
+ mail: string | null;
94
+ };
95
+ users = (rows as UserRow[]).map((row) => ({
96
+ id: row.id,
97
+ uid: row.uid,
98
+ roles: buildRoles({
99
+ provider: row.provider,
100
+ profile: row.profile,
101
+ memberofGroup: [],
102
+ manages: [],
103
+ admin: Boolean(row.effective_admin),
104
+ }),
105
+ provider: row.provider,
106
+ profile: row.profile,
107
+ givenname: row.given_name ?? "",
108
+ sn: row.sn ?? "",
109
+ displayName: row.display_name ?? "",
110
+ mail: row.mail ?? null,
111
+ }));
112
+ }
113
+
114
+ // ========== Search Groups ==========
115
+ if (options.groups) {
116
+ const excludeIds = options.excludeGroups ?? [];
117
+ const onlyUserGroups = options.onlyUserGroups ?? [];
118
+ const onlyPosix = options.onlyPosixGroups ?? false;
119
+
120
+ // Build optional WHERE fragments
121
+ const excludeFilter = excludeIds.length > 0 ? sql`AND id <> ALL(${toPgUuidArray(excludeIds)}::uuid[])` : sql``;
122
+ const userGroupsFilter = onlyUserGroups.length > 0 ? sql`AND id = ANY(${toPgUuidArray(onlyUserGroups)}::uuid[])` : sql``;
123
+ const posixFilter = onlyPosix ? sql`AND gid_number IS NOT NULL` : sql``;
124
+
125
+ const rows = await sql`
126
+ SELECT id, provider, name, description, gid_number
127
+ FROM auth.groups
128
+ WHERE provider = 'ipa'
129
+ AND (LOWER(name) LIKE ${q} ESCAPE '\\' OR LOWER(description) LIKE ${q} ESCAPE '\\')
130
+ ${excludeFilter}
131
+ ${userGroupsFilter}
132
+ ${posixFilter}
133
+ ORDER BY name
134
+ LIMIT 10
135
+ `;
136
+
137
+ type GroupRow = {
138
+ id: string;
139
+ provider: UserProvider;
140
+ name: string;
141
+ description: string | null;
142
+ gid_number: number | null;
143
+ };
144
+ groups = (rows as GroupRow[]).map((row) => ({
145
+ id: row.id,
146
+ provider: row.provider,
147
+ name: row.name,
148
+ description: row.description ?? null,
149
+ gidnumber: row.gid_number ?? null,
150
+ }));
151
+ }
152
+
153
+ return { users, groups };
154
+ };