@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.
- package/package.json +18 -8
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +119 -47
- package/src/_internal/runtime-context.ts +1 -0
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +15 -25
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +4 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +4 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ok } from "@valentinkolb/stdlib";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
ActiveAnnouncementsResponseSchema,
|
|
7
|
+
AnnouncementEntrySchema,
|
|
8
|
+
AnnouncementListResponseSchema,
|
|
9
|
+
CreateAnnouncementSchema,
|
|
10
|
+
ErrorResponseSchema,
|
|
11
|
+
MessageResponseSchema,
|
|
12
|
+
parseAnnouncementCookieHeader,
|
|
13
|
+
UpdateAnnouncementSchema,
|
|
14
|
+
} from "../contracts";
|
|
15
|
+
import { type AuthContext, auth, jsonResponse, requiresAdmin, requiresAuth, respond, v } from "../server";
|
|
16
|
+
import { announcements } from "../services";
|
|
17
|
+
|
|
18
|
+
const IdParamSchema = z.object({ id: z.uuid() });
|
|
19
|
+
|
|
20
|
+
const withMessage = async <T>(operation: Promise<import("@valentinkolb/stdlib").Result<T>>, message: string) => {
|
|
21
|
+
const result = await operation;
|
|
22
|
+
if (!result.ok) return result;
|
|
23
|
+
return ok({ message });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const announcementRoutes = new Hono<AuthContext>().get(
|
|
27
|
+
"/active",
|
|
28
|
+
auth.requireRole("authenticated"),
|
|
29
|
+
describeRoute({
|
|
30
|
+
tags: ["Announcements"],
|
|
31
|
+
summary: "List active user announcements",
|
|
32
|
+
description: "Returns active banners and unseen announcements for the current request cookie state.",
|
|
33
|
+
...requiresAuth,
|
|
34
|
+
responses: {
|
|
35
|
+
200: jsonResponse(ActiveAnnouncementsResponseSchema, "Active announcements"),
|
|
36
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
async (c) => {
|
|
40
|
+
const state = parseAnnouncementCookieHeader(c.req.header("Cookie"));
|
|
41
|
+
return respond(c, ok(await announcements.active.forState({ state })));
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const adminAnnouncementRoutes = new Hono<AuthContext>()
|
|
46
|
+
.use(auth.requireRole("admin"))
|
|
47
|
+
.get(
|
|
48
|
+
"/",
|
|
49
|
+
describeRoute({
|
|
50
|
+
tags: ["Admin Announcements"],
|
|
51
|
+
summary: "List announcements",
|
|
52
|
+
...requiresAdmin,
|
|
53
|
+
responses: {
|
|
54
|
+
200: jsonResponse(AnnouncementListResponseSchema, "Announcements"),
|
|
55
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
56
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
v(
|
|
60
|
+
"query",
|
|
61
|
+
z.object({
|
|
62
|
+
kind: z.enum(["announcement", "banner"]).optional(),
|
|
63
|
+
search: z.string().optional(),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
async (c) => {
|
|
67
|
+
const query = c.req.valid("query");
|
|
68
|
+
const items = await announcements.admin.list({
|
|
69
|
+
filter: { kind: query.kind, query: query.search },
|
|
70
|
+
});
|
|
71
|
+
return respond(c, ok({ items }));
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
.post(
|
|
75
|
+
"/",
|
|
76
|
+
describeRoute({
|
|
77
|
+
tags: ["Admin Announcements"],
|
|
78
|
+
summary: "Create announcement",
|
|
79
|
+
...requiresAdmin,
|
|
80
|
+
responses: {
|
|
81
|
+
201: jsonResponse(AnnouncementEntrySchema, "Created announcement"),
|
|
82
|
+
400: jsonResponse(ErrorResponseSchema, "Validation error"),
|
|
83
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
84
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
v("json", CreateAnnouncementSchema),
|
|
88
|
+
async (c) => respond(c, announcements.admin.create({ data: c.req.valid("json"), actorId: c.get("user").id }), 201),
|
|
89
|
+
)
|
|
90
|
+
.patch(
|
|
91
|
+
"/:id",
|
|
92
|
+
describeRoute({
|
|
93
|
+
tags: ["Admin Announcements"],
|
|
94
|
+
summary: "Update announcement",
|
|
95
|
+
...requiresAdmin,
|
|
96
|
+
responses: {
|
|
97
|
+
200: jsonResponse(AnnouncementEntrySchema, "Updated announcement"),
|
|
98
|
+
400: jsonResponse(ErrorResponseSchema, "Validation error"),
|
|
99
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
100
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
101
|
+
404: jsonResponse(ErrorResponseSchema, "Announcement not found"),
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
v("param", IdParamSchema),
|
|
105
|
+
v("json", UpdateAnnouncementSchema),
|
|
106
|
+
async (c) =>
|
|
107
|
+
respond(
|
|
108
|
+
c,
|
|
109
|
+
announcements.admin.update({
|
|
110
|
+
id: c.req.valid("param").id,
|
|
111
|
+
data: c.req.valid("json"),
|
|
112
|
+
actorId: c.get("user").id,
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
.delete(
|
|
117
|
+
"/:id",
|
|
118
|
+
describeRoute({
|
|
119
|
+
tags: ["Admin Announcements"],
|
|
120
|
+
summary: "Delete announcement",
|
|
121
|
+
...requiresAdmin,
|
|
122
|
+
responses: {
|
|
123
|
+
200: jsonResponse(MessageResponseSchema, "Announcement deleted"),
|
|
124
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
125
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
126
|
+
404: jsonResponse(ErrorResponseSchema, "Announcement not found"),
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
v("param", IdParamSchema),
|
|
130
|
+
async (c) => respond(c, withMessage(announcements.admin.remove({ id: c.req.valid("param").id }), "Announcement deleted.")),
|
|
131
|
+
);
|
package/src/api/auth/schemas.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const LoginSchema = z.object({
|
|
|
11
11
|
export const EmailLoginSchema = z.object({
|
|
12
12
|
email: z.email(),
|
|
13
13
|
acceptedAgb: z.literal(true),
|
|
14
|
+
redirectTo: z.string().max(2048).optional(),
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
export const VerifyTokenSchema = z.object({
|
|
@@ -18,6 +19,24 @@ export const VerifyTokenSchema = z.object({
|
|
|
18
19
|
acceptedAgb: z.literal(true),
|
|
19
20
|
});
|
|
20
21
|
|
|
22
|
+
export const PasswordResetRequestSchema = z.object({
|
|
23
|
+
email: z.email(),
|
|
24
|
+
acceptedAgb: z.literal(true),
|
|
25
|
+
redirectTo: z.string().max(2048).optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const PasswordResetCompleteSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
token: z.uuid(),
|
|
31
|
+
newPassword: z.string().min(8),
|
|
32
|
+
confirmPassword: z.string().min(1),
|
|
33
|
+
acceptedAgb: z.literal(true),
|
|
34
|
+
})
|
|
35
|
+
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
36
|
+
message: "Passwords do not match",
|
|
37
|
+
path: ["confirmPassword"],
|
|
38
|
+
});
|
|
39
|
+
|
|
21
40
|
export const AdminLoginSchema = z.object({
|
|
22
41
|
token: z.string().min(1),
|
|
23
42
|
});
|
|
@@ -26,3 +45,8 @@ export const AuthResponseSchema = z.object({
|
|
|
26
45
|
session_token: z.string(),
|
|
27
46
|
user: UserSchema,
|
|
28
47
|
});
|
|
48
|
+
|
|
49
|
+
export const VerifyPasskeyAuthenticationSchema = z.object({
|
|
50
|
+
response: z.unknown(),
|
|
51
|
+
acceptedAgb: z.literal(true),
|
|
52
|
+
});
|
package/src/api/auth.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Hono, type Context } from "hono";
|
|
2
2
|
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { v } from "../server";
|
|
4
5
|
import { jsonResponse } from "../server";
|
|
5
6
|
import { auth, type AuthContext } from "../server";
|
|
6
7
|
import { rateLimit } from "../server";
|
|
7
|
-
import {
|
|
8
|
+
import { respond } from "../server";
|
|
9
|
+
import { authFlows, accounts, getFreeIpaConfig, logger, webauthn } from "../services";
|
|
8
10
|
import { sql } from "bun";
|
|
9
11
|
import { env } from "../config";
|
|
10
12
|
import { ChangeExpiredPasswordSchema } from "../contracts";
|
|
@@ -14,8 +16,11 @@ import {
|
|
|
14
16
|
LoginSchema,
|
|
15
17
|
EmailLoginSchema,
|
|
16
18
|
VerifyTokenSchema,
|
|
19
|
+
PasswordResetRequestSchema,
|
|
20
|
+
PasswordResetCompleteSchema,
|
|
17
21
|
AdminLoginSchema,
|
|
18
22
|
AuthResponseSchema,
|
|
23
|
+
VerifyPasskeyAuthenticationSchema,
|
|
19
24
|
} from "./auth/schemas";
|
|
20
25
|
import { ErrorResponseSchema, MessageResponseSchema } from "../contracts";
|
|
21
26
|
|
|
@@ -56,7 +61,7 @@ const app = new Hono<AuthContext>()
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
// Store minimal session in Redis
|
|
59
|
-
const sessionToken = await auth.session.create(c, loginResult.userId
|
|
64
|
+
const sessionToken = await auth.session.create(c, loginResult.userId);
|
|
60
65
|
|
|
61
66
|
log.info("Login successful", { uid: username });
|
|
62
67
|
return c.json({
|
|
@@ -65,13 +70,51 @@ const app = new Hono<AuthContext>()
|
|
|
65
70
|
});
|
|
66
71
|
},
|
|
67
72
|
)
|
|
73
|
+
.post(
|
|
74
|
+
"/passkeys/authentication/start",
|
|
75
|
+
describeRoute({
|
|
76
|
+
tags: ["Auth"],
|
|
77
|
+
summary: "Start passkey login",
|
|
78
|
+
description: "Create WebAuthn authentication options for passkey sign-in.",
|
|
79
|
+
responses: {
|
|
80
|
+
200: jsonResponse(z.unknown(), "Passkey authentication options"),
|
|
81
|
+
400: jsonResponse(ErrorResponseSchema, "Passkey login is not available"),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
async (c) => respond(c, webauthn.beginAuthentication()),
|
|
85
|
+
)
|
|
86
|
+
.post(
|
|
87
|
+
"/passkeys/authentication/verify",
|
|
88
|
+
describeRoute({
|
|
89
|
+
tags: ["Auth"],
|
|
90
|
+
summary: "Verify passkey login",
|
|
91
|
+
description: "Verify a WebAuthn authentication response and create a normal Cloud session.",
|
|
92
|
+
responses: {
|
|
93
|
+
200: jsonResponse(AuthResponseSchema, "Passkey login successful"),
|
|
94
|
+
401: jsonResponse(ErrorResponseSchema, "Passkey verification failed"),
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
v("json", VerifyPasskeyAuthenticationSchema),
|
|
98
|
+
async (c) => {
|
|
99
|
+
const result = await webauthn.finishAuthentication({
|
|
100
|
+
response: c.req.valid("json").response as never,
|
|
101
|
+
});
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
const status = result.error.status;
|
|
104
|
+
return c.json({ message: result.error.message, code: result.error.code }, status as 400 | 401 | 403 | 404 | 409 | 500);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sessionToken = await auth.session.create(c, result.data.user.id);
|
|
108
|
+
log.info("Passkey login successful", { uid: result.data.user.uid });
|
|
109
|
+
return c.json({ session_token: sessionToken, user: result.data.user });
|
|
110
|
+
},
|
|
111
|
+
)
|
|
68
112
|
.post(
|
|
69
113
|
"/logout",
|
|
70
114
|
describeRoute({
|
|
71
115
|
tags: ["Auth"],
|
|
72
116
|
summary: "Logout",
|
|
73
|
-
description:
|
|
74
|
-
"Idempotent: clears the session cookie and deletes the session key if present. No authentication required — logout must always succeed.",
|
|
117
|
+
description: "Idempotent: clears the session cookie and deletes the session key if present. No authentication required — logout must always succeed.",
|
|
75
118
|
responses: {
|
|
76
119
|
200: jsonResponse(MessageResponseSchema, "Session invalidated"),
|
|
77
120
|
},
|
|
@@ -116,7 +159,7 @@ const app = new Hono<AuthContext>()
|
|
|
116
159
|
return jsonError(c, changeResult.message, changeResult.status === 401 ? 401 : 400);
|
|
117
160
|
}
|
|
118
161
|
|
|
119
|
-
const sessionToken = await auth.session.create(c, changeResult.userId
|
|
162
|
+
const sessionToken = await auth.session.create(c, changeResult.userId);
|
|
120
163
|
|
|
121
164
|
log.info("Password changed via expired flow", { uid: username });
|
|
122
165
|
return c.json({ session_token: sessionToken, user: changeResult.user });
|
|
@@ -135,9 +178,12 @@ const app = new Hono<AuthContext>()
|
|
|
135
178
|
}),
|
|
136
179
|
v("json", EmailLoginSchema),
|
|
137
180
|
async (c) => {
|
|
138
|
-
const { email } = c.req.valid("json");
|
|
181
|
+
const { email, redirectTo } = c.req.valid("json");
|
|
139
182
|
|
|
140
|
-
const requestResult = await authFlows.magicLink.request({
|
|
183
|
+
const requestResult = await authFlows.magicLink.request({
|
|
184
|
+
email,
|
|
185
|
+
redirectTo,
|
|
186
|
+
});
|
|
141
187
|
if (!requestResult.ok) {
|
|
142
188
|
return c.json({ message: requestResult.message }, requestResult.status);
|
|
143
189
|
}
|
|
@@ -171,15 +217,72 @@ const app = new Hono<AuthContext>()
|
|
|
171
217
|
}
|
|
172
218
|
|
|
173
219
|
// Create session (no IPA session for email-only users)
|
|
174
|
-
const sessionToken = await auth.session.create(c, verifyResult.userId
|
|
220
|
+
const sessionToken = await auth.session.create(c, verifyResult.userId);
|
|
175
221
|
|
|
176
222
|
if (verifyResult.createdGuest) {
|
|
177
|
-
log.info("Guest user created", {
|
|
223
|
+
log.info("Guest user created", {
|
|
224
|
+
email: verifyResult.email,
|
|
225
|
+
uid: verifyResult.user.uid,
|
|
226
|
+
});
|
|
178
227
|
}
|
|
179
228
|
log.info("Token verified", { email: verifyResult.email });
|
|
180
229
|
return c.json({ session_token: sessionToken, user: verifyResult.user });
|
|
181
230
|
},
|
|
182
231
|
)
|
|
232
|
+
.post(
|
|
233
|
+
"/password-reset/request",
|
|
234
|
+
describeRoute({
|
|
235
|
+
tags: ["Auth"],
|
|
236
|
+
summary: "Request password reset",
|
|
237
|
+
description: "Request a one-time password reset email for an IPA-backed account. The response is always generic to avoid account enumeration.",
|
|
238
|
+
responses: {
|
|
239
|
+
200: jsonResponse(MessageResponseSchema, "Request accepted"),
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
v("json", PasswordResetRequestSchema),
|
|
243
|
+
async (c) => {
|
|
244
|
+
const { email, redirectTo } = c.req.valid("json");
|
|
245
|
+
|
|
246
|
+
const result = await authFlows.passwordReset.request({
|
|
247
|
+
email,
|
|
248
|
+
redirectTo,
|
|
249
|
+
});
|
|
250
|
+
return c.json({ message: result.message });
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
.post(
|
|
254
|
+
"/password-reset/complete",
|
|
255
|
+
describeRoute({
|
|
256
|
+
tags: ["Auth"],
|
|
257
|
+
summary: "Complete password reset",
|
|
258
|
+
description: "Set a new password using a one-time reset token. Creates a normal Cloud session only after the password was changed successfully.",
|
|
259
|
+
responses: {
|
|
260
|
+
200: jsonResponse(AuthResponseSchema, "Password reset completed and session created"),
|
|
261
|
+
400: jsonResponse(ErrorResponseSchema, "Password reset failed"),
|
|
262
|
+
401: jsonResponse(ErrorResponseSchema, "Invalid or expired reset token"),
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
v("json", PasswordResetCompleteSchema),
|
|
266
|
+
async (c) => {
|
|
267
|
+
const { token, newPassword } = c.req.valid("json");
|
|
268
|
+
|
|
269
|
+
const result = await authFlows.passwordReset.complete({
|
|
270
|
+
token,
|
|
271
|
+
newPassword,
|
|
272
|
+
});
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
if (result.reason === "policy_failed") {
|
|
275
|
+
return c.json({ message: result.message }, 400);
|
|
276
|
+
}
|
|
277
|
+
const status = result.status >= 500 ? 500 : result.status === 401 ? 401 : 400;
|
|
278
|
+
return jsonError(c, result.message, status);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const sessionToken = await auth.session.create(c, result.userId);
|
|
282
|
+
log.info("Password reset completed", { uid: result.user.uid });
|
|
283
|
+
return c.json({ user: result.user, session_token: sessionToken });
|
|
284
|
+
},
|
|
285
|
+
)
|
|
183
286
|
.post(
|
|
184
287
|
"/admin-login",
|
|
185
288
|
describeRoute({
|
|
@@ -221,7 +324,7 @@ const app = new Hono<AuthContext>()
|
|
|
221
324
|
const user = await accounts.users.get({ uid: "admin" });
|
|
222
325
|
if (!user) return jsonError(c, "Failed to resolve admin user.", 500);
|
|
223
326
|
|
|
224
|
-
const sessionToken = await auth.session.create(c, user.id
|
|
327
|
+
const sessionToken = await auth.session.create(c, user.id);
|
|
225
328
|
log.info("Admin login successful");
|
|
226
329
|
return c.json({ session_token: sessionToken, user });
|
|
227
330
|
},
|
package/src/api/index.ts
CHANGED
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
* Apps that need a typed client to these routes import from
|
|
9
9
|
* `@valentinkolb/cloud/clients/core`. The client and the routes share their
|
|
10
10
|
* type via `CoreApiType` below.
|
|
11
|
+
*
|
|
12
|
+
* The OpenAPI spec for these routes is generated by `defineApp` (driven by
|
|
13
|
+
* core's `openapi: "/api/openapi.json"` opt-in) — this file no longer mounts
|
|
14
|
+
* any docs UI; the api-docs aggregator at `/app/api-docs` is the only consumer.
|
|
11
15
|
*/
|
|
12
16
|
import { Hono } from "hono";
|
|
13
|
-
import { Scalar } from "@scalar/hono-api-reference";
|
|
14
|
-
import { generateSpecs } from "hono-openapi";
|
|
15
17
|
import { prettyJSON } from "hono/pretty-json";
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
+
import accountsEntitiesRoutes from "./accounts-entities";
|
|
19
|
+
import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
20
|
+
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
21
|
+
import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
|
|
18
22
|
import authRoutes from "./auth";
|
|
19
23
|
import meRoutes from "./me";
|
|
20
|
-
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
21
|
-
import accountsEntitiesRoutes from "./accounts-entities";
|
|
22
24
|
import { createSearchRoutes } from "./search";
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -33,6 +35,9 @@ const buildCoreApi = () => {
|
|
|
33
35
|
.route("/auth", authRoutes)
|
|
34
36
|
.route("/me", meRoutes)
|
|
35
37
|
.route("/accounts", accountsEntitiesRoutes)
|
|
38
|
+
.route("/announcements", announcementRoutes)
|
|
39
|
+
.route("/admin/core/announcements", adminAnnouncementRoutes)
|
|
40
|
+
.route("/admin/core/settings", adminCoreSettingsRoutes)
|
|
36
41
|
.route("/admin/lifecycle", adminLifecycleRoutes)
|
|
37
42
|
.route("/", searchRoutes);
|
|
38
43
|
};
|
|
@@ -41,26 +46,11 @@ const buildCoreApi = () => {
|
|
|
41
46
|
export type CoreApiType = ReturnType<typeof buildCoreApi>;
|
|
42
47
|
|
|
43
48
|
/**
|
|
44
|
-
* Build the core router
|
|
45
|
-
*
|
|
49
|
+
* Build the core router. The core-app calls this and mounts the returned
|
|
50
|
+
* router under `/api`.
|
|
46
51
|
*/
|
|
47
|
-
export const createCoreApiRouter =
|
|
52
|
+
export const createCoreApiRouter = () => {
|
|
48
53
|
const api = buildCoreApi();
|
|
49
|
-
|
|
50
|
-
const spec = await generateSpecs(api, openApiMeta);
|
|
51
|
-
const llmsTxt = await createMarkdownFromOpenApi(JSON.stringify(spec));
|
|
52
|
-
|
|
53
|
-
api.get("/openapi.json", (c) => c.json(spec));
|
|
54
|
-
api.get(
|
|
55
|
-
"/docs",
|
|
56
|
-
Scalar({
|
|
57
|
-
theme: "saturn",
|
|
58
|
-
url: "/api/openapi.json",
|
|
59
|
-
hideClientButton: true,
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
|
|
63
54
|
api.all("/*", (c) => c.json({ message: "API route not found" }, 404));
|
|
64
|
-
|
|
65
|
-
return { api, llmsTxt };
|
|
55
|
+
return { api };
|
|
66
56
|
};
|