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.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
{
/// <summary>
/// Logs into Mizuki as a given user.
@ -75,10 +77,12 @@ 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();
}
}
/// <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.
/// </summary>
/// <param name="Reason">The error reason.</param>
public class FileCreationErrorDto(
public record FileCreationErrorDto(
string Reason);

View file

@ -4,5 +4,5 @@ namespace Mizuki.Dtos;
/// The file creation result.
/// </summary>
/// <param name="Filename">The filename.</param>
public class FileCreationResultDto(
public record FileCreationResultDto(
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="password">The password.</param>
/// <returns>Whether the user was created.</returns>
public async Task<bool> CreateUser(
public async Task<User?> 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;
}
/// <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">
import { RouterView } from 'vue-router'
import ConfirmDialog from 'primevue/confirmdialog';
import Toast from 'primevue/toast';
</script>
<template>
<Toast />
<ConfirmDialog></ConfirmDialog>
<RouterView />
</template>

View file

@ -6,7 +6,7 @@
import FileListItem from "@/components/FileListItem.vue";
import {useToast} from "primevue/usetoast";
const files = ref<FileDto[]>([]);
const files = ref<FileDto[] | null>(null);
const toast = useToast();
onBeforeMount(async () => {
@ -30,16 +30,23 @@
});
await refresh();
};
defineExpose({
refresh
});
</script>
<template>
<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">
<FileListItem :item="file" @deleted="onDeleted" />
</div>
</div>
<ProgressSpinner v-else />
</Panel>
</template>

View file

@ -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 = () => {

View file

@ -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<string, any> => {
const resp = {
@ -31,11 +34,48 @@ const formResolver = (ev: FormResolverOptions): Record<string, any> => {
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."
});
}
};
</script>

View file

@ -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",

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 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')

View file

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

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { onBeforeMount, ref } from "vue";
import {onBeforeMount, onMounted, ref} from "vue";
import { checkIfLoggedIn } from "@/helpers/api.ts";
import { useRouter } from "vue-router";
import {useRoute, useRouter} from "vue-router";
import Password from "primevue/password";
import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms";
import InputText from "primevue/inputtext";
@ -11,6 +11,7 @@ import Divider from "primevue/divider";
import { useToast } from 'primevue/usetoast';
const toast = useToast();
const route = useRoute();
const router = useRouter();
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) => {
if (!ev.valid) {
toast.add({