Skip to content

Commit c7eb4da

Browse files
committed
feat: add loading and saving state and basic profile
1 parent 3d5f27e commit c7eb4da

48 files changed

Lines changed: 2729 additions & 855 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
# wrangler
2727
.dev.vars
28+
.wrangler
2829

2930
npm-debug.log*
3031
yarn-debug.log*

functions/api/admin/history.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Admin API: Get review history (approved/rejected engines)
2+
// GET /api/admin/history
3+
4+
import type { Env } from '../../types';
5+
import { getAuthUser, isAdminEmail, json, errorResponse } from '../../utils';
6+
7+
interface EngineRow {
8+
id: string;
9+
owner_id: string;
10+
definition: string;
11+
visibility: string;
12+
submitted_for_review_at: string;
13+
reviewed_at: string;
14+
reviewed_by: string;
15+
rejection_reason: string | null;
16+
use_count: number;
17+
created_at: string;
18+
updated_at: string;
19+
user_email: string;
20+
user_display_name: string | null;
21+
reviewer_email: string | null;
22+
reviewer_display_name: string | null;
23+
}
24+
25+
export const onRequestGet: PagesFunction<Env> = async (context) => {
26+
const { request, env } = context;
27+
28+
try {
29+
// Verify admin authentication
30+
const session = await getAuthUser(request, env);
31+
if (!session) {
32+
return errorResponse('Unauthorized', 401);
33+
}
34+
35+
if (!isAdminEmail(session.email, env)) {
36+
return errorResponse('Forbidden - Admin access required', 403);
37+
}
38+
39+
// Get query params for filtering
40+
const url = new URL(request.url);
41+
const filter = url.searchParams.get('filter') || 'all'; // 'all', 'approved', 'rejected'
42+
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100);
43+
44+
// Build visibility filter
45+
let visibilityFilter = "e.visibility IN ('public', 'private')";
46+
if (filter === 'approved') {
47+
visibilityFilter = "e.visibility = 'public'";
48+
} else if (filter === 'rejected') {
49+
visibilityFilter = "e.visibility = 'private'";
50+
}
51+
52+
// Get reviewed engines with user and reviewer info
53+
const result = await env.DB.prepare(`
54+
SELECT
55+
e.id,
56+
e.owner_id,
57+
e.definition,
58+
e.visibility,
59+
e.submitted_for_review_at,
60+
e.reviewed_at,
61+
e.reviewed_by,
62+
e.rejection_reason,
63+
e.use_count,
64+
e.created_at,
65+
e.updated_at,
66+
p.email as user_email,
67+
p.display_name as user_display_name,
68+
r.email as reviewer_email,
69+
r.display_name as reviewer_display_name
70+
FROM engines e
71+
JOIN profiles p ON e.owner_id = p.id
72+
LEFT JOIN profiles r ON e.reviewed_by = r.id
73+
WHERE e.reviewed_at IS NOT NULL
74+
AND ${visibilityFilter}
75+
ORDER BY e.reviewed_at DESC
76+
LIMIT ?
77+
`).bind(limit).all();
78+
79+
const engines = (result.results as unknown as EngineRow[]).map((row) => ({
80+
id: row.id,
81+
ownerId: row.owner_id,
82+
definition: JSON.parse(row.definition),
83+
visibility: row.visibility,
84+
submittedAt: row.submitted_for_review_at,
85+
reviewedAt: row.reviewed_at,
86+
rejectionReason: row.rejection_reason,
87+
useCount: row.use_count,
88+
createdAt: row.created_at,
89+
updatedAt: row.updated_at,
90+
user: {
91+
email: row.user_email,
92+
displayName: row.user_display_name,
93+
},
94+
reviewer: row.reviewer_email ? {
95+
email: row.reviewer_email,
96+
displayName: row.reviewer_display_name,
97+
} : null,
98+
}));
99+
100+
return json({ engines });
101+
} catch (error) {
102+
console.error('Admin history error:', error);
103+
return errorResponse('Failed to fetch review history', 500);
104+
}
105+
};

