Added simple sessions

This commit is contained in:
the1mason 2025-01-07 05:29:38 +05:00
parent ad00f6580d
commit fb77065731
8 changed files with 146 additions and 32 deletions

View File

@ -3,7 +3,9 @@ using FastBlog.Core.Abstractions.Repositories.Blogs;
using FastBlog.Core.Abstractions.Repositories.Files; using FastBlog.Core.Abstractions.Repositories.Files;
using FastBlog.Core.Abstractions.Repositories.Users; using FastBlog.Core.Abstractions.Repositories.Users;
using FastBlog.Core.Db; using FastBlog.Core.Db;
using FastBlog.Core.Models;
using FastBlog.Core.Models.Blogs; using FastBlog.Core.Models.Blogs;
using FastBlog.Core.Models.Users;
using FastBlog.Core.Options; using FastBlog.Core.Options;
using FastBlog.Core.Repositories; using FastBlog.Core.Repositories;
using FastBlog.Core.Services; using FastBlog.Core.Services;
@ -19,22 +21,22 @@ public static class DependencyInjection
public static IServiceCollection AddCore(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddCore(this IServiceCollection services, IConfiguration configuration)
{ {
// application // application
services.AddSingleton<BlogService>(); services.AddTransient<BlogService>();
services.AddSingleton<FileService>(); services.AddTransient<FileService>();
services.AddSingleton<UserService>(); services.AddTransient<UserService>();
services.AddSingleton<SessionService>(); services.AddTransient<SessionService>();
// options // options
services.Configure<FileStoreOptions>(configuration.GetSection("FileStore")); services.Configure<FileStoreOptions>(configuration.GetSection("FileStore"));
services.Configure<SessionOptions>(configuration.GetSection("Sessions")); services.Configure<SessionOptions>(configuration.GetSection("Sessions"));
// infrastructure // infrastructure
services.AddSingleton<IBlogMetaRepository, BlogMetaRepository>(); services.AddTransient<IBlogMetaRepository, BlogMetaRepository>();
services.AddSingleton<IBlogFileRepository, BlogFileRepository>(); services.AddTransient<IBlogFileRepository, BlogFileRepository>();
services.AddSingleton<IFileMetaRepository, FileMetaRepository>(); services.AddTransient<IFileMetaRepository, FileMetaRepository>();
services.AddSingleton<IFileRepository, FileRepository>(); services.AddTransient<IFileRepository, FileRepository>();
services.AddSingleton<ISessionRepository,SessionRepository>(); services.AddTransient<ISessionRepository,SessionRepository>();
services.AddSingleton<IUserRepository, UserRepository>(); services.AddTransient<IUserRepository, UserRepository>();
// storage // storage
var connectionString = configuration.GetConnectionString("Sqlite"); var connectionString = configuration.GetConnectionString("Sqlite");
@ -58,8 +60,13 @@ public static class DependencyInjection
await using var scope = provider.CreateAsyncScope(); await using var scope = provider.CreateAsyncScope();
var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>(); var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp(); runner.MigrateUp();
await TryAddMainPage(scope.ServiceProvider);
await TryAddDefaultUser(scope.ServiceProvider);
}
var blogService = scope.ServiceProvider.GetRequiredService<BlogService>(); private static async Task TryAddMainPage(IServiceProvider provider)
{
var blogService = provider.GetRequiredService<BlogService>();
var mainPage = await blogService.Get(null); var mainPage = await blogService.Get(null);
if (mainPage is not null) if (mainPage is not null)
return; return;
@ -80,6 +87,24 @@ public static class DependencyInjection
}); });
} }
private static async Task TryAddDefaultUser(this IServiceProvider provider)
{
var userService = provider.GetRequiredService<UserService>();
var existingUserResult = await userService.GetUsers(new PagedRequest(1, 0));
if (existingUserResult.Amount is 1)
return;
var newUser = new NewUser
{
Username = "admin",
Password = "admin"
};
var addResult = await userService.CreateUser(newUser);
if (addResult is null)
throw new ApplicationException("Failed to create new user!");
}
private const string BlogDefaultBody = """ private const string BlogDefaultBody = """
# Index Page # Index Page
--- ---

View File

@ -25,9 +25,9 @@ public sealed class SessionRepository(SqliteConnectionFactory connectionFactory)
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
const string sql = const string sql =
""" """
INSERT INTO Sessions (Token, UserId, Expires, Active) INSERT INTO Sessions (Token, UserId, CreatedAt, LastUsed, Active)
VALUES (@Token, @UserId, @Expires, @Active) VALUES (@Token, @UserId, @CreatedAt, @LastUsed, @Active)
ON CONFLICT(Token) DO UPDATE SET Expires = @Expires ON CONFLICT(Token) DO UPDATE SET LastUsed = @LastUsed
RETURNING * RETURNING *
"""; """;
return await connection.QueryFirstOrDefaultAsync<Session>(sql, session); return await connection.QueryFirstOrDefaultAsync<Session>(sql, session);
@ -39,12 +39,12 @@ public sealed class SessionRepository(SqliteConnectionFactory connectionFactory)
const string sql = const string sql =
""" """
UPDATE Sessions UPDATE Sessions
SET Expires = @Expires SET LastUsed = @LastUsed
WHERE Token = @Token WHERE Token = @Token
RETURNING * RETURNING *
"""; """;
return await connection.QueryFirstOrDefaultAsync<Session>(sql, return await connection.QueryFirstOrDefaultAsync<Session>(sql,
new { Token = token, Expires = DateTime.UtcNow.AddHours(1) }); new { Token = token, LastUsed = DateTime.UtcNow });
} }
public async Task<bool> Deactivate(string token) public async Task<bool> Deactivate(string token)

