Added simple sessions
This commit is contained in:
parent
ad00f6580d
commit
fb77065731
|
|
@ -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
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue