feat(auth) : let Google-only users add a password

This commit is contained in:
2026-10-12 00:34:43 +07:00 Unverified
parent 839f98aab7
commit e2af59292c
3 changed files with 76 additions and 25 deletions
+28
View File
@@ -0,0 +1,28 @@
import { APIError } from "better-auth/api";
import * as z from "zod";
import { getAuth, getCustomerSession } from "@/lib/auth/server";
import { errorResponse, HttpError, readJson, requireSameOrigin } from "@/lib/security/http";
import { limitRequest } from "@/lib/security/rate-limit";
export async function POST(request: Request) {
try {
requireSameOrigin(request);
const session = await getCustomerSession();
if (!session) throw new HttpError(401, "unauthorized");
await limitRequest("password-set", session.user.id, 5, 3600);
const parsed = z.object({ newPassword: z.string().min(6).max(128) })
.safeParse(await readJson(request, 1024));
if (!parsed.success) throw new HttpError(400, "invalid-password");
await getAuth().api.setPassword({ headers: request.headers, body: parsed.data });
return new Response(null, { status: 204, headers: { "Cache-Control": "no-store" } });
} catch (cause) {
if (cause instanceof APIError) {
const error = typeof cause.body?.code === "string" ? cause.body.code : "password-setting-failed";
return Response.json({ error }, {
status: cause.statusCode || 400,
headers: { "Cache-Control": "no-store" },
});
}
return errorResponse(cause);
}
}
+7 -5
View File
@@ -1,4 +1,4 @@
import { eq } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";
import { redirect } from "next/navigation";
import { connection } from "next/server";
import { UserRoundIcon } from "lucide-react";
@@ -27,15 +27,17 @@ export default async function ProfilePage({ searchParams }: PageProps<"/profile"
getDb().select({ name: users.name, email: users.email,
emailVerified: users.emailVerified, image: users.image })
.from(users).where(eq(users.id, session.user.id)).limit(1),
getDb().select({ id: accounts.id, providerId: accounts.providerId })
getDb().select({ id: accounts.id, providerId: accounts.providerId,
hasPassword: sql<boolean>`${accounts.password} is not null` })
.from(accounts).where(eq(accounts.userId, session.user.id)),
]);
const [user] = userRows;
if (!user) redirect("/auth/login?next=%2Fprofile");
const hasCredential = accountRows.some((account) => account.providerId === "credential");
const hasPassword = accountRows.some((account) => account.providerId === "credential" && account.hasPassword);
const googleAccountIds = accountRows.filter((account) => account.providerId === "google")
.map((account) => account.id);
const hasOtherAuthMethod = accountRows.some((account) => account.providerId !== "google");
const hasOtherAuthMethod = accountRows.some((account) => account.providerId !== "google"
&& (account.providerId !== "credential" || account.hasPassword));
return <div className="min-h-svh">{isAuthorizedAdmin(session.user) ? <AdminHeader /> : <SiteHeader />}
<main className="mx-auto max-w-5xl p-4 py-10 sm:p-8">
<h1 className="mb-6 flex items-center gap-2 font-heading text-3xl font-semibold">
@@ -52,7 +54,7 @@ export default async function ProfilePage({ searchParams }: PageProps<"/profile"
callbackCompleted={emailAction === "1" && !error} callbackError={emailAction === "1" && typeof error === "string"} />
{isGoogleAuthEnabled() && <GoogleAccountSettings googleAccountIds={googleAccountIds}
hasOtherAuthMethod={hasOtherAuthMethod} linkError={googleLinkError === "1"} />}
{hasCredential && <PasswordForm />}
<PasswordForm hasPassword={hasPassword} />
</div>
</main>
</div>;