functions/api/admin/pending.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Admin API: Get pending engines for review
2+
// GET /api/admin/pending
3+
4+
import type { Env } from '../../types';
5+
import { getAuthUser, isAdminEmail, json, errorResponse } from '../../utils';
6+
7+
interface EngineRow {
8+
id: string;
9+
owner_id: string;
10+
definition: string;
11+
visibility: string;
12+
submitted_at: string;
13+
use_count: number;
14+
created_at: string;
15+
updated_at: string;
16+
user_email: string;
17+
user_display_name: string | null;
18+
}
19+
20+
export const onRequestGet: PagesFunction<Env> = async (context) => {
21+
const { request, env } = context;
22+
23+
try {
24+
// Verify admin authentication
25+
const session = await getAuthUser(request, env);
26+
if (!session) {
27+
return errorResponse('Unauthorized', 401);
28+
}
29+
30+
if (!isAdminEmail(session.email, env)) {
31+
return errorResponse('Forbidden - Admin access required', 403);
32+
}
33+
34+
// Get all pending engines with user info
35+
const result = await env.DB.prepare(`
36+
SELECT
37+
e.id,
38+
e.owner_id,
39+
e.definition,
40+
e.visibility,
41+
e.submitted_for_review_at as submitted_at,
42+
e.use_count,
43+
e.created_at,
44+
e.updated_at,
45+
p.email as user_email,
46+
p.display_name as user_display_name
47+
FROM engines e
48+
JOIN profiles p ON e.owner_id = p.id
49+
WHERE e.visibility = 'pending_review'
50+
ORDER BY e.submitted_for_review_at ASC
51+
`).all();
52+
53+
const engines = (result.results as unknown as EngineRow[]).map((row) => ({
54+
id: row.id,
55+
ownerId: row.owner_id,
56+
definition: JSON.parse(row.definition),
57+
visibility: row.visibility,
58+
submittedAt: row.submitted_at,
59+
useCount: row.use_count,
60+
createdAt: row.created_at,
61+
updatedAt: row.updated_at,
62+
user: {
63+
email: row.user_email,
64+
displayName: row.user_display_name,
65+
},
66+
}));
67+
68+
return json({ engines });
69+
} catch (error) {
70+
console.error('Admin pending error:', error);
71+
return errorResponse('Failed to fetch pending engines', 500);
72+
}
73+
};

functions/api/admin/review.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Admin API: Review (approve/reject) an engine
2+
// POST /api/admin/review
3+
4+
import type { Env } from '../../types';
5+
import { getAuthUser, isAdminEmail, json, errorResponse } from '../../utils';
6+
7+
interface ReviewRequest {
8+
engineId: string;
9+
action: 'approve' | 'reject';
10+
reason?: string;
11+
}
12+
13+
export const onRequestPost: PagesFunction<Env> = async (context) => {
14+
const { request, env } = context;
15+
16+
try {
17+
// Verify admin authentication
18+
const session = await getAuthUser(request, env);
19+
if (!session) {
20+
return errorResponse('Unauthorized', 401);
21+
}
22+
23+
if (!isAdminEmail(session.email, env)) {
24+
return errorResponse('Forbidden - Admin access required', 403);
25+
}
26+
27+
const body = await request.json() as ReviewRequest;
28+
const { engineId, action, reason } = body;
29+
30+
if (!engineId || !action) {
31+
return errorResponse('engineId and action are required', 400);
32+
}
33+
34+
if (action !== 'approve' && action !== 'reject') {
35+
return errorResponse('action must be "approve" or "reject"', 400);
36+
}
37+
38+
// Get engine to verify it's pending
39+
const engine = await env.DB.prepare(`
40+
SELECT id, owner_id, visibility FROM engines WHERE id = ?
41+
`).bind(engineId).first() as { id: string; owner_id: string; visibility: string } | null;
42+
43+
if (!engine) {
44+
return errorResponse('Engine not found', 404);
45+
}
46+
47+
if (engine.visibility !== 'pending_review') {
48+
return errorResponse('Engine is not pending review', 400);
49+
}
50+
51+
if (action === 'approve') {
52+
// Approve: set visibility to public
53+
await env.DB.prepare(`
54+
UPDATE engines
55+
SET visibility = 'public',
56+
reviewed_at = datetime('now'),
57+
reviewed_by = ?,
58+
updated_at = datetime('now')
59+
WHERE id = ?
60+
`).bind(session.userId, engineId).run();
61+
62+
return json({
63+
success: true,
64+
message: 'Engine approved and published to The Garden',
65+
visibility: 'public'
66+
});
67+
} else {
68+
// Reject: set visibility back to private
69+
await env.DB.prepare(`
70+
UPDATE engines
71+
SET visibility = 'private',
72+
reviewed_at = datetime('now'),
73+
reviewed_by = ?,
74+
rejection_reason = ?,
75+
updated_at = datetime('now')
76+
WHERE id = ?
77+
`).bind(session.userId, reason || null, engineId).run();
78+
79+
return json({
80+
success: true,
81+
message: 'Engine rejected and returned to private',
82+
visibility: 'private'
83+
});
84+
}
85+
} catch (error) {
86+
console.error('Admin review error:', error);
87+
return errorResponse('Failed to review engine', 500);
88+
}
89+
};

functions/api/auth/me.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const onRequestGet: PagesFunction<Env> = async (context) => {
1414

1515
// Get user profile
1616
const user = await env.DB.prepare(`
17-
SELECT id, email, display_name, avatar_url, is_admin, created_at, updated_at
17+
SELECT id, email, display_name, avatar_url, is_admin, default_engine_id, created_at, updated_at
1818
FROM profiles
1919
WHERE id = ?
2020
`).bind(session.userId).first();
@@ -29,5 +29,6 @@ export const onRequestGet: PagesFunction<Env> = async (context) => {
2929
displayName: user.display_name,
3030
avatarUrl: user.avatar_url,
3131
isAdmin: user.is_admin === 1,
32+
defaultEngineId: user.default_engine_id,
3233
});
3334
};

0 commit comments

Comments
 (0)