Add key management to admin settings

This commit is contained in:
2023-06-30 20:52:36 +02:00
parent 78cec1c24f
commit 757cec7438
5 changed files with 251 additions and 44 deletions

View File

@ -6,6 +6,11 @@ export function registerAdmin(app) {
app.post("/api/admin/timetable", createTimetable);
app.put("/api/admin/timetable", editTimetable);
app.delete("/api/admin/timetable", deleteTimetable);
app.get("/api/admin/key", listKeys);
app.post("/api/admin/key", createKey);
app.put("/api/admin/key", editKey);
app.delete("/api/admin/key", deleteKey);
}
function sendMissingArguments(res) {
@ -76,3 +81,72 @@ async function deleteTimetable(req, res) {
res.status(500).send(e);
}
}
async function listKeys(_, res) {
res.send(await prisma.key.findMany());
}
async function createKey(req, res) {
let data = req.body;
if (!data.key) {
sendMissingArguments(res);
return;
}
const existingKey = await prisma.key.findUnique({
where: {
key: data.key,
},
});
if (existingKey) {
res.status(400).send({
success: false,
error: "key_already_exists",
});
return;
}
const key = await prisma.key.create({
data: {
key: data.key,
permissions: data.permissions || [],
validUntil: data.validUntil,
notes: data.notes,
},
});
res.status(201).send(key);
}
async function editKey(req, res) {
if (!req.query.id) {
sendMissingArguments(res);
return;
}
try {
const timetable = await prisma.key.update({
where: {
key: req.query.id,
},
data: req.body,
});
res.status(201).send(timetable);
} catch (e) {
res.status(500).send(e);
}
}
async function deleteKey(req, res) {
if (!req.query.id) {
sendMissingArguments(res);
return;
}
try {
await prisma.key.delete({
where: {
key: req.query.id,
},
});
res.status(200).send();
} catch (e) {
res.status(500).send(e);
}
}

View File

@ -24,5 +24,6 @@ defineProps(["title"]);
user-select: none;
font-size: 19px;
margin: 5px 0px;
cursor: pointer;
}
</style>

View File

