Skip to content

Commit e57684c

Browse files
authored
API key management (#247)
1 parent 85f77f6 commit e57684c

File tree

3 files changed

+242
-33
lines changed

3 files changed

+242
-33
lines changed

src/components/ApiKeyManagement.vue

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<template>
2+
<v-card>
3+
<v-card-title>API Keys</v-card-title>
4+
<v-card-text>
5+
<v-data-table :items="Object.values(apiKeys)" :headers="apiKeyHeaders">
6+
<template v-slot:item.created="{ item }">
7+
{{ moment(item.created).format("YYYY-MM-DD HH:mm:ss ZZ") }}
8+
</template>
9+
<template v-slot:item.scopes="{ item }">
10+
<v-chip v-for="scope in item.scopes" density="compact" color="green">{{ scope }}</v-chip>
11+
</template>
12+
<template v-slot:item.last_used="{ item }">
13+
<span v-if="item.last_used">
14+
{{ moment(item.last_used).format("YYYY-MM-DD HH:mm:ss ZZ") }}
15+
</span>
16+
<span v-else>Never</span>
17+
</template>
18+
19+
<template v-slot:item.actions="{ item }">
20+
<v-dialog max-width="420px">
21+
<template v-slot:activator="{ props }">
22+
<v-btn v-bind="props" class="ms-2" size="small" variant="outlined" color="error"
23+
><v-icon>mdi-delete</v-icon></v-btn
24+
>
25+
</template>
26+
<template v-slot:default="{ isActive }">
27+
<v-card>
28+
<v-card-title class="text-h6">Delete API key !'{{ item.name }}'?</v-card-title>
29+
<v-card-text>This action is irreversible</v-card-text>
30+
<v-card-actions>
31+
<v-spacer></v-spacer>
32+
<v-btn color="light" variant="text" @click="isActive.value = false">Cancel</v-btn>
33+
<v-btn color="error" variant="flat" @click="deleteApiKey(item)">Delete</v-btn>
34+
<v-spacer></v-spacer>
35+
</v-card-actions>
36+
</v-card>
37+
</template>
38+
</v-dialog>
39+
</template>
40+
41+
<template v-slot:item.enabled="{ item }">
42+
<v-switch
43+
density="compact"
44+
v-model="item.enabled"
45+
color="green"
46+
hide-details
47+
@click="toggleApiKey(item)"
48+
></v-switch>
49+
</template>
50+
</v-data-table>
51+
</v-card-text>
52+
<v-card-actions>
53+
<v-dialog max-width="420px">
54+
<template v-slot:activator="{ props }">
55+
<v-btn v-bind="props" class="ms-2" size="small" variant="outlined" prepend-icon="mdi-plus">New API key</v-btn>
56+
</template>
57+
<template v-slot:default="{ isActive }">
58+
<v-card>
59+
<v-card-title class="text-h6">New API key</v-card-title>
60+
<v-card-text v-if="!newApiKeyToken">
61+
<v-text-field density="compact" label="Key name" v-model="newApiKey.name"></v-text-field>
62+
<v-combobox
63+
v-model="newApiKey.scopes"
64+
label="Scopes"
65+
:items="['all']"
66+
clearable
67+
multiple
68+
density="compact"
69+
:delimiters="[',', ' ', ';']"
70+
/>
71+
</v-card-text>
72+
<v-card-text v-else>
73+
This is the only time your API key will be accessable. Please copy it to your clipboard now and store it
74+
in a secure location. You'll need it to access the API and won't be able to retrieve it again later.
75+
<br />
76+
<div class="d-flex justify-center">
77+
<v-btn class="mt-6" @click="copyApiKey(newApiKeyToken, isActive)" prepend-icon="mdi-content-copy"
78+
>Copy API key to clipboard</v-btn
79+
>
80+
</div>
81+
</v-card-text>
82+
<v-card-actions v-if="!newApiKeyToken">
83+
<v-spacer></v-spacer>
84+
<v-btn color="light" variant="text" @click="isActive.value = false">Cancel</v-btn>
85+
<v-btn variant="flat" @click="addApiKey()">Create</v-btn>
86+
<v-spacer></v-spacer>
87+
</v-card-actions>
88+
</v-card>
89+
</template>
90+
</v-dialog>
91+
</v-card-actions>
92+
</v-card>
93+
</template>
94+
95+
<script lang="ts" setup>
96+
import moment from "moment";
97+
import axios from "axios";
98+
</script>
99+
100+
<script lang="ts">
101+
export default {
102+
name: "ApiKeyManagement",
103+
data() {
104+
return {
105+
apiKeyHeaders: [
106+
{ title: "Created", value: "created" },
107+
{ title: "Name", value: "name" },
108+
{ title: "Scopes", value: "scopes" },
109+
{ title: "Last used", value: "last_used" },
110+
{ title: "Enabled", value: "enabled" },
111+
{ title: "Actions", value: "actions" }
112+
],
113+
newApiKey: {
114+
name: "",
115+
scopes: []
116+
},
117+
newApiKeyToken: null
118+
};
119+
},
120+
props: {
121+
profileId: {
122+
type: String,
123+
default: null
124+
},
125+
apiKeys: {
126+
type: Object,
127+
default: {}
128+
}
129+
},
130+
methods: {
131+
addApiKey() {
132+
axios
133+
.post(`/api/v2/users/new-api-key`, {
134+
user_id: this.profileId,
135+
name: this.newApiKey.name,
136+
scopes: this.newApiKey.scopes
137+
})
138+
.then(response => {
139+
this.newApiKeyToken = response.data.token;
140+
this.$emit("apiKeyUpdate", response.data.api_keys);
141+
})
142+
.catch(error => {
143+
console.log(error);
144+
})
145+
.finally(() => {});
146+
},
147+
toggleApiKey(item) {
148+
axios
149+
.post(`/api/v2/users/toggle-api-key`, { user_id: this.profileId, name: item.name })
150+
.then(response => {
151+
item.enabled = response.data.enabled;
152+
})
153+
.catch(error => {
154+
console.log(error);
155+
})
156+
.finally(() => {});
157+
},
158+
deleteApiKey(item) {
159+
axios
160+
.post(`/api/v2/users/delete-api-key`, { user_id: this.profileId, name: item.name })
161+
.then(response => {
162+
this.$emit("apiKeyUpdate", response.data.api_keys);
163+
this.$eventBus.emit("displayMessage", {
164+
message: `API key '${item.name}' succesfully deleted.`,
165+
status: "success"
166+
});
167+
})
168+
.catch(error => {
169+
console.log(error);
170+
})
171+
.finally(() => {});
172+
},
173+
copyApiKey(text, isActive) {
174+
navigator.clipboard.writeText(text);
175+
this.$eventBus.emit("displayMessage", {
176+
status: "info",
177+
message: "API key copied to clipboard!"
178+
});
179+
this.newApiKeyToken = null;
180+
isActive.value = false;
181+
}
182+
}
183+
};
184+
</script>
185+
186+
<style scoped>
187+
/* Add custom styles here */
188+
</style>

src/views/UserAdmin.vue

+44-8
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,57 @@
66
<router-link :to="{ name: 'UserProfileAdmin', params: { id: item.id } }">{{ item.username }}</router-link>
77
</template>
88
<template v-slot:item.admin="{ item }">
9-
<v-switch density="compact" color="green" v-model="item.admin" @change="toggleUser(item, 'admin')"></v-switch>
9+
<v-switch
10+
density="compact"
11+
color="green"
12+
v-model="item.admin"
13+
hide-details
14+
@change="toggleUser(item, 'admin')"
15+
></v-switch>
1016
</template>
1117
<template v-slot:item.enabled="{ item }">
1218
<v-switch
1319
density="compact"
1420
color="green"
1521
v-model="item.enabled"
22+
hide-details
1623
@change="toggleUser(item, 'enabled')"
1724
></v-switch>
1825
</template>
19-
<template v-slot:item.api_key="{ item }">
20-
<code>{{ item.api_key }}</code>
26+
<template v-slot:item.api_keys="{ item }">
27+
<code
28+
><v-chip v-for="apiKey in item.api_keys" class="me-2" density="compact" color="green">{{
29+
apiKey.name
30+
}}</v-chip
31+
>{{
32+
}}</code>
2133
</template>
2234

2335
<template v-slot:item.actions="{ item }">
24-
<v-btn size="small" variant="outlined" @click="resetApiKey(item)">
25-
<v-icon class="me-2">mdi-key</v-icon>
26-
Reset API key
27-
</v-btn>
36+
<v-dialog>
37+
<template v-slot:activator="{ props }">
38+
<v-btn v-bind="props" size="small" variant="outlined" prepend-icon="mdi-key"> Manage API keys </v-btn>
39+
</template>
40+
<template v-slot:default="{ isActive }">
41+
<v-card>
42+
<v-card-title class="text-h6">API key management for {{ item.username }}</v-card-title>
43+
<v-card-text>
44+
<api-key-management
45+
:profile-id="item.id"
46+
:apiKeys="item.api_keys"
47+
@api-key-update="data => (item.api_keys = data)"
48+
>
49+
</api-key-management>
50+
</v-card-text>
51+
<v-card-actions>
52+
<v-spacer></v-spacer>
53+
<v-btn color="light" variant="text" @click="isActive.value = false">Cancel</v-btn>
54+
<v-spacer></v-spacer>
55+
</v-card-actions>
56+
</v-card>
57+
</template>
58+
</v-dialog>
59+
2860
<v-btn class="ms-2" size="small" variant="outlined" color="error" @click="showDeleteDialog(item)">
2961
<v-icon>mdi-delete</v-icon>
3062
</v-btn>
@@ -77,17 +109,21 @@
77109

78110
<script lang="ts" setup>
79111
import axios from "axios";
112+
import ApiKeyManagement from "@/components/ApiKeyManagement.vue";
80113
</script>
81114

82115
<script lang="ts">
83116
export default {
84117
name: "UserAdmin",
118+
components: {
119+
ApiKeyManagement
120+
},
85121
data() {
86122
return {
87123
users: [],
88124
headers: [
89125
{ key: "username", sortable: true, title: "Username" },
90-
{ key: "api_key", sortable: false, title: "API key" },
126+
{ key: "api_keys", sortable: false, title: "API keys" },
91127
{ key: "admin", sortable: false, title: "Admin" },
92128
{ key: "enabled", sortable: false, title: "Enabled" },
93129
{ key: "actions", sortable: false, title: "Actions" }

src/views/UserProfile.vue

+10-25
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,6 @@
1313
<td>{{ profile.username }}</td>
1414
<td></td>
1515
</tr>
16-
<tr>
17-
<td>API key</td>
18-
<td>
19-
<code>{{ profile.api_key }}</code>
20-
</td>
21-
<td>
22-
<v-btn size="small" variant="outlined" @click="resetApiKey(profile)"> Reset key </v-btn>
23-
</td>
24-
</tr>
2516
<tr>
2617
<th>Global role</th>
2718
<td>
@@ -45,6 +36,13 @@
4536
</v-table>
4637
</v-card-text>
4738
</v-card>
39+
<api-key-management
40+
v-if="profile"
41+
:profile-id="profile.id"
42+
:apiKeys="profile.api_keys"
43+
@api-key-update="data => (profile.api_keys = data)"
44+
>
45+
</api-key-management>
4846
</v-col>
4947
<v-col cols="4">
5048
<v-card v-if="authModule === 'local'" variant="flat">
@@ -78,13 +76,15 @@ import axios from "axios";
7876
import { useUserStore } from "@/store/user";
7977
import { useAppStore } from "@/store/app";
8078
import GroupList from "@/components/GroupList.vue";
79+
import ApiKeyManagement from "@/components/ApiKeyManagement.vue";
8180
</script>
8281

8382
<script lang="ts">
8483
export default {
8584
name: "UserProfile",
8685
components: {
87-
GroupList
86+
GroupList,
87+
ApiKeyManagement
8888
},
8989
data() {
9090
return {
@@ -123,21 +123,6 @@ export default {
123123
})
124124
.finally(() => {});
125125
},
126-
resetApiKey() {
127-
axios
128-
.post(`/api/v2/users/reset-api-key`, { user_id: this.profile.id })
129-
.then(response => {
130-
this.profile.api_key = response.data.api_key;
131-
this.$eventBus.emit("displayMessage", {
132-
message: "API key succesfully reset.",
133-
status: "success"
134-
});
135-
})
136-
.catch(error => {
137-
console.log(error);
138-
})
139-
.finally(() => {});
140-
},
141126
changeUserPassword() {
142127
var params = {
143128
user_id: this.user.id,

0 commit comments

Comments
 (0)