more stuff

This commit is contained in:
purifetchi 2025-01-15 21:13:51 +01:00
parent 04a01788f8
commit e2a4487064
16 changed files with 232 additions and 22 deletions

View file

@ -1,3 +1,4 @@
using Isopoh.Cryptography.Argon2;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -15,7 +16,8 @@ namespace Mizuki.Controllers;
public class LoginController( public class LoginController(
UserService userService, UserService userService,
LoginService loginService, LoginService loginService,
LoginDataValidator loginDataValidator) : ControllerBase LoginDataValidator loginDataValidator,
PasswordChangeValidator passwordChangeValidator) : ControllerBase
{ {
/// <summary> /// <summary>
/// Logs into Mizuki as a given user. /// Logs into Mizuki as a given user.
@ -75,9 +77,11 @@ public class LoginController(
return Redirect("/register?error=Invalid password."); return Redirect("/register?error=Invalid password.");
} }
await userService.CreateUser( var user = await userService.CreateUser(
dto.Username, dto.Username,
dto.Password); dto.Password);
await loginService.LoginAsUser(user);
return Redirect("/"); return Redirect("/");
} }
@ -102,4 +106,52 @@ public class LoginController(
return TypedResults.Forbid(); return TypedResults.Forbid();
} }
} }
/// <summary>
/// Checks whether we have logged in.
/// </summary>
/// <returns>Either an OK or a forbidden result.</returns>
[Route("change_password")]
[HttpPost]
[Authorize]
public async Task<Results<Ok, BadRequest, ForbidHttpResult>> 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();
}
}
/// <summary>
/// Gets the info for this user.
/// </summary>
/// <returns>The user's info.</returns>
[Route("info")]
[HttpGet]
[Authorize]
public async Task<UserInfoDto> Info()
{
var user = await loginService.GetActiveUser();
return new UserInfoDto(
user.Username,
null);
}
} }

View file

@ -4,5 +4,5 @@ namespace Mizuki.Dtos;
/// A file creation error. /// A file creation error.
/// </summary> /// </summary>
/// <param name="Reason">The error reason.</param> /// <param name="Reason">The error reason.</param>
public class FileCreationErrorDto( public record FileCreationErrorDto(
string Reason); string Reason);

View file

@ -4,5 +4,5 @@ namespace Mizuki.Dtos;
/// The file creation result. /// The file creation result.
/// </summary> /// </summary>
/// <param name="Filename">The filename.</param> /// <param name="Filename">The filename.</param>
public class FileCreationResultDto( public record FileCreationResultDto(
string Filename); string Filename);

10
Dtos/PasswordChangeDto.cs Normal file
View file

@ -0,0 +1,10 @@
namespace Mizuki.Dtos;
/// <summary>
/// The password change DTO.
/// </summary>
/// <param name="OldPassword">The old password.</param>
/// <param name="NewPassword">The new password.</param>
public record PasswordChangeDto(
string OldPassword,
string NewPassword);

10
Dtos/UserInfoDto.cs Normal file
View file

@ -0,0 +1,10 @@
namespace Mizuki.Dtos;
/// <summary>
/// The user info.
/// </summary>
/// <param name="Username">The username.</param>
/// <param name="Avatar">The avatar link.</param>
public record UserInfoDto(
string Username,
string? Avatar);

View file

@ -61,12 +61,12 @@ public class UserService(
/// <param name="username">The username.</param> /// <param name="username">The username.</param>
/// <param name="password">The password.</param> /// <param name="password">The password.</param>
/// <returns>Whether the user was created.</returns> /// <returns>Whether the user was created.</returns>
public async Task<bool> CreateUser( public async Task<User?> CreateUser(
string username, string username,
string password) string password)
{ {
if (await UsernameTaken(username)) if (await UsernameTaken(username))
return false; return null;
var hash = Argon2.Hash(password); var hash = Argon2.Hash(password);
var userModel = new User var userModel = new User
@ -78,6 +78,21 @@ public class UserService(
await dbContext.Users.AddAsync(userModel); await dbContext.Users.AddAsync(userModel);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return true; return userModel;
}
/// <summary>
/// Updates the password for a given user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="newPassword">The new password.</param>
public async Task UpdatePasswordFor(
User user,
string newPassword)
{
user.PasswordHash = Argon2.Hash(newPassword);
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync();
} }
} }

