Skip to content
56 changes: 52 additions & 4 deletions api/src/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ export type Json =
| Json[]

export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
__InternalSupabase: {
PostgrestVersion: "14.5"
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
extensions?: Json
operationName?: string
query?: string
variables?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
Expand Down Expand Up @@ -1039,6 +1059,24 @@ export type Database = {
},
]
}
user_statuses: {
Row: {
status: Database["public"]["Enums"]["user_status"]
updated_at: string | null
user_id: string
}
Insert: {
status?: Database["public"]["Enums"]["user_status"]
updated_at?: string | null
user_id: string
}
Update: {
status?: Database["public"]["Enums"]["user_status"]
updated_at?: string | null
user_id?: string
}
Relationships: []
}
votes: {
Row: {
created_at: string
Expand Down Expand Up @@ -1233,6 +1271,10 @@ export type Database = {
Args: { p_revision_id: string }
Returns: string
}
reassign_panel_member: {
Args: { p_member_id: string; p_panel_id: string }
Returns: string
}
revise_guide_revision: {
Args: { p_revision_id: string }
Returns: string
Expand Down Expand Up @@ -1284,6 +1326,7 @@ export type Database = {
seat_status: "assigned" | "recused" | "replaced" | "completed"
subject_status: "draft" | "published"
todo_status: "open" | "resolved"
user_status: "active" | "inactive" | "suspended"
vote_direction: "up" | "down"
}
CompositeTypes: {
Expand Down Expand Up @@ -1410,6 +1453,9 @@ export type CompositeTypes<
: never

export const Constants = {
graphql_public: {
Enums: {},
},
public: {
Enums: {
app_role: ["verifier", "moderator", "curator", "admin", "official"],
Expand Down Expand Up @@ -1447,7 +1493,9 @@ export const Constants = {
seat_status: ["assigned", "recused", "replaced", "completed"],
subject_status: ["draft", "published"],
todo_status: ["open", "resolved"],
user_status: ["active", "inactive", "suspended"],
vote_direction: ["up", "down"],
},
},
} as const

7 changes: 5 additions & 2 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { subjectsRouter } from "./routes/subjects";
import { reviewsRouter } from "./routes/reviews";
import { mediaRouter } from "./routes/media";
import { searchRouter } from "./routes/search";
import { dashboardRouter } from "./routes/dashboard";

let specHandler: MiddlewareHandler<HonoEnv> | undefined;
const openApiHandler: MiddlewareHandler<HonoEnv> = (c, next) => {
Expand Down Expand Up @@ -61,7 +62,8 @@ const app = new Hono<HonoEnv>()
.route("/subjects", subjectsRouter)
.route("/reviews", reviewsRouter)
.route("/media", mediaRouter)
.route("/search", searchRouter);
.route("/search", searchRouter)
.route("/dashboard", dashboardRouter);

// Services throw ServiceError to signal HTTP-meaningful failures; map them to
// JSON here so handlers stay free of repeated `if (error) return c.json(...)`.
Expand All @@ -81,10 +83,11 @@ async function scheduled(event: ScheduledController, env: Bindings) {
);

if (event.cron === "*/5 * * * *") {
await Promise.allSettled([
const results = await Promise.allSettled([
assemblePendingPanels(supabase),
sweepExpiredReviewSeats(supabase),
]);
console.log(results);
}
if (event.cron === "0 */12 * * *") await promoteAllCanonicals(supabase);
}
Expand Down
1 change: 1 addition & 0 deletions api/src/middleware/rateLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export const CONTRIBUTION = { windowSeconds: 3_600, max: 60 } as const;
export const MODERATION = { windowSeconds: 3_600, max: 30 } as const;
export const HEAVY = { windowSeconds: 3_600, max: 30 } as const;
export const DESTRUCTIVE = { windowSeconds: 3_600, max: 5 } as const;
export const DASHBOARD = { windowSeconds: 60, max: 10 } as const;
export const READ = { windowSeconds: 60, max: 600, keyBy: "ip" } as const;
export const SEARCH = { windowSeconds: 60, max: 30, keyBy: "ip" } as const;
94 changes: 94 additions & 0 deletions api/src/routes/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import type { HonoEnv } from "../types";
import { requireUser } from "../middleware/auth.middleware";
import {
getUserStatus,
markUserStatus,
suspendUser,
unsuspendUser,
addRole,
removeRole,
fetchRolesTable,
fetchMembersTable,
fetchAssignmentsTable,
reassignPanelMember,
updateStatusSchema,
updateRoleSchema,
roleParamSchema,
} from "../services/dashboard.service";

export const dashboardRouter = new Hono<HonoEnv>()
.use("*", requireUser)
// Get user status (Active, Inactive, Suspended)
.get("/:id/status", async (c) => {
const { id } = c.req.param();
const status = await getUserStatus(c.get("supabase"), id);
return c.json({ status }, 200);
})

// Change users status
.patch("/:id/status", zValidator("json", updateStatusSchema), async (c) => {
const { id: userId } = c.req.param();
const { status } = c.req.valid("json");
const data = await markUserStatus(c.get("supabase"), userId, status);
return c.json({ data }, 200);
})

// Add role to user
.post("/:id/role", zValidator("json", updateRoleSchema), async (c) => {
const { id: userId } = c.req.param();
const { role } = c.req.valid("json");
await addRole(c.get("supabase"), userId, role);
return c.json({ success: true }, 200);
})

// Remove role from user
.delete(
"/:id/role/:roleName",
zValidator("param", roleParamSchema),
async (c) => {
const { id, roleName } = c.req.valid("param");
await removeRole(c.get("supabase"), id, roleName);
return c.json({ success: true }, 200);
}
)

// Fetch roles table
.get("/roles", async (c) => {
const data = await fetchRolesTable(c.get("supabase"));
return c.json({ data }, 200);
})

// Fetch members table
.get("/members", async (c) => {
const data = await fetchMembersTable(c.get("supabase"));
return c.json({ data }, 200);
})

// Fetch assignments table
.get("/assignments", async (c) => {
const data = await fetchAssignmentsTable(c.get("supabase"));
return c.json({ data }, 200);
})

// Suspend user
.patch("/:id/suspend", async (c) => {
const { id } = c.req.param();
await suspendUser(c.get("supabase"), id);
return c.json({ success: true }, 200);
})

// Unsuspend user
.patch("/:id/unsuspend", async (c) => {
const { id } = c.req.param();
await unsuspendUser(c.get("supabase"), id);
return c.json({ success: true }, 200);
})

// Reassign a panel member
.patch("/:id/reassign/:panel_id", async (c) => {
const { id, panel_id } = c.req.param();
await reassignPanelMember(c.get("supabase"), id, panel_id);
return c.json({ success: true }, 200);
});
Loading
Loading