- added api UserController.ts for 2FA
Some checks failed
CI Pipeline / japa-tests (push) Failing after 56s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 56s
- added PersonalTotpSettings.vue vor enablin/disabling 2FA - changed User.ts: added attributes: state, twoFactorSecret and twoFactorRecoveryCodes - added resources/js/utils/toast.ts for notifications - modified start/routes/api.ts - npm updates
This commit is contained in:
parent
18635f77b3
commit
ebc62d9117
18 changed files with 1151 additions and 315 deletions
246
resources/js/Components/PersonalTotpSettings.vue
Normal file
246
resources/js/Components/PersonalTotpSettings.vue
Normal file
|
@ -0,0 +1,246 @@
|
|||
<template>
|
||||
<!-- <div id="twofactor-totp-settings">
|
||||
<template v-if="loading">
|
||||
<span class="icon-loading-small totp-loading" />
|
||||
<span> {{ t('twofactor_totp', 'Enable TOTP') }} </span>
|
||||
</template>
|
||||
<div v-else>
|
||||
<input id="totp-enabled" v-model="enabled" type="checkbox" class="checkbox" :disabled="loading"
|
||||
@change="toggleEnabled">
|
||||
<label for="totp-enabled">{{
|
||||
t('twofactor_totp', 'Enable TOTP')
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<SetupConfirmation v-if="secret" :secret="secret" :qr-url="qrUrl" :loading="loadingConfirmation"
|
||||
:confirmation.sync="confirmation" @confirm="enableTOTP" />
|
||||
</div> -->
|
||||
<CardBox :icon="mdiTwoFactorAuthentication" id="twofactor-totp-settings" title="Two-Factor Authentication" form>
|
||||
<template v-if="loading">
|
||||
<!-- <span class="icon-loading-small totp-loading" /> -->
|
||||
<div class="relative inline-flex">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded-full"></div>
|
||||
<div class="w-6 h-6 bg-blue-500 rounded-full absolute top-0 left-0 animate-ping"></div>
|
||||
<div class="w-6 h-6 bg-blue-500 rounded-full absolute top-0 left-0 animate-pulse"></div>
|
||||
<span class="ml-4 max-w-xl text-sm text-gray-600">Enabling TOTP...</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
<!-- <div class="text-lg font-medium text-gray-900">
|
||||
You have not enabled two factor authentication.
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
When two factor authentication is enabled, you will be prompted for a secure,
|
||||
random token during authentication. You may retrieve this token from your phone's
|
||||
Google Authenticator application.
|
||||
</div> -->
|
||||
<input id="totp-enabled" v-model="enabled" type="checkbox" class="checkbox" :disabled="loading"
|
||||
@change="toggleEnabled" />
|
||||
<!-- <label for="totp-enabled"> Enable TOTP </label> -->
|
||||
<label for="totp-enabled">{{ checkboxLabel }}</label>
|
||||
</div>
|
||||
|
||||
<!-- <SetupConfirmation v-if="secret" :secret="secret" :qr-url="qrUrl" :loading="loadingConfirmation"
|
||||
:confirmation.sync="confirmation" @confirm="enableTOTP" /> -->
|
||||
<div v-if="qrSecret != ''">
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<!-- <p class="font-semibold">
|
||||
Two factor authentication is now enabled.
|
||||
</p> -->
|
||||
<p>Your new TOTP secret is: {{ qrSecret }}</p>
|
||||
<p>For quick setup, scan this QR code with your phone's authenticator application (TOTP):</p>
|
||||
<div class="mt-4">
|
||||
<img :src="qrSvg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p>
|
||||
After you configured your app, enter a test code below to ensure everything works correctly:
|
||||
</p>
|
||||
|
||||
</div> -->
|
||||
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p>After you configured your app, enter a test code below to ensure everything works correctly:</p>
|
||||
<!-- :disabled="loading" -->
|
||||
<input id="totp-confirmation" :disabled="loadingConfirmation" v-model="confirmationCode" type="tel"
|
||||
minlength="6" maxlength="10" autocomplete="off" autocapitalize="off"
|
||||
:placeholder="'Authentication code'" @keydown="onConfirmKeyDown" />
|
||||
|
||||
<!-- <input id="totp-confirmation-submit" type="button" :disabled="loading" :value="'Verify'"
|
||||
@click="enableTOTP"> -->
|
||||
|
||||
<!-- <BaseButtons>
|
||||
<BaseButton :icon="mdiContentSaveCheck" type="button" :disabled="loadingConfirmation" color="info"
|
||||
label="Verify" @click="enableTOTP" />
|
||||
</BaseButtons> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<BaseButtons v-if="qrSecret != ''">
|
||||
<BaseButton :icon="mdiContentSaveCheck" type="button" :disabled="loadingConfirmation" color="info"
|
||||
label="Verify" @click="enableTOTP" />
|
||||
</BaseButtons>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { MainService, State } from '@/Stores/main';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import Notification from '@/utils/toast';
|
||||
import { mdiContentSaveCheck, mdiTwoFactorAuthentication } from '@mdi/js';
|
||||
|
||||
const mainService = MainService();
|
||||
const emit = defineEmits(['confirm', 'update:confirmation']);
|
||||
|
||||
const props = defineProps({
|
||||
// user will be returned from controller action
|
||||
// user: {
|
||||
// type: Object,
|
||||
// default: () => ({}),
|
||||
// },
|
||||
twoFactorEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// // code: {
|
||||
// // type: Object,
|
||||
// // },
|
||||
// // recoveryCodes: {
|
||||
// // type: Array<string>,
|
||||
// // default: () => [],
|
||||
// // },
|
||||
// // errors: {
|
||||
// // type: Object,
|
||||
// // default: () => ({}),
|
||||
// // },
|
||||
});
|
||||
let loading = ref(false);
|
||||
let loadingConfirmation = ref(false);
|
||||
let test;
|
||||
if (props.twoFactorEnabled) {
|
||||
test = State.STATE_ENABLED;
|
||||
} else {
|
||||
test = State.STATE_DISABLED;
|
||||
}
|
||||
mainService.setState(test);
|
||||
|
||||
const enabled = ref(mainService.totpState == State.STATE_ENABLED);
|
||||
|
||||
let qrSecret = ref('');
|
||||
let qrUrl = ref('');
|
||||
let qrSvg = ref('');
|
||||
|
||||
const confirmationCode = ref('');
|
||||
|
||||
const confirm = () => {
|
||||
emit('update:confirmation', confirmationCode.value);
|
||||
emit('confirm');
|
||||
};
|
||||
|
||||
const onConfirmKeyDown = (e) => {
|
||||
if (e.which === 13) {
|
||||
confirm();
|
||||
}
|
||||
};
|
||||
|
||||
const state = computed(() => mainService.totpState);
|
||||
const checkboxLabel = computed(() => {
|
||||
if (enabled.value == true) {
|
||||
return ' Disable TOTP';
|
||||
} else {
|
||||
return ' Enable TOTP';
|
||||
}
|
||||
});
|
||||
|
||||
const toggleEnabled = async () => {
|
||||
if (loading.value == true) {
|
||||
// Ignore event
|
||||
// Logger.debug('still loading -> ignoring event')
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled.value) {
|
||||
return await createTOTP();
|
||||
} else {
|
||||
return await disableTOTP();
|
||||
}
|
||||
};
|
||||
|
||||
const createTOTP = async () => {
|
||||
// Show loading spinner
|
||||
loading.value = true;
|
||||
// Logger.debug('starting setup')
|
||||
|
||||
try {
|
||||
const { url, secret, svg } = await mainService.create();
|
||||
qrSecret.value = secret;
|
||||
qrUrl.value = url;
|
||||
qrSvg.value = svg;
|
||||
// If the stat could be changed, keep showing the loading
|
||||
// spinner until the user has finished the registration
|
||||
// if state isCretaed, show loading:
|
||||
loading.value = state.value === State.STATE_CREATED;
|
||||
} catch (e) {
|
||||
Notification.showWarning('Could not enable TOTP');
|
||||
// Logger.error('Could not enable TOTP', e)
|
||||
console.log('Could not create TOTP', e.message);
|
||||
|
||||
// Restore on error
|
||||
loading.value = false;
|
||||
enabled.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const disableTOTP = async () => {
|
||||
loading.value = false;
|
||||
// Logger.debug('starting disable');
|
||||
|
||||
await mainService.disable();
|
||||
enabled.value = false;
|
||||
loading.value = false;
|
||||
Notification.showSuccess('TOTP disabled!');
|
||||
};
|
||||
|
||||
const enableTOTP = async () => {
|
||||
loading.value = true;
|
||||
loadingConfirmation.value = true;
|
||||
|
||||
try {
|
||||
await mainService.confirm(confirmationCode.value);
|
||||
if (mainService.totpState === State.STATE_ENABLED) {
|
||||
// Success
|
||||
loading.value = false;
|
||||
enabled.value = true;
|
||||
qrUrl.value = '';
|
||||
qrSecret.value = '';
|
||||
Notification.showSuccess('two factor authentication enabled');
|
||||
} else {
|
||||
Notification.showWarning('Could not verify your key. Please try again');
|
||||
console.log('Could not verify your key. Please try again');
|
||||
}
|
||||
confirmationCode.value = '';
|
||||
loadingConfirmation.value = false;
|
||||
} catch (e) {
|
||||
console.log('Could not enable TOTP', e.message);
|
||||
Notification.showWarning('Could not enable TOTP ' + e.message);
|
||||
confirmationCode.value = '';
|
||||
loadingConfirmation.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.totp-loading {
|
||||
display: inline-block;
|
||||
vertical-align: sub;
|
||||
margin-left: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
||||
import { useForm, router } from '@inertiajs/vue3';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
// import { ref } from 'vue';
|
||||
// import { reactive } from 'vue';
|
||||
import {
|
||||
mdiAccount,
|
||||
|
@ -10,8 +11,7 @@ import {
|
|||
mdiAsterisk,
|
||||
mdiFormTextboxPassword,
|
||||
mdiArrowLeftBoldOutline,
|
||||
mdiAlertBoxOutline,
|
||||
mdiInformation
|
||||
mdiAlertBoxOutline,
|
||||
} from '@mdi/js';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
|
@ -28,10 +28,13 @@ import { stardust } from '@eidellev/adonis-stardust/client';
|
|||
import { computed, Ref } from 'vue';
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
||||
// import { Inertia } from '@inertiajs/inertia';
|
||||
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
|
||||
// import { MainService } from '@/Stores/main';
|
||||
// const mainService = MainService();
|
||||
|
||||
const emit = defineEmits(['confirm', 'update:confirmation'])
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
// user will be returned from controller action
|
||||
user: {
|
||||
type: Object,
|
||||
|
@ -58,12 +61,12 @@ const props = defineProps({
|
|||
// login: props.user.login,
|
||||
// email: props.user.email,
|
||||
// });
|
||||
const enableTwoFactorAuthentication = async () => {
|
||||
await router.post(stardust.route('account.password.enable2fa'));
|
||||
};
|
||||
const disableTwoFactorAuthentication = async () => {
|
||||
await router.post(stardust.route('account.password.disable2fa'));
|
||||
};
|
||||
// const enableTwoFactorAuthentication = async () => {
|
||||
// await router.post(stardust.route('account.password.enable2fa'));
|
||||
// };
|
||||
// const disableTwoFactorAuthentication = async () => {
|
||||
// await router.post(stardust.route('account.password.disable2fa'));
|
||||
// };
|
||||
|
||||
|
||||
const passwordForm = useForm({
|
||||
|
@ -84,6 +87,28 @@ const passwordSubmit = async () => {
|
|||
const flash: Ref<any> = computed(() => {
|
||||
return usePage().props.flash;
|
||||
});
|
||||
|
||||
// const confirmationCode = ref('');
|
||||
|
||||
// const confirm = () => {
|
||||
// emit('update:confirmation', confirmationCode);
|
||||
// emit('confirm');
|
||||
// };
|
||||
|
||||
// const onConfirmKeyDown = (e) => {
|
||||
// if (e.which === 13) {
|
||||
// confirm()
|
||||
// }
|
||||
// };
|
||||
|
||||
// const generateSecretCode = (user) => {
|
||||
// const secret = generateSecret({
|
||||
// name: 'TethysCloud',
|
||||
// account: user.email,
|
||||
// });
|
||||
// return secret.secret;
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -101,8 +126,8 @@ const flash: Ref<any> = computed(() => {
|
|||
{{ $page.props.flash.message }}
|
||||
</NotificationBar> -->
|
||||
|
||||
<!-- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- <div class="grid grid-cols-1 lg:grid-cols-1 gap-6"> -->
|
||||
|
||||
<!-- password form -->
|
||||
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileForm.post(route('admin.account.info.store'))"> -->
|
||||
|
@ -186,24 +211,9 @@ const flash: Ref<any> = computed(() => {
|
|||
|
||||
|
||||
|
||||
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileForm.post(route('admin.account.info.store'))"> -->
|
||||
<CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
|
||||
@submit.prevent="enableTwoFactorAuthentication()">
|
||||
<!-- <FormField label="Login" help="Required. Your login name" :class="{ 'text-red-400': errors.login }">
|
||||
<FormControl v-model="factorForm.login" v-bind:icon="mdiAccount" name="login" required :error="errors.login">
|
||||
<div class="text-red-400 text-sm" v-if="errors.login">
|
||||
{{ errors.login }}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormField>
|
||||
<FormField label="Email" help="Required. Your e-mail" :class="{ 'text-red-400': errors.email }">
|
||||
<FormControl v-model="factorForm.email" :icon="mdiMail" type="email" name="email" required :error="errors.email">
|
||||
<div class="text-red-400 text-sm" v-if="errors.email">
|
||||
{{ errors.email }}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormField> -->
|
||||
|
||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled"/>
|
||||
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
|
||||
@submit.prevent="enableTwoFactorAuthentication()">
|
||||
<div class="text-lg font-medium text-gray-900">
|
||||
You have not enabled two factor authentication.
|
||||
</div>
|
||||
|
@ -218,70 +228,9 @@ const flash: Ref<any> = computed(() => {
|
|||
<BaseButton color="info" type="submit" label="Enable" />
|
||||
</BaseButtons>
|
||||
</template>
|
||||
</CardBox>
|
||||
</CardBox> -->
|
||||
|
||||
<CardBox v-else-if="props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form @submit.prevent="disableTwoFactorAuthentication()">
|
||||
<!-- <div class="w-1/2 space-y-4 bg-gray-100 p-8"> -->
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
You have enabled two factor authentication.
|
||||
</h3>
|
||||
<div class="mt-3 max-w-xl text-sm text-gray-600">
|
||||
<p>
|
||||
When two factor authentication is enabled, you will be prompted for a secure, random
|
||||
token during authentication. You may retrieve this token from your phone's Google
|
||||
Authenticator application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="code">
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
Two factor authentication is now enabled. Scan the following QR code using your
|
||||
phone's authenticator application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<img :src="code?.svg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- @if(recoveryCodes) -->
|
||||
<div v-if="recoveryCodes" class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
Store these recovery codes in a secure password manager. They can be used to recover
|
||||
access to your account if your two factor authentication device is lost.
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div class="mt-4 grid max-w-xl gap-1 rounded-lg bg-gray-100 px-4 py-4 font-mono text-sm">
|
||||
@each(code in recoveryCodes)
|
||||
<div>
|
||||
{{ code }}
|
||||
</div>
|
||||
@endeach
|
||||
</div> -->
|
||||
<!-- @endif -->
|
||||
|
||||
<div class="flex justify-between">
|
||||
<!-- <form action="{{ route('UserController.fetchRecoveryCodes') }}" method="GET">
|
||||
<button type="submit" class="px-auto items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs
|
||||
font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none
|
||||
">
|
||||
Show Recovery Codes
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('UserController.disableTwoFactorAuthentication') }}" method="POST">
|
||||
<button type="submit" class="px-auto items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs
|
||||
font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none
|
||||
">
|
||||
Disable
|
||||
</button>
|
||||
</form> -->
|
||||
<BaseButton color="info" type="submit" label="Disable" />
|
||||
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</CardBox>
|
||||
|
||||
|
||||
</div>
|
||||
</SectionMain>
|
||||
|
|
|
@ -22,6 +22,26 @@ interface TransactionItem {
|
|||
business: string;
|
||||
}
|
||||
|
||||
export enum State {
|
||||
STATE_DISABLED = 0,
|
||||
STATE_CREATED = 1,
|
||||
STATE_ENABLED = 2,
|
||||
}
|
||||
|
||||
export const saveState = async (data) => {
|
||||
const url = '/api/twofactor_totp/settings/enable';
|
||||
|
||||
const resp = await axios.post(url, data);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
// Anfrage staet : 1
|
||||
|
||||
// ANtwort json:
|
||||
// state: 1
|
||||
// secret:"OX7IQ4OI3GXGFPHY"
|
||||
// qUrl:"https://odysseus.geologie.ac.at/apps/twofactor_totp/settings/enable"
|
||||
|
||||
export const MainService = defineStore('main', {
|
||||
state: () => ({
|
||||
/* User */
|
||||
|
@ -40,11 +60,13 @@ export const MainService = defineStore('main', {
|
|||
authors: [] as Array<Person>,
|
||||
// persons: [] as Array<Person>,
|
||||
datasets: [],
|
||||
files:[],
|
||||
files: [],
|
||||
|
||||
dataset: {} as Dataset,
|
||||
|
||||
menu: menu,
|
||||
|
||||
totpState: 0,
|
||||
}),
|
||||
actions: {
|
||||
// payload = authenticated user
|
||||
|
@ -113,6 +135,36 @@ export const MainService = defineStore('main', {
|
|||
});
|
||||
},
|
||||
|
||||
setState(state) {
|
||||
this.totpState = state;
|
||||
},
|
||||
|
||||
async create(): Promise<{ url: any; secret: any; svg: any }> {
|
||||
const { state, secret, url, svg } = await saveState({ state: State.STATE_CREATED });
|
||||
this.totpState = state;
|
||||
return { url, secret, svg };
|
||||
// .then(({ state, secret, qrUrl }) => {
|
||||
// this.totpState = state;
|
||||
// return { qrUrl, secret };
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// alert(error.message);
|
||||
// });
|
||||
},
|
||||
|
||||
async disable() {
|
||||
const { state } = await saveState({ state: State.STATE_DISABLED });
|
||||
this.totpState = state;
|
||||
},
|
||||
|
||||
async confirm(code: string) {
|
||||
const { state } = await saveState({
|
||||
state: State.STATE_ENABLED,
|
||||
code,
|
||||
});
|
||||
this.totpState = state;
|
||||
},
|
||||
|
||||
// fetchfiles(id) {
|
||||
// // sampleDataKey= authors or datasets
|
||||
// axios
|
||||
|
|
3
resources/js/utils/Close.svg
Normal file
3
resources/js/utils/Close.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
|
||||
<path d="M14 12.3L12.3 14 8 9.7 3.7 14 2 12.3 6.3 8 2 3.7 3.7 2 8 6.3 12.3 2 14 3.7 9.7 8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 170 B |
103
resources/js/utils/toast.css
Normal file
103
resources/js/utils/toast.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
|
||||
/* remember to import this scss file into your app */
|
||||
.toastify.dialogs {
|
||||
min-width: 200px;
|
||||
background: none;
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
box-shadow: 0 0 6px 0 var(--color-box-shadow);
|
||||
padding: 0 12px;
|
||||
margin-top: 45px;
|
||||
position: fixed;
|
||||
z-index: 10100;
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.toast-undo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-undo-button,
|
||||
.toast-close {
|
||||
position: static;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: 44px;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
white-space: nowrap;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: transparent;
|
||||
min-height: 0;
|
||||
|
||||
/* icon styling */
|
||||
&.toast-close {
|
||||
text-indent: 0;
|
||||
opacity: 0.4;
|
||||
border: none;
|
||||
min-height: 44px;
|
||||
margin-left: 10px;
|
||||
font-size: 0;
|
||||
|
||||
/* dark theme overrides for Nextcloud 25 and later */
|
||||
&::before {
|
||||
background-image: url('./Close.svg');
|
||||
content: ' ';
|
||||
filter: var(--background-invert-if-dark);
|
||||
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-undo-button {
|
||||
/* $margin: 3px; */
|
||||
/* margin: $margin; */
|
||||
/* height: calc(100% - 2 * #{$margin}); */
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.toastify-top {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
/* Toast with onClick callback */
|
||||
&.toast-with-click {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Various toasts types */
|
||||
&.toast-error {
|
||||
border-left: 3px solid var(--color-error);
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
&.toast-warning {
|
||||
border-left: 3px solid var(--color-warning);
|
||||
}
|
||||
|
||||
&.toast-success {
|
||||
border-left: 3px solid var(--color-success);
|
||||
}
|
||||
|
||||
&.toast-undo {
|
||||
border-left: 3px solid var(--color-success);
|
||||
}
|
||||
}
|
||||
|
||||
|
216
resources/js/utils/toast.ts
Normal file
216
resources/js/utils/toast.ts
Normal file
|
@ -0,0 +1,216 @@
|
|||
import Toastify from 'toastify-js';
|
||||
// import { t } from './utils/l10n.js';
|
||||
import './toast.css';
|
||||
|
||||
/**
|
||||
* Enum of available Toast types
|
||||
*/
|
||||
export enum ToastType {
|
||||
ERROR = 'toast-error',
|
||||
WARNING = 'toast-warning',
|
||||
INFO = 'toast-info',
|
||||
SUCCESS = 'toast-success',
|
||||
PERMANENT = 'toast-error',
|
||||
UNDO = 'toast-undo',
|
||||
}
|
||||
|
||||
/** @deprecated Use ToastAriaLive.OFF */
|
||||
export const TOAST_ARIA_LIVE_OFF = 'off';
|
||||
/** @deprecated Use ToastAriaLive.POLITE */
|
||||
export const TOAST_ARIA_LIVE_POLITE = 'polite';
|
||||
/** @deprecated Use ToastAriaLive.ASSERTIVE */
|
||||
export const TOAST_ARIA_LIVE_ASSERTIVE = 'assertive';
|
||||
|
||||
export enum ToastAriaLive {
|
||||
OFF = TOAST_ARIA_LIVE_OFF,
|
||||
POLITE = TOAST_ARIA_LIVE_POLITE,
|
||||
ASSERTIVE = TOAST_ARIA_LIVE_ASSERTIVE,
|
||||
}
|
||||
|
||||
/** Timeout in ms of a undo toast */
|
||||
export const TOAST_UNDO_TIMEOUT = 10000;
|
||||
/** Default timeout in ms of toasts */
|
||||
export const TOAST_DEFAULT_TIMEOUT = 4000;
|
||||
/** Timeout value to show a toast permanently */
|
||||
export const TOAST_PERMANENT_TIMEOUT = -1;
|
||||
|
||||
/**
|
||||
* Type of a toast
|
||||
* @see https://apvarun.github.io/toastify-js/
|
||||
* @notExported
|
||||
*/
|
||||
type Toast = ReturnType<typeof Toastify>;
|
||||
|
||||
export interface ToastOptions {
|
||||
/**
|
||||
* Defines the timeout in milliseconds after which the toast is closed. Set to -1 to have a persistent toast.
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* Set to true to allow HTML content inside of the toast text
|
||||
* @default false
|
||||
*/
|
||||
isHTML?: boolean;
|
||||
|
||||
/**
|
||||
* Set a type of {ToastType} to style the modal
|
||||
*/
|
||||
type?: ToastType;
|
||||
|
||||
/**
|
||||
* Provide a function that is called after the toast is removed
|
||||
*/
|
||||
onRemove?: () => void;
|
||||
|
||||
/**
|
||||
* Provide a function that is called when the toast is clicked
|
||||
*/
|
||||
onClick?: () => void;
|
||||
|
||||
/**
|
||||
* Make the toast closable
|
||||
*/
|
||||
close?: boolean;
|
||||
|
||||
/**
|
||||
* Specify the element to attach the toast element to (for testing)
|
||||
*/
|
||||
selector?: string;
|
||||
|
||||
/**
|
||||
* Whether the messages should be announced to screen readers.
|
||||
* See the following docs for an explanation when to use which:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
|
||||
*
|
||||
* By default, errors are announced assertive and other messages "polite".
|
||||
*/
|
||||
ariaLive?: ToastAriaLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a toast message
|
||||
*
|
||||
* @param data Message to be shown in the toast, any HTML is removed by default
|
||||
* @param options
|
||||
*/
|
||||
export function showMessage(data: string | Node, options?: ToastOptions): Toast {
|
||||
options = Object.assign(
|
||||
{
|
||||
timeout: TOAST_DEFAULT_TIMEOUT,
|
||||
isHTML: false,
|
||||
type: undefined,
|
||||
// An undefined selector defaults to the body element
|
||||
selector: undefined,
|
||||
onRemove: () => {},
|
||||
onClick: undefined,
|
||||
close: true,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (typeof data === 'string' && !options.isHTML) {
|
||||
// fime mae sure that text is extracted
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = data;
|
||||
data = element.innerText;
|
||||
}
|
||||
let classes = options.type ?? '';
|
||||
|
||||
if (typeof options.onClick === 'function') {
|
||||
classes += ' toast-with-click ';
|
||||
}
|
||||
|
||||
const isNode = data instanceof Node;
|
||||
|
||||
let ariaLive: ToastAriaLive = ToastAriaLive.POLITE;
|
||||
if (options.ariaLive) {
|
||||
ariaLive = options.ariaLive;
|
||||
} else if (options.type === ToastType.ERROR || options.type === ToastType.UNDO) {
|
||||
ariaLive = ToastAriaLive.ASSERTIVE;
|
||||
}
|
||||
|
||||
const toast = Toastify({
|
||||
[!isNode ? 'text' : 'node']: data,
|
||||
duration: options.timeout,
|
||||
callback: options.onRemove,
|
||||
onClick: options.onClick,
|
||||
close: options.close,
|
||||
gravity: 'top',
|
||||
selector: options.selector,
|
||||
position: 'right',
|
||||
backgroundColor: '',
|
||||
className: 'dialogs ' + classes,
|
||||
escapeMarkup: !options.isHTML,
|
||||
ariaLive,
|
||||
});
|
||||
|
||||
toast.showToast();
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
export default {
|
||||
updatableNotification: null,
|
||||
|
||||
getDefaultNotificationFunction: null,
|
||||
|
||||
/**
|
||||
* Shows a notification that disappears after x seconds, default is
|
||||
* 7 seconds
|
||||
*
|
||||
* @param {string} text Message to show
|
||||
* @param {Array} [options] options array
|
||||
* @param {number} [options.timeout=7] timeout in seconds, if this is 0 it will show the message permanently
|
||||
* @param {boolean} [options.isHTML=false] an indicator for HTML notifications (true) or text (false)
|
||||
* @param {string} [options.type] notification type
|
||||
* @return {JQuery} the toast element
|
||||
*/
|
||||
showTemporary(text, options = { timeout: 3000 }) {
|
||||
options = options || {};
|
||||
options.timeout = options.timeout || TOAST_DEFAULT_TIMEOUT;
|
||||
const toast = showMessage(text, options);
|
||||
toast.toastElement.toastify = toast;
|
||||
// return $(toast.toastElement)
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a toast message with error styling
|
||||
*
|
||||
* @param text Message to be shown in the toast, any HTML is removed by default
|
||||
* @param options
|
||||
*/
|
||||
showError(text: string, options?: ToastOptions): Toast {
|
||||
return showMessage(text, { ...options, type: ToastType.ERROR });
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a toast message with warning styling
|
||||
*
|
||||
* @param text Message to be shown in the toast, any HTML is removed by default
|
||||
* @param options
|
||||
*/
|
||||
showWarning(text: string, options?: ToastOptions): Toast {
|
||||
return showMessage(text, { ...options, type: ToastType.WARNING });
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a toast message with info styling
|
||||
*
|
||||
* @param text Message to be shown in the toast, any HTML is removed by default
|
||||
* @param options
|
||||
*/
|
||||
showInfo(text: string, options?: ToastOptions): Toast {
|
||||
return showMessage(text, { ...options, type: ToastType.INFO });
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a toast message with success styling
|
||||
*
|
||||
* @param text Message to be shown in the toast, any HTML is removed by default
|
||||
* @param options
|
||||
*/
|
||||
showSuccess(text: string, options?: ToastOptions): Toast {
|
||||
return showMessage(text, { ...options, type: ToastType.SUCCESS });
|
||||
},
|
||||
};
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue