Compare commits

..

No commits in common. "1bba76b71aa2c659054e12b823f120c7b43490aa" and "fb770657316fe1c751d2879831d4dcb32a103556" have entirely different histories.

25 changed files with 100 additions and 536 deletions

View File

@ -6,7 +6,6 @@ namespace FastBlog.Core.Abstractions.Repositories.Users;
public interface IUserRepository public interface IUserRepository
{ {
Task<User?> Get(int id); Task<User?> Get(int id);
Task<UserPassword?> GetWithPassword(int id);
Task<UserPassword?> GetByName(string username); Task<UserPassword?> GetByName(string username);
Task<bool> SetPassword(int id, string newPassword); Task<bool> SetPassword(int id, string newPassword);
Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest); Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest);

View File

@ -91,7 +91,7 @@ public static class DependencyInjection
{ {
var userService = provider.GetRequiredService<UserService>(); var userService = provider.GetRequiredService<UserService>();
var existingUserResult = await userService.GetUsers(new PagedRequest(1, 0)); var existingUserResult = await userService.GetUsers(new PagedRequest(1, 0));
if (existingUserResult.Amount >= 1) if (existingUserResult.Amount is 1)
return; return;
var newUser = new NewUser var newUser = new NewUser

View File

@ -34,16 +34,4 @@
<PackageReference Include="SQLite" Version="3.13.0" /> <PackageReference Include="SQLite" Version="3.13.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove="obj\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="obj\**" />
</ItemGroup>
<ItemGroup>
<None Remove="obj\**" />
</ItemGroup>
</Project> </Project>

View File

@ -16,10 +16,10 @@ public class SessionOptions
(bool slidingValid, bool absoluteValid) = (true, true); (bool slidingValid, bool absoluteValid) = (true, true);
if (EnableAbsoluteExpiration) if (EnableAbsoluteExpiration)
slidingValid = session.CreatedAt.AddSeconds(AbsoluteExpirationSeconds) > DateTime.UtcNow; slidingValid = session.CreatedAt.AddSeconds(AbsoluteExpirationSeconds) < DateTime.UtcNow;
if (EnableSlidingExpiration) if (EnableSlidingExpiration)
absoluteValid = session.LastUsed.AddSeconds(SlidingExpirationSeconds) > DateTime.UtcNow; absoluteValid = session.LastUsed.AddSeconds(SlidingExpirationSeconds) < DateTime.UtcNow;
return (slidingValid, absoluteValid); return (slidingValid, absoluteValid);
} }

View File

@ -15,23 +15,16 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
return await connection.QueryFirstOrDefaultAsync<User>(sql, new { id }); return await connection.QueryFirstOrDefaultAsync<User>(sql, new { id });
} }
public async Task<UserPassword?> GetWithPassword(int id)
{
const string sql = "select * from Users where id = @id and deleted is false";
using var connection = connectionFactory.Create();
return await connection.QueryFirstOrDefaultAsync<UserPassword>(sql, new { id });
}
public async Task<UserPassword?> GetByName(string username) public async Task<UserPassword?> GetByName(string username)
{ {
const string sql = "select * from Users where username = @username"; const string sql = "select * from Users where username = @username and deleted is false";
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
return await connection.QueryFirstOrDefaultAsync<UserPassword>(sql, new { username }); return await connection.QueryFirstOrDefaultAsync<UserPassword>(sql, new { username });
} }
public async Task<bool> SetPassword(int id, string newPassword) public async Task<bool> SetPassword(int id, string newPassword)
{ {
const string sql = "update Users set passwordHash = @newPassword where id = @id and deleted = 0"; const string sql = "update Users set passwordHash = @newPassword where id = @id";
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
return await connection.ExecuteAsync(sql, new { id, newPassword }) > 0; return await connection.ExecuteAsync(sql, new { id, newPassword }) > 0;
} }
@ -41,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
where deleted = 0
limit @Amount offset @Offset; limit @Amount offset @Offset;
"""; """;
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
@ -61,7 +53,7 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
public async Task<bool> DeleteUser(int userId) public async Task<bool> DeleteUser(int userId)
{ {
const string sql = "update Users set deleted = true where id = @userId and deleted = 0"; const string sql = "update Users set deleted = true where id = @userId";
using var connection = connectionFactory.Create(); using var connection = connectionFactory.Create();
return await connection.ExecuteAsync(sql, new { userId }) > 0; return await connection.ExecuteAsync(sql, new { userId }) > 0;
} }

