Skip to content

Commit 960bec9

Browse files
committed
enh(settings): Refactor frontend for session and app token management
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 0dcea03 commit 960bec9

9 files changed

Lines changed: 802 additions & 602 deletions

File tree

apps/settings/src/components/AuthToken.vue

Lines changed: 189 additions & 130 deletions
Large diffs are not rendered by default.

apps/settings/src/components/AuthTokenList.vue

Lines changed: 41 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
33
-
44
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
5+
- @author Ferdinand Thiessen <opensource@fthiessen.de>
56
-
67
- @license GNU AGPL version 3 or any later version
78
-
@@ -20,115 +21,74 @@
2021
-->
2122

2223
<template>
23-
<table id="app-tokens-table">
24-
<thead v-if="tokens.length">
24+
<table id="app-tokens-table" class="token-list">
25+
<thead>
2526
<tr>
26-
<th />
27-
<th>{{ t('settings', 'Device') }}</th>
28-
<th>{{ t('settings', 'Last activity') }}</th>
29-
<th />
27+
<th class="token-list__header-device">
28+
{{ t('settings', 'Device') }}
29+
</th>
30+
<th class="toke-list__header-activity">
31+
{{ t('settings', 'Last activity') }}
32+
</th>
33+
<th>
34+
<span class="hidden-visually">
35+
{{ t('settings', 'Actions') }}
36+
</span>
37+
</th>
3038
</tr>
3139
</thead>
32-
<tbody class="token-list">
40+
<tbody class="token-list__body">
3341
<AuthToken v-for="token in sortedTokens"
3442
:key="token.id"
35-
:token="token"
36-
@toggle-scope="toggleScope"
37-
@rename="rename"
38-
@delete="onDelete"
39-
@wipe="onWipe" />
43+
:token="token" />
4044
</tbody>
4145
</table>
4246
</template>
4347

