diff --git a/apps/web/src/components/auth/auth-layout.tsx b/apps/web/src/components/auth/auth-layout.tsx
index 77a146022..19043c473 100644
--- a/apps/web/src/components/auth/auth-layout.tsx
+++ b/apps/web/src/components/auth/auth-layout.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import Logo from "~//logo.svg";
+import Logo from "~/logo.svg";
export const AuthLayout = ({ children }: { children?: React.ReactNode }) => {
return (
diff --git a/apps/web/src/components/user-provider.tsx b/apps/web/src/components/user-provider.tsx
index 2780e843d..b72ff7746 100644
--- a/apps/web/src/components/user-provider.tsx
+++ b/apps/web/src/components/user-provider.tsx
@@ -1,23 +1,11 @@
import { trpc, UserSession } from "@rallly/backend";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
-import posthog from "posthog-js";
-import { PostHogProvider } from "posthog-js/react";
import React from "react";
-import { useRequiredContext } from "./use-required-context";
+import { PostHogProvider } from "@/contexts/posthog";
-if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
- posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
- api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
- opt_out_capturing_by_default: false,
- capture_pageview: false,
- persistence: "memory",
- capture_pageleave: false,
- autocapture: false,
- opt_in_site_apps: true,
- });
-}
+import { useRequiredContext } from "./use-required-context";
export const UserContext = React.createContext<{
user: UserSession & { shortName: string };
@@ -73,19 +61,6 @@ export const UserProvider = (props: {
},
});
- React.useEffect(() => {
- if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY || !user) {
- return;
- }
-
- posthog.identify(
- user.id,
- !user.isGuest
- ? { email: user.email, name: user.name }
- : { name: user.id },
- );
- }, [user]);
-
const shortName = user
? user.isGuest === false
? user.name
@@ -97,30 +72,28 @@ export const UserProvider = (props: {
}
return (
-
- {
- return queryClient.whoami.invalidate();
- },
- ownsObject: ({ userId }) => {
- if (
- (userId && user.id === userId) ||
- (props.forceUserId && props.forceUserId === userId)
- ) {
- return true;
- }
- return false;
- },
- logout: () => {
- logout.mutate();
- },
- }}
- >
- {props.children}
-
-
+ {
+ return queryClient.whoami.invalidate();
+ },
+ ownsObject: ({ userId }) => {
+ if (
+ (userId && user.id === userId) ||
+ (props.forceUserId && props.forceUserId === userId)
+ ) {
+ return true;
+ }
+ return false;
+ },
+ logout: () => {
+ logout.mutate();
+ },
+ }}
+ >
+ {props.children}
+
);
};
diff --git a/apps/web/src/contexts/posthog.tsx b/apps/web/src/contexts/posthog.tsx
new file mode 100644
index 000000000..ad92d6a75
--- /dev/null
+++ b/apps/web/src/contexts/posthog.tsx
@@ -0,0 +1,42 @@
+import posthog from "posthog-js";
+import { PostHogProvider as Provider } from "posthog-js/react";
+import { useMount } from "react-use";
+
+import { useUser } from "@/components/user-provider";
+
+type PostHogProviderProps = React.PropsWithChildren;
+
+const PostHogProviderInner = (props: PostHogProviderProps) => {
+ const { user } = useUser();
+
+ useMount(() => {
+ // initalize posthog with our user id
+ if (
+ typeof window !== "undefined" &&
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ ) {
+ posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
+ api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
+ opt_out_capturing_by_default: false,
+ capture_pageview: true,
+ persistence: "memory",
+ capture_pageleave: false,
+ autocapture: false,
+ opt_in_site_apps: true,
+ bootstrap: {
+ distinctID: user.id,
+ },
+ });
+ }
+ });
+
+ return {props.children};
+};
+
+export const PostHogProvider = (props: PostHogProviderProps) => {
+ if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ return <>{props.children}>;
+ }
+
+ return ;
+};
diff --git a/apps/web/src/pages/auth/invalid-token.tsx b/apps/web/src/pages/auth/invalid-token.tsx
deleted file mode 100644
index d5d49f6aa..000000000
--- a/apps/web/src/pages/auth/invalid-token.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useTranslation } from "next-i18next";
-import { NextSeo } from "next-seo";
-
-import { AuthLayout } from "@/components/layouts/auth-layout";
-import { withPageTranslations } from "@/utils/with-page-translations";
-
-const Page = () => {
- const { t } = useTranslation();
- return (
-
-
- {t("expiredOrInvalidLink")}
-
- );
-};
-
-export const getStaticProps = withPageTranslations();
-
-export default Page;
diff --git a/apps/web/src/pages/auth/login.tsx b/apps/web/src/pages/auth/login.tsx
index 5fe8b62fa..963787172 100644
--- a/apps/web/src/pages/auth/login.tsx
+++ b/apps/web/src/pages/auth/login.tsx
@@ -1,138 +1,81 @@
-import { LoginTokenPayload } from "@rallly/backend";
-import {
- composeGetServerSideProps,
- withSessionSsr,
-} from "@rallly/backend/next";
-import { decryptToken } from "@rallly/backend/session";
-import { prisma } from "@rallly/database";
+import { trpc } from "@rallly/backend";
+import { withSessionSsr } from "@rallly/backend/next";
import { CheckCircleIcon } from "@rallly/icons";
import clsx from "clsx";
import { GetServerSideProps } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { Trans, useTranslation } from "next-i18next";
-import React from "react";
+import { useMount } from "react-use";
import { AuthLayout } from "@/components/layouts/auth-layout";
import { Spinner } from "@/components/spinner";
+import { withSession } from "@/components/user-provider";
+import { usePostHog } from "@/utils/posthog";
import { withPageTranslations } from "@/utils/with-page-translations";
const defaultRedirectPath = "/profile";
-const redirectToInvalidToken = {
- redirect: {
- destination: "/auth/invalid-token",
- permanent: false,
- },
-};
-
-const Redirect = () => {
+export const Page = () => {
const { t } = useTranslation();
- const [enabled, setEnabled] = React.useState(false);
+
const router = useRouter();
+ const { token } = router.query;
+ const posthog = usePostHog();
+ const authenticate = trpc.whoami.authenticate.useMutation();
- React.useEffect(() => {
- setTimeout(() => {
- setEnabled(true);
- }, 500);
- setTimeout(() => {
- router.replace(defaultRedirectPath);
- }, 3000);
- }, [router]);
+ useMount(() => {
+ authenticate.mutate(
+ { token: token as string },
+ {
+ onSuccess: (user) => {
+ posthog?.identify(user.id, {
+ name: user.name,
+ email: user.email,
+ });
- return (
-
-
- {enabled ? (
-
- ) : (
-
- )}
-
-
{t("loginSuccessful")}
-
- ,
- }}
- />
-
-
- );
-};
-export const Page = (
- props:
- | {
- success: true;
- name: string;
- }
- | {
- success: false;
- errorCode: "userNotFound";
+ setTimeout(() => {
+ router.replace(defaultRedirectPath);
+ }, 1000);
+ },
},
-) => {
- const { t } = useTranslation();
+ );
+ });
+
return (
- {props.success ? (
-
+ {authenticate.isLoading ? (
+
+
+
+
+ ) : authenticate.isSuccess ? (
+
+
+
+
+
{t("loginSuccessful")}
+
+ ,
+ }}
+ />
+
+
) : (
-
+
+
+
)}
);
};
-export default Page;
+export default withSession(Page);
-export const getServerSideProps: GetServerSideProps = composeGetServerSideProps(
+export const getServerSideProps: GetServerSideProps = withSessionSsr(
withPageTranslations(),
- withSessionSsr(async (ctx) => {
- const token = ctx.query.token as string;
-
- if (!token) {
- // token is missing
- return redirectToInvalidToken;
- }
-
- const payload = await decryptToken(token);
-
- if (!payload) {
- // token is invalid or expired
- return redirectToInvalidToken;
- }
-
- const user = await prisma.user.findFirst({
- select: {
- id: true,
- },
- where: { id: payload.userId },
- });
-
- if (!user) {
- // user does not exist
- return {
- props: {
- success: false,
- errorCode: "userNotFound",
- },
- };
- }
-
- ctx.req.session.user = { id: user.id, isGuest: false };
-
- await ctx.req.session.save();
-
- return {
- props: {
- success: true,
- },
- };
- }),
);
diff --git a/packages/backend/trpc/routers/whoami.ts b/packages/backend/trpc/routers/whoami.ts
index a733f65a1..87bc98882 100644
--- a/packages/backend/trpc/routers/whoami.ts
+++ b/packages/backend/trpc/routers/whoami.ts
@@ -1,7 +1,10 @@
import { prisma } from "@rallly/database";
+import { TRPCError } from "@trpc/server";
+import z from "zod";
+import { decryptToken } from "../../session";
import { publicProcedure, router } from "../trpc";
-import { UserSession } from "../types";
+import { LoginTokenPayload, UserSession } from "../types";
export const whoami = router({
get: publicProcedure.query(async ({ ctx }): Promise => {
@@ -24,4 +27,34 @@ export const whoami = router({
destroy: publicProcedure.mutation(async ({ ctx }) => {
ctx.session.destroy();
}),
+ authenticate: publicProcedure
+ .input(z.object({ token: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ const payload = await decryptToken(input.token);
+
+ if (!payload) {
+ // token is invalid or expired
+ throw new TRPCError({ code: "PARSE_ERROR", message: "Invalid token" });
+ }
+
+ const user = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ },
+ where: { id: payload.userId },
+ });
+
+ if (!user) {
+ // user does not exist
+ throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
+ }
+
+ ctx.session.user = { id: user.id, isGuest: false };
+
+ await ctx.session.save();
+
+ return user;
+ }),
});