Skip to content

Add moderation endpoints for author profiles#150

Open
admdly wants to merge 3 commits into
mainfrom
feat/dev-profile
Open

Add moderation endpoints for author profiles#150
admdly wants to merge 3 commits into
mainfrom
feat/dev-profile

Conversation

@admdly

@admdly admdly commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary by cubic

Adds developer profile endpoints with a moderator approval flow and new profile fields (bio, avatar, contact email) to the extensions/v2 API. Users can manage their author profile; moderators can review and approve; submissions now require an existing, owned author.

  • New Features

    • PUT /authors/me: create or update the caller’s author profile; returns approved: false until reviewed.
    • GET /authors/unapproved: list unapproved profiles (moderators only).
    • POST /authors/{id}/approve: approve a profile (moderators only).
    • New fields: bio (max 500 chars), avatar_url, contact_email with validation.
    • Editing a profile clears approval.
    • Submissions require the referenced author to exist and be owned by the caller.
    • OpenAPI updated to document new routes.
  • Migration

    • Run 0002_add_author_approval.sql (adds approved_at, timestamps, index) and 0003_add_author_profile_fields.sql (adds bio, avatar_url, contact_email). Update clients/workflows to create an author profile before submitting extensions.

Written for commit 21872cf. Summary will update on new commits.

@admdly admdly self-assigned this Jul 24, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
api 21872cf Commit Preview URL

Branch Preview URL
Jul 24 2026, 01:42 PM

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/services/extensions/v2/index.ts">

<violation number="1" location="src/services/extensions/v2/index.ts:469">
P2: Concurrent first-time `PUT /authors/me` requests with different ids can both succeed, leaving one user with multiple profiles despite the route's singular ownership model; subsequent updates select an arbitrary profile. Enforce unique non-null `owner_user_id` in the schema and map the resulting constraint conflict to 409.</violation>
</file>

<file name="test/services/extensions/v2/index.test.ts">

<violation number="1" location="test/services/extensions/v2/index.test.ts:594">
P3: This test does not establish that the profile was approved before editing, so it can pass without covering the approval-clearing behavior. Checking the approval response (and its `approved: true` result) before the edit would make the regression test meaningful.</violation>
</file>

<file name="src/services/extensions/v2/authors-database.ts">

<violation number="1" location="src/services/extensions/v2/authors-database.ts:22">
P3: `getOwn()` is never called anywhere in the repository, so this new method adds an unused API surface and untested profile-reading logic. Removing it, or wiring it to an intended GET-own-profile endpoint, would keep the author database surface aligned with the actual routes.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const platform = getPlatform(c);
const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS"));

const { data, error } = await db.upsertOwn(auth.userId, body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Concurrent first-time PUT /authors/me requests with different ids can both succeed, leaving one user with multiple profiles despite the route's singular ownership model; subsequent updates select an arbitrary profile. Enforce unique non-null owner_user_id in the schema and map the resulting constraint conflict to 409.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/services/extensions/v2/index.ts, line 469:

<comment>Concurrent first-time `PUT /authors/me` requests with different ids can both succeed, leaving one user with multiple profiles despite the route's singular ownership model; subsequent updates select an arbitrary profile. Enforce unique non-null `owner_user_id` in the schema and map the resulting constraint conflict to 409.</comment>

<file context>
@@ -416,6 +419,185 @@ extensionsV2.openapi(rejectRoute, async (c) => {
+  const platform = getPlatform(c);
+  const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS"));
+
+  const { data, error } = await db.upsertOwn(auth.userId, body);
+  if (error || !data) {
+    const status = error?.code === "CONFLICT" ? 409 : 500;
</file context>

);
tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 });
await post(
"/extensions/v2/authors/dev-author/approve",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test does not establish that the profile was approved before editing, so it can pass without covering the approval-clearing behavior. Checking the approval response (and its approved: true result) before the edit would make the regression test meaningful.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/services/extensions/v2/index.test.ts, line 594:

<comment>This test does not establish that the profile was approved before editing, so it can pass without covering the approval-clearing behavior. Checking the approval response (and its `approved: true` result) before the edit would make the regression test meaningful.</comment>

<file context>
@@ -448,6 +512,166 @@ describe("Extensions API v2", () => {
+      );
+      tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 });
+      await post(
+        "/extensions/v2/authors/dev-author/approve",
+        await authHeaders("mod-1")
+      );
</file context>

this.db = db;
}

async getOwn(userId: string): Promise<DatabaseResult<AuthorProfile | null>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: getOwn() is never called anywhere in the repository, so this new method adds an unused API surface and untested profile-reading logic. Removing it, or wiring it to an intended GET-own-profile endpoint, would keep the author database surface aligned with the actual routes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/services/extensions/v2/authors-database.ts, line 22:

<comment>`getOwn()` is never called anywhere in the repository, so this new method adds an unused API surface and untested profile-reading logic. Removing it, or wiring it to an intended GET-own-profile endpoint, would keep the author database surface aligned with the actual routes.</comment>

<file context>
@@ -0,0 +1,184 @@
+    this.db = db;
+  }
+
+  async getOwn(userId: string): Promise<DatabaseResult<AuthorProfile | null>> {
+    try {
+      const row = await this.db
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/services/extensions/v2/db/migrations/0002_add_author_approval.sql">

<violation number="1" location="src/services/extensions/v2/db/migrations/0002_add_author_approval.sql:11">
P2: Authors created while approving a submission retain the 1970 timestamp, because that insert path omits both new columns. They will sort ahead of every real submission in `/authors/unapproved`; set timestamps in that insert path (or use a trigger) so the migration's placeholder cannot persist.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

-- outside of this migration.

ALTER TABLE authors ADD COLUMN approved_at TEXT;
ALTER TABLE authors ADD COLUMN created_at TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Authors created while approving a submission retain the 1970 timestamp, because that insert path omits both new columns. They will sort ahead of every real submission in /authors/unapproved; set timestamps in that insert path (or use a trigger) so the migration's placeholder cannot persist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/services/extensions/v2/db/migrations/0002_add_author_approval.sql, line 11:

<comment>Authors created while approving a submission retain the 1970 timestamp, because that insert path omits both new columns. They will sort ahead of every real submission in `/authors/unapproved`; set timestamps in that insert path (or use a trigger) so the migration's placeholder cannot persist.</comment>

<file context>
@@ -1,8 +1,16 @@
 ALTER TABLE authors ADD COLUMN approved_at TEXT;
-ALTER TABLE authors ADD COLUMN created_at  TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP;
-ALTER TABLE authors ADD COLUMN updated_at  TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP;
+ALTER TABLE authors ADD COLUMN created_at  TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
+ALTER TABLE authors ADD COLUMN updated_at  TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
+
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/services/extensions/v2/index.test.ts">

<violation number="1" location="test/services/extensions/v2/index.test.ts:609">
P3: Updates to an existing author are not covered for `bio`, `avatar_url`, or `contact_email`; the new test exercises only insertion, so the `UPDATE authors SET ...` path could drop these fields without failing tests. A second PUT with the profile fields followed by response and stored-row assertions would cover the update workflow.</violation>
</file>

<file name="src/services/extensions/v2/interfaces.ts">

<violation number="1" location="src/services/extensions/v2/interfaces.ts:38">
P2: Author profile fields sent with an extension submission are silently discarded when that submission is approved. Persist these fields in the approval UPSERT, or use a submission-specific author schema that rejects profile fields until `/authors/me` owns them.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

name: z.string().min(1),
URL: httpUrl().optional()
URL: httpUrl().optional(),
bio: z.string().max(500).optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Author profile fields sent with an extension submission are silently discarded when that submission is approved. Persist these fields in the approval UPSERT, or use a submission-specific author schema that rejects profile fields until /authors/me owns them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/services/extensions/v2/interfaces.ts, line 38:

<comment>Author profile fields sent with an extension submission are silently discarded when that submission is approved. Persist these fields in the approval UPSERT, or use a submission-specific author schema that rejects profile fields until `/authors/me` owns them.</comment>

<file context>
@@ -34,7 +34,10 @@ export const AuthorSchema = z
     name: z.string().min(1),
-    URL: httpUrl().optional()
+    URL: httpUrl().optional(),
+    bio: z.string().max(500).optional(),
+    avatar_url: httpUrl().optional(),
+    contact_email: z.string().email().optional()
</file context>

expect(data.result.approved).toBe(false);
});

it("round-trips bio, avatar_url, and contact_email", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Updates to an existing author are not covered for bio, avatar_url, or contact_email; the new test exercises only insertion, so the UPDATE authors SET ... path could drop these fields without failing tests. A second PUT with the profile fields followed by response and stored-row assertions would cover the update workflow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/services/extensions/v2/index.test.ts, line 609:

<comment>Updates to an existing author are not covered for `bio`, `avatar_url`, or `contact_email`; the new test exercises only insertion, so the `UPDATE authors SET ...` path could drop these fields without failing tests. A second PUT with the profile fields followed by response and stored-row assertions would cover the update workflow.</comment>

<file context>
@@ -605,6 +605,53 @@ describe("Extensions API v2", () => {
       expect(data.result.approved).toBe(false);
     });
+
+    it("round-trips bio, avatar_url, and contact_email", async () => {
+      const headers = await authHeaders("user-1");
+      const res = await put("/extensions/v2/authors/me", headers, {
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant