From 226f7c3370b679a3f219e81235b2a861bff2dff5 Mon Sep 17 00:00:00 2001 From: Hamid Reza Mohammadi Date: Sat, 26 Sep 2026 19:11:07 +0330 Subject: [PATCH] user sessions --- GanjooRazor/Areas/User/Pages/Index.cshtml | 4 + .../Areas/User/Pages/MySessions.cshtml | 84 ++++++++++++ .../Areas/User/Pages/MySessions.cshtml.cs | 126 ++++++++++++++++++ .../Pages/Shared/_UserPanelLayout.cshtml | 4 + 4 files changed, 218 insertions(+) create mode 100644 GanjooRazor/Areas/User/Pages/MySessions.cshtml create mode 100644 GanjooRazor/Areas/User/Pages/MySessions.cshtml.cs diff --git a/GanjooRazor/Areas/User/Pages/Index.cshtml b/GanjooRazor/Areas/User/Pages/Index.cshtml index 7aad3f0d..772d9b69 100644 --- a/GanjooRazor/Areas/User/Pages/Index.cshtml +++ b/GanjooRazor/Areas/User/Pages/Index.cshtml @@ -47,6 +47,10 @@ else مشق‌های پیشنهادی من اشعاری که به عنوان مشق پیشنهاد داده‌اید + + نشست‌های من + دستگاه‌های واردشده به حساب شما و خروج از هر یک از آنها +
diff --git a/GanjooRazor/Areas/User/Pages/MySessions.cshtml b/GanjooRazor/Areas/User/Pages/MySessions.cshtml new file mode 100644 index 00000000..48d6a128 --- /dev/null +++ b/GanjooRazor/Areas/User/Pages/MySessions.cshtml @@ -0,0 +1,84 @@ +@page +@model GanjooRazor.Areas.User.Pages.MySessionsModel +@using DNTPersianUtils.Core +@{ + Layout = "_UserPanelLayout"; + ViewData["Title"] = "نشست‌های من"; + await GanjooRazor.Utils.GanjoorSessionChecker.ApplyPermissionsToViewData(Request, Response, ViewData); +} +
+

@ViewData["Title"]

+
دستگاه‌ها و مرورگرهایی که هم‌اکنون با حساب کاربری شما وارد گنجور شده‌اند. اگر دستگاهی را نمی‌شناسید یا دیگر از آن استفاده نمی‌کنید، آن را از این فهرست خارج کنید.
+
+@if (Model.LastError != "") +{ +
@Model.LastError
+} +else +{ + + + @if (Model.Sessions.Count == 0) + { + + } + else + { +
+ @foreach (var session in Model.Sessions) + { + bool isCurrent = session.Id == Model.CurrentSessionId; +
+
+ @(string.IsNullOrEmpty(session.ClientAppName) ? "برنامهٔ نامشخص" : session.ClientAppName) + @if (isCurrent) + { + این دستگاه + } +
+ @session.ClientIPAddress +
+
+ ورود: @session.LoginTime.ToLongPersianDateTimeString() +  ·  + آخرین تمدید: @session.LastRenewal.ToLongPersianDateTimeString() +  ·  + معتبر تا: @session.ValidUntil.ToLongPersianDateTimeString() +
+
+
+ + + +
+
+ } +
+ } +} diff --git a/GanjooRazor/Areas/User/Pages/MySessions.cshtml.cs b/GanjooRazor/Areas/User/Pages/MySessions.cshtml.cs new file mode 100644 index 00000000..736a68bf --- /dev/null +++ b/GanjooRazor/Areas/User/Pages/MySessions.cshtml.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using GanjooRazor.Pages; +using GanjooRazor.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; +using RSecurityBackend.Models.Auth.ViewModels; + +namespace GanjooRazor.Areas.User.Pages +{ + /// + /// Lets a logged-on user see every session (device/browser) currently able to use their + /// account, and revoke any one of them - the self-service alternative to a hard absolute + /// session ceiling: instead of forcing everyone to relogin periodically, the user can look at + /// this list and kill a session they don't recognize (a lost/stolen device, a shared computer + /// they forgot to log out of, ...) whenever they suspect something. + /// + [IgnoreAntiforgeryToken(Order = 1001)] + public class MySessionsModel : GanjoorPageModelBase + { + /// + /// Last Error + /// + public string LastError { get; set; } + + /// + /// the user's own sessions, most recently active first + /// + public List Sessions { get; set; } + + /// + /// the SessionId cookie of the browser rendering this page, so the view can mark it as + /// "this device" and the delete handler can special-case removing it. + /// + public Guid CurrentSessionId { get; set; } + + public MySessionsModel(HttpClient httpClient) : base(httpClient) + { + } + + public async Task OnGetAsync() + { + if (string.IsNullOrEmpty(Request.Cookies["Token"])) + return Redirect("/"); + + Guid.TryParse(Request.Cookies["SessionId"], out Guid currentSessionId); + CurrentSessionId = currentSessionId; + + LastError = ""; + using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response))) + if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response)) + { + var response = await secureClient.GetAsync($"{APIRoot.Url}/api/users/sessions?userId={Request.Cookies["UserId"]}"); + if (!response.IsSuccessStatusCode) + { + LastError = await ReadErrorMessageAsync(response); + return Page(); + } + + Sessions = JArray.Parse(await response.Content.ReadAsStringAsync()) + .ToObject>() + .OrderByDescending(s => s.LastRenewal) + .ToList(); + } + else + { + LastError = NotLoggedInMessage; + } + return Page(); + } + + /// + /// Deletes one of the user's own sessions (the API only allows deleting your own session + /// here without the extra user:delothersession permission - see AppUserControllerBase.Logout). + /// If the session being deleted is the one this very request is authenticated with, this + /// browser's auth cookies are cleared too (mirroring LoginPartialEnabledPageModel's + /// OnPostLogoutAsync), so it doesn't keep showing a "logged in" page for a session that no + /// longer exists server-side; the caller (see the view) then redirects home instead of just + /// removing the row from the list. + /// + public async Task OnDeleteSessionAsync(Guid id) + { + using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response))) + { + if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response)) + { + var response = await secureClient.DeleteAsync($"{APIRoot.Url}/api/users/delsession?userId={Request.Cookies["UserId"]}&sessionId={id}"); + if (response.StatusCode != HttpStatusCode.OK) + { + return new BadRequestObjectResult(await ReadErrorMessageAsync(response)); + } + } + else + { + return new BadRequestObjectResult(NotLoggedInMessage); + } + } + + bool loggedOutSelf = Request.Cookies["SessionId"] == id.ToString(); + if (loggedOutSelf) + { + var cookieOption = new CookieOptions() + { + Expires = DateTime.Now.AddDays(-1), + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Lax, + }; + foreach (var cookieName in new string[] { "UserId", "SessionId", "Token", "Username", "Name", "NickName", "CanEdit", "KeepHistory", "CanTranslate" }) + { + if (Request.Cookies[cookieName] != null) + { + Response.Cookies.Append(cookieName, "", cookieOption); + } + } + } + + return new JsonResult(new { loggedOutSelf }); + } + } +} diff --git a/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml b/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml index 443c06e7..afaf056c 100644 --- a/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml +++ b/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml @@ -102,6 +102,10 @@ اعلان‌‌های من + +