106 lines
4.2 KiB
C#
106 lines
4.2 KiB
C#
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace JobTrackerApi.Controllers;
|
|
|
|
// List/revoke the server-tracked UserSession rows behind the JWTs AppSessionIssuer hands out.
|
|
// Not 2FA-specific (any local-auth user has sessions, 2FA or not), hence its own small controller
|
|
// rather than folding into TwoFactorController.
|
|
[ApiController]
|
|
[Route("api/auth/sessions")]
|
|
[Authorize(AuthenticationSchemes = "local")]
|
|
public sealed class SessionsController : ControllerBase
|
|
{
|
|
private readonly UserManager<ApplicationUser> _users;
|
|
private readonly JobTrackerContext _db;
|
|
private readonly ExternalOrigin _externalOrigin;
|
|
|
|
public SessionsController(UserManager<ApplicationUser> users, JobTrackerContext db, ExternalOrigin? externalOrigin = null)
|
|
{
|
|
_users = users;
|
|
_db = db;
|
|
_externalOrigin = externalOrigin ?? ExternalOrigin.Parse(null, production: false);
|
|
}
|
|
|
|
public sealed record SessionDto(string Id, string? DeviceLabel, DateTimeOffset CreatedAtUtc, DateTimeOffset LastSeenAtUtc, DateTimeOffset ExpiresAtUtc, bool IsCurrentSession);
|
|
|
|
private string? CurrentSid => User.FindFirst("sid")?.Value;
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
|
{
|
|
var user = await _users.GetUserAsync(User);
|
|
if (user is null) return Unauthorized();
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
var currentSid = CurrentSid;
|
|
// IgnoreQueryFilters + an explicit UserId filter, same convention as
|
|
// TrustedDeviceService/TwoFactorController's device-list queries.
|
|
// Equality-only in the DB query, then filter/sort DateTimeOffset client-side: SQLite's EF
|
|
// Core provider cannot translate ">" or ORDER BY over DateTimeOffset to SQL ("SQLite does
|
|
// not support expressions of type 'DateTimeOffset' in ORDER BY clauses"), so ExpiresAtUtc
|
|
// comparison and the LastSeenAtUtc sort have to happen after materializing the (small,
|
|
// per-user) row set.
|
|
var candidates = await _db.UserSessions.IgnoreQueryFilters()
|
|
.Where(x => x.UserId == user.Id && x.RevokedAtUtc == null)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var sessions = candidates
|
|
.Where(x => x.ExpiresAtUtc > now)
|
|
.OrderByDescending(x => x.LastSeenAtUtc)
|
|
.Select(x => new SessionDto(x.Id, x.DeviceLabel, x.CreatedAtUtc, x.LastSeenAtUtc, x.ExpiresAtUtc, x.Id == currentSid))
|
|
.ToList();
|
|
|
|
return Ok(sessions);
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
public async Task<IActionResult> Revoke(string id, CancellationToken cancellationToken)
|
|
{
|
|
var user = await _users.GetUserAsync(User);
|
|
if (user is null) return Unauthorized();
|
|
|
|
var session = await _db.UserSessions.IgnoreQueryFilters().FirstOrDefaultAsync(x => x.Id == id && x.UserId == user.Id, cancellationToken);
|
|
if (session is null) return NotFound();
|
|
|
|
session.RevokedAtUtc = DateTimeOffset.UtcNow;
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
if (string.Equals(id, CurrentSid, StringComparison.Ordinal))
|
|
{
|
|
Response.Cookies.Delete(AuthSessionOptions.SessionCookieName, AuthSessionOptions.BuildExpiredCookie(_externalOrigin.UsesHttps));
|
|
}
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpPost("revoke-others")]
|
|
public async Task<IActionResult> RevokeOthers(CancellationToken cancellationToken)
|
|
{
|
|
var user = await _users.GetUserAsync(User);
|
|
if (user is null) return Unauthorized();
|
|
|
|
var currentSid = CurrentSid;
|
|
var now = DateTimeOffset.UtcNow;
|
|
var others = await _db.UserSessions.IgnoreQueryFilters()
|
|
.Where(x => x.UserId == user.Id && x.RevokedAtUtc == null && x.Id != currentSid)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
foreach (var session in others)
|
|
{
|
|
session.RevokedAtUtc = now;
|
|
}
|
|
if (others.Count > 0)
|
|
{
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
return NoContent();
|
|
}
|
|
}
|