View file

@ -0,0 +1,31 @@
using FluentValidation;
using Mizuki.Dtos;
namespace Mizuki.Validators;
/// <summary>
/// The data validator for the PasswordChangeDto.
/// </summary>
public class PasswordChangeValidator : AbstractValidator<PasswordChangeDto>
{
/// <summary>
/// Constructs the ruleset for the PasswordChangeValidator.
/// </summary>
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,}$");
}
}

View file

@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import ConfirmDialog from 'primevue/confirmdialog';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
</script> </script>
<template> <template>
<Toast /> <Toast />
<ConfirmDialog></ConfirmDialog>
<RouterView /> <RouterView />
</template> </template>

View file

@ -6,7 +6,7 @@
import FileListItem from "@/components/FileListItem.vue"; import FileListItem from "@/components/FileListItem.vue";
import {useToast} from "primevue/usetoast"; import {useToast} from "primevue/usetoast";
const files = ref<FileDto[]>([]); const files = ref<FileDto[] | null>(null);
const toast = useToast(); const toast = useToast();
onBeforeMount(async () => { onBeforeMount(async () => {
@ -30,16 +30,23 @@
}); });
await refresh(); await refresh();
}; };
defineExpose({
refresh
});
</script> </script>
<template> <template>
<Panel header="Uploaded files"> <Panel header="Uploaded files">
<div v-if="files.length > 0"> <ProgressSpinner v-if="files === null" />
<div v-else-if="files.length < 1" class="text-center">
No files uploaded yet.
</div>
<div v-else>
<div class="inline-block m-3" v-for="file of files" :key="file.filename"> <div class="inline-block m-3" v-for="file of files" :key="file.filename">
<FileListItem :item="file" @deleted="onDeleted" /> <FileListItem :item="file" @deleted="onDeleted" />
</div> </div>
</div> </div>
<ProgressSpinner v-else />
</Panel> </Panel>
</template> </template>

View file

