dineway 0.1.4 → 0.1.6
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 +6 -3
- package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
- package/dist/astro/index.d.mts +18 -9
- package/dist/astro/index.mjs +238 -16
- package/dist/astro/middleware/auth.d.mts +16 -5
- package/dist/astro/middleware/auth.mjs +74 -37
- package/dist/astro/middleware/redirect.mjs +24 -8
- package/dist/astro/middleware/request-context.mjs +18 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +411 -169
- package/dist/astro/types.d.mts +25 -8
- package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
- package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
- package/dist/cache-BdSY-gQN.mjs +42 -0
- package/dist/chunks--4F8ddV4.mjs +18 -0
- package/dist/cli/index.mjs +935 -15
- package/dist/client/external-auth-headers.d.mts +1 -1
- package/dist/client/index.d.mts +11 -3
- package/dist/client/index.mjs +4 -3
- package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
- package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
- package/dist/database/instrumentation.d.mts +34 -0
- package/dist/database/instrumentation.mjs +53 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.mjs +11 -5
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +7 -1
- package/dist/db-errors-CEqD7qH9.mjs +23 -0
- package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
- package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
- package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +24 -22
- package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/page/index.d.mts +10 -2
- package/dist/page/index.mjs +22 -1
- package/dist/patterns-CrCYkMBb.mjs +92 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
- package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
- package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
- package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
- package/dist/request-cache-Dk5qPSOx.mjs +66 -0
- package/dist/request-context.d.mts +4 -16
- package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
- package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BNruJHDL.mjs → search-BApX1xhM.mjs} +570 -424
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +11 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.mjs +78 -15
- package/dist/taxonomies-1s5PaS_8.mjs +266 -0
- package/dist/transaction-Cn2rjY78.mjs +27 -0
- package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
- package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
- package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
- package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
- package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
- package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
- package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
- package/dist/version-hmtC3Cmv.mjs +6 -0
- package/package.json +49 -38
- package/src/astro/routes/admin.astro +25 -9
- package/src/astro/routes/api/admin/api-tokens/[id].ts +4 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +24 -2
- package/src/astro/routes/api/admin/briefing.ts +76 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
- package/src/astro/routes/api/admin/bylines/index.ts +2 -0
- package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
- package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
- package/src/astro/routes/api/admin/context/diff.ts +35 -0
- package/src/astro/routes/api/admin/context/index.ts +69 -0
- package/src/astro/routes/api/admin/context/stale.ts +35 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
- package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +58 -17
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +28 -1
- package/src/astro/routes/api/admin/oauth-clients/index.ts +25 -1
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +54 -2
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +54 -2
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +51 -1
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +98 -3
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +72 -1
- package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
- package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +26 -23
- package/src/astro/routes/api/admin/users/[id]/index.ts +41 -21
- package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/auth/signup/request.ts +20 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +3 -4
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +16 -2
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +16 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +9 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +45 -1
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +12 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +24 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +3 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +20 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +13 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +36 -0
- package/src/astro/routes/api/content/[collection]/index.ts +48 -4
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/health.ts +54 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +2 -10
- package/src/astro/routes/api/import/wordpress/execute.ts +40 -6
- package/src/astro/routes/api/import/wordpress/prepare.ts +36 -5
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +33 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +3 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +57 -15
- package/src/astro/routes/api/manifest.ts +13 -1
- package/src/astro/routes/api/mcp.ts +1 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +7 -2
- package/src/astro/routes/api/media/upload-url.ts +11 -2
- package/src/astro/routes/api/media.ts +9 -7
- package/src/astro/routes/api/menus/[name]/items.ts +124 -5
- package/src/astro/routes/api/menus/[name]/reorder.ts +47 -1
- package/src/astro/routes/api/menus/[name].ts +84 -4
- package/src/astro/routes/api/menus/index.ts +46 -2
- package/src/astro/routes/api/oauth/authorize.ts +21 -8
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/oauth/register.ts +182 -0
- package/src/astro/routes/api/oauth/token.ts +18 -7
- package/src/astro/routes/api/openapi.json.ts +3 -2
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +21 -4
- package/src/astro/routes/api/redirects/[id].ts +103 -4
- package/src/astro/routes/api/redirects/index.ts +50 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +28 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +15 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +13 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +27 -0
- package/src/astro/routes/api/schema/collections/index.ts +14 -0
- package/src/astro/routes/api/search/index.ts +1 -0
- package/src/astro/routes/api/search/suggest.ts +1 -0
- package/src/astro/routes/api/sections/[slug].ts +123 -4
- package/src/astro/routes/api/sections/index.ts +57 -2
- package/src/astro/routes/api/settings.ts +51 -2
- package/src/astro/routes/api/setup/admin-verify.ts +25 -5
- package/src/astro/routes/api/setup/admin.ts +16 -8
- package/src/astro/routes/api/setup/index.ts +3 -2
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +141 -4
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +64 -2
- package/src/astro/routes/api/taxonomies/index.ts +57 -2
- package/src/astro/routes/api/well-known/auth.ts +3 -1
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +8 -5
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +3 -2
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +58 -16
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +124 -38
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +66 -20
- package/src/astro/routes/api/widget-areas/[name].ts +55 -7
- package/src/astro/routes/api/widget-areas/index.ts +56 -6
- package/src/components/DinewayHead.astro +15 -7
- package/src/components/DinewayMedia.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +1 -1
- package/src/components/Table.astro +68 -41
- package/src/components/index.ts +2 -12
- package/src/components/marks.ts +19 -0
- package/LICENSE +0 -9
- /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
- /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
- /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
- /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
- /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
- /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
- /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
- /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
- /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
- /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin review requests
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/admin/review-requests - List review requests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
import { requirePerm } from "#api/authorize.js";
|
|
10
|
+
import { handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
11
|
+
import { handleReviewRequestList } from "#api/handlers/review-requests.js";
|
|
12
|
+
import { isParseError, parseQuery } from "#api/parse.js";
|
|
13
|
+
import { reviewRequestListQuery } from "#api/schemas.js";
|
|
14
|
+
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
|
|
17
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
18
|
+
const { dineway, user } = locals;
|
|
19
|
+
|
|
20
|
+
const dbErr = requireDb(dineway?.db);
|
|
21
|
+
if (dbErr) return dbErr;
|
|
22
|
+
|
|
23
|
+
const denied = requirePerm(user, "content:edit_any");
|
|
24
|
+
if (denied) return denied;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const query = parseQuery(url, reviewRequestListQuery);
|
|
28
|
+
if (isParseError(query)) return query;
|
|
29
|
+
|
|
30
|
+
const result = await handleReviewRequestList(dineway.db, query);
|
|
31
|
+
return unwrapResult(result);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return handleError(error, "Failed to list review requests", "REVIEW_REQUEST_LIST_ERROR");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -9,6 +9,7 @@ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
|
9
9
|
import type { APIRoute } from "astro";
|
|
10
10
|
|
|
11
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
|
+
import { withTransaction } from "#db/transaction.js";
|
|
12
13
|
|
|
13
14
|
export const prerender = false;
|
|
14
15
|
|
|
@@ -23,8 +24,6 @@ export const POST: APIRoute = async ({ params, locals }) => {
|
|
|
23
24
|
return apiError("FORBIDDEN", "Admin privileges required", 403);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const adapter = createKyselyAdapter(dineway.db);
|
|
27
|
-
|
|
28
27
|
const { id } = params;
|
|
29
28
|
|
|
30
29
|
if (!id) {
|
|
@@ -37,32 +36,36 @@ export const POST: APIRoute = async ({ params, locals }) => {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
return await withTransaction(dineway.db, async (trx) => {
|
|
40
|
+
const adapter = createKyselyAdapter(trx);
|
|
41
|
+
|
|
42
|
+
// Get target user
|
|
43
|
+
const targetUser = await adapter.getUserById(id);
|
|
44
|
+
if (!targetUser) {
|
|
45
|
+
return apiError("NOT_FOUND", "User not found", 404);
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
// Check if this would leave no active admins
|
|
49
|
+
if (targetUser.role === Role.ADMIN) {
|
|
50
|
+
const adminCount = await adapter.countAdmins();
|
|
51
|
+
if (adminCount <= 1) {
|
|
52
|
+
return apiError(
|
|
53
|
+
"VALIDATION_ERROR",
|
|
54
|
+
"Cannot disable the last admin. Promote another user first.",
|
|
55
|
+
400,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
|
-
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
// Disable user
|
|
61
|
+
await adapter.updateUser(id, { disabled: true });
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// SEC-43: Revoke all OAuth tokens for the disabled user.
|
|
64
|
+
// Without this, existing refresh tokens remain valid for up to 90 days.
|
|
65
|
+
await trx.deleteFrom("_dineway_oauth_tokens").where("user_id", "=", id).execute();
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
return apiSuccess({ success: true });
|
|
68
|
+
});
|
|
66
69
|
} catch (error) {
|
|
67
70
|
return handleError(error, "Failed to disable user", "USER_DISABLE_ERROR");
|
|
68
71
|
}
|
|
@@ -12,6 +12,7 @@ import type { APIRoute } from "astro";
|
|
|
12
12
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
13
13
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
14
|
import { userUpdateBody } from "#api/schemas.js";
|
|
15
|
+
import { withTransaction } from "#db/transaction.js";
|
|
15
16
|
|
|
16
17
|
export const prerender = false;
|
|
17
18
|
|
|
@@ -117,28 +118,47 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
return await withTransaction(dineway.db, async (trx) => {
|
|
122
|
+
const txAdapter = createKyselyAdapter(trx);
|
|
123
|
+
const latestTargetUser = await txAdapter.getUserById(id);
|
|
124
|
+
if (!latestTargetUser) {
|
|
125
|
+
return apiError("NOT_FOUND", "User not found", 404);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (role !== undefined && latestTargetUser.role === Role.ADMIN && role < Role.ADMIN) {
|
|
129
|
+
const adminCount = await txAdapter.countAdmins();
|
|
130
|
+
if (adminCount <= 1) {
|
|
131
|
+
return apiError(
|
|
132
|
+
"VALIDATION_ERROR",
|
|
133
|
+
"Cannot demote the last admin. Promote another user first.",
|
|
134
|
+
400,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// Update user
|
|
140
|
+
await txAdapter.updateUser(id, {
|
|
141
|
+
name: body.name,
|
|
142
|
+
email: body.email,
|
|
143
|
+
role,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Fetch updated user
|
|
147
|
+
const updated = await txAdapter.getUserById(id);
|
|
148
|
+
|
|
149
|
+
return apiSuccess({
|
|
150
|
+
item: {
|
|
151
|
+
id: updated!.id,
|
|
152
|
+
email: updated!.email,
|
|
153
|
+
name: updated!.name,
|
|
154
|
+
avatarUrl: updated!.avatarUrl,
|
|
155
|
+
role: updated!.role,
|
|
156
|
+
emailVerified: updated!.emailVerified,
|
|
157
|
+
disabled: updated!.disabled,
|
|
158
|
+
createdAt: updated!.createdAt.toISOString(),
|
|
159
|
+
updatedAt: updated!.updatedAt.toISOString(),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
142
162
|
});
|
|
143
163
|
} catch (error) {
|
|
144
164
|
return handleError(error, "Failed to update user", "USER_UPDATE_ERROR");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/invite/register-options
|
|
3
|
+
*
|
|
4
|
+
* Generate WebAuthn registration options for an invited user.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { validateInvite, InviteError } from "@dineway-ai/auth";
|
|
12
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
13
|
+
import { generateRegistrationOptions } from "@dineway-ai/auth/passkey";
|
|
14
|
+
import { ulid } from "ulidx";
|
|
15
|
+
|
|
16
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
17
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
18
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
19
|
+
import { inviteRegisterOptionsBody } from "#api/schemas.js";
|
|
20
|
+
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
21
|
+
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
23
|
+
|
|
24
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
25
|
+
const { dineway } = locals;
|
|
26
|
+
|
|
27
|
+
if (!dineway?.db) {
|
|
28
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const body = await parseBody(request, inviteRegisterOptionsBody);
|
|
33
|
+
if (isParseError(body)) return body;
|
|
34
|
+
|
|
35
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
36
|
+
const invite = await validateInvite(adapter, body.token);
|
|
37
|
+
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
const optionsRepo = new OptionsRepository(dineway.db);
|
|
40
|
+
const siteName = (await optionsRepo.get<string>("dineway:site_title")) ?? undefined;
|
|
41
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
42
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
43
|
+
|
|
44
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
45
|
+
const registrationOptions = await generateRegistrationOptions(
|
|
46
|
+
passkeyConfig,
|
|
47
|
+
{
|
|
48
|
+
id: ulid(),
|
|
49
|
+
email: invite.email,
|
|
50
|
+
name: body.name || null,
|
|
51
|
+
},
|
|
52
|
+
[],
|
|
53
|
+
challengeStore,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return apiSuccess({ options: registrationOptions });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof InviteError) {
|
|
59
|
+
const statusMap: Record<string, number> = {
|
|
60
|
+
invalid_token: 404,
|
|
61
|
+
token_expired: 410,
|
|
62
|
+
user_exists: 409,
|
|
63
|
+
};
|
|
64
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return handleError(
|
|
68
|
+
error,
|
|
69
|
+
"Failed to generate registration options",
|
|
70
|
+
"INVITE_REGISTER_OPTIONS_ERROR",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -19,6 +19,7 @@ import { isParseError, parseBody } from "#api/parse.js";
|
|
|
19
19
|
import { magicLinkSendBody } from "#api/schemas.js";
|
|
20
20
|
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
21
|
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
22
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
23
24
|
|
|
24
25
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
@@ -36,7 +37,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
36
37
|
if (isParseError(body)) return body;
|
|
37
38
|
|
|
38
39
|
// Rate limit: 3 requests per 300 seconds (5 minutes) per IP
|
|
39
|
-
const ip = getClientIp(request);
|
|
40
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
40
41
|
const rateLimit = await checkRateLimit(dineway.db, ip, "magic-link/send", 3, 300);
|
|
41
42
|
if (!rateLimit.allowed) {
|
|
42
43
|
// Return success-shaped response to avoid revealing rate limit
|
|
@@ -20,6 +20,7 @@ import { passkeyOptionsBody } from "#api/schemas.js";
|
|
|
20
20
|
import { createChallengeStore, cleanupExpiredChallenges } from "#auth/challenge-store.js";
|
|
21
21
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
22
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
23
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
23
24
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
24
25
|
|
|
25
26
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
@@ -38,7 +39,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
38
39
|
if (isParseError(body)) return body;
|
|
39
40
|
|
|
40
41
|
// Rate limit: 10 requests per 60 seconds per IP
|
|
41
|
-
const ip = getClientIp(request);
|
|
42
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
42
43
|
const rateLimit = await checkRateLimit(dineway.db, ip, "passkey/options", 10, 60);
|
|
43
44
|
if (!rateLimit.allowed) {
|
|
44
45
|
return rateLimitResponse(60);
|
|
@@ -9,7 +9,7 @@ import type { APIRoute } from "astro";
|
|
|
9
9
|
export const prerender = false;
|
|
10
10
|
|
|
11
11
|
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
12
|
-
import { authenticateWithPasskey } from "@dineway-ai/auth/passkey";
|
|
12
|
+
import { authenticateWithPasskey, PasskeyAuthenticationError } from "@dineway-ai/auth/passkey";
|
|
13
13
|
|
|
14
14
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
@@ -63,6 +63,10 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
63
63
|
},
|
|
64
64
|
});
|
|
65
65
|
} catch (error) {
|
|
66
|
+
if (error instanceof PasskeyAuthenticationError) {
|
|
67
|
+
return apiError("UNAUTHORIZED", "Authentication failed", 401);
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
return handleError(error, "Authentication failed", "PASSKEY_VERIFY_ERROR");
|
|
67
71
|
}
|
|
68
72
|
};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Request self-signup. Sends verification email if domain is allowed.
|
|
5
5
|
* Always returns 200 to prevent email enumeration.
|
|
6
|
+
*
|
|
7
|
+
* Rate limited: 3 requests per 5 minutes per IP. Mirrors magic-link/send.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { APIRoute } from "astro";
|
|
@@ -16,8 +18,15 @@ import { apiError, apiSuccess } from "#api/error.js";
|
|
|
16
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
17
19
|
import { signupRequestBody } from "#api/schemas.js";
|
|
18
20
|
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
|
+
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
19
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
20
24
|
|
|
25
|
+
const GENERIC_SUCCESS = {
|
|
26
|
+
success: true,
|
|
27
|
+
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
22
31
|
const { dineway } = locals;
|
|
23
32
|
|
|
@@ -38,6 +47,15 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
38
47
|
const body = await parseBody(request, signupRequestBody);
|
|
39
48
|
if (isParseError(body)) return body;
|
|
40
49
|
|
|
50
|
+
// Rate limit: 3 requests per 300 seconds per IP. Return the same
|
|
51
|
+
// response as the normal path so callers cannot observe the limit.
|
|
52
|
+
const trustedHeaders = getTrustedProxyHeaders(dineway.config);
|
|
53
|
+
const ip = getClientIp(request, trustedHeaders);
|
|
54
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "signup/request", 3, 300);
|
|
55
|
+
if (!rateLimit.allowed) {
|
|
56
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
const adapter = createKyselyAdapter(dineway.db);
|
|
42
60
|
|
|
43
61
|
// Get site config for signup email
|
|
@@ -60,18 +78,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
60
78
|
);
|
|
61
79
|
|
|
62
80
|
// Always return success to prevent email enumeration
|
|
63
|
-
return apiSuccess(
|
|
64
|
-
success: true,
|
|
65
|
-
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
66
|
-
});
|
|
81
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
67
82
|
} catch (error) {
|
|
68
83
|
console.error("Signup request error:", error);
|
|
69
84
|
|
|
70
85
|
// Don't reveal internal errors - just return generic success
|
|
71
86
|
// to prevent information leakage
|
|
72
|
-
return apiSuccess(
|
|
73
|
-
success: true,
|
|
74
|
-
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
75
|
-
});
|
|
87
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
76
88
|
}
|
|
77
89
|
};
|
|
@@ -15,6 +15,7 @@ import { getSiteBaseUrl } from "#api/site-url.js";
|
|
|
15
15
|
import { sendCommentNotification } from "#comments/notifications.js";
|
|
16
16
|
import { createComment, type CommentHookRunner } from "#comments/service.js";
|
|
17
17
|
import { CommentRepository } from "#db/repositories/comment.js";
|
|
18
|
+
import { validateIdentifier } from "#db/validate.js";
|
|
18
19
|
import { extractRequestMeta } from "#plugins/request-meta.js";
|
|
19
20
|
import type { CollectionCommentSettings, ModerationDecision } from "#plugins/types.js";
|
|
20
21
|
|
|
@@ -106,6 +107,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Verify the content item exists, is published, and not soft-deleted
|
|
110
|
+
validateIdentifier(collection, "collection");
|
|
109
111
|
const contentRow = await dineway.db
|
|
110
112
|
.selectFrom(`ec_${collection}` as never)
|
|
111
113
|
.select(["id" as never, "slug" as never, "author_id" as never, "published_at" as never])
|
|
@@ -137,15 +139,12 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
// Anti-spam: Rate limiting
|
|
140
|
-
const meta = extractRequestMeta(request);
|
|
142
|
+
const meta = extractRequestMeta(request, dineway.config);
|
|
141
143
|
const ipSalt =
|
|
142
144
|
import.meta.env.DINEWAY_AUTH_SECRET || import.meta.env.AUTH_SECRET || "dineway-ip-salt";
|
|
143
145
|
let ipHash: string;
|
|
144
146
|
if (meta.ip) {
|
|
145
147
|
ipHash = await hashIp(meta.ip, ipSalt);
|
|
146
|
-
} else if (meta.userAgent) {
|
|
147
|
-
// Fallback: hash user-agent as a rough identifier when IP is unavailable
|
|
148
|
-
ipHash = await hashIp(`ua:${meta.userAgent}`, ipSalt);
|
|
149
148
|
} else {
|
|
150
149
|
// Fail closed: all unidentifiable requests share one rate-limit bucket.
|
|
151
150
|
// Use a larger limit since this bucket is shared across all anonymous users.
|
|
@@ -13,7 +13,7 @@ export const prerender = false;
|
|
|
13
13
|
|
|
14
14
|
export const GET: APIRoute = async ({ params, locals }) => {
|
|
15
15
|
const { dineway, user } = locals;
|
|
16
|
-
const denied = requirePerm(user, "content:
|
|
16
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
17
17
|
if (denied) return denied;
|
|
18
18
|
const collection = params.collection!;
|
|
19
19
|
const id = params.id!;
|
|
@@ -8,6 +8,11 @@ import type { APIRoute } from "astro";
|
|
|
8
8
|
|
|
9
9
|
import { requireOwnerPerm } from "#api/authorize.js";
|
|
10
10
|
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
11
|
+
import {
|
|
12
|
+
contentApiRouteSource,
|
|
13
|
+
extractActivityItemId,
|
|
14
|
+
logContentActivity,
|
|
15
|
+
} from "#site-context/activity-events.js";
|
|
11
16
|
|
|
12
17
|
export const prerender = false;
|
|
13
18
|
|
|
@@ -43,12 +48,21 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
43
48
|
const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : "";
|
|
44
49
|
const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
45
50
|
if (denied) return denied;
|
|
51
|
+
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
46
52
|
|
|
47
|
-
const result = await dineway.handleContentDiscardDraft(collection,
|
|
53
|
+
const result = await dineway.handleContentDiscardDraft(collection, resolvedId);
|
|
48
54
|
|
|
49
55
|
if (!result.success) return unwrapResult(result);
|
|
50
56
|
|
|
51
|
-
if (cache.enabled) await cache.invalidate({ tags: [collection,
|
|
57
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
|
|
58
|
+
|
|
59
|
+
await logContentActivity(dineway.db, locals, {
|
|
60
|
+
action: "draft_discarded",
|
|
61
|
+
collection,
|
|
62
|
+
entryId: extractActivityItemId(result.data, resolvedId),
|
|
63
|
+
...contentApiRouteSource("draft_discarded"),
|
|
64
|
+
summary: `Discarded draft for content in ${collection}`,
|
|
65
|
+
});
|
|
52
66
|
|
|
53
67
|
return unwrapResult(result);
|
|
54
68
|
};
|
|
@@ -8,6 +8,11 @@ import type { APIRoute } from "astro";
|
|
|
8
8
|
|
|
9
9
|
import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
|
|
10
10
|
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
11
|
+
import {
|
|
12
|
+
contentApiRouteSource,
|
|
13
|
+
extractActivityItemId,
|
|
14
|
+
logContentActivity,
|
|
15
|
+
} from "#site-context/activity-events.js";
|
|
11
16
|
|
|
12
17
|
export const prerender = false;
|
|
13
18
|
|
|
@@ -57,5 +62,16 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
57
62
|
|
|
58
63
|
if (cache.enabled) await cache.invalidate({ tags: [collection] });
|
|
59
64
|
|
|
65
|
+
await logContentActivity(dineway.db, locals, {
|
|
66
|
+
action: "duplicated",
|
|
67
|
+
collection,
|
|
68
|
+
entryId: extractActivityItemId(result.data),
|
|
69
|
+
...contentApiRouteSource("duplicated"),
|
|
70
|
+
summary: `Duplicated content in ${collection}`,
|
|
71
|
+
detail: {
|
|
72
|
+
sourceEntryId: resolvedId,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
60
76
|
return unwrapResult(result, 201);
|
|
61
77
|
};
|
|
@@ -8,6 +8,7 @@ import type { APIRoute } from "astro";
|
|
|
8
8
|
|
|
9
9
|
import { requirePerm } from "#api/authorize.js";
|
|
10
10
|
import { apiError, unwrapResult } from "#api/error.js";
|
|
11
|
+
import { contentApiRouteSource, logContentActivity } from "#site-context/activity-events.js";
|
|
11
12
|
|
|
12
13
|
export const prerender = false;
|
|
13
14
|
|
|
@@ -29,5 +30,13 @@ export const DELETE: APIRoute = async ({ params, locals, cache }) => {
|
|
|
29
30
|
|
|
30
31
|
if (cache.enabled) await cache.invalidate({ tags: [collection, id] });
|
|
31
32
|
|
|
33
|
+
await logContentActivity(dineway.db, locals, {
|
|
34
|
+
action: "permanently_deleted",
|
|
35
|
+
collection,
|
|
36
|
+
entryId: id,
|
|
37
|
+
...contentApiRouteSource("permanently_deleted"),
|
|
38
|
+
summary: `Permanently deleted content in ${collection}`,
|
|
39
|
+
});
|
|
40
|
+
|
|
32
41
|
return unwrapResult(result);
|
|
33
42
|
};
|
|
@@ -30,7 +30,7 @@ const DURATION_PATTERN = /^(\d+)([smhdw])$/;
|
|
|
30
30
|
|
|
31
31
|
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
32
32
|
const { dineway, user } = locals;
|
|
33
|
-
const denied = requirePerm(user, "content:
|
|
33
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
34
34
|
if (denied) return denied;
|
|
35
35
|
const collection = params.collection!;
|
|
36
36
|
const id = params.id!;
|
|
@@ -8,10 +8,18 @@ import type { APIRoute } from "astro";
|
|
|
8
8
|
|
|
9
9
|
import { requireOwnerPerm } from "#api/authorize.js";
|
|
10
10
|
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
11
|
+
import { isParseError, parseOptionalBody } from "#api/parse.js";
|
|
12
|
+
import { contentPublishBody } from "#api/schemas.js";
|
|
13
|
+
import {
|
|
14
|
+
contentApiRouteSource,
|
|
15
|
+
extractActivityItemId,
|
|
16
|
+
logContentActivity,
|
|
17
|
+
} from "#site-context/activity-events.js";
|
|
18
|
+
import { resolveActorIdentity, RiskPolicyEvaluator } from "#site-context/index.js";
|
|
11
19
|
|
|
12
20
|
export const prerender = false;
|
|
13
21
|
|
|
14
|
-
export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
22
|
+
export const POST: APIRoute = async ({ params, request, locals, cache }) => {
|
|
15
23
|
const { dineway, user } = locals;
|
|
16
24
|
const collection = params.collection!;
|
|
17
25
|
const id = params.id!;
|
|
@@ -45,6 +53,31 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
45
53
|
if (denied) return denied;
|
|
46
54
|
|
|
47
55
|
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
56
|
+
const body = await parseOptionalBody(request, contentPublishBody, {});
|
|
57
|
+
if (isParseError(body)) return body;
|
|
58
|
+
|
|
59
|
+
const actor = resolveActorIdentity({
|
|
60
|
+
user: user ? { id: user.id } : null,
|
|
61
|
+
tokenScopes: locals.tokenScopes,
|
|
62
|
+
authToken: locals.authToken,
|
|
63
|
+
});
|
|
64
|
+
const evaluator = new RiskPolicyEvaluator({
|
|
65
|
+
db: dineway.db,
|
|
66
|
+
handlers: dineway,
|
|
67
|
+
});
|
|
68
|
+
const reviewRequestId = body.reviewRequestId ?? body.review_request_id;
|
|
69
|
+
const decision = await evaluator.evaluateContentPublishReview({
|
|
70
|
+
actor,
|
|
71
|
+
collection,
|
|
72
|
+
id: resolvedId,
|
|
73
|
+
reviewRequestId,
|
|
74
|
+
});
|
|
75
|
+
if (!decision.allowed) {
|
|
76
|
+
return apiError("REVIEW_REQUEST_REQUIRED", decision.message, 409, {
|
|
77
|
+
reason: decision.reason,
|
|
78
|
+
target: decision.target ?? null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
48
81
|
|
|
49
82
|
const result = await dineway.handleContentPublish(collection, resolvedId);
|
|
50
83
|
|
|
@@ -52,5 +85,16 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
52
85
|
|
|
53
86
|
if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
|
|
54
87
|
|
|
88
|
+
await logContentActivity(dineway.db, locals, {
|
|
89
|
+
action: "published",
|
|
90
|
+
collection,
|
|
91
|
+
entryId: extractActivityItemId(result.data, resolvedId),
|
|
92
|
+
...contentApiRouteSource("published"),
|
|
93
|
+
summary: `Published content in ${collection}`,
|
|
94
|
+
detail: {
|
|
95
|
+
reviewRequestId: decision.required ? decision.reviewRequest.id : null,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
55
99
|
return unwrapResult(result);
|
|
56
100
|
};
|
|
@@ -8,6 +8,7 @@ import type { APIRoute } from "astro";
|
|
|
8
8
|
|
|
9
9
|
import { requireOwnerPerm } from "#api/authorize.js";
|
|
10
10
|
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
11
|
+
import { contentApiRouteSource, logContentActivity } from "#site-context/activity-events.js";
|
|
11
12
|
|
|
12
13
|
export const prerender = false;
|
|
13
14
|
|
|
@@ -43,12 +44,21 @@ export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
|
43
44
|
const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : "";
|
|
44
45
|
const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
45
46
|
if (denied) return denied;
|
|
47
|
+
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
46
48
|
|
|
47
|
-
const result = await dineway.handleContentRestore(collection,
|
|
49
|
+
const result = await dineway.handleContentRestore(collection, resolvedId);
|
|
48
50
|
|
|
49
51
|
if (!result.success) return unwrapResult(result);
|
|
50
52
|
|
|
51
|
-
if (cache.enabled) await cache.invalidate({ tags: [collection,
|
|
53
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
|
|
54
|
+
|
|
55
|
+
await logContentActivity(dineway.db, locals, {
|
|
56
|
+
action: "restored",
|
|
57
|
+
collection,
|
|
58
|
+
entryId: resolvedId,
|
|
59
|
+
...contentApiRouteSource("restored"),
|
|
60
|
+
summary: `Restored content in ${collection}`,
|
|
61
|
+
});
|
|
52
62
|
|
|
53
63
|
return unwrapResult(result);
|
|
54
64
|
};
|
|
@@ -13,7 +13,7 @@ export const prerender = false;
|
|
|
13
13
|
|
|
14
14
|
export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
15
15
|
const { dineway, user } = locals;
|
|
16
|
-
const denied = requirePerm(user, "content:
|
|
16
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
17
17
|
if (denied) return denied;
|
|
18
18
|
const collection = params.collection!;
|
|
19
19
|
const id = params.id!;
|