more stuff
This commit is contained in:
parent
04a01788f8
commit
e2a4487064
16 changed files with 232 additions and 22 deletions
|
|
@ -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,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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
10
Dtos/PasswordChangeDto.cs
Normal 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
10
Dtos/UserInfoDto.cs
Normal 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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
31
Validators/PasswordChangeValidator.cs
Normal file
31
Validators/PasswordChangeValidator.cs
Normal 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,}$");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
4
mizuki-frontend/src/dto/password-change-dto.ts
Normal file
4
mizuki-frontend/src/dto/password-change-dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface PasswordChangeDto {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue