💄 Improved UI regarding Login
This commit is contained in:
parent
40e2721b82
commit
2930460102
@ -1,3 +1,5 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class User
|
||||
{
|
||||
username?: string;
|
||||
@ -5,6 +7,40 @@ export class User
|
||||
password?: string;
|
||||
roles?: string[];
|
||||
initial?: boolean;
|
||||
|
||||
static get(onSuccess: (users: User[]) => void)
|
||||
{
|
||||
axios.get(ApiConfig.getHttpBase() + '/users', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
static create(user: UserCreation, onSuccess: () => void)
|
||||
{
|
||||
axios.post(ApiConfig.getHttpBase() + '/users', user, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class UserCreation
|
||||
{
|
||||
firstname?: string;
|
||||
lastname?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
username?: string;
|
||||
role: string = "USER";
|
||||
}
|
||||
|
||||
export function hasAnyRole(user: User | undefined, requiredRoles: string[])
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<p class="grid-element">{{ configMap.metadata.name }}</p>
|
||||
<p class="grid-element">{{ configMap.metadata.namespace }}</p>
|
||||
<p class="grid-element">{{ calcAge(configMap.metadata.creationTimestamp) }}</p>
|
||||
<p class="grid-element"><span v-if="configMap.data">{{ Object.keys(configMap.data).length }}</span><span v-else>-</span></p>
|
||||
<div class="grid-element">
|
||||
<ActionButton>delete</ActionButton>
|
||||
@ -11,6 +12,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ConfigMap } from '~/classes/ConfigMap';
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
|
||||
defineProps<{
|
||||
configMap: ConfigMap
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="grid-element">{{ namespace.metadata.name }}</p>
|
||||
<p class="grid-element">{{ calcAge(namespace.metadata.creationTimestamp) }}</p>
|
||||
<div class="grid-element">
|
||||
<ActionButton>delete</ActionButton>
|
||||
</div>
|
||||
@ -9,6 +10,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Namespace } from '~/classes/Namespace';
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
|
||||
defineProps<{
|
||||
namespace: Namespace
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
<template>
|
||||
<SidebarTemplate>
|
||||
<div class="nav">
|
||||
<NuxtLink class="namespace" to="/account/settings">Account</NuxtLink>
|
||||
<NuxtLink class="namespace" to="/account/users">Users</NuxtLink>
|
||||
<NuxtLink class="namespace" to="/account/password">Password</NuxtLink>
|
||||
<NuxtLink class="namespace" to="/account/settings/password">Password</NuxtLink>
|
||||
<NuxtLink class="namespace" to="/account/settings/users">Users</NuxtLink>
|
||||
</div>
|
||||
<div class="left-center">
|
||||
<UiIcon>account_circle</UiIcon>
|
||||
<p>{{ user.username }}</p>
|
||||
</div>
|
||||
</SidebarTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const user = requireUser();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
min-width: 10rem;
|
||||
}
|
||||
.namespace, .resources {
|
||||
padding: 0.35rem 0.5rem;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h2>Account</h2>
|
||||
<div class="content-m">
|
||||
<div class="content-m tile-l">
|
||||
<h3>Passwort ändern</h3>
|
||||
<div class="col-2">
|
||||
<UiInput label="Passwort">
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Dataset } from './Dataset';
|
||||
import { TimeScale, LinearScale, LineController, PointElement, LineElement, Chart, type ChartConfiguration } from 'chart.js';
|
||||
import { TimeScale, LinearScale, LineController, PointElement, LineElement, Chart, Filler, type ChartConfiguration } from 'chart.js';
|
||||
import 'chartjs-adapter-moment';
|
||||
|
||||
Chart.register(TimeScale)
|
||||
@ -14,6 +14,7 @@ Chart.register(LinearScale)
|
||||
Chart.register(PointElement)
|
||||
Chart.register(LineController)
|
||||
Chart.register(LineElement)
|
||||
Chart.register(Filler)
|
||||
|
||||
|
||||
const id = Math.random().toString().replaceAll(".", "");
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="grid-element" v-if="deployment.metadata">{{ deployment.metadata.name }}</p>
|
||||
<p class="grid-element" v-if="deployment.metadata">{{ deployment.metadata.namespace }}</p>
|
||||
<p class="grid-element" v-if="deployment.spec">{{ deployment.spec.replicas }}</p>
|
||||
<p class="grid-element">{{ deployment.metadata.name }}</p>
|
||||
<p class="grid-element">{{ deployment.metadata.namespace }}</p>
|
||||
<p class="grid-element">{{ calcAge(deployment.metadata.creationTimestamp) }}</p>
|
||||
<p class="grid-element">{{ deployment.spec.replicas }}</p>
|
||||
<div class="grid-element action-buttons">
|
||||
<ActionButton>delete</ActionButton>
|
||||
<ActionButton>autorenew</ActionButton>
|
||||
@ -15,6 +16,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Deployment } from '~/classes/Deployment';
|
||||
import RescaleDeploymentPopup from '../RescaleDeploymentPopup.vue';
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
|
||||
defineProps<{
|
||||
deployment: Deployment
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div @click="() => showViewPopup = true">
|
||||
<p class="grid-element">{{ ingress.metadata.name }}</p>
|
||||
<p class="grid-element cursor">{{ ingress.metadata.name }}</p>
|
||||
<p class="grid-element">{{ ingress.metadata.namespace }}</p>
|
||||
<p class="grid-element">{{ calcAge(ingress.metadata.creationTimestamp) }}</p>
|
||||
<p class="grid-element">{{ ingress.spec.ingressClassName }}</p>
|
||||
<p class="grid-element">{{ ingress.spec.rules.length }}</p>
|
||||
<div class="grid-element action-buttons">
|
||||
@ -14,6 +15,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Ingress } from '~/classes/Ingress';
|
||||
import IngressViewPopup from './view/IngressViewPopup.vue';
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
|
||||
defineProps<{
|
||||
ingress: Ingress
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
<div class="header">
|
||||
<p>Name</p>
|
||||
<p>Namespace</p>
|
||||
<p>Age</p>
|
||||
<p>Entries</p>
|
||||
<p>Aktionen</p>
|
||||
</div>
|
||||
@ -28,6 +29,6 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.config-map-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr auto;
|
||||
grid-template-columns: auto auto auto 1fr auto;
|
||||
}
|
||||
</style>
|
||||
@ -4,6 +4,7 @@
|
||||
<div class="header">
|
||||
<p>Name</p>
|
||||
<p>Namespace</p>
|
||||
<p>Age</p>
|
||||
<p>Replicas</p>
|
||||
<p>Aktionen</p>
|
||||
</div>
|
||||
@ -28,6 +29,6 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.deployment-container {
|
||||
grid-template-columns: auto 1fr 1fr auto;
|
||||
grid-template-columns: auto auto auto 1fr auto;
|
||||
}
|
||||
</style>
|
||||
@ -4,6 +4,7 @@
|
||||
<div class="header">
|
||||
<p>Name</p>
|
||||
<p>Namespace</p>
|
||||
<p>Age</p>
|
||||
<p>Ingress Class Name</p>
|
||||
<p>Rules</p>
|
||||
<p>Actions</p>
|
||||
@ -28,6 +29,6 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.ingress-container {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
||||
grid-template-columns: auto auto auto auto 1fr auto;
|
||||
}
|
||||
</style>
|
||||
@ -3,6 +3,7 @@
|
||||
<div class="resource-container namespace-container">
|
||||
<div class="header">
|
||||
<p>Name</p>
|
||||
<p>Age</p>
|
||||
<p>Actions</p>
|
||||
</div>
|
||||
<NamespaceComponent :namespace="namespace" v-for="namespace, index in namespaces" class="resource" :class="{ even: index % 2 }"></NamespaceComponent>
|
||||
@ -26,6 +27,6 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.namespace-container {
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
</style>
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="header">
|
||||
<p>Name</p>
|
||||
<p>Namespace</p>
|
||||
<p>Alter</p>
|
||||
<p>Age</p>
|
||||
<p>Storage Class</p>
|
||||
<p>Access Modes</p>
|
||||
<p>Aktionen</p>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="resource-container persistent-volume-container">
|
||||
<div class="header">
|
||||
<p>Name</p>
|
||||
<p>Alter</p>
|
||||
<p>Age</p>
|
||||
<p>Aktionen</p>
|
||||
</div>
|
||||
<PersistentVolumeComponent v-for="persistentVolume, index in persistentVolumes" :persistent-volume="persistentVolume" class="resource" :class="{ even: index % 2 }"></PersistentVolumeComponent>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="header">
|
||||
<p>Pod</p>
|
||||
<p>Namespace</p>
|
||||
<p>Alter</p>
|
||||
<p>Age</p>
|
||||
<p>Node</p>
|
||||
<p>Containers</p>
|
||||
<p>Status</p>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<UiButton v-if="false" icon="add" class="extra-small" reverse @click="() => secretAddComponent.open()">Add</UiButton>
|
||||
</div>
|
||||
<p>Namespace</p>
|
||||
<p>Alter</p>
|
||||
<p>Age</p>
|
||||
<p>Aktionen</p>
|
||||
</div>
|
||||
<SecretComponent v-for="secret, index in secrets" :secret="secret" class="resource" :class="{ even: index % 2 }"></SecretComponent>
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
<div class="header">
|
||||
<p>Service</p>
|
||||
<p>Namespace</p>
|
||||
<p>Age</p>
|
||||
<p>Type</p>
|
||||
<p>Aktionen</p>
|
||||
</div>
|
||||
@ -27,6 +28,6 @@ onUnmounted(() => {
|
||||
|
||||
<style>
|
||||
.service-container {
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
grid-template-columns: auto auto auto 1fr auto;
|
||||
}
|
||||
</style>
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="resource">
|
||||
<p class="grid-element">{{ secret.metadata.name }}</p>
|
||||
<p class="grid-element">{{ secret.metadata.namespace }}</p>
|
||||
<p class="grid-element">-</p>
|
||||
<p class="grid-element">{{ calcAge(secret.metadata.creationTimestamp) }}</p>
|
||||
<div class="grid-element action-buttons">
|
||||
<ActionButton>edit</ActionButton>
|
||||
<ActionButton>delete</ActionButton>
|
||||
@ -11,6 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
import type { Secret } from '~/classes/Secret';
|
||||
|
||||
defineProps<{
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="grid-element" v-if="service.metadata">{{ service.metadata.name }}</p>
|
||||
<p class="grid-element" v-if="service.metadata">{{ service.metadata.namespace }}</p>
|
||||
<p class="grid-element">{{ service.metadata.name }}</p>
|
||||
<p class="grid-element">{{ service.metadata.namespace }}</p>
|
||||
<p class="grid-element">{{ calcAge(service.metadata.creationTimestamp) }}</p>
|
||||
<p class="grid-element" v-if="service.spec">{{ service.spec.type }}</p>
|
||||
<div class="grid-element">
|
||||
<ActionButton v-if="hasAnyRole(getUser(), ['admin', 'maintainer'])">delete</ActionButton>
|
||||
<ActionButton>delete</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
import type { Service } from '~/classes/Service';
|
||||
import { hasAnyRole } from '~/classes/User';
|
||||
|
||||
defineProps<{
|
||||
service: Service
|
||||
|
||||
13
components/ui/Prompt.ts
Normal file
13
components/ui/Prompt.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export class Prompt
|
||||
{
|
||||
constructor (
|
||||
public text: string,
|
||||
public type: PromptType
|
||||
) {}
|
||||
}
|
||||
|
||||
export enum PromptType
|
||||
{
|
||||
ERROR = "error",
|
||||
SUCCESS = "success"
|
||||
}
|
||||
@ -37,6 +37,7 @@ const props = defineProps<{
|
||||
white-space: nowrap;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.button.square {
|
||||
padding: 0;
|
||||
|
||||
27
components/ui/UiError.vue
Normal file
27
components/ui/UiError.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="spaced-center padding-m error" v-if="error">
|
||||
<p>{{ error }}</p>
|
||||
<UiIcon class="pointer" @click="() => emit('close')">close</UiIcon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
error?: string
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
background-color: rgb(218, 57, 57);
|
||||
border-radius: 0.25rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
.error * {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
34
components/ui/UiPompt.vue
Normal file
34
components/ui/UiPompt.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="spaced-center padding-m prompt" v-if="prompt" :class="prompt.type">
|
||||
<p>{{ prompt.text }}</p>
|
||||
<UiIcon class="pointer" @click="() => emit('close')">close</UiIcon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Prompt } from './Prompt';
|
||||
|
||||
defineProps<{
|
||||
prompt?: Prompt
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prompt {
|
||||
border-radius: 0.25rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
.error {
|
||||
background-color: rgb(218, 57, 57);
|
||||
}
|
||||
.success {
|
||||
background-color: rgb(43, 161, 49);
|
||||
}
|
||||
.prompt * {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="left-center">
|
||||
<NuxtLink to="/account/inspect/nodes/_all">Inspect</NuxtLink>
|
||||
<NuxtLink to="/account/monitorings/nodes">Monitorings</NuxtLink>
|
||||
<NuxtLink to="/account/settings">Settings</NuxtLink>
|
||||
<NuxtLink to="/account/settings/password">Settings</NuxtLink>
|
||||
<p class="pointer" @click="logout()">Logout</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="monitorings-page">
|
||||
<SidebarTemplate>
|
||||
<div class="content-l">
|
||||
<h2>Kubooboo</h2>
|
||||
<NuxtLink class="resources" to="/account/monitorings/nodes">Nodes</NuxtLink>
|
||||
</div>
|
||||
</SidebarTemplate>
|
||||
<NuxtPage></NuxtPage>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<SettingsSidebar></SettingsSidebar>
|
||||
<div>
|
||||
<div class="padding-l">
|
||||
<NuxtPage></NuxtPage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@ -1,11 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="content-l">
|
||||
<h2>Change Password</h2>
|
||||
<UiPompt :prompt="prompt" @close="prompt = undefined"></UiPompt>
|
||||
<div class="col-2 tile-l">
|
||||
<UiInput label="New Password" required>
|
||||
<input type="password" v-model="password">
|
||||
</UiInput>
|
||||
<UiInput label="Repeat Password" required>
|
||||
<input type="password" v-model="repeatPassword">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="center">
|
||||
<UiButton class="width-6rem" icon="change_circle" reverse @click="() => change()">Change</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Prompt, PromptType } from '~/components/ui/Prompt';
|
||||
import { changePassword } from '~/requests/user';
|
||||
import UiPompt from '~/components/ui/UiPompt.vue';
|
||||
|
||||
const password = ref('');
|
||||
const repeatPassword = ref('');
|
||||
|
||||
const prompt: Ref<Prompt | undefined> = ref(undefined);
|
||||
function change()
|
||||
{
|
||||
if (password.value && password.value === repeatPassword.value)
|
||||
{
|
||||
changePassword(requireUser().username, password.value, () => {
|
||||
password.value = '';
|
||||
repeatPassword.value = '';
|
||||
prompt.value = new Prompt("Password changed successfully.", PromptType.SUCCESS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
prompt.value = new Prompt("Passwords don't match.", PromptType.ERROR);
|
||||
throw new Error("Passwords don't match.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
65
pages/account/settings/users/add.vue
Normal file
65
pages/account/settings/users/add.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h2>Add User</h2>
|
||||
<UiError :error="error" @close="error = undefined"></UiError>
|
||||
<div class="col-2 tile-l">
|
||||
<UiInput label="First Name">
|
||||
<input type="text" v-model="user.firstname">
|
||||
</UiInput>
|
||||
<UiInput label="Last Name">
|
||||
<input type="text" v-model="user.lastname">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="col-2 tile-l">
|
||||
<UiInput label="Username" required>
|
||||
<input type="text" v-model="user.username">
|
||||
</UiInput>
|
||||
<UiInput label="E-Mail" required>
|
||||
<input type="text" v-model="user.email">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="col-2 tile-l">
|
||||
<UiInput label="Role" required>
|
||||
<select v-model="user.role">
|
||||
<option value="USER">User</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
</select>
|
||||
</UiInput>
|
||||
<UiInput label="Password" required>
|
||||
<input type="password" v-model="user.password">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="center">
|
||||
<UiButton class="width-6rem" @click="() => useRouter().push('/account/settings/users')">Zurück</UiButton>
|
||||
<UiButton class="width-6rem" @click="() => create()">Add</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { User, UserCreation } from '~/classes/User';
|
||||
import UiError from '~/components/ui/UiError.vue';
|
||||
|
||||
const user = ref(new UserCreation());
|
||||
|
||||
const error: Ref<string | undefined> = ref(undefined);
|
||||
function create()
|
||||
{
|
||||
const _user = user.value;
|
||||
if (_user.email && _user.username && _user.role && _user.password)
|
||||
{
|
||||
User.create(user.value, () => {
|
||||
useRouter().push('/account/settings/users');
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
error.value = "Please fill out all required fields.";
|
||||
throw new Error("Invalid user object.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
65
pages/account/settings/users/index.vue
Normal file
65
pages/account/settings/users/index.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<div class="spaced-center">
|
||||
<h2>Users</h2>
|
||||
<UiButton icon="person_add" reverse @click="() => useRouter().push('/account/settings/users/add')">Add</UiButton>
|
||||
</div>
|
||||
<div class="content-l">
|
||||
<div class="user-container">
|
||||
<div class="contents">
|
||||
<h3 class="gray">Username</h3>
|
||||
<h3 class="gray">E-Mail</h3>
|
||||
<h3 class="gray">Roles</h3>
|
||||
</div>
|
||||
<div v-for="user in users" class="contents user-row">
|
||||
<p>{{ user.username }}</p>
|
||||
<p>{{ Optional.ofNullable(user.email).orElse("-") }}</p>
|
||||
<p v-if="user.roles">{{ user.roles.join(", ") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { User } from '~/classes/User';
|
||||
|
||||
const users: Ref<User[] | undefined> = ref(undefined);
|
||||
|
||||
onMounted(() => {
|
||||
User.get((_users) => {
|
||||
users.value = _users;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
.user-container > :first-child > * {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.contents {
|
||||
display: contents;
|
||||
}
|
||||
.contents > * {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.gray {
|
||||
background-color: var(--shade-light);
|
||||
}
|
||||
.user-row > * {
|
||||
background-color: var(--tile-color);
|
||||
}
|
||||
.contents > :first-child {
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
.contents > :last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<div class="tile-l login-window">
|
||||
<h2>Kubooboo</h2>
|
||||
<img class="logo" src="@/assets/transparent_logo.png" alt="">
|
||||
<form class="login content-xl" @submit.prevent="doLogin()">
|
||||
<div class="tile-l content-l login-window">
|
||||
<div class="content-l">
|
||||
<h2 class="center">Kubooboo</h2>
|
||||
<div class="center">
|
||||
<img class="logo" src="@/assets/transparent_logo.png" alt=""></img>
|
||||
</div>
|
||||
<div class="center">
|
||||
<h1>Login</h1>
|
||||
</div>
|
||||
</div>
|
||||
<form class="login content-l" @submit.prevent="doLogin()">
|
||||
<div class="content-l">
|
||||
<UiInput label="Username">
|
||||
<UiError :error="error" @close="() => { error = undefined }"></UiError>
|
||||
<UiInput label="Username" required>
|
||||
<input type="text" v-model="loginCredentials.username">
|
||||
</UiInput>
|
||||
<UiInput label="Passwort">
|
||||
<UiInput label="Passwort" required>
|
||||
<input type="password" v-model="loginCredentials.password">
|
||||
</UiInput>
|
||||
</div>
|
||||
@ -30,11 +35,15 @@ import { login } from '~/requests/login';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { getUser } from '~/requests/user';
|
||||
import { Session } from '~/classes/Session';
|
||||
import UiError from '~/components/ui/UiError.vue';
|
||||
|
||||
const loginCredentials = ref(new User());
|
||||
|
||||
const error: Ref<string | undefined> = ref(undefined);
|
||||
const loading = ref(false);
|
||||
function doLogin()
|
||||
{
|
||||
if (loginCredentials.value.username && loginCredentials.value.password)
|
||||
{
|
||||
loading.value = true;
|
||||
login(loginCredentials.value, (token: string) => {
|
||||
@ -43,8 +52,23 @@ function doLogin()
|
||||
setSessionCookie(new Session(user, token), decode.exp as number);
|
||||
useRouter().push('/account/inspect/nodes/_all');
|
||||
});
|
||||
}, (code?: string) => {
|
||||
if (code === 'user_not_found')
|
||||
{
|
||||
error.value = "User does not exist.";
|
||||
}
|
||||
if (code === 'wrong_password')
|
||||
{
|
||||
error.value = "Invalid password.";
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Error("Cannot send invalid form.");
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionCookie(session: Session, exp: number)
|
||||
{
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import axios from "axios";
|
||||
import axios, { AxiosError, type AxiosResponse } from "axios";
|
||||
import type { User } from "~/classes/User";
|
||||
|
||||
export function login(user: User, onSuccess: (token: string) => void)
|
||||
export function login(user: User, onSuccess: (token: string) => void, onError: (code?: string) => void)
|
||||
{
|
||||
axios.post<string>(ApiConfig.getHttpBase() + '/login', user)
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
.catch((error: AxiosError<string, any>) => {
|
||||
onError(error.response?.data);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user