Added user list, updated stuff
This commit is contained in:
parent
f503166d9c
commit
1bba76b71a
|
|
@ -6,6 +6,7 @@ namespace FastBlog.Core.Abstractions.Repositories.Users;
|
|||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> Get(int id);
|
||||
Task<UserPassword?> GetWithPassword(int id);
|
||||
Task<UserPassword?> GetByName(string username);
|
||||
Task<bool> SetPassword(int id, string newPassword);
|
||||
Task<PagedResponse<User>> GetUsers(PagedRequest pagedRequest);
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public static class DependencyInjection
|
|||
{
|
||||
var userService = provider.GetRequiredService<UserService>();
|
||||
var existingUserResult = await userService.GetUsers(new PagedRequest(1, 0));
|
||||
if (existingUserResult.Amount is 1)
|
||||
if (existingUserResult.Amount >= 1)
|
||||
return;
|
||||
|
||||
var newUser = new NewUser
|
||||
|
|
|
|||
|
|
@ -34,4 +34,16 @@
|
|||
<PackageReference Include="SQLite" Version="3.13.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ public class SessionOptions
|
|||
(bool slidingValid, bool absoluteValid) = (true, true);
|
||||
|
||||
if (EnableAbsoluteExpiration)
|
||||
slidingValid = session.CreatedAt.AddSeconds(AbsoluteExpirationSeconds) < DateTime.UtcNow;
|
||||
slidingValid = session.CreatedAt.AddSeconds(AbsoluteExpirationSeconds) > DateTime.UtcNow;
|
||||
|
||||
if (EnableSlidingExpiration)
|
||||
absoluteValid = session.LastUsed.AddSeconds(SlidingExpirationSeconds) < DateTime.UtcNow;
|
||||
absoluteValid = session.LastUsed.AddSeconds(SlidingExpirationSeconds) > DateTime.UtcNow;
|
||||
|
||||
return (slidingValid, absoluteValid);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,16 +15,23 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
|
|||
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)
|
||||
{
|
||||
const string sql = "select * from Users where username = @username and deleted is false";
|
||||
const string sql = "select * from Users where username = @username";
|
||||
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";
|
||||
const string sql = "update Users set passwordHash = @newPassword where id = @id and deleted = 0";
|
||||
using var connection = connectionFactory.Create();
|
||||
return await connection.ExecuteAsync(sql, new { id, newPassword }) > 0;
|
||||
}
|
||||
|
|
@ -34,6 +41,7 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
|
|||
const string sql = """
|
||||
select COUNT(*) from Users;
|
||||
select * from Users
|
||||
where deleted = 0
|
||||
limit @Amount offset @Offset;
|
||||
""";
|
||||
using var connection = connectionFactory.Create();
|
||||
|
|
@ -53,7 +61,7 @@ public sealed class UserRepository(SqliteConnectionFactory connectionFactory) :
|
|||
|
||||
public async Task<bool> DeleteUser(int userId)
|
||||
{
|
||||
const string sql = "update Users set deleted = true where id = @userId";
|
||||
const string sql = "update Users set deleted = true where id = @userId and deleted = 0";
|
||||
using var connection = connectionFactory.Create();
|
||||
return await connection.ExecuteAsync(sql, new { userId }) > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,41 @@ public sealed class UserService(IUserRepository repository)
|
|||
{
|
||||
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)
|
||||
{
|
||||
var user = await repository.GetByName(username);
|
||||
if (user is null) return null;
|
||||
if (user is null || user.Deleted) 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 sealed record ChangePasswordResult(bool Success, string Message);
|
||||
public async Task<ChangePasswordResult> ChangePassword(int id, string oldPassword, string 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ public class FilesController(FileService fileService) : Controller
|
|||
private const long MaxFileSize = 8L * 1024L * 1024L * 1024L;
|
||||
|
||||
[HttpGet]
|
||||
[SimpleAuth]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var files = await fileService.GetFiles(new PagedRequest(25, 0));
|
||||
|
|
@ -20,6 +21,7 @@ public class FilesController(FileService fileService) : Controller
|
|||
}
|
||||
|
||||
[HttpGet("{offset:int}")]
|
||||
[SimpleAuth]
|
||||
public async Task<IActionResult> Page(int offset)
|
||||
{
|
||||
var files = await fileService.GetFiles(new PagedRequest(25, offset));
|
||||
|
|
@ -27,6 +29,7 @@ public class FilesController(FileService fileService) : Controller
|
|||
}
|
||||
|
||||
[HttpDelete("{metaId:int}")]
|
||||
[SimpleAuth]
|
||||
public async Task<IActionResult> Delete(int metaId)
|
||||
{
|
||||
var result = await fileService.DeleteFile(metaId);
|
||||
|
|
@ -36,6 +39,7 @@ public class FilesController(FileService fileService) : Controller
|
|||
|
||||
|
||||
[HttpPost]
|
||||
[SimpleAuth]
|
||||
public async Task<IActionResult> Upload([FromForm(Name = "sourcePath")] string? sourcePath)
|
||||
{
|
||||
// TODO: Validate path to prevent directory traversal
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace FastBlog.Web.Controllers;
|
||||
|
||||
public class RateLimitController : Controller
|
||||
public class TooManyRequestsController : Controller
|
||||
{
|
||||
[HttpGet("/RateLimited")]
|
||||
[HttpGet("/too-many-requests")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
|
|
|
|||
|
|
@ -1,25 +1,41 @@
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using FastBlog.Core.Models;
|
||||
using FastBlog.Core.Models.Users;
|
||||
using FastBlog.Core.Services;
|
||||
using FastBlog.Web.Middlewares;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
namespace FastBlog.Web.Controllers;
|
||||
|
||||
|
||||
[EnableRateLimiting("fixed")]
|
||||
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 CreateRequest(string Login, string Password);
|
||||
|
||||
public sealed record LoginModel(string? Login, string? Password, List<string> Errors);
|
||||
|
||||
[HttpGet("/users/login")]
|
||||
public async Task<IActionResult> Login()
|
||||
public IActionResult LogIn()
|
||||
{
|
||||
if (HttpContext.IsUser())
|
||||
return Redirect("/");
|
||||
|
||||
return View(new LoginModel(null, null, []));
|
||||
}
|
||||
|
||||
[Htmx]
|
||||
[EnableRateLimiting("fixed")]
|
||||
[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 = [];
|
||||
// validation
|
||||
if (string.IsNullOrWhiteSpace(request.Login)
|
||||
|
|
@ -36,7 +52,7 @@ public sealed class UsersController(UserService userService, SessionService sess
|
|||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return View(new LoginModel(request.Login, request.Password, errors));
|
||||
return PartialView(new LoginModel(request.Login, request.Password, errors));
|
||||
}
|
||||
|
||||
Debug.Assert(request.Login is not null);
|
||||
|
|
@ -48,11 +64,124 @@ public sealed class UsersController(UserService userService, SessionService sess
|
|||
if (session is null)
|
||||
{
|
||||
errors.Add("Unknown user or incorrect password");
|
||||
return View(new LoginModel(request.Login, request.Password, errors));
|
||||
return PartialView(new LoginModel(request.Login, request.Password, errors));
|
||||
}
|
||||
|
||||
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("/");
|
||||
|
||||
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)
|
||||
|
|
@ -62,8 +191,8 @@ public sealed class UsersController(UserService userService, SessionService sess
|
|||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Secure = true,
|
||||
IsEssential = true
|
||||
// TODO: more options w/ configs, ill think bout that later
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
namespace FastBlog.Web;
|
||||
|
||||
public class HtmxAttribute
|
||||
{
|
||||
|
||||
}
|
||||
/// <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;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using FastBlog.Core.Models.Users;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FastBlog.Core.Models.Users;
|
||||
using FastBlog.Core.Services;
|
||||
|
||||
namespace FastBlog.Web.Middlewares;
|
||||
|
|
@ -12,7 +13,7 @@ public class SimpleAuthMiddleware(SessionService service, RequestDelegate next)
|
|||
|
||||
if (sessionToken is null && meta is not null)
|
||||
{
|
||||
context.Response.Redirect("/user/login");
|
||||
context.Response.Redirect("/");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -24,10 +25,10 @@ public class SimpleAuthMiddleware(SessionService service, RequestDelegate next)
|
|||
|
||||
var result = await service.Get(sessionToken!);
|
||||
|
||||
if (result.IsError)
|
||||
if (result.IsError || result.AsOk is null)
|
||||
{
|
||||
context.Response.Cookies.Delete("session");
|
||||
context.Response.Redirect("/user/login");
|
||||
context.Response.Cookies.Delete("session-t");
|
||||
context.Response.Redirect("/users/login");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ internal class SimpleAuthAttribute : Attribute;
|
|||
|
||||
public static class SimpleAuthExtensions
|
||||
{
|
||||
public static bool TryGetUser(this HttpContext context, out User? user)
|
||||
public static bool TryGetUser(this HttpContext context, [NotNullWhen(true)] out User? user)
|
||||
{
|
||||
user = context.Features.Get<User>();
|
||||
return user is not null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using FastBlog.Core;
|
||||
using FastBlog.Web;
|
||||
using FastBlog.Web.Middlewares;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -11,8 +13,33 @@ else
|
|||
builder.Services.AddCore(builder.Configuration);
|
||||
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();
|
||||
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
|
|
@ -23,8 +50,12 @@ app.UseStaticFiles();
|
|||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseMiddleware<SimpleAuthMiddleware>();
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Blogs}/{action=Index}/{slug?}");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@model FastBlog.Core.Models.Blogs.Blog
|
||||
|
||||
@{
|
||||
ViewBag.Title = Model.Metadata.Title;
|
||||
ViewData["Title"] = Model.Metadata.Title;
|
||||
}
|
||||
|
||||
@if(DateTime.UtcNow < Model.Metadata.CreatedAt)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Files";
|
||||
ViewData["Title"] = "Files";
|
||||
}
|
||||
|
||||
<div class="container" id="file-list">
|
||||
|
|
@ -22,15 +22,6 @@
|
|||
<input type="file" name="file" required/>
|
||||
<label for="upload-path">Path to upload (optional)</label>
|
||||
<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">
|
||||
Upload
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using FastBlog.Web.Middlewares
|
||||
@using Microsoft.Extensions.Options
|
||||
@inject IOptions<DisplayOptions> options;
|
||||
|
||||
@{
|
||||
ViewContext.HttpContext.Response.Headers.Add("Vary", "Hx-Request");
|
||||
}
|
||||
|
||||
@if (ViewContext.HttpContext.Request.Headers["Hx-Request"].Contains("true"))
|
||||
{
|
||||
@RenderBody()
|
||||
return;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
|
|
@ -24,20 +22,52 @@ return;
|
|||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
|
||||
</head>
|
||||
<body>
|
||||
<script src="~/js/htmx.min.js"></script>
|
||||
<header class="nav-container">
|
||||
<script src="~/lib/htmx/htmx.2.min.js"></script>
|
||||
<header>
|
||||
@if (Context.IsUser())
|
||||
{
|
||||
<div class="nav-admin">
|
||||
<nav>
|
||||
<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>
|
||||
<ul>
|
||||
<li><a href="~/"><h1>@options.Value.Title</h1></a></li>
|
||||
<li><a href="~/list">Blog</a></li>
|
||||
<li><a href="~/edit">New</a></li>
|
||||
<li><a href="~/files">Files</a></li>
|
||||
<li>
|
||||
<a href="~/">
|
||||
<h1>@options.Value.Title</h1>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="~/list">Blog</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
@foreach (var link in options.Value.Links)
|
||||
{
|
||||
<li><a href="@link.Value">@link.Key</a></li>
|
||||
<li>
|
||||
<a href="@link.Value">@link.Key</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
@{
|
||||
ViewBag.Title = "Rate limit exceeded";
|
||||
ViewData["Title"] = "Rate limit exceeded";
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<article>
|
||||
<header>Rate limit exceeded!</header>
|
||||
<header>Rate limit exceeded</header>
|
||||
<div>
|
||||
<p>You have sent too many requests!</p>
|
||||
<p>You have sent too many requests</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
@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>
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
@model FastBlog.Web.Controllers.UsersController.LoginModel
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Login";
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
|
||||
<div id="login-form" class="container">
|
||||
<article >
|
||||
<form
|
||||
hx-post="/users/login"
|
||||
hx-swap="outerHTML"
|
||||
|
|
@ -18,7 +17,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<label for="login">Password:</label>
|
||||
<input type="password" id="password" name="password" value="@Model.Login"
|
||||
<input type="password" id="password" name="password" value="@Model.Password"
|
||||
minlength="3"
|
||||
required>
|
||||
</div>
|
||||
|
|
@ -29,7 +28,10 @@
|
|||
</div>
|
||||
@foreach (var error in Model.Errors)
|
||||
{
|
||||
<div class="alert-caution">error</div>
|
||||
<div class="alert alert-caution">
|
||||
<p>@error</p>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
|
@ -1,2 +1,86 @@
|
|||
@model TModel
|
||||
@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>
|
||||
|
|
@ -93,6 +93,23 @@ body {
|
|||
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 {
|
||||
margin: 0 20px 20px;
|
||||
padding-top: 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue