feat: initial commit
This commit is contained in:
commit
7d443a004a
76 changed files with 15704 additions and 0 deletions
71
lib/auth/messages.ts
Normal file
71
lib/auth/messages.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
export type AuthNoticeTone = "error" | "success" | "info";
|
||||
|
||||
export type AuthNotice = {
|
||||
tone: AuthNoticeTone;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const errorMessages: Record<string, AuthNotice> = {
|
||||
"auth-not-configured": {
|
||||
tone: "error",
|
||||
text: "Supabase is nog niet geconfigureerd. Voeg eerst je URL en publishable key toe.",
|
||||
},
|
||||
"invalid-credentials": {
|
||||
tone: "error",
|
||||
text: "De combinatie van e-mailadres en wachtwoord klopt niet.",
|
||||
},
|
||||
"email-not-confirmed": {
|
||||
tone: "error",
|
||||
text: "Bevestig eerst je e-mailadres via de link in je inbox.",
|
||||
},
|
||||
"missing-fields": {
|
||||
tone: "error",
|
||||
text: "Vul zowel je e-mailadres als je wachtwoord in.",
|
||||
},
|
||||
"signup-failed": {
|
||||
tone: "error",
|
||||
text: "Je account kon niet worden aangemaakt. Probeer het opnieuw.",
|
||||
},
|
||||
"signup-rate-limited": {
|
||||
tone: "error",
|
||||
text: "Er zijn nu te veel verificatie-e-mails verstuurd. Wacht even en probeer het daarna opnieuw.",
|
||||
},
|
||||
"login-failed": {
|
||||
tone: "error",
|
||||
text: "Inloggen is niet gelukt. Probeer het opnieuw.",
|
||||
},
|
||||
"verification-failed": {
|
||||
tone: "error",
|
||||
text: "De verificatielink is ongeldig of verlopen. Vraag zo nodig een nieuwe aan.",
|
||||
},
|
||||
};
|
||||
|
||||
const statusMessages: Record<string, AuthNotice> = {
|
||||
"check-email": {
|
||||
tone: "success",
|
||||
text: "Controleer je e-mail en activeer je account via de verificatielink.",
|
||||
},
|
||||
"signed-out": {
|
||||
tone: "info",
|
||||
text: "Je bent uitgelogd.",
|
||||
},
|
||||
verified: {
|
||||
tone: "success",
|
||||
text: "Je e-mailadres is bevestigd. Welkom terug.",
|
||||
},
|
||||
};
|
||||
|
||||
export function getAuthNotice(
|
||||
error?: string | null,
|
||||
status?: string | null,
|
||||
): AuthNotice | null {
|
||||
if (error && errorMessages[error]) {
|
||||
return errorMessages[error];
|
||||
}
|
||||
|
||||
if (status && statusMessages[status]) {
|
||||
return statusMessages[status];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
41
lib/auth/navigation.ts
Normal file
41
lib/auth/navigation.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const DEFAULT_NEXT_PATH = "/dashboard";
|
||||
|
||||
export function sanitizeNextPath(candidate?: string | null): string {
|
||||
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) {
|
||||
return DEFAULT_NEXT_PATH;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function buildPathWithQuery(
|
||||
pathname: string,
|
||||
params: Record<string, string | null | undefined>,
|
||||
): string {
|
||||
const search = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value) {
|
||||
search.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const query = search.toString();
|
||||
return query ? `${pathname}?${query}` : pathname;
|
||||
}
|
||||
|
||||
export function getRequestOrigin(headers: Pick<Headers, "get">): string {
|
||||
const origin = headers.get("origin");
|
||||
if (origin) {
|
||||
return origin;
|
||||
}
|
||||
|
||||
const protocol = headers.get("x-forwarded-proto") ?? "http";
|
||||
const host = headers.get("x-forwarded-host") ?? headers.get("host");
|
||||
|
||||
if (host) {
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
return "http://localhost:3000";
|
||||
}
|
||||
64
lib/auth/session.ts
Normal file
64
lib/auth/session.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { createClient } from "@/lib/supabase/server";
|
||||
import { hasSupabaseEnv } from "@/lib/supabase/config";
|
||||
|
||||
type ClaimsShape = {
|
||||
sub?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
export type AuthenticatedUser = {
|
||||
id: string;
|
||||
email: string | null;
|
||||
};
|
||||
|
||||
export type AuthState = {
|
||||
isConfigured: boolean;
|
||||
isAuthenticated: boolean;
|
||||
userId: string | null;
|
||||
email: string | null;
|
||||
};
|
||||
|
||||
export async function getAuthenticatedUser(): Promise<AuthenticatedUser | null> {
|
||||
if (!hasSupabaseEnv()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
const { data } = await supabase.auth.getClaims();
|
||||
|
||||
if (!data?.claims) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const claims = data.claims as ClaimsShape;
|
||||
const userId = typeof claims.sub === "string" ? claims.sub : null;
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
email: typeof claims.email === "string" ? claims.email : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAuthState(): Promise<AuthState> {
|
||||
if (!hasSupabaseEnv()) {
|
||||
return {
|
||||
isConfigured: false,
|
||||
isAuthenticated: false,
|
||||
userId: null,
|
||||
email: null,
|
||||
};
|
||||
}
|
||||
|
||||
const authenticatedUser = await getAuthenticatedUser();
|
||||
|
||||
return {
|
||||
isConfigured: true,
|
||||
isAuthenticated: Boolean(authenticatedUser),
|
||||
userId: authenticatedUser?.id ?? null,
|
||||
email: authenticatedUser?.email ?? null,
|
||||
};
|
||||
}
|
||||
28
lib/onboarding/options.ts
Normal file
28
lib/onboarding/options.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export const ONBOARDING_TIMEZONE_OPTIONS = [
|
||||
{
|
||||
value: "Europe/Amsterdam",
|
||||
label: "Europa/Amsterdam",
|
||||
},
|
||||
{
|
||||
value: "Europe/Brussels",
|
||||
label: "Europa/Brussel",
|
||||
},
|
||||
{
|
||||
value: "Europe/Berlin",
|
||||
label: "Europa/Berlijn",
|
||||
},
|
||||
{
|
||||
value: "UTC",
|
||||
label: "UTC",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const ONBOARDING_TIMEZONE_SET = new Set(
|
||||
ONBOARDING_TIMEZONE_OPTIONS.map((option) => option.value),
|
||||
);
|
||||
|
||||
export function isSupportedOnboardingTimezone(value: string) {
|
||||
return ONBOARDING_TIMEZONE_SET.has(
|
||||
value as (typeof ONBOARDING_TIMEZONE_OPTIONS)[number]["value"],
|
||||
);
|
||||
}
|
||||
385
lib/profile/service.ts
Normal file
385
lib/profile/service.ts
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
import { getAuthenticatedUser } from "@/lib/auth/session";
|
||||
import { isSupportedOnboardingTimezone } from "@/lib/onboarding/options";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import type {
|
||||
OnboardingSubmission,
|
||||
ProfileBundle,
|
||||
ProfileRecord,
|
||||
SettingsSubmission,
|
||||
UserSettingsRecord,
|
||||
} from "@/lib/profile/types";
|
||||
|
||||
type SupabaseServerClient = Awaited<ReturnType<typeof createClient>>;
|
||||
|
||||
type ProfileRow = {
|
||||
id: string;
|
||||
email: string | null;
|
||||
display_name: string | null;
|
||||
locale: string;
|
||||
timezone: string;
|
||||
onboarding_seen: boolean;
|
||||
onboarding_completed: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type UserSettingsRow = {
|
||||
profile_id: string;
|
||||
morning_reminder_enabled: boolean;
|
||||
morning_reminder_time: string | null;
|
||||
reflection_reminder_enabled: boolean;
|
||||
show_energy_points: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type ProfileInsert = {
|
||||
id: string;
|
||||
email: string | null;
|
||||
display_name: string | null;
|
||||
locale: string;
|
||||
timezone: string;
|
||||
onboarding_seen: boolean;
|
||||
onboarding_completed: boolean;
|
||||
};
|
||||
|
||||
type UserSettingsInsert = {
|
||||
profile_id: string;
|
||||
morning_reminder_enabled: boolean;
|
||||
morning_reminder_time: string | null;
|
||||
reflection_reminder_enabled: boolean;
|
||||
show_energy_points: boolean;
|
||||
};
|
||||
|
||||
const PROFILE_COLUMNS =
|
||||
"id, email, display_name, locale, timezone, onboarding_seen, onboarding_completed, created_at, updated_at";
|
||||
const USER_SETTINGS_COLUMNS =
|
||||
"profile_id, morning_reminder_enabled, morning_reminder_time, reflection_reminder_enabled, show_energy_points, created_at, updated_at";
|
||||
|
||||
const DEFAULT_LOCALE = "nl-NL";
|
||||
const DEFAULT_TIMEZONE = "Europe/Amsterdam";
|
||||
const SUPPORTED_LOCALES = new Set([DEFAULT_LOCALE]);
|
||||
|
||||
function mapProfileRow(row: ProfileRow): ProfileRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
displayName: row.display_name,
|
||||
locale: row.locale,
|
||||
timezone: row.timezone,
|
||||
onboardingSeen: row.onboarding_seen,
|
||||
onboardingCompleted: row.onboarding_completed,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapUserSettingsRow(row: UserSettingsRow): UserSettingsRecord {
|
||||
return {
|
||||
profileId: row.profile_id,
|
||||
morningReminderEnabled: row.morning_reminder_enabled,
|
||||
morningReminderTime: row.morning_reminder_time,
|
||||
reflectionReminderEnabled: row.reflection_reminder_enabled,
|
||||
showEnergyPoints: row.show_energy_points,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDefaultProfileFromClaims(user: {
|
||||
id: string;
|
||||
email: string | null;
|
||||
}): ProfileInsert {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
display_name: null,
|
||||
locale: DEFAULT_LOCALE,
|
||||
timezone: DEFAULT_TIMEZONE,
|
||||
onboarding_seen: false,
|
||||
onboarding_completed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDefaultSettings(profileId: string): UserSettingsInsert {
|
||||
return {
|
||||
profile_id: profileId,
|
||||
morning_reminder_enabled: false,
|
||||
morning_reminder_time: null,
|
||||
reflection_reminder_enabled: false,
|
||||
show_energy_points: true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDisplayName(value: string | null) {
|
||||
const trimmedValue = value?.trim() ?? "";
|
||||
return trimmedValue ? trimmedValue : null;
|
||||
}
|
||||
|
||||
function normalizeReminderTime(value: string | null, enabled: boolean) {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmedValue = value?.trim() ?? "";
|
||||
return trimmedValue || "08:30";
|
||||
}
|
||||
|
||||
function normalizeLocale(value: string) {
|
||||
return SUPPORTED_LOCALES.has(value) ? value : DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
function resolveTimezone(value: string) {
|
||||
return isSupportedOnboardingTimezone(value) ? value : DEFAULT_TIMEZONE;
|
||||
}
|
||||
|
||||
async function readProfileRow(
|
||||
supabase: SupabaseServerClient,
|
||||
userId: string,
|
||||
): Promise<ProfileRow | null> {
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select(PROFILE_COLUMNS)
|
||||
.eq("id", userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Profiel kon niet worden geladen: ${error.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function readUserSettingsRow(
|
||||
supabase: SupabaseServerClient,
|
||||
userId: string,
|
||||
): Promise<UserSettingsRow | null> {
|
||||
const { data, error } = await supabase
|
||||
.from("user_settings")
|
||||
.select(USER_SETTINGS_COLUMNS)
|
||||
.eq("profile_id", userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Instellingen konden niet worden geladen: ${error.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function insertMissingProfile(
|
||||
supabase: SupabaseServerClient,
|
||||
user: { id: string; email: string | null },
|
||||
) {
|
||||
const { error } = await supabase.from("profiles").upsert(
|
||||
buildDefaultProfileFromClaims(user),
|
||||
{
|
||||
onConflict: "id",
|
||||
ignoreDuplicates: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Profiel kon niet worden aangemaakt: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncProfileEmailIfNeeded(
|
||||
supabase: SupabaseServerClient,
|
||||
profile: ProfileRow,
|
||||
email: string | null,
|
||||
): Promise<ProfileRow> {
|
||||
if (!email || profile.email === email) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.update({ email })
|
||||
.eq("id", profile.id)
|
||||
.select(PROFILE_COLUMNS)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Profiel-e-mailadres kon niet worden bijgewerkt: ${error.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function insertMissingUserSettings(
|
||||
supabase: SupabaseServerClient,
|
||||
userId: string,
|
||||
) {
|
||||
const { error } = await supabase.from("user_settings").upsert(
|
||||
buildDefaultSettings(userId),
|
||||
{
|
||||
onConflict: "profile_id",
|
||||
ignoreDuplicates: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Instellingen konden niet worden aangemaakt: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function markOnboardingSeenForCurrentUser() {
|
||||
const user = await getAuthenticatedUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update({ onboarding_seen: true, onboarding_completed: false })
|
||||
.eq("id", user.id);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Onboardingstatus kon niet worden bijgewerkt: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function completeOnboardingForCurrentUser(
|
||||
submission: OnboardingSubmission,
|
||||
) {
|
||||
const user = await getAuthenticatedUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
|
||||
}
|
||||
|
||||
const timezone = resolveTimezone(submission.timezone);
|
||||
|
||||
const supabase = await createClient();
|
||||
const displayName = normalizeDisplayName(submission.displayName);
|
||||
const morningReminderTime = normalizeReminderTime(
|
||||
submission.morningReminderTime,
|
||||
submission.morningReminderEnabled,
|
||||
);
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
display_name: displayName,
|
||||
timezone,
|
||||
onboarding_seen: true,
|
||||
onboarding_completed: true,
|
||||
})
|
||||
.eq("id", user.id);
|
||||
|
||||
if (profileError) {
|
||||
throw new Error(`Profiel kon niet worden bijgewerkt: ${profileError.message}`);
|
||||
}
|
||||
|
||||
const { error: settingsError } = await supabase
|
||||
.from("user_settings")
|
||||
.update({
|
||||
morning_reminder_enabled: submission.morningReminderEnabled,
|
||||
morning_reminder_time: morningReminderTime,
|
||||
reflection_reminder_enabled: submission.reflectionReminderEnabled,
|
||||
show_energy_points: submission.showEnergyPoints,
|
||||
})
|
||||
.eq("profile_id", user.id);
|
||||
|
||||
if (settingsError) {
|
||||
throw new Error(`Instellingen konden niet worden bijgewerkt: ${settingsError.message}`);
|
||||
}
|
||||
|
||||
return ensureProfileBundleForCurrentUser();
|
||||
}
|
||||
|
||||
export async function saveSettingsForCurrentUser(
|
||||
submission: SettingsSubmission,
|
||||
) {
|
||||
const user = await getAuthenticatedUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
|
||||
}
|
||||
|
||||
await ensureProfileBundleForCurrentUser();
|
||||
|
||||
const supabase = await createClient();
|
||||
const locale = normalizeLocale(submission.locale);
|
||||
const timezone = resolveTimezone(submission.timezone);
|
||||
const morningReminderTime = normalizeReminderTime(
|
||||
submission.morningReminderTime,
|
||||
submission.morningReminderEnabled,
|
||||
);
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
locale,
|
||||
timezone,
|
||||
})
|
||||
.eq("id", user.id);
|
||||
|
||||
if (profileError) {
|
||||
throw new Error(`Profielinstellingen konden niet worden bijgewerkt: ${profileError.message}`);
|
||||
}
|
||||
|
||||
const { error: settingsError } = await supabase
|
||||
.from("user_settings")
|
||||
.update({
|
||||
morning_reminder_enabled: submission.morningReminderEnabled,
|
||||
morning_reminder_time: morningReminderTime,
|
||||
reflection_reminder_enabled: submission.reflectionReminderEnabled,
|
||||
show_energy_points: submission.showEnergyPoints,
|
||||
})
|
||||
.eq("profile_id", user.id);
|
||||
|
||||
if (settingsError) {
|
||||
throw new Error(`Gebruikersinstellingen konden niet worden bijgewerkt: ${settingsError.message}`);
|
||||
}
|
||||
|
||||
return ensureProfileBundleForCurrentUser();
|
||||
}
|
||||
|
||||
export async function ensureProfileBundleForCurrentUser(): Promise<ProfileBundle | null> {
|
||||
const user = await getAuthenticatedUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
// We bootstrap records app-side so the first protected page load is enough
|
||||
// to give every authenticated user a minimal profile and settings basis.
|
||||
let profileRow = await readProfileRow(supabase, user.id);
|
||||
|
||||
if (!profileRow) {
|
||||
await insertMissingProfile(supabase, user);
|
||||
profileRow = await readProfileRow(supabase, user.id);
|
||||
}
|
||||
|
||||
if (!profileRow) {
|
||||
throw new Error("Profielrecord ontbreekt na bootstrap.");
|
||||
}
|
||||
|
||||
profileRow = await syncProfileEmailIfNeeded(supabase, profileRow, user.email);
|
||||
|
||||
let userSettingsRow = await readUserSettingsRow(supabase, user.id);
|
||||
|
||||
if (!userSettingsRow) {
|
||||
await insertMissingUserSettings(supabase, user.id);
|
||||
userSettingsRow = await readUserSettingsRow(supabase, user.id);
|
||||
}
|
||||
|
||||
if (!userSettingsRow) {
|
||||
throw new Error("Settingsrecord ontbreekt na bootstrap.");
|
||||
}
|
||||
|
||||
return {
|
||||
profile: mapProfileRow(profileRow),
|
||||
settings: mapUserSettingsRow(userSettingsRow),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getProfileBundleForCurrentUser(): Promise<ProfileBundle | null> {
|
||||
return ensureProfileBundleForCurrentUser();
|
||||
}
|
||||
44
lib/profile/types.ts
Normal file
44
lib/profile/types.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
export type ProfileRecord = {
|
||||
id: string;
|
||||
email: string | null;
|
||||
displayName: string | null;
|
||||
locale: string;
|
||||
timezone: string;
|
||||
onboardingSeen: boolean;
|
||||
onboardingCompleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type UserSettingsRecord = {
|
||||
profileId: string;
|
||||
morningReminderEnabled: boolean;
|
||||
morningReminderTime: string | null;
|
||||
reflectionReminderEnabled: boolean;
|
||||
showEnergyPoints: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ProfileBundle = {
|
||||
profile: ProfileRecord;
|
||||
settings: UserSettingsRecord;
|
||||
};
|
||||
|
||||
export type OnboardingSubmission = {
|
||||
displayName: string | null;
|
||||
timezone: string;
|
||||
morningReminderEnabled: boolean;
|
||||
morningReminderTime: string | null;
|
||||
reflectionReminderEnabled: boolean;
|
||||
showEnergyPoints: boolean;
|
||||
};
|
||||
|
||||
export type SettingsSubmission = {
|
||||
locale: string;
|
||||
timezone: string;
|
||||
morningReminderEnabled: boolean;
|
||||
morningReminderTime: string | null;
|
||||
reflectionReminderEnabled: boolean;
|
||||
showEnergyPoints: boolean;
|
||||
};
|
||||
15
lib/supabase/client.ts
Normal file
15
lib/supabase/client.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import { getSupabaseEnv } from "@/lib/supabase/config";
|
||||
|
||||
let browserClient: ReturnType<typeof createBrowserClient> | undefined;
|
||||
|
||||
export function createClient() {
|
||||
if (!browserClient) {
|
||||
const { url, publishableKey } = getSupabaseEnv();
|
||||
browserClient = createBrowserClient(url, publishableKey);
|
||||
}
|
||||
|
||||
return browserClient;
|
||||
}
|
||||
22
lib/supabase/config.ts
Normal file
22
lib/supabase/config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export function hasSupabaseEnv(): boolean {
|
||||
return Boolean(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL &&
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSupabaseEnv() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
|
||||
|
||||
if (!url || !publishableKey) {
|
||||
throw new Error(
|
||||
"Supabase configuratie ontbreekt. Voeg NEXT_PUBLIC_SUPABASE_URL en NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY toe.",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
publishableKey,
|
||||
};
|
||||
}
|
||||
40
lib/supabase/proxy.ts
Normal file
40
lib/supabase/proxy.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { createServerClient } from "@supabase/ssr";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { getSupabaseEnv, hasSupabaseEnv } from "@/lib/supabase/config";
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let response = NextResponse.next({
|
||||
request,
|
||||
});
|
||||
|
||||
if (!hasSupabaseEnv()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { url, publishableKey } = getSupabaseEnv();
|
||||
|
||||
const supabase = createServerClient(url, publishableKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) => {
|
||||
request.cookies.set(name, value);
|
||||
});
|
||||
|
||||
response = NextResponse.next({
|
||||
request,
|
||||
});
|
||||
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
response.cookies.set(name, value, options);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await supabase.auth.getClaims();
|
||||
|
||||
return response;
|
||||
}
|
||||
26
lib/supabase/server.ts
Normal file
26
lib/supabase/server.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
import { getSupabaseEnv } from "@/lib/supabase/config";
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
const { url, publishableKey } = getSupabaseEnv();
|
||||
|
||||
return createServerClient(url, publishableKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options),
|
||||
);
|
||||
} catch {
|
||||
// Server Components cannot always write cookies directly.
|
||||
// The proxy keeps the session in sync for those cases.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue