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);
+}
+
+@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 @@
اعلانهای من
+
+ نشستهای من
+
+
مشارکتهای من