vue
This commit is contained in:
parent
35ffaf3e48
commit
04a01788f8
22 changed files with 1814 additions and 76 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
|
|
|
|||
1251
mizuki-frontend/package-lock.json
generated
1251
mizuki-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,11 @@
|
|||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.5",
|
||||
"tailwindcss-primeui": "^0.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
|
|
@ -19,7 +24,10 @@
|
|||
"@types/node": "^22.10.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-vue-devtools": "^7.6.8",
|
||||
|
|
|
|||
6
mizuki-frontend/postcss.config.js
Normal file
6
mizuki-frontend/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import Toast from 'primevue/toast';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
||||
@import 'primeicons/primeicons.css';
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Inter", serif;
|
||||
}
|
||||
|
|
@ -1,23 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import {onBeforeMount, ref} from "vue";
|
||||
import Panel from 'primevue/panel';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import type FileDto from "@/dto/file-dto.ts";
|
||||
import FileListItem from "@/components/FileListItem.vue";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
|
||||
const files = ref<FileDto[]>([]);
|
||||
const toast = useToast();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await refresh();
|
||||
});
|
||||
|
||||
const refresh = async () => {
|
||||
files.value = [];
|
||||
|
||||
const items = await fetch('/api/file/all')
|
||||
.then(i => i.json())
|
||||
.then(j => j as FileDto[]);
|
||||
|
||||
files.value = items;
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleted = async () => {
|
||||
toast.add({
|
||||
severity: "success",
|
||||
detail: "File successfully deleted."
|
||||
});
|
||||
await refresh();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="files.length > 0">
|
||||
<FileListItem v-for="file of files" :item="file" :key="file.filename" />
|
||||
</div>
|
||||
<Panel header="Uploaded files">
|
||||
<div v-if="files.length > 0">
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type FileDto from "@/dto/file-dto.ts";
|
||||
import Card from 'primevue/card';
|
||||
import Button from 'primevue/button';
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
item: FileDto
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["deleted"]);
|
||||
|
||||
const url = computed(() => {
|
||||
return `http://localhost:5118/f/${props.item.filename}`;
|
||||
})
|
||||
|
|
@ -15,15 +19,29 @@
|
|||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
window.location.reload();
|
||||
|
||||
emit("deleted");
|
||||
}
|
||||
|
||||
const download = () => {
|
||||
location.href = url.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>{{ item.filename }} - {{ item.originalFilename }}</div>
|
||||
<a :href="url">Download</a><br/>
|
||||
<a href="#" v-on:click.prevent="removeItem">Delete</a>
|
||||
<Card style="width: 25rem; overflow: hidden;">
|
||||
<template #title>{{ item.originalFilename }}</template>
|
||||
<template #content>
|
||||
<p>Uploaded as {{ item.filename }}</p>
|
||||
<p>Size in bytes: {{ item.sizeInBytes }}B</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<Button v-on:click.prevent="removeItem" label="Delete" severity="secondary" outlined class="w-full" />
|
||||
<Button v-on:click.prevent="download" label="Download" class="w-full" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
72
mizuki-frontend/src/components/Menu.vue
Normal file
72
mizuki-frontend/src/components/Menu.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import Menubar from "primevue/menubar";
|
||||
import Popover from "primevue/popover";
|
||||
import Button from "primevue/button";
|
||||
import Avatar from "primevue/avatar";
|
||||
import Divider from "primevue/divider";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import type {MenuItem} from "primevue/menuitem";
|
||||
import type {UserInfoDto} from "@/dto/user-info-dto.ts";
|
||||
import {getUserInfo} from "@/helpers/api.ts";
|
||||
|
||||
const info = ref<UserInfoDto>({
|
||||
username: "[loading]",
|
||||
avatar: null
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
info.value = await getUserInfo();
|
||||
})
|
||||
|
||||
const letter = computed(() => {
|
||||
return info.value.username[0].toLowerCase();
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
await fetch('/api/user/logout');
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const onAvatarClicked = (event: MouseEvent) => {
|
||||
op.value.toggle(event);
|
||||
}
|
||||
|
||||
const op = ref();
|
||||
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
label: "Home",
|
||||
url: "/",
|
||||
icon: "pi pi-cloud-upload"
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
url: "/settings",
|
||||
icon: "pi pi-folder-open"
|
||||
}
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menubar :model="items">
|
||||
<template #start>
|
||||
<div class="font-black">Mizuki</div>
|
||||
</template>
|
||||
<template #end>
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar @click="onAvatarClicked" shape="circle" :label="letter" />
|
||||
<Popover ref="op">
|
||||
<p>Logged in as {{ info.username }}</p>
|
||||
<Divider />
|
||||
<Button @click="logout" severity="secondary" label="Log out" icon="pi pi-sign-out" />
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
</Menubar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
80
mizuki-frontend/src/components/PasswordChanger.vue
Normal file
80
mizuki-frontend/src/components/PasswordChanger.vue
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import Message from "primevue/message";
|
||||
import Password from "primevue/password";
|
||||
import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms";
|
||||
import Button from "primevue/button";
|
||||
import Divider from "primevue/divider";
|
||||
|
||||
const formResolver = (ev: FormResolverOptions): Record<string, any> => {
|
||||
const resp = {
|
||||
errors: {
|
||||
CurrentPassword: [],
|
||||
Password: [],
|
||||
RepeatPassword: []
|
||||
}
|
||||
};
|
||||
|
||||
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&*!])[A-Za-z\d@#$%^&*!]{8,}$/;
|
||||
if ("CurrentPassword" in ev.values && ev.values["CurrentPassword"] === undefined || !ev.values["CurrentPassword"].match(regex)) {
|
||||
resp.errors["Password"] = ["Current password doesn't meet requirements."];
|
||||
}
|
||||
|
||||
if ("Password" in ev.values && ev.values["Password"] === undefined || !ev.values["Password"].match(regex)) {
|
||||
resp.errors["Password"] = ["New password doesn't meet requirements."];
|
||||
}
|
||||
|
||||
if ("RepeatPassword" in ev.values && ev.values["RepeatPassword"] === undefined || ev.values["RepeatPassword"] != ev.values["Password"]) {
|
||||
resp.errors["RepeatPassword"] = ["Invalid repeat password."];
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
const onFormSubmit = (event: FormSubmitEvent) => {
|
||||
if (!event.valid) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Form v-slot="$form" :resolver="formResolver" :validateOnValueUpdate="true" :validateOnBlur="true" @submit="onFormSubmit" class="flex flex-col space-y-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<Password name="CurrentPassword" type="text" placeholder="Current Password" toggleMask required />
|
||||
<Message v-if="$form.CurrentPassword?.invalid" severity="error" size="small" variant="simple">{{ $form.CurrentPassword.error }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Password name="Password" type="text" placeholder="Password" toggleMask required>
|
||||
<template #header>
|
||||
<h6>Pick a password</h6>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Divider />
|
||||
<p class="mt-2">Requirements</p>
|
||||
<ul class="pl-2 ml-2 mt-0" style="line-height: 1.5">
|
||||
<li>At least one lowercase</li>
|
||||
<li>At least one uppercase</li>
|
||||
<li>At least one numeric</li>
|
||||
<li>At least one of @#$%^&*!</li>
|
||||
<li>Minimum 8 characters</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Password>
|
||||
<Message v-if="$form.Password?.invalid" severity="error" size="small" variant="simple">{{ $form.Password.error }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Password name="RepeatPassword" type="text" placeholder="Repeat Password" toggleMask required />
|
||||
<Message v-if="$form.RepeatPassword?.invalid" severity="error" size="small" variant="simple">{{ $form.RepeatPassword.error }}</Message>
|
||||
</div>
|
||||
<Button type="submit" severity="secondary" label="Change password" />
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,35 +1,138 @@
|
|||
<script setup lang="ts">
|
||||
const sendData = async () => {
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
// noinspection JSIncompatibleTypesComparison
|
||||
if (input.files === null || input.files.length < 1) {
|
||||
return;
|
||||
}
|
||||
const data = new FormData();
|
||||
data.append('formFile', input.files[0]);
|
||||
|
||||
const resp = await fetch('/api/file/upload', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
credentials: 'include'
|
||||
import FileUpload, {type FileUploadUploadEvent} from 'primevue/fileupload';
|
||||
import Button from 'primevue/button';
|
||||
import Badge from 'primevue/badge';
|
||||
import Message from 'primevue/message';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import {ref} from "vue";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import type {FileCreationResultDto} from "@/dto/file-creation-result-dto.ts";
|
||||
import type {FileCreationErrorDto} from "@/dto/file-creation-error-dto.ts";
|
||||
|
||||
const totalSize = ref(0);
|
||||
const totalSizePercent = ref(0);
|
||||
const files = ref([]);
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
const toast = useToast();
|
||||
const filename = ref<string>("");
|
||||
|
||||
const onRemoveTemplatingFile = (file, removeFileCallback, index) => {
|
||||
removeFileCallback(index);
|
||||
totalSize.value -= parseInt(formatSize(file.size));
|
||||
totalSizePercent.value = totalSize.value / 10;
|
||||
};
|
||||
|
||||
const onClearTemplatingUpload = (clear) => {
|
||||
clear();
|
||||
totalSize.value = 0;
|
||||
totalSizePercent.value = 0;
|
||||
};
|
||||
|
||||
const onSelectedFiles = (event) => {
|
||||
files.value = event.files;
|
||||
files.value.forEach((file) => {
|
||||
totalSize.value += parseInt(formatSize(file.size));
|
||||
});
|
||||
|
||||
const json = resp.json();
|
||||
|
||||
if ('filename' in json) {
|
||||
alert(`Your file is able to be downloaded at https://localhost:5881/f/${json.filename}!`);
|
||||
} else if ('reason' in json) {
|
||||
alert(json.reason);
|
||||
};
|
||||
|
||||
const uploadEvent = (callback) => {
|
||||
totalSizePercent.value = totalSize.value / 10;
|
||||
callback();
|
||||
};
|
||||
|
||||
const onTemplatedUpload = (ev : FileUploadUploadEvent) => {
|
||||
const resp = JSON.parse(ev.xhr.responseText) as FileCreationResultDto | FileCreationErrorDto;
|
||||
if ('filename' in resp) {
|
||||
filename.value = resp.filename;
|
||||
visible.value = true;
|
||||
} else if ('reason' in resp) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
detail: `There was an error uploading your file: ${resp.reason}`,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
const k = 1024;
|
||||
const dm = 3;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
if (bytes === 0) {
|
||||
return `0 ${sizes[0]}`;
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
||||
|
||||
return `${formattedSize} ${sizes[i]}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input type="file" name="formFile" />
|
||||
<input type="submit" v-on:click.prevent="sendData" />
|
||||
</div>
|
||||
<FileUpload name="formFile" url="/api/file/upload" @upload="onTemplatedUpload($event)" :multiple="false" :maxFileSize="52428800">
|
||||
<template #header="{ chooseCallback, uploadCallback, clearCallback, files }">
|
||||
<div class="flex flex-wrap justify-between items-center flex-1 gap-4">
|
||||
<div class="flex gap-2">
|
||||
<Button @click="chooseCallback()" icon="pi pi-images" rounded outlined severity="secondary"></Button>
|
||||
<Button @click="uploadEvent(uploadCallback)" icon="pi pi-cloud-upload" rounded outlined severity="success" :disabled="!files || files.length === 0"></Button>
|
||||
<Button @click="clearCallback()" icon="pi pi-times" rounded outlined severity="danger" :disabled="!files || files.length === 0"></Button>
|
||||
</div>
|
||||
<ProgressBar :value="totalSizePercent" :showValue="false" class="md:w-20rem h-1 w-full md:ml-auto">
|
||||
<span class="whitespace-nowrap">{{ totalSize }}B / 1Mb</span>
|
||||
</ProgressBar>
|
||||
</div>
|
||||
</template>
|
||||
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback, messages }">
|
||||
<div class="flex flex-col gap-8 pt-4">
|
||||
<Message v-for="message of messages" :key="message" :class="{ 'mb-8': !files.length && !uploadedFiles.length}" severity="error">
|
||||
{{ message }}
|
||||
</Message>
|
||||
|
||||
<div v-if="files.length > 0">
|
||||
<h5>Pending</h5>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div v-for="(file, index) of files" :key="file.name + file.type + file.size" class="p-8 rounded-border flex flex-col border border-surface items-center gap-4">
|
||||
<span class="font-semibold text-ellipsis max-w-60 whitespace-nowrap overflow-hidden">{{ file.name }}</span>
|
||||
<div>{{ formatSize(file.size) }}</div>
|
||||
<Badge value="Pending" severity="warn" />
|
||||
<Button icon="pi pi-times" @click="onRemoveTemplatingFile(file, removeFileCallback, index)" outlined rounded severity="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadedFiles.length > 0">
|
||||
<h5>Completed</h5>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div v-for="(file, index) of uploadedFiles" :key="file.name + file.type + file.size" class="p-8 rounded-border flex flex-col border border-surface items-center gap-4">
|
||||
<span class="font-semibold text-ellipsis max-w-60 whitespace-nowrap overflow-hidden">{{ file.name }}</span>
|
||||
<div>{{ formatSize(file.size) }}</div>
|
||||
<Badge value="Completed" class="mt-4" severity="success" />
|
||||
<Button icon="pi pi-times" @click="removeUploadedFileCallback(index)" outlined rounded severity="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<i class="pi pi-cloud-upload !border-2 !rounded-full !p-8 !text-4xl !text-muted-color" />
|
||||
<p class="mt-6 mb-0">Drag and drop files to here to upload.</p>
|
||||
</div>
|
||||
</template>
|
||||
</FileUpload>
|
||||
<Dialog v-model:visible="visible" modal header="Success!" :style="{ width: '30rem' }">
|
||||
<div class="space-y-3">
|
||||
<p>Your file has been uploaded and is now accessible at:</p>
|
||||
<div class="p-5 bg-neutral-950 text-gray-50 rounded-2xl font-bold ">
|
||||
https://localhost:5118/f/{{ filename }}
|
||||
</div>
|
||||
<Button label="Click to copy" class="float-right"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
3
mizuki-frontend/src/dto/file-creation-error-dto.ts
Normal file
3
mizuki-frontend/src/dto/file-creation-error-dto.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface FileCreationErrorDto {
|
||||
reason: string
|
||||
}
|
||||
3
mizuki-frontend/src/dto/file-creation-result-dto.ts
Normal file
3
mizuki-frontend/src/dto/file-creation-result-dto.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface FileCreationResultDto {
|
||||
filename: string
|
||||
}
|
||||
4
mizuki-frontend/src/dto/user-info-dto.ts
Normal file
4
mizuki-frontend/src/dto/user-info-dto.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface UserInfoDto {
|
||||
username: string,
|
||||
avatar: string | null
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import type {UserInfoDto} from "@/dto/user-info-dto.ts";
|
||||
|
||||
export const checkIfLoggedIn = async () => {
|
||||
try {
|
||||
const resp = await fetch("/api/user/check", {
|
||||
|
|
@ -8,4 +10,12 @@ export const checkIfLoggedIn = async () => {
|
|||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserInfo = async () => {
|
||||
const resp = await fetch("/api/user/info", {
|
||||
redirect: 'error',
|
||||
credentials: 'include'
|
||||
});
|
||||
return await resp.json() as unknown as UserInfoDto;
|
||||
};
|
||||
|
|
@ -3,9 +3,21 @@ import './assets/main.css'
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
import Aura from '@primevue/themes/aura';
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
darkModeSelector: '.my-app-dark',
|
||||
}
|
||||
}
|
||||
});
|
||||
app.use(ToastService);
|
||||
|
||||
app.mount('#app')
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||
import HomeView from '../views/HomeView.vue'
|
||||
import LoginView from "@/views/LoginView.vue";
|
||||
import RegisterView from "@/views/RegisterView.vue";
|
||||
import SettingsView from "@/views/SettingsView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
|
@ -11,6 +12,11 @@ const router = createRouter({
|
|||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: SettingsView,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
|
|
|
|||
|
|
@ -1,30 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { onBeforeMount } from "vue";
|
||||
import { checkIfLoggedIn } from "@/helpers/api.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import Uploader from "@/components/Uploader.vue";
|
||||
import FileList from "@/components/FileList.vue";
|
||||
import Menu from "@/components/Menu.vue"
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
const loggedIn = await checkIfLoggedIn();
|
||||
if (!loggedIn) {
|
||||
await router.push('/login');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
await fetch('/api/user/logout');
|
||||
window.location.reload();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FileList />
|
||||
<Uploader />
|
||||
<a href="#" v-on:click.prevent="logout">Log out</a>
|
||||
<div class="p-5 space-y-5 h-full">
|
||||
<Menu />
|
||||
<div>
|
||||
<Uploader />
|
||||
</div>
|
||||
<div>
|
||||
<FileList />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
<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 Button from "primevue/button";
|
||||
import Password from 'primevue/password';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import {Form, type FormSubmitEvent} from '@primevue/forms';
|
||||
import {useToast} from "primevue/usetoast";
|
||||
|
||||
const router = useRouter();
|
||||
const loggedIn = ref<boolean>(true);
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
loggedIn.value = await checkIfLoggedIn();
|
||||
if (loggedIn.value) {
|
||||
|
|
@ -13,14 +21,37 @@ onBeforeMount(async () => {
|
|||
return;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if ("error" in route.query) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
detail: route.query.error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = (ev: FormSubmitEvent) => {
|
||||
if (ev.valid) {
|
||||
const form = ev.originalEvent.target as HTMLFormElement;
|
||||
form.method = "POST";
|
||||
form.action = "/api/user/login";
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form v-if="!loggedIn" action="/api/user/login" method="post">
|
||||
<span>Username</span> <input type="text" name="Username" /><br />
|
||||
<span>Password</span> <input type="password" name="Password" />
|
||||
<input type="submit" value="Login" />
|
||||
</form>
|
||||
<div class="w-full h-full flex flex-col justify-center items-center space-y-3">
|
||||
<h1 class="text-8xl font-extrabold text-amber-500">Mizuki</h1>
|
||||
<Form v-slot="$form" @submit="onFormSubmit" class="flex flex-col space-y-2">
|
||||
<InputText name="Username" type="text" placeholder="Username" required /><br/>
|
||||
<Password name="Password" type="text" placeholder="Password" :feedback="false" required /><br/>
|
||||
<Button type="submit" severity="secondary" label="Login" />
|
||||
<div class="text-gray-500 text-sm">Don't have an account? <a href="/register" class="text-amber-600 hover:underline">Register here!</a></div>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
import { onBeforeMount, ref } from "vue";
|
||||
import { checkIfLoggedIn } from "@/helpers/api.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import Password from "primevue/password";
|
||||
import {Form, type FormResolverOptions, type FormSubmitEvent} from "@primevue/forms";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Button from "primevue/button";
|
||||
import Message from "primevue/message";
|
||||
import Divider from "primevue/divider";
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const loggedIn = ref<boolean>(true);
|
||||
|
|
@ -13,15 +22,87 @@ onBeforeMount(async () => {
|
|||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = (ev: FormSubmitEvent) => {
|
||||
if (!ev.valid) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
detail: "Invalid data supplied."
|
||||
});
|
||||
} else {
|
||||
const form = ev.originalEvent.target as HTMLFormElement;
|
||||
form.method = "POST";
|
||||
form.action = "/api/user/register";
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const formResolver = (ev: FormResolverOptions): Record<string, any> => {
|
||||
const resp = {
|
||||
errors: {
|
||||
Username: [],
|
||||
Password: [],
|
||||
RepeatPassword: []
|
||||
}
|
||||
};
|
||||
|
||||
const usernameRegex = /^[a-zA-Z0-9_.]*$/;
|
||||
if ("Username" in ev.values) {
|
||||
if (ev.values["Username"] === undefined) {
|
||||
resp.errors["Username"] = ["Username cannot be blank."];
|
||||
} else if (!ev.values["Username"].match(usernameRegex)) {
|
||||
resp.errors["Username"] = ["Username contains illegal characters."];
|
||||
}
|
||||
}
|
||||
|
||||
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$%^&*!])[A-Za-z\d@#$%^&*!]{8,}$/;
|
||||
if ("Password" in ev.values && ev.values["Password"] === undefined || !ev.values["Password"].match(regex)) {
|
||||
resp.errors["Password"] = ["Password doesn't meet requirements."];
|
||||
}
|
||||
|
||||
if ("RepeatPassword" in ev.values && ev.values["RepeatPassword"] === undefined || ev.values["RepeatPassword"] != ev.values["Password"]) {
|
||||
resp.errors["RepeatPassword"] = ["Invalid repeat password."];
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form v-if="!loggedIn" method="post" action="/api/user/register">
|
||||
<span>Username</span> <input type="text" name="Username" /><br />
|
||||
<span>Password</span> <input type="password" name="Password" /><br />
|
||||
<span>Repeat Password</span> <input type="password" name="ValidatePassword" /><br />
|
||||
<input type="submit" value="Register" />
|
||||
</form>
|
||||
<div class="w-full h-full flex flex-col justify-center items-center space-y-3">
|
||||
<h1 class="text-7xl font-extrabold text-amber-500">Register</h1>
|
||||
<Form v-slot="$form" :resolver="formResolver" :validateOnValueUpdate="true" :validateOnBlur="true" @submit="onFormSubmit" class="flex flex-col space-y-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<InputText name="Username" type="text" placeholder="Username" fluid required />
|
||||
<Message v-if="$form.Username?.invalid" severity="error" size="small" variant="simple">{{ $form.Username.error }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Password name="Password" type="text" placeholder="Password" toggleMask required>
|
||||
<template #header>
|
||||
<h6>Pick a password</h6>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Divider />
|
||||
<p class="mt-2">Requirements</p>
|
||||
<ul class="pl-2 ml-2 mt-0" style="line-height: 1.5">
|
||||
<li>At least one lowercase</li>
|
||||
<li>At least one uppercase</li>
|
||||
<li>At least one numeric</li>
|
||||
<li>At least one of @#$%^&*!</li>
|
||||
<li>Minimum 8 characters</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Password>
|
||||
<Message v-if="$form.Password?.invalid" severity="error" size="small" variant="simple">{{ $form.Password.error }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Password name="RepeatPassword" type="text" placeholder="Repeat Password" toggleMask required />
|
||||
<Message v-if="$form.RepeatPassword?.invalid" severity="error" size="small" variant="simple">{{ $form.RepeatPassword.error }}</Message>
|
||||
</div>
|
||||
<Button type="submit" severity="secondary" label="Register" />
|
||||
<div class="text-gray-500 text-sm">Already have an account? <a href="/login" class="text-amber-600 hover:underline">Log in here!</a></div>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
34
mizuki-frontend/src/views/SettingsView.vue
Normal file
34
mizuki-frontend/src/views/SettingsView.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import Menu from "@/components/Menu.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {onBeforeMount} from "vue";
|
||||
import {checkIfLoggedIn} from "@/helpers/api.ts";
|
||||
import Panel from "primevue/panel";
|
||||
import Fieldset from 'primevue/fieldset';
|
||||
import PasswordChanger from "@/components/PasswordChanger.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const loggedIn = await checkIfLoggedIn();
|
||||
if (!loggedIn) {
|
||||
await router.push('/login');
|
||||
return;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5 space-y-5 h-full">
|
||||
<Menu />
|
||||
<Panel header="Settings" class="h-[85vh]">
|
||||
<Fieldset legend="Password" class="w-max">
|
||||
<PasswordChanger />
|
||||
</Fieldset>
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
11
mizuki-frontend/tailwind.config.js
Normal file
11
mizuki-frontend/tailwind.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('tailwindcss-primeui')],
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue