From ad8796a25d9094481cf4d1b1051ad877ac0f3b37 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sat, 6 Jun 2026 22:36:23 +0330 Subject: [PATCH] feat(admin): edit any user's full profile (PATCH/POST /v1/users/{id} admin + UI modal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Identity: admin-only PATCH /v1/users/{id} (reuses UpdateMeAsync) + POST {id}/avatar. Admin Users panel: «پروفایل» modal to view/edit name/slogan/about/company/website/ country/national-code/birthdate/gender/avatar for any user. Verified admin→other-user edit. Co-Authored-By: Claude Opus 4.8 --- .../Controllers/UsersController.cs | 15 +++ src/components/admin/UserProfileEdit.tsx | 117 ++++++++++++++++++ src/components/admin/admin-resources.tsx | 2 + 3 files changed, 134 insertions(+) create mode 100644 src/components/admin/UserProfileEdit.tsx diff --git a/services/identity/FlatRender.IdentitySvc/Controllers/UsersController.cs b/services/identity/FlatRender.IdentitySvc/Controllers/UsersController.cs index bbfde98..d270a4c 100644 --- a/services/identity/FlatRender.IdentitySvc/Controllers/UsersController.cs +++ b/services/identity/FlatRender.IdentitySvc/Controllers/UsersController.cs @@ -49,6 +49,21 @@ public class UsersController(IUserService userService) : ControllerBase [FromQuery] int pageSize = 20) => Ok(await userService.SearchAsync(q, tenantId, page, pageSize)); + [HttpPatch("{userId:guid}")] + [Authorize(Roles = "Admin")] + [ProducesResponseType(typeof(UserResponse), 200)] + [ProducesResponseType(404)] + public async Task UpdateUser(Guid userId, [FromBody] UpdateUserRequest request) + => Ok(await userService.UpdateMeAsync(userId, request)); + + [HttpPost("{userId:guid}/avatar")] + [Authorize(Roles = "Admin")] + public async Task SetUserAvatar(Guid userId, [FromBody] SetAvatarRequest request) + { + await userService.UpdateAvatarAsync(userId, request.AvatarId, request.AvatarUrl); + return Ok(); + } + [HttpPost("{userId:guid}/ban")] [Authorize(Roles = "Admin")] [ProducesResponseType(204)] diff --git a/src/components/admin/UserProfileEdit.tsx b/src/components/admin/UserProfileEdit.tsx new file mode 100644 index 0000000..6404261 --- /dev/null +++ b/src/components/admin/UserProfileEdit.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useState } from "react"; + +const inp = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"; +const lbl = "mb-1 block text-xs font-medium text-gray-400"; + +type Profile = { + full_name: string; slogan: string; about_me: string; company_name: string; + website_name: string; country_code: string; national_code: string; + birth_date: string; gender: string; avatar_url: string; +}; + +const EMPTY: Profile = { + full_name: "", slogan: "", about_me: "", company_name: "", website_name: "", + country_code: "", national_code: "", birth_date: "", gender: "", avatar_url: "", +}; + +const GENDERS = ["Male", "Female", "Other", "PreferNotToSay"] as const; + +/** Admin: edit any user's full profile (name, avatar, bio, company, …). */ +export function UserProfileEdit({ row }: { row: Record; reload?: () => void }) { + const id = String(row.id); + const [open, setOpen] = useState(false); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(null); + const [f, setF] = useState(EMPTY); + const set = (k: keyof Profile, v: string) => setF((p) => ({ ...p, [k]: v })); + + const load = async () => { + setOpen(true); setMsg(null); + const r = await fetch(`/api/admin/resource/users/${id}`, { cache: "no-store" }).then((x) => x.json()).catch(() => null); + if (r) { + const s = (v: unknown) => (typeof v === "string" ? v : ""); + setF({ + full_name: s(r.full_name), slogan: s(r.slogan), about_me: s(r.about_me), + company_name: s(r.company_name), website_name: s(r.website_name), + country_code: s(r.country_code), national_code: s(r.national_code), + birth_date: s(r.birth_date), gender: s(r.gender), avatar_url: s(r.avatar_url), + }); + } + }; + + const save = async () => { + setBusy(true); setMsg(null); + try { + const res = await fetch(`/api/admin/resource/users/${id}`, { + method: "PATCH", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + full_name: f.full_name, slogan: f.slogan, about_me: f.about_me, + company_name: f.company_name, website_name: f.website_name, + country_code: f.country_code, national_code: f.national_code, + birth_date: f.birth_date || null, gender: f.gender || null, + }), + }); + if (res.ok && f.avatar_url.trim()) { + await fetch(`/api/admin/resource/users/${id}/avatar`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ avatar_url: f.avatar_url.trim() }), + }); + } + const d = await res.json().catch(() => null); + setMsg(res.ok ? "ذخیره شد ✓" : (d?.error?.message ?? d?.error ?? "خطا")); + } catch { setMsg("خطای شبکه"); } + finally { setBusy(false); } + }; + + return ( + <> + + {open && ( +
setOpen(false)}> +
e.stopPropagation()}> +
+

ویرایش پروفایل کاربر

+ +
+
+

{String(row.email ?? "")}

+
+
set("full_name", e.target.value)} />
+
set("slogan", e.target.value)} />
+
+