WIP
This commit is contained in:
parent
a2c2db053e
commit
ad00f6580d
|
|
@ -0,0 +1,12 @@
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Abstractions.Repositories.Users;
|
||||||
|
|
||||||
|
public interface ISessionRepository
|
||||||
|
{
|
||||||
|
Task<Session?> Get(string token);
|
||||||
|
Task<Session?> Set(Session session);
|
||||||
|
Task<Session?> Touch(string token);
|
||||||
|
Task<bool> Deactivate(string token);
|
||||||
|
Task<bool> RemoveAll(int userId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using FastBlog.Core.Models;
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Abstractions.Repositories.Users;
|
||||||
|
|
||||||
|
public interface IUserRepository
|
||||||
|
{
|
||||||
|
Task<User?> Get(int id);
|
||||||
|
Task<UserPassword?> GetByName(string username);
|
||||||
|
Task<bool> SetPassword(int id, string newPassword);
|
||||||
|
Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest);
|
||||||
|
Task<User> CreateUser(NewUser newUser);
|
||||||
|
Task<bool> DeleteUser(int userId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Db.Migrations;
|
||||||
|
|
||||||
|
[Migration(202410222104, "Add sessions")]
|
||||||
|
public class AddSessions : Migration
|
||||||
|
{
|
||||||
|
public override void Up()
|
||||||
|
{
|
||||||
|
Create.Table("Users")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Unique().Identity()
|
||||||
|
.WithColumn("Username").AsString().Unique()
|
||||||
|
.WithColumn("PasswordHash").AsString()
|
||||||
|
.WithColumn("Deleted").AsBoolean().WithDefaultValue(false);
|
||||||
|
|
||||||
|
Create.Table("Sessions")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Unique().Identity()
|
||||||
|
.WithColumn("Token").AsString().Unique()
|
||||||
|
.WithColumn("LastUsed").AsDateTime()
|
||||||
|
.WithColumn("CreatedAt").AsDateTime()
|
||||||
|
.WithColumn("Active").AsBoolean().WithDefaultValue(true).Indexed("IX_Sessions_Active")
|
||||||
|
.WithColumn("UserId").AsInt32().ForeignKey("Users", "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Down()
|
||||||
|
{
|
||||||
|
Delete.Table("Users");
|
||||||
|
Delete.Table("Sessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using FastBlog.Core.Abstractions.Repositories.Blogs;
|
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.Db;
|
using FastBlog.Core.Db;
|
||||||
using FastBlog.Core.Models.Blogs;
|
using FastBlog.Core.Models.Blogs;
|
||||||
using FastBlog.Core.Options;
|
using FastBlog.Core.Options;
|
||||||
|
|
@ -17,18 +18,23 @@ public static class DependencyInjection
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddCore(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddCore(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// applicatrion
|
// application
|
||||||
services.AddSingleton<BlogService>();
|
services.AddSingleton<BlogService>();
|
||||||
services.AddSingleton<FileService>();
|
services.AddSingleton<FileService>();
|
||||||
|
services.AddSingleton<UserService>();
|
||||||
|
services.AddSingleton<SessionService>();
|
||||||
|
|
||||||
// options
|
// options
|
||||||
services.Configure<FileStoreOptions>(configuration.GetSection("FileStore"));
|
services.Configure<FileStoreOptions>(configuration.GetSection("FileStore"));
|
||||||
|
services.Configure<SessionOptions>(configuration.GetSection("Sessions"));
|
||||||
|
|
||||||
// infrastructure
|
// infrastructure
|
||||||
services.AddSingleton<IBlogMetaRepository, BlogMetaRepository>();
|
services.AddSingleton<IBlogMetaRepository, BlogMetaRepository>();
|
||||||
services.AddSingleton<IBlogFileRepository, BlogFileRepository>();
|
services.AddSingleton<IBlogFileRepository, BlogFileRepository>();
|
||||||
services.AddSingleton<IFileMetaRepository, FileMetaRepository>();
|
services.AddSingleton<IFileMetaRepository, FileMetaRepository>();
|
||||||
services.AddSingleton<IFileRepository, FileRepository>();
|
services.AddSingleton<IFileRepository, FileRepository>();
|
||||||
|
services.AddSingleton<ISessionRepository,SessionRepository>();
|
||||||
|
services.AddSingleton<IUserRepository, UserRepository>();
|
||||||
|
|
||||||
// storage
|
// storage
|
||||||
var connectionString = configuration.GetConnectionString("Sqlite");
|
var connectionString = configuration.GetConnectionString("Sqlite");
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.Caching.Memory">
|
||||||
|
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.4\Microsoft.Extensions.Caching.Memory.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Microsoft.Extensions.Configuration.Abstractions">
|
<Reference Include="Microsoft.Extensions.Configuration.Abstractions">
|
||||||
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.4\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
|
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.4\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|
@ -16,6 +19,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||||
<PackageReference Include="Dapper.SqlBuilder" Version="2.0.78" />
|
<PackageReference Include="Dapper.SqlBuilder" Version="2.0.78" />
|
||||||
<PackageReference Include="FluentMigrator" Version="5.2.0" />
|
<PackageReference Include="FluentMigrator" Version="5.2.0" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
public sealed class NewUser
|
||||||
|
{
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
public string Password { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Token { get; set; } = null!;
|
||||||
|
public DateTime LastUsed { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
public bool Active { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
namespace FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
public bool Deleted { get; set; }
|
||||||
|
|
||||||
|
public static User? FromUserPassword(UserPassword? userDb)
|
||||||
|
{
|
||||||
|
if (userDb == null) return null;
|
||||||
|
return new User
|
||||||
|
{
|
||||||
|
Id = userDb.Id,
|
||||||
|
Username = userDb.Username,
|
||||||
|
Deleted = userDb.Deleted
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
public class UserPassword
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
public string PasswordHash { get; set; } = null!;
|
||||||
|
public bool Deleted { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Options;
|
||||||
|
|
||||||
|
public class SessionOptions
|
||||||
|
{
|
||||||
|
public int SlidingExpirationSeconds { get; set; }
|
||||||
|
public int AbsoluteExpirationSeconds { get; set; }
|
||||||
|
public bool EnableAbsoluteExpiration { get; set; }
|
||||||
|
public bool EnableSlidingExpiration { get; set; }
|
||||||
|
|
||||||
|
public int TouchAfterSeconds { get; set; }
|
||||||
|
|
||||||
|
public (bool SlidingValid, bool AbsoluteValid) Validate(Session session)
|
||||||
|
{
|
||||||
|
(bool slidingValid, bool absoluteValid) = (true, true);
|
||||||
|
|
||||||
|
if (EnableAbsoluteExpiration)
|
||||||
|
slidingValid = session.CreatedAt.AddSeconds(AbsoluteExpirationSeconds) < DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (EnableSlidingExpiration)
|
||||||
|
absoluteValid = session.LastUsed.AddSeconds(SlidingExpirationSeconds) < DateTime.UtcNow;
|
||||||
|
|
||||||
|
return (slidingValid, absoluteValid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using Dapper;
|
||||||
|
using FastBlog.Core.Abstractions.Repositories.Users;
|
||||||
|
using FastBlog.Core.Db;
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Repositories;
|
||||||
|
|
||||||
|
public sealed class SessionRepository(SqliteConnectionFactory connectionFactory) : ISessionRepository
|
||||||
|
{
|
||||||
|
public async Task<Session?> Get(string token)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
const string sql =
|
||||||
|
"""
|
||||||
|
SELECT s.*, u.UserName
|
||||||
|
FROM Sessions s
|
||||||
|
JOIN Users u ON s.UserId = u.Id
|
||||||
|
WHERE s.Token = @Token AND s.Active = true AND u.Deleted = false
|
||||||
|
""";
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<Session>(sql, new { Token = token });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Session?> Set(Session session)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
const string sql =
|
||||||
|
"""
|
||||||
|
INSERT INTO Sessions (Token, UserId, Expires, Active)
|
||||||
|
VALUES (@Token, @UserId, @Expires, @Active)
|
||||||
|
ON CONFLICT(Token) DO UPDATE SET Expires = @Expires
|
||||||
|
RETURNING *
|
||||||
|
""";
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<Session>(sql, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Session?> Touch(string token)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
const string sql =
|
||||||
|
"""
|
||||||
|
UPDATE Sessions
|
||||||
|
SET Expires = @Expires
|
||||||
|
WHERE Token = @Token
|
||||||
|
RETURNING *
|
||||||
|
""";
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<Session>(sql,
|
||||||
|
new { Token = token, Expires = DateTime.UtcNow.AddHours(1) });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Deactivate(string token)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
const string sql = "UPDATE Sessions SET Active = false WHERE Token = @Token";
|
||||||
|
var result = await connection.ExecuteAsync(sql, new { Token = token });
|
||||||
|
return result > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemoveAll(int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
const string sql = "DELETE FROM Sessions WHERE UserId = @UserId";
|
||||||
|
var result = await connection.ExecuteAsync(sql, new { UserId = userId });
|
||||||
|
return result > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
using Dapper;
|
||||||
|
using FastBlog.Core.Abstractions.Repositories.Users;
|
||||||
|
using FastBlog.Core.Db;
|
||||||
|
using FastBlog.Core.Models;
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Repositories;
|
||||||
|
|
||||||
|
public sealed class UserRepository(SqliteConnectionFactory connectionFactory) : IUserRepository
|
||||||
|
{
|
||||||
|
public async Task<User?> Get(int id)
|
||||||
|
{
|
||||||
|
const string sql = "select * from Users where id = @id";
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<User>(sql, new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserPassword?> GetByName(string username)
|
||||||
|
{
|
||||||
|
const string sql = "select * from Users where username = @username and deleted is false";
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<UserPassword>(sql, new { username });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetPassword(int id, string newPassword)
|
||||||
|
{
|
||||||
|
const string sql = "update Users set passwordHash = @newPassword where id = @id";
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
return await connection.ExecuteAsync(sql, new { id, newPassword }) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
select COUNT(*) from Users;
|
||||||
|
select * from Users
|
||||||
|
order by CreatedAt desc
|
||||||
|
limit @Amount offset @Offset;
|
||||||
|
""";
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
await using var multi = await connection.QueryMultipleAsync(sql, pagedRequest);
|
||||||
|
var total = await multi.ReadFirstAsync<int>();
|
||||||
|
var result = (await multi.ReadAsync<User>()).ToArray();
|
||||||
|
|
||||||
|
return PagedResponse<User>.Create(total, total, pagedRequest.Offset, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> CreateUser(NewUser newUser)
|
||||||
|
{
|
||||||
|
const string sql = "insert into Users (username, passwordHash, email) values (@Username, @Password, @Email) returning *";
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
return await connection.QueryFirstAsync<User>(sql, newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteUser(int userId)
|
||||||
|
{
|
||||||
|
const string sql = "update Users set deleted = true where id = @userId";
|
||||||
|
using var connection = connectionFactory.Create();
|
||||||
|
return await connection.ExecuteAsync(sql, new { userId }) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using FastBlog.Core.Abstractions.Repositories.Users;
|
||||||
|
using FastBlog.Core.Models;
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
using FastBlog.Core.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Services;
|
||||||
|
|
||||||
|
public sealed class SessionService(
|
||||||
|
ISessionRepository repository,
|
||||||
|
UserService userService,
|
||||||
|
IOptionsMonitor<SessionOptions> options)
|
||||||
|
{
|
||||||
|
public async Task<Result<Session?, BusinessError>> Get(string token)
|
||||||
|
{
|
||||||
|
var session = await repository.Get(token);
|
||||||
|
if (session is null)
|
||||||
|
return null as Session;
|
||||||
|
(bool slidingValid, bool absoluteValid) = options.CurrentValue.Validate(session);
|
||||||
|
|
||||||
|
if(!slidingValid || !absoluteValid)
|
||||||
|
{
|
||||||
|
await repository.Deactivate(token);
|
||||||
|
return new BusinessError("session-expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.LastUsed = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if(session.LastUsed - session.CreatedAt > TimeSpan.FromSeconds(options.CurrentValue.TouchAfterSeconds))
|
||||||
|
await repository.Touch(token);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Session?> Authenticate(string username, string password)
|
||||||
|
{
|
||||||
|
var user = await userService.GetByCredentials(username, password);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var session = new Session
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Token = RandomNumberGenerator.GetHexString(64, true),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
LastUsed = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
return await repository.Set(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> Deactivate(string token) => repository.Deactivate(token);
|
||||||
|
|
||||||
|
public Task<bool> RemoveAll(int userId) => repository.RemoveAll(userId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
using FastBlog.Core.Abstractions.Repositories.Users;
|
||||||
|
using FastBlog.Core.Models;
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
|
||||||
|
namespace FastBlog.Core.Services;
|
||||||
|
|
||||||
|
public sealed class UserService(IUserRepository repository)
|
||||||
|
{
|
||||||
|
public Task<User?> Get(int id) => repository.Get(id);
|
||||||
|
|
||||||
|
public async Task<User?> GetByCredentials(string username, string password)
|
||||||
|
{
|
||||||
|
var user = await repository.GetByName(username);
|
||||||
|
if (user is null) return null;
|
||||||
|
|
||||||
|
return !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash) ? null : User.FromUserPassword(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> SetPassword(int id, string newPassword) =>
|
||||||
|
repository.SetPassword(id, BCrypt.Net.BCrypt.HashPassword(newPassword));
|
||||||
|
|
||||||
|
public Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest) => repository.GetUsers(pagedRequest);
|
||||||
|
|
||||||
|
public Task<User> CreateUser(NewUser newUser)
|
||||||
|
{
|
||||||
|
newUser.Password = BCrypt.Net.BCrypt.HashPassword(newUser.Password);
|
||||||
|
return repository.CreateUser(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteUser(int userId) => await repository.DeleteUser(userId);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using FastBlog.Core.Models;
|
using FastBlog.Core.Models;
|
||||||
using FastBlog.Core.Models.Blogs;
|
using FastBlog.Core.Models.Blogs;
|
||||||
using FastBlog.Core.Services;
|
using FastBlog.Core.Services;
|
||||||
|
using FastBlog.Web.Middlewares;
|
||||||
using FastBlog.Web.Models;
|
using FastBlog.Web.Models;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ public class BlogsController(BlogService service) : Controller
|
||||||
return View(blog);
|
return View(blog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("unpublished/{*slug}")]
|
[Route("unpublished/{*slug}")]
|
||||||
public async Task<IActionResult> Unpublished(string? slug)
|
public async Task<IActionResult> Unpublished(string? slug)
|
||||||
|
|
@ -53,6 +54,7 @@ public class BlogsController(BlogService service) : Controller
|
||||||
return View(blog);
|
return View(blog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[HttpGet("list/edit")]
|
[HttpGet("list/edit")]
|
||||||
public async Task<IActionResult> ListEdit(
|
public async Task<IActionResult> ListEdit(
|
||||||
[FromQuery(Name = "amount")] int amount = 25,
|
[FromQuery(Name = "amount")] int amount = 25,
|
||||||
|
|
@ -71,6 +73,7 @@ public class BlogsController(BlogService service) : Controller
|
||||||
return View(await service.ListMetas(new BlogFilter(from,to, query), new PagedRequest(amount, offset)));
|
return View(await service.ListMetas(new BlogFilter(from,to, query), new PagedRequest(amount, offset)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("edit/{id:int?}")]
|
[Route("edit/{id:int?}")]
|
||||||
public async ValueTask<IActionResult> Edit(int? id)
|
public async ValueTask<IActionResult> Edit(int? id)
|
||||||
|
|
@ -116,6 +119,7 @@ public class BlogsController(BlogService service) : Controller
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("edit")]
|
[Route("edit")]
|
||||||
public async Task<IActionResult> Edit([FromForm] EditBlog editBlog)
|
public async Task<IActionResult> Edit([FromForm] EditBlog editBlog)
|
||||||
|
|
@ -152,6 +156,7 @@ public class BlogsController(BlogService service) : Controller
|
||||||
return Redirect($"/blogs/{editBlog.Slug}");
|
return Redirect($"/blogs/{editBlog.Slug}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("preview")]
|
[Route("preview")]
|
||||||
public IActionResult Preview([FromForm] EditBlog editBlog)
|
public IActionResult Preview([FromForm] EditBlog editBlog)
|
||||||
|
|
@ -175,6 +180,7 @@ public class BlogsController(BlogService service) : Controller
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[Route("{id:int}")]
|
[Route("{id:int}")]
|
||||||
public async Task<IActionResult> Delete(int id)
|
public async Task<IActionResult> Delete(int id)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
using FastBlog.Core.Models;
|
using FastBlog.Core.Models;
|
||||||
using FastBlog.Core.Models.Files;
|
using FastBlog.Core.Models.Files;
|
||||||
using FastBlog.Core.Services;
|
using FastBlog.Core.Services;
|
||||||
|
using FastBlog.Web.Middlewares;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace FastBlog.Web.Controllers;
|
namespace FastBlog.Web.Controllers;
|
||||||
|
|
||||||
|
[SimpleAuth]
|
||||||
[Route("files")]
|
[Route("files")]
|
||||||
public class FilesController(FileService fileService) : Controller
|
public class FilesController(FileService fileService) : Controller
|
||||||
{
|
{
|
||||||
|
|
@ -37,7 +39,7 @@ public class FilesController(FileService fileService) : Controller
|
||||||
public async Task<IActionResult> Upload([FromForm(Name = "sourcePath")] string? sourcePath)
|
public async Task<IActionResult> Upload([FromForm(Name = "sourcePath")] string? sourcePath)
|
||||||
{
|
{
|
||||||
// TODO: Validate path to prevent directory traversal
|
// TODO: Validate path to prevent directory traversal
|
||||||
// rn I am the only user so I dont care for now
|
// rn I am the only user so I dont care
|
||||||
|
|
||||||
if (HttpContext.Request.Form.Files.Count < 1)
|
if (HttpContext.Request.Form.Files.Count < 1)
|
||||||
return BadRequest("File is required.");
|
return BadRequest("File is required.");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using FastBlog.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace FastBlog.Web.Controllers;
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class UsersController(UserService userService, SessionService sessionService) : Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -17,4 +17,9 @@
|
||||||
<PackageReference Include="Pek.Markdig.HighlightJs" Version="0.5.1" />
|
<PackageReference Include="Pek.Markdig.HighlightJs" Version="0.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="md\" />
|
||||||
|
<Folder Include="wwwroot\static\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
namespace FastBlog.Web.Helpers;
|
namespace FastBlog.Web.Helpers;
|
||||||
|
|
||||||
public static class PropertyHelper
|
public static class HtmlPropertyHelper
|
||||||
{
|
{
|
||||||
public static string If(bool condition, string property)
|
public static string If(bool condition, string property)
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
using FastBlog.Core.Models;
|
||||||
|
using FastBlog.Core.Models.Users;
|
||||||
|
using FastBlog.Core.Services;
|
||||||
|
|
||||||
|
namespace FastBlog.Web.Middlewares;
|
||||||
|
|
||||||
|
public class SimpleAuthMiddleware(SessionService service, RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task Invoke(HttpContext context)
|
||||||
|
{
|
||||||
|
var meta = context.GetEndpoint()?.Metadata.GetMetadata<SimpleAuthAttribute>();
|
||||||
|
var session = context.Request.Cookies["session"];
|
||||||
|
|
||||||
|
if (session is null && meta is not null)
|
||||||
|
{
|
||||||
|
context.Response.Redirect("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session is null && meta is null)
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await service.Get(session!);
|
||||||
|
|
||||||
|
if (result.IsError)
|
||||||
|
{
|
||||||
|
context.Response.Cookies.Delete("session");
|
||||||
|
context.Response.Redirect("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Features.Set(new User
|
||||||
|
{
|
||||||
|
Id = result.AsOk!.UserId,
|
||||||
|
Username = result.AsOk.UserName!,
|
||||||
|
Deleted = false
|
||||||
|
});
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SimpleAuthAttribute : Attribute;
|
||||||
|
|
||||||
|
public static class SimpleAuthExtensions
|
||||||
|
{
|
||||||
|
public static bool TryGetUser(this HttpContext context, out User? user)
|
||||||
|
{
|
||||||
|
user = context.Features.Get<User>();
|
||||||
|
return user is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsUser(this HttpContext context)
|
||||||
|
{
|
||||||
|
return context.Features.Get<User>() is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
@model EditBlog
|
@model EditBlog
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Blog";
|
||||||
|
}
|
||||||
|
|
||||||
<link rel="stylesheet" href="/lib/simplemde/simplemde.min.css">
|
<link rel="stylesheet" href="/lib/simplemde/simplemde.min.css">
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
@model FastBlog.Core.Models.Blogs.Blog
|
@using FastBlog.Web.Middlewares
|
||||||
|
@model FastBlog.Core.Models.Blogs.Blog
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = Model.Metadata.Title;
|
ViewData["Title"] = Model.Metadata.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@await Html.PartialAsync("Blogs/Content", Model)
|
@await Html.PartialAsync("Blogs/Content", Model)
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@if(Context.IsUser())
|
||||||
|
{
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<hr/>
|
<hr/>
|
||||||
@await Html.PartialAsync("Blogs/ManageCard", Model.Metadata)
|
@await Html.PartialAsync("Blogs/ManageCard", Model.Metadata)
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = "Blogs";
|
ViewData["Title"] = "Blogs";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container" id="file-list">
|
<div class="container" id="file-list">
|
||||||
|
|
@ -107,14 +107,14 @@
|
||||||
hx-get="/edit-list?offset=@(Model.Offset - 25)"
|
hx-get="/edit-list?offset=@(Model.Offset - 25)"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#file-list"
|
hx-target="#file-list"
|
||||||
@PropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
|
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
hx-get="/edit-list?offset=@(Model.Offset + 25)"
|
hx-get="/edit-list?offset=@(Model.Offset + 25)"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#file-list"
|
hx-target="#file-list"
|
||||||
@PropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
|
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
@using FastBlog.Web.Helpers
|
@using FastBlog.Web.Helpers
|
||||||
@model FastBlog.Core.Models.PagedResponse<FastBlog.Core.Models.Blogs.BlogMeta>
|
@model FastBlog.Core.Models.PagedResponse<FastBlog.Core.Models.Blogs.BlogMeta>
|
||||||
|
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = "Blogs";
|
ViewData["Title"] = "Blogs";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container" id="file-list">
|
<div class="container" id="file-list">
|
||||||
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<a href="/edit">
|
<a href="/edit">
|
||||||
|
|
@ -107,14 +103,14 @@
|
||||||
hx-get="/edit-list?offset=@(Model.Offset - 25)"
|
hx-get="/edit-list?offset=@(Model.Offset - 25)"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#file-list"
|
hx-target="#file-list"
|
||||||
@PropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
|
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
hx-get="/edit-list?offset=@(Model.Offset + 25)"
|
hx-get="/edit-list?offset=@(Model.Offset + 25)"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#file-list"
|
hx-target="#file-list"
|
||||||
@PropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
|
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
@model FastBlog.Core.Models.Blogs.Blog
|
@model FastBlog.Core.Models.Blogs.Blog
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Preview of " + Model.Metadata.Title;
|
||||||
|
}
|
||||||
|
|
||||||
@await Html.PartialAsync("Blogs/Content", Model)
|
@await Html.PartialAsync("Blogs/Content", Model)
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,18 @@
|
||||||
<input type="file" name="file" required/>
|
<input type="file" name="file" required/>
|
||||||
<label for="upload-path">Path to upload (optional)</label>
|
<label for="upload-path">Path to upload (optional)</label>
|
||||||
<input type="text" id="upload-path" name="sourcePath" placeholder="/subfolders/"/>
|
<input type="text" id="upload-path" name="sourcePath" placeholder="/subfolders/"/>
|
||||||
|
|
||||||
|
<input type="radio" id="age1" name="age" value="30">
|
||||||
|
<label for="age1">0 - 30</label><br>
|
||||||
|
<input type="radio" id="age2" name="age" value="60">
|
||||||
|
<label for="age2">31 - 60</label><br>
|
||||||
|
<input type="radio" id="age3" name="age" value="100">
|
||||||
|
<label for="age3">61 - 100</label><br><br>
|
||||||
|
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
|
||||||
|
@Html.RadioButtonFor()
|
||||||
|
|
||||||
<button type="submit" class="btn-fw">
|
<button type="submit" class="btn-fw">
|
||||||
Upload
|
Upload
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -74,14 +86,14 @@
|
||||||
hx-get="/files?page=@(Model.Offset - 25)"
|
hx-get="/files?page=@(Model.Offset - 25)"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#file-list"
|
hx-target="#file-list"
|
||||||
@PropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
|
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
hx-get="/files?page=@(Model.Offset + 25)"
|
hx-get="/files?page=@(Model.Offset + 25)"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-target="#file-list"
|
hx-target="#file-list"
|
||||||
@PropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
|
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ return;
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="~/"><h1>@options.Value.Title</h1></a></li>
|
<li><a href="~/"><h1>@options.Value.Title</h1></a></li>
|
||||||
<li><a href="~/blogs/list">Blog</a></li>
|
<li><a href="~/list">Blog</a></li>
|
||||||
<li><a href="~/edit">New</a></li>
|
<li><a href="~/edit">New</a></li>
|
||||||
<li><a href="~/files">Files</a></li>
|
<li><a href="~/files">Files</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,11 @@
|
||||||
"GitHub": "https://github.com",
|
"GitHub": "https://github.com",
|
||||||
"Mail": "mailto:mail@example.com"
|
"Mail": "mailto:mail@example.com"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Sessions": {
|
||||||
|
"SlidingExpirationSeconds": 2592000,
|
||||||
|
"AbsoluteExpirationSeconds": 7776000,
|
||||||
|
"EnableAbsoluteExpiration": true,
|
||||||
|
"EnableSlidingExpiration": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue