Vorstand
-
+
Nils Wisiol
Peter Thomassen
Wolfgang Studier
@@ -196,38 +196,10 @@ import {
export default {
name: 'App',
- data: () => ({
- user: useUserStore(),
- drawer: false,
- email: import.meta.env.VITE_APP_EMAIL,
- mdiHeart,
- mdiMenuDown,
- tabmenu: {
- 'domains': {
- 'name': 'domains',
- 'text': 'Domain Management',
- },
- 'tokens': {
- 'name': 'tokens',
- 'text': 'Token Management',
- },
- },
- tabmenumore: {
- 'totp': {
- 'name': 'totp',
- 'text': 'Manage 2-Factor Authentication',
- },
- 'change-email': {
- 'name': 'change-email',
- 'text': 'Change Email Address',
- },
- 'delete-account': {
- 'name': 'delete-account',
- 'text': 'Delete Account',
- },
- },
- }),
computed: {
+ authenticated() {
+ return this.user?.authenticated;
+ },
menu: () => {
const user = useUserStore();
const menu_perma = {
@@ -277,6 +249,46 @@ export default {
return {...menu_perma, ...menu_opt};
},
},
+ data: () => ({
+ user: useUserStore(),
+ drawer: false,
+ email: import.meta.env.VITE_APP_EMAIL,
+ activeTab: null,
+ mdiHeart,
+ mdiMenuDown,
+ tabmenu: {
+ 'domains': {
+ 'name': 'domains',
+ 'text': 'Domain Management',
+ },
+ 'tokens': {
+ 'name': 'tokens',
+ 'text': 'Token Management',
+ },
+ },
+ tabmenumore: {
+ 'totp': {
+ 'name': 'totp',
+ 'text': 'Manage 2-Factor Authentication',
+ },
+ 'change-email': {
+ 'name': 'change-email',
+ 'text': 'Change Email Address',
+ },
+ 'delete-account': {
+ 'name': 'delete-account',
+ 'text': 'Delete Account',
+ },
+ },
+ }),
+ watch: {
+ $route: {
+ immediate: true,
+ handler(to) {
+ this.activeTab = to?.name ?? null;
+ },
+ },
+ },
methods: {
async logout() {
await logout();
@@ -285,3 +297,21 @@ export default {
}
}
+
+
diff --git a/www/webapp/src/components/ActivateAccountActionHandler.vue b/www/webapp/src/components/ActivateAccountActionHandler.vue
index f4bdba18f..2688fe66c 100644
--- a/www/webapp/src/components/ActivateAccountActionHandler.vue
+++ b/www/webapp/src/components/ActivateAccountActionHandler.vue
@@ -6,24 +6,23 @@
tabindex="1"
ref="captchaField"
/>
-
+
-
+
Yes, I agree to the Terms of Use and
Privacy Policy.
-
+
-
+
-
+
-
+
Tell me about deSEC developments. No ads. (recommended)
-
+
-
+
-
+
-
+
Yes, I agree to the Terms of Use and
Privacy Policy.
-
+
-
+
Again, thank you so much.
- Done
+ Done
-
+
{{ error }}
diff --git a/www/webapp/src/components/Field/GenericCaptcha.vue b/www/webapp/src/components/Field/GenericCaptcha.vue
index c8eb7fdc1..ca5e2e3aa 100644
--- a/www/webapp/src/components/Field/GenericCaptcha.vue
+++ b/www/webapp/src/components/Field/GenericCaptcha.vue
@@ -1,18 +1,18 @@
-
+
-
- {{ mdiRefresh }}
+
+
-
- {{ mdiEye }}
+
+
-
- {{ mdiEarHearing }}
+
+
diff --git a/www/webapp/src/components/Field/GenericCheckbox.vue b/www/webapp/src/components/Field/GenericCheckbox.vue
index f086ab29e..099b757d7 100644
--- a/www/webapp/src/components/Field/GenericCheckbox.vue
+++ b/www/webapp/src/components/Field/GenericCheckbox.vue
@@ -3,10 +3,10 @@
:label="label"
:disabled="disabled || readonly"
:error-messages="errorMessages"
- :input-value="value"
+ :model-value="inputValue"
:required="required"
:rules="[v => !required || !!v || 'Required.']"
- @change="change"
+ @update:modelValue="change"
/>
@@ -34,14 +34,24 @@ export default {
type: Boolean,
default: false,
},
+ modelValue: {
+ type: Boolean,
+ required: false,
+ },
value: {
type: Boolean,
- required: true,
+ required: false,
+ },
+ },
+ computed: {
+ inputValue() {
+ return this.modelValue ?? this.value;
},
},
methods: {
- change(event) {
- this.$emit('input', event);
+ change(value) {
+ this.$emit('update:modelValue', value);
+ this.$emit('input', value);
this.$emit('dirty', {target: this.$el});
},
},
diff --git a/www/webapp/src/components/Field/GenericSwitchbox.vue b/www/webapp/src/components/Field/GenericSwitchbox.vue
index 6510bd415..3d6ee13e6 100644
--- a/www/webapp/src/components/Field/GenericSwitchbox.vue
+++ b/www/webapp/src/components/Field/GenericSwitchbox.vue
@@ -3,10 +3,10 @@
:label="label"
:disabled="disabled || readonly"
:error-messages="errorMessages"
- :input-value="value"
+ :model-value="inputValue"
:required="required"
:rules="[v => !required || !!v || 'Required.']"
- @change="change"
+ @update:modelValue="change"
/>
@@ -34,14 +34,24 @@ export default {
type: Boolean,
default: false,
},
+ modelValue: {
+ type: Boolean,
+ required: false,
+ },
value: {
type: Boolean,
- required: true,
+ required: false,
+ },
+ },
+ computed: {
+ inputValue() {
+ return this.modelValue ?? this.value;
},
},
methods: {
- change(event) {
- this.$emit('input', event);
+ change(value) {
+ this.$emit('update:modelValue', value);
+ this.$emit('input', value);
this.$emit('dirty', {target: this.$el});
},
},
diff --git a/www/webapp/src/components/Field/GenericText.vue b/www/webapp/src/components/Field/GenericText.vue
index e07cbd744..4033a0f42 100644
--- a/www/webapp/src/components/Field/GenericText.vue
+++ b/www/webapp/src/components/Field/GenericText.vue
@@ -2,18 +2,18 @@
@@ -53,6 +53,10 @@ export default {
type: Array,
default: () => [],
},
+ modelValue: {
+ type: [String, Number],
+ required: false,
+ },
value: {
type: [String, Number],
required: false,
@@ -73,15 +77,28 @@ export default {
data() { return {
hintClass: '',
}},
+ computed: {
+ inputValue() {
+ return this.modelValue ?? this.value;
+ },
+ resolvedHint() {
+ return this.hintWarning(this.inputValue) !== false ? (this.hint || '') : '';
+ },
+ },
methods: {
- changed(event, e) {
- this.$emit(event, e);
+ updateValue(value) {
+ this.$emit('update:modelValue', value);
+ this.$emit('input', value);
+ this.$emit('dirty');
+ },
+ handleKeyup(event) {
+ this.$emit('keyup', event);
this.$emit('dirty');
},
},
watch: {
- value: function() {
- this.hintClass = this.hintWarning(this.value) ? 'hint-warning' : '';
+ inputValue: function() {
+ this.hintClass = this.hintWarning(this.inputValue) ? 'hint-warning' : '';
},
},
};
@@ -111,4 +128,4 @@ export default {
.hint-warning .v-messages__message {
color: #fb8c00;
}
-
\ No newline at end of file
+
diff --git a/www/webapp/src/components/Field/GenericTextarea.vue b/www/webapp/src/components/Field/GenericTextarea.vue
index 9b68bcd19..e611e10e7 100644
--- a/www/webapp/src/components/Field/GenericTextarea.vue
+++ b/www/webapp/src/components/Field/GenericTextarea.vue
@@ -3,17 +3,16 @@
:label="label"
:disabled="disabled || readonly"
:error-messages="errorMessages"
- :value="value"
+ :model-value="inputValue"
:type="type || ''"
:placeholder="required ? '' : '(optional)'"
:hint="hint"
persistent-hint
:required="required"
:rules="[v => !required || !!v || 'Required.'].concat(rules)"
- @input="changed('input', $event)"
- @input.native="$emit('dirty', $event)"
- @keyup="changed('keyup', $event)"
- dense
+ @update:modelValue="updateValue"
+ @keyup="handleKeyup"
+ density="compact"
rows="8"
/>
@@ -50,6 +49,10 @@ export default {
type: Array,
default: () => [],
},
+ modelValue: {
+ type: [String, Number],
+ required: false,
+ },
value: {
type: [String, Number],
required: false,
@@ -59,9 +62,19 @@ export default {
required: false,
},
},
+ computed: {
+ inputValue() {
+ return this.modelValue ?? this.value;
+ },
+ },
methods: {
- changed(event, e) {
- this.$emit(event, e);
+ updateValue(value) {
+ this.$emit('update:modelValue', value);
+ this.$emit('input', value);
+ this.$emit('dirty');
+ },
+ handleKeyup(event) {
+ this.$emit('keyup', event);
this.$emit('dirty');
},
},
diff --git a/www/webapp/src/components/Field/RRSetType.vue b/www/webapp/src/components/Field/RRSetType.vue
index ed63b1a35..d6f172857 100644
--- a/www/webapp/src/components/Field/RRSetType.vue
+++ b/www/webapp/src/components/Field/RRSetType.vue
@@ -5,11 +5,11 @@
:error-messages="errorMessages"
hint="You can also enter other types. For a full list, check the documentation."
:persistent-hint="!readonly"
- :value="value"
+ :model-value="inputValue"
:items="types"
:required="required"
:rules="[v => !required || !!v || 'Required.']"
- @input="input($event)"
+ @update:modelValue="input"
/>
@@ -37,9 +37,13 @@ export default {
type: Boolean,
default: false,
},
+ modelValue: {
+ type: String,
+ required: false,
+ },
value: {
type: String,
- required: true,
+ required: false,
},
},
data: () => ({
@@ -60,8 +64,14 @@ export default {
'DS',
],
}),
+ computed: {
+ inputValue() {
+ return this.modelValue ?? this.value;
+ },
+ },
methods: {
input(event) {
+ this.$emit('update:modelValue', event);
this.$emit('input', event);
},
},
diff --git a/www/webapp/src/components/Field/RecordA.vue b/www/webapp/src/components/Field/RecordA.vue
index 868ccc184..20e8ce2be 100644
--- a/www/webapp/src/components/Field/RecordA.vue
+++ b/www/webapp/src/components/Field/RecordA.vue
@@ -1,10 +1,11 @@
diff --git a/www/webapp/src/components/ResetPasswordActionHandler.vue b/www/webapp/src/components/ResetPasswordActionHandler.vue
index f5b29363a..577b27b4e 100644
--- a/www/webapp/src/components/ResetPasswordActionHandler.vue
+++ b/www/webapp/src/components/ResetPasswordActionHandler.vue
@@ -3,7 +3,7 @@
h(App)
-}).$mount('#app')
+app.use(pinia)
+app.use(router)
+app.use(vuetify)
+
+app.mount('#app')
diff --git a/www/webapp/src/plugins/vuetify.js b/www/webapp/src/plugins/vuetify.js
index b616aac30..7f38e5e85 100644
--- a/www/webapp/src/plugins/vuetify.js
+++ b/www/webapp/src/plugins/vuetify.js
@@ -1,21 +1,30 @@
-import Vue from 'vue';
-import Vuetify from 'vuetify/lib';
-import colors from 'vuetify/lib/util/colors'
+import 'vuetify/styles'
+import { createVuetify } from 'vuetify'
+import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
+import { VDataTable, VOtpInput } from 'vuetify/components'
+import colors from 'vuetify/util/colors'
-
-Vue.use(Vuetify);
-
-
-export default new Vuetify({
+export default createVuetify({
+ components: {
+ VDataTable,
+ VOtpInput,
+ },
icons: {
- iconfont: 'mdiSvg', // 'mdi' || 'mdiSvg' || 'md' || 'fa' || 'fa4' || 'faSvg'
+ defaultSet: 'mdi',
+ aliases,
+ sets: {
+ mdi,
+ },
},
theme: {
+ defaultTheme: 'light',
themes: {
light: {
- primary: colors.amber,
- secondary: colors.lightBlue.darken1,
- accent: colors.amber.accent4,
+ colors: {
+ primary: colors.amber.base,
+ secondary: colors.lightBlue.darken1,
+ accent: colors.amber.accent4,
+ },
},
},
},
diff --git a/www/webapp/src/router/index.js b/www/webapp/src/router/index.js
index 123fb6429..681a3d846 100644
--- a/www/webapp/src/router/index.js
+++ b/www/webapp/src/router/index.js
@@ -1,8 +1,20 @@
-import VueRouter from 'vue-router'
+import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '@/views/HomePage.vue'
import {HTTP} from '@/utils';
import {useUserStore} from "@/store/user";
+const lazy = (loader) => () => loader().catch((error) => {
+ const message = error?.message || '';
+ if (
+ message.includes('Failed to fetch dynamically imported module') ||
+ message.includes('Importing a module script failed') ||
+ message.includes('error loading dynamically imported module')
+ ) {
+ window.location.reload();
+ }
+ throw error;
+});
+
const routes = [
{
path: '/',
@@ -12,136 +24,132 @@ const routes = [
{
path: '/signup/:email?',
name: 'signup',
- // route level code-splitting
- // this generates a separate chunk (about.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () => import('@/views/SignUp.vue'),
+ component: lazy(() => import('@/views/SignUp.vue')),
},
{
path: '/custom-setup/:domain',
name: 'customSetup',
- component: () => import('@/views/DomainSetupPage.vue'),
+ component: lazy(() => import('@/views/DomainSetupPage.vue')),
props: true,
},
{
path: '/dyn-setup/:domain',
alias: '/dynsetup/:domain',
name: 'dynSetup',
- component: () => import('@/views/DynSetup.vue'),
+ component: lazy(() => import('@/views/DynSetup.vue')),
},
{
path: '/welcome/:domain?',
name: 'welcome',
- component: () => import('@/views/WelcomePage.vue'),
+ component: lazy(() => import('@/views/WelcomePage.vue')),
},
{
- path: 'https://desec.readthedocs.io/',
+ path: '/docs',
name: 'docs',
- beforeEnter(to) { location.href = to.path },
+ beforeEnter() { location.href = 'https://desec.readthedocs.io/' },
},
{
- path: 'https://talk.desec.io/',
+ path: '/talk',
name: 'talk',
- beforeEnter(to) { location.href = to.path },
+ beforeEnter() { location.href = 'https://talk.desec.io/' },
},
{
path: '/confirm/:action/:code',
name: 'confirmation',
- component: () => import('@/views/ConfirmationPage.vue')
+ component: lazy(() => import('@/views/ConfirmationPage.vue'))
},
{
path: '/reset-password/:email?',
name: 'reset-password',
- component: () => import('@/views/ResetPassword.vue'),
+ component: lazy(() => import('@/views/ResetPassword.vue')),
},
{
path: '/totp/',
name: 'totp',
- component: () => import('@/views/CrudListTOTP.vue'),
+ component: lazy(() => import('@/views/CrudListTOTP.vue')),
meta: {guest: false},
},
{
path: '/totp-verify/',
name: 'TOTPVerify',
- component: () => import('@/views/Console/TOTPVerifyDialog.vue'),
+ component: lazy(() => import('@/views/Console/TOTPVerifyDialog.vue')),
props: (route) => ({...route.params}),
},
{
path: '/mfa/',
name: 'mfa',
- component: () => import('@/views/MFA.vue'),
+ component: lazy(() => import('@/views/MFA.vue')),
meta: {guest: false},
},
{
path: '/change-email/:email?',
name: 'change-email',
- component: () => import('@/views/ChangeEmail.vue'),
+ component: lazy(() => import('@/views/ChangeEmail.vue')),
meta: {guest: false},
},
{
path: '/delete-account/',
name: 'delete-account',
- component: () => import('@/views/DeleteAccount.vue'),
+ component: lazy(() => import('@/views/DeleteAccount.vue')),
meta: {guest: false},
},
{
path: '/donate/',
name: 'donate',
- component: () => import('@/views/DonatePage.vue'),
+ component: lazy(() => import('@/views/DonatePage.vue')),
},
{
- path: 'https://github.com/desec-io/desec-stack/milestones?direction=asc&sort=title&state=open',
+ path: '/roadmap',
name: 'roadmap',
- beforeEnter(to) { location.href = to.path },
+ beforeEnter() { location.href = 'https://github.com/desec-io/desec-stack/milestones?direction=asc&sort=title&state=open' },
},
{
path: '/impressum/',
name: 'impressum',
- component: () => import('@/views/ImpressumPage.vue'),
+ component: lazy(() => import('@/views/ImpressumPage.vue')),
},
{
path: '/privacy-policy/',
name: 'privacy-policy',
- component: () => import('@/views/PrivacyPolicy.vue'),
+ component: lazy(() => import('@/views/PrivacyPolicy.vue')),
},
{
path: '/terms/',
name: 'terms',
- component: () => import('@/views/TermsPage.vue'),
+ component: lazy(() => import('@/views/TermsPage.vue')),
},
{
path: '/about/',
name: 'about',
- component: () => import('@/views/AboutPage.vue'),
+ component: lazy(() => import('@/views/AboutPage.vue')),
},
{
path: '/login',
name: 'login',
- component: () => import('@/views/LoginPage.vue'),
+ component: lazy(() => import('@/views/LoginPage.vue')),
},
{
path: '/tokens',
name: 'tokens',
- component: () => import('@/views/CrudListToken.vue'),
+ component: lazy(() => import('@/views/CrudListToken.vue')),
meta: {guest: false},
},
{
path: '/domains',
name: 'domains',
- component: () => import('@/views/CrudListDomain.vue'),
+ component: lazy(() => import('@/views/CrudListDomain.vue')),
meta: {guest: false},
},
{
path: '/domains/:domain',
name: 'domain',
- component: () => import('@/views/CrudListRecord.vue'),
+ component: lazy(() => import('@/views/CrudListRecord.vue')),
meta: {guest: false},
},
]
-const router = new VueRouter({
- mode: 'history',
- base: import.meta.env.BASE_URL,
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
scrollBehavior (to, from) {
// Skip if destination full path has query parameters and differs in no other way from previous
if (from && Object.keys(to.query).length) {
diff --git a/www/webapp/src/views/AboutPage.vue b/www/webapp/src/views/AboutPage.vue
index dbf24088e..62bb9fb2c 100644
--- a/www/webapp/src/views/AboutPage.vue
+++ b/www/webapp/src/views/AboutPage.vue
@@ -26,7 +26,7 @@
-
+
diff --git a/www/webapp/src/views/ChangeEmail.vue b/www/webapp/src/views/ChangeEmail.vue
index b275b2dc1..d6d7f018c 100644
--- a/www/webapp/src/views/ChangeEmail.vue
+++ b/www/webapp/src/views/ChangeEmail.vue
@@ -20,7 +20,6 @@
Change Account Email Address
@@ -38,32 +37,30 @@
{{ actionName }} Confirmation
@@ -41,7 +40,7 @@
If you like our service, please consider donating.
- Donate
+ Donate
@@ -127,11 +126,6 @@
this.errors.splice(0, this.errors.length);
}
},
- filters: {
- replace: function (value, a, b) {
- return value.replace(a, b)
- }
- },
};
diff --git a/www/webapp/src/views/Console/DomainSetupDialog.vue b/www/webapp/src/views/Console/DomainSetupDialog.vue
index c112777df..f491a0acc 100644
--- a/www/webapp/src/views/Console/DomainSetupDialog.vue
+++ b/www/webapp/src/views/Console/DomainSetupDialog.vue
@@ -12,15 +12,13 @@
Setup Instructions for {{ domain }}
-
- {{ mdiClose }}
-
+
Your domain {{ domain }} has been successfully created!
@@ -49,20 +47,21 @@ export default {
type: Boolean,
default: false,
},
- },
- data: () => ({
- mdiClose,
- value: {
+ modelValue: {
type: Boolean,
default: true,
},
+ },
+ data: () => ({
+ mdiClose,
}),
computed: {
show: {
get() {
- return this.value
+ return this.modelValue
},
set(value) {
+ this.$emit('update:modelValue', value)
this.$emit('input', value)
}
}
diff --git a/www/webapp/src/views/Console/TOTPVerifyDialog.vue b/www/webapp/src/views/Console/TOTPVerifyDialog.vue
index 4290b6b76..3cb202055 100644
--- a/www/webapp/src/views/Console/TOTPVerifyDialog.vue
+++ b/www/webapp/src/views/Console/TOTPVerifyDialog.vue
@@ -8,43 +8,48 @@
>
-
-
- Verify TOTP: {{ name }}
-
+
+
+ Verify TOTP: {{ displayName }}
+
-
- {{ mdiClose }}
-
+
+
+
-
+
{{ detail }}
-
+
{{ successDetail }}
- {{ mdiCheck }}
+
Great! Continue to log in.
-
+
- {{ mdiNumeric1Circle }}
+
Please scan the following QR code with an authenticator app (e.g. Google Authenticator).
This code is only displayed once.
-
+
- {{ mdiNumeric2Circle }}
+
Enter the code displayed in the authenticator app to confirm and activate the token:
@@ -65,7 +70,7 @@
Want to know what's in the code? — It's your TOTP secret:
- {{ data.secret }}
+ {{ payload.secret }}
@@ -92,7 +97,7 @@