🛂 Add key-based permission system

This commit is contained in:
2023-06-20 19:53:34 +02:00
parent 917783d114
commit 5d9317ac01
11 changed files with 432 additions and 17 deletions

View File

@ -0,0 +1,72 @@
<script setup>
import { TrashIcon, AlertCircleIcon } from "lucide-vue-next";
import { ref, watch } from "vue";
const deleteConfirm = ref(false);
watch(deleteConfirm, (value) => {
if (value) {
setTimeout(() => (deleteConfirm.value = false), 2000);
}
});
defineProps(["keyData"]);
defineEmits(["delete"]);
</script>
<template>
<div class="card">
<div class="info">
<p class="key">{{ keyData.key }}</p>
<div class="permissions">
<div
class="permission"
v-for="permission in keyData.permissions"
:key="permission"
>
{{ permission }}
</div>
</div>
</div>
<div class="button">
<TrashIcon v-if="!deleteConfirm" @click="deleteConfirm = true" />
<AlertCircleIcon color="red" v-else @click="$emit('delete')" />
</div>
</div>
</template>
<style scoped>
.card {
background-color: var(--element-color);
border-radius: 10px;
display: grid;
grid-template-columns: minmax(90%, auto) 50px;
align-items: center;
}
.key {
font-family: monospace;
font-size: 15px;
padding: 15px 10px 0px 10px;
}
.permissions {
display: flex;
flex-direction: row;
gap: 10px;
padding: 5px 10px 0px 10px;
user-select: none;
max-width: 90%;
overflow-x: scroll;
}
.permission {
padding: 10px;
background-color: var(--element-color-hover);
border-radius: 5px;
}
.button {
margin-right: 20px;
cursor: pointer;
}
</style>

View File

