Claude Code से सुरक्षित यूजर प्रोफाइल फीचर बनाना
DB डिजाइन, फॉर्म, वैलिडेशन, ऑथराइजेशन, अवतार और ऑडिट लॉग के साथ प्रोफाइल बनाएं।
यूजर प्रोफाइल फीचर देखने में छोटा लगता है: नाम, छोटा परिचय और अवतार। लेकिन प्रोडक्शन में यही फीचर डेटा ओनरशिप, पब्लिक फील्ड, इमेज अपलोड लिमिट, वैलिडेशन, ऑथराइजेशन और ऑडिट लॉग से जुड़ जाता है। अगर आप Claude Code से सिर्फ “एक प्रोफाइल पेज बना दो” कहेंगे, तो पेज बन सकता है, लेकिन जरूरी सुरक्षा नियम छूट सकते हैं।
इस लेख में हम Next.js App Router, Prisma, Zod, React और Sharp के साथ एक सुरक्षित यूजर प्रोफाइल फीचर बनाएंगे। मुख्य बात यह है कि Claude Code को कोड लिखने से पहले सीमा बताई जाए: कौन से फील्ड यूजर बदल सकता है, कौन से फील्ड request body से कभी नहीं लेने हैं, और API response में कौन सी जानकारी लौटानी है।
ऑथेंटिकेशन की पृष्ठभूमि के लिए Claude Code authentication guide और फॉर्म इनपुट के लिए Claude Code form validation देखें। सुरक्षा संदर्भ के लिए OWASP Mass Assignment Cheat Sheet, OWASP File Upload Cheat Sheet, Next.js Route Handlers और Prisma relations उपयोगी हैं।
स्कोप और सुरक्षा सीमा
यह फीचर लॉगिन किए हुए यूजर को अपना प्रोफाइल एडिट करने और चाहें तो उसे public बनाने देता है। editable फील्ड हैं username, display name, bio, location, website, social links, public flag और avatar URL। email, plan, role, team permissions और email verification इस फीचर में नहीं आते।
Authentication का मतलब है “यूजर कौन है”। Authorization का मतलब है “यह यूजर कौन सा काम कर सकता है”। प्रोफाइल स्क्रीन सिर्फ presentation के लिए है, admin role या billing plan बदलने के लिए नहीं।
| क्षेत्र | नियम | कारण |
|---|---|---|
| owner | सिर्फ session.user.id अपडेट करें | दूसरे यूजर का प्रोफाइल बदलने से रोकता है |
| input | केवल Zod से स्वीकृत फील्ड लें | mass assignment रोकता है |
| response | सिर्फ public-safe फील्ड लौटाएं | email और internal ID leak नहीं होते |
| avatar | MIME, size, pixels जांचें और re-encode करें | unsafe upload कम होता है |
| audit | बदले हुए field names और कम metadata रखें | traceability और privacy संतुलित रहती है |
flowchart TD
A["Profile form"] --> B["Zod validation"]
B --> C["Authorization check"]
C --> D["Prisma transaction"]
D --> E["Profile table"]
D --> F["ProfileAuditLog"]
A --> G["Avatar upload"]
G --> H["MIME / size / pixel checks"]
H --> I["Sharp resize to WebP"]
I --> E
डेटाबेस डिजाइन
User और Profile को अलग रखें। User में identity और account state रहेगी। Profile में केवल दिखाने वाली जानकारी रहेगी। बदलावों को ProfileAuditLog में रखा जाएगा।
// prisma/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
emailVerified DateTime?
profile Profile?
profileAuditLogs ProfileAuditLog[] @relation("ProfileAuditActor")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Profile {
id String @id @default(cuid())
userId String @unique
username String @unique
displayName String
bio String @default("")
location String @default("")
websiteUrl String @default("")
avatarUrl String?
socialLinks Json @default("{}")
isPublic Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([isPublic, updatedAt])
}
model ProfileAuditLog {
id String @id @default(cuid())
userId String
actorUserId String
action String
changedFields Json
metadata Json?
createdAt DateTime @default(now())
actor User @relation("ProfileAuditActor", fields: [actorUserId], references: [id])
@@index([userId, createdAt])
}
Profile में role, plan, isAdmin या emailVerified न रखें। अगर admin किसी member का प्रोफाइल बदल सकता है, तो अलग admin endpoint बनाएं और audit log में admin को actorUserId के रूप में रखें।
Zod वैलिडेशन
Zod untrusted request body और database के बीच की सुरक्षा दीवार है। .strict() unknown keys को reject करता है, इसलिए role, emailVerified या userId जैसे फील्ड आगे नहीं जाते।
// src/lib/profile-schema.ts
import { z } from "zod";
const usernamePattern = /^[a-z0-9][a-z0-9_-]{2,29}$/;
function isHttpUrl(value: string) {
if (value === "") return true;
try {
const url = new URL(value);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
const optionalHttpUrl = z
.string()
.trim()
.max(200)
.refine(isHttpUrl, "Use http or https URLs only");
export const profileInputSchema = z
.object({
username: z
.string()
.trim()
.regex(usernamePattern, "Use 3-30 lowercase letters, numbers, _ or -"),
displayName: z.string().trim().min(1).max(40),
bio: z.string().trim().max(280).default(""),
location: z.string().trim().max(80).default(""),
websiteUrl: optionalHttpUrl.default(""),
socialLinks: z
.object({
github: optionalHttpUrl.default(""),
x: optionalHttpUrl.default(""),
linkedin: optionalHttpUrl.default(""),
})
.default({}),
isPublic: z.boolean().default(false),
})
.strict();
export type ProfileInput = z.infer<typeof profileInputSchema>;
Username public URL में आता है, इसलिए इसे lowercase letters, numbers, underscore और hyphen तक सीमित रखना अच्छा है। Display name को HTML की तरह render न करें। Social links खाली हो सकते हैं, लेकिन अगर value है तो http या https URL होना चाहिए।
प्रोफाइल अपडेट API
API route को server side पर session पढ़ना चाहिए और owner को session.user.id से तय करना चाहिए। Client से आया userId भरोसेमंद नहीं है। Profile save और audit log को एक transaction में रखना चाहिए।
// src/app/api/profile/route.ts
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { profileInputSchema } from "@/lib/profile-schema";
const publicProfileSelect = {
username: true,
displayName: true,
bio: true,
location: true,
websiteUrl: true,
avatarUrl: true,
socialLinks: true,
isPublic: true,
updatedAt: true,
} as const;
function changedKeys(before: Record<string, unknown> | null, after: Record<string, unknown>) {
if (!before) return Object.keys(after);
return Object.keys(after).filter((key) => {
return JSON.stringify(before[key]) !== JSON.stringify(after[key]);
});
}
export async function GET() {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const profile = await prisma.profile.findUnique({
where: { userId: session.user.id },
select: publicProfileSelect,
});
return NextResponse.json({ profile });
}
export async function PUT(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const json = await request.json().catch(() => null);
const parsed = profileInputSchema.safeParse(json);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid profile input", issues: parsed.error.flatten() },
{ status: 400 }
);
}
const userId = session.user.id;
const input = parsed.data;
const before = await prisma.profile.findUnique({ where: { userId } });
const fields = changedKeys(before, input);
const profile = await prisma.$transaction(async (tx) => {
const saved = await tx.profile.upsert({
where: { userId },
update: input,
create: { userId, ...input },
select: publicProfileSelect,
});
if (fields.length > 0) {
await tx.profileAuditLog.create({
data: {
userId,
actorUserId: userId,
action: before ? "profile.update" : "profile.create",
changedFields: fields,
metadata: {
source: "profile-settings",
beforeDisplayName: before?.displayName ?? null,
},
},
});
}
return saved;
});
return NextResponse.json({ profile });
}
Response में सिर्फ public-safe fields हैं। Email, role, plan, internal ID और audit logs नहीं लौटते। Audit log में changed field names रखे गए हैं, पूरी पुरानी bio या पुराने social links नहीं।
अवतार अपलोड
Image upload में frontend picker पर भरोसा नहीं करना चाहिए। Server MIME type, byte size और pixel dimensions जांचेगा, फिर Sharp से image को WebP में बदल देगा। यह example local testing के लिए public/uploads/avatars में लिखता है।
// src/app/api/profile/avatar/route.ts
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { NextResponse } from "next/server";
import sharp from "sharp";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export const runtime = "nodejs";
const MAX_BYTES = 2 * 1024 * 1024;
const MAX_PIXELS = 4096 * 4096;
const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
export async function POST(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const formData = await request.formData();
const file = formData.get("avatar");
if (!(file instanceof File)) {
return NextResponse.json({ error: "Avatar file is required" }, { status: 400 });
}
if (!allowedTypes.has(file.type)) {
return NextResponse.json({ error: "Use JPEG, PNG, or WebP" }, { status: 400 });
}
if (file.size > MAX_BYTES) {
return NextResponse.json({ error: "Avatar must be 2MB or smaller" }, { status: 400 });
}
const input = Buffer.from(await file.arrayBuffer());
const image = sharp(input, { limitInputPixels: MAX_PIXELS });
const metadata = await image.metadata();
if (!metadata.width || !metadata.height || metadata.width < 64 || metadata.height < 64) {
return NextResponse.json({ error: "Image must be at least 64x64 pixels" }, { status: 400 });
}
const output = await sharp(input, { limitInputPixels: MAX_PIXELS })
.rotate()
.resize(256, 256, { fit: "cover" })
.webp({ quality: 82 })
.toBuffer();
const fileName = `${session.user.id}-${randomUUID()}.webp`;
const uploadDir = path.join(process.cwd(), "public", "uploads", "avatars");
await mkdir(uploadDir, { recursive: true });
await writeFile(path.join(uploadDir, fileName), output);
const avatarUrl = `/uploads/avatars/${fileName}`;
await prisma.$transaction(async (tx) => {
await tx.profile.update({
where: { userId: session.user.id },
data: { avatarUrl },
});
await tx.profileAuditLog.create({
data: {
userId: session.user.id,
actorUserId: session.user.id,
action: "profile.avatar.update",
changedFields: ["avatarUrl"],
metadata: { contentType: "image/webp", bytes: output.byteLength },
},
});
});
return NextResponse.json({ avatarUrl });
}
Production में object storage, signed URL, malware scanning, old avatar cleanup और CDN cache policy भी तय करें। Claude Code को ये नीति पहले बतानी चाहिए।
React फॉर्म
Form में avatar upload और profile JSON save को अलग रखें। Image upload fail हो तो text data बचा रहे, और text validation fail हो तो image फिर से upload न करनी पड़े। Save के बाद UI state को server response से update करें।
// src/components/ProfileForm.tsx
"use client";
import { useState, useTransition } from "react";
import type { ProfileInput } from "@/lib/profile-schema";
type ProfileFormProps = {
initialProfile: ProfileInput & { avatarUrl?: string | null };
};
export function ProfileForm({ initialProfile }: ProfileFormProps) {
const [form, setForm] = useState<ProfileInput>(initialProfile);
const [avatarUrl, setAvatarUrl] = useState(initialProfile.avatarUrl ?? "");
const [message, setMessage] = useState("");
const [isPending, startTransition] = useTransition();
function updateField<K extends keyof ProfileInput>(key: K, value: ProfileInput[K]) {
setForm((current) => ({ ...current, [key]: value }));
}
async function uploadAvatar(file: File) {
setMessage("");
if (file.size > 2 * 1024 * 1024) {
setMessage("Avatar must be 2MB or smaller.");
return;
}
const body = new FormData();
body.append("avatar", file);
const response = await fetch("/api/profile/avatar", {
method: "POST",
body,
});
const result = await response.json();
if (!response.ok) {
setMessage(result.error ?? "Avatar upload failed.");
return;
}
setAvatarUrl(result.avatarUrl);
setMessage("Avatar updated.");
}
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setMessage("");
startTransition(async () => {
const response = await fetch("/api/profile", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
const result = await response.json();
if (!response.ok) {
setMessage(result.error ?? "Profile save failed.");
return;
}
setForm(result.profile);
setMessage("Profile saved.");
});
}
return (
<form onSubmit={submit} className="mx-auto max-w-2xl space-y-5">
<div className="flex items-center gap-4">
<img
src={avatarUrl || "/images/default-avatar.png"}
alt=""
className="h-20 w-20 rounded-full object-cover"
/>
<label className="block">
<span className="text-sm font-medium">Avatar image</span>
<input
type="file"
accept="image/jpeg,image/png,image/webp"
onChange={(event) => {
const file = event.currentTarget.files?.[0];
if (file) void uploadAvatar(file);
}}
className="mt-2 block text-sm"
/>
</label>
</div>
<label className="block">
<span className="text-sm font-medium">Username</span>
<input
value={form.username}
onChange={(event) => updateField("username", event.target.value as ProfileInput["username"])}
className="mt-1 w-full rounded border px-3 py-2"
autoComplete="off"
/>
</label>
<label className="block">
<span className="text-sm font-medium">Display name</span>
<input
value={form.displayName}
onChange={(event) => updateField("displayName", event.target.value)}
className="mt-1 w-full rounded border px-3 py-2"
maxLength={40}
/>
</label>
<label className="block">
<span className="text-sm font-medium">Bio</span>
<textarea
value={form.bio}
onChange={(event) => updateField("bio", event.target.value)}
className="mt-1 h-28 w-full rounded border px-3 py-2"
maxLength={280}
/>
<span className="text-xs text-slate-500">{form.bio.length}/280</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={form.isPublic}
onChange={(event) => updateField("isPublic", event.target.checked)}
/>
<span>Show this profile publicly</span>
</label>
<button
type="submit"
disabled={isPending}
className="rounded bg-slate-900 px-4 py-2 font-medium text-white disabled:bg-slate-400"
>
{isPending ? "Saving..." : "Save profile"}
</button>
{message && <p className="text-sm text-slate-700">{message}</p>}
</form>
);
}
बाद में field-level errors, duplicate username message, image preview और focus handling जोड़ें। लेकिन ये सब server validation और authorization के ऊपर होना चाहिए।
Claude Code के लिए prompt
Claude Code को implementation के साथ review checklist भी दें।
Implement a user profile feature in this Next.js App Router project.
Scope:
- Edit only the files needed for profile schema, API routes, avatar upload, and ProfileForm.
- Use Prisma for Profile and ProfileAuditLog.
- Use Zod for request validation.
- Never accept userId, email, role, plan, or emailVerified from the request body.
- Use session.user.id as the only profile owner.
- Return only public-safe profile fields from API responses.
- Limit avatars to JPEG, PNG, or WebP, 2MB max, then resize to 256x256 WebP.
- Create an audit log row for profile and avatar changes.
After implementation, review your own diff for:
- mass assignment
- broken authorization
- unsafe file upload
- PII in logs
- optimistic UI inconsistency
- missing tests or manual verification steps
यह prompt सिर्फ फीचर नहीं बताता, बल्कि गलत रास्ते भी रोकता है। खासकर userId, role और emailVerified को request body से न लेना बहुत जरूरी है।
उपयोग के उदाहरण
पहला उदाहरण SaaS account profile है। यूजर नाम, department, avatar और bio बदलता है। Plan और admin role अलग flow में रहते हैं।
दूसरा उदाहरण team management है। Admin member profiles देख सकता है, लेकिन role change अलग admin API से होना चाहिए। अगर admin किसी member का profile बदलता है, audit log में actor और target अलग होने चाहिए।
तीसरा उदाहरण onboarding है। पहले login पर username और display name लेने से invite और public page आसान बनते हैं। Terms acceptance और email verification अलग state हैं।
चौथा उदाहरण public profile है। केवल isPublic true होने पर /users/[username] पेज दिखना चाहिए। Public route email, internal ID, team data या audit logs नहीं लौटाएगा।
आम गलतियां
पहली गलती request से target userId लेना है। Self-service profile में target session से ही तय होना चाहिए।
दूसरी गलती ...body को सीधे Prisma में देना है। इससे future migration के बाद mass assignment risk बढ़ता है।
तीसरी गलती upload validation सिर्फ browser में रखना है। Server check अनिवार्य है।
चौथी गलती logs में बहुत PII रखना है। Audit log को पूरी bio या पुराने links की copy नहीं बनाना चाहिए।
पांचवीं गलती optimistic UI mismatch है। Public flag, username और avatar URL को server response से सही करें।
कमाई और संचालन
Profile feature revenue से भी जुड़ता है। SaaS में complete team profiles भरोसा बढ़ाते हैं। Community में public profile reputation बनाता है। Training business में instructor और learner profile consultation decision को प्रभावित करते हैं।
ClaudeCodeLab ऐसी छोटी लेकिन sensitive surfaces पर मदद करता है: authentication, profiles, admin screens, audit logs और billing के पास वाले settings। अगर आप existing product में सुरक्षित profile फीचर जोड़ना चाहते हैं या team को Claude Code review सिखाना चाहते हैं, तो Claude Code training and consultation देखें।
Launch के बाद profile completion, avatar upload failures, duplicate username errors, public profile views और CTA clicks देखें। Logs support और incident review के लिए हैं, personal data store करने के लिए नहीं।
वास्तव में टेस्ट करने का परिणाम
Masa ने इस flow को एक छोटे Next.js app में test किया। जब पहले database boundary और forbidden fields लिखे गए, तो Claude Code का output review करना आसान था। जब सिर्फ “अच्छा profile page बना दो” कहा गया, तो form state में userId, client-only image validation और पुरानी bio को पूरा audit log में save करने जैसी समस्याएं आईं।
Publish करने से पहले दूसरे userId को JSON में भेजकर देखें, role और emailVerified भेजें, 2MB से बड़ी image upload करें, javascript: social link भेजें, duplicate username test करें और save failure के समय UI देखें। Claude Code speed देता है, लेकिन profile quality ownership, validation, logs और public exposure की जांच से आती है।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.