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.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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
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="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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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">
|
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
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 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')
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue