Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
995e97c
feat: add comprehensive chat moderation tools for streamers
Josue19-08 Mar 25, 2026
ddd4e6e
chore: fix code formatting for CI
Josue19-08 Mar 25, 2026
c78b481
fix: add required curly braces for if statements
Josue19-08 Mar 25, 2026
90ceb73
test: update chat API tests for moderation enforcement
Josue19-08 Mar 25, 2026
466c282
test: fix mock count for chat moderation
Josue19-08 Mar 25, 2026
5450635
db: add stream access control foundation schema and migration
KevinMB0220 Mar 25, 2026
298da3b
lib: implement checkStreamAccess and API endpoint
KevinMB0220 Mar 25, 2026
3928f6e
api: update user and stream routes with access control fields
KevinMB0220 Mar 25, 2026
a707e94
feat: implement AccessGate component and frontend gating
KevinMB0220 Mar 25, 2026
b4a7ad8
feat: add Stream Access settings to creator dashboard
KevinMB0220 Mar 25, 2026
987ae7c
Merge pull request #379 from Josue19-08/issue-370-chat-moderation
davedumto Mar 27, 2026
8840653
merge: resolve conflicts with origin/dev
KevinMB0220 Mar 27, 2026
78aba8a
Merge pull request #381 from KevinMB0220/feat/access-control-foundation
davedumto Mar 27, 2026
8829991
fix(stream-manager): preserve access config when saving stream info
KevinMB0220 Mar 27, 2026
38d1ce4
test(import): add 28 unit tests for import api route
KevinMB0220 Mar 27, 2026
fc14526
fix(lint): resolve pre-existing eslint errors blocking ci
KevinMB0220 Mar 27, 2026
50b7910
fix(security): single-pass html entity decode; fix remaining lint errors
KevinMB0220 Mar 27, 2026
54badeb
style: format streamaccesssettings and view-stream with prettier
KevinMB0220 Mar 27, 2026
913c8f2
Merge pull request #426 from KevinMB0220/feat/import-api-397
davedumto Mar 27, 2026
7e763e2
feat: add creator verification routes
zuru122 Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
606 changes: 606 additions & 0 deletions app/api/routes-f/import/__tests__/route.test.ts

Large diffs are not rendered by default.

567 changes: 567 additions & 0 deletions app/api/routes-f/import/route.ts

Large diffs are not rendered by default.

301 changes: 301 additions & 0 deletions app/api/routes-f/verification/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
jest.mock("next/server", () => ({
NextResponse: {
json: (body: unknown, init?: ResponseInit) =>
new Response(JSON.stringify(body), {
...init,
headers: { "Content-Type": "application/json" },
}),
},
}));

jest.mock("@vercel/postgres", () => ({ sql: jest.fn() }));

jest.mock("@/lib/auth/verify-session", () => ({
verifySession: jest.fn(),
}));

import { sql } from "@vercel/postgres";
import { verifySession } from "@/lib/auth/verify-session";
import { GET as getVerification, POST } from "../route";
import { GET as getAdminVerification } from "../admin/route";

const sqlMock = sql as unknown as jest.Mock;
const verifySessionMock = verifySession as jest.Mock;

const authedSession = {
ok: true as const,
userId: "user-123",
wallet: "G123",
privyId: "did:privy:abc",
username: "creator",
email: "creator@example.com",
};

function makeRequest(path: string, method: string, body?: object) {
return new Request(`http://localhost${path}`, {
method,
headers: { "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
}) as unknown as import("next/server").NextRequest;
}

function mockEnsureTable() {
sqlMock.mockResolvedValueOnce({ rows: [], rowCount: 0 });
sqlMock.mockResolvedValueOnce({ rows: [], rowCount: 0 });
}

describe("/api/routes-f/verification", () => {
beforeEach(() => {
jest.clearAllMocks();
delete process.env.VERIFICATION_ADMIN_USER_IDS;
delete process.env.VERIFICATION_ADMIN_EMAILS;
verifySessionMock.mockResolvedValue(authedSession);
});

it("returns unverified when no request exists", async () => {
mockEnsureTable();
sqlMock.mockResolvedValueOnce({ rows: [] });

const res = await getVerification(
makeRequest("/api/routes-f/verification", "GET")
);

expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({
status: "unverified",
request: null,
});
});

it("creates a pending request for a new submission", async () => {
mockEnsureTable();
sqlMock.mockResolvedValueOnce({ rows: [] }); // initial SELECT
sqlMock.mockResolvedValueOnce({ rows: [], rowCount: 1 }); // INSERT
sqlMock.mockResolvedValueOnce({
rows: [
{
id: "req-1",
user_id: "user-123",
status: "pending",
social_links: [
{
socialTitle: "Twitter",
socialLink: "https://twitter.com/creator",
},
],
reason: "Large audience and active community.",
id_document_url: "https://example.com/id.png",
rejection_reason: null,
reviewed_by: null,
reviewed_at: null,
created_at: "2026-03-27T10:00:00.000Z",
updated_at: "2026-03-27T10:00:00.000Z",
},
],
});

const res = await POST(
makeRequest("/api/routes-f/verification", "POST", {
social_links: [
{ title: "Twitter", url: "https://twitter.com/creator" },
],
reason: "Large audience and active community.",
id_document_url: "https://example.com/id.png",
})
);

expect(res.status).toBe(201);
await expect(res.json()).resolves.toEqual({
status: "pending",
request: {
social_links: [
{
socialTitle: "Twitter",
socialLink: "https://twitter.com/creator",
},
],
reason: "Large audience and active community.",
id_document_url: "https://example.com/id.png",
rejection_reason: null,
reviewed_at: null,
created_at: "2026-03-27T10:00:00.000Z",
updated_at: "2026-03-27T10:00:00.000Z",
},
});
});

it("rejects resubmission while a request is pending", async () => {
mockEnsureTable();
sqlMock.mockResolvedValueOnce({
rows: [
{
id: "req-1",
user_id: "user-123",
status: "pending",
social_links: [],
reason: "Already pending",
id_document_url: null,
rejection_reason: null,
reviewed_by: null,
reviewed_at: null,
created_at: "2026-03-27T10:00:00.000Z",
updated_at: "2026-03-27T10:00:00.000Z",
},
],
});

const res = await POST(
makeRequest("/api/routes-f/verification", "POST", {
social_links: [
{ socialTitle: "Twitter", socialLink: "https://twitter.com/creator" },
],
reason: "Retrying while pending",
})
);

expect(res.status).toBe(409);
await expect(res.json()).resolves.toEqual({
error: "Cannot resubmit while verification is pending",
});
});

it("allows rejected creators to resubmit", async () => {
mockEnsureTable();
sqlMock.mockResolvedValueOnce({
rows: [
{
id: "req-1",
user_id: "user-123",
status: "rejected",
social_links: [],
reason: "Old request",
id_document_url: null,
rejection_reason: "Need stronger proof",
reviewed_by: "admin-1",
reviewed_at: "2026-03-26T10:00:00.000Z",
created_at: "2026-03-25T10:00:00.000Z",
updated_at: "2026-03-26T10:00:00.000Z",
},
],
});
sqlMock.mockResolvedValueOnce({ rows: [], rowCount: 1 }); // UPDATE
sqlMock.mockResolvedValueOnce({
rows: [
{
id: "req-1",
user_id: "user-123",
status: "pending",
social_links: [
{
socialTitle: "YouTube",
socialLink: "https://youtube.com/@creator",
},
],
reason: "Updated information",
id_document_url: null,
rejection_reason: null,
reviewed_by: null,
reviewed_at: null,
created_at: "2026-03-25T10:00:00.000Z",
updated_at: "2026-03-27T10:00:00.000Z",
},
],
});

const res = await POST(
makeRequest("/api/routes-f/verification", "POST", {
social_links: [
{ platform: "YouTube", url: "https://youtube.com/@creator" },
],
reason: "Updated information",
})
);

expect(res.status).toBe(201);
const updateCall = sqlMock.mock.calls[3];
expect(updateCall[1]).toContain("YouTube");
});

it("requires admin session for the admin endpoint", async () => {
process.env.VERIFICATION_ADMIN_EMAILS = "admin@example.com";
verifySessionMock.mockResolvedValue({
...authedSession,
email: "creator@example.com",
});

const res = await getAdminVerification(
makeRequest("/api/routes-f/verification/admin", "GET")
);

expect(res.status).toBe(403);
await expect(res.json()).resolves.toEqual({ error: "Forbidden" });
});

it("lists pending verification requests for admins", async () => {
process.env.VERIFICATION_ADMIN_EMAILS = "admin@example.com";
verifySessionMock.mockResolvedValue({
...authedSession,
email: "admin@example.com",
});

mockEnsureTable();
sqlMock.mockResolvedValueOnce({
rows: [
{
id: "req-9",
user_id: "user-999",
status: "pending",
social_links: [
{
socialTitle: "Twitter",
socialLink: "https://twitter.com/pending",
},
],
reason: "Please verify my creator profile.",
id_document_url: "https://example.com/id.pdf",
rejection_reason: null,
reviewed_by: null,
reviewed_at: null,
created_at: "2026-03-27T09:00:00.000Z",
updated_at: "2026-03-27T09:00:00.000Z",
username: "pending-creator",
email: "pending@example.com",
wallet: "G999",
avatar: "/Images/user.png",
},
],
});

const res = await getAdminVerification(
makeRequest("/api/routes-f/verification/admin", "GET")
);

expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({
requests: [
{
id: "req-9",
user_id: "user-999",
username: "pending-creator",
email: "pending@example.com",
wallet: "G999",
avatar: "/Images/user.png",
status: "pending",
social_links: [
{
socialTitle: "Twitter",
socialLink: "https://twitter.com/pending",
},
],
reason: "Please verify my creator profile.",
id_document_url: "https://example.com/id.pdf",
rejection_reason: null,
reviewed_by: null,
reviewed_at: null,
created_at: "2026-03-27T09:00:00.000Z",
updated_at: "2026-03-27T09:00:00.000Z",
},
],
});
});
});
Loading