✨ Allow updating remote timetables with permission
This commit is contained in:
@ -82,6 +82,34 @@ export async function getTimetable(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit timetable API endpoint (/api/timetable)
|
||||||
|
// Updates a remote timetable with the requested data
|
||||||
|
export async function putTimetable(req, res) {
|
||||||
|
const timetableId = parseInt(req.query.id);
|
||||||
|
const data = req.body.data;
|
||||||
|
if (
|
||||||
|
!(await hasPermission(req.locals.session, "timetable.update", timetableId))
|
||||||
|
) {
|
||||||
|
res.status(401).send({
|
||||||
|
success: false,
|
||||||
|
error: "missing_permission",
|
||||||
|
message: "You don't have permission to update this timetable!",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.timetable.update({
|
||||||
|
where: {
|
||||||
|
id: timetableId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
title: req.body.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.status(201).send();
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function for converting a date string
|
// Helper function for converting a date string
|
||||||
// (eg. "2022-06-02" or "1654128000000") to a
|
// (eg. "2022-06-02" or "1654128000000") to a
|
||||||
// unix timestamp
|
// unix timestamp
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
getSubstitutions,
|
getSubstitutions,
|
||||||
getHistory,
|
getHistory,
|
||||||
getClasses,
|
getClasses,
|
||||||
|
putTimetable,
|
||||||
getInfo,
|
getInfo,
|
||||||
putKey,
|
putKey,
|
||||||
deleteKey,
|
deleteKey,
|
||||||
@ -63,6 +64,7 @@ app.get("/api/info", getInfo);
|
|||||||
app.put("/api/key", putKey);
|
app.put("/api/key", putKey);
|
||||||
app.delete("/api/key", deleteKey);
|
app.delete("/api/key", deleteKey);
|
||||||
app.get("/api/timetable", getTimetable);
|
app.get("/api/timetable", getTimetable);
|
||||||
|
app.put("/api/timetable", putTimetable);
|
||||||
app.get("/api/substitutions", getSubstitutions);
|
app.get("/api/substitutions", getSubstitutions);
|
||||||
app.get("/api/history", getHistory);
|
app.get("/api/history", getHistory);
|
||||||
app.get("/api/classes", getClasses);
|
app.get("/api/classes", getClasses);
|
||||||
|
@ -7,11 +7,12 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
AlertCircleIcon,
|
AlertCircleIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
UploadCloudIcon,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { DownloadIcon } from "lucide-vue-next";
|
import { DownloadIcon } from "lucide-vue-next";
|
||||||
|
|
||||||
defineProps(["timetable", "editable", "selected"]);
|
defineProps(["timetable", "editable", "remote", "selected"]);
|
||||||
defineEmits(["click", "edit", "delete", "copy", "export"]);
|
defineEmits(["click", "edit", "delete", "copy", "export", "upload"]);
|
||||||
|
|
||||||
const deleteConfirm = ref(false);
|
const deleteConfirm = ref(false);
|
||||||
|
|
||||||
@ -38,7 +39,8 @@ watch(deleteConfirm, (value) => {
|
|||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<DownloadIcon @click="$emit('export')" />
|
<DownloadIcon @click="$emit('export')" />
|
||||||
<Edit2Icon v-if="editable" @click="$emit('edit')" />
|
<Edit2Icon v-if="editable && !remote" @click="$emit('edit')" />
|
||||||
|
<UploadCloudIcon v-if="editable && remote" @click="$emit('upload')" />
|
||||||
<CopyIcon @click="$emit('copy')" />
|
<CopyIcon @click="$emit('copy')" />
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
v-if="editable && !deleteConfirm"
|
v-if="editable && !deleteConfirm"
|
||||||
|
14
src/permission.js
Normal file
14
src/permission.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { sessionInfo } from "@/store";
|
||||||
|
|
||||||
|
export function hasPermission(permission, forValue) {
|
||||||
|
let hasPermission = false;
|
||||||
|
for (const perm of sessionInfo.value.permissions) {
|
||||||
|
if (perm == permission) hasPermission = true;
|
||||||
|
else if (perm == permission + ":" + forValue) hasPermission = true;
|
||||||
|
}
|
||||||
|
return hasPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEditTimetable(id) {
|
||||||
|
return hasPermission("timetable.update", id);
|
||||||
|
}
|
@ -1,9 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TimetableCard from "@/components/settings/timetable-card.vue";
|
import TimetableCard from "@/components/settings/timetable-card.vue";
|
||||||
import { timetables, localTimetables, timetableId } from "@/store";
|
import {
|
||||||
|
timetable,
|
||||||
|
timetables,
|
||||||
|
localTimetables,
|
||||||
|
timetableId,
|
||||||
|
baseUrl,
|
||||||
|
fetchTimetables,
|
||||||
|
} from "@/store";
|
||||||
|
import { canEditTimetable } from "@/permission";
|
||||||
import { PlusIcon, PaperclipIcon } from "lucide-vue-next";
|
import { PlusIcon, PaperclipIcon } from "lucide-vue-next";
|
||||||
import { toRaw, ref } from "vue";
|
import { toRaw, ref } from "vue";
|
||||||
import download from "downloadjs";
|
import download from "downloadjs";
|
||||||
|
import { loading, loadingProgress } from "@/store";
|
||||||
|
|
||||||
function copyTimetable(timetable) {
|
function copyTimetable(timetable) {
|
||||||
const newTimetable = structuredClone(toRaw(timetable));
|
const newTimetable = structuredClone(toRaw(timetable));
|
||||||
@ -47,6 +56,34 @@ function exportTimetable(timetable) {
|
|||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadTimetable(id) {
|
||||||
|
loadingProgress.value = 0.1;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const timetableData = timetable.value;
|
||||||
|
const response = await fetch(
|
||||||
|
baseUrl + "/timetable?id=" + encodeURIComponent(id),
|
||||||
|
{
|
||||||
|
method: "put",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(timetableData),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status != 201) {
|
||||||
|
alert("Failed uploading timetable!");
|
||||||
|
} else {
|
||||||
|
loadingProgress.value = 0.5;
|
||||||
|
await fetchTimetables();
|
||||||
|
timetableId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingProgress.value = 1;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -59,6 +96,7 @@ function exportTimetable(timetable) {
|
|||||||
:timetable="timetable"
|
:timetable="timetable"
|
||||||
:selected="timetableId == timetable.id"
|
:selected="timetableId == timetable.id"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
|
:remote="false"
|
||||||
@click="timetableId = timetable.id"
|
@click="timetableId = timetable.id"
|
||||||
@edit="$router.push('timetable/edit/' + timetable.id)"
|
@edit="$router.push('timetable/edit/' + timetable.id)"
|
||||||
@copy="copyTimetable(timetable)"
|
@copy="copyTimetable(timetable)"
|
||||||
@ -92,10 +130,12 @@ function exportTimetable(timetable) {
|
|||||||
:key="timetable.id"
|
:key="timetable.id"
|
||||||
:timetable="timetable"
|
:timetable="timetable"
|
||||||
:selected="timetableId == timetable.id"
|
:selected="timetableId == timetable.id"
|
||||||
:editable="false"
|
:editable="canEditTimetable(timetable.id)"
|
||||||
|
:remote="true"
|
||||||
@click="timetableId = timetable.id"
|
@click="timetableId = timetable.id"
|
||||||
@copy="copyTimetable(timetable)"
|
@copy="copyTimetable(timetable)"
|
||||||
@export="exportTimetable(timetable)"
|
@export="exportTimetable(timetable)"
|
||||||
|
@upload="uploadTimetable(timetable.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user