From 2adaf57f1087644fc7ce0b16dd7fa01903a7ec3b Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sat, 30 May 2026 06:06:47 +0330 Subject: [PATCH] feat(frontend): move settings profile + password off Supabase to V2 Identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two authenticated gateway proxies and rewires the settings UI to them: - POST /api/auth/password → Identity /v1/auth/password/change (server-side re-validates current password; drops the client-side re-auth round-trip) - PATCH /api/profile → Identity PATCH /v1/users/me (full_name) SettingsProfile and SettingsSecurity now fetch these routes instead of the Supabase browser client. Co-Authored-By: Claude Opus 4.7 --- src/app/api/auth/password/route.ts | 55 +++++++++++++++++++ src/app/api/profile/route.ts | 42 ++++++++++++++ .../dashboard/settings/SettingsProfile.tsx | 24 +++++--- .../dashboard/settings/SettingsSecurity.tsx | 44 ++++++--------- 4 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 src/app/api/auth/password/route.ts create mode 100644 src/app/api/profile/route.ts diff --git a/src/app/api/auth/password/route.ts b/src/app/api/auth/password/route.ts new file mode 100644 index 0000000..7b6c476 --- /dev/null +++ b/src/app/api/auth/password/route.ts @@ -0,0 +1,55 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { gatewayFetch } from "@/lib/api/gateway"; +import { ACCESS_TOKEN_COOKIE } from "@/lib/auth/constants"; + +export const dynamic = "force-dynamic"; + +/** + * Change the signed-in user's password via Identity (`/v1/auth/password/change`). + * Identity re-validates the current password server-side, so no client-side + * re-authentication round-trip is needed. + */ +export async function POST(req: Request) { + const token = (await cookies()).get(ACCESS_TOKEN_COOKIE)?.value; + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await req.json().catch(() => null); + const currentPassword = body?.current_password; + const newPassword = body?.new_password; + if (!currentPassword || !newPassword) { + return NextResponse.json( + { error: "Current and new password are required." }, + { status: 400 } + ); + } + if (typeof newPassword !== "string" || newPassword.length < 8) { + return NextResponse.json( + { error: "New password must be at least 8 characters." }, + { status: 400 } + ); + } + + const res = await gatewayFetch("/v1/auth/password/change", { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => null); + const message = + res.status === 400 || res.status === 401 + ? (data?.message ?? "Current password is incorrect.") + : (data?.message ?? "Could not change password."); + return NextResponse.json({ error: message }, { status: res.status }); + } + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts new file mode 100644 index 0000000..c60a272 --- /dev/null +++ b/src/app/api/profile/route.ts @@ -0,0 +1,42 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { gatewayFetch } from "@/lib/api/gateway"; +import { ACCESS_TOKEN_COOKIE } from "@/lib/auth/constants"; + +export const dynamic = "force-dynamic"; + +/** + * Update the signed-in user's profile via Identity (`PATCH /v1/users/me`). + * Currently surfaces the display name (full_name); the Identity DTO accepts more + * fields that can be added here as the settings UI grows. + */ +export async function PATCH(req: Request) { + const token = (await cookies()).get(ACCESS_TOKEN_COOKIE)?.value; + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await req.json().catch(() => null); + const fullName = typeof body?.full_name === "string" ? body.full_name.trim() : undefined; + if (fullName === undefined) { + return NextResponse.json({ error: "Nothing to update" }, { status: 400 }); + } + + const res = await gatewayFetch("/v1/users/me", { + method: "PATCH", + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ full_name: fullName }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => null); + return NextResponse.json( + { error: data?.message ?? "Could not update profile." }, + { status: res.status } + ); + } + + const user = await res.json().catch(() => null); + return NextResponse.json({ user }); +} diff --git a/src/components/dashboard/settings/SettingsProfile.tsx b/src/components/dashboard/settings/SettingsProfile.tsx index 6f3c4c4..0daabec 100644 --- a/src/components/dashboard/settings/SettingsProfile.tsx +++ b/src/components/dashboard/settings/SettingsProfile.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { User } from "lucide-react"; -import { createClient } from "@/lib/supabase/client"; interface SettingsProfileProps { email: string; @@ -20,13 +19,22 @@ export function SettingsProfile({ email, displayName }: SettingsProfileProps) { e.preventDefault(); setSaving(true); setMessage(null); - const supabase = createClient(); - const { error } = await supabase.auth.updateUser({ data: { full_name: name.trim() } }); - setSaving(false); - if (error) { - setMessage({ type: "error", text: error.message }); - } else { - setMessage({ type: "success", text: "Profile updated successfully." }); + try { + const res = await fetch("/api/profile", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ full_name: name.trim() }), + }); + const data = (await res.json().catch(() => null)) as { error?: string } | null; + if (!res.ok) { + setMessage({ type: "error", text: data?.error ?? "Could not update profile." }); + } else { + setMessage({ type: "success", text: "Profile updated successfully." }); + } + } catch { + setMessage({ type: "error", text: "Network error. Please try again." }); + } finally { + setSaving(false); } } diff --git a/src/components/dashboard/settings/SettingsSecurity.tsx b/src/components/dashboard/settings/SettingsSecurity.tsx index 1857d6a..c4ec7f3 100644 --- a/src/components/dashboard/settings/SettingsSecurity.tsx +++ b/src/components/dashboard/settings/SettingsSecurity.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { Eye, EyeOff } from "lucide-react"; -import { createClient } from "@/lib/supabase/client"; export function SettingsSecurity() { const [current, setCurrent] = useState(""); @@ -26,32 +25,25 @@ export function SettingsSecurity() { } setSaving(true); - const supabase = createClient(); - - // Re-authenticate with current password first - const { data: session } = await supabase.auth.getSession(); - const email = session.session?.user?.email; - if (!email) { - setMessage({ type: "error", text: "Session expired. Please sign in again." }); + try { + // Identity re-validates the current password server-side, so no separate + // client-side re-authentication round-trip is needed. + const res = await fetch("/api/auth/password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ current_password: current, new_password: next }), + }); + const data = (await res.json().catch(() => null)) as { error?: string } | null; + if (!res.ok) { + setMessage({ type: "error", text: data?.error ?? "Could not change password." }); + } else { + setMessage({ type: "success", text: "Password changed successfully." }); + setCurrent(""); setNext(""); setConfirm(""); + } + } catch { + setMessage({ type: "error", text: "Network error. Please try again." }); + } finally { setSaving(false); - return; - } - - const { error: signInError } = await supabase.auth.signInWithPassword({ email, password: current }); - if (signInError) { - setSaving(false); - setMessage({ type: "error", text: "Current password is incorrect." }); - return; - } - - const { error } = await supabase.auth.updateUser({ password: next }); - setSaving(false); - - if (error) { - setMessage({ type: "error", text: error.message }); - } else { - setMessage({ type: "success", text: "Password changed successfully." }); - setCurrent(""); setNext(""); setConfirm(""); } }