View File

@ -8,41 +8,16 @@ public sealed class UserService(IUserRepository repository)
{ {
public Task<User?> Get(int id) => repository.Get(id); public Task<User?> Get(int id) => repository.Get(id);
public async Task<User?> GetByName(string username)
{
return User.FromUserPassword(await repository.GetByName(username));
}
public async Task<User?> GetByCredentials(string username, string password) public async Task<User?> GetByCredentials(string username, string password)
{ {
var user = await repository.GetByName(username); var user = await repository.GetByName(username);
if (user is null || user.Deleted) return null; if (user is null) return null;
return !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash) ? null : User.FromUserPassword(user); return !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash) ? null : User.FromUserPassword(user);
} }
public sealed record ChangePasswordResult(bool Success, string Message); public Task<bool> SetPassword(int id, string newPassword) =>
public async Task<ChangePasswordResult> ChangePassword(int id, string oldPassword, string newPassword) repository.SetPassword(id, BCrypt.Net.BCrypt.HashPassword(newPassword));
{
var user = await repository.GetWithPassword(id);
if (user is null)
return new(false, "Unknown user");
if (!BCrypt.Net.BCrypt.Verify(oldPassword, user.PasswordHash))
return new(false, "Wrong password");
if (!await repository.SetPassword(id, BCrypt.Net.BCrypt.HashPassword(newPassword)))
return new(false, "Couldn't update password");
return new(true, "Successfully updated the password");
}
public async Task<ChangePasswordResult> SetPassword(int id, string newPassword)
{
var user = await repository.GetWithPassword(id);
if (user is null)
return new(false, "Unknown user");
if (!await repository.SetPassword(id, BCrypt.Net.BCrypt.HashPassword(newPassword)))
return new(false, "Couldn't update password");
return new(true, "Successfully updated the password");
}
public Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest) => repository.GetUsers(pagedRequest); public Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest) => repository.GetUsers(pagedRequest);

View File

