Skip to content
This repository was archived by the owner on Apr 19, 2023. It is now read-only.

Commit 8937cf0

Browse files
✨ Access token pages
1 parent c995d6a commit 8937cf0

File tree

5 files changed

+660
-30
lines changed

5 files changed

+660
-30
lines changed

components/Layout.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<Settings v-if="activeRoute === 'user-settings'">
44
<nuxt />
55
</Settings>
6-
<Settings v-else-if="activeRoute === 'users'">
6+
<Users v-else-if="activeRoute === 'users'">
77
<nuxt />
8-
</Settings>
8+
</Users>
99
<Manage v-else-if="activeRoute === 'organization-settings'">
1010
<nuxt />
1111
</Manage>
@@ -18,12 +18,14 @@
1818

1919
<script lang="ts">
2020
import { Component, Vue, Watch } from "vue-property-decorator";
21+
import Users from "@/components/Users.vue";
2122
import Settings from "@/components/Settings.vue";
2223
import Manage from "@/components/Manage.vue";
2324
import Policies from "@/components/Policies.vue";
2425
2526
@Component({
2627
components: {
28+
Users,
2729
Settings,
2830
Policies,
2931
Manage

components/Users.vue

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<template>
2+
<div class="container">
3+
<aside>
4+
<nav>
5+
<nuxt-link class="item" :to="`/users/${$route.params.slug}/profile`">
6+
<font-awesome-icon class="nav-icon" icon="user" fixed-width />
7+
<span>Account</span>
8+
</nuxt-link>
9+
<nuxt-link
10+
class="item"
11+
:to="`/users/${$route.params.slug}/access-tokens`"
12+
>
13+
<font-awesome-icon class="nav-icon" icon="code" fixed-width />
14+
<span>Access tokens</span>
15+
</nuxt-link>
16+
</nav>
17+
</aside>
18+
<div class="card">
19+
<slot />
20+
</div>
21+
</div>
22+
</template>
23+
24+
<script lang="ts">
25+
import { Component, Vue } from "vue-property-decorator";
26+
import { mapGetters } from "vuex";
27+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
28+
import { library } from "@fortawesome/fontawesome-svg-core";
29+
import {
30+
faEnvelope,
31+
faKey,
32+
faDatabase,
33+
faBuilding,
34+
faUser,
35+
faCode
36+
} from "@fortawesome/free-solid-svg-icons";
37+
library.add(faEnvelope, faKey, faDatabase, faBuilding, faUser, faCode);
38+
39+
@Component({
40+
components: {
41+
FontAwesomeIcon
42+
}
43+
})
44+
export default class Settings extends Vue {}
45+
</script>
46+
47+
<style lang="scss" scoped>
48+
.container {
49+
display: flex;
50+
}
51+
aside {
52+
width: 300px;
53+
}
54+
aside nav {
55+
position: sticky;
56+
top: 1rem;
57+
}
58+
.card {
59+
flex-grow: 1;
60+
padding: 2rem;
61+
}
62+
.nav-heading {
63+
font-weight: bold;
64+
margin: 1rem 0;
65+
font-size: 110%;
66+
}
67+
.nav-icon {
68+
margin-right: 0.5rem;
69+
opacity: 0.3;
70+
transition: 0.3s;
71+
}
72+
.item {
73+
transition: 0.3s;
74+
display: block;
75+
text-decoration: none;
76+
padding: 0.7rem 0;
77+
color: inherit;
78+
&:hover {
79+
.nav-icon {
80+
opacity: 0.75;
81+
}
82+
}
83+
&.nuxt-link-active:not(.item--type-parent) {
84+
font-weight: bold;
85+
}
86+
}
87+
.sub-nav {
88+
margin-top: -0.5rem;
89+
border-left: 0.1rem solid rgba(0, 0, 0, 0.1);
90+
padding-left: 1.4rem;
91+
margin-left: 0.5rem;
92+
}
93+
.sub-item {
94+
display: block;
95+
color: inherit;
96+
margin: 0.5rem 0;
97+
&.nuxt-link-active {
98+
font-weight: bold;
99+
}
100+
}
101+
</style>
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
<template>
2+
<main>
3+
<Loading v-if="loading" :message="loading" />
4+
<div v-else>
5+
<div class="row">
6+
<div>
7+
<nuxt-link
8+
:to="`/users/${$route.params.slug}/access-tokens`"
9+
aria-label="Back"
10+
data-balloon-pos="down"
11+
class="button button--type-icon button--type-back"
12+
>
13+
<font-awesome-icon class="icon" icon="arrow-left" fixed-width />
14+
</nuxt-link>
15+
<h1>Access tokens</h1>
16+
</div>
17+
<div class="text text--align-right">
18+
<button
19+
aria-label="Refresh"
20+
data-balloon-pos="down"
21+
class="button button--type-icon"
22+
@click="load"
23+
>
24+
<font-awesome-icon class="icon" icon="sync" fixed-width />
25+
</button>
26+
</div>
27+
</div>
28+
<div v-if="accessToken">
29+
<h2>Use access token</h2>
30+
<Input
31+
label="access token"
32+
:value="accessToken.jwtAccessToken"
33+
disabled
34+
/>
35+
<button class="button" @click="copy(accessToken.jwtAccessToken)">
36+
<font-awesome-icon class="icon icon--mr-1" icon="copy" />
37+
<span v-if="copied">Copied</span>
38+
<span v-else>Copy</span>
39+
</button>
40+
<button
41+
type="button"
42+
class="button button--color-danger"
43+
style="margin-left: 0.5rem"
44+
@click.prevent="showDelete = accessToken"
45+
>
46+
<font-awesome-icon class="icon icon--mr-1" icon="trash" />
47+
<span>Delete</span>
48+
</button>
49+
<h2>Edit access token</h2>
50+
<form
51+
v-meta-ctrl-enter="() => (showUpdate = true)"
52+
@submit.prevent="() => (showUpdate = true)"
53+
>
54+
<Input
55+
label="Name"
56+
placeholder="Enter a name for this access token"
57+
:value="accessToken.name"
58+
@input="val => (accessToken.name = val)"
59+
/>
60+
<CheckList
61+
label="API restrictions"
62+
:options="scopes"
63+
:value="accessToken.scopes"
64+
@input="val => (accessToken.scopes = val)"
65+
/>
66+
<button class="button">Update access token</button>
67+
</form>
68+
</div>
69+
</div>
70+
<transition name="modal">
71+
<Confirm v-if="showDelete" :on-close="() => (showDelete = false)">
72+
<h2>Are you sure you want to delete this access token?</h2>
73+
<p>
74+
Deleting an access token is not reversible, and you'll need to update
75+
any apps using this key.
76+
</p>
77+
<button
78+
class="button button--color-danger button--state-cta"
79+
@click="deleteAccessToken(showDelete.id)"
80+
>
81+
Yes, delete access token
82+
</button>
83+
<button type="button" class="button" @click="showDelete = false">
84+
No, don't delete
85+
</button>
86+
</Confirm>
87+
</transition>
88+
<transition name="modal">
89+
<Confirm v-if="showUpdate" :on-close="() => (showUpdate = false)">
90+
<h2>
91+
Are you sure you want to update and regenerate this access token?
92+
</h2>
93+
<p>
94+
Updating your access token will generate a new access token, so you'll
95+
have to update it wherever you're using it.
96+
</p>
97+
<p>The current access token will stop working instantly.</p>
98+
<button
99+
class="button button--color-primary button--state-cta"
100+
@click="updateAccessToken"
101+
>
102+
Yes, regenerate access token
103+
</button>
104+
<button type="button" class="button" @click="showUpdate = false">
105+
No, don't update
106+
</button>
107+
</Confirm>
108+
</transition>
109+
</main>
110+
</template>
111+
112+
<script lang="ts">
113+
import { Component, Vue, Watch } from "vue-property-decorator";
114+
import { mapGetters } from "vuex";
115+
import { getAllCountries } from "countries-and-timezones";
116+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
117+
import { library } from "@fortawesome/fontawesome-svg-core";
118+
import {
119+
faPencilAlt,
120+
faArrowDown,
121+
faSync,
122+
faTrash,
123+
faEye,
124+
faEyeSlash,
125+
faArrowLeft,
126+
faCopy
127+
} from "@fortawesome/free-solid-svg-icons";
128+
import copy from "copy-to-clipboard";
129+
import Loading from "@/components/Loading.vue";
130+
import Confirm from "@/components/Confirm.vue";
131+
import TimeAgo from "@/components/TimeAgo.vue";
132+
import LargeMessage from "@/components/LargeMessage.vue";
133+
import Input from "@/components/form/Input.vue";
134+
import CheckList from "@/components/form/CheckList.vue";
135+
import CommaList from "@/components/form/CommaList.vue";
136+
import Checkbox from "@/components/form/Checkbox.vue";
137+
import Select from "@/components/form/Select.vue";
138+
import { User } from "@/types/auth";
139+
import { AccessTokens, emptyPagination, AccessToken } from "@/types/users";
140+
import translations from "@/locales/en";
141+
import { removeNulls } from "@/helpers/crud";
142+
const scopes = translations.scopes;
143+
library.add(
144+
faPencilAlt,
145+
faArrowDown,
146+
faSync,
147+
faTrash,
148+
faCopy,
149+
faEye,
150+
faEyeSlash,
151+
faArrowLeft
152+
);
153+
154+
@Component({
155+
components: {
156+
Loading,
157+
Confirm,
158+
Input,
159+
TimeAgo,
160+
CommaList,
161+
FontAwesomeIcon,
162+
CheckList,
163+
Select,
164+
LargeMessage,
165+
Checkbox
166+
},
167+
middleware: "auth"
168+
})
169+
export default class ManageSettings extends Vue {
170+
accessTokens: AccessTokens = emptyPagination;
171+
showDelete = false;
172+
showUpdate = false;
173+
loading = "";
174+
accessToken: AccessToken | null = null;
175+
scopes = scopes;
176+
copied = false;
177+
178+
private created() {
179+
this.accessTokens = {
180+
...this.$store.getters["users/accessTokens"](this.$route.params.slug)
181+
};
182+
}
183+
184+
private load() {
185+
this.loading = "Loading your access tokens";
186+
this.$store
187+
.dispatch("users/getAccessToken", {
188+
slug: this.$route.params.slug,
189+
id: this.$route.params.key
190+
})
191+
.then(accessToken => {
192+
this.accessToken = { ...accessToken };
193+
})
194+
.catch(error => {
195+
throw new Error(error);
196+
})
197+
.finally(() => (this.loading = ""));
198+
}
199+
200+
private mounted() {
201+
this.load();
202+
}
203+
204+
private updateAccessToken() {
205+
this.showUpdate = false;
206+
this.loading = "Updating your access token";
207+
const accessToken = { ...this.accessToken };
208+
if (accessToken) {
209+
[
210+
"jwtAccessToken",
211+
"userId",
212+
"expiresAt",
213+
"createdAt",
214+
"updatedAt"
215+
].forEach(k => delete accessToken[k]);
216+
}
217+
this.$store
218+
.dispatch("users/updateAccessToken", {
219+
slug: this.$route.params.slug,
220+
id: this.$route.params.key,
221+
...accessToken
222+
})
223+
.then(accessToken => {
224+
this.accessToken = { ...accessToken };
225+
})
226+
.catch(error => {
227+
throw new Error(error);
228+
})
229+
.finally(() => {
230+
this.loading = "";
231+
});
232+
}
233+
234+
private deleteAccessToken(key: number) {
235+
this.showDelete = false;
236+
this.loading = "Deleting your access token";
237+
this.$store
238+
.dispatch("users/deleteAccessToken", {
239+
slug: this.$route.params.slug,
240+
id: key
241+
})
242+
.then(accessTokens => {
243+
this.accessTokens = { ...accessTokens };
244+
this.$router.push(`/users/${this.$route.params.slug}/access-tokens`);
245+
})
246+
.catch(error => {
247+
throw new Error(error);
248+
})
249+
.finally(() => (this.loading = ""));
250+
}
251+
252+
private copy(text: string) {
253+
copy(text);
254+
this.copied = true;
255+
setTimeout(() => {
256+
this.copied = false;
257+
}, 3000);
258+
}
259+
}
260+
</script>
261+
262+
<style lang="scss" scoped></style>

0 commit comments

Comments
 (0)