✨ Add key management to admin settings
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -24,5 +24,6 @@ defineProps(["title"]);
|
||||
user-select: none;
|
||||
font-size: 19px;
|
||||
margin: 5px 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user