advance
This commit is contained in:
36
lib/auth/clientAuth.ts
Normal file
36
lib/auth/clientAuth.ts
Normal 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
34
lib/auth/demoAuth.ts
Normal 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
67
lib/auth/requireTeacher.ts
Normal file → Executable 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
28
lib/auth/requireUser.ts
Normal file → Executable 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;
|
||||
}
|
||||
16
lib/auth/teacherAllowlist.ts
Normal file
16
lib/auth/teacherAllowlist.ts
Normal 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());
|
||||
};
|
||||
Reference in New Issue
Block a user