feat: initial.
This commit is contained in:
commit
81b824bb79
27 changed files with 1139 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
.idea/
|
||||||
|
mizuki.db
|
||||||
82
Controllers/FileController.cs
Normal file
82
Controllers/FileController.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mizuki.Dtos;
|
||||||
|
using Mizuki.Services;
|
||||||
|
using Mizuki.Validators;
|
||||||
|
|
||||||
|
namespace Mizuki.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file api
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uploadService"></param>
|
||||||
|
[Authorize]
|
||||||
|
[Route("/api/file")]
|
||||||
|
public class FileController(
|
||||||
|
UploadService uploadService,
|
||||||
|
LoginService loginService,
|
||||||
|
FormFileValidator formFileValidator)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formFile">The given file.</param>
|
||||||
|
/// <returns>Either the result, or an error.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("upload")]
|
||||||
|
public async Task<Results<Ok<FileCreationResultDto>, BadRequest<FileCreationErrorDto>>> UploadFile(
|
||||||
|
IFormFile formFile)
|
||||||
|
{
|
||||||
|
var result = await formFileValidator.ValidateAsync(formFile);
|
||||||
|
if (!result.IsValid)
|
||||||
|
{
|
||||||
|
if (result.Errors.Any(e => e.PropertyName == "Length"))
|
||||||
|
return TypedResults.BadRequest(new FileCreationErrorDto("The file is too big. (Max is 50MiB)"));
|
||||||
|
|
||||||
|
if (result.Errors.Any(e => e.PropertyName == "Filename"))
|
||||||
|
return TypedResults.BadRequest(new FileCreationErrorDto("Invalid filename."));
|
||||||
|
|
||||||
|
return TypedResults.BadRequest(new FileCreationErrorDto("Unknown validation error."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await loginService.GetActiveUser();
|
||||||
|
var upload = await uploadService.CreateUpload(user, formFile);
|
||||||
|
if (upload is null)
|
||||||
|
{
|
||||||
|
return TypedResults.BadRequest(new FileCreationErrorDto("There was a problem processing your upload, please try again later."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypedResults.Ok(new FileCreationResultDto(upload.Filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists all of the files for the logged in user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The list of all files this user has.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("all")]
|
||||||
|
public async Task<IEnumerable<FileDto>> ListAllFiles()
|
||||||
|
{
|
||||||
|
var user = await loginService.GetActiveUser();
|
||||||
|
var uploads = await uploadService.GetAllUploadsForUser(user);
|
||||||
|
|
||||||
|
return uploads.Select(u => new FileDto(
|
||||||
|
u.Filename,
|
||||||
|
u.OriginalFilename,
|
||||||
|
u.SizeInBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename of the file.</param>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("delete")]
|
||||||
|
public async Task DeleteFile(
|
||||||
|
[FromQuery] string filename)
|
||||||
|
{
|
||||||
|
var user = await loginService.GetActiveUser();
|
||||||
|
await uploadService.DeleteUpload(user, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Controllers/LoginController.cs
Normal file
81
Controllers/LoginController.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mizuki.Dtos;
|
||||||
|
using Mizuki.Services;
|
||||||
|
using Mizuki.Validators;
|
||||||
|
|
||||||
|
namespace Mizuki.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The login controller for Mizuki.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/user")]
|
||||||
|
public class LoginController(
|
||||||
|
UserService userService,
|
||||||
|
LoginService loginService,
|
||||||
|
LoginDataValidator loginDataValidator) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Logs into Mizuki as a given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The login data dto.</param>
|
||||||
|
/// <returns>Redirect.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("login")]
|
||||||
|
public async Task<RedirectResult> Login(
|
||||||
|
LoginDataDto dto)
|
||||||
|
{
|
||||||
|
if (!await userService.CheckPasswordForUser(dto.Username, dto.Password))
|
||||||
|
{
|
||||||
|
return Redirect("/login?error=Invalid username or password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userService.GetUserForUsername(dto.Username);
|
||||||
|
await loginService.LoginAsUser(user);
|
||||||
|
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs out of Mizuki.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Redirect.</returns>
|
||||||
|
[Authorize]
|
||||||
|
[Route("logout")]
|
||||||
|
public async Task<RedirectResult> Logout()
|
||||||
|
{
|
||||||
|
await loginService.Logout();
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new user in Mizuki.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The login data dto.</param>
|
||||||
|
/// <returns>Redirect.</returns>
|
||||||
|
[Route("register")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<RedirectResult> Register(
|
||||||
|
LoginDataDto dto)
|
||||||
|
{
|
||||||
|
if (await userService.UsernameTaken(dto.Username))
|
||||||
|
return Redirect("/register?error=This user already exists.");
|
||||||
|
|
||||||
|
var result = await loginDataValidator.ValidateAsync(dto);
|
||||||
|
if (!result.IsValid)
|
||||||
|
{
|
||||||
|
if (result.Errors.Any(e => e.PropertyName == "Username"))
|
||||||
|
return Redirect("/register?error=Invalid username.");
|
||||||
|
|
||||||
|
if (result.Errors.Any(e => e.PropertyName == "Password"))
|
||||||
|
return Redirect("/register?error=Invalid password.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await userService.CreateUser(
|
||||||
|
dto.Username,
|
||||||
|
dto.Password);
|
||||||
|
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Controllers/ServeController.cs
Normal file
37
Controllers/ServeController.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mizuki.Services;
|
||||||
|
|
||||||
|
namespace Mizuki.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The controller serving the uploaded files.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("/f")]
|
||||||
|
public class ServeController(
|
||||||
|
DriveService driveService,
|
||||||
|
UploadService uploadService) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get the file by its filename.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <returns>The file, or an error.</returns>
|
||||||
|
[Route("/{filename}")]
|
||||||
|
public async Task<Results<FileStreamHttpResult, NotFound>> GetFile(
|
||||||
|
[FromRoute] string filename)
|
||||||
|
{
|
||||||
|
var file = await uploadService.GetByFilename(filename);
|
||||||
|
if (file is null)
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
|
||||||
|
var stream = driveService.OpenFileByFilename(filename);
|
||||||
|
if (stream is null)
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
|
||||||
|
return TypedResults.File(
|
||||||
|
stream,
|
||||||
|
fileDownloadName: file.OriginalFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Database/MizukiDbContext.cs
Normal file
31
Database/MizukiDbContext.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Mizuki.Database.Models;
|
||||||
|
|
||||||
|
namespace Mizuki.Database;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The DB context for Mizuki.
|
||||||
|
/// </summary>
|
||||||
|
public class MizukiDbContext : DbContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The users.
|
||||||
|
/// </summary>
|
||||||
|
public virtual DbSet<User> Users { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The files.
|
||||||
|
/// </summary>
|
||||||
|
public virtual DbSet<Upload> Uploads { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<User>();
|
||||||
|
modelBuilder.Entity<Upload>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
|
=> options.UseSqlite($"Data Source=mizuki.db");
|
||||||
|
}
|
||||||
49
Database/Models/Upload.cs
Normal file
49
Database/Models/Upload.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Mizuki.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An uploaded file.
|
||||||
|
/// </summary>
|
||||||
|
[Index(nameof(Filename), IsUnique = true)]
|
||||||
|
public class Upload
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the uploaded file.
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
public required Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The filename.
|
||||||
|
/// </summary>
|
||||||
|
public required string Filename { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The filename.
|
||||||
|
/// </summary>
|
||||||
|
public required string OriginalFilename { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The size of the file in bytes.
|
||||||
|
/// </summary>
|
||||||
|
public required long SizeInBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time of upload.
|
||||||
|
/// </summary>
|
||||||
|
public required DateTimeOffset TimeOfUpload { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The author of the uploaded file.
|
||||||
|
/// </summary>
|
||||||
|
public required User Author { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the author.
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey(nameof(Author))]
|
||||||
|
public required Guid AuthorId { get; set; }
|
||||||
|
}
|
||||||
25
Database/Models/User.cs
Normal file
25
Database/Models/User.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mizuki.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A user.
|
||||||
|
/// </summary>
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the user.
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
public required Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The username.
|
||||||
|
/// </summary>
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hashed password.
|
||||||
|
/// </summary>
|
||||||
|
public required string PasswordHash { get; set; }
|
||||||
|
}
|
||||||
8
Dtos/FileCreationErrorDto.cs
Normal file
8
Dtos/FileCreationErrorDto.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Mizuki.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A file creation error.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Reason">The error reason.</param>
|
||||||
|
public class FileCreationErrorDto(
|
||||||
|
string Reason);
|
||||||
8
Dtos/FileCreationResultDto.cs
Normal file
8
Dtos/FileCreationResultDto.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Mizuki.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file creation result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Filename">The filename.</param>
|
||||||
|
public class FileCreationResultDto(
|
||||||
|
string Filename);
|
||||||
12
Dtos/FileDto.cs
Normal file
12
Dtos/FileDto.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Mizuki.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A DTO describing a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Filename">The filename.</param>
|
||||||
|
/// <param name="OriginalFilename">The original filename.</param>
|
||||||
|
/// <param name="SizeInBytes">The size in bytes.</param>
|
||||||
|
public record FileDto(
|
||||||
|
string Filename,
|
||||||
|
string OriginalFilename,
|
||||||
|
long SizeInBytes);
|
||||||
17
Dtos/LoginDataDto.cs
Normal file
17
Dtos/LoginDataDto.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Mizuki.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The DTO for login data.
|
||||||
|
/// </summary>
|
||||||
|
public class LoginDataDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The username.
|
||||||
|
/// </summary>
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The password.
|
||||||
|
/// </summary>
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
||||||
71
Helpers/Tid.cs
Normal file
71
Helpers/Tid.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Mizuki.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An AT protocol time-based ID.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct Tid
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The Base32 sort alphabet.
|
||||||
|
/// </summary>
|
||||||
|
private const string Base32SortAlphabet = "234567abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The TID value.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a TID from an integer representation of the TID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tid">The TID value.</param>
|
||||||
|
private Tid(ulong tid)
|
||||||
|
{
|
||||||
|
Value = NumericTidToStringTid(tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new TID for the current time.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current time.</returns>
|
||||||
|
public static Tid NewTid()
|
||||||
|
{
|
||||||
|
var microseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000;
|
||||||
|
var randomness = (uint)RandomNumberGenerator.GetInt32(int.MaxValue);
|
||||||
|
|
||||||
|
var value = (ulong)(((microseconds & 0x1F_FFFF_FFFF_FFFF) << 10) | (randomness & 0x3FF));
|
||||||
|
|
||||||
|
return new Tid(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An empty TID.
|
||||||
|
/// </summary>
|
||||||
|
public static Tid Empty => new Tid(0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a numeric TID to a string TID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tid">The numeric TID.</param>
|
||||||
|
/// <returns>The string TID.</returns>
|
||||||
|
private static string NumericTidToStringTid(ulong tid)
|
||||||
|
{
|
||||||
|
var value = 0x7FFF_FFFF_FFFF_FFFF & tid;
|
||||||
|
var output = "";
|
||||||
|
for (var i = 0; i < 13; i++)
|
||||||
|
{
|
||||||
|
output = Base32SortAlphabet[(int)(value & 0x1F)] + output;
|
||||||
|
value >>= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Migrations/20250112191512_Initial migration.Designer.cs
generated
Normal file
88
Migrations/20250112191512_Initial migration.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Mizuki.Database;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Mizuki.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MizukiDbContext))]
|
||||||
|
[Migration("20250112191512_Initial migration")]
|
||||||
|
partial class Initialmigration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Mizuki.Database.Models.Upload", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AuthorId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFilename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("SizeInBytes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("TimeOfUpload")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("Filename")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Uploads");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Mizuki.Database.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Mizuki.Database.Models.Upload", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Mizuki.Database.Models.User", "Author")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AuthorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Author");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Migrations/20250112191512_Initial migration.cs
Normal file
71
Migrations/20250112191512_Initial migration.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Mizuki.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initialmigration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Username = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Uploads",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Filename = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
OriginalFilename = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
SizeInBytes = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
TimeOfUpload = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
|
AuthorId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Uploads", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Uploads_Users_AuthorId",
|
||||||
|
column: x => x.AuthorId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Uploads_AuthorId",
|
||||||
|
table: "Uploads",
|
||||||
|
column: "AuthorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Uploads_Filename",
|
||||||
|
table: "Uploads",
|
||||||
|
column: "Filename",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Uploads");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
Migrations/MizukiDbContextModelSnapshot.cs
Normal file
85
Migrations/MizukiDbContextModelSnapshot.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Mizuki.Database;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Mizuki.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MizukiDbContext))]
|
||||||
|
partial class MizukiDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Mizuki.Database.Models.Upload", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("AuthorId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFilename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("SizeInBytes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("TimeOfUpload")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("Filename")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Uploads");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Mizuki.Database.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Mizuki.Database.Models.Upload", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Mizuki.Database.Models.User", "Author")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AuthorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Author");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Mizuki.csproj
Normal file
22
Mizuki.csproj
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||||
|
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
16
Mizuki.sln
Normal file
16
Mizuki.sln
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mizuki", "Mizuki.csproj", "{84F0FCED-314C-4FAB-B279-E104D79907B6}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{84F0FCED-314C-4FAB-B279-E104D79907B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{84F0FCED-314C-4FAB-B279-E104D79907B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{84F0FCED-314C-4FAB-B279-E104D79907B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{84F0FCED-314C-4FAB-B279-E104D79907B6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
37
Program.cs
Normal file
37
Program.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using FluentValidation;
|
||||||
|
using Mizuki.Database;
|
||||||
|
using Mizuki.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddValidatorsFromAssembly(
|
||||||
|
Assembly.GetCallingAssembly());
|
||||||
|
|
||||||
|
builder.Services.AddScoped<DriveService>();
|
||||||
|
builder.Services.AddScoped<LoginService>();
|
||||||
|
builder.Services.AddScoped<UploadService>();
|
||||||
|
builder.Services.AddScoped<UserService>();
|
||||||
|
builder.Services.AddDbContext<MizukiDbContext>();
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication("MizukiAuth")
|
||||||
|
.AddCookie("MizukiAuth", options =>
|
||||||
|
{
|
||||||
|
options.LoginPath = "/login";
|
||||||
|
options.LogoutPath = "/api/user/logout";
|
||||||
|
options.AccessDeniedPath = "/";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.MapGet("/", () => "Hello World!");
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapRazorPages();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
38
Properties/launchSettings.json
Normal file
38
Properties/launchSettings.json
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:25566",
|
||||||
|
"sslPort": 44302
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5118",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7215;http://localhost:5118",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Services/DriveService.cs
Normal file
50
Services/DriveService.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
namespace Mizuki.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The service responsible for managing file uploads.
|
||||||
|
/// </summary>
|
||||||
|
public class DriveService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The path to the uploads folder.
|
||||||
|
/// </summary>
|
||||||
|
private const string UploadsFolder = "./uploads";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The drive service.
|
||||||
|
/// </summary>
|
||||||
|
public DriveService()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(UploadsFolder))
|
||||||
|
Directory.CreateDirectory(UploadsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a file by its filename.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">Its filename.</param>
|
||||||
|
/// <returns>The file's stream.</returns>
|
||||||
|
public Stream? OpenFileByFilename(string filename)
|
||||||
|
{
|
||||||
|
var actualFilename = Path.GetFileName(filename);
|
||||||
|
var path = Path.Combine(UploadsFolder, actualFilename);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return File.OpenRead(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a file by its filename.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">Its filename.</param>
|
||||||
|
public void DeleteFileByFilename(string filename)
|
||||||
|
{
|
||||||
|
var actualFilename = Path.GetFileName(filename);
|
||||||
|
var path = Path.Combine(UploadsFolder, actualFilename);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Services/LoginService.cs
Normal file
57
Services/LoginService.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Mizuki.Database.Models;
|
||||||
|
|
||||||
|
namespace Mizuki.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The login service for Mizuki.
|
||||||
|
/// </summary>
|
||||||
|
public class LoginService(
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
UserService userService)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Logs in as a given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
public async Task LoginAsUser(
|
||||||
|
User user)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new(ClaimTypes.Name, user.Username)
|
||||||
|
};
|
||||||
|
|
||||||
|
var claimsIdentity = new ClaimsIdentity(claims, "MizukiAuth");
|
||||||
|
await httpContextAccessor.HttpContext!
|
||||||
|
.SignInAsync(
|
||||||
|
"MizukiAuth",
|
||||||
|
new ClaimsPrincipal(claimsIdentity),
|
||||||
|
new AuthenticationProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs out.
|
||||||
|
/// </summary>
|
||||||
|
public async Task Logout()
|
||||||
|
{
|
||||||
|
await httpContextAccessor.HttpContext!
|
||||||
|
.SignOutAsync("MizukiAuth");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the active user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The active user.</returns>
|
||||||
|
public async Task<User> GetActiveUser()
|
||||||
|
{
|
||||||
|
var username = httpContextAccessor.HttpContext!
|
||||||
|
.User
|
||||||
|
.FindFirst(ClaimTypes.Name)!
|
||||||
|
.ToString();
|
||||||
|
|
||||||
|
return await userService.GetUserForUsername(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
Services/UploadService.cs
Normal file
90
Services/UploadService.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Mizuki.Database;
|
||||||
|
using Mizuki.Database.Models;
|
||||||
|
using Mizuki.Helpers;
|
||||||
|
|
||||||
|
namespace Mizuki.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The service managing uploads.
|
||||||
|
/// </summary>
|
||||||
|
public class UploadService(
|
||||||
|
MizukiDbContext dbContext,
|
||||||
|
DriveService driveService)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new upload record for a given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="file">The form file.</param>
|
||||||
|
/// <returns>The created upload.</returns>
|
||||||
|
public async Task<Upload?> CreateUpload(
|
||||||
|
User user,
|
||||||
|
IFormFile file)
|
||||||
|
{
|
||||||
|
var upload = new Upload
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
|
||||||
|
Filename = Tid.NewTid().ToString(),
|
||||||
|
OriginalFilename = file.FileName,
|
||||||
|
SizeInBytes = file.Length,
|
||||||
|
|
||||||
|
TimeOfUpload = DateTimeOffset.UtcNow,
|
||||||
|
|
||||||
|
Author = user,
|
||||||
|
AuthorId = user.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Uploads.AddAsync(upload);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an upload.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="filename">The name of the file that the user wishes to delete.</param>
|
||||||
|
public async Task DeleteUpload(
|
||||||
|
User user,
|
||||||
|
string filename)
|
||||||
|
{
|
||||||
|
var upload = await dbContext.Uploads
|
||||||
|
.Where(u => u.AuthorId == user.Id && u.Filename == filename)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (upload is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
dbContext.Uploads.Remove(upload);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
driveService.DeleteFileByFilename(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all the uploads for a user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>Their uploads.</returns>
|
||||||
|
public async Task<IEnumerable<Upload>> GetAllUploadsForUser(
|
||||||
|
User user)
|
||||||
|
{
|
||||||
|
return await dbContext.Uploads
|
||||||
|
.OrderByDescending(u => u.TimeOfUpload)
|
||||||
|
.Where(u => u.AuthorId == user.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an upload by its filename.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename.</param>
|
||||||
|
/// <returns>The upload, if it exists.</returns>
|
||||||
|
public async Task<Upload?> GetByFilename(string filename)
|
||||||
|
{
|
||||||
|
return await dbContext.Uploads
|
||||||
|
.FirstOrDefaultAsync(u => u.Filename == filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Services/UserService.cs
Normal file
83
Services/UserService.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
using Isopoh.Cryptography.Argon2;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Mizuki.Database;
|
||||||
|
using Mizuki.Database.Models;
|
||||||
|
|
||||||
|
namespace Mizuki.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user service.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbContext">The Mizuki database context.</param>
|
||||||
|
public class UserService(
|
||||||
|
MizukiDbContext dbContext)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a user with the given username exists.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username.</param>
|
||||||
|
/// <returns>Whether they exist.</returns>
|
||||||
|
public async Task<bool> UsernameTaken(
|
||||||
|
string username)
|
||||||
|
{
|
||||||
|
return await dbContext.Users
|
||||||
|
.AnyAsync(u => u.Username == username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a user with the given username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username.</param>
|
||||||
|
/// <returns>The user model.</returns>
|
||||||
|
public async Task<User> GetUserForUsername(
|
||||||
|
string username)
|
||||||
|
{
|
||||||
|
return await dbContext.Users
|
||||||
|
.FirstAsync(u => u.Username == username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the password is valid for the given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The user's name.</param>
|
||||||
|
/// <param name="password">The password.</param>
|
||||||
|
/// <returns>Whether it's valid.</returns>
|
||||||
|
public async Task<bool> CheckPasswordForUser(
|
||||||
|
string username,
|
||||||
|
string password)
|
||||||
|
{
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Username == username);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Argon2.Verify(user.PasswordHash, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a user with the given username and password.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username.</param>
|
||||||
|
/// <param name="password">The password.</param>
|
||||||
|
/// <returns>Whether the user was created.</returns>
|
||||||
|
public async Task<bool> CreateUser(
|
||||||
|
string username,
|
||||||
|
string password)
|
||||||
|
{
|
||||||
|
if (await UsernameTaken(username))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var hash = Argon2.Hash(password);
|
||||||
|
var userModel = new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = username,
|
||||||
|
PasswordHash = hash
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Users.AddAsync(userModel);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Validators/FormFileValidator.cs
Normal file
26
Validators/FormFileValidator.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Mizuki.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The validator for uploaded form files.
|
||||||
|
/// </summary>
|
||||||
|
public class FormFileValidator : AbstractValidator<IFormFile>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs the rule set for the FormFileValidator.
|
||||||
|
/// </summary>
|
||||||
|
public FormFileValidator()
|
||||||
|
{
|
||||||
|
const int MiB = 1024 * 1024;
|
||||||
|
|
||||||
|
RuleFor(f => f.Length)
|
||||||
|
.LessThan(50 * MiB);
|
||||||
|
|
||||||
|
RuleFor(f => f.FileName)
|
||||||
|
.NotEmpty();
|
||||||
|
|
||||||
|
RuleFor(f => f.FileName)
|
||||||
|
.Matches(@"^(?!\s)(?!.*\s$)[^<>:""/\\|?*\x00-\x1F]+\.(?!\.)[^<>:""/\\|?*\x00-\x1F]{1,4}$|^(?!\s)(?!.*\s$)[^<>:""/\\|?*\x00-\x1F]+$");
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Validators/LoginDataValidator.cs
Normal file
31
Validators/LoginDataValidator.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using FluentValidation;
|
||||||
|
using Mizuki.Dtos;
|
||||||
|
|
||||||
|
namespace Mizuki.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The validator for the login data.
|
||||||
|
/// </summary>
|
||||||
|
public class LoginDataValidator : AbstractValidator<LoginDataDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs the ruleset for the LoginDataValidator.
|
||||||
|
/// </summary>
|
||||||
|
public LoginDataValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Username)
|
||||||
|
.NotEmpty();
|
||||||
|
|
||||||
|
RuleFor(x => x.Username)
|
||||||
|
.Matches("^[a-zA-Z0-9_.]*$");
|
||||||
|
|
||||||
|
RuleFor(x => x.Password)
|
||||||
|
.NotEmpty();
|
||||||
|
|
||||||
|
RuleFor(x => x.Password)
|
||||||
|
.MinimumLength(8);
|
||||||
|
|
||||||
|
RuleFor(x => x.Password)
|
||||||
|
.Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&*!])[A-Za-z\d@#$%^&*!]{8,}$");
|
||||||
|
}
|
||||||
|
}
|
||||||
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
appsettings.json
Normal file
9
appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue