diff --git a/GanjooRazor/Areas/Admin/Pages/Roles.cshtml b/GanjooRazor/Areas/Admin/Pages/Roles.cshtml
new file mode 100644
index 00000000..a2c9e34b
--- /dev/null
+++ b/GanjooRazor/Areas/Admin/Pages/Roles.cshtml
@@ -0,0 +1,183 @@
+@page
+@model GanjooRazor.Areas.Admin.Pages.RolesModel
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@{
+ Layout = "_AdminLayout";
+ ViewData["Title"] = "مدیریت نقشها";
+}
+
+
+
+@if (!string.IsNullOrEmpty(Model.LastError))
+{
+ @Model.LastError
+}
+
+
+
+
+
+@if (Model.Roles != null && Model.Roles.Length > 0)
+{
+
+
+ | نام |
+ توضیحات |
+ # |
+
+ @foreach (var role in Model.Roles)
+ {
+
+ | @role.Name |
+ @role.Description |
+
+ 🔑
+ 🗑
+ |
+
+
+ |
+
+ |
+
+ }
+
+}
+else
+{
+
+}
diff --git a/GanjooRazor/Areas/Admin/Pages/Roles.cshtml.cs b/GanjooRazor/Areas/Admin/Pages/Roles.cshtml.cs
new file mode 100644
index 00000000..97a4e7b0
--- /dev/null
+++ b/GanjooRazor/Areas/Admin/Pages/Roles.cshtml.cs
@@ -0,0 +1,132 @@
+using GanjooRazor.Utils;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Newtonsoft.Json;
+using RSecurityBackend.Models.Auth.Db;
+using RSecurityBackend.Models.Auth.Memory;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GanjooRazor.Areas.Admin.Pages
+{
+ [IgnoreAntiforgeryToken(Order = 1001)]
+ public class RolesModel : PageModel
+ {
+ ///
+ /// Last Error
+ ///
+ public string LastError { get; set; }
+
+ public RAppRole[] Roles { get; set; }
+
+ public async Task OnGetAsync()
+ {
+ if (string.IsNullOrEmpty(Request.Cookies["Token"]))
+ return Redirect("/");
+
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ {
+ var response = await secureClient.GetAsync($"{APIRoot.Url}/api/roles");
+ if (response.IsSuccessStatusCode)
+ {
+ Roles = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ }
+ else
+ {
+ LastError = await response.Content.ReadAsStringAsync();
+ }
+ }
+ else
+ {
+ LastError = "لطفاً از گنجور خارج و مجددا به آن وارد شوید.";
+ }
+ }
+
+ return Page();
+ }
+
+ ///
+ /// create a new role - ?handler=NewRole
+ ///
+ public async Task OnPostNewRoleAsync(string name, string description)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var newRole = new { name = name, description = description };
+ var response = await secureClient.PostAsync($"{APIRoot.Url}/api/roles", new StringContent(JsonConvert.SerializeObject(newRole), Encoding.UTF8, "application/json"));
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ return new JsonResult(true);
+ }
+ }
+
+ ///
+ /// delete a role - ?handler=Role (DELETE)
+ ///
+ public async Task OnDeleteRoleAsync(string roleName)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var response = await secureClient.DeleteAsync($"{APIRoot.Url}/api/roles/{roleName}");
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ return new JsonResult(true);
+ }
+ }
+
+ ///
+ /// securable items tree annotated with this role's current permission status - ?handler=Permissions&roleName=...
+ ///
+ public async Task OnGetPermissionsAsync(string roleName)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var response = await secureClient.GetAsync($"{APIRoot.Url}/api/roles/permissions/{roleName}");
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ var items = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ return new JsonResult(items);
+ }
+ }
+
+ ///
+ /// save this role's permissions - ?handler=Permissions&roleName=... (PUT)
+ ///
+ public async Task OnPutPermissionsAsync(string roleName)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ string body;
+ using (var reader = new StreamReader(Request.Body))
+ {
+ body = await reader.ReadToEndAsync();
+ }
+
+ var response = await secureClient.PutAsync($"{APIRoot.Url}/api/roles/permissions/{roleName}", new StringContent(body, Encoding.UTF8, "application/json"));
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ return new JsonResult(true);
+ }
+ }
+ }
+}
diff --git a/GanjooRazor/Areas/Admin/Pages/Users.cshtml b/GanjooRazor/Areas/Admin/Pages/Users.cshtml
new file mode 100644
index 00000000..84b3e50f
--- /dev/null
+++ b/GanjooRazor/Areas/Admin/Pages/Users.cshtml
@@ -0,0 +1,216 @@
+@page
+@model GanjooRazor.Areas.Admin.Pages.UsersModel
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@{
+ Layout = "_AdminLayout";
+ ViewData["Title"] = "مدیریت کاربران";
+ ViewData["WideShell"] = true;
+}
+
+
+
+@if (!string.IsNullOrEmpty(Model.LastError))
+{
+ @Model.LastError
+}
+
+
+
+
+
+
+
+@if (Model.Users != null && Model.Users.Length > 0)
+{
+
+}
+else
+{
+
+}
+
+
diff --git a/GanjooRazor/Areas/Admin/Pages/Users.cshtml.cs b/GanjooRazor/Areas/Admin/Pages/Users.cshtml.cs
new file mode 100644
index 00000000..5651ebfc
--- /dev/null
+++ b/GanjooRazor/Areas/Admin/Pages/Users.cshtml.cs
@@ -0,0 +1,216 @@
+using GanjooRazor.Utils;
+using GSpotifyProxy.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Newtonsoft.Json;
+using RSecurityBackend.Models.Auth.Db;
+using RSecurityBackend.Models.Auth.ViewModels;
+using RSecurityBackend.Models.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GanjooRazor.Areas.Admin.Pages
+{
+ [IgnoreAntiforgeryToken(Order = 1001)]
+ public class UsersModel : PageModel
+ {
+ ///
+ /// Last Error
+ ///
+ public string LastError { get; set; }
+
+ public PublicRAppUser[] Users { get; set; }
+
+ public PaginationMetadata Paging { get; set; }
+
+ public List PaginationLinks { get; set; } = new List();
+
+ public int PageNumber { get; set; } = 1;
+
+ public int PageSize { get; set; } = 15;
+
+ public string FilterByEmail { get; set; }
+
+ public async Task OnGetAsync(int pageNumber = 1, int pageSize = 15, string filterByEmail = null)
+ {
+ if (string.IsNullOrEmpty(Request.Cookies["Token"]))
+ return Redirect("/");
+
+ PageNumber = pageNumber < 1 ? 1 : pageNumber;
+ PageSize = pageSize;
+ FilterByEmail = filterByEmail;
+
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ {
+ var url = $"{APIRoot.Url}/api/users?PageNumber={PageNumber}&PageSize={PageSize}";
+ if (!string.IsNullOrEmpty(FilterByEmail))
+ url += $"&filterByEmail={Uri.EscapeDataString(FilterByEmail)}";
+
+ var response = await secureClient.GetAsync(url);
+ if (response.IsSuccessStatusCode)
+ {
+ Users = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ if (response.Headers.TryGetValues("paging-headers", out var values))
+ {
+ Paging = JsonConvert.DeserializeObject(values.FirstOrDefault());
+ if (Paging != null)
+ {
+ for (int p = 1; p <= Paging.totalPages; p++)
+ {
+ var pageUrl = $"?pageNumber={p}&pageSize={PageSize}";
+ if (!string.IsNullOrEmpty(FilterByEmail))
+ pageUrl += $"&filterByEmail={Uri.EscapeDataString(FilterByEmail)}";
+ PaginationLinks.Add(new NameIdUrlImage()
+ {
+ Name = p.ToString(),
+ Url = p == PageNumber ? "" : pageUrl
+ });
+ }
+ }
+ }
+ }
+ else
+ {
+ LastError = await response.Content.ReadAsStringAsync();
+ }
+ }
+ else
+ {
+ LastError = "لطفاً از گنجور خارج و مجددا به آن وارد شوید.";
+ }
+ }
+
+ return Page();
+ }
+
+ ///
+ /// role names for a specific user - ?handler=Roles&id=...
+ ///
+ public async Task OnGetRolesAsync(Guid id)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var response = await secureClient.GetAsync($"{APIRoot.Url}/api/users/{id}/roles");
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ var roles = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ return new JsonResult(roles);
+ }
+ }
+
+ ///
+ /// all defined role names, for populating the "add role" picker - ?handler=AllRoles
+ ///
+ public async Task OnGetAllRolesAsync()
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var response = await secureClient.GetAsync($"{APIRoot.Url}/api/roles");
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ var roles = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ return new JsonResult(roles.Select(r => r.Name).ToArray());
+ }
+ }
+
+ ///
+ /// add a role to a user - ?handler=AddRole
+ ///
+ public async Task OnPostAddRoleAsync(Guid id, string role)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var response = await secureClient.PostAsync($"{APIRoot.Url}/api/users/{id}/roles/{Uri.EscapeDataString(role)}", null);
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ return new JsonResult(true);
+ }
+ }
+
+ ///
+ /// remove a role from a user - ?handler=Role (DELETE)
+ ///
+ public async Task OnDeleteRoleAsync(Guid id, string role)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var response = await secureClient.DeleteAsync($"{APIRoot.Url}/api/users/{id}/roles/{Uri.EscapeDataString(role)}");
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ return new JsonResult(true);
+ }
+ }
+
+ ///
+ /// toggle a user's active/inactive status - ?handler=ToggleStatus
+ ///
+ public async Task OnPostToggleStatusAsync(Guid id, bool active)
+ {
+ using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
+ {
+ if (!await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
+ return BadRequest("لطفاً از گنجور خارج و مجددا به آن وارد شوید.");
+
+ var userInfoResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/users/{id}");
+ if (!userInfoResponse.IsSuccessStatusCode)
+ return BadRequest(await userInfoResponse.Content.ReadAsStringAsync());
+
+ var userInfo = JsonConvert.DeserializeObject(await userInfoResponse.Content.ReadAsStringAsync());
+
+ // ModifyUser rejects the request unless IsAdmin exactly matches the user's actual
+ // current admin-role membership (this endpoint cannot itself change admin status),
+ // so we need their real membership, not a guess.
+ var rolesResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/users/{id}/roles");
+ if (!rolesResponse.IsSuccessStatusCode)
+ return BadRequest(await rolesResponse.Content.ReadAsStringAsync());
+ var currentRoles = JsonConvert.DeserializeObject(await rolesResponse.Content.ReadAsStringAsync());
+ bool isCurrentlyAdmin = currentRoles.Contains("Administrator");
+
+ var updateModel = new RegisterRAppUser()
+ {
+ Password = "",
+ IsAdmin = isCurrentlyAdmin,
+ Id = userInfo.Id,
+ Username = userInfo.Username,
+ Email = userInfo.Email,
+ PhoneNumber = userInfo.PhoneNumber,
+ FirstName = userInfo.FirstName,
+ SurName = userInfo.SurName,
+ Status = active ? RAppUserStatus.Active : RAppUserStatus.Inactive,
+ RImageId = userInfo.RImageId,
+ NickName = userInfo.NickName,
+ Bio = userInfo.Bio,
+ Website = userInfo.Website
+ };
+
+ var response = await secureClient.PutAsync($"{APIRoot.Url}/api/users/{id}", new StringContent(JsonConvert.SerializeObject(updateModel), Encoding.UTF8, "application/json"));
+ if (!response.IsSuccessStatusCode)
+ return BadRequest(await response.Content.ReadAsStringAsync());
+
+ return new JsonResult(true);
+ }
+ }
+ }
+}
diff --git a/GanjooRazor/Pages/Shared/_AdminLayout.cshtml b/GanjooRazor/Pages/Shared/_AdminLayout.cshtml
index ee990074..2c631c56 100644
--- a/GanjooRazor/Pages/Shared/_AdminLayout.cshtml
+++ b/GanjooRazor/Pages/Shared/_AdminLayout.cshtml
@@ -498,6 +498,14 @@
+
+ کاربران و نقشها
+
+
+
diff --git a/GanjooRazor/wwwroot/css/user-panel.css b/GanjooRazor/wwwroot/css/user-panel.css
index 491078d4..0c6fd6e2 100644
--- a/GanjooRazor/wwwroot/css/user-panel.css
+++ b/GanjooRazor/wwwroot/css/user-panel.css
@@ -713,7 +713,6 @@ body.up-modal-open {
}
.up-verse-number {
- display: inline-flex;
align-items: center;
justify-content: center;
min-width: 26px;
@@ -788,3 +787,57 @@ body.up-modal-open {
color: var(--up-link-hover, #0056b3);
text-decoration: none;
}
+
+/* ---------- role chips (Users.cshtml) ---------- */
+
+.up-role-chip-row {
+ display: flex;
+ align-items: center;
+ gap: var(--up-space-2);
+ flex-wrap: wrap;
+ padding: var(--up-space-2) 0;
+}
+
+.up-role-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: 999px;
+ background: var(--up-surface-alt);
+ border: 1px solid var(--up-border);
+ font-size: 0.85rem;
+}
+
+.up-role-chip__remove {
+ cursor: pointer;
+ color: var(--up-text-muted);
+ font-size: 0.75rem;
+}
+
+.up-role-chip__remove:hover {
+ color: var(--up-danger, #c0392b);
+}
+
+/* ---------- permission tree (Roles.cshtml) ---------- */
+
+.up-perm-tree {
+ text-align: right;
+ margin-bottom: var(--up-space-3);
+}
+
+.up-perm-tree__item {
+ margin-top: var(--up-space-3);
+}
+
+.up-perm-tree__item:first-child {
+ margin-top: 0;
+}
+
+.up-perm-tree__ops {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--up-space-3);
+ margin-top: var(--up-space-1);
+ padding-right: var(--up-space-3);
+}