Implement cli and web token revoking
This commit is contained in:
@@ -51,6 +51,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<h2>{{ $t('profile.appPasswords.title') }}</h2>
|
||||
<Card>
|
||||
</Card>
|
||||
|
||||
<h2 v-show="user.isAtLeastAdmin">{{ $t('profile.apiTokens.title') }}</h2>
|
||||
<Card v-show="user.isAtLeastAdmin">
|
||||
</Card>
|
||||
|
||||
<h2>{{ $t('profile.loginTokens.title') }}</h2>
|
||||
<Card>
|
||||
<p>{{ $t('profile.loginTokens.description', { webadminTokenCount: webadminTokens.length, cliTokenCount: cliTokens.length }) }}</p>
|
||||
<Button danger :loading="revokeTokensBusy" :disabled="revokeTokensBusy" @click="onRevokeAllWebAndCliTokens()">{{ $t('profile.loginTokens.logoutAll') }}</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -59,9 +73,11 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, Dropdown, InputDialog } from 'pankow';
|
||||
import { TOKEN_TYPES } from '../constants.js';
|
||||
import Card from './Card.vue';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
import CloudronModel from '../models/CloudronModel.js';
|
||||
import TokensModel from '../models/TokensModel.js';
|
||||
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
@@ -70,33 +86,15 @@ const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_OR
|
||||
|
||||
const profileModel = ProfileModel.create(API_ORIGIN, localStorage.token);
|
||||
const cloudronModel = CloudronModel.create(API_ORIGIN, localStorage.token);
|
||||
const tokensModel = TokensModel.create(API_ORIGIN, localStorage.token);
|
||||
|
||||
// TODO what is this?
|
||||
const config = ref({});
|
||||
|
||||
const config = ref({}); // TODO what is this?
|
||||
const user = ref({});
|
||||
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
|
||||
const languages = ref([]);
|
||||
const language = ref('');
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
const avatarFileInput = useTemplateRef('avatarFileInput');
|
||||
|
||||
onMounted(async () => {
|
||||
user.value = await profileModel.get();
|
||||
|
||||
const langs = await cloudronModel.languages();
|
||||
languages.value = langs.map(l => {
|
||||
return {
|
||||
id: l,
|
||||
display: t(`lang.${l}`)
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
return a.display.localeCompare(b.display);
|
||||
});
|
||||
|
||||
const usedLang = window.localStorage.NG_TRANSLATE_LANG_KEY || 'en';
|
||||
language.value = languages.value.find(l => l.id === usedLang).id;
|
||||
});
|
||||
|
||||
async function onSelectLanguage(lang) {
|
||||
window.localStorage.NG_TRANSLATE_LANG_KEY = lang.id;
|
||||
|
||||
@@ -190,6 +188,7 @@ async function onLogout() {
|
||||
await profileModel.logout();
|
||||
}
|
||||
|
||||
const avatarFileInput = useTemplateRef('avatarFileInput');
|
||||
async function onAvatarChanged() {
|
||||
if (!avatarFileInput.value.files[0]) return;
|
||||
await profileModel.setAvatar(avatarFileInput.value.files[0]);
|
||||
@@ -200,6 +199,47 @@ async function onAvatarChanged() {
|
||||
user.value.avatarUrl = u.toString();
|
||||
}
|
||||
|
||||
// Tokens
|
||||
const webadminTokens = ref([]);
|
||||
const cliTokens = ref([]);
|
||||
const apiTokens = ref([]);
|
||||
const revokeTokensBusy = ref(false);
|
||||
async function onRevokeAllWebAndCliTokens() {
|
||||
revokeTokensBusy.value = true;
|
||||
|
||||
// filter current access token to be able to logout still
|
||||
const tokens = webadminTokens.value.concat(cliTokens.value).filter(t => t.accessToken !== localStorage.token);
|
||||
for (const token of tokens) {
|
||||
await tokensModel.remove(token.id);
|
||||
}
|
||||
|
||||
await profileModel.logout();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
user.value = await profileModel.get();
|
||||
|
||||
const langs = await cloudronModel.languages();
|
||||
languages.value = langs.map(l => {
|
||||
return {
|
||||
id: l,
|
||||
display: t(`lang.${l}`)
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
return a.display.localeCompare(b.display);
|
||||
});
|
||||
|
||||
const usedLang = window.localStorage.NG_TRANSLATE_LANG_KEY || 'en';
|
||||
language.value = languages.value.find(l => l.id === usedLang).id;
|
||||
|
||||
const tokens = await tokensModel.list();
|
||||
|
||||
// dashboard and development clientIds were issued with 7.5.0
|
||||
webadminTokens.value = tokens.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_WEBADMIN || c.clientId === TOKEN_TYPES.ID_DEVELOPMENT || c.clientId === 'dashboard' || c.clientId === 'development'; });
|
||||
cliTokens.value = tokens.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_CLI; });
|
||||
apiTokens.value = tokens.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_SDK; });
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -83,6 +83,14 @@ const APP_TYPES = {
|
||||
|
||||
const PROXY_APP_ID = 'io.cloudron.builtin.appproxy';
|
||||
|
||||
// sync up with tokens.js
|
||||
const TOKEN_TYPES = {
|
||||
ID_WEBADMIN: 'cid-webadmin', // dashboard
|
||||
ID_DEVELOPMENT: 'cid-development', // dashboard development
|
||||
ID_CLI: 'cid-cli', // cloudron cli
|
||||
ID_SDK: 'cid-sdk', // created by user via dashboard
|
||||
};
|
||||
|
||||
// named exports
|
||||
export {
|
||||
APP_TYPES,
|
||||
@@ -93,6 +101,7 @@ export {
|
||||
ROLES,
|
||||
TASK_TYPES,
|
||||
PROXY_APP_ID,
|
||||
TOKEN_TYPES,
|
||||
};
|
||||
|
||||
// default export
|
||||
@@ -105,4 +114,5 @@ export default {
|
||||
ROLES,
|
||||
TASK_TYPES,
|
||||
PROXY_APP_ID,
|
||||
TOKEN_TYPES,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
import { fetcher } from 'pankow';
|
||||
|
||||
function create(origin, accessToken) {
|
||||
return {
|
||||
name: 'TokensModel',
|
||||
async list() {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${origin}/api/v1/tokens`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) {
|
||||
console.error('Failed to list tokens.', error || result.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.body.tokens;
|
||||
},
|
||||
async remove(id) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.del(`${origin}/api/v1/tokens/${id}`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error) return error;
|
||||
if (result.status !== 204) return result;
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
};
|
||||
@@ -27,6 +27,7 @@ html, body {
|
||||
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -11,7 +11,7 @@ $brand-danger: #ff4c4c !default;
|
||||
|
||||
$body-bg: #f4f4f4;
|
||||
$font-family-sans-serif: "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
$font-family-heading: "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
$font-family-heading: "Noto Sans Light", Helvetica, Arial, sans-serif;
|
||||
|
||||
$navbar-default-link-color: $brand-primary !default;
|
||||
$navbar-default-link-hover-color: #62bdfc !default;
|
||||
@@ -531,6 +531,10 @@ h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
padding-left: 15px;
|
||||
padding-right: 20px;
|
||||
|
||||
Reference in New Issue
Block a user