diff --git a/apps/web/.env.test b/apps/web/.env.test index 10a839121..211b6be27 100644 --- a/apps/web/.env.test +++ b/apps/web/.env.test @@ -4,6 +4,6 @@ NEXTAUTH_URL=$NEXT_PUBLIC_BASE_URL SECRET_PASSWORD=abcdef1234567890abcdef1234567890 DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly SUPPORT_EMAIL=support@rallly.co -SMTP_HOST=localhost +SMTP_HOST=0.0.0.0 SMTP_PORT=1025 QUICK_CREATE_ENABLED=true \ No newline at end of file diff --git a/apps/web/declarations/next-auth.d.ts b/apps/web/declarations/next-auth.d.ts index cfcdd3bfa..9f5f276a1 100644 --- a/apps/web/declarations/next-auth.d.ts +++ b/apps/web/declarations/next-auth.d.ts @@ -20,6 +20,7 @@ declare module "next-auth" { } interface User extends DefaultUser { + id: string; locale?: string | null; timeZone?: string | null; timeFormat?: TimeFormat | null; diff --git a/apps/web/i18next-scanner.config.js b/apps/web/i18next-scanner.config.js index 04cb299b9..51accfa19 100644 --- a/apps/web/i18next-scanner.config.js +++ b/apps/web/i18next-scanner.config.js @@ -1,7 +1,7 @@ const typescriptTransform = require("i18next-scanner-typescript"); module.exports = { - input: ["src/**/*.{ts,tsx}", "!src/auth.ts"], + input: ["src/**/*.{ts,tsx}", "!src/next-auth*.ts"], options: { nsSeparator: false, defaultNs: "app", diff --git a/apps/web/package.json b/apps/web/package.json index 8632f442e..67a292e26 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ "docker:start": "./scripts/docker-start.sh" }, "dependencies": { - "@auth/prisma-adapter": "^1.0.3", + "@auth/prisma-adapter": "^2.7.4", "@aws-sdk/client-s3": "^3.645.0", "@aws-sdk/s3-request-presigner": "^3.645.0", "@hookform/resolvers": "^3.3.1", @@ -67,7 +67,7 @@ "lucide-react": "^0.387.0", "micro": "^10.0.1", "nanoid": "^5.0.9", - "next-auth": "^4.24.5", + "next-auth": "^5.0.0-beta.25", "next-i18next": "^13.0.3", "php-serialize": "^4.1.1", "postcss": "^8.4.31", @@ -95,7 +95,7 @@ "cross-env": "^7.0.3", "i18next-scanner": "^4.2.0", "i18next-scanner-typescript": "^1.1.1", - "vitest": "^2.1.1", + "vitest": "^2.1.9", "wait-on": "^6.0.1" } } diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 39c7aa7e1..262a80ef2 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -32,7 +32,6 @@ "emailNotAllowed": "This email is not allowed.", "emailPlaceholder": "jessie.smith@example.com", "exportToCsv": "Export to CSV", - "forgetMe": "Forget me", "guest": "Guest", "ifNeedBe": "If need be", "location": "Location", @@ -199,9 +198,6 @@ "pollStatusFinalized": "Finalized", "share": "Share", "noParticipants": "No participants", - "userId": "User ID", - "aboutGuest": "Guest User", - "aboutGuestDescription": "Profile settings are not available for guest users. <0>Sign in to your existing account or <1>create a new account to customize your profile.", "logoutDescription": "Sign out of your existing session", "events": "Events", "inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.", @@ -305,5 +301,6 @@ "registerVerifyDescription": "Check your email for the verification code", "loginVerifyTitle": "Finish Logging In", "loginVerifyDescription": "Check your email for the verification code", - "createAccount": "Create Account" + "createAccount": "Create Account", + "loginMagicLinkError": "This link is invalid or expired. Please request a new link." } diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx index b4503d097..a29186241 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx @@ -38,7 +38,7 @@ export function DeleteAccountDialog({ onSuccess() { posthog?.capture("delete account"); signOut({ - callbackUrl: "/login", + redirectTo: "/login", }); }, }); diff --git a/apps/web/src/app/[locale]/(auth)/auth/login/components/login-page.tsx b/apps/web/src/app/[locale]/(auth)/auth/login/components/login-page.tsx index 04ed2e884..ecf276892 100644 --- a/apps/web/src/app/[locale]/(auth)/auth/login/components/login-page.tsx +++ b/apps/web/src/app/[locale]/(auth)/auth/login/components/login-page.tsx @@ -4,10 +4,12 @@ import { Button } from "@rallly/ui/button"; import { useMutation } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; +import React from "react"; import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; import { Skeleton } from "@/components/skeleton"; import { Trans } from "@/components/trans"; +import { useTranslation } from "@/i18n/client"; import { trpc } from "@/trpc/client"; type PageProps = { magicLink: string; email: string }; @@ -15,6 +17,9 @@ type PageProps = { magicLink: string; email: string }; export const LoginPage = ({ magicLink, email }: PageProps) => { const session = useSession(); const posthog = usePostHog(); + const { t } = useTranslation(); + const [error, setError] = React.useState(null); + const magicLinkFetch = useMutation({ mutationFn: async () => { const res = await fetch(magicLink); @@ -31,9 +36,15 @@ export const LoginPage = ({ magicLink, email }: PageProps) => { name: updatedSession.user.name, }); } + router.push(data.url); + } else { + setError( + t("loginMagicLinkError", { + defaultValue: + "This link is invalid or expired. Please request a new link.", + }), + ); } - - router.push(data.url); }, }); const { data } = trpc.user.getByEmail.useQuery({ email }); @@ -72,6 +83,7 @@ export const LoginPage = ({ magicLink, email }: PageProps) => { + {error &&

{error}

} ); diff --git a/apps/web/src/app/[locale]/(auth)/login/actions.ts b/apps/web/src/app/[locale]/(auth)/login/actions.ts index b7b7281d2..86f18197e 100644 --- a/apps/web/src/app/[locale]/(auth)/login/actions.ts +++ b/apps/web/src/app/[locale]/(auth)/login/actions.ts @@ -4,18 +4,24 @@ import { prisma } from "@rallly/database"; import { cookies } from "next/headers"; export async function setVerificationEmail(email: string) { - const count = await prisma.user.count({ + const user = await prisma.user.findUnique({ where: { email, }, + select: { + email: true, + }, }); - cookies().set("verification-email", email, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 15 * 60, - }); + if (user) { + cookies().set("verification-email", user.email, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 15 * 60, + }); + return true; + } - return count > 0; + return false; } diff --git a/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx b/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx index b0e4e9de2..5c8a26920 100644 --- a/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx @@ -53,13 +53,13 @@ export function LoginWithEmailForm() { if (doesExist) { await signIn("email", { email: identifier, - callbackUrl: searchParams?.get("callbackUrl") ?? undefined, + redirectTo: searchParams?.get("redirectTo") ?? undefined, redirect: false, }); - // redirect to verify page with callbackUrl + // redirect to verify page with redirectTo router.push( - `/login/verify?callbackUrl=${encodeURIComponent( - searchParams?.get("callbackUrl") ?? "", + `/login/verify?redirectTo=${encodeURIComponent( + searchParams?.get("redirectTo") ?? "", )}`, ); } else { diff --git a/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx b/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx index 67f9bd237..ebe251b28 100644 --- a/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx @@ -6,16 +6,16 @@ import { Trans } from "@/components/trans"; export async function LoginWithOIDC({ name, - callbackUrl, + redirectTo, }: { name: string; - callbackUrl?: string; + redirectTo?: string; }) { return (