Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions apps/web/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { Input } from "@/components/ui/Input";
import { Button } from "@/components/ui/Button";
import { PageLoadingState } from "@/components/ui/brand-loader";
import { api, type ApiError } from "@/lib/api";
import {
buildGoogleOAuthStartUrl,
getGoogleAuthErrorMessage,
getSafeAuthRedirect,
} from "@/lib/auth";

const loginSchema = z.object({
email: z.string().email("Enter a valid email address"),
Expand All @@ -21,18 +26,6 @@ type LoginFields = z.infer<typeof loginSchema>;

const DEFAULT_LOGIN_DESTINATION = "/home";

function getSafeRedirectDestination(value: string | null): string | null {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return null;
}

if (value === "/login" || value.startsWith("/login?")) {
return null;
}

return value;
}

function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
Expand All @@ -49,9 +42,10 @@ function LoginForm() {
setServerError(null);
try {
await api.post("/auth/login", data);
const destination =
getSafeRedirectDestination(searchParams.get("redirect")) ??
DEFAULT_LOGIN_DESTINATION;
const destination = getSafeAuthRedirect(
searchParams.get("redirect"),
DEFAULT_LOGIN_DESTINATION,
);
router.push(destination);
} catch (err) {
setServerError(
Expand All @@ -60,6 +54,17 @@ function LoginForm() {
}
};

const handleGoogleSignIn = () => {
const destination = getSafeAuthRedirect(
searchParams.get("redirect"),
DEFAULT_LOGIN_DESTINATION,
);
window.location.assign(buildGoogleOAuthStartUrl(destination));
};

const oauthError = getGoogleAuthErrorMessage(searchParams.get("error"));
const visibleError = serverError ?? oauthError;

return (
<div>
<h1 className="font-cabinet font-bold text-2xl text-[var(--color-espresso)] mb-1">
Expand All @@ -69,6 +74,31 @@ function LoginForm() {
Sign in to your twizrr account.
</p>

<div className="mb-6 flex flex-col gap-3">
<Button
type="button"
variant="secondary"
fullWidth
size="lg"
onClick={handleGoogleSignIn}
>
Continue with Google
</Button>

<p className="text-center text-xs text-muted-foreground font-cabinet">
Google verifies your email. Phone verification may still be required
before checkout.
</p>

<div className="relative flex items-center gap-3 py-1">
<div className="flex-1 h-px bg-[var(--border)]" />
<span className="text-xs text-muted-foreground font-cabinet shrink-0">
or
</span>
<div className="flex-1 h-px bg-[var(--border)]" />
</div>
</div>

<form
onSubmit={handleSubmit(onSubmit)}
noValidate
Expand Down Expand Up @@ -116,12 +146,12 @@ function LoginForm() {
</Link>
</div>

{serverError && (
{visibleError && (
<p
role="alert"
className="text-sm text-[var(--color-error)] font-cabinet rounded-md bg-[var(--color-error)]/10 px-3 py-2"
>
{serverError}
{visibleError}
</p>
)}

Expand Down
56 changes: 51 additions & 5 deletions apps/web/src/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"use client";

import { useState } from "react";
import { Suspense, useState } from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { ArrowLeft, Eye, EyeOff } from "lucide-react";
import { Input } from "@/components/ui/Input";
import { Button } from "@/components/ui/Button";
import { PageLoadingState } from "@/components/ui/brand-loader";
import { api, type ApiError } from "@/lib/api";
import {
buildGoogleOAuthStartUrl,
getGoogleAuthErrorMessage,
getSafeAuthRedirect,
} from "@/lib/auth";

function calcAge(dobString: string): number {
const dob = new Date(dobString);
Expand Down Expand Up @@ -120,8 +126,11 @@ function StepProgress({ step, total }: { step: number; total: number }) {
);
}

export default function RegisterPage() {
const DEFAULT_REGISTER_DESTINATION = "/explore";

function RegisterForm() {
const router = useRouter();
const searchParams = useSearchParams();

const [step, setStep] = useState(1);
const [serverError, setServerError] = useState<string | null>(null);
Expand Down Expand Up @@ -150,6 +159,14 @@ export default function RegisterPage() {
router.push(`/verify-email?${params.toString()}`);
}

function startGoogleAuth() {
const destination = getSafeAuthRedirect(
searchParams.get("redirect"),
DEFAULT_REGISTER_DESTINATION,
);
window.location.assign(buildGoogleOAuthStartUrl(destination));
}

const form2 = useForm<Step2Fields>({
resolver: zodResolver(step2Schema),
defaultValues: { email: data.email, password: data.password },
Expand Down Expand Up @@ -210,6 +227,8 @@ export default function RegisterPage() {
}
};

const oauthError = getGoogleAuthErrorMessage(searchParams.get("error"));

return (
<div>
{step > 1 && (
Expand All @@ -235,13 +254,32 @@ export default function RegisterPage() {
</p>

<div className="flex flex-col gap-3">
<Button variant="secondary" fullWidth size="lg" disabled>
Continue with Google - coming soon
<Button
type="button"
variant="secondary"
fullWidth
size="lg"
onClick={startGoogleAuth}
>
Continue with Google
</Button>
<p className="text-center text-xs text-muted-foreground font-cabinet">
Google verifies your email. Phone verification may still be
required before checkout.
</p>
<Button variant="secondary" fullWidth size="lg" disabled>
Continue with Apple - coming soon
</Button>

{oauthError && (
<p
role="alert"
className="text-sm text-[var(--color-error)] font-cabinet rounded-md bg-[var(--color-error)]/10 px-3 py-2"
>
{oauthError}
</p>
)}

<div className="relative flex items-center gap-3 py-1">
<div className="flex-1 h-px bg-[var(--border)]" />
<span className="text-xs text-muted-foreground font-cabinet shrink-0">
Expand Down Expand Up @@ -459,3 +497,11 @@ export default function RegisterPage() {
</div>
);
}

export default function RegisterPage() {
return (
<Suspense fallback={<PageLoadingState label="Loading registration..." />}>
<RegisterForm />
</Suspense>
);
}
54 changes: 54 additions & 0 deletions apps/web/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { api } from "@/lib/api";

const API_BASE = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");

const AUTH_ROUTE_PREFIXES = [
"/login",
"/register",
"/forgot-password",
"/verify-email",
];

export type GoogleOnboardingPrefill = {
providerEmail: string;
providerEmailVerified: boolean;
Expand All @@ -20,6 +29,51 @@ export type CompleteGoogleOnboardingResponse = {
redirectTo: string;
};

export function getSafeAuthRedirect(
value: string | null | undefined,
fallback: string,
): string {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return fallback;
}

if (AUTH_ROUTE_PREFIXES.some((prefix) => value.startsWith(prefix))) {
return fallback;
}

return value;
}

export function buildGoogleOAuthStartUrl(redirectPath: string): string {
const params = new URLSearchParams({ redirect: redirectPath });
return `${API_BASE}/auth/google?${params.toString()}`;
}

export function getGoogleAuthErrorMessage(error: string | null): string | null {
if (!error) {
return null;
}

const normalized = error.trim().toUpperCase();

if (
normalized.includes("EMAIL") &&
(normalized.includes("UNVERIFIED") || normalized.includes("NOT_VERIFIED"))
) {
return "Google email must be verified.";
}

if (normalized.includes("SESSION") || normalized.includes("EXPIRED")) {
return "Google session expired. Please try again.";
}

if (normalized.includes("GOOGLE") || normalized.includes("OAUTH")) {
return "Google sign-in failed. Please try again.";
}

return "Authentication failed. Please try again.";
}

export function getGoogleOnboardingPrefill(
sessionId: string,
): Promise<GoogleOnboardingPrefill> {
Expand Down
Loading