diff --git a/dashboard/src/components/ProfileView.vue b/dashboard/src/components/ProfileView.vue
index 97e74a5ca..c0845fec0 100644
--- a/dashboard/src/components/ProfileView.vue
+++ b/dashboard/src/components/ProfileView.vue
@@ -51,6 +51,20 @@
+
+
{{ $t('profile.appPasswords.title') }}
+
+
+
+ {{ $t('profile.apiTokens.title') }}
+
+
+
+ {{ $t('profile.loginTokens.title') }}
+
+ {{ $t('profile.loginTokens.description', { webadminTokenCount: webadminTokens.length, cliTokenCount: cliTokens.length }) }}
+
+
@@ -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; });
+});
+
diff --git a/dashboard/src/constants.js b/dashboard/src/constants.js
index 24b8cffd4..83df3f548 100644
--- a/dashboard/src/constants.js
+++ b/dashboard/src/constants.js
@@ -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,
};
diff --git a/dashboard/src/models/TokensModel.js b/dashboard/src/models/TokensModel.js
new file mode 100644
index 000000000..d384a3964
--- /dev/null
+++ b/dashboard/src/models/TokensModel.js
@@ -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,
+};
diff --git a/dashboard/src/style.css b/dashboard/src/style.css
index 497d0fc7c..72a9dfa06 100644
--- a/dashboard/src/style.css
+++ b/dashboard/src/style.css
@@ -27,6 +27,7 @@ html, body {
h2 {
font-weight: 400;
+ font-size: 24px;
}
a {
diff --git a/dashboard/src/theme.scss b/dashboard/src/theme.scss
index c7161b067..e5f694401 100644
--- a/dashboard/src/theme.scss
+++ b/dashboard/src/theme.scss
@@ -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;