mirror of
https://github.com/HangarMC/Hangar.git
synced 2024-12-21 06:51:19 +08:00
implement api key page
This commit is contained in:
parent
0887a25ae2
commit
7bed534ecc
@ -19,6 +19,7 @@ Stuff that needs to be done before I consider this a successful POC
|
||||
- [x] date formatting needs to go thru i18n
|
||||
- [x] investigate why eslint/prettier don't auto fix
|
||||
- [x] actually implement page transitions (as opposed to popping up below the page)
|
||||
- [ ] validation of forms/inputs etc
|
||||
|
||||
## Big list of pages!
|
||||
|
||||
@ -114,10 +115,10 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
|
||||
- [ ] qa
|
||||
* settings (empty)
|
||||
- api-keys
|
||||
- [ ] fetch
|
||||
- [ ] layout
|
||||
- [ ] functionality
|
||||
- [ ] design
|
||||
- [x] fetch
|
||||
- [x] layout
|
||||
- [ ] functionality (loading animations, validation)
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
* [project]
|
||||
- [ ] fetch
|
||||
|
5
frontend-new/src/components/design/Alert.vue
Normal file
5
frontend-new/src/components/design/Alert.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="rounded bg-red-500 p-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
@ -15,7 +15,7 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="group relative cursor-pointer pl-30px customCheckboxContainer">
|
||||
<label class="group relative cursor-pointer pl-30px customCheckboxContainer w-max">
|
||||
<template v-if="props.label">{{ props.label }}</template>
|
||||
<input v-model="value" type="checkbox" class="hidden" v-bind="$attrs" />
|
||||
<span
|
||||
|
@ -15,5 +15,5 @@ const props = defineProps<{
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy -->
|
||||
<input v-model="date" type="date" />
|
||||
<input v-model="date" type="date" v-bind="$attrs" />
|
||||
</template>
|
||||
|
23
frontend-new/src/components/ui/InputText.vue
Normal file
23
frontend-new/src/components/ui/InputText.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}>();
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit("update:modelValue", v),
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
label?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy -->
|
||||
<label>
|
||||
<template v-if="label">{{ label }}</template>
|
||||
<input v-model="value" type="text" class="ml-2" v-bind="$attrs" />
|
||||
</label>
|
||||
</template>
|
@ -1,5 +1,137 @@
|
||||
<script lang="ts" setup>
|
||||
import PageTitle from "~/components/design/PageTitle.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import InputText from "~/components/ui/InputText.vue";
|
||||
import Button from "~/components/design/Button.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { ApiKey, HangarApiException, IPermission } from "hangar-api";
|
||||
import { useRoute } from "vue-router";
|
||||
import { AxiosError } from "axios";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import InputCheckbox from "~/components/ui/InputCheckbox.vue";
|
||||
import Table from "~/components/design/Table.vue";
|
||||
import Alert from "~/components/design/Alert.vue";
|
||||
import Card from "~/components/design/Card.vue";
|
||||
|
||||
const ctx = useContext();
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const apiKeys = ref(await useInternalApi<ApiKey[]>("api-keys/existing-keys/" + route.params.user));
|
||||
const possiblePerms = await useInternalApi<IPermission[]>("api-keys/possible-perms/" + route.params.user);
|
||||
|
||||
// TODO validation, loading animations, validation loading
|
||||
const name = ref("");
|
||||
const loading = ref(false);
|
||||
const selectedPerms = ref([]);
|
||||
const validateLoading = ref(false);
|
||||
const nameErrorMessages = ref([]);
|
||||
|
||||
watch(name, async (val: string) => {
|
||||
if (!val) return;
|
||||
validateLoading.value = true;
|
||||
nameErrorMessages.value = [];
|
||||
await useInternalApi(`api-keys/check-key/${route.params.user}`, true, "get", {
|
||||
name: name.value,
|
||||
}).catch((err: AxiosError<HangarApiException>) => {
|
||||
if (!err.response?.data.isHangarApiException) {
|
||||
return handleRequestError(err, ctx, i18n);
|
||||
}
|
||||
nameErrorMessages.value.push(i18n.t(err.response.data.message || ""));
|
||||
});
|
||||
validateLoading.value = false;
|
||||
});
|
||||
|
||||
async function create() {
|
||||
loading.value = true;
|
||||
const key = await useInternalApi<string>(`api-keys/create-key/${route.params.user}`, true, "post", {
|
||||
name: name.value,
|
||||
permissions: selectedPerms.value,
|
||||
}).catch((err) => handleRequestError(err, ctx, i18n));
|
||||
if (key) {
|
||||
apiKeys.value.unshift({
|
||||
token: key,
|
||||
name: name.value,
|
||||
permissions: selectedPerms.value,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
// TODO success notification
|
||||
// this.$util.success(i18n.$t('apiKeys.success.create', [name.value]));
|
||||
}
|
||||
loading.value = true;
|
||||
}
|
||||
|
||||
async function deleteKey(key: ApiKey) {
|
||||
loading.value = false;
|
||||
await useInternalApi(`api-keys/delete-key/${route.params.user}`, true, "post", {
|
||||
content: key.name,
|
||||
}).catch((err) => handleRequestError(err, ctx, i18n));
|
||||
apiKeys.value = apiKeys.value.filter((k) => k.name !== key.name);
|
||||
// TODO success notification
|
||||
// this.$util.success(i18n.t('apiKeys.success.delete', [key.name]));
|
||||
loading.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>api keys settings</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<template #header>
|
||||
<PageTitle>{{ i18n.t("apiKeys.createNew") }}</PageTitle>
|
||||
</template>
|
||||
<InputText v-model="name" :label="i18n.t('apiKeys.name')"></InputText>
|
||||
<Button class="ml-2" @click="create">{{ i18n.t("apiKeys.createKey") }}</Button>
|
||||
<div class="grid autofix mt-4">
|
||||
<InputCheckbox v-for="perm in possiblePerms" :key="perm" v-model="selectedPerms" :label="perm" :value="perm" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<template #header>
|
||||
<PageTitle>{{ i18n.t("apiKeys.existing") }}</PageTitle>
|
||||
</template>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ i18n.t("apiKeys.name") }}
|
||||
</th>
|
||||
<th>
|
||||
{{ i18n.t("apiKeys.key") }}
|
||||
</th>
|
||||
<th>
|
||||
{{ i18n.t("apiKeys.keyIdentifier") }}
|
||||
</th>
|
||||
<th>
|
||||
{{ i18n.t("apiKeys.permissions") }}
|
||||
</th>
|
||||
<th>
|
||||
{{ i18n.t("apiKeys.delete") }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="key in apiKeys" :key="key.name">
|
||||
<td>{{ key.name }}</td>
|
||||
<td>{{ key.token }}</td>
|
||||
<td>{{ key.tokenIdentifier }}</td>
|
||||
<td>{{ key.permissions.join(", ") }}</td>
|
||||
<td>
|
||||
<Button @click="deleteKey(key)">{{ i18n.t("apiKeys.deleteKey") }}</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="apiKeys.length === 0">
|
||||
<td colspan="5">
|
||||
<Alert class="mt-4">
|
||||
{{ i18n.t("apiKeys.noKeys") }}
|
||||
</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
@ -7,3 +139,9 @@ meta:
|
||||
requireCurrentUser: true
|
||||
requireGlobalPerm: ["EDIT_API_KEYS"]
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.autofix {
|
||||
grid-template-columns: repeat(auto-fit, 250px);
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user