@ -13,7 +13,6 @@ public class FilesController(FileService fileService) : Controller
private const long MaxFileSize = 8L * 1024L * 1024L * 1024L; private const long MaxFileSize = 8L * 1024L * 1024L * 1024L;
[HttpGet] [HttpGet]
[SimpleAuth]
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
var files = await fileService.GetFiles(new PagedRequest(25, 0)); var files = await fileService.GetFiles(new PagedRequest(25, 0));
@ -21,7 +20,6 @@ public class FilesController(FileService fileService) : Controller
} }
[HttpGet("{offset:int}")] [HttpGet("{offset:int}")]
[SimpleAuth]
public async Task<IActionResult> Page(int offset) public async Task<IActionResult> Page(int offset)
{ {
var files = await fileService.GetFiles(new PagedRequest(25, offset)); var files = await fileService.GetFiles(new PagedRequest(25, offset));
@ -29,7 +27,6 @@ public class FilesController(FileService fileService) : Controller
} }
[HttpDelete("{metaId:int}")] [HttpDelete("{metaId:int}")]
[SimpleAuth]
public async Task<IActionResult> Delete(int metaId) public async Task<IActionResult> Delete(int metaId)
{ {
var result = await fileService.DeleteFile(metaId); var result = await fileService.DeleteFile(metaId);
@ -39,7 +36,6 @@ public class FilesController(FileService fileService) : Controller
[HttpPost] [HttpPost]
[SimpleAuth]
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

View File

@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace FastBlog.Web.Controllers;
public class TooManyRequestsController : Controller
{
[HttpGet("/too-many-requests")]
public IActionResult Index()
{
return View();
}
}

View File

@ -1,41 +1,25 @@
using System.Diagnostics; using System.Diagnostics;
using FastBlog.Core.Models; using System.Globalization;
using FastBlog.Core.Models.Users;
using FastBlog.Core.Services; using FastBlog.Core.Services;
using FastBlog.Web.Middlewares;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace FastBlog.Web.Controllers; namespace FastBlog.Web.Controllers;
[EnableRateLimiting("fixed")]
public sealed class UsersController(UserService userService, SessionService sessionService) : Controller public sealed class UsersController(UserService userService, SessionService sessionService) : Controller
{ {
public UserService UserService { get; } = userService;
public sealed record LoginRequest(string? Login, string? Password); public sealed record LoginRequest(string? Login, string? Password);
public sealed record CreateRequest(string Login, string Password);
public sealed record LoginModel(string? Login, string? Password, List<string> Errors); public sealed record LoginModel(string? Login, string? Password, List<string> Errors);
[HttpGet("/users/login")] [HttpGet("/users/login")]
public IActionResult LogIn() public async Task<IActionResult> Login()
{ {
if (HttpContext.IsUser())
return Redirect("/");
return View(new LoginModel(null, null, [])); return View(new LoginModel(null, null, []));
} }
[Htmx]
[EnableRateLimiting("fixed")]
[HttpPost("/users/login")] [HttpPost("/users/login")]
public async Task<IActionResult> LogIn(LoginRequest request) public async Task<IActionResult> Login(LoginRequest request)
{ {
if (HttpContext.IsUser())
return Redirect("/");
List<string> errors = []; List<string> errors = [];
// validation // validation
if (string.IsNullOrWhiteSpace(request.Login) if (string.IsNullOrWhiteSpace(request.Login)
@ -52,7 +36,7 @@ public sealed class UsersController(UserService userService, SessionService sess
if (errors.Count > 0) if (errors.Count > 0)
{ {
return PartialView(new LoginModel(request.Login, request.Password, errors)); return View(new LoginModel(request.Login, request.Password, errors));
} }
Debug.Assert(request.Login is not null); Debug.Assert(request.Login is not null);
@ -64,124 +48,11 @@ public sealed class UsersController(UserService userService, SessionService sess
if (session is null) if (session is null)
{ {
errors.Add("Unknown user or incorrect password"); errors.Add("Unknown user or incorrect password");
return PartialView(new LoginModel(request.Login, request.Password, errors)); return View(new LoginModel(request.Login, request.Password, errors));
} }
UpdateCookie(HttpContext, "session-t", session.Token); UpdateCookie(HttpContext, "session-t", session.Token);
HttpContext.Response.Headers["HX-Redirect"] = "/";
return Ok();
}
[SimpleAuth]
[Htmx]
[HttpDelete("/users/logout")]
public async ValueTask<IActionResult> LogOut()
{
if (!HttpContext.Request.Cookies.TryGetValue("session-t", out var sessionT))
return Redirect("/"); return Redirect("/");
await sessionService.Deactivate(sessionT);
HttpContext.Response.Cookies.Delete("session-t");
HttpContext.Response.Headers["HX-Redirect"] = "/";
return Ok();
}
[HttpGet("/users")]
[SimpleAuth]
public async Task<IActionResult> Page([FromQuery] int page = 0)
{
var users = await userService.GetUsers(new PagedRequest(25, page));
return View("Index", users);
}
[HttpDelete("/users/{id:int}")]
[SimpleAuth]
public async Task<IActionResult> Delete(int id)
{
_ = await userService.DeleteUser(id);
HttpContext.Response.Headers["HX-Redirect"] = "/users";
return Ok();
}
public sealed record UserViewModel(User User, bool Success = true, string? Message = null);
[SimpleAuth]
[HttpGet("/users/me")]
public IActionResult GetCurrent()
{
if (!HttpContext.TryGetUser(out var user))
{
return Redirect("/users/login");
}
return View("Profile", new UserViewModel(user));
}
[SimpleAuth]
[HttpPost("/users")]
public async Task<IActionResult> Create(CreateRequest createRequest)
{
if (await userService.GetByName(createRequest.Login) is not null)
{
var u = await userService.GetUsers(new PagedRequest(25, 0));
var v = View("Index", u);
v.ViewData.Add("error", "User already exists");
return v;
}
var user = await userService.CreateUser(new NewUser
{
Password = createRequest.Password,
Username = createRequest.Login
});
var users = await userService.GetUsers(new PagedRequest(25, 0));
return View("Index", users);
}
[SimpleAuth]
[HttpGet("/users/{id:int}")]
public async Task<IActionResult> Get(int id)
{
var user = await userService.Get(id);
if (user is null)
return Redirect("~/");
return View("Profile", new UserViewModel(user));
}
public sealed record ChangePasswordModel(string OldPassword, string NewPassword);
[SimpleAuth]
[HttpPost("/users/{id:int}/change-password")]
public async Task<IActionResult> ChangePassword(int id, ChangePasswordModel model)
{
var user = await userService.Get(id);
if (user is null)
return Redirect("~/");
var result = await userService.ChangePassword(id, model.OldPassword, model.NewPassword);
return View("Profile", new UserViewModel(user, result.Success, result.Message));
}
[SimpleAuth]
[HttpPost("/users/{id:int}/set-password")]
public async Task<IActionResult> SetPassword(int id, string newPassword)
{
if (!HttpContext.TryGetUser(out var currentUser))
{
return Redirect("~/");
}
if (currentUser.Id == id)
{
return View("Profile", new UserViewModel(currentUser, false, "Provide the old password too"));
}
var user = await userService.Get(id);
if (user is null)
return Redirect("~/");
var result = await userService.SetPassword(id, newPassword);
return View("Profile", new UserViewModel(user, result.Success, result.Message));
} }
private static void UpdateCookie(HttpContext context, string key, string value) private static void UpdateCookie(HttpContext context, string key, string value)
@ -191,8 +62,8 @@ public sealed class UsersController(UserService userService, SessionService sess
{ {
HttpOnly = true, HttpOnly = true,
SameSite = SameSiteMode.Strict, SameSite = SameSiteMode.Strict,
Secure = true,
IsEssential = true IsEssential = true
// TODO: more options w/ configs, ill think bout that later
}); });
} }
} }

View File

@ -1,9 +0,0 @@
namespace FastBlog.Web;
/// <summary>
/// Marks methods used by HTMX requests<br/>
/// Use Redirect() with caution!<br/>
/// Does not affect behaviour<br/>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class HtmxAttribute : Attribute;

View File

@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; 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;
@ -13,7 +12,7 @@ public class SimpleAuthMiddleware(SessionService service, RequestDelegate next)
if (sessionToken is null && meta is not null) if (sessionToken is null && meta is not null)
{ {
context.Response.Redirect("/"); context.Response.Redirect("/user/login");
return; return;
} }
@ -25,10 +24,10 @@ public class SimpleAuthMiddleware(SessionService service, RequestDelegate next)
var result = await service.Get(sessionToken!); var result = await service.Get(sessionToken!);
if (result.IsError || result.AsOk is null) if (result.IsError)
{ {
context.Response.Cookies.Delete("session-t"); context.Response.Cookies.Delete("session");
context.Response.Redirect("/users/login"); context.Response.Redirect("/user/login");
return; return;
} }
@ -47,7 +46,7 @@ internal class SimpleAuthAttribute : Attribute;
public static class SimpleAuthExtensions public static class SimpleAuthExtensions
{ {
public static bool TryGetUser(this HttpContext context, [NotNullWhen(true)] out User? user) public static bool TryGetUser(this HttpContext context, out User? user)
{ {
user = context.Features.Get<User>(); user = context.Features.Get<User>();
return user is not null; return user is not null;

View File

@ -1,7 +1,5 @@
using FastBlog.Core; using FastBlog.Core;
using FastBlog.Web; using FastBlog.Web;
using FastBlog.Web.Middlewares;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -13,33 +11,8 @@ else
builder.Services.AddCore(builder.Configuration); builder.Services.AddCore(builder.Configuration);
builder.Services.Configure<DisplayOptions>(builder.Configuration.GetSection("Display")); builder.Services.Configure<DisplayOptions>(builder.Configuration.GetSection("Display"));
builder.Services.AddRateLimiter(rateLimiterOptions =>
{
rateLimiterOptions.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 30;
options.Window = TimeSpan.FromSeconds(300);
options.QueueLimit = 0;
})
.OnRejected += (context, _) =>
{
context.HttpContext.Response.StatusCode = 200;
if (context.HttpContext.Request.Headers.ContainsKey("HX-Request"))
{
context.HttpContext.Response.Headers.Append("HX-Redirect", "/too-many-requests");
}
else
{
context.HttpContext.Response.Redirect("/too-many-requests");
}
return ValueTask.CompletedTask;
};
});
var app = builder.Build(); var app = builder.Build();
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Home/Error"); app.UseExceptionHandler("/Home/Error");
@ -50,12 +23,8 @@ app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseRateLimiter();
app.UseAuthorization(); app.UseAuthorization();
app.UseMiddleware<SimpleAuthMiddleware>();
app.MapControllerRoute( app.MapControllerRoute(
name: "default", name: "default",
pattern: "{controller=Blogs}/{action=Index}/{slug?}"); pattern: "{controller=Blogs}/{action=Index}/{slug?}");

View File

@ -1,7 +1,7 @@
@model FastBlog.Core.Models.Blogs.Blog @model FastBlog.Core.Models.Blogs.Blog
@{ @{
ViewData["Title"] = Model.Metadata.Title; ViewBag.Title = Model.Metadata.Title;
} }
@if(DateTime.UtcNow < Model.Metadata.CreatedAt) @if(DateTime.UtcNow < Model.Metadata.CreatedAt)

View File

@ -3,7 +3,7 @@
@{ @{
ViewData["Title"] = "Files"; ViewBag.Title = "Files";
} }
<div class="container" id="file-list"> <div class="container" id="file-list">
@ -22,6 +22,15 @@
<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">
<button type="submit" class="btn-fw"> <button type="submit" class="btn-fw">
Upload Upload
</button> </button>

View File

@ -1,14 +1,16 @@
@using FastBlog.Web.Middlewares @using Microsoft.Extensions.Options
@using Microsoft.Extensions.Options
@inject IOptions<DisplayOptions> options; @inject IOptions<DisplayOptions> options;
@{ @{
ViewContext.HttpContext.Response.Headers.Add("Vary", "Hx-Request"); ViewContext.HttpContext.Response.Headers.Add("Vary", "Hx-Request");
} }
@if (ViewContext.HttpContext.Request.Headers["Hx-Request"].Contains("true")) @if (ViewContext.HttpContext.Request.Headers["Hx-Request"].Contains("true"))
{ {
@RenderBody() @RenderBody()
return; return;
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="dark"> <html lang="en" data-theme="dark">
<head> <head>
@ -22,52 +24,20 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
</head> </head>
<body> <body>
<script src="~/lib/htmx/htmx.2.min.js"></script> <script src="~/js/htmx.min.js"></script>
<header> <header class="nav-container">
@if (Context.IsUser())
{
<div class="nav-admin">
<nav>
<div> <div>
<li>
<a href="~/edit">New</a>
</li>
<li>
<a href="~/files">Files</a>
</li>
<li>
<a href="~/users">Users</a>
</li>
</div>
<div>
<li>
<a href="/users/me">Profile</a>
</li>
<li>
<a href="#" hx-delete="/users/logout">Logout</a>
</li>
</div>
</nav>
</div>
}
<div class="nav-container">
<nav> <nav>
<ul> <ul>
<li> <li><a href="~/"><h1>@options.Value.Title</h1></a></li>
<a href="~/"> <li><a href="~/list">Blog</a></li>
<h1>@options.Value.Title</h1> <li><a href="~/edit">New</a></li>
</a> <li><a href="~/files">Files</a></li>
</li>
<li>
<a href="~/list">Blog</a>
</li>
</ul> </ul>
<ul> <ul>
@foreach (var link in options.Value.Links) @foreach (var link in options.Value.Links)
{ {
<li> <li><a href="@link.Value">@link.Key</a></li>
<a href="@link.Value">@link.Key</a>
</li>
} }
</ul> </ul>
</nav> </nav>

View File

@ -1,12 +0,0 @@
@{
ViewData["Title"] = "Rate limit exceeded";
}
<div class="container">
<article>
<header>Rate limit exceeded</header>
<div>
<p>You have sent too many requests</p>
</div>
</article>
</div>

View File

@ -1,76 +0,0 @@
@using FastBlog.Web.Helpers
@model FastBlog.Core.Models.PagedResponse<FastBlog.Core.Models.Users.User>
@{
ViewData["Title"] = "Users";
}
<div class="container" id="user-list">
<br/>
<article>
<header>
<h3>Create a user</h3>
</header>
@if (ViewData.TryGetValue("error", out var errorMsg))
{
<div class="alert alert-caution">@errorMsg</div>
}
<form
hx-post="/users"
hx-swap="outerHTML"
hx-target="#user-list">
<label for="login">Login</label>
<input required type="text" id="login" name="login" placeholder="username"/>
<label for="password">Password</label>
<input required type="password" id="password" name="password"/>
<button type="submit" class="btn-fw">
Create
</button>
</form>
</article>
<article>
<header>
<h3>Manage users</h3>
</header>
<body>
@if (Model.Data.Length is 0)
{
<h4>No users have been found</h4>
}
@foreach (var user in Model.Data)
{
<div class="grid">
<div style="display: flex; font-size: 16px">
<p class="no-margin" style="margin-right: 10px; font-weight: bold">
<a href="~/users/@user.Id/">@user.Username</a>
</p>
</div>
</div>
<hr/>
}
</body>
<footer>
<div>
<button
hx-get="/users?page=@(Model.Offset - 25)"
hx-swap="outerHTML"
hx-target="#file-list"
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) == 0, "disabled")>
Previous
</button>
<button
hx-get="/users?page=@(Model.Offset + 25)"
hx-swap="outerHTML"
hx-target="#file-list"
@HtmlPropertyHelper.If(Math.Clamp(Model.Offset, 0, Model.Amount) >= Model.Amount - 25, "disabled")>
Next
</button>
</div>
</footer>
</article>
</div>

View File

@ -1,37 +0,0 @@
@model FastBlog.Web.Controllers.UsersController.LoginModel
@{
ViewData["Title"] = "Login";
}
<div id="login-form" class="container">
<article >
<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.Password"
minlength="3"
required>
</div>
<div>
<button type="submit">
<span>Create</span>
</button>
</div>
@foreach (var error in Model.Errors)
{
<div class="alert alert-caution">
<p>@error</p>
</div>
}
</form>
</article>
</div>

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>

View File

@ -1,86 +0,0 @@
@using FastBlog.Web.Middlewares
@model FastBlog.Web.Controllers.UsersController.UserViewModel
@{
ViewData["Title"] = $"{Model.User.Username} - Профиль";
bool isSelf = Context.TryGetUser(out var user) && user.Id == Model.User.Id;
}
<div class="container">
<div class="grid">
<article>
<header>
<h2>@Model.User.Username</h2>
</header>
<div class="grid">
@if (isSelf)
{
<p>This is your profile, @Model.User.Username</p>
}
else
{
<div>
<p>Now editing @Model.User.Username's profile</p>
@if (!Model.User.Deleted)
{
<button hx-delete="/users/@Model.User.Id">Delete profile</button>
}
</div>
}
</div>
</article>
@if (isSelf)
{
<article>
<header>Password</header>
<div>
<form action="~/users/@Model.User.Id/change-password" method="post">
<div>
<label for="oldPassword">Old password:</label>
<input type="password" id="oldPassword" name="oldPassword" required>
</div>
<div>
<label for="newPassword">New password:</label>
<input type="password" id="newPassword" name="newPassword" required>
</div>
<div>
<button type="submit">
<span>Update</span>
</button>
</div>
</form>
@if (Model.Message is not null)
{
<div class="alert @(Model.Success ? "alert-tip" : "alert-caution")">
<p>@Model.Message</p>
</div>
}
</div>
</article>
}
else
{
<article>
<header>Password</header>
<div>
<form action="~/users/@Model.User.Id/set-password" method="post">
<div>
<label for="newPassword">New password:</label>
<input type="password" id="newPassword" name="newPassword" required>
</div>
<div>
<button type="submit">
<span>Set</span>
</button>
</div>
</form>
@if (Model.Message is not null)
{
<div class="alert @(Model.Success ? "alert-tip" : "alert-caution")">
<p>@Model.Message</p>
</div>
}
</div>
</article>
}
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@ -93,23 +93,6 @@ body {
margin-bottom: 60px; margin-bottom: 60px;
} }
body>header {
margin: 0;
padding-block: 0;
}
.nav-admin {
margin: 0;
padding: 0 20px;
background: #0a121b;
border-bottom: solid 1px rgba(255, 255, 255, 0.09);
}
.nav-admin li {
margin: 0;
padding: 5px
}
.nav-container { .nav-container {
margin: 0 20px 20px; margin: 0 20px 20px;
padding-top: 0; padding-top: 0;

File diff suppressed because one or more lines are too long