✨ 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.post("/api/admin/timetable", createTimetable);
|
||||||
app.put("/api/admin/timetable", editTimetable);
|
app.put("/api/admin/timetable", editTimetable);
|
||||||
app.delete("/api/admin/timetable", deleteTimetable);
|
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) {
|
function sendMissingArguments(res) {
|
||||||
@ -76,3 +81,72 @@ async function deleteTimetable(req, res) {
|
|||||||
res.status(500).send(e);
|
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;
|
user-select: none;
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
margin: 5px 0px;
|
margin: 5px 0px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { EditIcon } from "lucide-vue-next";
|
||||||
import { TrashIcon, AlertCircleIcon } from "lucide-vue-next";
|
import { TrashIcon, AlertCircleIcon } from "lucide-vue-next";
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
@ -9,14 +10,15 @@ watch(deleteConfirm, (value) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps(["keyData"]);
|
defineProps(["keyData", "edit"]);
|
||||||
defineEmits(["delete"]);
|
defineEmits(["delete", "edit"]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<p class="key">{{ keyData.key }}</p>
|
<p class="key">{{ keyData.key }}</p>
|
||||||
|
<p class="notes" v-if="edit">{{ keyData.notes }}</p>
|
||||||
<div class="permissions">
|
<div class="permissions">
|
||||||
<div
|
<div
|
||||||
class="permission"
|
class="permission"
|
||||||
@ -28,6 +30,7 @@ defineEmits(["delete"]);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button">
|
<div class="button">
|
||||||
|
<EditIcon v-if="edit" @click="$emit('edit')" />
|
||||||
<TrashIcon v-if="!deleteConfirm" @click="deleteConfirm = true" />
|
<TrashIcon v-if="!deleteConfirm" @click="deleteConfirm = true" />
|
||||||
<AlertCircleIcon color="red" v-else @click="$emit('delete')" />
|
<AlertCircleIcon color="red" v-else @click="$emit('delete')" />
|
||||||
</div>
|
</div>
|
||||||
@ -49,6 +52,12 @@ defineEmits(["delete"]);
|
|||||||
padding: 15px 10px 0px 10px;
|
padding: 15px 10px 0px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
padding: 0px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.permissions {
|
.permissions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -1,71 +1,112 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ExpandSection from "@/components/settings/expand-section.vue";
|
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 TimetableCard from "@/components/settings/timetable-card.vue";
|
||||||
import { baseUrl } from "@/store";
|
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";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const timetables = ref([]);
|
function confirm(message) {
|
||||||
async function fetchTimetables() {
|
return window.confirm(message);
|
||||||
const response = await fetch(baseUrl + "/admin/timetable");
|
|
||||||
if (response.status != 200) return;
|
|
||||||
timetables.value = await response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
const response = await fetch(
|
||||||
baseUrl + "/admin/timetable?id=" + encodeURIComponent(id),
|
baseUrl + `/admin/${type}?id=${encodeURIComponent(id)}`,
|
||||||
{ method: "delete" }
|
{ method: "delete" }
|
||||||
);
|
);
|
||||||
if (response.status != 200) alert("Delete failed!");
|
if (response.status != 200) alert("Delete failed!");
|
||||||
fetchTimetables();
|
updateData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const editId = ref(-1);
|
async function createObject(type, data) {
|
||||||
const timetableName = ref();
|
const response = await fetch(baseUrl + "/admin/" + type, {
|
||||||
const timetableClass = ref();
|
|
||||||
const timetableSource = ref();
|
|
||||||
const timetableTrusted = ref(true);
|
|
||||||
|
|
||||||
async function createTimetable() {
|
|
||||||
const response = await fetch(baseUrl + "/admin/timetable", {
|
|
||||||
method: "post",
|
method: "post",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(data),
|
||||||
title: timetableName.value,
|
|
||||||
class: timetableClass.value,
|
|
||||||
source: timetableSource.value,
|
|
||||||
trusted: timetableTrusted.value,
|
|
||||||
data: [],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (response.status != 201) alert("Post failed!");
|
if (response.status != 201) alert("Post failed!");
|
||||||
fetchTimetables();
|
updateData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTimetable() {
|
async function updateObject(type, id, data) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
baseUrl + "/admin/timetable?id=" + encodeURIComponent(editId.value),
|
baseUrl + `/admin/${type}?id=${encodeURIComponent(id)}`,
|
||||||
{
|
{
|
||||||
method: "put",
|
method: "put",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(data),
|
||||||
title: timetableName.value,
|
|
||||||
class: timetableClass.value,
|
|
||||||
source: timetableSource.value,
|
|
||||||
trusted: timetableTrusted.value,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (response.status != 201) alert("Post failed!");
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -80,15 +121,27 @@ fetchTimetables();
|
|||||||
<input type="checkbox" v-model="timetableTrusted" />
|
<input type="checkbox" v-model="timetableTrusted" />
|
||||||
<span>Trusted</span>
|
<span>Trusted</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button" v-if="editId == -1" @click="createTimetable()">
|
<div
|
||||||
|
class="button"
|
||||||
|
v-if="timetableEditId == -1"
|
||||||
|
@click="createTimetable()"
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
<span>Create Timetable</span>
|
<span>Create Timetable</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button" v-if="editId != -1" @click="updateTimetable()">
|
<div
|
||||||
|
class="button"
|
||||||
|
v-if="timetableEditId != -1"
|
||||||
|
@click="updateTimetable()"
|
||||||
|
>
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
<span>Save Timetable</span>
|
<span>Save Timetable</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button" v-if="editId != -1" @click="editId = -1">
|
<div
|
||||||
|
class="button"
|
||||||
|
v-if="timetableEditId != -1"
|
||||||
|
@click="timetableEditId = -1"
|
||||||
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span>Cancel edit</span>
|
<span>Cancel edit</span>
|
||||||
</div>
|
</div>
|
||||||
@ -98,12 +151,12 @@ fetchTimetables();
|
|||||||
:key="timetable"
|
:key="timetable"
|
||||||
:timetable="timetable"
|
:timetable="timetable"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:selected="timetable.id == editId"
|
:selected="timetable.id == timetableEditId"
|
||||||
:admin="true"
|
:admin="true"
|
||||||
@delete="deleteTimetable(timetable.id)"
|
@delete="deleteObject('timetable', timetable.id)"
|
||||||
@edit="
|
@edit="
|
||||||
() => {
|
() => {
|
||||||
editId = timetable.id;
|
timetableEditId = timetable.id;
|
||||||
timetableName = timetable.title;
|
timetableName = timetable.title;
|
||||||
timetableClass = timetable.class;
|
timetableClass = timetable.class;
|
||||||
timetableSource = timetable.source;
|
timetableSource = timetable.source;
|
||||||
@ -113,6 +166,63 @@ fetchTimetables();
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ExpandSection>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -138,11 +248,23 @@ h1 {
|
|||||||
|
|
||||||
.create_options {
|
.create_options {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline .button {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -77,6 +77,7 @@ async function revokeKey(key) {
|
|||||||
v-for="key in sessionInfo.appliedKeys"
|
v-for="key in sessionInfo.appliedKeys"
|
||||||
:key="key"
|
:key="key"
|
||||||
:keyData="key"
|
:keyData="key"
|
||||||
|
:edit="false"
|
||||||
@delete="() => revokeKey(key.key)"
|
@delete="() => revokeKey(key.key)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user