From e2a448706476cd468bc62eafaa2ab5a69a918c1e Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Wed, 15 Jan 2025 21:13:51 +0100 Subject: [PATCH] more stuff --- Controllers/LoginController.cs | 56 ++++++++++++++++++- Dtos/FileCreationErrorDto.cs | 2 +- Dtos/FileCreationResultDto.cs | 2 +- Dtos/PasswordChangeDto.cs | 10 ++++ Dtos/UserInfoDto.cs | 10 ++++ Services/UserService.cs | 21 ++++++- Validators/PasswordChangeValidator.cs | 31 ++++++++++ mizuki-frontend/src/App.vue | 2 + mizuki-frontend/src/components/FileList.vue | 13 ++++- .../src/components/FileListItem.vue | 29 ++++++++-- .../src/components/PasswordChanger.vue | 44 ++++++++++++++- mizuki-frontend/src/components/Uploader.vue | 3 + .../src/dto/password-change-dto.ts | 4 ++ mizuki-frontend/src/main.ts | 2 + mizuki-frontend/src/views/HomeView.vue | 11 +++- mizuki-frontend/src/views/RegisterView.vue | 14 ++++- 16 files changed, 232 insertions(+), 22 deletions(-) create mode 100644 Dtos/PasswordChangeDto.cs create mode 100644 Dtos/UserInfoDto.cs create mode 100644 Validators/PasswordChangeValidator.cs create mode 100644 mizuki-frontend/src/dto/password-change-dto.ts diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs index ec75737..31bc72f 100644 --- a/Controllers/LoginController.cs +++ b/Controllers/LoginController.cs @@ -1,3 +1,4 @@ +using Isopoh.Cryptography.Argon2; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -15,7 +16,8 @@ namespace Mizuki.Controllers; public class LoginController( UserService userService, LoginService loginService, - LoginDataValidator loginDataValidator) : ControllerBase + LoginDataValidator loginDataValidator, + PasswordChangeValidator passwordChangeValidator) : ControllerBase { /// /// Logs into Mizuki as a given user. @@ -75,9 +77,11 @@ public class LoginController( return Redirect("/register?error=Invalid password."); } - await userService.CreateUser( + var user = await userService.CreateUser( dto.Username, dto.Password); + + await loginService.LoginAsUser(user); return Redirect("/"); } @@ -102,4 +106,52 @@ public class LoginController( return TypedResults.Forbid(); } } + + /// + /// Checks whether we have logged in. + /// + /// Either an OK or a forbidden result. + [Route("change_password")] + [HttpPost] + [Authorize] + public async Task> ChangePassword( + [FromBody] PasswordChangeDto dto) + { + try + { + var user = await loginService.GetActiveUser(); + if (!await userService.CheckPasswordForUser(user.Username, dto.OldPassword)) + { + return TypedResults.Forbid(); + } + + var validation = await passwordChangeValidator.ValidateAsync(dto); + if (!validation.IsValid) + { + return TypedResults.BadRequest(); + } + + await userService.UpdatePasswordFor(user, dto.NewPassword); + return TypedResults.Ok(); + } + catch + { + return TypedResults.Forbid(); + } + } + + /// + /// Gets the info for this user. + /// + /// The user's info. + [Route("info")] + [HttpGet] + [Authorize] + public async Task Info() + { + var user = await loginService.GetActiveUser(); + return new UserInfoDto( + user.Username, + null); + } } \ No newline at end of file diff --git a/Dtos/FileCreationErrorDto.cs b/Dtos/FileCreationErrorDto.cs index 2130484..946b1bb 100644 --- a/Dtos/FileCreationErrorDto.cs +++ b/Dtos/FileCreationErrorDto.cs @@ -4,5 +4,5 @@ namespace Mizuki.Dtos; /// A file creation error. /// /// The error reason. -public class FileCreationErrorDto( +public record FileCreationErrorDto( string Reason); \ No newline at end of file diff --git a/Dtos/FileCreationResultDto.cs b/Dtos/FileCreationResultDto.cs index e504f3b..b71dffc 100644 --- a/Dtos/FileCreationResultDto.cs +++ b/Dtos/FileCreationResultDto.cs @@ -4,5 +4,5 @@ namespace Mizuki.Dtos; /// The file creation result. /// /// The filename. -public class FileCreationResultDto( +public record FileCreationResultDto( string Filename); \ No newline at end of file diff --git a/Dtos/PasswordChangeDto.cs b/Dtos/PasswordChangeDto.cs new file mode 100644 index 0000000..0916cd8 --- /dev/null +++ b/Dtos/PasswordChangeDto.cs @@ -0,0 +1,10 @@ +namespace Mizuki.Dtos; + +/// +/// The password change DTO. +/// +/// The old password. +/// The new password. +public record PasswordChangeDto( + string OldPassword, + string NewPassword); \ No newline at end of file diff --git a/Dtos/UserInfoDto.cs b/Dtos/UserInfoDto.cs new file mode 100644 index 0000000..81d0cb2 --- /dev/null +++ b/Dtos/UserInfoDto.cs @@ -0,0 +1,10 @@ +namespace Mizuki.Dtos; + +/// +/// The user info. +/// +/// The username. +/// The avatar link. +public record UserInfoDto( + string Username, + string? Avatar); \ No newline at end of file diff --git a/Services/UserService.cs b/Services/UserService.cs index 2d18770..8bfea77 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -61,12 +61,12 @@ public class UserService( /// The username. /// The password. /// Whether the user was created. - public async Task CreateUser( + public async Task CreateUser( string username, string password) { if (await UsernameTaken(username)) - return false; + return null; var hash = Argon2.Hash(password); var userModel = new User @@ -78,6 +78,21 @@ public class UserService( await dbContext.Users.AddAsync(userModel); await dbContext.SaveChangesAsync(); - return true; + return userModel; + } + + /// + /// Updates the password for a given user. + /// + /// The user. + /// The new password. + public async Task UpdatePasswordFor( + User user, + string newPassword) + { + user.PasswordHash = Argon2.Hash(newPassword); + dbContext.Users.Update(user); + + await dbContext.SaveChangesAsync(); } } \ No newline at end of file diff --git a/Validators/PasswordChangeValidator.cs b/Validators/PasswordChangeValidator.cs new file mode 100644 index 0000000..37fb697 --- /dev/null +++ b/Validators/PasswordChangeValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using Mizuki.Dtos; + +namespace Mizuki.Validators; + +/// +/// The data validator for the PasswordChangeDto. +/// +public class PasswordChangeValidator : AbstractValidator +{ + /// + /// Constructs the ruleset for the PasswordChangeValidator. + /// + public PasswordChangeValidator() + { + RuleFor(x => x.OldPassword) + .NotEmpty(); + + RuleFor(x => x.OldPassword) + .MinimumLength(8); + + RuleFor(x => x.NewPassword) + .NotEmpty(); + + RuleFor(x => x.NewPassword) + .MinimumLength(8); + + RuleFor(x => x.NewPassword) + .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&*!])[A-Za-z\d@#$%^&*!]{8,}$"); + } +} \ No newline at end of file diff --git a/mizuki-frontend/src/App.vue b/mizuki-frontend/src/App.vue index 1efcabf..21b6676 100644 --- a/mizuki-frontend/src/App.vue +++ b/mizuki-frontend/src/App.vue @@ -1,10 +1,12 @@ diff --git a/mizuki-frontend/src/components/FileList.vue b/mizuki-frontend/src/components/FileList.vue index e4d4356..abf40d4 100644 --- a/mizuki-frontend/src/components/FileList.vue +++ b/mizuki-frontend/src/components/FileList.vue @@ -6,7 +6,7 @@ import FileListItem from "@/components/FileListItem.vue"; import {useToast} from "primevue/usetoast"; - const files = ref([]); + const files = ref(null); const toast = useToast(); onBeforeMount(async () => { @@ -30,16 +30,23 @@ }); await refresh(); }; + + defineExpose({ + refresh + }); diff --git a/mizuki-frontend/src/components/FileListItem.vue b/mizuki-frontend/src/components/FileListItem.vue index 0c5aa18..f78a3cb 100644 --- a/mizuki-frontend/src/components/FileListItem.vue +++ b/mizuki-frontend/src/components/FileListItem.vue @@ -3,6 +3,9 @@ import Card from 'primevue/card'; import Button from 'primevue/button'; import {computed} from "vue"; + import { useConfirm } from "primevue/useconfirm"; + + const confirm = useConfirm(); const props = defineProps<{ item: FileDto @@ -15,12 +18,28 @@ }) const removeItem = async () => { - await fetch(`/api/file/delete?filename=${props.item.filename}`, { - method: 'GET', - credentials: 'include' - }); + confirm.require({ + message: `Are you sure you want to delete ${props.item.originalFilename}?`, + header: 'Confirmation', + icon: 'pi pi-exclamation-triangle', + rejectProps: { + label: 'Cancel', + severity: 'secondary', + outlined: true + }, + acceptProps: { + label: 'Delete' + }, + accept: async () => { + await fetch(`/api/file/delete?filename=${props.item.filename}`, { + method: 'GET', + credentials: 'include' + }); - emit("deleted"); + emit("deleted"); + }, + reject: () => {} + }); } const download = () => { diff --git a/mizuki-frontend/src/components/PasswordChanger.vue b/mizuki-frontend/src/components/PasswordChanger.vue index 18a1774..bfe0639 100644 --- a/mizuki-frontend/src/components/PasswordChanger.vue +++ b/mizuki-frontend/src/components/PasswordChanger.vue @@ -5,6 +5,9 @@ import Password from "primevue/password"; import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms"; import Button from "primevue/button"; import Divider from "primevue/divider"; +import {useToast} from "primevue/usetoast"; + +const toast = useToast(); const formResolver = (ev: FormResolverOptions): Record => { const resp = { @@ -31,11 +34,48 @@ const formResolver = (ev: FormResolverOptions): Record => { return resp; }; -const onFormSubmit = (event: FormSubmitEvent) => { +const onFormSubmit = async (event: FormSubmitEvent) => { if (!event.valid) { + toast.add({ + severity: "error", + detail: "Invalid data supplied." + }); + return; + } + + const resp = await fetch("/api/user/change_password", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + oldPassword: event.states.CurrentPassword.value, + newPassword: event.states.Password.value + }), + credentials: "include" + }) + + if (resp.status == 400) { + toast.add({ + severity: "error", + detail: "Password didn't pass security validation." + }); + } else if (resp.status == 403) { + toast.add({ + severity: "error", + detail: "The old password was incorrect." + }); + } else if (resp.status == 200) { + toast.add({ + severity: "success", + detail: "Password changed!" + }); } else { - + toast.add({ + severity: "error", + detail: "An unexpected error has occurred." + }); } }; diff --git a/mizuki-frontend/src/components/Uploader.vue b/mizuki-frontend/src/components/Uploader.vue index 3e08294..3b6b5a9 100644 --- a/mizuki-frontend/src/components/Uploader.vue +++ b/mizuki-frontend/src/components/Uploader.vue @@ -10,6 +10,8 @@ import type {FileCreationResultDto} from "@/dto/file-creation-result-dto.ts"; import type {FileCreationErrorDto} from "@/dto/file-creation-error-dto.ts"; + const emit = defineEmits(["uploaded"]); + const totalSize = ref(0); const totalSizePercent = ref(0); const files = ref([]); @@ -48,6 +50,7 @@ if ('filename' in resp) { filename.value = resp.filename; visible.value = true; + emit("uploaded"); } else if ('reason' in resp) { toast.add({ severity: "error", diff --git a/mizuki-frontend/src/dto/password-change-dto.ts b/mizuki-frontend/src/dto/password-change-dto.ts new file mode 100644 index 0000000..97a3fdd --- /dev/null +++ b/mizuki-frontend/src/dto/password-change-dto.ts @@ -0,0 +1,4 @@ +export interface PasswordChangeDto { + oldPassword: string; + newPassword: string; +} \ No newline at end of file diff --git a/mizuki-frontend/src/main.ts b/mizuki-frontend/src/main.ts index e82f937..49d3fc8 100644 --- a/mizuki-frontend/src/main.ts +++ b/mizuki-frontend/src/main.ts @@ -6,6 +6,7 @@ import router from './router' import PrimeVue from 'primevue/config'; import ToastService from 'primevue/toastservice'; import Aura from '@primevue/themes/aura'; +import ConfirmationService from 'primevue/confirmationservice'; const app = createApp(App) @@ -19,5 +20,6 @@ app.use(PrimeVue, { } }); app.use(ToastService); +app.use(ConfirmationService); app.mount('#app') diff --git a/mizuki-frontend/src/views/HomeView.vue b/mizuki-frontend/src/views/HomeView.vue index 2e87efa..ff3ddde 100644 --- a/mizuki-frontend/src/views/HomeView.vue +++ b/mizuki-frontend/src/views/HomeView.vue @@ -1,5 +1,5 @@ diff --git a/mizuki-frontend/src/views/RegisterView.vue b/mizuki-frontend/src/views/RegisterView.vue index 3c33190..7e8498d 100644 --- a/mizuki-frontend/src/views/RegisterView.vue +++ b/mizuki-frontend/src/views/RegisterView.vue @@ -1,7 +1,7 @@