44-
<script>
48+
<script lang="ts">
49+
import { translate as t } from '@nextcloud/l10n'
50+
import { defineComponent } from 'vue'
51+
import { useAuthTokenStore } from '../store/authtoken'
52+
4553
import AuthToken from './AuthToken.vue'
4654
47-
export default {
55+
export default defineComponent({
4856
name: 'AuthTokenList',
4957
components: {
5058
AuthToken,
5159
},
52-
props: {
53-
tokens: {
54-
type: Array,
55-
required: true,
56-
},
60+
setup() {
61+
const authTokenStore = useAuthTokenStore()
62+
return { authTokenStore }
5763
},
5864
computed: {
5965
sortedTokens() {
60-
return this.tokens.slice().sort((t1, t2) => {
61-
const ts1 = parseInt(t1.lastActivity, 10)
62-
const ts2 = parseInt(t2.lastActivity, 10)
63-
return ts2 - ts1
64-
})
66+
return [...this.authTokenStore.tokens].sort((t1, t2) => t2.lastActivity - t1.lastActivity)
6567
},
6668
},
6769
methods: {
68-
toggleScope(token, scope, value) {
69-
// Just pass it on
70-
this.$emit('toggle-scope', token, scope, value)
71-
},
72-
rename(token, newName) {
73-
// Just pass it on
74-
this.$emit('rename', token, newName)
75-
},
76-
onDelete(token) {
77-
// Just pass it on
78-
this.$emit('delete', token)
79-
},
80-
onWipe(token) {
81-
// Just pass it on
82-
this.$emit('wipe', token)
83-
},
70+
t,
8471
},
85-
}
72+
})
8673
</script>
8774

8875
<style lang="scss" scoped>
89-
table {
90-
width: 100%;
91-
min-height: 50px;
92-
padding-top: 5px;
93-
max-width: 580px;
76+
.token-list {
77+
width: 100%;
78+
min-height: 50px;
79+
padding-top: 5px;
80+
max-width: fit-content;
9481
95-
th {
96-
padding: 10px 0;
97-
}
82+
th {
83+
padding-block: 10px;
84+
padding-inline-start: 10px;
9885
}
9986
100-
.token-list {
101-
td > a.icon-more {
102-
transition: opacity var(--animation-quick);
103-
}
104-
105-
a.icon-more {
106-
padding: 14px;
107-
display: block;
108-
width: 44px;
109-
height: 44px;
110-
opacity: .5;
111-
}
112-
113-
tr {
114-
&:hover td > a.icon,
115-
td > a.icon:focus,
116-
&.active td > a.icon {
117-
opacity: 1;
118-
}
119-
}
87+
#{&}__header-device {
88+
padding-inline-start: 50px; // 44px icon + 6px padding
12089
}
121-
</style>
122-
123-
<!-- some styles are not scoped to make them work on subcomponents -->
124-
<style lang="scss">
125-
#app-tokens-table {
126-
tr > *:nth-child(2) {
127-
padding-left: 6px;
128-
}
129-
130-
tr > *:nth-child(3) {
131-
text-align: right;
132-
}
90+
&__header-activity {
91+
text-align: end;
13392
}
93+
}
13494
</style>

apps/settings/src/components/AuthTokenSection.vue

Lines changed: 13 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
33
-
44
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
5+
- @author Ferdinand Thiessen <opensource@fthiessen.de>
56
-
67
- @license GNU AGPL version 3 or any later version
78
-
@@ -25,164 +26,32 @@
2526
<p class="settings-hint hidden-when-empty">
2627
{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}
2728
</p>
28-
<AuthTokenList :tokens="tokens"
29-
@toggle-scope="toggleTokenScope"
30-
@rename="rename"
31-
@delete="deleteToken"
32-
@wipe="wipeToken" />
33-
<AuthTokenSetupDialogue v-if="canCreateToken" :add="addNewToken" />
29+
<AuthTokenList />
30+
<AuthTokenSetup v-if="canCreateToken" />
3431
</div>
3532
</template>
3633

37-
<script>
38-
import axios from '@nextcloud/axios'
39-
import { confirmPassword } from '@nextcloud/password-confirmation'
40-
import '@nextcloud/password-confirmation/dist/style.css'
41-
import { generateUrl } from '@nextcloud/router'
34+
<script lang="ts">
35+
import { loadState } from '@nextcloud/initial-state'
36+
import { translate as t } from '@nextcloud/l10n'
37+
import { defineComponent } from 'vue'
4238
4339
import AuthTokenList from './AuthTokenList.vue'
44-
import AuthTokenSetupDialogue from './AuthTokenSetupDialogue.vue'
40+
import AuthTokenSetup from './AuthTokenSetup.vue'
4541
46-
const confirm = () => {
47-
return new Promise(resolve => {
48-
OC.dialogs.confirm(
49-
t('settings', 'Do you really want to wipe your data from this device?'),
50-
t('settings', 'Confirm wipe'),
51-
resolve,
52-
true,
53-
)
54-
})
55-
}
56-
57-
/**
58-
* Tap into a promise without losing the value
59-
*
60-
* @param {Function} cb the callback
61-
* @return {any} val the value
62-
*/
63-
const tap = cb => val => {
64-
cb(val)
65-
return val
66-
}
67-
68-
export default {
42+
export default defineComponent({
6943
name: 'AuthTokenSection',
7044
components: {
71-
AuthTokenSetupDialogue,
7245
AuthTokenList,
73-
},
74-
props: {
75-
tokens: {
76-
type: Array,
77-
required: true,
78-
},
79-
canCreateToken: {
80-
type: Boolean,
81-
required: true,
82-
},
46+
AuthTokenSetup,
8347
},
8448
data() {
8549
return {
86-
baseUrl: generateUrl('/settings/personal/authtokens'),
50+
canCreateToken: loadState('settings', 'can_create_app_token'),
8751
}
8852
},
8953
methods: {
90-
addNewToken(name) {
91-
console.debug('creating a new app token', name)
92-
93-
const data = {
94-
name,
95-
}
96-
return axios.post(this.baseUrl, data)
97-
.then(resp => resp.data)
98-
.then(tap(() => console.debug('app token created')))
99-
// eslint-disable-next-line vue/no-mutating-props
100-
.then(tap(data => this.tokens.push(data.deviceToken)))
101-
.catch(err => {
102-
console.error.bind('could not create app password', err)
103-
OC.Notification.showTemporary(t('settings', 'Error while creating device token'))
104-
throw err
105-
})
106-
},
107-
toggleTokenScope(token, scope, value) {
108-
console.debug('updating app token scope', token.id, scope, value)
109-
110-
const oldVal = token.scope[scope]
111-
token.scope[scope] = value
112-
113-
return this.updateToken(token)
114-
.then(tap(() => console.debug('app token scope updated')))
115-
.catch(err => {
116-
console.error.bind('could not update app token scope', err)
117-
OC.Notification.showTemporary(t('settings', 'Error while updating device token scope'))
118-
119-
// Restore
120-
token.scope[scope] = oldVal
121-
122-
throw err
123-
})
124-
},
125-
rename(token, newName) {
126-
console.debug('renaming app token', token.id, token.name, newName)
127-
128-
const oldName = token.name
129-
token.name = newName
130-
131-
return this.updateToken(token)
132-
.then(tap(() => console.debug('app token name updated')))
133-
.catch(err => {
134-
console.error.bind('could not update app token name', err)
135-
OC.Notification.showTemporary(t('settings', 'Error while updating device token name'))
136-
137-
// Restore
138-
token.name = oldName
139-
})
140-
},
141-
updateToken(token) {
142-
return axios.put(this.baseUrl + '/' + token.id, token)
143-
.then(resp => resp.data)
144-
},
145-
deleteToken(token) {
146-
console.debug('deleting app token', token)
147-
148-
// eslint-disable-next-line vue/no-mutating-props
149-
this.tokens = this.tokens.filter(t => t !== token)
150-
151-
return axios.delete(this.baseUrl + '/' + token.id)
152-
.then(resp => resp.data)
153-
.then(tap(() => console.debug('app token deleted')))
154-
.catch(err => {
155-
console.error.bind('could not delete app token', err)
156-
OC.Notification.showTemporary(t('settings', 'Error while deleting the token'))
157-
158-
// Restore
159-
// eslint-disable-next-line vue/no-mutating-props
160-
this.tokens.push(token)
161-
})
162-
},
163-
async wipeToken(token) {
164-
console.debug('wiping app token', token)
165-
166-
try {
167-
await confirmPassword()
168-
169-
if (!(await confirm())) {
170-
console.debug('wipe aborted by user')
171-
return
172-
}
173-
await axios.post(this.baseUrl + '/wipe/' + token.id)
174-
console.debug('app token marked for wipe')
175-
176-
token.type = 2
177-
} catch (err) {
178-
console.error('could not wipe app token', err)
179-
OC.Notification.showTemporary(t('settings', 'Error while wiping the device with the token'))
180-
}
181-
},
54+
t,
18255
},
183-
}
56+
})
18457
</script>
185-
186-
<style scoped>
187-
188-
</style>

0 commit comments

Comments
 (0)