@ -12,6 +12,7 @@ import TimetableSettings from "@/views/settings/TimetableSettings.vue";
import TimetableEditor from "@/views/settings/TimetableEditor.vue";
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
import KeySettings from "@/views/settings/KeySettings.vue";
import AboutPage from "@/views/settings/AboutPage.vue";
const router = createRouter({
@ -70,6 +71,11 @@ const router = createRouter({
name: "title.settings.appearance",
component: AppearanceSettings,
},
{
path: "keys",
name: "title.settings.keys",
component: KeySettings,
},
{
path: "about",
name: "title.settings.about",

View File

@ -72,6 +72,7 @@ export const timetable = computed(() => {
localTimetable || remoteTimetable || { trusted: true, source: "", data: [] }
);
});
export const sessionInfo = ref({});
export const timetables = ref([]);
export const times = ref([]);
export const substitutions = ref({});
@ -81,7 +82,7 @@ export const classList = ref([]);
/* API functions */
// Set the `VITE_API_ENDPOINT` env variable when
// building the frontend to use an external api server
const baseUrl = import.meta.env.VITE_API_ENDPOINT || "/api";
export const baseUrl = import.meta.env.VITE_API_ENDPOINT || "/api";
export async function fetchData(days, partial) {
if (!timetable.value.data || Object.keys(classList.value).length == 0) {
@ -96,23 +97,12 @@ export async function fetchData(days, partial) {
// Check if the API server is reachable
// and the user is authenticated
try {
const checkResponse = await fetch(`${baseUrl}/check`);
if (checkResponse.status == 401) {
shouldLogin.value = true;
return;
} else if (checkResponse.status != 200) {
loadingFailed.value = true;
loadingProgress.value = 1;
console.log("Other error while fetching data: " + checkResponse.status);
return;
}
} catch {
if (!(await fetchSessionInfo())) {
loadingFailed.value = true;
loadingProgress.value = 1;
console.log("Error while fetching data: No internet connection!");
return;
}
loadingProgress.value = step++ / steps;
if (!partial) {
@ -137,6 +127,25 @@ watch(selectedDate, () =>
fetchData(getNextAndPrevDay(selectedDate.value), true)
);
export async function fetchSessionInfo() {
try {
const checkResponse = await fetch(`${baseUrl}/info`);
if (checkResponse.status == 401) {
shouldLogin.value = true;
return false;
} else if (checkResponse.status != 200) {
console.log("Other error while fetching data: " + checkResponse.status);
return false;
} else {
sessionInfo.value = await checkResponse.json();
}
} catch {
console.log("Error while fetching data: No internet connection!");
return false;
}
return true;
}
export async function fetchClassList() {
const classListResponse = await fetch(`${baseUrl}/classes`);
const classListData = await classListResponse.json();

View File

@ -13,6 +13,7 @@ export const strings = {
timetable: "Manage Timetables",
groups: "Timetable Groups",
appearance: "Appearance",
keys: "Manage Keys",
about: "About",
},
},
@ -25,6 +26,7 @@ export const strings = {
language: "Language",
about: "About",
theme: "Theme",
keys: "Manage Keys",
},
text: {
filtering:
@ -38,12 +40,15 @@ export const strings = {
"This Tool queries and parses the latest substitution-plan data every minute. The correctness of the data can in no way be guaranteed, so please check the data against the official plan if something seems wrong! Due to the format of the plan files, it it sometimes not easily possible to extract the correct data, so the plan displayed here may not be correct.",
theme:
"Select a Theme to change the colors of the app. The 'Auto' option selects a theme based on your system preferences.",
keys: "Keys are used to give you special permissions, for example editing a timetable. You can enter keys that you received here.",
},
other: "Other",
back: "Back",
none: "None",
version: "Version",
source: "Source",
key: "Key",
invalidKey: "This key does not exist!",
theme: {
auto: "Auto",
dark: "Dark",
@ -130,6 +135,7 @@ export const strings = {
timetable: "Stundenpläne Verwalten",
groups: "Stundenplan-Gruppen",
appearance: "Aussehen",
keys: "Schlüssel Verwalten",
about: "Über",
},
},
@ -142,6 +148,7 @@ export const strings = {
language: "Sprache",
about: "Über diese Anwendung",
theme: "Farbschema",
keys: "Schlüssel Verwalten",
},
text: {
filtering:
@ -155,12 +162,15 @@ export const strings = {
"Diese Anwendung fragt jede Minute die neusten Vertretungsplan-Daten an und verarbeitet sie, um sie dir schöner und besser lesbar anzuzeigen. Die Richtigkeit dieser Daten kann nicht garantiert werden, da es manchmal kompliziert ist, die richtigen Daten zu extrahieren. Wenn etwas nicht richtig aussieht, überprüfe es bitte auf dem offiziellen Plan!",
theme:
"Wähle ein Farbschema aus, um die Farben dieser Anwendung anzupassen. Die Option 'Automatisch' wählt ein Farbschema basierend auf den Einstellungen deines Systems aus.",
keys: "Schlüssel können dir erweiterte rechte geben, zum Beispiel zum editieren eines Stundenplans. Wenn du einen Schlüssel bekommen hast, kannst du ihn hier eingeben.",
},
other: "Andere",
back: "Zurück",
none: "Keine",
version: "Version",
source: "Quelle",
key: "Schlüssel",
invalidKey: "Dieser Schlüssel existiert nicht!",
theme: {
auto: "Automatisch",
dark: "Dunkel",

View File

@ -6,6 +6,7 @@ import {
CalendarIcon,
CopyCheckIcon,
PaletteIcon,
KeyRoundIcon,
InfoIcon,
ChevronLeft,
} from "lucide-vue-next";
@ -35,6 +36,11 @@ import {
:icon="PaletteIcon"
route="settings/appearance"
/>
<PageCard
:name="$t('title.settings.keys')"
:icon="KeyRoundIcon"
route="settings/keys"
/>
<PageCard
:name="$t('title.settings.about')"
:icon="InfoIcon"

View File

@ -0,0 +1,142 @@
<script setup>
import {
baseUrl,
sessionInfo,
fetchSessionInfo,
loading,
loadingProgress,
} from "@/store";
import KeyCard from "@/components/settings/key-card.vue";
import { ref } from "vue";
import { AlertTriangleIcon, SendIcon } from "lucide-vue-next";
const key = ref();
const invalidKey = ref(false);
async function applyKey() {
loadingProgress.value = 0.1;
loading.value = true;
const result = await fetch(
baseUrl + "/key?key=" + encodeURIComponent(key.value),
{
method: "put",
}
);
loadingProgress.value = 0.5;
if (result.status == 400) {
invalidKey.value = true;
} else if (result.status != 200) {
alert("Error while applying key!");
console.warn(result.body);
} else {
await fetchSessionInfo();
}
loadingProgress.value = 1;
loading.value = false;
}
async function revokeKey(key) {
loadingProgress.value = 0.1;
loading.value = true;
const result = await fetch(baseUrl + "/key?key=" + encodeURIComponent(key), {
method: "delete",
});
loadingProgress.value = 0.5;
if (result.status != 200) {
alert("Error while revoking key!");
console.warn(result.body);
} else {
await fetchSessionInfo();
}
loadingProgress.value = 1;
loading.value = false;
}
</script>
<template>
<h2>{{ $t("settings.heading.keys") }}</h2>
<p>{{ $t("settings.text.keys") }}</p>
<div class="input">
<input
type="text"
:placeholder="$t('settings.key')"
v-model="key"
@keyup.enter="applyKey"
@keydown="invalidKey = false"
/>
<div class="button" @click="applyKey">
<SendIcon />
</div>
</div>
<div class="invalidKey" v-if="invalidKey">
<AlertTriangleIcon />
<span>{{ $t("settings.invalidKey") }}</span>
</div>
<div class="list">
<KeyCard
v-for="key in sessionInfo.appliedKeys"
:key="key"
:keyData="key"
@delete="() => revokeKey(key.key)"
/>
</div>
</template>
<style scoped>
h2 {
margin: 0px;
}
p {
margin: 5px 0px;
}
.invalidKey {
color: red;
display: flex;
align-items: center;
gap: 5px;
padding: 0px 0px 15px 5px;
}
.input {
padding: 10px 0px;
display: grid;
grid-template-columns: 1fr 50px;
grid-auto-rows: 1fr;
align-items: center;
gap: 10px;
}
.input > * {
background-color: var(--element-color);
border-radius: 7px;
padding: 10px;
transition: 0.2s background-color;
}
.input > *:hover {
background-color: var(--element-color-hover);
}
.input input {
border: none;
color: var(--text-color);
font-size: 15px;
font-family: monospace;
height: 24px;
}
.button {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>