🚧 Work

This commit is contained in:
Andreas Dinauer 2026-04-04 19:40:30 +02:00
parent 875576ef2d
commit 431c0dda99
22 changed files with 362 additions and 37 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="app" v-if="realms.data"> <div id="app" v-if="realms">
<Sidebar :realms="realms.data"></Sidebar> <Sidebar :realms="realms"></Sidebar>
<div id="page"> <div id="page">
<NuxtPage></NuxtPage> <NuxtPage></NuxtPage>
</div> </div>
@ -9,12 +9,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useGet} from "~/utils/HttpUtils";
import type {Realm} from "~/realm/Realm";
import Sidebar from "~/components/Sidebar.vue"; import Sidebar from "~/components/Sidebar.vue";
import PopupSlot from '~/components/ui/popup/PopupSlot.vue'; import PopupSlot from '~/components/ui/popup/PopupSlot.vue';
import {useRealmStore} from "~/realm/RealmStore";
const realms = useGet<Realm[]>("/api/realms"); const realms = computed(() => useRealmStore().getRealms());
onMounted(() => {
useRealmStore().init();
})
</script> </script>
<style scoped> <style scoped>

View File

@ -28,7 +28,7 @@ const client = ref(new ClientCreation());
function addClient() function addClient()
{ {
console.log(client.value); console.log(client.value);
usePost<Client>("/api/realms/" + useRoute().params.realm_key + "/clients", client.value, (response) => { usePost<Client, ClientCreation>("/api/realms/" + useRoute().params.realm_key + "/clients", client.value, (response) => {
const callback = usePopup().get()?.config?.callback; const callback = usePopup().get()?.config?.callback;
if (callback) if (callback)
{ {

View File

@ -1,25 +1,38 @@
<template> <template>
<div class="sidebar content-l"> <div class="sidebar content-l">
<h2>Verifoo</h2> <h2>Verifoo</h2>
<div class="content-s"> <div class="inner-sidebar">
<NuxtLink class="sidebar-link">Home</NuxtLink> <div class="content-s">
<NuxtLink class="sidebar-link" v-for="realm in realms" :to="'/realms/' + realm.key">{{ realm.name }}</NuxtLink> <NuxtLink class="sidebar-link" to="/">Home</NuxtLink>
<NuxtLink class="sidebar-link" v-for="realm in realms" :to="'/realms/' + realm.key + '/home'">{{ realm.name }}</NuxtLink>
</div>
<div class="content-s">
<UiButton icon="add" reverse @click="addRealm">Create</UiButton>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {Realm} from "~/realm/Realm"; import type {Realm} from "~/realm/Realm";
import {Popup, PopupSize, usePopup} from "~/components/ui/popup/Popup";
import RealmAdd from "~/realm/RealmAdd.vue";
defineProps<{ defineProps<{
realms: Realm[] realms: Realm[]
}>() }>()
function addRealm()
{
usePopup().open(Popup.component(RealmAdd).setConfig({ heading: 'Add Realm', size: PopupSize.MEDIUM }));
}
</script> </script>
<style scoped> <style scoped>
.sidebar { .sidebar {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
padding: 1rem 2rem; padding: 1rem 2rem;
background-color: #f1f1f1; background-color: #f1f1f1;
align-content: flex-start; align-content: flex-start;
@ -35,4 +48,8 @@ defineProps<{
background-color: #000000; background-color: #000000;
color: white; color: white;
} }
.inner-sidebar {
display: grid;
grid-template-rows: 1fr auto;
}
</style> </style>

View File

@ -34,11 +34,17 @@ const props = defineProps<{
white-space: nowrap; white-space: nowrap;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 0.5rem; padding: 0.5rem;
font-weight: 500; font-weight: 550;
font-size: 1rem; font-size: 1rem;
} }
.button * { .button * {
color: white; color: white;
fill: white; fill: white;
} }
.small {
height: 1.75rem;
}
.button.disabled {
background-color: #7a7a7a;
}
</style> </style>

View File

@ -0,0 +1,18 @@
<template>
<div class="error-alert">
<slot></slot>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.error-alert {
background-color: #ffeadf;
border: 1px solid #ff9c80;
padding: 0.5rem;
border-radius: 0.25rem;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="overlay center" @click="closeOutside" v-if="popup"> <div class="overlay center" @click="closeOutside" v-if="popup && popup.config">
<div class="popup" :class="popup.config.size" @click.stop> <div class="popup" :class="popup.config.size" @click.stop>
<div class="popup__header"> <div class="popup__header">
<h2>{{ popup.config.heading }}</h2> <h2>{{ popup.config.heading }}</h2>
@ -19,7 +19,7 @@ const popup = computed(() => usePopup().get());
function closeOutside() function closeOutside()
{ {
if (popup.value?.config.closeOnOverlayClick) if (popup.value?.config?.closeOnOverlayClick)
{ {
usePopup().close(); usePopup().close();
} }

View File

@ -1,24 +1,42 @@
<template> <template>
<div> <div class="content-l" v-if="realm.data">
<Table columns="1fr"> <div class="col-2">
<HeaderRow> <div class="content-m">
<HeaderCell>Client ID</HeaderCell> <h3>Name</h3>
</HeaderRow> <p class="tile-m">{{realm.data.name}}</p>
<ContentRow> </div>
</ContentRow> <div class="content-m">
</Table> <h3>Access Token Lifetime</h3>
<p class="tile-m">{{(realm.data.lifetime / 60)}} Minutes</p>
</div>
</div>
<ErrorAlert>
<div class="spaced-center">
<p>Delete Realm</p>
<UiButton class="delete-button" @click="deleteRealm(realm.data)">Delete</UiButton>
</div>
</ErrorAlert>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import HeaderRow from "~/components/ui/table/HeaderRow.vue"; import ErrorAlert from "~/components/ui/alert/ErrorAlert.vue";
import Table from "~/components/ui/table/Table.vue"; import {Popup, PopupSize, usePopup} from "~/components/ui/popup/Popup";
import HeaderCell from "~/components/ui/table/HeaderCell.vue"; import RealmDelete from "~/realm/RealmDelete.vue";
import ContentCell from "~/components/ui/table/ContentCell.vue"; import type {Realm} from "~/realm/Realm";
import ContentRow from "~/components/ui/table/ContentRow.vue"; import {useGet} from "~/utils/HttpUtils";
import UiButton from "~/components/ui/UiButton.vue";
const realm = useGet<Realm>("/api/realms/" + useRoute().params.realm_key);
function deleteRealm(realm: Realm)
{
usePopup().open(Popup.component<Realm>(RealmDelete).setConfig({ heading: 'Delete Realm', size: PopupSize.MEDIUM }).setPayload(realm))
}
</script> </script>
<style scoped> <style scoped>
.delete-button {
background-color: red;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<Table columns="auto auto auto 1fr" v-if="keys.data"> <Table columns="auto auto auto 1fr" :show-footer="keys.data.keys.length === 0" v-if="keys.data">
<HeaderRow> <HeaderRow>
<HeaderCell>ID</HeaderCell> <HeaderCell>ID</HeaderCell>
<HeaderCell>Key Type</HeaderCell> <HeaderCell>Key Type</HeaderCell>
@ -13,6 +13,9 @@
<ContentCell v-for="key in keys.data.keys">{{ key.alg }}</ContentCell> <ContentCell v-for="key in keys.data.keys">{{ key.alg }}</ContentCell>
<ContentCell v-for="key in keys.data.keys">{{ key.use }}</ContentCell> <ContentCell v-for="key in keys.data.keys">{{ key.use }}</ContentCell>
</ContentRow> </ContentRow>
<template #footer>
<p>No keys found.</p>
</template>
</Table> </Table>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="content-l"> <div class="content-l">
<Table columns="1fr auto"> <Table columns="1fr auto" :show-footer="roles.data.length === 0" v-if="roles.data">
<HeaderRow> <HeaderRow>
<HeaderCell>Name</HeaderCell> <HeaderCell>Name</HeaderCell>
<HeaderCell></HeaderCell> <HeaderCell></HeaderCell>
@ -12,6 +12,9 @@
<UiButton @click="deleteRole(role)"><UiIcon>delete</UiIcon></UiButton> <UiButton @click="deleteRole(role)"><UiIcon>delete</UiIcon></UiButton>
</ContentCell> </ContentCell>
</ContentRow> </ContentRow>
<template #footer>
<p>No roles found.</p>
</template>
</Table> </Table>
<div> <div>
<UiButton @click="addRole">Create</UiButton> <UiButton @click="addRole">Create</UiButton>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="content-l" v-if="user.data"> <div class="content-l" v-if="user.data && roleAssignments.data">
<p class="left-center" style="gap: 0.25rem"><span class="link" @click="useRouter().push('/realms/' + useRoute().params.realm_key + '/users')">Users</span><UiIcon>chevron_right</UiIcon><span>{{user.data.email}}</span></p> <p class="left-center" style="gap: 0.25rem"><span class="link" @click="useRouter().push('/realms/' + useRoute().params.realm_key + '/users')">Users</span><UiIcon>chevron_right</UiIcon><span>{{user.data.email}}</span></p>
<div class="col-2"> <div class="col-2">
<div class="tile-m"> <div class="tile-m">
@ -11,6 +11,7 @@
<p>{{user.data.firstname}} {{user.data.lastname}}</p> <p>{{user.data.firstname}} {{user.data.lastname}}</p>
</div> </div>
</div> </div>
<UserRoleDisplay :role-assignments="roleAssignments.data"></UserRoleDisplay>
<div class="left-center"> <div class="left-center">
<UiButton icon="edit" reverse>Edit</UiButton> <UiButton icon="edit" reverse>Edit</UiButton>
<UiButton icon="delete" reverse>Delete</UiButton> <UiButton icon="delete" reverse>Delete</UiButton>
@ -20,8 +21,23 @@
<script setup lang="ts"> <script setup lang="ts">
import type {User} from "~/user/User"; import type {User} from "~/user/User";
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
import ContentRow from "~/components/ui/table/ContentRow.vue";
import ContentCell from "~/components/ui/table/ContentCell.vue";
import Table from "~/components/ui/table/Table.vue";
import type {RoleAssignment} from "~/role-assignment/RoleAssignment";
import {Popup, PopupSize, usePopup} from "~/components/ui/popup/Popup";
import type {Role} from "~/role/Role";
import UserRoleAssignment from "~/user/UserRoleAssignment.vue";
import UiButton from "~/components/ui/UiButton.vue";
import UserRoleDisplay from "~/user/UserRoleDisplay.vue";
const user = useGet<User>("/api/realms/" + useRoute().params.realm_key + "/users/" + useRoute().params.user_id); const realmKey = useRoute().params.realm_key;
const userId = useRoute().params.user_id;
const user = useGet<User>("/api/realms/" + realmKey + "/users/" + userId);
const roleAssignments = useGet<RoleAssignment>("/api/realms/" + realmKey + "/users/" + userId + "/role-assignments");
</script> </script>
<style scoped> <style scoped>

View File

@ -2,8 +2,15 @@ export class Realm
{ {
constructor( constructor(
public key: string, public key: string,
public name: string public name: string,
public lifetime: number
) )
{ {
} }
}
export class RealmCreation
{
key?: string;
name?: string;
} }

32
app/realm/RealmAdd.vue Normal file
View File

@ -0,0 +1,32 @@
<template>
<form class="content-l" @submit.prevent="addRealm">
<div class="col-2">
<UiInput label="Name" required>
<input type="text" v-model="realm.name" required>
</UiInput>
<UiInput label="Key" required>
<input type="text" v-model="realm.key" required>
</UiInput>
</div>
<div class="center">
<UiButton type="submit">Create</UiButton>
</div>
</form>
</template>
<script setup lang="ts">
import {usePopup} from "~/components/ui/popup/Popup";
import {Realm, RealmCreation} from "~/realm/Realm";
import {useRealmStore} from "~/realm/RealmStore";
const realm = ref(new RealmCreation());
function addRealm()
{
useRealmStore().add(realm.value);
}
</script>
<style scoped>
</style>

30
app/realm/RealmDelete.vue Normal file
View File

@ -0,0 +1,30 @@
<template>
<form class="content-l" v-if="popup.payload" @submit.prevent="deleteRealm(popup.payload)">
<UiWarning><p>Are you sure you want to delete realm <span>{{ popup.payload.name }} ({{ popup.payload.key }})</span>?</p></UiWarning>
<div class="center">
<UiButton type="submit">Delete</UiButton>
</div>
</form>
</template>
<script setup lang="ts">
import {usePopup} from "~/components/ui/popup/Popup";
import type {User} from "~/user/User";
import {useDelete} from "~/utils/HttpUtils";
import UiWarning from "~/components/ui/UiWarning.vue";
import type {Realm} from "~/realm/Realm";
import {useRealmStore} from "~/realm/RealmStore";
const popup = usePopup().require<Realm>();
function deleteRealm(realm: Realm)
{
useRealmStore().delete(realm);
}
</script>
<style scoped>
span {
font-weight: bold;
}
</style>

45
app/realm/RealmStore.ts Normal file
View File

@ -0,0 +1,45 @@
import {Realm, RealmCreation} from "~/realm/Realm";
import {useGet} from "~/utils/HttpUtils";
import {usePopup} from "~/components/ui/popup/Popup";
export const useRealmStore = defineStore('realms', {
state: () => ({
realms: useGet<Realm[]>("/api/realms").value.data
}),
getters: {
getRealms: (state) => {
return () => {
return state.realms;
}
}
},
actions: {
init()
{
const realms = useGet<Realm[]>("/api/realms");
watchEffect(() => {
this.realms = realms.value.data;
})
},
add(realm: RealmCreation)
{
usePost<Realm, RealmCreation>("/api/realms", realm, (response) => {
this.realms?.push(response);
usePopup().close();
useRouter().push("/realms/" + response.key + '/home');
});
},
delete(realm: Realm)
{
useDelete("/api/realms/" + realm.key, () => {
const index = this.realms?.findIndex(item => item.key === realm.key);
if (index && index >= 0)
{
this.realms?.splice(index, 1);
usePopup().close();
useRouter().push("/");
}
})
}
},
})

View File

@ -0,0 +1,9 @@
import type {Role} from "~/role/Role";
export class RoleAssignment
{
constructor(
public assigned: Role[],
public unassigned: Role[]
) {}
}

View File

@ -16,7 +16,7 @@ const role = ref(new RoleCreation());
function addRole() function addRole()
{ {
usePost<Role>("/api/realms/" + useRoute().params.realm_key + "/roles", role.value, (role: Role) => { usePost<Role, RoleCreation>("/api/realms/" + useRoute().params.realm_key + "/roles", role.value, (role: Role) => {
usePopup().callback(role); usePopup().callback(role);
usePopup().close(); usePopup().close();
}); });

View File

@ -28,7 +28,7 @@ const user = ref(new UserCreation());
function addUser() function addUser()
{ {
usePost<User>("/api/realms/" + useRoute().params.realm_key + "/users", user.value, (response) => { usePost<User, UserCreation>("/api/realms/" + useRoute().params.realm_key + "/users", user.value, (response) => {
const callback = usePopup().get()?.config?.callback; const callback = usePopup().get()?.config?.callback;
if (callback) if (callback)
{ {

View File

@ -28,7 +28,7 @@ const user = ref(new UserCreation());
function addUser() function addUser()
{ {
usePost<User>("/api/realms/" + useRoute().params.realm_key + "/users", user.value, (response) => { usePost<User, UserCreation>("/api/realms/" + useRoute().params.realm_key + "/users", user.value, (response) => {
const callback = usePopup().get()?.config?.callback; const callback = usePopup().get()?.config?.callback;
if (callback) if (callback)
{ {

View File

@ -0,0 +1,46 @@
<template>
<form class="content-l" @submit.prevent="assign(selection)" v-if="roles && roles.length > 0">
<div class="content-m">
<p class="tile-m role left-center" :class="{ selected: selection?.name === role.name }" v-for="role in roles" @click="selection = role">{{ role.name }}</p>
</div>
<div class="center">
<UiButton type="submit" :disabled="selection == null">Assign</UiButton>
</div>
</form>
<div v-else>
<p class="tile-m">There are no roles available for assignment.</p>
</div>
</template>
<script setup lang="ts">
import type {Role} from "~/role/Role";
import {usePopup} from "~/components/ui/popup/Popup";
import {usePost, usePut} from "~/utils/HttpUtils";
const selection: Ref<Role | undefined> = ref();
const roles = usePopup().require<Role[]>().payload;
function assign(role?: Role)
{
if (role != null)
{
const realmKey = useRoute().params.realm_key;
const userId = useRoute().params.user_id;
usePut<any, { assign: string[] }>("/api/realms/" + realmKey + "/users/" + userId + "/role-assignments", { assign: [role.name]}, () => {
usePopup().callback(role);
usePopup().close();
})
}
}
</script>
<style scoped>
.role {
height: 2.5rem;
border: 2px solid transparent;
}
.selected {
border-color: black;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="content-m">
<div class="left-center">
<h3>Roles</h3>
<UiButton class="small" icon="group_add" @click="assignRoles(roleAssignments.unassigned)" reverse>Assign</UiButton>
</div>
<Table columns="1fr auto" :show-footer="roleAssignments.assigned.length === 0">
<HeaderRow>
<HeaderCell>Name</HeaderCell>
<HeaderCell></HeaderCell>
</HeaderRow>
<ContentRow v-for="role in roleAssignments.assigned">
<ContentCell>{{role.name}}</ContentCell>
<ContentCell style="padding: 0.25rem;" @click="unassign(role)"><UiButton icon="delete"></UiButton></ContentCell>
</ContentRow>
<template #footer>
<p>No roles assigned.</p>
</template>
</Table>
</div>
</template>
<script setup lang="ts">
import UiButton from "~/components/ui/UiButton.vue";
import Table from "~/components/ui/table/Table.vue";
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
import ContentCell from "~/components/ui/table/ContentCell.vue";
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
import ContentRow from "~/components/ui/table/ContentRow.vue";
import type {RoleAssignment} from "~/role-assignment/RoleAssignment";
import type {Role} from "~/role/Role";
import {Popup, PopupSize, usePopup} from "~/components/ui/popup/Popup";
import UserRoleAssignment from "~/user/UserRoleAssignment.vue";
const props = defineProps<{
roleAssignments: RoleAssignment
}>()
function unassign(role: Role)
{
const callback = (role: Role) => {
props.roleAssignments.unassigned.push(role);
const index = props.roleAssignments.assigned.findIndex(item => item.name === role.name);
if (index >= 0)
{
props.roleAssignments.assigned.splice(index, 1);
}
}
const realmKey = useRoute().params.realm_key;
const userId = useRoute().params.user_id;
usePut<any, { unassign: string[] }>("/api/realms/" + realmKey + "/users/" + userId + "/role-assignments", { unassign: [role.name] }, () => {
callback(role);
});
}
function assignRoles(unassignedRoles: Role[])
{
const callback = (role: Role) => {
props.roleAssignments.assigned.push(role);
const index = props.roleAssignments.unassigned.findIndex(item => item.name === role.name);
if (index >= 0)
{
props.roleAssignments.unassigned.splice(index, 1);
}
}
usePopup().open(Popup.component<Role[]>(UserRoleAssignment).setPayload(unassignedRoles).setConfig({ heading: 'Assign Roles', size: PopupSize.MEDIUM, callback: callback }));
}
</script>
<style scoped>
</style>

View File

@ -5,7 +5,7 @@ export class PostRequestState
loading?: boolean = true; loading?: boolean = true;
} }
export function usePost<R>(path: string, body?: any, onSuccess?: (response: R) => void, onError?: () => {}) export function usePost<R, P>(path: string, body?: P, onSuccess?: (response: R) => void, onError?: () => {})
{ {
const state = ref(new PostRequestState()); const state = ref(new PostRequestState());
axios.post<R>(path, body) axios.post<R>(path, body)
@ -82,7 +82,7 @@ export class PutRequestState
loading?: boolean = true; loading?: boolean = true;
} }
export function usePut<R>(path: string, body?: any, onSuccess?: (response: R) => void, onError?: () => {}) export function usePut<R, P>(path: string, body?: P, onSuccess?: (response: R) => void, onError?: () => {})
{ {
const state = ref(new PutRequestState()); const state = ref(new PutRequestState());
axios.put<R>(path, body) axios.put<R>(path, body)

View File

@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
event.headers.delete("host"); event.headers.delete("host");
event.headers.delete("origin"); event.headers.delete("origin");
event.headers.set("authorization", credentials) event.headers.set("authorization", credentials);
return sendProxy(event, process.env.NUXT_PUBLIC_BASE_URL + event.path, { return sendProxy(event, process.env.NUXT_PUBLIC_BASE_URL + event.path, {
fetchOptions: { fetchOptions: {