From 1db1367a84fa22bb675f285a6a772771769110db Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Fri, 26 Dec 2025 13:28:50 -1000 Subject: [PATCH] Allow admin marking profile as verified --- src/app/dashboard/page.tsx | 21 +++++++-- src/app/hack/[slug]/actions.ts | 4 +- src/app/hack/[slug]/approve/page.tsx | 44 ++++++++++++++----- src/app/hack/[slug]/page.tsx | 12 ++++- src/app/hack/actions.ts | 23 +++++++--- src/types/db.ts | 3 ++ ...20251226215900_profile_verified_column.sql | 2 + 7 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 supabase/migrations/20251226215900_profile_verified_column.sql diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c9fc9de..ea51be7 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -6,6 +6,7 @@ import DashboardClient from "@/components/Dashboard/DashboardClient"; import ArchiverManagement from "@/components/Dashboard/ArchiverManagement"; import { getDownloadsSeriesAll } from "./actions"; import type { HackRow } from "@/components/Dashboard/DashboardClient"; +import { FaCircleCheck } from "react-icons/fa6"; export default async function DashboardPage() { const supa = await createClient(); @@ -18,7 +19,8 @@ export default async function DashboardPage() { created_by: string; creator_username: string | null; creator_full_name: string | null; - creator_email: string | null + creator_email: string | null; + creator_verified: boolean; })[] = []; if (isAdmin) { const { data: pendingHacksData } = await supa @@ -32,14 +34,16 @@ export default async function DashboardPage() { const creatorIds = [...new Set(pendingHacksData.map(h => h.created_by as string))]; const { data: profiles } = await supa .from("profiles") - .select("id,username,full_name") + .select("id,username,full_name,verified") .in("id", creatorIds); const usernameById = new Map(); const fullNameById = new Map(); + const verifiedById = new Map(); (profiles || []).forEach((p) => { usernameById.set(p.id, p.username); fullNameById.set(p.id, p.full_name); + verifiedById.set(p.id, p.verified); }); // Fetch creator emails using service client (admin API) @@ -62,6 +66,7 @@ export default async function DashboardPage() { creator_username: usernameById.get(h.created_by as string) || null, creator_full_name: fullNameById.get(h.created_by as string) || null, creator_email: emailById.get(h.created_by as string) || null, + creator_verified: verifiedById.get(h.created_by as string) || false, })); } } @@ -164,7 +169,17 @@ export default async function DashboardPage() {
{h.creator_full_name &&
{h.creator_full_name}
} -
{creator}
+
+ {creator} + {h.creator_verified && ( +
+ +
+ Creator is verified +
+
+ )} +
{h.creator_email && (
{h.creator_email}
)} diff --git a/src/app/hack/[slug]/actions.ts b/src/app/hack/[slug]/actions.ts index 2e92159..9368661 100644 --- a/src/app/hack/[slug]/actions.ts +++ b/src/app/hack/[slug]/actions.ts @@ -34,6 +34,7 @@ export interface HackMetadata { profile: { username: string | null; avatar_url: string | null; + verified: boolean; } | null; otherHacks: { slug: string; @@ -90,7 +91,7 @@ export async function getHackMetadata(slug: string): Promise By approving this hack, it will become visible to the public.

-
- - - - + + + +
)} diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index 67c3106..76aaf6e 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -298,7 +298,7 @@ export default async function HackDetail({ params }: HackDetailProps) { )} -
+
{!hack.original_author ? ( <> -

{author}

+

{author}

+ {isAdmin && profile?.verified && ( +
+ +
+ Creator is verified +
+
+ )} ) : (

By {author}

diff --git a/src/app/hack/actions.ts b/src/app/hack/actions.ts index c1a91b9..7268da1 100644 --- a/src/app/hack/actions.ts +++ b/src/app/hack/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { createClient } from "@/utils/supabase/server"; +import { createClient, createServiceClient } from "@/utils/supabase/server"; import type { TablesInsert } from "@/types/db"; import { getMinioClient, PATCHES_BUCKET, COVERS_BUCKET } from "@/utils/minio/server"; import { revalidatePath, revalidateTag } from "next/cache"; @@ -273,7 +273,7 @@ export async function presignCoverUpload(args: { slug: string; objectKey: string } -export async function approveHack(slug: string) { +export async function approveHack(slug: string, verified?: boolean) { const supabase = await createClient(); const { data: { user }, @@ -284,8 +284,10 @@ export async function approveHack(slug: string) { const { data: isAdmin } = await supabase.rpc("is_admin"); if (!isAdmin) return { ok: false, error: "Forbidden" } as const; + const serviceClient = await createServiceClient(); + // Check if hack exists - const { data: hack, error: hErr } = await supabase + const { data: hack, error: hErr } = await serviceClient .from("hacks") .select("slug, approved, title, created_by") .eq("slug", slug) @@ -293,6 +295,17 @@ export async function approveHack(slug: string) { if (hErr) return { ok: false, error: hErr.message } as const; if (!hack) return { ok: false, error: "Hack not found" } as const; + if (verified === true) { + const { error: updateErr } = await serviceClient + .from("profiles") + .update({ verified: true }) + .eq("id", hack.created_by); + if (updateErr) { + // No need to return an error here + console.error(updateErr); + } + } + // If already approved, return success if (hack.approved) { revalidatePath(`/hack/${slug}`); @@ -300,7 +313,7 @@ export async function approveHack(slug: string) { } // Approve the hack - const { error: updateErr } = await supabase + const { error: updateErr } = await serviceClient .from("hacks") .update({ approved: true, @@ -312,7 +325,7 @@ export async function approveHack(slug: string) { if (updateErr) return { ok: false, error: updateErr.message } as const; if (process.env.DISCORD_WEBHOOK_HACKDEX_HACKS_URL) { - const { data: profile } = await supabase.from('profiles').select('*').eq('id', hack.created_by).single(); + const { data: profile } = await serviceClient.from('profiles').select('*').eq('id', hack.created_by).single(); const displayName = profile?.username ? `@${profile.username}` : user.id; const embed: APIEmbed = { title: `:tada: ${hack.title} :tada:`, diff --git a/src/types/db.ts b/src/types/db.ts index c0416cf..a25f0d5 100644 --- a/src/types/db.ts +++ b/src/types/db.ts @@ -396,6 +396,7 @@ export type Database = { id: string updated_at: string | null username: string | null + verified: boolean website: string | null } Insert: { @@ -404,6 +405,7 @@ export type Database = { id: string updated_at?: string | null username?: string | null + verified?: boolean website?: string | null } Update: { @@ -412,6 +414,7 @@ export type Database = { id?: string updated_at?: string | null username?: string | null + verified?: boolean website?: string | null } Relationships: [] diff --git a/supabase/migrations/20251226215900_profile_verified_column.sql b/supabase/migrations/20251226215900_profile_verified_column.sql new file mode 100644 index 0000000..c676c9a --- /dev/null +++ b/supabase/migrations/20251226215900_profile_verified_column.sql @@ -0,0 +1,2 @@ +alter table if exists public.profiles + add column if not exists verified boolean not null default false;