♻️ Use server side session API (#1684)

This commit is contained in:
Luke Vella 2025-04-24 09:48:02 +01:00 committed by GitHub
parent aba602ef89
commit 5c61057a84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 219 additions and 235 deletions

View file

@ -1,9 +1,7 @@
"use client"; "use client";
import { usePostHog } from "@rallly/posthog/client";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import React from "react"; import React from "react";
import { Logo } from "@/components/logo"; import { Logo } from "@/components/logo";
@ -16,8 +14,6 @@ import { trpc } from "@/trpc/client";
type PageProps = { magicLink: string; email: string }; type PageProps = { magicLink: string; email: string };
export const LoginPage = ({ magicLink, email }: PageProps) => { export const LoginPage = ({ magicLink, email }: PageProps) => {
const session = useSession();
const posthog = usePostHog();
const { t } = useTranslation(); const { t } = useTranslation();
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
@ -28,15 +24,6 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
}, },
onSuccess: async (data) => { onSuccess: async (data) => {
if (!data.url.includes("auth/error")) { if (!data.url.includes("auth/error")) {
// if login was successful, update the session
const updatedSession = await session.update();
if (updatedSession?.user) {
// identify the user in posthog
posthog?.identify(updatedSession.user.id, {
email: updatedSession.user.email,
name: updatedSession.user.name,
});
}
router.push(data.url); router.push(data.url);
} else { } else {
setError( setError(

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { usePostHog } from "@rallly/posthog/client";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { import {
Form, Form,
@ -37,12 +36,11 @@ export function OTPForm({ token }: { token: string }) {
resolver: zodResolver(otpFormSchema), resolver: zodResolver(otpFormSchema),
}); });
const { timeZone } = useDayjs(); const { timeZone, weekStart, timeFormat } = useDayjs();
const locale = i18n.language; const locale = i18n.language;
const queryClient = trpc.useUtils(); const queryClient = trpc.useUtils();
const posthog = usePostHog();
const authenticateRegistration = const authenticateRegistration =
trpc.auth.authenticateRegistration.useMutation(); trpc.auth.authenticateRegistration.useMutation();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -52,6 +50,8 @@ export function OTPForm({ token }: { token: string }) {
token, token,
timeZone, timeZone,
locale, locale,
weekStart,
timeFormat,
code: data.otp, code: data.otp,
}); });
@ -64,11 +64,6 @@ export function OTPForm({ token }: { token: string }) {
queryClient.invalidate(); queryClient.invalidate();
posthog?.identify(res.user.id, {
email: res.user.email,
name: res.user.name,
});
signIn("registration-token", { signIn("registration-token", {
token, token,
redirectTo: searchParams?.get("redirectTo") ?? "/", redirectTo: searchParams?.get("redirectTo") ?? "/",

View file

@ -0,0 +1,17 @@
"use server";
import { prisma } from "@rallly/database";
import { getUserId } from "@/next-auth";
export async function updateLocale(locale: string) {
const userId = await getUserId();
await prisma.user.update({
where: {
id: userId,
},
data: {
locale,
},
});
}

View file

@ -27,8 +27,8 @@ const formSchema = z.object({
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const DateTimePreferencesForm = () => { const DateTimePreferencesForm = () => {
const { timeFormat, weekStart, timeZone, locale } = useDayjs(); const { timeFormat, weekStart, timeZone } = useDayjs();
const { preferences, updatePreferences } = usePreferences(); const { updatePreferences } = usePreferences();
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -126,25 +126,6 @@ const DateTimePreferencesForm = () => {
> >
<Trans i18nKey="save" /> <Trans i18nKey="save" />
</Button> </Button>
{preferences.timeFormat || preferences.weekStart ? (
<Button
onClick={async () => {
updatePreferences({
weekStart: null,
timeFormat: null,
});
form.reset({
weekStart: locale.weekStart,
timeFormat: locale.timeFormat,
});
}}
>
<Trans
defaults="Use locale defaults"
i18nKey="useLocaleDefaults"
/>
</Button>
) : null}
</div> </div>
</form> </form>
</Form> </Form>

View file

@ -8,9 +8,10 @@ import { z } from "zod";
import { LanguageSelect } from "@/components/poll/language-selector"; import { LanguageSelect } from "@/components/poll/language-selector";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { usePreferences } from "@/contexts/preferences";
import { useTranslation } from "@/i18n/client"; import { useTranslation } from "@/i18n/client";
import { updateLocale } from "../actions";
const formSchema = z.object({ const formSchema = z.object({
language: z.string(), language: z.string(),
}); });
@ -25,13 +26,12 @@ export const LanguagePreference = () => {
}, },
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
}); });
const { updatePreferences } = usePreferences();
return ( return (
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(async (data) => { onSubmit={form.handleSubmit(async (data) => {
await updatePreferences({ locale: data.language }); await updateLocale(data.language);
i18n.changeLanguage(data.language); i18n.changeLanguage(data.language);
form.reset({ language: data.language }); form.reset({ language: data.language });
})} })}

View file

@ -2,12 +2,12 @@
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { DateTimePreferences } from "../components/date-time-preferences";
import { LanguagePreference } from "../components/language-preference";
import { import {
SettingsContent, SettingsContent,
SettingsSection, SettingsSection,
} from "../components/settings-layout"; } from "../components/settings-layout";
import { DateTimePreferences } from "./components/date-time-preferences";
import { LanguagePreference } from "./components/language-preference";
export function PreferencesPage() { export function PreferencesPage() {
return ( return (

View file

@ -8,11 +8,11 @@ import { TooltipProvider } from "@rallly/ui/tooltip";
import { domAnimation, LazyMotion } from "motion/react"; import { domAnimation, LazyMotion } from "motion/react";
import type { Viewport } from "next"; import type { Viewport } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import { SessionProvider } from "next-auth/react";
import React from "react"; import React from "react";
import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector"; import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
import { UserProvider } from "@/components/user-provider"; import { UserProvider } from "@/components/user-provider";
import { PreferencesProvider } from "@/contexts/preferences";
import { getUser } from "@/data/get-user"; import { getUser } from "@/data/get-user";
import { TimezoneProvider } from "@/features/timezone/client/context"; import { TimezoneProvider } from "@/features/timezone/client/context";
import { I18nProvider } from "@/i18n/client"; import { I18nProvider } from "@/i18n/client";
@ -33,32 +33,26 @@ export const viewport: Viewport = {
initialScale: 1, initialScale: 1,
}; };
async function loadLocale() {
let locale = getLocale();
const userId = await getUserId();
if (userId) {
const user = await getUser();
if (user.locale) {
locale = user.locale;
}
}
if (!supportedLngs.includes(locale)) {
return defaultLocale;
}
return locale;
}
export default async function Root({ export default async function Root({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const session = await auth(); const session = await auth();
const locale = await loadLocale();
let locale = getLocale();
const userId = await getUserId();
const user = userId ? await getUser() : null;
if (user?.locale) {
locale = user.locale;
}
if (!supportedLngs.includes(locale)) {
locale = defaultLocale;
}
return ( return (
<html lang={locale} className={inter.className}> <html lang={locale} className={inter.className}>
@ -67,23 +61,48 @@ export default async function Root({
<I18nProvider locale={locale}> <I18nProvider locale={locale}>
<TRPCProvider> <TRPCProvider>
<LazyMotion features={domAnimation}> <LazyMotion features={domAnimation}>
<SessionProvider session={session}> <PostHogProvider>
<PostHogProvider> <PostHogPageView />
<PostHogPageView /> <TooltipProvider>
<TooltipProvider> <UserProvider
<UserProvider> user={
<TimezoneProvider user
initialTimezone={session?.user?.timeZone ?? undefined} ? {
> id: user.id,
name: user.name,
email: user.email,
tier: user
? user.isPro
? "pro"
: "hobby"
: "guest",
image: user.image,
}
: session?.user
? {
id: session.user.id,
tier: "guest",
}
: undefined
}
>
<PreferencesProvider
initialValue={{
timeFormat: user?.timeFormat,
timeZone: user?.timeZone,
weekStart: user?.weekStart,
}}
>
<TimezoneProvider initialTimezone={user?.timeZone}>
<ConnectedDayjsProvider> <ConnectedDayjsProvider>
{children} {children}
<TimeZoneChangeDetector /> <TimeZoneChangeDetector />
</ConnectedDayjsProvider> </ConnectedDayjsProvider>
</TimezoneProvider> </TimezoneProvider>
</UserProvider> </PreferencesProvider>
</TooltipProvider> </UserProvider>
</PostHogProvider> </TooltipProvider>
</SessionProvider> </PostHogProvider>
</LazyMotion> </LazyMotion>
</TRPCProvider> </TRPCProvider>
</I18nProvider> </I18nProvider>

View file

@ -1,22 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { LanguageSelect } from "@/components/poll/language-selector";
import { usePreferences } from "@/contexts/preferences";
import { useTranslation } from "@/i18n/client";
export function UserLanguageSwitcher() {
const { i18n } = useTranslation();
const { preferences, updatePreferences } = usePreferences();
const router = useRouter();
return (
<LanguageSelect
value={preferences.locale ?? i18n.language}
onChange={async (language) => {
await updatePreferences({ locale: language });
router.refresh();
}}
/>
);
}

View file

@ -11,7 +11,6 @@ import {
import { Form } from "@rallly/ui/form"; import { Form } from "@rallly/ui/form";
import { useToast } from "@rallly/ui/hooks/use-toast"; import { useToast } from "@rallly/ui/hooks/use-toast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { signIn, useSession } from "next-auth/react";
import React from "react"; import React from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist"; import useFormPersist from "react-hook-form-persist";
@ -42,9 +41,8 @@ export interface CreatePollPageProps {
export const CreatePoll: React.FunctionComponent = () => { export const CreatePoll: React.FunctionComponent = () => {
const router = useRouter(); const router = useRouter();
const { user } = useUser(); const { user, createGuestIfNeeded } = useUser();
const { toast } = useToast(); const { toast } = useToast();
const session = useSession();
const form = useForm<NewEventData>({ const form = useForm<NewEventData>({
defaultValues: { defaultValues: {
title: "", title: "",
@ -84,11 +82,7 @@ export const CreatePoll: React.FunctionComponent = () => {
<form <form
onSubmit={form.handleSubmit(async (formData) => { onSubmit={form.handleSubmit(async (formData) => {
const title = required(formData?.title); const title = required(formData?.title);
if (session.status !== "authenticated") { await createGuestIfNeeded();
await signIn("guest", {
redirect: false,
});
}
await createPoll.mutateAsync( await createPoll.mutateAsync(
{ {
title: title, title: title,

View file

@ -26,7 +26,6 @@ import {
MoreHorizontalIcon, MoreHorizontalIcon,
TrashIcon, TrashIcon,
} from "lucide-react"; } from "lucide-react";
import { signIn, useSession } from "next-auth/react";
import * as React from "react"; import * as React from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -57,8 +56,7 @@ function NewCommentForm({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const poll = usePoll(); const poll = usePoll();
const { user } = useUser(); const { user, createGuestIfNeeded } = useUser();
const session = useSession();
const { participants } = useParticipants(); const { participants } = useParticipants();
const authorName = React.useMemo(() => { const authorName = React.useMemo(() => {
@ -98,11 +96,7 @@ function NewCommentForm({
<form <form
className="w-full space-y-2.5" className="w-full space-y-2.5"
onSubmit={handleSubmit(async ({ authorName, content }) => { onSubmit={handleSubmit(async ({ authorName, content }) => {
if (session.status !== "authenticated") { await createGuestIfNeeded();
await signIn("guest", {
redirect: false,
});
}
await addComment.mutateAsync({ authorName, content, pollId }); await addComment.mutateAsync({ authorName, content, pollId });
reset({ authorName, content: "" }); reset({ authorName, content: "" });
onSubmit?.(); onSubmit?.();

View file

@ -7,7 +7,6 @@ import { FormMessage } from "@rallly/ui/form";
import { Input } from "@rallly/ui/input"; import { Input } from "@rallly/ui/input";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { TRPCClientError } from "@trpc/client"; import { TRPCClientError } from "@trpc/client";
import { signIn, useSession } from "next-auth/react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import z from "zod"; import z from "zod";
@ -91,8 +90,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {
const isEmailRequired = poll.requireParticipantEmail; const isEmailRequired = poll.requireParticipantEmail;
const { user } = useUser(); const { user, createGuestIfNeeded } = useUser();
const session = useSession();
const isLoggedIn = !user.isGuest; const isLoggedIn = !user.isGuest;
const { register, setError, formState, handleSubmit } = const { register, setError, formState, handleSubmit } =
useForm<NewParticipantFormData>({ useForm<NewParticipantFormData>({
@ -113,11 +111,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {
<form <form
onSubmit={handleSubmit(async (data) => { onSubmit={handleSubmit(async (data) => {
try { try {
if (session.status !== "authenticated") { await createGuestIfNeeded();
await signIn("guest", {
redirect: false,
});
}
const newParticipant = await addParticipant.mutateAsync({ const newParticipant = await addParticipant.mutateAsync({
name: data.name, name: data.name,
votes: props.votes, votes: props.votes,

View file

@ -1,14 +1,11 @@
"use client"; "use client";
import { usePostHog } from "@rallly/posthog/client"; import { usePostHog } from "@rallly/posthog/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { Session } from "next-auth"; import { signIn, signOut } from "next-auth/react";
import { signOut, useSession } from "next-auth/react";
import React from "react"; import React from "react";
import { useSubscription } from "@/contexts/plan"; import { useSubscription } from "@/contexts/plan";
import { PreferencesProvider } from "@/contexts/preferences";
import { useTranslation } from "@/i18n/client"; import { useTranslation } from "@/i18n/client";
import { trpc } from "@/trpc/client";
import { isOwner } from "@/utils/permissions"; import { isOwner } from "@/utils/permissions";
import { useRequiredContext } from "./use-required-context"; import { useRequiredContext } from "./use-required-context";
@ -16,23 +13,20 @@ import { useRequiredContext } from "./use-required-context";
type UserData = { type UserData = {
id?: string; id?: string;
name: string; name: string;
email?: string | null; email?: string;
isGuest: boolean; isGuest: boolean;
tier: "guest" | "hobby" | "pro"; tier: "guest" | "hobby" | "pro";
timeZone?: string | null; image?: string;
timeFormat?: "hours12" | "hours24" | null;
weekStart?: number | null;
image?: string | null;
locale?: string | null;
}; };
export const UserContext = React.createContext<{ export const UserContext = React.createContext<{
user: UserData; user: UserData;
refresh: (data?: Record<string, unknown>) => Promise<Session | null>; refresh: () => void;
ownsObject: (obj: { ownsObject: (obj: {
userId?: string | null; userId?: string | null;
guestId?: string | null; guestId?: string | null;
}) => boolean; }) => boolean;
createGuestIfNeeded: () => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} | null>(null); } | null>(null);
@ -58,16 +52,37 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
return <>{props.children}</>; return <>{props.children}</>;
}; };
export const UserProvider = (props: { children?: React.ReactNode }) => { type BaseUser = {
const session = useSession(); id: string;
const user = session.data?.user; tier: "guest" | "hobby" | "pro";
image?: string;
name?: string;
email?: string;
};
type RegisteredUser = BaseUser & {
email: string;
name: string;
tier: "hobby" | "pro";
};
type GuestUser = BaseUser & {
tier: "guest";
};
export const UserProvider = ({
children,
user,
}: {
children?: React.ReactNode;
user?: RegisteredUser | GuestUser;
}) => {
const subscription = useSubscription(); const subscription = useSubscription();
const updatePreferences = trpc.user.updatePreferences.useMutation(); const { t } = useTranslation();
const { t, i18n } = useTranslation();
const router = useRouter(); const router = useRouter();
const posthog = usePostHog(); const posthog = usePostHog();
const isGuest = !user?.email; const isGuest = !user || user.tier === "guest";
const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby"; const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby";
React.useEffect(() => { React.useEffect(() => {
@ -76,9 +91,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
email: user.email, email: user.email,
name: user.name, name: user.name,
tier, tier,
timeZone: user.timeZone ?? null, image: user.image,
image: user.image ?? null,
locale: user.locale ?? i18n.language,
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -90,17 +103,20 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
user: { user: {
id: user?.id, id: user?.id,
name: user?.name ?? t("guest"), name: user?.name ?? t("guest"),
email: user?.email || null, email: user?.email,
isGuest, isGuest,
tier, tier,
timeZone: user?.timeZone ?? null, image: user?.image,
image: user?.image ?? null,
locale: user?.locale ?? i18n.language,
}, },
refresh: async (data) => { createGuestIfNeeded: async () => {
router.refresh(); if (!user) {
return await session.update(data); await signIn("guest", {
redirect: false,
});
router.refresh();
}
}, },
refresh: router.refresh,
logout: async () => { logout: async () => {
await signOut(); await signOut();
posthog?.capture("logout"); posthog?.capture("logout");
@ -111,27 +127,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
}, },
}} }}
> >
<PreferencesProvider {children}
initialValue={{
locale: user?.locale ?? undefined,
timeZone: user?.timeZone ?? undefined,
timeFormat: user?.timeFormat ?? undefined,
weekStart: user?.weekStart ?? undefined,
}}
onUpdate={async (newPreferences) => {
if (!isGuest) {
await updatePreferences.mutateAsync({
locale: newPreferences.locale ?? undefined,
timeZone: newPreferences.timeZone ?? undefined,
timeFormat: newPreferences.timeFormat ?? undefined,
weekStart: newPreferences.weekStart ?? undefined,
});
}
await session.update(newPreferences);
}}
>
{props.children}
</PreferencesProvider>
</UserContext.Provider> </UserContext.Provider>
); );
}; };

View file

@ -1,16 +1,22 @@
"use client";
import type { TimeFormat } from "@rallly/database"; import type { TimeFormat } from "@rallly/database";
import React from "react"; import React from "react";
import { useSetState } from "react-use"; import { useLocalStorage } from "react-use";
import { z } from "zod";
import { useRequiredContext } from "@/components/use-required-context"; import { useRequiredContext } from "@/components/use-required-context";
import { useUser } from "@/components/user-provider";
import { trpc } from "@/trpc/client";
type Preferences = { type Preferences = {
timeZone?: string | null; timeZone?: string;
locale?: string | null; timeFormat?: TimeFormat;
timeFormat?: TimeFormat | null; weekStart?: number;
weekStart?: number | null;
}; };
const timeFormatSchema = z.enum(["hours12", "hours24"]);
type PreferencesContextValue = { type PreferencesContextValue = {
preferences: Preferences; preferences: Preferences;
updatePreferences: (preferences: Partial<Preferences>) => Promise<void>; updatePreferences: (preferences: Partial<Preferences>) => Promise<void>;
@ -23,21 +29,58 @@ const PreferencesContext = React.createContext<PreferencesContextValue | null>(
export const PreferencesProvider = ({ export const PreferencesProvider = ({
children, children,
initialValue, initialValue,
onUpdate,
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
initialValue: Partial<Preferences>; initialValue: Partial<Preferences>;
onUpdate?: (preferences: Partial<Preferences>) => Promise<void>;
}) => { }) => {
const [preferences, setPreferences] = useSetState<Preferences>(initialValue); const { user } = useUser();
const [preferredTimezone, setPreferredTimezone] = useLocalStorage(
"rallly.preferredTimezone",
initialValue.timeZone,
);
const [preferredTimeFormat, setPreferredTimeFormat] = useLocalStorage(
"rallly.preferredTimeFormat",
initialValue.timeFormat,
{
raw: false,
serializer: timeFormatSchema.parse,
deserializer: timeFormatSchema.optional().catch(undefined).parse,
},
);
const [preferredWeekStart, setPreferredWeekStart] = useLocalStorage(
"rallly.preferredWeekStart",
initialValue.weekStart,
);
const updatePreferences = trpc.user.updatePreferences.useMutation();
return ( return (
<PreferencesContext.Provider <PreferencesContext.Provider
value={{ value={{
preferences, preferences: {
timeZone: preferredTimezone,
timeFormat: preferredTimeFormat,
weekStart: preferredWeekStart,
},
updatePreferences: async (newPreferences) => { updatePreferences: async (newPreferences) => {
setPreferences(newPreferences); if (newPreferences.timeZone) {
await onUpdate?.(newPreferences); setPreferredTimezone(newPreferences.timeZone);
}
if (newPreferences.timeFormat) {
setPreferredTimeFormat(newPreferences.timeFormat);
}
if (newPreferences.weekStart) {
setPreferredWeekStart(newPreferences.weekStart);
}
if (!user.isGuest) {
await updatePreferences.mutateAsync({
timeZone: newPreferences.timeZone ?? undefined,
timeFormat: newPreferences.timeFormat ?? undefined,
weekStart: newPreferences.weekStart ?? undefined,
});
}
}, },
}} }}
> >

View file

@ -18,6 +18,8 @@ export const getUser = cache(async () => {
image: true, image: true,
locale: true, locale: true,
timeZone: true, timeZone: true,
timeFormat: true,
weekStart: true,
subscription: { subscription: {
select: { select: {
active: true, active: true,
@ -37,6 +39,8 @@ export const getUser = cache(async () => {
image: user.image ?? undefined, image: user.image ?? undefined,
locale: user.locale ?? undefined, locale: user.locale ?? undefined,
timeZone: user.timeZone ?? undefined, timeZone: user.timeZone ?? undefined,
timeFormat: user.timeFormat ?? undefined,
weekStart: user.weekStart ?? undefined,
isPro: user.subscription?.active ?? false, isPro: user.subscription?.active ?? false,
}; };
}); });

View file

@ -4,7 +4,6 @@ import { redirect } from "next/navigation";
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import type { Provider } from "next-auth/providers"; import type { Provider } from "next-auth/providers";
import { cache } from "react"; import { cache } from "react";
import z from "zod";
import { CustomPrismaAdapter } from "./auth/adapters/prisma"; import { CustomPrismaAdapter } from "./auth/adapters/prisma";
import { isEmailBanned, isEmailBlocked } from "./auth/helpers/is-email-blocked"; import { isEmailBanned, isEmailBlocked } from "./auth/helpers/is-email-blocked";
@ -17,13 +16,6 @@ import { OIDCProvider } from "./auth/providers/oidc";
import { RegistrationTokenProvider } from "./auth/providers/registration-token"; import { RegistrationTokenProvider } from "./auth/providers/registration-token";
import { nextAuthConfig } from "./next-auth.config"; import { nextAuthConfig } from "./next-auth.config";
const sessionUpdateSchema = z.object({
locale: z.string().nullish(),
timeFormat: z.enum(["hours12", "hours24"]).nullish(),
timeZone: z.string().nullish(),
weekStart: z.number().nullish(),
});
const { const {
auth: originalAuth, auth: originalAuth,
handlers, handlers,
@ -162,43 +154,26 @@ const {
return true; return true;
}, },
async jwt({ token, session }) { async jwt({ token }) {
if (session) { const userId = token.sub;
const parsed = sessionUpdateSchema.safeParse(session); const isGuest = userId?.startsWith("guest-");
if (parsed.success) {
Object.entries(parsed.data).forEach(([key, value]) => {
token[key] = value;
});
} else {
console.error(parsed.error);
}
} else {
const userId = token.sub;
const isGuest = userId?.startsWith("guest-");
if (userId && !isGuest) { if (userId && !isGuest) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: userId, id: userId,
}, },
select: { select: {
name: true, name: true,
email: true, email: true,
timeFormat: true, image: true,
timeZone: true, },
weekStart: true, });
image: true,
},
});
if (user) { if (user) {
token.name = user.name; token.name = user.name;
token.email = user.email; token.email = user.email;
token.picture = user.image; token.picture = user.image;
token.timeFormat = user.timeFormat;
token.timeZone = user.timeZone;
token.weekStart = user.weekStart;
}
} }
} }

View file

@ -97,6 +97,8 @@ export const auth = router({
token: z.string(), token: z.string(),
code: z.string(), code: z.string(),
timeZone: z.string().optional(), timeZone: z.string().optional(),
weekStart: z.number().min(0).max(6).optional(),
timeFormat: z.enum(["hours12", "hours24"]).optional(),
locale: z.string().optional(), locale: z.string().optional(),
}), }),
) )
@ -118,6 +120,8 @@ export const auth = router({
name, name,
email, email,
timeZone: input.timeZone, timeZone: input.timeZone,
timeFormat: input.timeFormat,
weekStart: input.weekStart,
locale: input.locale, locale: input.locale,
}, },
}); });
@ -139,6 +143,9 @@ export const auth = router({
name: user.name, name: user.name,
timeZone: input.timeZone, timeZone: input.timeZone,
locale: input.locale, locale: input.locale,
tier: "hobby",
weekStart: input.weekStart,
timeFormat: input.timeFormat,
}, },
}, },
}); });

View file

@ -14,11 +14,11 @@ import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import updateLocale from "dayjs/plugin/updateLocale"; import updateLocale from "dayjs/plugin/updateLocale";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { useParams } from "next/navigation";
import * as React from "react"; import * as React from "react";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { usePreferences } from "@/contexts/preferences"; import { usePreferences } from "@/contexts/preferences";
import { useTranslation } from "@/i18n/client";
import { getBrowserTimeZone, normalizeTimeZone } from "@/utils/date-time-utils"; import { getBrowserTimeZone, normalizeTimeZone } from "@/utils/date-time-utils";
import { useRequiredContext } from "../components/use-required-context"; import { useRequiredContext } from "../components/use-required-context";
@ -203,11 +203,10 @@ export const DayjsProvider: React.FunctionComponent<{
}; };
}; };
}> = ({ config, children }) => { }> = ({ config, children }) => {
const locale = useParams()?.locale as string; const locale = config?.locale ?? "en";
const l = config?.locale ?? locale ?? "en";
const state = useAsync(async () => { const state = useAsync(async () => {
return await dayjsLocales[l].import(); return await dayjsLocales[locale].import();
}, [l]); }, [locale]);
const preferredTimeZone = React.useMemo( const preferredTimeZone = React.useMemo(
() => () =>
@ -234,7 +233,7 @@ export const DayjsProvider: React.FunctionComponent<{
} }
const dayjsLocale = state.value; const dayjsLocale = state.value;
const localeConfig = dayjsLocales[l]; const localeConfig = dayjsLocales[locale];
const localeTimeFormat = localeConfig.timeFormat; const localeTimeFormat = localeConfig.timeFormat;
if (config?.localeOverrides) { if (config?.localeOverrides) {
@ -278,10 +277,11 @@ export const ConnectedDayjsProvider = ({
children, children,
}: React.PropsWithChildren) => { }: React.PropsWithChildren) => {
const { preferences } = usePreferences(); const { preferences } = usePreferences();
const { i18n } = useTranslation();
return ( return (
<DayjsProvider <DayjsProvider
config={{ config={{
locale: preferences.locale ?? undefined, locale: i18n.language,
timeZone: preferences.timeZone ?? undefined, timeZone: preferences.timeZone ?? undefined,
localeOverrides: { localeOverrides: {
weekStart: preferences.weekStart ?? undefined, weekStart: preferences.weekStart ?? undefined,