This commit is contained in:
Marcelo
2026-02-17 00:07:00 +00:00
parent b7a86a2d1c
commit be4ca2ed78
92 changed files with 6850 additions and 1188 deletions

36
lib/auth/clientAuth.ts Normal file
View File

@@ -0,0 +1,36 @@
import { DEMO_AUTH_EMAIL_COOKIE, DEMO_AUTH_ROLE_COOKIE } from "@/lib/auth/demoAuth";
export type ClientAuthSnapshot = {
userId: string;
userEmail: string | null;
isAuthed: boolean;
isTeacher: boolean;
};
const readCookieMap = (): Map<string, string> =>
new Map(
document.cookie
.split(";")
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const [key, ...rest] = entry.split("=");
return [key, decodeURIComponent(rest.join("="))] as const;
}),
);
export const readDemoClientAuth = (): ClientAuthSnapshot => {
if (typeof document === "undefined") {
return { userId: "guest", userEmail: null, isAuthed: false, isTeacher: false };
}
const cookies = readCookieMap();
const userEmail = cookies.get(DEMO_AUTH_EMAIL_COOKIE) ?? null;
const role = cookies.get(DEMO_AUTH_ROLE_COOKIE) ?? "";
return {
userId: userEmail ?? "guest",
userEmail,
isAuthed: Boolean(userEmail),
isTeacher: role === "teacher",
};
};

34
lib/auth/demoAuth.ts Normal file
View File

@@ -0,0 +1,34 @@
export const DEMO_AUTH_EMAIL_COOKIE = "acve_demo_email";
export const DEMO_AUTH_ROLE_COOKIE = "acve_demo_role";
type DemoAccount = {
email: string;
password: string;
role: "teacher" | "learner";
};
const demoAccounts: DemoAccount[] = [
{
email: "teacher@acve.local",
password: "teacher123",
role: "teacher",
},
{
email: "learner@acve.local",
password: "learner123",
role: "learner",
},
];
export const demoCredentialsHint = demoAccounts
.map((account) => `${account.email} / ${account.password}`)
.join(" | ");
export const findDemoAccount = (email: string, password: string): DemoAccount | null => {
const cleanEmail = email.trim().toLowerCase();
return (
demoAccounts.find(
(account) => account.email.toLowerCase() === cleanEmail && account.password === password,
) ?? null
);
};

67
lib/auth/requireTeacher.ts Normal file → Executable file
View File

@@ -1,26 +1,53 @@
import { redirect } from "next/navigation";
import { requireUser } from "@/lib/auth/requireUser";
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
import { db } from "@/lib/prisma";
import { UserRole } from "@prisma/client";
import { logger } from "@/lib/logger";
const readTeacherEmails = (): string[] =>
(process.env.TEACHER_EMAILS ?? "")
.split(",")
.map((email) => email.trim().toLowerCase())
.filter(Boolean);
export async function requireTeacher() {
export const requireTeacher = async () => {
const user = await requireUser("/teacher");
if (!user?.email) {
redirect("/");
const cookieStore = await cookies();
// 1. Get Supabase Session
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll() },
setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch (error) {
// This is expected in Server Components, but let's log it just in case
logger.warn("Failed to set cookies in Server Component context (expected behavior)", error);
}
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return null; // Let the caller handle the redirect
}
const allowed = readTeacherEmails();
if (allowed.length === 0) {
redirect("/");
// 2. Check Role in Database
const profile = await db.profile.findUnique({
where: { id: user.id },
}
);
console.log("AUTH_USER_ID:", user.id);
console.log("DB_PROFILE:", profile);
if (!profile || (profile.role !== UserRole.TEACHER && profile.role !== UserRole.SUPER_ADMIN)) {
// You can decide to return null or throw an error here
return null;
}
if (!allowed.includes(user.email.toLowerCase())) {
redirect("/");
}
return user;
};
return profile;
}

28
lib/auth/requireUser.ts Normal file → Executable file
View File

@@ -1,16 +1,18 @@
import { redirect } from "next/navigation";
import { supabaseServer } from "@/lib/supabase/server";
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { db } from "@/lib/prisma";
export const requireUser = async (redirectTo: string) => {
const supabase = await supabaseServer();
if (!supabase) {
return null;
}
export async function requireUser() {
const cookieStore = await cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll() { return cookieStore.getAll() } } }
);
const { data } = await supabase.auth.getUser();
if (!data.user) {
redirect(`/auth/login?redirectTo=${encodeURIComponent(redirectTo)}`);
}
const { data: { user } } = await supabase.auth.getUser();
if (!user) return null;
return data.user;
};
const profile = await db.profile.findUnique({ where: { id: user.id } });
return profile;
}

View File

@@ -0,0 +1,16 @@
const parseTeacherEmails = (source: string | undefined): string[] =>
(source ?? "")
.split(",")
.map((email) => email.trim().toLowerCase())
.filter(Boolean);
export const readTeacherEmailsServer = (): string[] => parseTeacherEmails(process.env.TEACHER_EMAILS);
export const readTeacherEmailsBrowser = (): string[] =>
parseTeacherEmails(process.env.NEXT_PUBLIC_TEACHER_EMAILS);
export const isTeacherEmailAllowed = (email: string | null, allowed: string[]): boolean => {
if (!email) return false;
if (allowed.length === 0) return false;
return allowed.includes(email.toLowerCase());
};