Spaces:
Running
Running
Download src/supabase.ts from Shoraky/Sporalize-api: direct link, hf CLI and curl.
- Browser
- Download file 4.81 kB
-
https://huggingface.co/spaces/Shoraky/Sporalize-api/resolve/main/src/supabase.ts
- Command line
-
hf download hf://spaces/Shoraky/Sporalize-api/src/supabase.ts
-
curl -L -o supabase.ts https://huggingface.co/spaces/Shoraky/Sporalize-api/resolve/main/src/supabase.ts
4.81 kB
| import { createClient, type User } from "@supabase/supabase-js"; | |
| import bcrypt from "bcryptjs"; | |
| import { randomBytes } from "node:crypto"; | |
| import type { Request, Response } from "express"; | |
| import { env, isProduction } from "./config.js"; | |
| import { timedFetch, timedAuthFetch } from "./request-timing.js"; | |
| // Bind Google's PKCE verifier to the initiating browser, not a shared client's | |
| // in-memory store (which concurrent logins can overwrite or a restart can lose). | |
| export function oauthClient(req: Request, res: Response) { | |
| const cookieName = "sp_oauth_verifier"; | |
| const options = { httpOnly: true, secure: isProduction, sameSite: "lax" as const, path: "/api/auth" }; | |
| return createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY, { | |
| global: { fetch: timedAuthFetch }, | |
| auth: { | |
| autoRefreshToken: false, persistSession: true, detectSessionInUrl: false, flowType: "pkce", | |
| storage: { | |
| getItem: (key) => key.endsWith("-code-verifier") ? req.cookies?.[cookieName] ?? null : null, | |
| setItem: (key, value) => { | |
| if (key.endsWith("-code-verifier")) res.cookie(cookieName, value, { ...options, maxAge: 10 * 60 * 1000 }); | |
| }, | |
| removeItem: (key) => { | |
| if (key.endsWith("-code-verifier")) res.clearCookie(cookieName, options); | |
| }, | |
| }, | |
| }, | |
| }); | |
| } | |
| export const supabaseAuth = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY, { | |
| global: { fetch: timedAuthFetch }, | |
| // OAuth returns a one-time code for the backend callback to exchange, rather | |
| // than placing session tokens in the browser URL fragment. | |
| auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false, flowType: "pkce" }, | |
| }); | |
| export const supabaseAdmin = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, { | |
| global: { fetch: timedFetch }, | |
| auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }, | |
| }); | |
| export async function hashPassword(password: string) { | |
| return bcrypt.hash(password, 12); | |
| } | |
| export function userHasPassword(user: User) { | |
| return user.app_metadata?.has_password === true || Boolean(user.identities?.some((identity) => identity.provider === "email")); | |
| } | |
| async function randomPasswordHash() { | |
| return bcrypt.hash(randomBytes(32).toString("hex"), 12); | |
| } | |
| export async function syncProfile(user: User, profile?: { fname?: string; lname?: string; academy?: string | null; role?: string | null; passwordHash?: string }) { | |
| const metadata = user.user_metadata ?? {}; | |
| const { data: existing, error: readError } = await supabaseAdmin.from("users").select("user_id,email").eq("user_id", user.id).maybeSingle(); | |
| if (readError) throw new Error(`Unable to read user profile: ${readError.message}`); | |
| if (existing) { | |
| if (existing.email === user.email) return; | |
| // The profile table is the canonical source for editable profile fields. | |
| // Auth callbacks should only keep the email in sync, never reset user edits. | |
| const { error } = await supabaseAdmin.from("users").update({ email: user.email }).eq("user_id", user.id); | |
| if (error) throw new Error(`Unable to update user profile: ${error.message}`); | |
| return; | |
| } | |
| const fields = { | |
| user_id: user.id, | |
| fname: profile?.fname ?? metadata.fname ?? metadata.full_name?.split(" ")[0] ?? "User", | |
| lname: profile?.lname ?? metadata.lname ?? metadata.full_name?.split(" ").slice(1).join(" ") ?? "User", | |
| email: user.email, | |
| academy: profile?.academy ?? metadata.academy ?? null, | |
| role: profile?.role ?? metadata.role ?? null, | |
| account_role: "user", | |
| }; | |
| const { error } = await supabaseAdmin.from("users").insert({ ...fields, password_hash: profile?.passwordHash ?? await randomPasswordHash() }); | |
| if (error?.code === "23505") { | |
| // Two OAuth callbacks or sign-in requests can race after both observe that | |
| // the profile is missing. Re-read the user ID before treating the conflict | |
| // as a harmless concurrent insert; an email conflict for another user must | |
| // still fail. | |
| const { data: racedProfile, error: racedReadError } = await supabaseAdmin.from("users") | |
| .select("user_id").eq("user_id", user.id).maybeSingle(); | |
| if (racedReadError) throw new Error(`Unable to verify user profile: ${racedReadError.message}`); | |
| if (racedProfile) { | |
| const { error: syncError } = await supabaseAdmin.from("users").update({ email: user.email }).eq("user_id", user.id); | |
| if (syncError) throw new Error(`Unable to update user profile: ${syncError.message}`); | |
| return; | |
| } | |
| } | |
| if (error) throw new Error(`Unable to create user profile: ${error.message}`); | |
| } | |
| export function userPayload(user: User, accountRole = "user") { | |
| return { id: user.id, email: user.email, emailVerified: Boolean(user.email_confirmed_at), accountRole, metadata: user.user_metadata ?? {} }; | |
| } | |