Fix Docker signup and document deploy, migrate, and rebuild flow.
Use FormData and server-side sign-in after register, trust proxy origins for reverse-proxy deploys, pass public URL build args in Docker, and clarify that git pull plus up -d --build is required to ship code and run pending migrations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,31 +2,19 @@ import bcrypt from "bcryptjs";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "~/lib/auth";
|
||||
import { env } from "~/env";
|
||||
import { db } from "~/server/db";
|
||||
import { accounts, users } from "~/server/db/schema";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
firstName: z.string().trim().min(1, "First name is required"),
|
||||
lastName: z.string().trim().min(1, "Last name is required"),
|
||||
name: z.string().trim().optional(),
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const hasSplitName =
|
||||
Boolean(data.firstName?.length) && Boolean(data.lastName?.length);
|
||||
const hasFullName = Boolean(data.name?.length);
|
||||
|
||||
if (!hasSplitName && !hasFullName) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "First and last name are required",
|
||||
path: ["firstName"],
|
||||
});
|
||||
}
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.name?.length) {
|
||||
const parts = data.name.trim().split(/\s+/);
|
||||
@@ -41,8 +29,8 @@ const registerSchema = z
|
||||
}
|
||||
|
||||
return {
|
||||
firstName: data.firstName!.trim(),
|
||||
lastName: data.lastName!.trim(),
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
};
|
||||
@@ -151,6 +139,22 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.api.signInEmail({
|
||||
body: {
|
||||
email: normalizedEmail,
|
||||
password,
|
||||
},
|
||||
headers: request.headers,
|
||||
});
|
||||
} catch (signInError) {
|
||||
console.error("Post-register sign-in failed:", signInError);
|
||||
return NextResponse.json(
|
||||
{ message: "User created successfully", signInRequired: true },
|
||||
{ status: 201 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "User created successfully" },
|
||||
{ status: 201 },
|
||||
|
||||
@@ -9,24 +9,27 @@ import { Label } from "~/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { Logo } from "~/components/branding/logo";
|
||||
import { LegalAgreementNotice } from "~/components/legal/legal-links";
|
||||
import { authClient } from "~/lib/auth-client";
|
||||
import { Mail, Lock, ArrowRight, User } from "lucide-react";
|
||||
|
||||
function formatAuthError(message: string | undefined, fallback: string): string {
|
||||
if (!message || message === "Required") {
|
||||
return fallback;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function RegisterForm() {
|
||||
const router = useRouter();
|
||||
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleRegister(e: React.FormEvent) {
|
||||
async function handleRegister(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
const trimmedFirstName = firstName.trim();
|
||||
const trimmedLastName = lastName.trim();
|
||||
const trimmedEmail = email.trim();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const trimmedFirstName = String(formData.get("firstName") ?? "").trim();
|
||||
const trimmedLastName = String(formData.get("lastName") ?? "").trim();
|
||||
const trimmedEmail = String(formData.get("email") ?? "").trim();
|
||||
const password = String(formData.get("password") ?? "");
|
||||
|
||||
if (!trimmedFirstName || !trimmedLastName || !trimmedEmail) {
|
||||
toast.error("Please enter your first name, last name, and email.");
|
||||
@@ -52,19 +55,22 @@ function RegisterForm() {
|
||||
}),
|
||||
});
|
||||
|
||||
const data = (await res.json()) as { error?: string };
|
||||
|
||||
if (!res.ok) {
|
||||
toast.error(data.error ?? "Registration failed");
|
||||
let data: { error?: string; signInRequired?: boolean } = {};
|
||||
try {
|
||||
data = (await res.json()) as typeof data;
|
||||
} catch {
|
||||
toast.error("Registration failed. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { error: signInError } = await authClient.signIn.email({
|
||||
email: trimmedEmail,
|
||||
password,
|
||||
});
|
||||
if (!res.ok) {
|
||||
toast.error(
|
||||
formatAuthError(data.error, "Registration failed. Please check the form."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signInError) {
|
||||
if (data.signInRequired) {
|
||||
toast.success("Account created! Please sign in.");
|
||||
router.push("/auth/signin");
|
||||
return;
|
||||
@@ -106,11 +112,11 @@ function RegisterForm() {
|
||||
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="given-name"
|
||||
className="h-10 pl-10"
|
||||
placeholder="John"
|
||||
/>
|
||||
@@ -123,10 +129,10 @@ function RegisterForm() {
|
||||
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
required
|
||||
autoComplete="family-name"
|
||||
className="h-10 pl-10"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
@@ -140,10 +146,10 @@ function RegisterForm() {
|
||||
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoComplete="email"
|
||||
className="h-10 pl-10"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
@@ -156,11 +162,11 @@ function RegisterForm() {
|
||||
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
className="h-10 pl-10"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,11 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
|
||||
setLoading(false);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Invalid email or password");
|
||||
toast.error(
|
||||
error.message && error.message !== "Required"
|
||||
? error.message
|
||||
: "Invalid email or password",
|
||||
);
|
||||
} else {
|
||||
toast.success("Signed in successfully!");
|
||||
router.push(callbackUrl);
|
||||
|
||||
Reference in New Issue
Block a user