feat: add blog admin page and simple login
This commit is contained in:
parent
f0aa1c0ae9
commit
8fda2e9907
|
@ -0,0 +1,78 @@
|
|||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Services;
|
||||
using ISession = OliverBooth.Data.Blog.ISession;
|
||||
|
||||
namespace OliverBooth.Controllers.Blog;
|
||||
|
||||
[Controller]
|
||||
[Route("auth/admin")]
|
||||
public sealed class AdminController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<AdminController> _logger;
|
||||
private readonly IBlogUserService _userService;
|
||||
private readonly ISessionService _sessionService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AdminController" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <param name="sessionService">The session service.</param>
|
||||
public AdminController(ILogger<AdminController> logger,
|
||||
IBlogUserService userService,
|
||||
ISessionService sessionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
_sessionService = sessionService;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public IActionResult Login()
|
||||
{
|
||||
string? loginEmail = Request.Form["login-email"];
|
||||
string? loginPassword = Request.Form["login-password"];
|
||||
IPAddress? remoteIpAddress = Request.HttpContext.Connection.RemoteIpAddress;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(loginEmail))
|
||||
{
|
||||
_logger.LogInformation("Login attempt from {Host} with empty login", remoteIpAddress);
|
||||
return RedirectToPage("/blog/admin/login");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(loginPassword))
|
||||
{
|
||||
_logger.LogInformation("Login attempt as '{Email}' from {Host} with empty password", loginEmail,
|
||||
remoteIpAddress);
|
||||
return RedirectToPage("/blog/admin/login");
|
||||
}
|
||||
|
||||
if (_userService.VerifyLogin(loginEmail, loginPassword, out IUser? user))
|
||||
{
|
||||
_logger.LogInformation("Login attempt for '{Email}' succeeded from {Host}", loginEmail, remoteIpAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Login attempt for '{Email}' failed from {Host}", loginEmail, remoteIpAddress);
|
||||
return RedirectToPage("/blog/admin/login");
|
||||
}
|
||||
|
||||
ISession session = _sessionService.CreateSession(Request, user);
|
||||
Span<byte> sessionBytes = stackalloc byte[16];
|
||||
session.Id.TryWriteBytes(sessionBytes);
|
||||
Response.Cookies.Append("sid", Convert.ToBase64String(sessionBytes));
|
||||
return RedirectToPage("/blog/admin/index");
|
||||
}
|
||||
|
||||
[HttpGet("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
if (_sessionService.TryGetSession(Request, out ISession? session, true))
|
||||
_sessionService.DeleteSession(session);
|
||||
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("/blog/admin/login");
|
||||
}
|
||||
}
|
|
@ -25,6 +25,12 @@ internal sealed class BlogContext : DbContext
|
|||
/// <value>The collection of blog posts.</value>
|
||||
public DbSet<BlogPost> BlogPosts { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of sessions in the database.
|
||||
/// </summary>
|
||||
/// <value>The collection of sessions.</value>
|
||||
public DbSet<Session> Sessions { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of users in the database.
|
||||
/// </summary>
|
||||
|
@ -43,6 +49,7 @@ internal sealed class BlogContext : DbContext
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new BlogPostConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SessionConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new UserConfiguration());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace OliverBooth.Data.Blog.Configuration;
|
||||
|
||||
internal sealed class SessionConfiguration : IEntityTypeConfiguration<Session>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<Session> builder)
|
||||
{
|
||||
builder.ToTable("Session");
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.Property(e => e.Id).IsRequired();
|
||||
builder.Property(e => e.Created).IsRequired();
|
||||
builder.Property(e => e.Updated).IsRequired();
|
||||
builder.Property(e => e.LastAccessed).IsRequired();
|
||||
builder.Property(e => e.Expires).IsRequired();
|
||||
builder.Property(e => e.UserId);
|
||||
builder.Property(e => e.IpAddress).HasConversion<IPAddressToBytesConverter>().IsRequired();
|
||||
builder.Property(e => e.RequiresTotp).IsRequired();
|
||||
}
|
||||
}
|
|
@ -17,5 +17,6 @@ internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
|||
builder.Property(e => e.Password).HasMaxLength(255).IsRequired();
|
||||
builder.Property(e => e.Salt).HasMaxLength(255).IsRequired();
|
||||
builder.Property(e => e.Registered).IsRequired();
|
||||
builder.Property(e => e.Totp);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
using System.Net;
|
||||
|
||||
namespace OliverBooth.Data.Blog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a login session.
|
||||
/// </summary>
|
||||
public interface ISession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the date and time at which this session was created.
|
||||
/// </summary>
|
||||
/// <value>The creation timestamp.</value>
|
||||
DateTimeOffset Created { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date and time at which this session expires.
|
||||
/// </summary>
|
||||
/// <value>The expiration timestamp.</value>
|
||||
DateTimeOffset Expires { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the session.
|
||||
/// </summary>
|
||||
/// <value>The ID of the session.</value>
|
||||
Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IP address of the session.
|
||||
/// </summary>
|
||||
/// <value>The IP address.</value>
|
||||
IPAddress IpAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date and time at which this session was last accessed.
|
||||
/// </summary>
|
||||
/// <value>The last access timestamp.</value>
|
||||
DateTimeOffset LastAccessed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this session is valid.
|
||||
/// </summary>
|
||||
/// <value><see langword="true" /> if the session is valid; otherwise, <see langword="false" />.</value>
|
||||
bool RequiresTotp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date and time at which this session was updated.
|
||||
/// </summary>
|
||||
/// <value>The update timestamp.</value>
|
||||
DateTimeOffset Updated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user ID associated with the session.
|
||||
/// </summary>
|
||||
/// <value>The user ID.</value>
|
||||
Guid UserId { get; }
|
||||
}
|
||||
|
||||
internal sealed class Session : ISession
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset Expires { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; private set; } = Guid.NewGuid();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPAddress IpAddress { get; set; } = IPAddress.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset LastAccessed { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RequiresTotp { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset Updated { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid UserId { get; set; }
|
||||
}
|
|
@ -35,6 +35,12 @@ public interface IUser
|
|||
/// <value>The registration date and time.</value>
|
||||
DateTimeOffset Registered { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user's TOTP token.
|
||||
/// </summary>
|
||||
/// <value>The TOTP token.</value>
|
||||
string? Totp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL of the user's avatar.
|
||||
/// </summary>
|
||||
|
|
|
@ -26,6 +26,9 @@ internal sealed class User : IUser, IBlogAuthor
|
|||
/// <inheritdoc />
|
||||
public DateTimeOffset Registered { get; private set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Totp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password hash.
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
@page
|
||||
@model OliverBooth.Pages.Blog.Admin.Index
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Admin";
|
||||
}
|
||||
|
||||
<h1>Hello @(Model.CurrentUser.DisplayName)!</h1>
|
||||
<a asp-controller="Admin" asp-action="Logout">Logout</a>
|
|
@ -0,0 +1,81 @@
|
|||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Services;
|
||||
using ISession = OliverBooth.Data.Blog.ISession;
|
||||
|
||||
namespace OliverBooth.Pages.Blog.Admin;
|
||||
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IBlogUserService _userService;
|
||||
private readonly ISessionService _sessionService;
|
||||
|
||||
public Index(IBlogUserService userService, ISessionService sessionService)
|
||||
{
|
||||
_userService = userService;
|
||||
_sessionService = sessionService;
|
||||
}
|
||||
|
||||
public IUser CurrentUser { get; private set; } = null!;
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
IPAddress? remoteIpAddress = Request.HttpContext.Connection.RemoteIpAddress;
|
||||
if (remoteIpAddress is null)
|
||||
{
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
if (!Request.Cookies.TryGetValue("sid", out string? sessionIdCookie))
|
||||
{
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
if (!Convert.TryFromBase64Chars(sessionIdCookie, bytes, out int bytesWritten) || bytesWritten < 16)
|
||||
{
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
var sessionId = new Guid(bytes);
|
||||
if (!_sessionService.TryGetSession(sessionId, out ISession? session))
|
||||
{
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
if (session.Expires <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
_sessionService.DeleteSession(session);
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
Span<byte> remoteAddressBytes = stackalloc byte[16];
|
||||
Span<byte> sessionAddressBytes = stackalloc byte[16];
|
||||
if (!remoteIpAddress.TryWriteBytes(remoteAddressBytes, out _) ||
|
||||
!session.IpAddress.TryWriteBytes(sessionAddressBytes, out _))
|
||||
{
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
if (!remoteAddressBytes.SequenceEqual(sessionAddressBytes))
|
||||
{
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
if (!_userService.TryGetUser(session.UserId, out IUser? user))
|
||||
{
|
||||
Response.Cookies.Delete("sid");
|
||||
return RedirectToPage("login");
|
||||
}
|
||||
|
||||
CurrentUser = user;
|
||||
return Page();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
@page
|
||||
@model OliverBooth.Pages.Blog.Admin.Login
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Admin";
|
||||
}
|
||||
|
||||
<div style="width: 256px; margin: 0 auto;">
|
||||
<form method="post" asp-controller="Admin" asp-action="Login">
|
||||
<div class="form-outline mb-4">
|
||||
<label class="sr-only" for="login-email">Email address</label>
|
||||
<input id="login-email" name="login-email" type="email" class="form-control" placeholder="Email address">
|
||||
</div>
|
||||
|
||||
<div class="form-outline mb-4">
|
||||
<label class="sr-only" for="login-password">Password</label>
|
||||
<input id="login-password" name="login-password" type="password" class="form-control" placeholder="Password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using OtpNet;
|
||||
using QRCoder;
|
||||
|
||||
namespace OliverBooth.Pages.Blog.Admin;
|
||||
|
||||
public class Login : PageModel
|
||||
{
|
||||
public string QrCode { get; set; }
|
||||
|
||||
public string Secret { get; set; }
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if (Request.Cookies.ContainsKey("sid"))
|
||||
{
|
||||
return RedirectToPage("index");
|
||||
}
|
||||
|
||||
Secret = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));
|
||||
|
||||
var uri = $"otpauth://totp/oliverbooth.dev?secret={Secret}";
|
||||
var generator = new QRCodeGenerator();
|
||||
QRCodeData qrCodeData = generator.CreateQrCode(uri, QRCodeGenerator.ECCLevel.Q);
|
||||
using var pngByteQrCode = new PngByteQRCode(qrCodeData);
|
||||
byte[] data = pngByteQrCode.GetGraphic(20);
|
||||
QrCode = Convert.ToBase64String(data);
|
||||
return Page();
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@ builder.Services.AddSingleton<IContactService, ContactService>();
|
|||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
||||
builder.Services.AddSingleton<ISessionService, SessionService>();
|
||||
builder.Services.AddSingleton<IProjectService, ProjectService>();
|
||||
builder.Services.AddSingleton<IReadingListService, ReadingListService>();
|
||||
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Concurrent;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data.Blog;
|
||||
using BC = BCrypt.Net.BCrypt;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
|
@ -35,4 +36,12 @@ internal sealed class BlogUserService : IBlogUserService
|
|||
if (user is not null) _userCache.TryAdd(id, user);
|
||||
return user is not null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool VerifyLogin(string email, string password, [NotNullWhen(true)] out IUser? user)
|
||||
{
|
||||
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||
user = context.Users.FirstOrDefault(u => u.EmailAddress == email);
|
||||
return user is not null && BC.Verify(password, ((User)user).Password);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,4 +20,18 @@ public interface IBlogUserService
|
|||
/// <see langword="true" /> if a user with the specified ID is found; otherwise, <see langword="false" />.
|
||||
/// </returns>
|
||||
bool TryGetUser(Guid id, [NotNullWhen(true)] out IUser? user);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the login information of the specified user.
|
||||
/// </summary>
|
||||
/// <param name="email">The email address.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <param name="user">
|
||||
/// When this method returns, contains the user associated with the login credentials, or
|
||||
/// <see langword="null" /> if the credentials are invalid.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if the login credentials are valid; otherwise, <see langword="false" />.
|
||||
/// </returns>
|
||||
public bool VerifyLogin(string email, string password, [NotNullWhen(true)] out IUser? user);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using OliverBooth.Data.Blog;
|
||||
using ISession = OliverBooth.Data.Blog.ISession;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
public interface ISessionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new session for the specified user.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>The newly-created session.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="request" /> is <see langword="null" />.</para>
|
||||
/// -or-
|
||||
/// <para><paramref name="user" /> is <see langword="null" />.</para>
|
||||
/// </exception>
|
||||
ISession CreateSession(HttpRequest request, IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified session.
|
||||
/// </summary>
|
||||
/// <param name="session">The session to delete.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="session" /> is <see langword="null" />.</exception>
|
||||
void DeleteSession(ISession session);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find a session with the specified ID.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID.</param>
|
||||
/// <param name="session">
|
||||
/// When this method returns, contains the session with the specified ID, if the session is found; otherwise,
|
||||
/// <see langword="null" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if a session with the specified ID is found; otherwise, <see langword="false" />.
|
||||
/// </returns>
|
||||
bool TryGetSession(Guid sessionId, [NotNullWhen(true)] out ISession? session);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find the session associated with the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <param name="session">
|
||||
/// When this method returns, contains the session with the specified request, if the user is found; otherwise,
|
||||
/// <see langword="null" />.
|
||||
/// </param>
|
||||
/// <param name="includeInvalid">
|
||||
/// <see langword="true" /> to include invalid sessions in the search; otherwise, <see langword="false" />.
|
||||
/// </param>
|
||||
/// <returns><see langword="true" /> if the session was found; otherwise, <see langword="false" />.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="request" /> is <see langword="null" />.</exception>
|
||||
bool TryGetSession(HttpRequest request, [NotNullWhen(true)] out ISession? session, bool includeInvalid = false);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Data.Web;
|
||||
using ISession = OliverBooth.Data.Blog.ISession;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
internal sealed class SessionService : ISessionService
|
||||
{
|
||||
private readonly ILogger<SessionService> _logger;
|
||||
private readonly IBlogUserService _userService;
|
||||
private readonly IDbContextFactory<BlogContext> _blogContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SessionService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <param name="blogContextFactory">The <see cref="BlogContext" /> factory.</param>
|
||||
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||
public SessionService(ILogger<SessionService> logger,
|
||||
IBlogUserService userService,
|
||||
IDbContextFactory<BlogContext> blogContextFactory,
|
||||
IDbContextFactory<WebContext> webContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
_blogContextFactory = blogContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISession CreateSession(HttpRequest request, IUser user)
|
||||
{
|
||||
if (request is null) throw new ArgumentNullException(nameof(request));
|
||||
if (user is null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
using BlogContext context = _blogContextFactory.CreateDbContext();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var session = new Session
|
||||
{
|
||||
UserId = user.Id,
|
||||
IpAddress = request.HttpContext.Connection.RemoteIpAddress!,
|
||||
Created = now,
|
||||
Updated = now,
|
||||
LastAccessed = now,
|
||||
Expires = now + TimeSpan.FromDays(1),
|
||||
RequiresTotp = !string.IsNullOrWhiteSpace(user.Totp)
|
||||
};
|
||||
EntityEntry<Session> entry = context.Sessions.Add(session);
|
||||
context.SaveChanges();
|
||||
return entry.Entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteSession(ISession session)
|
||||
{
|
||||
using BlogContext context = _blogContextFactory.CreateDbContext();
|
||||
context.Sessions.Remove((Session)session);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetSession(Guid sessionId, [NotNullWhen(true)] out ISession? session)
|
||||
{
|
||||
using BlogContext context = _blogContextFactory.CreateDbContext();
|
||||
session = context.Sessions.FirstOrDefault(s => s.Id == sessionId);
|
||||
return session is not null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetSession(HttpRequest request, [NotNullWhen(true)] out ISession? session,
|
||||
bool includeInvalid = false)
|
||||
{
|
||||
if (request is null) throw new ArgumentNullException(nameof(request));
|
||||
|
||||
session = null;
|
||||
IPAddress? remoteIpAddress = request.HttpContext.Connection.RemoteIpAddress;
|
||||
if (remoteIpAddress is null) return false;
|
||||
|
||||
if (!request.Cookies.TryGetValue("sid", out string? sessionIdCookie))
|
||||
return false;
|
||||
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
if (!Convert.TryFromBase64Chars(sessionIdCookie, bytes, out int bytesWritten) || bytesWritten < 16)
|
||||
return false;
|
||||
|
||||
var sessionId = new Guid(bytes);
|
||||
if (!TryGetSession(sessionId, out session))
|
||||
return false;
|
||||
|
||||
if (!includeInvalid && session.Expires >= DateTimeOffset.UtcNow)
|
||||
{
|
||||
session = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> remoteAddressBytes = stackalloc byte[16];
|
||||
Span<byte> sessionAddressBytes = stackalloc byte[16];
|
||||
if (!remoteIpAddress.TryWriteBytes(remoteAddressBytes, out _) ||
|
||||
!session.IpAddress.TryWriteBytes(sessionAddressBytes, out _))
|
||||
{
|
||||
session = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!includeInvalid && !remoteAddressBytes.SequenceEqual(sessionAddressBytes))
|
||||
{
|
||||
session = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!includeInvalid && _userService.TryGetUser(session.UserId, out _))
|
||||
{
|
||||
session = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue