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 FastBlog.Core.Abstractions.Repositories.Blogs;
|
||||
using FastBlog.Core.Abstractions.Repositories.Files;
|
||||
using FastBlog.Core.Abstractions.Repositories.Users;
|
||||
using FastBlog.Core.Db;
|
||||
using FastBlog.Core.Models.Blogs;
|
||||
using FastBlog.Core.Options;
|
||||
|
|
@ -17,18 +18,23 @@ public static class DependencyInjection
|
|||
{
|
||||
public static IServiceCollection AddCore(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// applicatrion
|
||||
// application
|
||||
services.AddSingleton<BlogService>();
|
||||
services.AddSingleton<FileService>();
|
||||
services.AddSingleton<UserService>();
|
||||
services.AddSingleton<SessionService>();
|
||||
|
||||
// options
|
||||
services.Configure<FileStoreOptions>(configuration.GetSection("FileStore"));
|
||||
services.Configure<SessionOptions>(configuration.GetSection("Sessions"));
|
||||
|
||||
// infrastructure
|
||||
services.AddSingleton<IBlogMetaRepository, BlogMetaRepository>();
|
||||
services.AddSingleton<IBlogFileRepository, BlogFileRepository>();
|
||||
services.AddSingleton<IFileMetaRepository, FileMetaRepository>();
|
||||
services.AddSingleton<IFileRepository, FileRepository>();
|
||||
services.AddSingleton<ISessionRepository,SessionRepository>();
|
||||
services.AddSingleton<IUserRepository, UserRepository>();
|
||||
|
||||
// storage
|
||||
var connectionString = configuration.GetConnectionString("Sqlite");
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<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">
|
||||
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.4\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
@ -16,6 +19,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Dapper.SqlBuilder" Version="2.0.78" />
|
||||
<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.Blogs;
|
||||
using FastBlog.Core.Services;
|
||||
using FastBlog.Web.Middlewares;
|
||||
using FastBlog.Web.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ public class BlogsController(BlogService service) : Controller
|
|||
return View(blog);
|
||||
}
|
||||
|
||||
|
||||
[SimpleAuth]
|
||||
[HttpGet]
|
||||
[Route("unpublished/{*slug}")]
|
||||
public async Task<IActionResult> Unpublished(string? slug)
|
||||
|
|
@ -53,6 +54,7 @@ public class BlogsController(BlogService service) : Controller
|
|||
return View(blog);
|
||||
}
|
||||
|
||||
[SimpleAuth]
|
||||
[HttpGet("list/edit")]
|
||||
public async Task<IActionResult> ListEdit(
|
||||
[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)));
|
||||
}
|
||||
|
||||
[SimpleAuth]
|
||||
[HttpGet]
|
||||
[Route("edit/{id:int?}")]
|
||||
public async ValueTask<IActionResult> Edit(int? id)
|
||||
|
|
@ -116,6 +119,7 @@ public class BlogsController(BlogService service) : Controller
|
|||
});
|
||||
}
|
||||
|
||||
[SimpleAuth]
|
||||
[HttpPost]
|
||||
[Route("edit")]
|
||||
public async Task<IActionResult> Edit([FromForm] EditBlog editBlog)
|
||||
|
|
@ -152,6 +156,7 @@ public class BlogsController(BlogService service) : Controller
|
|||
return Redirect($"/blogs/{editBlog.Slug}");
|
||||
}
|
||||
|
||||
[SimpleAuth]
|
||||
[HttpPost]
|
||||
[Route("preview")]
|
||||
public IActionResult Preview([FromForm] EditBlog editBlog)
|
||||
|
|
@ -175,6 +180,7 @@ public class BlogsController(BlogService service) : Controller
|
|||
});
|
||||
}
|
||||
|
||||
[SimpleAuth]
|
||||
[HttpDelete]
|
||||
[Route("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using FastBlog.Core.Models;
|
||||
using FastBlog.Core.Models.Files;
|
||||
using FastBlog.Core.Services;
|
||||
using FastBlog.Web.Middlewares;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace FastBlog.Web.Controllers;
|
||||
|
||||
[SimpleAuth]
|
||||
[Route("files")]
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="md\" />
|
||||
<Folder Include="wwwroot\static\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace FastBlog.Web.Helpers;
|
||||
|
||||
public static class PropertyHelper
|
||||
public static class HtmlPropertyHelper
|
||||
{
|
||||
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
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Blog";
|
||||
}
|
||||
|
||||
<link rel="stylesheet" href="/lib/simplemde/simplemde.min.css">
|
||||
|
||||
<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)
|
||||
|
||||
<br>
|
||||
|
||||
@if(Context.IsUser())
|
||||
{
|
||||
<div class="container">
|
||||
<hr/>
|
||||
@await Html.PartialAsync("Blogs/ManageCard", Model.Metadata)
|
||||
</div>
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Blogs";
|
||||
ViewData["Title"] = "Blogs";
|
||||
}
|
||||
|
||||
<div class="container" id="file-list">
|
||||
|
|
@ -107,14 +107,14 @@
|
|||
hx-get="/edit-list?offset=@(Model.Offset - 25)"
|
||||
hx-swap="outerHTML"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
hx-get="/edit-list?offset=@(Model.Offset + 25)"
|
||||
hx-swap="outerHTML"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
@using FastBlog.Web.Helpers
|
||||
@model FastBlog.Core.Models.PagedResponse<FastBlog.Core.Models.Blogs.BlogMeta>
|
||||
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Blogs";
|
||||
ViewData["Title"] = "Blogs";
|
||||
}
|
||||
|
||||
<div class="container" id="file-list">
|
||||
|
||||
|
||||
<br/>
|
||||
<article>
|
||||
<header>
|
||||
<a href="/edit">
|
||||
|
|
@ -107,14 +103,14 @@
|
|||
hx-get="/edit-list?offset=@(Model.Offset - 25)"
|
||||
hx-swap="outerHTML"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
hx-get="/edit-list?offset=@(Model.Offset + 25)"
|
||||
hx-swap="outerHTML"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
@model FastBlog.Core.Models.Blogs.Blog
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Preview of " + Model.Metadata.Title;
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("Blogs/Content", Model)
|
||||
|
||||
<hr/>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,18 @@
|
|||
<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">
|
||||
|
||||
@Html.RadioButtonFor()
|
||||
|
||||
<button type="submit" class="btn-fw">
|
||||
Upload
|
||||
</button>
|
||||
|
|
@ -74,14 +86,14 @@
|
|||
hx-get="/files?page=@(Model.Offset - 25)"
|
||||
hx-swap="outerHTML"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
hx-get="/files?page=@(Model.Offset + 25)"
|
||||
hx-swap="outerHTML"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ return;
|
|||
<nav>
|
||||
<ul>
|
||||
<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="~/files">Files</a></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -19,5 +19,11 @@
|
|||
"GitHub": "https://github.com",
|
||||
"Mail": "mailto:mail@example.com"
|
||||
}
|
||||
},
|
||||
"Sessions": {
|
||||
"SlidingExpirationSeconds": 2592000,
|
||||
"AbsoluteExpirationSeconds": 7776000,
|
||||
"EnableAbsoluteExpiration": true,
|
||||
"EnableSlidingExpiration": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue