divan/GanjooRazor/Areas/User/Pages/MySessions.cshtml.cs
2026-09-27 15:49:07 +03:30

154 lines
6.6 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
public class MySessionsModel : GanjoorPageModelBase
{
/// <summary>
/// Last Error
/// </summary>
public string LastError { get; set; }
/// <summary>
/// the user's own sessions, most recently active first
/// </summary>
public List<PublicRUserSession> Sessions { get; set; }
/// <summary>
/// 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.
/// </summary>
public Guid CurrentSessionId { get; set; }
public MySessionsModel(HttpClient httpClient) : base(httpClient)
{
}
public async Task<IActionResult> 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<List<PublicRUserSession>>()
.OrderByDescending(s => s.LastRenewal)
.ToList();
}
else
{
LastError = NotLoggedInMessage;
}
return Page();
}
/// <summary>
/// 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.
/// </summary>
public async Task<IActionResult> 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 });
}
/// <summary>
/// Logs the user out of every session but this one (a critical-account cleanup shortcut
/// for someone logged in on several computers, instead of removing each session one at a
/// time). This browser's own session is always the one preserved - it never removes the
/// session the request itself is authenticated with, so unlike OnDeleteSessionAsync there
/// is no "logged out myself" case to special-case here.
/// </summary>
public async Task<IActionResult> OnDeleteLogoutOthersAsync()
{
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/delothersessions?userId={Request.Cookies["UserId"]}");
if (response.StatusCode != HttpStatusCode.OK)
{
return new BadRequestObjectResult(await ReadErrorMessageAsync(response));
}
int removedCount = int.Parse(await response.Content.ReadAsStringAsync());
return new JsonResult(new { removedCount });
}
else
{
return new BadRequestObjectResult(NotLoggedInMessage);
}
}
}
}
}