View File

@ -34,7 +34,6 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
const string sql = """ const string sql = """
select COUNT(*) from Users; select COUNT(*) from Users;
select * from Users select * from Users
order by CreatedAt desc
limit @Amount offset @Offset; limit @Amount offset @Offset;
"""; """;
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
@ -47,7 +46,7 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
public async Task<User> CreateUser(NewUser newUser) public async Task<User> CreateUser(NewUser newUser)
{ {
const string sql = "insert into Users (username, passwordHash, email) values (@Username, @Password, @Email) returning *"; const string sql = "insert into Users (username, passwordHash) values (@Username, @Password) returning *";
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
return await connection.QueryFirstAsync<User>(sql, newUser); return await connection.QueryFirstAsync<User>(sql, newUser);
} }

View File

@ -1,4 +1,6 @@
using FastBlog.Core.Services; using System.Diagnostics;
using System.Globalization;
using FastBlog.Core.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace FastBlog.Web.Controllers; namespace FastBlog.Web.Controllers;
@ -6,5 +8,62 @@ namespace FastBlog.Web.Controllers;
public sealed class UsersController(UserService userService, SessionService sessionService) : Controller public sealed class UsersController(UserService userService, SessionService sessionService) : Controller
{ {
public sealed record LoginRequest(string? Login, string? Password);
public sealed record LoginModel(string? Login, string? Password, List<string> Errors);
[HttpGet("/users/login")]
public async Task<IActionResult> Login()
{
return View(new LoginModel(null, null, []));
}
[HttpPost("/users/login")]
public async Task<IActionResult> Login(LoginRequest request)
{
List<string> errors = [];
// validation
if (string.IsNullOrWhiteSpace(request.Login)
|| request.Login.Length < 3 || request.Login.Length > 24)
{
errors.Add("Login is required and should be 3 to 24 symbols");
}
if (string.IsNullOrWhiteSpace(request.Password)
|| request.Password.Length < 3 || request.Password.Length > 64)
{
errors.Add("Password is required and should be 3 to 64 symbols");
}
if (errors.Count > 0)
{
return View(new LoginModel(request.Login, request.Password, errors));
}
Debug.Assert(request.Login is not null);
Debug.Assert(request.Password is not null);
// execute
var session = await sessionService.Authenticate(request.Login, request.Password);
if (session is null)
{
errors.Add("Unknown user or incorrect password");
return View(new LoginModel(request.Login, request.Password, errors));
}
UpdateCookie(HttpContext, "session-t", session.Token);
return Redirect("/");
}
private static void UpdateCookie(HttpContext context, string key, string value)
{
context.Response.Cookies.Delete(key);
context.Response.Cookies.Append(key, value, new CookieOptions()
{
HttpOnly = true,
SameSite = SameSiteMode.Strict,
IsEssential = true
// TODO: more options w/ configs, ill think bout that later
});
}
} }

View File

@ -1,5 +1,4 @@
using FastBlog.Core.Models; using FastBlog.Core.Models.Users;
using FastBlog.Core.Models.Users;
using FastBlog.Core.Services; using FastBlog.Core.Services;
namespace FastBlog.Web.Middlewares; namespace FastBlog.Web.Middlewares;
@ -9,26 +8,26 @@ public class SimpleAuthMiddleware(SessionService service, RequestDelegate next)
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
var meta = context.GetEndpoint()?.Metadata.GetMetadata<SimpleAuthAttribute>(); var meta = context.GetEndpoint()?.Metadata.GetMetadata<SimpleAuthAttribute>();
var session = context.Request.Cookies["session"]; var sessionToken = context.Request.Cookies["session-t"];
if (session is null && meta is not null) if (sessionToken is null && meta is not null)
{ {
context.Response.Redirect("/"); context.Response.Redirect("/user/login");
return; return;
} }
if (session is null && meta is null) if (sessionToken is null && meta is null)
{ {
await next(context); await next(context);
return; return;
} }
var result = await service.Get(session!); var result = await service.Get(sessionToken!);
if (result.IsError) if (result.IsError)
{ {
context.Response.Cookies.Delete("session"); context.Response.Cookies.Delete("session");
context.Response.Redirect("/"); context.Response.Redirect("/user/login");
return; return;
} }

View File

@ -31,9 +31,6 @@
<label for="age3">61 - 100</label><br><br> <label for="age3">61 - 100</label><br><br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
@Html.RadioButtonFor()
<button type="submit" class="btn-fw"> <button type="submit" class="btn-fw">
Upload Upload
</button> </button>

View File

@ -0,0 +1,35 @@
@model FastBlog.Web.Controllers.UsersController.LoginModel
@{
ViewBag.Title = "Login";
}
<div id="login-form" class="container">
<form
hx-post="/users/login"
hx-swap="outerHTML"
hx-target="#login-form">
<div>
<label for="login">Login:</label>
<input type="text" id="login" name="login" value="@Model.Login"
minlength="3"
maxlength="10"
required>
</div>
<div>
<label for="login">Password:</label>
<input type="password" id="password" name="password" value="@Model.Login"
minlength="3"
required>
</div>
<div>
<button type="submit">
<span>Create</span>
</button>
</div>
@foreach (var error in Model.Errors)
{
<div class="alert-caution">error</div>
}
</form>
</div>