@ -1,4 +1,5 @@
<script setup>
import { EditIcon } from "lucide-vue-next";
import { TrashIcon, AlertCircleIcon } from "lucide-vue-next";
import { ref, watch } from "vue";
@ -9,14 +10,15 @@ watch(deleteConfirm, (value) => {
}
});
defineProps(["keyData"]);
defineEmits(["delete"]);
defineProps(["keyData", "edit"]);
defineEmits(["delete", "edit"]);
</script>
<template>
<div class="card">
<div class="info">
<p class="key">{{ keyData.key }}</p>
<p class="notes" v-if="edit">{{ keyData.notes }}</p>
<div class="permissions">
<div
class="permission"
@ -28,6 +30,7 @@ defineEmits(["delete"]);
</div>
</div>
<div class="button">
<EditIcon v-if="edit" @click="$emit('edit')" />
<TrashIcon v-if="!deleteConfirm" @click="deleteConfirm = true" />
<AlertCircleIcon color="red" v-else @click="$emit('delete')" />
</div>
@ -49,6 +52,12 @@ defineEmits(["delete"]);
padding: 15px 10px 0px 10px;
}
.notes {
padding: 0px 10px;
font-size: 14px;
opacity: 0.7;
}
.permissions {
display: flex;
flex-direction: row;

View File

@ -1,71 +1,112 @@
<script setup>
import ExpandSection from "@/components/settings/expand-section.vue";
import KeyCard from "@/components/settings/key-card.vue";
import TimetableCard from "@/components/settings/timetable-card.vue";
import { baseUrl } from "@/store";
import { PlusIcon, SaveIcon, XIcon } from "lucide-vue-next";
import { PlusIcon, SaveIcon, XIcon, RefreshCwIcon } from "lucide-vue-next";
import { ref } from "vue";
const timetables = ref([]);
async function fetchTimetables() {
const response = await fetch(baseUrl + "/admin/timetable");
if (response.status != 200) return;
timetables.value = await response.json();
function confirm(message) {
return window.confirm(message);
}
async function deleteTimetable(id) {
/* General */
async function fetchObjects(type) {
const response = await fetch(baseUrl + "/admin/" + type);
if (response.status != 200) return;
return await response.json();
}
async function deleteObject(type, id) {
const response = await fetch(
baseUrl + "/admin/timetable?id=" + encodeURIComponent(id),
baseUrl + `/admin/${type}?id=${encodeURIComponent(id)}`,
{ method: "delete" }
);
if (response.status != 200) alert("Delete failed!");
fetchTimetables();
updateData();
}
const editId = ref(-1);
const timetableName = ref();
const timetableClass = ref();
const timetableSource = ref();
const timetableTrusted = ref(true);
async function createTimetable() {
const response = await fetch(baseUrl + "/admin/timetable", {
async function createObject(type, data) {
const response = await fetch(baseUrl + "/admin/" + type, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: timetableName.value,
class: timetableClass.value,
source: timetableSource.value,
trusted: timetableTrusted.value,
data: [],
}),
body: JSON.stringify(data),
});
if (response.status != 201) alert("Post failed!");
fetchTimetables();
updateData();
}
async function updateTimetable() {
async function updateObject(type, id, data) {
const response = await fetch(
baseUrl + "/admin/timetable?id=" + encodeURIComponent(editId.value),
baseUrl + `/admin/${type}?id=${encodeURIComponent(id)}`,
{
method: "put",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: timetableName.value,
class: timetableClass.value,
source: timetableSource.value,
trusted: timetableTrusted.value,
}),
body: JSON.stringify(data),
}
);
if (response.status != 201) alert("Post failed!");
fetchTimetables();
updateData();
}
fetchTimetables();
/* Timetable */
const timetables = ref([]);
const timetableEditId = ref(-1);
const timetableName = ref();
const timetableClass = ref();
const timetableSource = ref();
const timetableTrusted = ref(true);
async function createTimetable() {
return await createObject("timetable", {
title: timetableName.value,
class: timetableClass.value,
source: timetableSource.value,
trusted: timetableTrusted.value,
data: [],
});
}
async function updateTimetable() {
return await updateObject("timetable", timetableEditId.value, {
title: timetableName.value,
class: timetableClass.value,
source: timetableSource.value,
trusted: timetableTrusted.value,
});
}
/* Key */
const keys = ref([]);
const keyEditId = ref(-1);
const keyId = ref();
const keyPermissions = ref();
const keyNotes = ref();
async function createKey() {
return await createObject("key", {
key: keyId.value,
permissions: keyPermissions.value,
notes: keyNotes.value,
});
}
async function updateKey() {
return await updateObject("key", keyEditId.value, {
key: keyId.value,
permissions: keyPermissions.value.split(","),
notes: keyNotes.value,
});
}
function regenerateId() {
keyId.value = Math.random().toString(36).slice(-8);
}
async function updateData() {
timetables.value = await fetchObjects("timetable");
keys.value = await fetchObjects("key");
}
updateData();
</script>
<template>
@ -80,15 +121,27 @@ fetchTimetables();
<input type="checkbox" v-model="timetableTrusted" />
<span>Trusted</span>
</div>
<div class="button" v-if="editId == -1" @click="createTimetable()">
<div
class="button"
v-if="timetableEditId == -1"
@click="createTimetable()"
>
<PlusIcon />
<span>Create Timetable</span>
</div>
<div class="button" v-if="editId != -1" @click="updateTimetable()">
<div
class="button"
v-if="timetableEditId != -1"
@click="updateTimetable()"
>
<SaveIcon />
<span>Save Timetable</span>
</div>
<div class="button" v-if="editId != -1" @click="editId = -1">
<div
class="button"
v-if="timetableEditId != -1"
@click="timetableEditId = -1"
>
<XIcon />
<span>Cancel edit</span>
</div>
@ -98,12 +151,12 @@ fetchTimetables();
:key="timetable"
:timetable="timetable"
:editable="true"
:selected="timetable.id == editId"
:selected="timetable.id == timetableEditId"
:admin="true"
@delete="deleteTimetable(timetable.id)"
@delete="deleteObject('timetable', timetable.id)"
@edit="
() => {
editId = timetable.id;
timetableEditId = timetable.id;
timetableName = timetable.title;
timetableClass = timetable.class;
timetableSource = timetable.source;
@ -113,6 +166,63 @@ fetchTimetables();
/>
</div>
</ExpandSection>
<ExpandSection title="Keys">
<div class="list">
<div class="create_options">
<div class="inline">
<input type="text" placeholder="Key" v-model="keyId" />
<RefreshCwIcon
:size="24"
class="button inline"
@click="regenerateId()"
/>
</div>
<input
type="text"
placeholder="Permissions (eg. perm1,perm2)"
v-model="keyPermissions"
/>
<input type="text" placeholder="Notes" v-model="keyNotes" />
<div class="button" v-if="keyEditId == -1" @click="createKey()">
<PlusIcon />
<span>Create Key</span>
</div>
<div class="button" v-if="keyEditId != -1" @click="updateKey()">
<SaveIcon />
<span>Save Key</span>
</div>
<div class="button" v-if="keyEditId != -1" @click="keyEditId = -1">
<XIcon />
<span>Cancel edit</span>
</div>
</div>
<KeyCard
:edit="true"
:keyData="key"
v-for="key in keys"
:key="key"
@delete="
() => {
if (
key.permissions.includes('admin') &&
!confirm('Are you sure you want to delete an admin key?')
) {
return;
}
deleteObject('key', key.key);
}
"
@edit="
() => {
keyEditId = key.key;
keyId = key.key;
keyPermissions = key.permissions.join(',');
keyNotes = key.notes;
}
"
/>
</div>
</ExpandSection>
</template>
<style scoped>
@ -138,11 +248,23 @@ h1 {
.create_options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
justify-content: center;
align-items: center;
gap: 10px;
padding: 10px;
cursor: pointer;
}
.inline {
display: grid;
grid-template-columns: 1fr auto;
box-sizing: border-box;
gap: 5px;
width: 100%;
}
.inline .button {
padding: 5px;
}
</style>

View File

@ -77,6 +77,7 @@ async function revokeKey(key) {
v-for="key in sessionInfo.appliedKeys"
:key="key"
:keyData="key"
:edit="false"
@delete="() => revokeKey(key.key)"
/>
</div>