@actuate-media/cms-core 0.5.0 → 0.9.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/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +797 -0
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/reset-email.d.ts +10 -0
- package/dist/auth/reset-email.d.ts.map +1 -0
- package/dist/auth/reset-email.js +30 -0
- package/dist/auth/reset-email.js.map +1 -0
- package/dist/auth/reset.d.ts +35 -0
- package/dist/auth/reset.d.ts.map +1 -0
- package/dist/auth/reset.js +83 -0
- package/dist/auth/reset.js.map +1 -0
- package/dist/config/types.d.ts +3 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/page-builder/__tests__/a11y-fix.test.d.ts +2 -0
- package/dist/page-builder/__tests__/a11y-fix.test.d.ts.map +1 -0
- package/dist/page-builder/__tests__/a11y-fix.test.js +246 -0
- package/dist/page-builder/__tests__/a11y-fix.test.js.map +1 -0
- package/dist/page-builder/__tests__/blocks.test.d.ts +2 -0
- package/dist/page-builder/__tests__/blocks.test.d.ts.map +1 -0
- package/dist/page-builder/__tests__/blocks.test.js +87 -0
- package/dist/page-builder/__tests__/blocks.test.js.map +1 -0
- package/dist/page-builder/__tests__/design-scorer.test.d.ts +2 -0
- package/dist/page-builder/__tests__/design-scorer.test.d.ts.map +1 -0
- package/dist/page-builder/__tests__/design-scorer.test.js +268 -0
- package/dist/page-builder/__tests__/design-scorer.test.js.map +1 -0
- package/dist/page-builder/__tests__/schema.test.d.ts +2 -0
- package/dist/page-builder/__tests__/schema.test.d.ts.map +1 -0
- package/dist/page-builder/__tests__/schema.test.js +191 -0
- package/dist/page-builder/__tests__/schema.test.js.map +1 -0
- package/dist/page-builder/__tests__/seo-analyzer.test.d.ts +2 -0
- package/dist/page-builder/__tests__/seo-analyzer.test.d.ts.map +1 -0
- package/dist/page-builder/__tests__/seo-analyzer.test.js +332 -0
- package/dist/page-builder/__tests__/seo-analyzer.test.js.map +1 -0
- package/dist/page-builder/__tests__/tree.test.d.ts +2 -0
- package/dist/page-builder/__tests__/tree.test.d.ts.map +1 -0
- package/dist/page-builder/__tests__/tree.test.js +257 -0
- package/dist/page-builder/__tests__/tree.test.js.map +1 -0
- package/dist/page-builder/a11y-fix.d.ts +20 -0
- package/dist/page-builder/a11y-fix.d.ts.map +1 -0
- package/dist/page-builder/a11y-fix.js +182 -0
- package/dist/page-builder/a11y-fix.js.map +1 -0
- package/dist/page-builder/ai-pipeline.d.ts +43 -0
- package/dist/page-builder/ai-pipeline.d.ts.map +1 -0
- package/dist/page-builder/ai-pipeline.js +167 -0
- package/dist/page-builder/ai-pipeline.js.map +1 -0
- package/dist/page-builder/blocks.d.ts +10 -0
- package/dist/page-builder/blocks.d.ts.map +1 -0
- package/dist/page-builder/blocks.js +252 -0
- package/dist/page-builder/blocks.js.map +1 -0
- package/dist/page-builder/design-scorer.d.ts +27 -0
- package/dist/page-builder/design-scorer.d.ts.map +1 -0
- package/dist/page-builder/design-scorer.js +404 -0
- package/dist/page-builder/design-scorer.js.map +1 -0
- package/dist/page-builder/index.d.ts +18 -0
- package/dist/page-builder/index.d.ts.map +1 -0
- package/dist/page-builder/index.js +20 -0
- package/dist/page-builder/index.js.map +1 -0
- package/dist/page-builder/schema.d.ts +107 -0
- package/dist/page-builder/schema.d.ts.map +1 -0
- package/dist/page-builder/schema.js +90 -0
- package/dist/page-builder/schema.js.map +1 -0
- package/dist/page-builder/seo-analyzer.d.ts +61 -0
- package/dist/page-builder/seo-analyzer.d.ts.map +1 -0
- package/dist/page-builder/seo-analyzer.js +582 -0
- package/dist/page-builder/seo-analyzer.js.map +1 -0
- package/dist/page-builder/templates.d.ts +9 -0
- package/dist/page-builder/templates.d.ts.map +1 -0
- package/dist/page-builder/templates.js +401 -0
- package/dist/page-builder/templates.js.map +1 -0
- package/dist/page-builder/tree.d.ts +17 -0
- package/dist/page-builder/tree.d.ts.map +1 -0
- package/dist/page-builder/tree.js +160 -0
- package/dist/page-builder/tree.js.map +1 -0
- package/dist/page-builder/types.d.ts +112 -0
- package/dist/page-builder/types.d.ts.map +1 -0
- package/dist/page-builder/types.js +4 -0
- package/dist/page-builder/types.js.map +1 -0
- package/dist/page-builder/validate.d.ts +6 -0
- package/dist/page-builder/validate.d.ts.map +1 -0
- package/dist/page-builder/validate.js +87 -0
- package/dist/page-builder/validate.js.map +1 -0
- package/package.json +6 -1
- package/prisma/migrations/0004_script_tags/migration.sql +21 -0
- package/prisma/migrations/0005_password_reset_tokens/migration.sql +20 -0
- package/prisma/migrations/0006_page_builder/migration.sql +38 -0
- package/prisma/schema.prisma +75 -11
package/dist/api/handlers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listDocuments, getDocument, createDocument, updateDocument, deleteDocument, getGlobal, updateGlobal, } from '../actions.js';
|
|
2
2
|
import { verifyPassword } from '../auth/password.js';
|
|
3
3
|
import { createSession, verifySession, revokeSession } from '../auth/session.js';
|
|
4
|
+
import { createPasswordReset, executePasswordReset } from '../auth/reset.js';
|
|
4
5
|
import { checkSetupRequired, createInitialAdmin } from '../setup/index.js';
|
|
5
6
|
import { getDB } from '../db.js';
|
|
6
7
|
import { generateCodeVerifier, generateCodeChallenge, generateState, getAuthorizationUrl, handleOAuthCallback, } from '../auth/oauth.js';
|
|
@@ -17,12 +18,19 @@ import { encryptField, decryptField } from '../security/encrypted-fields.js';
|
|
|
17
18
|
import { createRateLimiter } from '../security/rate-limit.js';
|
|
18
19
|
import { generateOpenAPISpec } from './openapi.js';
|
|
19
20
|
import { createSSEPresenceAdapter } from '../presence/index.js';
|
|
21
|
+
import { BUILT_IN_TEMPLATES } from '../page-builder/templates.js';
|
|
22
|
+
import { validateTree } from '../page-builder/validate.js';
|
|
23
|
+
import { auditAccessibility, fixAccessibility } from '../page-builder/a11y-fix.js';
|
|
20
24
|
// Opaque dynamic import so Turbopack/webpack won't statically analyze the specifier.
|
|
21
25
|
// Returns { put, del, ... } from @vercel/blob when available.
|
|
22
26
|
async function importBlobStorage() {
|
|
23
27
|
const mod = '@vercel/' + 'blob';
|
|
24
28
|
return import(/* webpackIgnore: true */ mod);
|
|
25
29
|
}
|
|
30
|
+
async function importAIPlugin() {
|
|
31
|
+
const mod = '@actuate-media/' + 'plugin-ai';
|
|
32
|
+
return import(/* webpackIgnore: true */ mod);
|
|
33
|
+
}
|
|
26
34
|
const SECURITY_HEADERS = {
|
|
27
35
|
'Content-Type': 'application/json',
|
|
28
36
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -460,6 +468,70 @@ export function registerCMSRoutes(router) {
|
|
|
460
468
|
return internalError(err, 'logout');
|
|
461
469
|
}
|
|
462
470
|
});
|
|
471
|
+
router.post('/auth/forgot-password', async (request) => {
|
|
472
|
+
try {
|
|
473
|
+
const body = await request.json();
|
|
474
|
+
const { email } = body;
|
|
475
|
+
if (!email) {
|
|
476
|
+
return errorResponse('Email is required', 400);
|
|
477
|
+
}
|
|
478
|
+
const clientIp = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
|
|
479
|
+
if (!(await checkRateLimitAsync(loginLimiter, `forgot:${clientIp}`))) {
|
|
480
|
+
return errorResponse('Too many requests. Please try again later.', 429);
|
|
481
|
+
}
|
|
482
|
+
const d = db();
|
|
483
|
+
if (!hasModel(d, 'user'))
|
|
484
|
+
return modelNotAvailable('user');
|
|
485
|
+
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? new URL(request.url).origin;
|
|
486
|
+
const cmsConfig = globalThis.__actuateConfig;
|
|
487
|
+
await createPasswordReset(d, email.toLowerCase().trim(), {
|
|
488
|
+
siteUrl,
|
|
489
|
+
platform: cmsConfig?.platform,
|
|
490
|
+
});
|
|
491
|
+
await logEvent({
|
|
492
|
+
event: 'password_reset_request',
|
|
493
|
+
ipAddress: clientIp,
|
|
494
|
+
userAgent: request.headers.get('user-agent') ?? undefined,
|
|
495
|
+
details: { email: email.toLowerCase().trim() },
|
|
496
|
+
});
|
|
497
|
+
return json({ data: { success: true } });
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
return internalError(err, 'forgot-password');
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
router.post('/auth/reset-password', async (request) => {
|
|
504
|
+
try {
|
|
505
|
+
const body = await request.json();
|
|
506
|
+
const { token, password } = body;
|
|
507
|
+
if (!token || !password) {
|
|
508
|
+
return errorResponse('Token and new password are required', 400);
|
|
509
|
+
}
|
|
510
|
+
if (password.length < 8) {
|
|
511
|
+
return errorResponse('Password must be at least 8 characters', 400);
|
|
512
|
+
}
|
|
513
|
+
const clientIp = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
|
|
514
|
+
if (!(await checkRateLimitAsync(loginLimiter, `reset:${clientIp}`))) {
|
|
515
|
+
return errorResponse('Too many requests. Please try again later.', 429);
|
|
516
|
+
}
|
|
517
|
+
const d = db();
|
|
518
|
+
if (!hasModel(d, 'user'))
|
|
519
|
+
return modelNotAvailable('user');
|
|
520
|
+
const result = await executePasswordReset(d, token, password);
|
|
521
|
+
if (!result.success) {
|
|
522
|
+
return errorResponse(result.error ?? 'Password reset failed', 400);
|
|
523
|
+
}
|
|
524
|
+
await logEvent({
|
|
525
|
+
event: 'password_reset_complete',
|
|
526
|
+
ipAddress: clientIp,
|
|
527
|
+
userAgent: request.headers.get('user-agent') ?? undefined,
|
|
528
|
+
});
|
|
529
|
+
return json({ data: { success: true } });
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
return internalError(err, 'reset-password');
|
|
533
|
+
}
|
|
534
|
+
});
|
|
463
535
|
router.get('/auth/me', async (request) => {
|
|
464
536
|
try {
|
|
465
537
|
const auth = await requireAuth(request);
|
|
@@ -2760,6 +2832,208 @@ export function registerCMSRoutes(router) {
|
|
|
2760
2832
|
}
|
|
2761
2833
|
});
|
|
2762
2834
|
// ---------------------------------------------------------------------------
|
|
2835
|
+
// Script Tags
|
|
2836
|
+
// ---------------------------------------------------------------------------
|
|
2837
|
+
router.get('/script-tags/resolve', async (request) => {
|
|
2838
|
+
try {
|
|
2839
|
+
const d = db();
|
|
2840
|
+
if (!hasModel(d, 'scriptTag'))
|
|
2841
|
+
return modelNotAvailable('ScriptTag');
|
|
2842
|
+
const url = new URL(request.url);
|
|
2843
|
+
const pathParam = url.searchParams.get('path') ?? '/';
|
|
2844
|
+
const normalizedPath = '/' + pathParam.replace(/^\/|\/$/g, '');
|
|
2845
|
+
const tags = await d.scriptTag.findMany({
|
|
2846
|
+
where: { enabled: true },
|
|
2847
|
+
orderBy: { priority: 'asc' },
|
|
2848
|
+
});
|
|
2849
|
+
const matched = [];
|
|
2850
|
+
for (const tag of tags) {
|
|
2851
|
+
if (tag.scope === 'site') {
|
|
2852
|
+
matched.push(tag);
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
if (tag.scope === 'parents' && Array.isArray(tag.targetPaths)) {
|
|
2856
|
+
for (const target of tag.targetPaths) {
|
|
2857
|
+
const norm = '/' + target.replace(/^\/|\/$/g, '');
|
|
2858
|
+
if (normalizedPath === norm || normalizedPath.startsWith(norm + '/')) {
|
|
2859
|
+
matched.push(tag);
|
|
2860
|
+
break;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
if (tag.scope === 'urls' && Array.isArray(tag.targetPaths)) {
|
|
2866
|
+
for (const target of tag.targetPaths) {
|
|
2867
|
+
const norm = '/' + target.replace(/^\/|\/$/g, '');
|
|
2868
|
+
if (normalizedPath === norm) {
|
|
2869
|
+
matched.push(tag);
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
const grouped = { head: [], body_open: [], body_close: [] };
|
|
2876
|
+
for (const tag of matched) {
|
|
2877
|
+
const bucket = grouped[tag.placement];
|
|
2878
|
+
if (bucket) {
|
|
2879
|
+
bucket.push(tag.code);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return json(grouped);
|
|
2883
|
+
}
|
|
2884
|
+
catch (err) {
|
|
2885
|
+
return internalError(err, 'script-tags/resolve');
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
router.get('/script-tags', async (request) => {
|
|
2889
|
+
try {
|
|
2890
|
+
const auth = await requireAuth(request);
|
|
2891
|
+
if (auth.error)
|
|
2892
|
+
return auth.error;
|
|
2893
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
2894
|
+
if (roleErr)
|
|
2895
|
+
return roleErr;
|
|
2896
|
+
const d = db();
|
|
2897
|
+
if (!hasModel(d, 'scriptTag'))
|
|
2898
|
+
return modelNotAvailable('ScriptTag');
|
|
2899
|
+
const tags = await d.scriptTag.findMany({
|
|
2900
|
+
orderBy: { priority: 'asc' },
|
|
2901
|
+
});
|
|
2902
|
+
return json({ data: tags });
|
|
2903
|
+
}
|
|
2904
|
+
catch (err) {
|
|
2905
|
+
return internalError(err, 'script-tags list');
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
router.post('/script-tags', async (request) => {
|
|
2909
|
+
try {
|
|
2910
|
+
const auth = await requireAuth(request);
|
|
2911
|
+
if (auth.error)
|
|
2912
|
+
return auth.error;
|
|
2913
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
2914
|
+
if (roleErr)
|
|
2915
|
+
return roleErr;
|
|
2916
|
+
const d = db();
|
|
2917
|
+
if (!hasModel(d, 'scriptTag'))
|
|
2918
|
+
return modelNotAvailable('ScriptTag');
|
|
2919
|
+
const body = await request.json();
|
|
2920
|
+
if (!body.name || !body.code || !body.placement) {
|
|
2921
|
+
return errorResponse('name, code, and placement are required', 400);
|
|
2922
|
+
}
|
|
2923
|
+
const validPlacements = ['head', 'body_open', 'body_close'];
|
|
2924
|
+
if (!validPlacements.includes(body.placement)) {
|
|
2925
|
+
return errorResponse('placement must be head, body_open, or body_close', 400);
|
|
2926
|
+
}
|
|
2927
|
+
const validScopes = ['site', 'parents', 'urls'];
|
|
2928
|
+
const scope = body.scope || 'site';
|
|
2929
|
+
if (!validScopes.includes(scope)) {
|
|
2930
|
+
return errorResponse('scope must be site, parents, or urls', 400);
|
|
2931
|
+
}
|
|
2932
|
+
const tag = await d.scriptTag.create({
|
|
2933
|
+
data: {
|
|
2934
|
+
name: body.name,
|
|
2935
|
+
code: body.code,
|
|
2936
|
+
placement: body.placement,
|
|
2937
|
+
scope,
|
|
2938
|
+
targetPaths: Array.isArray(body.targetPaths) ? body.targetPaths : [],
|
|
2939
|
+
priority: typeof body.priority === 'number' ? body.priority : 100,
|
|
2940
|
+
enabled: body.enabled !== false,
|
|
2941
|
+
},
|
|
2942
|
+
});
|
|
2943
|
+
await logEvent({
|
|
2944
|
+
event: 'settings_changed',
|
|
2945
|
+
userId: auth.session.userId,
|
|
2946
|
+
details: { action: 'script_tag_created', tagId: tag.id, name: tag.name },
|
|
2947
|
+
});
|
|
2948
|
+
return json({ data: tag }, 201);
|
|
2949
|
+
}
|
|
2950
|
+
catch (err) {
|
|
2951
|
+
return internalError(err, 'script-tags create');
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
router.put('/script-tags/:id', async (request, params) => {
|
|
2955
|
+
try {
|
|
2956
|
+
const auth = await requireAuth(request);
|
|
2957
|
+
if (auth.error)
|
|
2958
|
+
return auth.error;
|
|
2959
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
2960
|
+
if (roleErr)
|
|
2961
|
+
return roleErr;
|
|
2962
|
+
const d = db();
|
|
2963
|
+
if (!hasModel(d, 'scriptTag'))
|
|
2964
|
+
return modelNotAvailable('ScriptTag');
|
|
2965
|
+
const existing = await d.scriptTag.findUnique({ where: { id: params.id } });
|
|
2966
|
+
if (!existing)
|
|
2967
|
+
return errorResponse('Script tag not found', 404);
|
|
2968
|
+
const body = await request.json();
|
|
2969
|
+
const update = {};
|
|
2970
|
+
if (body.name !== undefined)
|
|
2971
|
+
update.name = body.name;
|
|
2972
|
+
if (body.code !== undefined)
|
|
2973
|
+
update.code = body.code;
|
|
2974
|
+
if (body.placement !== undefined) {
|
|
2975
|
+
const validPlacements = ['head', 'body_open', 'body_close'];
|
|
2976
|
+
if (!validPlacements.includes(body.placement)) {
|
|
2977
|
+
return errorResponse('placement must be head, body_open, or body_close', 400);
|
|
2978
|
+
}
|
|
2979
|
+
update.placement = body.placement;
|
|
2980
|
+
}
|
|
2981
|
+
if (body.scope !== undefined) {
|
|
2982
|
+
const validScopes = ['site', 'parents', 'urls'];
|
|
2983
|
+
if (!validScopes.includes(body.scope)) {
|
|
2984
|
+
return errorResponse('scope must be site, parents, or urls', 400);
|
|
2985
|
+
}
|
|
2986
|
+
update.scope = body.scope;
|
|
2987
|
+
}
|
|
2988
|
+
if (body.targetPaths !== undefined) {
|
|
2989
|
+
update.targetPaths = Array.isArray(body.targetPaths) ? body.targetPaths : [];
|
|
2990
|
+
}
|
|
2991
|
+
if (typeof body.priority === 'number')
|
|
2992
|
+
update.priority = body.priority;
|
|
2993
|
+
if (typeof body.enabled === 'boolean')
|
|
2994
|
+
update.enabled = body.enabled;
|
|
2995
|
+
const tag = await d.scriptTag.update({
|
|
2996
|
+
where: { id: params.id },
|
|
2997
|
+
data: update,
|
|
2998
|
+
});
|
|
2999
|
+
await logEvent({
|
|
3000
|
+
event: 'settings_changed',
|
|
3001
|
+
userId: auth.session.userId,
|
|
3002
|
+
details: { action: 'script_tag_updated', tagId: tag.id, name: tag.name },
|
|
3003
|
+
});
|
|
3004
|
+
return json({ data: tag });
|
|
3005
|
+
}
|
|
3006
|
+
catch (err) {
|
|
3007
|
+
return internalError(err, 'script-tags update');
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
router.delete('/script-tags/:id', async (request, params) => {
|
|
3011
|
+
try {
|
|
3012
|
+
const auth = await requireAuth(request);
|
|
3013
|
+
if (auth.error)
|
|
3014
|
+
return auth.error;
|
|
3015
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3016
|
+
if (roleErr)
|
|
3017
|
+
return roleErr;
|
|
3018
|
+
const d = db();
|
|
3019
|
+
if (!hasModel(d, 'scriptTag'))
|
|
3020
|
+
return modelNotAvailable('ScriptTag');
|
|
3021
|
+
const existing = await d.scriptTag.findUnique({ where: { id: params.id } });
|
|
3022
|
+
if (!existing)
|
|
3023
|
+
return errorResponse('Script tag not found', 404);
|
|
3024
|
+
await d.scriptTag.delete({ where: { id: params.id } });
|
|
3025
|
+
await logEvent({
|
|
3026
|
+
event: 'settings_changed',
|
|
3027
|
+
userId: auth.session.userId,
|
|
3028
|
+
details: { action: 'script_tag_deleted', tagId: existing.id, name: existing.name },
|
|
3029
|
+
});
|
|
3030
|
+
return json({ data: { deleted: true } });
|
|
3031
|
+
}
|
|
3032
|
+
catch (err) {
|
|
3033
|
+
return internalError(err, 'script-tags delete');
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
// ---------------------------------------------------------------------------
|
|
2763
3037
|
// Presence SSE
|
|
2764
3038
|
// ---------------------------------------------------------------------------
|
|
2765
3039
|
router.get('/presence/:documentId', async (request, params) => {
|
|
@@ -2788,5 +3062,528 @@ export function registerCMSRoutes(router) {
|
|
|
2788
3062
|
return internalError(err);
|
|
2789
3063
|
}
|
|
2790
3064
|
});
|
|
3065
|
+
// ---------------------------------------------------------------------------
|
|
3066
|
+
// Page Templates
|
|
3067
|
+
// ---------------------------------------------------------------------------
|
|
3068
|
+
async function seedBuiltInTemplates(d) {
|
|
3069
|
+
for (const [key, template] of Object.entries(BUILT_IN_TEMPLATES)) {
|
|
3070
|
+
await d.pageTemplate.upsert({
|
|
3071
|
+
where: { id: `builtin-${key}` },
|
|
3072
|
+
create: {
|
|
3073
|
+
id: `builtin-${key}`,
|
|
3074
|
+
name: template.name,
|
|
3075
|
+
description: template.description,
|
|
3076
|
+
category: template.category,
|
|
3077
|
+
tree: template.tree,
|
|
3078
|
+
builtIn: true,
|
|
3079
|
+
},
|
|
3080
|
+
update: {
|
|
3081
|
+
name: template.name,
|
|
3082
|
+
description: template.description,
|
|
3083
|
+
category: template.category,
|
|
3084
|
+
tree: template.tree,
|
|
3085
|
+
},
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
router.get('/page-templates', async (request) => {
|
|
3090
|
+
try {
|
|
3091
|
+
const auth = await requireAuth(request);
|
|
3092
|
+
if (auth.error)
|
|
3093
|
+
return auth.error;
|
|
3094
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3095
|
+
if (roleErr)
|
|
3096
|
+
return roleErr;
|
|
3097
|
+
const d = db();
|
|
3098
|
+
if (!hasModel(d, 'pageTemplate'))
|
|
3099
|
+
return modelNotAvailable('PageTemplate');
|
|
3100
|
+
const url = new URL(request.url, 'http://localhost');
|
|
3101
|
+
const category = url.searchParams.get('category');
|
|
3102
|
+
const builtInCount = await safeCount(d.pageTemplate, { builtIn: true });
|
|
3103
|
+
if (builtInCount === 0) {
|
|
3104
|
+
await seedBuiltInTemplates(d);
|
|
3105
|
+
}
|
|
3106
|
+
const where = {};
|
|
3107
|
+
if (category)
|
|
3108
|
+
where.category = category;
|
|
3109
|
+
const templates = await d.pageTemplate.findMany({
|
|
3110
|
+
where,
|
|
3111
|
+
orderBy: [{ builtIn: 'desc' }, { updatedAt: 'desc' }],
|
|
3112
|
+
});
|
|
3113
|
+
return json({ data: templates });
|
|
3114
|
+
}
|
|
3115
|
+
catch (err) {
|
|
3116
|
+
return internalError(err, 'page-templates list');
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
router.get('/page-templates/:id', async (request, params) => {
|
|
3120
|
+
try {
|
|
3121
|
+
const auth = await requireAuth(request);
|
|
3122
|
+
if (auth.error)
|
|
3123
|
+
return auth.error;
|
|
3124
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3125
|
+
if (roleErr)
|
|
3126
|
+
return roleErr;
|
|
3127
|
+
const d = db();
|
|
3128
|
+
if (!hasModel(d, 'pageTemplate'))
|
|
3129
|
+
return modelNotAvailable('PageTemplate');
|
|
3130
|
+
const template = await d.pageTemplate.findUnique({ where: { id: params.id } });
|
|
3131
|
+
if (!template)
|
|
3132
|
+
return errorResponse('Template not found', 404);
|
|
3133
|
+
return json({ data: template });
|
|
3134
|
+
}
|
|
3135
|
+
catch (err) {
|
|
3136
|
+
return internalError(err, 'page-templates get');
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
router.post('/page-templates', async (request) => {
|
|
3140
|
+
try {
|
|
3141
|
+
const auth = await requireAuth(request);
|
|
3142
|
+
if (auth.error)
|
|
3143
|
+
return auth.error;
|
|
3144
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3145
|
+
if (roleErr)
|
|
3146
|
+
return roleErr;
|
|
3147
|
+
const d = db();
|
|
3148
|
+
if (!hasModel(d, 'pageTemplate'))
|
|
3149
|
+
return modelNotAvailable('PageTemplate');
|
|
3150
|
+
const body = await request.json();
|
|
3151
|
+
if (!body.name)
|
|
3152
|
+
return errorResponse('name is required', 400);
|
|
3153
|
+
if (!body.tree)
|
|
3154
|
+
return errorResponse('tree is required', 400);
|
|
3155
|
+
const validation = validateTree(body.tree);
|
|
3156
|
+
if (!validation.valid) {
|
|
3157
|
+
return errorResponse(`Invalid tree: ${validation.errors.join('; ')}`, 400);
|
|
3158
|
+
}
|
|
3159
|
+
const validCategories = ['landing', 'content', 'utility'];
|
|
3160
|
+
const category = body.category || 'content';
|
|
3161
|
+
if (!validCategories.includes(category)) {
|
|
3162
|
+
return errorResponse('category must be landing, content, or utility', 400);
|
|
3163
|
+
}
|
|
3164
|
+
const template = await d.pageTemplate.create({
|
|
3165
|
+
data: {
|
|
3166
|
+
name: body.name,
|
|
3167
|
+
description: body.description || null,
|
|
3168
|
+
category,
|
|
3169
|
+
tree: body.tree,
|
|
3170
|
+
thumbnail: body.thumbnail || null,
|
|
3171
|
+
builtIn: false,
|
|
3172
|
+
},
|
|
3173
|
+
});
|
|
3174
|
+
await logEvent({
|
|
3175
|
+
event: 'settings_changed',
|
|
3176
|
+
userId: auth.session.userId,
|
|
3177
|
+
details: { action: 'page_template_created', templateId: template.id, name: template.name },
|
|
3178
|
+
});
|
|
3179
|
+
return json({ data: template }, 201);
|
|
3180
|
+
}
|
|
3181
|
+
catch (err) {
|
|
3182
|
+
return internalError(err, 'page-templates create');
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
router.put('/page-templates/:id', async (request, params) => {
|
|
3186
|
+
try {
|
|
3187
|
+
const auth = await requireAuth(request);
|
|
3188
|
+
if (auth.error)
|
|
3189
|
+
return auth.error;
|
|
3190
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3191
|
+
if (roleErr)
|
|
3192
|
+
return roleErr;
|
|
3193
|
+
const d = db();
|
|
3194
|
+
if (!hasModel(d, 'pageTemplate'))
|
|
3195
|
+
return modelNotAvailable('PageTemplate');
|
|
3196
|
+
const existing = await d.pageTemplate.findUnique({ where: { id: params.id } });
|
|
3197
|
+
if (!existing)
|
|
3198
|
+
return errorResponse('Template not found', 404);
|
|
3199
|
+
if (existing.builtIn)
|
|
3200
|
+
return errorResponse('Cannot update built-in templates', 403);
|
|
3201
|
+
const body = await request.json();
|
|
3202
|
+
const update = {};
|
|
3203
|
+
if (body.name !== undefined)
|
|
3204
|
+
update.name = body.name;
|
|
3205
|
+
if (body.description !== undefined)
|
|
3206
|
+
update.description = body.description;
|
|
3207
|
+
if (body.thumbnail !== undefined)
|
|
3208
|
+
update.thumbnail = body.thumbnail;
|
|
3209
|
+
if (body.category !== undefined) {
|
|
3210
|
+
const validCategories = ['landing', 'content', 'utility'];
|
|
3211
|
+
if (!validCategories.includes(body.category)) {
|
|
3212
|
+
return errorResponse('category must be landing, content, or utility', 400);
|
|
3213
|
+
}
|
|
3214
|
+
update.category = body.category;
|
|
3215
|
+
}
|
|
3216
|
+
if (body.tree !== undefined) {
|
|
3217
|
+
const validation = validateTree(body.tree);
|
|
3218
|
+
if (!validation.valid) {
|
|
3219
|
+
return errorResponse(`Invalid tree: ${validation.errors.join('; ')}`, 400);
|
|
3220
|
+
}
|
|
3221
|
+
update.tree = body.tree;
|
|
3222
|
+
}
|
|
3223
|
+
const template = await d.pageTemplate.update({
|
|
3224
|
+
where: { id: params.id },
|
|
3225
|
+
data: update,
|
|
3226
|
+
});
|
|
3227
|
+
await logEvent({
|
|
3228
|
+
event: 'settings_changed',
|
|
3229
|
+
userId: auth.session.userId,
|
|
3230
|
+
details: { action: 'page_template_updated', templateId: template.id, name: template.name },
|
|
3231
|
+
});
|
|
3232
|
+
return json({ data: template });
|
|
3233
|
+
}
|
|
3234
|
+
catch (err) {
|
|
3235
|
+
return internalError(err, 'page-templates update');
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
router.delete('/page-templates/:id', async (request, params) => {
|
|
3239
|
+
try {
|
|
3240
|
+
const auth = await requireAuth(request);
|
|
3241
|
+
if (auth.error)
|
|
3242
|
+
return auth.error;
|
|
3243
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3244
|
+
if (roleErr)
|
|
3245
|
+
return roleErr;
|
|
3246
|
+
const d = db();
|
|
3247
|
+
if (!hasModel(d, 'pageTemplate'))
|
|
3248
|
+
return modelNotAvailable('PageTemplate');
|
|
3249
|
+
const existing = await d.pageTemplate.findUnique({ where: { id: params.id } });
|
|
3250
|
+
if (!existing)
|
|
3251
|
+
return errorResponse('Template not found', 404);
|
|
3252
|
+
if (existing.builtIn)
|
|
3253
|
+
return errorResponse('Cannot delete built-in templates', 403);
|
|
3254
|
+
await d.pageTemplate.delete({ where: { id: params.id } });
|
|
3255
|
+
await logEvent({
|
|
3256
|
+
event: 'settings_changed',
|
|
3257
|
+
userId: auth.session.userId,
|
|
3258
|
+
details: { action: 'page_template_deleted', templateId: existing.id, name: existing.name },
|
|
3259
|
+
});
|
|
3260
|
+
return json({ data: { deleted: true } });
|
|
3261
|
+
}
|
|
3262
|
+
catch (err) {
|
|
3263
|
+
return internalError(err, 'page-templates delete');
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
router.post('/page-templates/seed', async (request) => {
|
|
3267
|
+
try {
|
|
3268
|
+
const auth = await requireAuth(request);
|
|
3269
|
+
if (auth.error)
|
|
3270
|
+
return auth.error;
|
|
3271
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3272
|
+
if (roleErr)
|
|
3273
|
+
return roleErr;
|
|
3274
|
+
const d = db();
|
|
3275
|
+
if (!hasModel(d, 'pageTemplate'))
|
|
3276
|
+
return modelNotAvailable('PageTemplate');
|
|
3277
|
+
await seedBuiltInTemplates(d);
|
|
3278
|
+
await logEvent({
|
|
3279
|
+
event: 'settings_changed',
|
|
3280
|
+
userId: auth.session.userId,
|
|
3281
|
+
details: { action: 'page_templates_seeded' },
|
|
3282
|
+
});
|
|
3283
|
+
return json({ data: { seeded: Object.keys(BUILT_IN_TEMPLATES).length } });
|
|
3284
|
+
}
|
|
3285
|
+
catch (err) {
|
|
3286
|
+
return internalError(err, 'page-templates seed');
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
// ---------------------------------------------------------------------------
|
|
3290
|
+
// Saved Sections
|
|
3291
|
+
// ---------------------------------------------------------------------------
|
|
3292
|
+
router.get('/saved-sections', async (request) => {
|
|
3293
|
+
try {
|
|
3294
|
+
const auth = await requireAuth(request);
|
|
3295
|
+
if (auth.error)
|
|
3296
|
+
return auth.error;
|
|
3297
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3298
|
+
if (roleErr)
|
|
3299
|
+
return roleErr;
|
|
3300
|
+
const d = db();
|
|
3301
|
+
if (!hasModel(d, 'savedSection'))
|
|
3302
|
+
return modelNotAvailable('SavedSection');
|
|
3303
|
+
const url = new URL(request.url, 'http://localhost');
|
|
3304
|
+
const category = url.searchParams.get('category');
|
|
3305
|
+
const where = {};
|
|
3306
|
+
if (category)
|
|
3307
|
+
where.category = category;
|
|
3308
|
+
const sections = await d.savedSection.findMany({
|
|
3309
|
+
where,
|
|
3310
|
+
orderBy: { updatedAt: 'desc' },
|
|
3311
|
+
});
|
|
3312
|
+
return json({ data: sections });
|
|
3313
|
+
}
|
|
3314
|
+
catch (err) {
|
|
3315
|
+
return internalError(err, 'saved-sections list');
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
router.get('/saved-sections/:id', async (request, params) => {
|
|
3319
|
+
try {
|
|
3320
|
+
const auth = await requireAuth(request);
|
|
3321
|
+
if (auth.error)
|
|
3322
|
+
return auth.error;
|
|
3323
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3324
|
+
if (roleErr)
|
|
3325
|
+
return roleErr;
|
|
3326
|
+
const d = db();
|
|
3327
|
+
if (!hasModel(d, 'savedSection'))
|
|
3328
|
+
return modelNotAvailable('SavedSection');
|
|
3329
|
+
const section = await d.savedSection.findUnique({ where: { id: params.id } });
|
|
3330
|
+
if (!section)
|
|
3331
|
+
return errorResponse('Saved section not found', 404);
|
|
3332
|
+
return json({ data: section });
|
|
3333
|
+
}
|
|
3334
|
+
catch (err) {
|
|
3335
|
+
return internalError(err, 'saved-sections get');
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
router.post('/saved-sections', async (request) => {
|
|
3339
|
+
try {
|
|
3340
|
+
const auth = await requireAuth(request);
|
|
3341
|
+
if (auth.error)
|
|
3342
|
+
return auth.error;
|
|
3343
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3344
|
+
if (roleErr)
|
|
3345
|
+
return roleErr;
|
|
3346
|
+
const d = db();
|
|
3347
|
+
if (!hasModel(d, 'savedSection'))
|
|
3348
|
+
return modelNotAvailable('SavedSection');
|
|
3349
|
+
const body = await request.json();
|
|
3350
|
+
if (!body.name)
|
|
3351
|
+
return errorResponse('name is required', 400);
|
|
3352
|
+
if (!body.tree)
|
|
3353
|
+
return errorResponse('tree is required', 400);
|
|
3354
|
+
const wrappedTree = { id: '__validation_wrapper__', type: 'page', children: [body.tree] };
|
|
3355
|
+
const validation = validateTree(wrappedTree);
|
|
3356
|
+
if (!validation.valid) {
|
|
3357
|
+
return errorResponse(`Invalid section tree: ${validation.errors.join('; ')}`, 400);
|
|
3358
|
+
}
|
|
3359
|
+
const validCategories = ['header', 'footer', 'content', 'sidebar'];
|
|
3360
|
+
const category = body.category || 'content';
|
|
3361
|
+
if (!validCategories.includes(category)) {
|
|
3362
|
+
return errorResponse('category must be header, footer, content, or sidebar', 400);
|
|
3363
|
+
}
|
|
3364
|
+
const section = await d.savedSection.create({
|
|
3365
|
+
data: {
|
|
3366
|
+
name: body.name,
|
|
3367
|
+
description: body.description || null,
|
|
3368
|
+
category,
|
|
3369
|
+
tree: body.tree,
|
|
3370
|
+
thumbnail: body.thumbnail || null,
|
|
3371
|
+
},
|
|
3372
|
+
});
|
|
3373
|
+
await logEvent({
|
|
3374
|
+
event: 'settings_changed',
|
|
3375
|
+
userId: auth.session.userId,
|
|
3376
|
+
details: { action: 'saved_section_created', sectionId: section.id, name: section.name },
|
|
3377
|
+
});
|
|
3378
|
+
return json({ data: section }, 201);
|
|
3379
|
+
}
|
|
3380
|
+
catch (err) {
|
|
3381
|
+
return internalError(err, 'saved-sections create');
|
|
3382
|
+
}
|
|
3383
|
+
});
|
|
3384
|
+
router.put('/saved-sections/:id', async (request, params) => {
|
|
3385
|
+
try {
|
|
3386
|
+
const auth = await requireAuth(request);
|
|
3387
|
+
if (auth.error)
|
|
3388
|
+
return auth.error;
|
|
3389
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3390
|
+
if (roleErr)
|
|
3391
|
+
return roleErr;
|
|
3392
|
+
const d = db();
|
|
3393
|
+
if (!hasModel(d, 'savedSection'))
|
|
3394
|
+
return modelNotAvailable('SavedSection');
|
|
3395
|
+
const existing = await d.savedSection.findUnique({ where: { id: params.id } });
|
|
3396
|
+
if (!existing)
|
|
3397
|
+
return errorResponse('Saved section not found', 404);
|
|
3398
|
+
const body = await request.json();
|
|
3399
|
+
const update = {};
|
|
3400
|
+
if (body.name !== undefined)
|
|
3401
|
+
update.name = body.name;
|
|
3402
|
+
if (body.description !== undefined)
|
|
3403
|
+
update.description = body.description;
|
|
3404
|
+
if (body.thumbnail !== undefined)
|
|
3405
|
+
update.thumbnail = body.thumbnail;
|
|
3406
|
+
if (body.category !== undefined) {
|
|
3407
|
+
const validCategories = ['header', 'footer', 'content', 'sidebar'];
|
|
3408
|
+
if (!validCategories.includes(body.category)) {
|
|
3409
|
+
return errorResponse('category must be header, footer, content, or sidebar', 400);
|
|
3410
|
+
}
|
|
3411
|
+
update.category = body.category;
|
|
3412
|
+
}
|
|
3413
|
+
if (body.tree !== undefined) {
|
|
3414
|
+
const wrappedTree = { id: '__validation_wrapper__', type: 'page', children: [body.tree] };
|
|
3415
|
+
const validation = validateTree(wrappedTree);
|
|
3416
|
+
if (!validation.valid) {
|
|
3417
|
+
return errorResponse(`Invalid section tree: ${validation.errors.join('; ')}`, 400);
|
|
3418
|
+
}
|
|
3419
|
+
update.tree = body.tree;
|
|
3420
|
+
}
|
|
3421
|
+
const section = await d.savedSection.update({
|
|
3422
|
+
where: { id: params.id },
|
|
3423
|
+
data: update,
|
|
3424
|
+
});
|
|
3425
|
+
await logEvent({
|
|
3426
|
+
event: 'settings_changed',
|
|
3427
|
+
userId: auth.session.userId,
|
|
3428
|
+
details: { action: 'saved_section_updated', sectionId: section.id, name: section.name },
|
|
3429
|
+
});
|
|
3430
|
+
return json({ data: section });
|
|
3431
|
+
}
|
|
3432
|
+
catch (err) {
|
|
3433
|
+
return internalError(err, 'saved-sections update');
|
|
3434
|
+
}
|
|
3435
|
+
});
|
|
3436
|
+
router.delete('/saved-sections/:id', async (request, params) => {
|
|
3437
|
+
try {
|
|
3438
|
+
const auth = await requireAuth(request);
|
|
3439
|
+
if (auth.error)
|
|
3440
|
+
return auth.error;
|
|
3441
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3442
|
+
if (roleErr)
|
|
3443
|
+
return roleErr;
|
|
3444
|
+
const d = db();
|
|
3445
|
+
if (!hasModel(d, 'savedSection'))
|
|
3446
|
+
return modelNotAvailable('SavedSection');
|
|
3447
|
+
const existing = await d.savedSection.findUnique({ where: { id: params.id } });
|
|
3448
|
+
if (!existing)
|
|
3449
|
+
return errorResponse('Saved section not found', 404);
|
|
3450
|
+
await d.savedSection.delete({ where: { id: params.id } });
|
|
3451
|
+
await logEvent({
|
|
3452
|
+
event: 'settings_changed',
|
|
3453
|
+
userId: auth.session.userId,
|
|
3454
|
+
details: { action: 'saved_section_deleted', sectionId: existing.id, name: existing.name },
|
|
3455
|
+
});
|
|
3456
|
+
return json({ data: { deleted: true } });
|
|
3457
|
+
}
|
|
3458
|
+
catch (err) {
|
|
3459
|
+
return internalError(err, 'saved-sections delete');
|
|
3460
|
+
}
|
|
3461
|
+
});
|
|
3462
|
+
// ─── Page Builder AI Generation ─────────────────────────────────────
|
|
3463
|
+
router.post('/page-builder/generate', async (request) => {
|
|
3464
|
+
try {
|
|
3465
|
+
const auth = await requireAuth(request);
|
|
3466
|
+
if (auth.error)
|
|
3467
|
+
return auth.error;
|
|
3468
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3469
|
+
if (roleErr)
|
|
3470
|
+
return roleErr;
|
|
3471
|
+
const body = await request.json();
|
|
3472
|
+
const { prompt, template, context, steps, tone } = body;
|
|
3473
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
3474
|
+
return errorResponse('prompt is required', 400);
|
|
3475
|
+
}
|
|
3476
|
+
if (!steps || !Array.isArray(steps) || steps.length === 0) {
|
|
3477
|
+
return errorResponse('steps array is required', 400);
|
|
3478
|
+
}
|
|
3479
|
+
const validSteps = ['structure', 'content', 'seo', 'accessibility'];
|
|
3480
|
+
for (const s of steps) {
|
|
3481
|
+
if (!validSteps.includes(s)) {
|
|
3482
|
+
return errorResponse(`Invalid step: ${s}. Valid steps: ${validSteps.join(', ')}`, 400);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
await logEvent({
|
|
3486
|
+
event: 'settings_changed',
|
|
3487
|
+
userId: auth.session.userId,
|
|
3488
|
+
details: { action: 'page_generation_started', prompt, steps, template },
|
|
3489
|
+
});
|
|
3490
|
+
let generatePage = null;
|
|
3491
|
+
try {
|
|
3492
|
+
const aiModule = await importAIPlugin();
|
|
3493
|
+
generatePage = aiModule.generatePage;
|
|
3494
|
+
}
|
|
3495
|
+
catch {
|
|
3496
|
+
return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use page generation.', 501);
|
|
3497
|
+
}
|
|
3498
|
+
const result = await generatePage({ prompt, template, context, steps, tone });
|
|
3499
|
+
await logEvent({
|
|
3500
|
+
event: 'settings_changed',
|
|
3501
|
+
userId: auth.session.userId,
|
|
3502
|
+
details: {
|
|
3503
|
+
action: 'page_generation_completed',
|
|
3504
|
+
totalTokensUsed: result.totalTokensUsed,
|
|
3505
|
+
totalDurationMs: result.totalDurationMs,
|
|
3506
|
+
stepsCompleted: result.steps.map((s) => s.step),
|
|
3507
|
+
},
|
|
3508
|
+
});
|
|
3509
|
+
return json({ data: result });
|
|
3510
|
+
}
|
|
3511
|
+
catch (err) {
|
|
3512
|
+
return internalError(err, 'page-builder generate');
|
|
3513
|
+
}
|
|
3514
|
+
});
|
|
3515
|
+
router.post('/page-builder/generate-block', async (request) => {
|
|
3516
|
+
try {
|
|
3517
|
+
const auth = await requireAuth(request);
|
|
3518
|
+
if (auth.error)
|
|
3519
|
+
return auth.error;
|
|
3520
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3521
|
+
if (roleErr)
|
|
3522
|
+
return roleErr;
|
|
3523
|
+
const body = await request.json();
|
|
3524
|
+
const { blockType, variant, pageContext, tone } = body;
|
|
3525
|
+
if (!blockType || typeof blockType !== 'string') {
|
|
3526
|
+
return errorResponse('blockType is required', 400);
|
|
3527
|
+
}
|
|
3528
|
+
let generateBlockContent = null;
|
|
3529
|
+
try {
|
|
3530
|
+
const aiModule = await importAIPlugin();
|
|
3531
|
+
generateBlockContent = aiModule.generateBlockContent;
|
|
3532
|
+
}
|
|
3533
|
+
catch {
|
|
3534
|
+
return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use block generation.', 501);
|
|
3535
|
+
}
|
|
3536
|
+
const data = await generateBlockContent({ blockType, variant, pageContext, tone });
|
|
3537
|
+
return json({ data });
|
|
3538
|
+
}
|
|
3539
|
+
catch (err) {
|
|
3540
|
+
return internalError(err, 'page-builder generate-block');
|
|
3541
|
+
}
|
|
3542
|
+
});
|
|
3543
|
+
router.post('/page-builder/audit-a11y', async (request) => {
|
|
3544
|
+
try {
|
|
3545
|
+
const auth = await requireAuth(request);
|
|
3546
|
+
if (auth.error)
|
|
3547
|
+
return auth.error;
|
|
3548
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3549
|
+
if (roleErr)
|
|
3550
|
+
return roleErr;
|
|
3551
|
+
const body = await request.json();
|
|
3552
|
+
const tree = body.tree;
|
|
3553
|
+
if (!tree || tree.type !== 'page') {
|
|
3554
|
+
return errorResponse('A valid page tree is required', 400);
|
|
3555
|
+
}
|
|
3556
|
+
const issues = auditAccessibility(tree);
|
|
3557
|
+
return json({ data: { issues } });
|
|
3558
|
+
}
|
|
3559
|
+
catch (err) {
|
|
3560
|
+
return internalError(err, 'page-builder audit-a11y');
|
|
3561
|
+
}
|
|
3562
|
+
});
|
|
3563
|
+
router.post('/page-builder/fix-a11y', async (request) => {
|
|
3564
|
+
try {
|
|
3565
|
+
const auth = await requireAuth(request);
|
|
3566
|
+
if (auth.error)
|
|
3567
|
+
return auth.error;
|
|
3568
|
+
const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
|
|
3569
|
+
if (roleErr)
|
|
3570
|
+
return roleErr;
|
|
3571
|
+
const body = await request.json();
|
|
3572
|
+
const tree = body.tree;
|
|
3573
|
+
if (!tree || tree.type !== 'page') {
|
|
3574
|
+
return errorResponse('A valid page tree is required', 400);
|
|
3575
|
+
}
|
|
3576
|
+
const result = fixAccessibility(tree);
|
|
3577
|
+
await logEvent({
|
|
3578
|
+
event: 'settings_changed',
|
|
3579
|
+
userId: auth.session.userId,
|
|
3580
|
+
details: { action: 'a11y_auto_fix', fixedCount: result.report.fixedCount, remainingCount: result.report.remainingCount },
|
|
3581
|
+
});
|
|
3582
|
+
return json({ data: result });
|
|
3583
|
+
}
|
|
3584
|
+
catch (err) {
|
|
3585
|
+
return internalError(err, 'page-builder fix-a11y');
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
2791
3588
|
}
|
|
2792
3589
|
//# sourceMappingURL=handlers.js.map
|