From 6efd626bc35958a3ed38c90f1366b5cd404d1d6c Mon Sep 17 00:00:00 2001 From: wlenig <30681316+wlenig@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:51:38 -0400 Subject: [PATCH 1/5] AUTH-7 Add User server actions, supabase client --- src/actions/users.ts | 83 ++++++++++++++++++++++++++++++++++++++++++++ src/lib/supabase.ts | 17 +++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/actions/users.ts create mode 100644 src/lib/supabase.ts diff --git a/src/actions/users.ts b/src/actions/users.ts new file mode 100644 index 0000000..eceb40f --- /dev/null +++ b/src/actions/users.ts @@ -0,0 +1,83 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { supabaseAdmin } from "@/lib/supabase"; +import type { User } from "@/generated/prisma/client"; + +export async function createUser(supabaseUserId: string): Promise { + const { error } = + await supabaseAdmin.auth.admin.getUserById(supabaseUserId); + + if (error) { + throw new Error("Supabase auth user not found"); + } + + return prisma.user.create({ data: { supabaseUserId } }); +} + +export async function getUser( + id: string, + options?: { includeEmail?: boolean } +): Promise { + const user = await prisma.user.findUnique({ where: { id } }); + + if (!user) { + throw new Error("User not found"); + } + + if (!options?.includeEmail) { + return user; + } + + const { data, error } = await supabaseAdmin.auth.admin.getUserById( + user.supabaseUserId + ); + + if (error) { + throw new Error(error.message); + } + + if (!data.user) { + throw new Error("Supabase auth user not found"); + } + + return { ...user, email: data.user.email }; +} + +export async function getUsers(filters?: { + isAdmin?: boolean; +}): Promise { + return prisma.user.findMany({ + where: filters, + orderBy: { createdAt: "desc" }, + }); +} + +export async function updateUser( + id: string, + data: { isAdmin?: boolean } +): Promise { + return prisma.user.update({ where: { id }, data }); +} + +export async function deleteUser(id: string): Promise { + const user = await prisma.user.findUnique({ where: { id } }); + + if (!user) { + throw new Error("User not found"); + } + + const { error } = await supabaseAdmin.auth.admin.deleteUser( + user.supabaseUserId + ); + + if (error) { + throw new Error("Failed to delete Supabase auth user"); + } + + await prisma.$transaction([ + prisma.session.deleteMany({ where: { userId: id } }), + prisma.userProject.deleteMany({ where: { userId: id } }), + prisma.user.delete({ where: { id } }), + ]); +} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts new file mode 100644 index 0000000..d4857b1 --- /dev/null +++ b/src/lib/supabase.ts @@ -0,0 +1,17 @@ +import "server-only"; +import { createClient } from "@supabase/supabase-js"; + +const globalForSupabase = globalThis as unknown as { + supabase?: ReturnType; +}; + +export const supabaseAdmin = + globalForSupabase.supabase ?? + createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); + +if (process.env.NODE_ENV !== "production") { + globalForSupabase.supabase = supabaseAdmin; +} From f12c22a1ba71b1020fee5257ff872bdd51765d03 Mon Sep 17 00:00:00 2001 From: wlenig <30681316+wlenig@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:01:28 -0400 Subject: [PATCH 2/5] AUTH-7 Remove redundant getUser error check --- src/actions/users.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/actions/users.ts b/src/actions/users.ts index eceb40f..3a4a2ae 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -37,10 +37,6 @@ export async function getUser( throw new Error(error.message); } - if (!data.user) { - throw new Error("Supabase auth user not found"); - } - return { ...user, email: data.user.email }; } From 119a9f75c9564096d3943c0a0c0d3fdf52004a73 Mon Sep 17 00:00:00 2001 From: wlenig <30681316+wlenig@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:23:10 -0400 Subject: [PATCH 3/5] AUTH-7 Move user actions to `lib/` --- src/{actions => lib}/users.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{actions => lib}/users.ts (100%) diff --git a/src/actions/users.ts b/src/lib/users.ts similarity index 100% rename from src/actions/users.ts rename to src/lib/users.ts From b4775ed4c391c783afd3b0831065a348a1718713 Mon Sep 17 00:00:00 2001 From: wlenig <30681316+wlenig@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:01:09 -0400 Subject: [PATCH 4/5] AUTH-7 Add documentation --- src/lib/users.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lib/users.ts b/src/lib/users.ts index 3a4a2ae..9c648bc 100644 --- a/src/lib/users.ts +++ b/src/lib/users.ts @@ -4,6 +4,11 @@ import { prisma } from "@/lib/prisma"; import { supabaseAdmin } from "@/lib/supabase"; import type { User } from "@/generated/prisma/client"; +/** + * Creates a new User linked to a pre-existing Supabase auth user + * @param supabaseUserId Supabase UID + * @returns The created User + */ export async function createUser(supabaseUserId: string): Promise { const { error } = await supabaseAdmin.auth.admin.getUserById(supabaseUserId); @@ -15,9 +20,16 @@ export async function createUser(supabaseUserId: string): Promise { return prisma.user.create({ data: { supabaseUserId } }); } +/** + * Gets a User by ID, optionally including their Supabase auth email + * @param id User UUID to lookup + * @param includeEmail Whether to include the user's auth email + * @returns The fetched User, with email if requested + * @throws If the User is not found or if there's an error fetching the email + */ export async function getUser( id: string, - options?: { includeEmail?: boolean } + includeEmail?: boolean ): Promise { const user = await prisma.user.findUnique({ where: { id } }); @@ -25,7 +37,7 @@ export async function getUser( throw new Error("User not found"); } - if (!options?.includeEmail) { + if (!includeEmail) { return user; } @@ -40,6 +52,11 @@ export async function getUser( return { ...user, email: data.user.email }; } +/** + * Gets all users with filters + * @param filters An optional filter by isAdmin status + * @returns The list of Users + */ export async function getUsers(filters?: { isAdmin?: boolean; }): Promise { @@ -49,6 +66,12 @@ export async function getUsers(filters?: { }); } +/** + * Updates User fields by ID + * @param id User UUID to update + * @param data The data to update (e.g. isAdmin status) + * @returns The updated User + */ export async function updateUser( id: string, data: { isAdmin?: boolean } @@ -56,6 +79,12 @@ export async function updateUser( return prisma.user.update({ where: { id }, data }); } +/** + * Deletes a User by ID. Attempts to delete the linked Supabase auth user first, + * then hard deletes all related data in a transaction + * @param id User UUID to delete + * @throws If the User is not found, or the Supabase auth user deletion fails + */ export async function deleteUser(id: string): Promise { const user = await prisma.user.findUnique({ where: { id } }); From 7e7beb0caf2ccc4dc7330c11da518d0e2733fe7c Mon Sep 17 00:00:00 2001 From: wlenig <30681316+wlenig@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:09:12 -0400 Subject: [PATCH 5/5] AUTH-7 Prettier --- src/lib/supabase.ts | 2 +- src/lib/users.ts | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index d4857b1..718a5c3 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -9,7 +9,7 @@ export const supabaseAdmin = globalForSupabase.supabase ?? createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! + process.env.SUPABASE_SERVICE_ROLE_KEY!, ); if (process.env.NODE_ENV !== "production") { diff --git a/src/lib/users.ts b/src/lib/users.ts index 9c648bc..1bfcbd6 100644 --- a/src/lib/users.ts +++ b/src/lib/users.ts @@ -10,8 +10,7 @@ import type { User } from "@/generated/prisma/client"; * @returns The created User */ export async function createUser(supabaseUserId: string): Promise { - const { error } = - await supabaseAdmin.auth.admin.getUserById(supabaseUserId); + const { error } = await supabaseAdmin.auth.admin.getUserById(supabaseUserId); if (error) { throw new Error("Supabase auth user not found"); @@ -29,7 +28,7 @@ export async function createUser(supabaseUserId: string): Promise { */ export async function getUser( id: string, - includeEmail?: boolean + includeEmail?: boolean, ): Promise { const user = await prisma.user.findUnique({ where: { id } }); @@ -42,7 +41,7 @@ export async function getUser( } const { data, error } = await supabaseAdmin.auth.admin.getUserById( - user.supabaseUserId + user.supabaseUserId, ); if (error) { @@ -74,13 +73,13 @@ export async function getUsers(filters?: { */ export async function updateUser( id: string, - data: { isAdmin?: boolean } + data: { isAdmin?: boolean }, ): Promise { return prisma.user.update({ where: { id }, data }); } /** - * Deletes a User by ID. Attempts to delete the linked Supabase auth user first, + * Deletes a User by ID. Attempts to delete the linked Supabase auth user first, * then hard deletes all related data in a transaction * @param id User UUID to delete * @throws If the User is not found, or the Supabase auth user deletion fails @@ -93,7 +92,7 @@ export async function deleteUser(id: string): Promise { } const { error } = await supabaseAdmin.auth.admin.deleteUser( - user.supabaseUserId + user.supabaseUserId, ); if (error) {