@ -3,6 +3,9 @@
import Card from 'primevue/card'; import Card from 'primevue/card';
import Button from 'primevue/button'; import Button from 'primevue/button';
import {computed} from "vue"; import {computed} from "vue";
import { useConfirm } from "primevue/useconfirm";
const confirm = useConfirm();
const props = defineProps<{ const props = defineProps<{
item: FileDto item: FileDto
@ -15,12 +18,28 @@
}) })
const removeItem = async () => { const removeItem = async () => {
await fetch(`/api/file/delete?filename=${props.item.filename}`, { confirm.require({
method: 'GET', message: `Are you sure you want to delete ${props.item.originalFilename}?`,
credentials: 'include' 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 = () => { const download = () => {

View file

@ -5,6 +5,9 @@ import Password from "primevue/password";
import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms"; import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms";
import Button from "primevue/button"; import Button from "primevue/button";
import Divider from "primevue/divider"; import Divider from "primevue/divider";
import {useToast} from "primevue/usetoast";
const toast = useToast();
const formResolver = (ev: FormResolverOptions): Record<string, any> => { const formResolver = (ev: FormResolverOptions): Record<string, any> => {
const resp = { const resp = {
@ -31,11 +34,48 @@ const formResolver = (ev: FormResolverOptions): Record<string, any> => {
return resp; return resp;
}; };
const onFormSubmit = (event: FormSubmitEvent) => { const onFormSubmit = async (event: FormSubmitEvent) => {
if (!event.valid) { 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 { } else {
toast.add({
severity: "error",
detail: "An unexpected error has occurred."
});
} }
}; };
</script> </script>

View file

@ -10,6 +10,8 @@
import type {FileCreationResultDto} from "@/dto/file-creation-result-dto.ts"; import type {FileCreationResultDto} from "@/dto/file-creation-result-dto.ts";
import type {FileCreationErrorDto} from "@/dto/file-creation-error-dto.ts"; import type {FileCreationErrorDto} from "@/dto/file-creation-error-dto.ts";
const emit = defineEmits(["uploaded"]);
const totalSize = ref(0); const totalSize = ref(0);
const totalSizePercent = ref(0); const totalSizePercent = ref(0);
const files = ref([]); const files = ref([]);
@ -48,6 +50,7 @@
if ('filename' in resp) { if ('filename' in resp) {
filename.value = resp.filename; filename.value = resp.filename;
visible.value = true; visible.value = true;
emit("uploaded");
} else if ('reason' in resp) { } else if ('reason' in resp) {
toast.add({ toast.add({
severity: "error", severity: "error",

View file

@ -0,0 +1,4 @@
export interface PasswordChangeDto {
oldPassword: string;
newPassword: string;
}

View file

@ -6,6 +6,7 @@ import router from './router'
import PrimeVue from 'primevue/config'; import PrimeVue from 'primevue/config';
import ToastService from 'primevue/toastservice'; import ToastService from 'primevue/toastservice';
import Aura from '@primevue/themes/aura'; import Aura from '@primevue/themes/aura';
import ConfirmationService from 'primevue/confirmationservice';
const app = createApp(App) const app = createApp(App)
@ -19,5 +20,6 @@ app.use(PrimeVue, {
} }
}); });
app.use(ToastService); app.use(ToastService);
app.use(ConfirmationService);
app.mount('#app') app.mount('#app')

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount } from "vue"; import {onBeforeMount, ref} from "vue";
import { checkIfLoggedIn } from "@/helpers/api.ts"; import { checkIfLoggedIn } from "@/helpers/api.ts";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import Uploader from "@/components/Uploader.vue"; import Uploader from "@/components/Uploader.vue";
@ -7,6 +7,7 @@ import { onBeforeMount } from "vue";
import Menu from "@/components/Menu.vue" import Menu from "@/components/Menu.vue"
const router = useRouter(); const router = useRouter();
const fileListRef = ref<InstanceType<typeof FileList>>();
onBeforeMount(async () => { onBeforeMount(async () => {
const loggedIn = await checkIfLoggedIn(); const loggedIn = await checkIfLoggedIn();
@ -15,16 +16,20 @@ import { onBeforeMount } from "vue";
return; return;
} }
}); });
const onUploaded = async () => {
await fileListRef.value!.refresh();
};
</script> </script>
<template> <template>
<div class="p-5 space-y-5 h-full"> <div class="p-5 space-y-5 h-full">
<Menu /> <Menu />
<div> <div>
<Uploader /> <Uploader @uploaded="onUploaded" />
</div> </div>
<div> <div>
<FileList /> <FileList ref="fileListRef" />
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount, ref } from "vue"; import {onBeforeMount, onMounted, ref} from "vue";
import { checkIfLoggedIn } from "@/helpers/api.ts"; import { checkIfLoggedIn } from "@/helpers/api.ts";
import { useRouter } from "vue-router"; import {useRoute, useRouter} from "vue-router";
import Password from "primevue/password"; import Password from "primevue/password";
import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms"; import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms";
import InputText from "primevue/inputtext"; import InputText from "primevue/inputtext";
@ -11,6 +11,7 @@ import Divider from "primevue/divider";
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
const toast = useToast(); const toast = useToast();
const route = useRoute();
const router = useRouter(); const router = useRouter();
const loggedIn = ref<boolean>(true); const loggedIn = ref<boolean>(true);
@ -23,6 +24,15 @@ onBeforeMount(async () => {
} }
}); });
onMounted(() => {
if ("error" in route.query) {
toast.add({
severity: "error",
detail: route.query.error
});
}
});
const onFormSubmit = (ev: FormSubmitEvent) => { const onFormSubmit = (ev: FormSubmitEvent) => {
if (!ev.valid) { if (!ev.valid) {
toast.add({ toast.add({