Update Vue front-end according to the new API definition and paths

This commit is contained in:
Bubka 2021-10-09 19:23:24 +02:00
parent 83f7370b57
commit 184237697b
51 changed files with 472 additions and 358 deletions

View File

@ -1,8 +1,6 @@
import Vue from 'vue'
import Errors from './FormErrors'
// import { deepCopy } from './util'
class Form {
/**
* Create a new form instance.
@ -14,7 +12,7 @@ class Form {
this.isDisabled = false
// this.successful = false
this.errors = new Errors()
// this.originalData = deepCopy(data)
this.originalData = this.deepCopy(data)
Object.assign(this, data)
}
@ -30,6 +28,31 @@ class Form {
})
}
/**
* Update original form data.
*/
setOriginal () {
Object.keys(this)
.filter(key => !Form.ignore.includes(key))
.forEach(key => {
this.originalData[key] = this.deepCopy(this[key])
})
}
/**
* Fill form data.
*
* @param {Object} data
*/
fillWithKeyValueObject (data) {
this.keys().forEach(key => {
const keyValueObject = data.find(s => s.key === key.toString())
if(keyValueObject != undefined) {
this[key] = keyValueObject.value
}
})
}
/**
* Get the form data.
*
@ -83,7 +106,7 @@ class Form {
Object.keys(this)
.filter(key => !Form.ignore.includes(key))
.forEach(key => {
this[key] = deepCopy(this.originalData[key])
this[key] = this.deepCopy(this.originalData[key])
})
}
@ -265,6 +288,26 @@ class Form {
this.errors.clear(event.target.name)
}
}
/**
* Deep copy the given object.
*
* @param {Object} obj
* @return {Object}
*/
deepCopy (obj) {
if (obj === null || typeof obj !== 'object') {
return obj
}
const copy = Array.isArray(obj) ? [] : {}
Object.keys(obj).forEach(key => {
copy[key] = this.deepCopy(obj[key])
})
return copy
}
}
Form.routes = {}

View File

@ -1,6 +1,6 @@
<template>
<div class="field">
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="form[fieldName]">
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="form[fieldName]" v-on:change="$emit(fieldName, form[fieldName])" >
<label :for="fieldName" class="label" v-html="label"></label>
<p class="help" v-html="help" v-if="help"></p>
</div>

View File

@ -3,7 +3,7 @@
<label class="label" v-html="label"></label>
<div class="control">
<div class="select">
<select v-model="form[fieldName]">
<select v-model="form[fieldName]" v-on:change="$emit(fieldName, form[fieldName])">
<option v-for="option in options" :value="option.value">{{ option.text }}</option>
</select>
</div>

View File

@ -3,7 +3,7 @@
<label class="label" v-html="label"></label>
<div class="is-toggle buttons">
<label class="button is-dark" :disabled="isDisabled" v-for="choice in choices" :class="{ 'is-link' : form[fieldName] === choice.value }">
<input type="radio" class="is-hidden" :checked="form[fieldName] === choice.value" :value="choice.value" v-model="form[fieldName]" :disabled="isDisabled" />
<input type="radio" class="is-hidden" :checked="form[fieldName] === choice.value" :value="choice.value" v-model="form[fieldName]" v-on:change="$emit(fieldName, form[fieldName])" :disabled="isDisabled" />
<font-awesome-icon :icon="['fas', choice.icon]" v-if="choice.icon" class="mr-3" /> {{ choice.text }}
</label>
</div>

View File

@ -5,109 +5,118 @@
</figure>
<p class="is-size-4 has-text-grey-light has-ellipsis">{{ internal_service }}</p>
<p class="is-size-6 has-text-grey has-ellipsis">{{ internal_account }}</p>
<p class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => token.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedToken }}</p>
<ul class="dots" v-if="internal_otpType === 'totp'">
<p class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => password.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedOtp }}</p>
<ul class="dots" v-show="internal_otp_type === 'totp'">
<li v-for="n in 10"></li>
</ul>
<ul v-else-if="internal_otpType === 'hotp'">
<li>counter: {{ internal_hotpCounter }}</li>
<ul v-show="internal_otp_type === 'hotp'">
<li>counter: {{ internal_counter }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'TokenDisplayer',
name: 'OtpDisplayer',
data() {
return {
id: null,
token : '',
remainingTimeout: null,
firstDotToNextOneTimeout: null,
dotToDotInterval: null,
position: null,
totpTimestamp: null,
internal_otpType: '',
internal_id: null,
internal_otp_type: '',
internal_account: '',
internal_service: '',
internal_icon: '',
internal_hotpCounter: null,
internal_secret: null,
internal_digits: null,
internal_algorithm: null,
internal_period: null,
internal_counter: null,
internal_password : '',
internal_uri : '',
lastActiveDot: null,
dotToDotCounter: null,
remainingTimeout: null,
firstDotToNextOneTimeout: null,
dotToDotInterval: null
}
},
props: {
otp_type : String,
account : String,
algorithm : String,
digits : Number,
hotpCounter : null,
icon : String,
imageLink : String,
otpType : String,
qrcode : null,
secret : String,
secretIsBase32Encoded : Number,
service : String,
totpPeriod : null,
icon : String,
secret : String,
digits : Number,
algorithm : String,
period : null,
counter : null,
image : String,
qrcode : null,
secretIsBase32Encoded : Number,
uri : String
},
computed: {
displayedToken() {
return this.$root.appSettings.showTokenAsDot ? this.token.replace(/[0-9]/g, '●') : this.token
displayedOtp() {
const spacePosition = Math.ceil(this.internal_password.length / 2)
let pwd = this.internal_password.substr(0, spacePosition) + " " + this.internal_password.substr(spacePosition)
return this.$root.appSettings.showOtpAsDot ? pwd.replace(/[0-9]/g, '●') : pwd
}
},
mounted: function() {
this.getToken()
this.show()
},
methods: {
async getToken(id) {
async show(id) {
// 3 possible cases :
// - Trigger when user ask for a token of an existing account: the ID is provided so we fetch the account data
// - Trigger when user ask for an otp of an existing account: the ID is provided so we fetch the account data
// from db but without the uri.
// This prevent the uri (a sensitive data) to transit via http request unnecessarily. In this
// case this.otpType is sent by the backend.
// case this.otp_type is sent by the backend.
// - Trigger when user use the Quick Uploader and preview the account: No ID but we have an URI.
// - Trigger when user use the Advanced form and preview the account: We should have all OTP parameter
// to obtain a token, including Secret and otpType which are required
// to obtain an otp, including Secret and otp_type which are required
this.internal_service = this.service
this.internal_otp_type = this.otp_type
this.internal_account = this.account
this.internal_service = this.service
this.internal_icon = this.icon
this.internal_otpType = this.otpType
this.internal_hotpCounter = this.hotpCounter
this.internal_secret = this.secret
this.internal_digits = this.digits
this.internal_algorithm = this.algorithm
this.internal_period = this.period
this.internal_counter = this.counter
if( id ) {
this.id = id
const { data } = await this.axios.get('api/twofaccounts/' + this.id)
this.internal_id = id
const { data } = await this.axios.get('api/twofaccounts/' + this.internal_id)
this.internal_service = data.service
this.internal_account = data.account
this.internal_icon = data.icon
this.internal_otpType = data.otpType
this.internal_otp_type = data.otp_type
if( data.otpType === 'hotp' && data.hotpCounter ) {
this.internal_hotpCounter = data.hotpCounter
if( data.otp_type === 'hotp' && data.counter ) {
this.internal_counter = data.counter
}
}
// We force the otpType to be based on the uri
// We force the otp_type to be based on the uri
if( this.uri ) {
this.internal_otpType = this.uri.slice(0, 15 ).toLowerCase() === "otpauth://totp/" ? 'totp' : 'hotp';
this.internal_uri = this.uri
this.internal_otp_type = this.uri.slice(0, 15 ).toLowerCase() === "otpauth://totp/" ? 'totp' : 'hotp';
}
if( this.id || this.uri || this.secret ) { // minimun required vars to get a token from the backend
if( this.internal_id || this.uri || this.secret ) { // minimun required vars to get an otp from the backend
switch(this.internal_otpType) {
switch(this.internal_otp_type) {
case 'totp':
await this.getTOTP()
await this.startTotpLoop()
break;
case 'hotp':
await this.getHOTP()
@ -120,94 +129,125 @@
}
},
getOtp: async function() {
getTOTP: function() {
if(this.internal_id) {
const { data } = await this.axios.get('/api/twofaccounts/' + this.internal_id + '/otp')
return data
}
else if(this.internal_uri) {
const { data } = await this.axios.post('/api/twofaccounts/otp', {
uri: this.internal_uri
})
return data
}
else {
const { data } = await this.axios.post('/api/twofaccounts/otp', {
service : this.internal_service,
account : this.internal_account,
icon : this.internal_icon,
otp_type : this.internal_otp_type,
secret : this.internal_secret,
digits : this.internal_digits,
algorithm : this.internal_algorithm,
period : this.internal_period,
counter : this.internal_counter,
})
return data
}
},
this.dotToDotCounter = 0
startTotpLoop: async function() {
let otp = await this.getOtp()
this.axios.post('/api/twofaccounts/token', { id: this.id, otp: this.$props }).then(response => {
this.internal_password = otp.password
this.internal_otp_type = otp.otp_type
let generated_at = otp.generated_at
let period = otp.period
let spacePosition = Math.ceil(response.data.token.length / 2);
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition);
this.totpTimestamp = response.data.totpTimestamp; // the timestamp used to generate the token
this.position = this.totpTimestamp % response.data.totpPeriod // The position of the totp timestamp in the current period
let elapsedTimeInCurrentPeriod,
remainingTimeBeforeEndOfPeriod,
durationBetweenTwoDots,
durationFromFirstToNextDot,
dots
// Hide all dots
let dots = this.$el.querySelector('.dots');
// |<----period p----->|
// | | |
// |------- ··· ------------|--------|----------|---------->
// | | | |
// unix T0 Tp.start Tgen_at Tp.end
// | | |
// elapsedTimeInCurrentPeriod--|<------>| |
// (in ms) | | |
// | |
// | | || |
// | | |<-------->|--remainingTimeBeforeEndOfPeriod (for remainingTimeout)
// durationBetweenTwoDots-->|-|< ||
// (for dotToDotInterval) | | >||<---durationFromFirstToNextDot (for firstDotToNextOneTimeout)
// |
// |
// dotIndex
while (dots.querySelector('[data-is-active]')) {
dots.querySelector('[data-is-active]').removeAttribute('data-is-active');
// The elapsed time from the start of the period that contains the OTP generated_at timestamp and the OTP generated_at timestamp itself
elapsedTimeInCurrentPeriod = generated_at % period
// Switch off all dots
dots = this.$el.querySelector('.dots')
while (dots.querySelector('[data-is-active]')) {
dots.querySelector('[data-is-active]').removeAttribute('data-is-active');
}
// We determine the position of the closest dot next to the generated_at timestamp
let relativePosition = (elapsedTimeInCurrentPeriod * 10) / period
let dotIndex = (Math.floor(relativePosition) +1)
// We switch the dot on
this.lastActiveDot = dots.querySelector('li:nth-child(' + dotIndex + ')');
this.lastActiveDot.setAttribute('data-is-active', true);
// Main timeout that run until the end of the period
remainingTimeBeforeEndOfPeriod = period - elapsedTimeInCurrentPeriod
let self = this; // because of the setInterval/setTimeout closures
this.remainingTimeout = setTimeout(function() {
self.stopLoop()
self.startTotpLoop();
}, remainingTimeBeforeEndOfPeriod*1000);
// During the remainingTimeout countdown we have to show a next dot every durationBetweenTwoDots seconds
// except for the first next dot
durationBetweenTwoDots = period / 10 // we have 10 dots
durationFromFirstToNextDot = (Math.ceil(elapsedTimeInCurrentPeriod / durationBetweenTwoDots) * durationBetweenTwoDots) - elapsedTimeInCurrentPeriod
this.firstDotToNextOneTimeout = setTimeout(function() {
if( durationFromFirstToNextDot > 0 ) {
self.activateNextDot()
dotIndex += 1
}
// Activate the dot at the totp position
let relativePosition = (this.position * 10) / response.data.totpPeriod
let dotNumber = (Math.floor(relativePosition) +1)
this.lastActiveDot = dots.querySelector('li:nth-child(' + dotNumber + ')');
this.lastActiveDot.setAttribute('data-is-active', true);
// Main timeout which run all over the totpPeriod.
let remainingTimeBeforeEndOfPeriod = response.data.totpPeriod - this.position
let self = this; // because of the setInterval/setTimeout closures
this.remainingTimeout = setTimeout(function() {
self.stopLoop()
self.getTOTP();
}, remainingTimeBeforeEndOfPeriod*1000);
// During the remainingTimeout countdown we have to show a next dot every durationBetweenTwoDots seconds
// except for the first next dot
let durationBetweenTwoDots = response.data.totpPeriod / 10 // we have 10 dots
let firstDotTimeout = (Math.ceil(this.position / durationBetweenTwoDots) * durationBetweenTwoDots) - this.position
this.firstDotToNextOneTimeout = setTimeout(function() {
if( firstDotTimeout > 0 ) {
self.activeNextDot()
dotNumber += 1
}
self.dotToDotInterval = setInterval(function() {
self.dotToDotCounter += 1
self.activeNextDot()
dotNumber += 1
}, durationBetweenTwoDots*1000)
}, firstDotTimeout*1000)
})
.catch(error => {
this.$router.push({ name: 'genericError', params: { err: error.response } });
});
self.dotToDotInterval = setInterval(function() {
self.activateNextDot()
dotIndex += 1
}, durationBetweenTwoDots*1000)
}, durationFromFirstToNextDot*1000)
},
getHOTP: function() {
getHOTP: async function() {
this.axios.post('/api/twofaccounts/token', { id: this.id, otp: this.$props }).then(response => {
let spacePosition = Math.ceil(response.data.token.length / 2);
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition)
let otp = await this.getOtp()
// returned counter & uri are incremented
this.$emit('increment-hotp', { nextHotpCounter: response.data.hotpCounter, nextUri: response.data.uri })
})
.catch(error => {
this.$router.push({ name: 'genericError', params: { err: error.response } });
});
// returned counter & uri are incremented
this.$emit('increment-hotp', { nextHotpCounter: otp.counter, nextUri: otp.uri })
},
clearOTP: function() {
this.stopLoop()
this.id = this.remainingTimeout = this.dotToDotInterval = this.firstDotToNextOneTimeout = this.position = this.internal_hotpCounter = null
this.internal_service = this.internal_account = this.internal_icon = this.internal_otpType = ''
this.token = '... ...'
this.internal_id = this.remainingTimeout = this.dotToDotInterval = this.firstDotToNextOneTimeout = this.elapsedTimeInCurrentPeriod = this.internal_counter = null
this.internal_service = this.internal_account = this.internal_icon = this.internal_otp_type = ''
this.internal_password = '... ...'
try {
this.$el.querySelector('[data-is-active]').removeAttribute('data-is-active');
@ -220,7 +260,7 @@
stopLoop: function() {
if( this.internal_otpType === 'totp' ) {
if( this.internal_otp_type === 'totp' ) {
clearTimeout(this.remainingTimeout)
clearTimeout(this.firstDotToNextOneTimeout)
clearInterval(this.dotToDotInterval)
@ -228,9 +268,8 @@
},
activeNextDot: function() {
activateNextDot: function() {
if(this.lastActiveDot.nextSibling !== null) {
this.lastActiveDot.removeAttribute('data-is-active')
this.lastActiveDot.nextSibling.setAttribute('data-is-active', true)
this.lastActiveDot = this.lastActiveDot.nextSibling
@ -243,7 +282,7 @@
if(this.$root.appSettings.kickUserAfter == -1) {
this.appLogout()
}
else if(this.$root.appSettings.closeTokenOnCopy) {
else if(this.$root.appSettings.closeOtpOnCopy) {
this.$parent.isActive = false
this.clearOTP()
}

View File

@ -12,7 +12,7 @@ Vue.mixin({
async appLogout(evt) {
await this.axios.get('api/logout')
await this.axios.get('api/user/logout')
this.$storage.clear()
delete this.axios.defaults.headers.common['Authorization']

View File

@ -27,12 +27,12 @@ const router = new Router({
{ path: '/accounts', name: 'accounts', component: Accounts, meta: { requiresAuth: true }, alias: '/', props: true },
{ path: '/account/create', name: 'createAccount', component: CreateAccount, meta: { requiresAuth: true } },
{ path: '/account/edit/:twofaccountId', name: 'editAccount', component: EditAccount, meta: { requiresAuth: true } },
{ path: '/account/qrcode/:twofaccountId', name: 'showQRcode', component: QRcodeAccount, meta: { requiresAuth: true } },
{ path: '/account/:twofaccountId/edit', name: 'editAccount', component: EditAccount, meta: { requiresAuth: true } },
{ path: '/account/:twofaccountId/qrcode', name: 'showQRcode', component: QRcodeAccount, meta: { requiresAuth: true } },
{ path: '/groups', name: 'groups', component: Groups, meta: { requiresAuth: true }, props: true },
{ path: '/group/create', name: 'createGroup', component: CreateGroup, meta: { requiresAuth: true } },
{ path: '/group/edit/:groupId', name: 'editGroup', component: EditGroup, meta: { requiresAuth: true }, props: true },
{ path: '/group/:groupId/edit', name: 'editGroup', component: EditGroup, meta: { requiresAuth: true }, props: true },
{ path: '/settings', name: 'settings', component: Settings, meta: { requiresAuth: true } },

View File

@ -5,7 +5,7 @@
<div class="columns is-centered">
<div class="column is-one-third-tablet is-one-quarter-desktop is-one-quarter-widescreen is-one-quarter-fullhd">
<div class="columns is-multiline">
<div class="column is-full" v-for="group in groups" v-if="group.count > 0" :key="group.id">
<div class="column is-full" v-for="group in groups" v-if="group.twofaccounts_count > 0" :key="group.id">
<button :disabled="group.id == $root.appSettings.activeGroup" class="button is-fullwidth is-dark has-text-light is-outlined" @click="setActiveGroup(group.id)">{{ group.name }}</button>
</div>
</div>
@ -84,7 +84,7 @@
<div class="tfa-cell tfa-content is-size-3 is-size-4-mobile" @click.stop="showAccount(account)">
<div class="tfa-text has-ellipsis">
<img :src="'/storage/icons/' + account.icon" v-if="account.icon && $root.appSettings.showAccountsIcons">
{{ account.service }}<font-awesome-icon class="has-text-danger is-size-5 ml-2" v-if="$root.appSettings.useEncryption && account.isConsistent === false" :icon="['fas', 'exclamation-circle']" />
{{ displayService(account.service) }}<font-awesome-icon class="has-text-danger is-size-5 ml-2" v-if="$root.appSettings.useEncryption && account.isConsistent === false" :icon="['fas', 'exclamation-circle']" />
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
</div>
</div>
@ -178,7 +178,7 @@
</div>
<!-- modal -->
<modal v-model="showTwofaccountInModal">
<token-displayer ref="TokenDisplayer" ></token-displayer>
<otp-displayer ref="OtpDisplayer"></otp-displayer>
</modal>
</div>
</template>
@ -193,7 +193,7 @@
*
* The main view of 2FAuth that list all existing account recorded in DB.
* Available feature in this view :
* - Token generation
* - {{OTP}} generation
* - Account fetching :
* ~ Search
* ~ Filtering (by group)
@ -219,7 +219,7 @@
*/
import Modal from '../components/Modal'
import TokenDisplayer from '../components/TokenDisplayer'
import OtpDisplayer from '../components/OtpDisplayer'
import draggable from 'vuedraggable'
import Form from './../components/Form'
import objectEquals from 'object-equals'
@ -238,24 +238,27 @@
showGroupSelector: false,
moveAccountsTo: false,
form: new Form({
activeGroup: this.$root.appSettings.activeGroup,
value: this.$root.appSettings.activeGroup,
}),
}
},
computed: {
/**
* The actual list of displayed accounts
*/
filteredAccounts: {
get: function() {
return this.accounts.filter(
item => {
if( parseInt(this.$root.appSettings.activeGroup) > 0 ) {
return (item.service.toLowerCase().includes(this.search.toLowerCase()) ||
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
item.account.toLowerCase().includes(this.search.toLowerCase())) &&
(item.group_id == parseInt(this.$root.appSettings.activeGroup))
}
else {
return (item.service.toLowerCase().includes(this.search.toLowerCase()) ||
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
item.account.toLowerCase().includes(this.search.toLowerCase()))
}
}
@ -266,10 +269,16 @@
}
},
/**
* Returns whether or not the accounts should be displayed
*/
showAccounts() {
return this.accounts.length > 0 && !this.showGroupSwitch && !this.showGroupSelector ? true : false
},
/**
* Returns the name of a group
*/
activeGroupName() {
let g = this.groups.find(el => el.id === parseInt(this.$root.appSettings.activeGroup))
@ -303,14 +312,14 @@
// stop OTP generation on modal close
this.$on('modalClose', function() {
console.log('modalClose triggered')
this.$refs.TokenDisplayer.clearOTP()
this.$refs.OtpDisplayer.clearOTP()
});
},
components: {
Modal,
TokenDisplayer,
OtpDisplayer,
draggable,
},
@ -340,14 +349,7 @@
this.axios.get('api/twofaccounts').then(response => {
response.data.forEach((data) => {
accounts.push({
id : data.id,
service : data.service,
account : data.account ? data.account : '-',
icon : data.icon,
isConsistent : data.isConsistent,
group_id : data.group_id,
})
accounts.push(data)
})
if ( this.accounts.length > 0 && !objectEquals(accounts, this.accounts) && !forceRefresh ) {
@ -366,10 +368,10 @@
},
/**
* Show account with a generated token rotation
* Show account with a generated {{OTP}} rotation
*/
showAccount(account) {
// In Edit mode clicking an account do not show the tokenDisplayer but select the account
// In Edit mode clicking an account do not show the otpDisplayer but select the account
if(this.editMode) {
for (var i=0 ; i<this.selectedAccounts.length ; i++) {
@ -382,7 +384,7 @@
this.selectedAccounts.push(account.id)
}
else {
this.$refs.TokenDisplayer.getToken(account.id)
this.$refs.OtpDisplayer.show(account.id)
}
},
@ -391,7 +393,7 @@
*/
saveOrder() {
this.drag = false
this.axios.patch('/api/twofaccounts/reorder', {orderedIds: this.accounts.map(a => a.id)})
this.axios.post('/api/twofaccounts/reorder', {orderedIds: this.accounts.map(a => a.id)})
},
/**
@ -404,7 +406,7 @@
this.selectedAccounts.forEach(id => ids.push(id))
// Backend will delete all accounts at the same time
await this.axios.delete('/api/twofaccounts/batch', {data: ids} )
await this.axios.delete('/api/twofaccounts?ids=' + ids.join())
// we fetch the accounts again to prevent the js collection being
// desynchronize from the backend php collection
@ -413,7 +415,7 @@
},
/**
* Move accounts selected from the Edit mode to another group
* Move accounts selected from the Edit mode to another group or withdraw them
*/
async moveAccounts() {
@ -421,7 +423,11 @@
this.selectedAccounts.forEach(id => accountsIds.push(id))
// Backend will associate all accounts with the selected group in the same move
await this.axios.patch('/api/group/accounts', {accountsIds: accountsIds, groupId: this.moveAccountsTo} )
// or withdraw the accounts if destination is 'no group' (id = 0)
if(this.moveAccountsTo === 0) {
await this.axios.patch('/api/twofaccounts/withdraw?ids=' + accountsIds.join() )
}
else await this.axios.post('/api/groups/' + this.moveAccountsTo + '/assign', {ids: accountsIds} )
// we fetch the accounts again to prevent the js collection being
// desynchronize from the backend php collection
@ -439,12 +445,7 @@
this.axios.get('api/groups').then(response => {
response.data.forEach((data) => {
groups.push({
id : data.id,
name : data.name,
isActive: data.isActive,
count: data.twofaccounts_count
})
groups.push(data)
})
if ( !objectEquals(groups, this.groups) ) {
@ -460,10 +461,12 @@
*/
setActiveGroup(id) {
this.form.activeGroup = this.$root.appSettings.activeGroup = id
// In memomry saving
this.form.value = this.$root.appSettings.activeGroup = id
// In db saving if the user set 2FAuth to memorize the active group
if( this.$root.appSettings.rememberActiveGroup ) {
this.form.post('/api/settings/options', {returnError: true})
this.form.put('/api/settings/activeGroup', {returnError: true})
.then(response => {
// everything's fine
})
@ -515,6 +518,13 @@
this.editMode = state
this.$parent.showToolbar = state
},
/**
*
*/
displayService(service) {
return service ? service : this.$t('twofaccounts.no_service')
}
}
};

View File

@ -90,6 +90,8 @@
},
/**
* Push a decoded URI to the Create form
*
* The basicQRcodeReader option is Off, so qrcode decoding has already be done by vue-qrcode-reader, whether
* from livescan or file input.
* We simply check the uri validity to prevent useless push to the Create form, but the form will check uri validity too.

View File

@ -23,7 +23,7 @@
<router-link :to="{ name: 'editGroup', params: { id: group.id, name: group.name }}" class="tag is-dark">
{{ $t('commons.rename') }}
</router-link>
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey">{{ group.count }} {{ $t('twofaccounts.accounts') }}</span>
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey">{{ group.twofaccounts_count }} {{ $t('twofaccounts.accounts') }}</span>
</div>
<div class="mt-2 is-size-7 is-pulled-right" v-if="groups.length > 0">
{{ $t('groups.deleting_group_does_not_delete_accounts')}}
@ -38,7 +38,7 @@
<vue-footer :showButtons="true">
<!-- close button -->
<p class="control">
<router-link :to="{ name: 'accounts' }" class="button is-dark is-rounded" @click="">{{ $t('commons.close') }}</router-link>
<router-link :to="{ name: 'accounts', params: { toRefresh: true } }" class="button is-dark is-rounded">{{ $t('commons.close') }}</router-link>
</p>
</vue-footer>
</div>
@ -72,6 +72,9 @@
methods: {
/**
* Get all groups from backend
*/
async fetchGroups() {
this.isFetching = true
@ -80,14 +83,11 @@
const groups = []
response.data.forEach((data) => {
groups.push({
id : data.id,
name : data.name,
count: data.twofaccounts_count
})
groups.push(data)
})
// Remove the pseudo 'All' group
// Remove the 'All' pseudo group from the collection
// and push it the TheAllGroup
this.TheAllGroup = groups.shift()
this.groups = groups
@ -96,6 +96,9 @@
this.isFetching = false
},
/**
* Delete a group (after confirmation)
*/
deleteGroup(id) {
if(confirm(this.$t('groups.confirm.delete'))) {
this.axios.delete('/api/groups/' + id)
@ -104,10 +107,9 @@
this.groups = this.groups.filter(a => a.id !== id)
// Reset persisted group filter to 'All' (groupId=0)
// (backend will save to change automatically)
if( parseInt(this.$root.appSettings.activeGroup) === id ) {
this.axios.post('/api/settings/options', { activeGroup: 0 }).then(response => {
this.$root.appSettings.activeGroup = 0
})
this.$root.appSettings.activeGroup = 0
}
}
}
@ -115,8 +117,8 @@
},
beforeRouteLeave(to, from, next) {
// reinject the 'All' pseudo group before refreshing the localstorage
this.groups.unshift(this.TheAllGroup)
// Refresh localstorage
this.$storage.set('groups', this.groups)
next()

View File

@ -96,7 +96,8 @@
methods: {
/**
* Send the submitted QR code to the backend for decoding then push ser to the create form
* Upload the submitted QR code file to the backend for decoding, then route the user
* to the Create form with decoded URI to prefill the form
*/
async submitQrCode() {
@ -106,7 +107,7 @@
const { data } = await this.form.upload('/api/qrcode/decode', imgdata)
this.$router.push({ name: 'createAccount', params: { decodedUri: data.uri } });
this.$router.push({ name: 'createAccount', params: { decodedUri: data.data } });
},
/**

View File

@ -37,10 +37,10 @@
handleSubmit(e) {
e.preventDefault()
this.form.post('/api/login', {returnError: true})
this.form.post('/api/user/login', {returnError: true})
.then(response => {
localStorage.setItem('user',response.data.message.name)
localStorage.setItem('jwt',response.data.message.token)
localStorage.setItem('user',response.data.name)
localStorage.setItem('jwt',response.data.token)
if (localStorage.getItem('jwt') != null){
this.$router.push({ name: 'accounts', params: { toRefresh: true } })
@ -66,13 +66,13 @@
}
next(async vm => {
const { data } = await vm.axios.post('api/checkuser')
const { data } = await vm.axios.get('api/user/name')
if( !data.username ) {
return next({ name: 'register' });
if( data.name ) {
vm.username = data.name
}
else {
vm.username = data.username
return next({ name: 'register' });
}
});

View File

@ -31,10 +31,10 @@
async handleSubmit(e) {
e.preventDefault()
this.form.post('/api/register', {returnError: true})
this.form.post('/api/user', {returnError: true})
.then(response => {
localStorage.setItem('user',response.data.message.name)
localStorage.setItem('jwt',response.data.message.token)
localStorage.setItem('user',response.data.name)
localStorage.setItem('jwt',response.data.token)
if (localStorage.getItem('jwt') != null){
this.$router.push({ name: 'accounts', params: { toRefresh: true } })
@ -42,7 +42,7 @@
})
.catch(error => {
console.log(error.response)
if( error.response.status === 422 && error.response.data.errors.taken ) {
if( error.response.status === 422 && error.response.data.errors.name ) {
this.$notify({ type: 'is-danger', text: this.$t('errors.already_one_user_registered') + ' ' + this.$t('errors.cannot_register_more_user'), duration:-1 })
}

View File

@ -23,10 +23,10 @@
handleSubmit(e) {
e.preventDefault()
this.form.post('/api/password/email', {returnError: true})
this.form.post('/api/user/password/lost', {returnError: true})
.then(response => {
this.$notify({ type: 'is-success', text: response.data.status, duration:-1 })
this.$notify({ type: 'is-success', text: response.data.message, duration:-1 })
})
.catch(error => {
if( error.response.data.requestFailed ) {

View File

@ -36,10 +36,10 @@
handleSubmit(e) {
e.preventDefault()
this.form.post('/api/password/reset', {returnError: true})
this.form.post('/api/user/password/reset', {returnError: true})
.then(response => {
this.$notify({ type: 'is-success', text: response.data.status, duration:-1 })
this.$notify({ type: 'is-success', text: response.data.message, duration:-1 })
})
.catch(error => {
if( error.response.data.resetFailed ) {

View File

@ -25,7 +25,7 @@
},
async mounted() {
const { data } = await this.form.get('/api/settings/account')
const { data } = await this.form.get('/api/user')
this.form.fill(data)
},
@ -34,10 +34,9 @@
handleSubmit(e) {
e.preventDefault()
this.form.patch('/api/settings/account', {returnError: true})
this.form.put('/api/user', {returnError: true})
.then(response => {
this.$notify({ type: 'is-success', text: response.data.message })
this.$notify({ type: 'is-success', text: this.$t('auth.forms.profile_saved') })
})
.catch(error => {
if( error.response.status === 400 ) {

View File

@ -1,37 +1,38 @@
<template>
<form-wrapper>
<form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
<!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> -->
<form>
<h4 class="title is-4">{{ $t('settings.general') }}</h4>
<!-- Language -->
<form-select :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
<form-select v-on:lang="saveSetting('lang', $event)" :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
<!-- display mode -->
<form-toggle :choices="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
<form-toggle v-on:displayMode="saveSetting('displayMode', $event)" :choices="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
<!-- show icon -->
<form-checkbox :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
<form-checkbox v-on:showAccountsIcons="saveSetting('showAccountsIcons', $event)" :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
<h4 class="title is-4 pt-4">{{ $t('groups.groups') }}</h4>
<!-- default group -->
<form-select :options="groups" :form="form" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" />
<form-select v-on:defaultGroup="saveSetting('defaultGroup', $event)" :options="groups" :form="form" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" />
<!-- retain active group -->
<form-checkbox :form="form" fieldName="rememberActiveGroup" :label="$t('settings.forms.remember_active_group.label')" :help="$t('settings.forms.remember_active_group.help')" />
<form-checkbox v-on:rememberActiveGroup="saveSetting('rememberActiveGroup', $event)" :form="form" fieldName="rememberActiveGroup" :label="$t('settings.forms.remember_active_group.label')" :help="$t('settings.forms.remember_active_group.help')" />
<h4 class="title is-4 pt-4">{{ $t('settings.security') }}</h4>
<!-- auto lock -->
<form-select :options="kickUserAfters" :form="form" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
<form-select v-on:kickUserAfter="saveSetting('kickUserAfter', $event)" :options="kickUserAfters" :form="form" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
<!-- protect db -->
<form-checkbox :form="form" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
<!-- token as dot -->
<form-checkbox :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
<!-- close token on copy -->
<form-checkbox :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
<form-checkbox v-on:useEncryption="saveSetting('useEncryption', $event)" :form="form" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
<!-- otp as dot -->
<form-checkbox v-on:showOtpAsDot="saveSetting('showOtpAsDot', $event)" :form="form" fieldName="showOtpAsDot" :label="$t('settings.forms.show_otp_as_dot.label')" :help="$t('settings.forms.show_otp_as_dot.help')" />
<!-- close otp on copy -->
<form-checkbox v-on:closeOtpOnCopy="saveSetting('closeOtpOnCopy', $event)" :form="form" fieldName="closeOtpOnCopy" :label="$t('settings.forms.close_otp_on_copy.label')" :help="$t('settings.forms.close_otp_on_copy.help')" />
<h4 class="title is-4 pt-4">{{ $t('settings.data_input') }}</h4>
<!-- basic qrcode -->
<form-checkbox :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
<form-checkbox v-on:useBasicQrcodeReader="saveSetting('useBasicQrcodeReader', $event)" :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
<!-- direct capture -->
<form-checkbox :form="form" fieldName="useDirectCapture" :label="$t('settings.forms.useDirectCapture.label')" :help="$t('settings.forms.useDirectCapture.help')" />
<form-checkbox v-on:useDirectCapture="saveSetting('useDirectCapture', $event)" :form="form" fieldName="useDirectCapture" :label="$t('settings.forms.useDirectCapture.label')" :help="$t('settings.forms.useDirectCapture.help')" />
<!-- default capture mode -->
<form-select :options="captureModes" :form="form" fieldName="defaultCaptureMode" :label="$t('settings.forms.defaultCaptureMode.label')" :help="$t('settings.forms.defaultCaptureMode.help')" />
<form-select v-on:defaultCaptureMode="saveSetting('defaultCaptureMode', $event)" :options="captureModes" :form="form" fieldName="defaultCaptureMode" :label="$t('settings.forms.defaultCaptureMode.label')" :help="$t('settings.forms.defaultCaptureMode.help')" />
</form>
</form-wrapper>
</template>
@ -61,8 +62,8 @@
return {
form: new Form({
lang: '',
showTokenAsDot: null,
closeTokenOnCopy: null,
showOtpAsDot: null,
closeOtpOnCopy: null,
useBasicQrcodeReader: null,
showAccountsIcons: null,
displayMode: '',
@ -84,7 +85,7 @@
],
kickUserAfters: [
{ text: this.$t('settings.forms.never'), value: '0' },
{ text: this.$t('settings.forms.on_token_copy'), value: '-1' },
{ text: this.$t('settings.forms.on_otp_copy'), value: '-1' },
{ text: this.$t('settings.forms.1_minutes'), value: '1' },
{ text: this.$t('settings.forms.5_minutes'), value: '5' },
{ text: this.$t('settings.forms.10_minutes'), value: '10' },
@ -105,28 +106,46 @@
}
},
mounted() {
this.form.fill(this.$root.appSettings)
async mounted() {
const { data } = await this.form.get('/api/settings')
this.form.fillWithKeyValueObject(data)
this.form.lang = this.$root.$i18n.locale
this.form.setOriginal()
this.fetchGroups()
},
methods : {
handleSubmit(e) {
e.preventDefault()
console.log(e)
this.form.post('/api/settings/options', {returnError: false})
.then(response => {
// this.form.post('/api/settings/options', {returnError: false})
// .then(response => {
this.$notify({ type: 'is-success', text: response.data.message })
// this.$notify({ type: 'is-success', text: response.data.message })
if(response.data.settings.lang !== this.$root.$i18n.locale) {
// if(response.data.settings.lang !== this.$root.$i18n.locale) {
// this.$router.go()
// }
// else {
// this.$root.appSettings = response.data.settings
// }
// });
},
saveSetting(settingName, event) {
this.axios.put('/api/settings/' + settingName, { value: event }).then(response => {
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
if(settingName === 'lang' && response.data.value !== this.$root.$i18n.locale) {
this.$router.go()
}
else {
this.$root.appSettings = response.data.settings
this.$root.appSettings[response.data.key] = response.data.value
}
});
})
},
fetchGroups() {

View File

@ -28,7 +28,7 @@
handleSubmit(e) {
e.preventDefault()
this.form.patch('/api/settings/password', {returnError: true})
this.form.patch('/api/user/password', {returnError: true})
.then(response => {
this.$notify({ type: 'is-success', text: response.data.message })

View File

@ -10,8 +10,8 @@
<font-awesome-icon :icon="['fas', 'image']" size="2x" />
</label>
<button class="delete delete-icon-button is-medium" v-if="tempIcon" @click.prevent="deleteIcon"></button>
<token-displayer ref="QuickFormTokenDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
</token-displayer>
<otp-displayer ref="QuickFormOtpDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
</otp-displayer>
</div>
</div>
<div class="columns is-mobile" v-if="form.errors.any()">
@ -80,8 +80,8 @@
</div>
<field-error :form="form" field="icon" class="help-for-file" />
<!-- otp type -->
<form-toggle class="has-uppercased-button" :form="form" :choices="otpTypes" fieldName="otpType" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
<div v-if="form.otpType">
<form-toggle class="has-uppercased-button" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
<div v-if="form.otp_type">
<!-- secret -->
<label class="label" v-html="$t('twofaccounts.forms.secret.label')"></label>
<div class="field has-addons">
@ -109,15 +109,15 @@
<!-- algorithm -->
<form-toggle :form="form" :choices="algorithms" fieldName="algorithm" :label="$t('twofaccounts.forms.algorithm.label')" :help="$t('twofaccounts.forms.algorithm.help')" />
<!-- TOTP period -->
<form-field v-if="form.otpType === 'totp'" :form="form" fieldName="totpPeriod" inputType="text" :label="$t('twofaccounts.forms.totpPeriod.label')" :placeholder="$t('twofaccounts.forms.totpPeriod.placeholder')" :help="$t('twofaccounts.forms.totpPeriod.help')" />
<form-field v-if="form.otp_type === 'totp'" :form="form" fieldName="period" inputType="text" :label="$t('twofaccounts.forms.period.label')" :placeholder="$t('twofaccounts.forms.period.placeholder')" :help="$t('twofaccounts.forms.period.help')" />
<!-- HOTP counter -->
<form-field v-if="form.otpType === 'hotp'" :form="form" fieldName="hotpCounter" inputType="text" :label="$t('twofaccounts.forms.hotpCounter.label')" :placeholder="$t('twofaccounts.forms.hotpCounter.placeholder')" :help="$t('twofaccounts.forms.hotpCounter.help')" />
<form-field v-if="form.otp_type === 'hotp'" :form="form" fieldName="counter" inputType="text" :label="$t('twofaccounts.forms.counter.label')" :placeholder="$t('twofaccounts.forms.counter.placeholder')" :help="$t('twofaccounts.forms.counter.help')" />
</div>
<vue-footer :showButtons="true">
<p class="control">
<v-button :isLoading="form.isBusy" class="is-rounded" >{{ $t('commons.create') }}</v-button>
</p>
<p class="control" v-if="form.otpType && form.secret">
<p class="control" v-if="form.otp_type && form.secret">
<button type="button" class="button is-success is-rounded" @click="previewAccount">{{ $t('twofaccounts.forms.test') }}</button>
</p>
<p class="control">
@ -127,8 +127,8 @@
</form>
<!-- modal -->
<modal v-model="ShowTwofaccountInModal">
<token-displayer ref="AdvancedFormTokenDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
</token-displayer>
<otp-displayer ref="AdvancedFormOtpDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
</otp-displayer>
</modal>
</form-wrapper>
</div>
@ -147,7 +147,7 @@
* ~ A qrcode can be used to automatically fill the form
* ~ If an 'image' parameter is embeded in the qrcode, the remote image is downloaded and preset in the icon field
*
* Both design use the tokenDisplayer component to preview the account with a token rotation.
* Both design use the otpDisplayer component to preview the account with an otp rotation.
*
* input : [optional, for the Quick Form] an URI previously decoded by the Start view
* submit : post account data to php backend to create the account
@ -155,7 +155,7 @@
import Modal from '../../components/Modal'
import Form from './../../components/Form'
import TokenDisplayer from '../../components/TokenDisplayer'
import OtpDisplayer from '../../components/OtpDisplayer'
export default {
data() {
@ -167,19 +167,18 @@
form: new Form({
service: '',
account: '',
otpType: '',
uri: '',
otp_type: '',
icon: '',
secret: '',
secretIsBase32Encoded: 0,
algorithm: '',
digits: null,
hotpCounter: null,
totpPeriod: null,
imageLink: '',
counter: null,
period: null,
image: '',
qrcode: null,
}),
otpTypes: [
otp_types: [
{ text: 'TOTP', value: 'totp' },
{ text: 'HOTP', value: 'hotp' },
],
@ -206,7 +205,7 @@
watch: {
tempIcon: function(val) {
if( this.showQuickForm ) {
this.$refs.QuickFormTokenDisplayer.internal_icon = val
this.$refs.QuickFormOtpDisplayer.internal_icon = val
}
},
},
@ -235,13 +234,13 @@
// stop TOTP generation on modal close
this.$on('modalClose', function() {
this.$refs.AdvancedFormTokenDisplayer.stopLoop()
this.$refs.AdvancedFormOtpDisplayer.stopLoop()
});
},
components: {
Modal,
TokenDisplayer,
OtpDisplayer,
},
methods: {
@ -259,12 +258,12 @@
},
previewAccount() {
this.$refs.AdvancedFormTokenDisplayer.getToken()
this.$refs.AdvancedFormOtpDisplayer.show()
},
cancelCreation: function() {
if( this.form.service && this.form.uri ) {
if( this.form.service ) {
if( confirm(this.$t('twofaccounts.confirm.cancel')) === false ) {
return
}
@ -286,11 +285,10 @@
const { data } = await this.form.upload('/api/qrcode/decode', imgdata)
// Then the otp described by the uri
this.axios.post('/api/twofaccounts/preview', { uri: data.uri }).then(response => {
this.axios.post('/api/twofaccounts/preview', { uri: data.data }).then(response => {
this.form.fill(response.data)
this.form.secretIsBase32Encoded = 1
this.tempIcon = response.data.icon ? response.data.icon : null
this.form.uri = '' // we don't want the uri because the user can change any otp parameter in the form
})
},
@ -302,15 +300,15 @@
let imgdata = new FormData();
imgdata.append('icon', this.$refs.iconInput.files[0]);
const { data } = await this.form.upload('/api/icon/upload', imgdata)
const { data } = await this.form.upload('/api/icons', imgdata)
this.tempIcon = data;
this.tempIcon = data.filename;
},
deleteIcon(event) {
if(this.tempIcon) {
this.axios.delete('/api/icon/delete/' + this.tempIcon)
this.axios.delete('/api/icons/' + this.tempIcon)
this.tempIcon = ''
}
},
@ -320,8 +318,7 @@
// the component.
// This could desynchronized the HOTP verification server and our local counter if the user never verified the HOTP but this
// is acceptable (and HOTP counter can be edited by the way)
this.form.hotpCounter = payload.nextHotpCounter
this.form.uri = payload.nextUri
this.form.counter = payload.nextHotpCounter
},
},

View File

@ -26,8 +26,8 @@
</div>
<field-error :form="form" field="icon" class="help-for-file" />
<!-- otp type -->
<form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otpTypes" fieldName="otpType" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
<div v-if="form.otpType">
<form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
<div v-if="form.otp_type">
<!-- secret -->
<label class="label" v-html="$t('twofaccounts.forms.secret.label')"></label>
<div class="field has-addons">
@ -55,33 +55,33 @@
<!-- algorithm -->
<form-toggle :form="form" :choices="algorithms" fieldName="algorithm" :label="$t('twofaccounts.forms.algorithm.label')" :help="$t('twofaccounts.forms.algorithm.help')" />
<!-- TOTP period -->
<form-field v-if="form.otpType === 'totp'" :form="form" fieldName="totpPeriod" inputType="text" :label="$t('twofaccounts.forms.totpPeriod.label')" :placeholder="$t('twofaccounts.forms.totpPeriod.placeholder')" :help="$t('twofaccounts.forms.totpPeriod.help')" />
<form-field v-if="form.otp_type === 'totp'" :form="form" fieldName="period" inputType="text" :label="$t('twofaccounts.forms.period.label')" :placeholder="$t('twofaccounts.forms.period.placeholder')" :help="$t('twofaccounts.forms.period.help')" />
<!-- HOTP counter -->
<div v-if="form.otpType === 'hotp'">
<div v-if="form.otp_type === 'hotp'">
<div class="field" style="margin-bottom: 0.5rem;">
<label class="label">{{ $t('twofaccounts.forms.hotpCounter.label') }}</label>
<label class="label">{{ $t('twofaccounts.forms.counter.label') }}</label>
</div>
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" placeholder="" v-model="form.hotpCounter" :disabled="hotpCounterIsLocked" />
<input class="input" type="text" placeholder="" v-model="form.counter" :disabled="counterIsLocked" />
</div>
<div class="control" v-if="hotpCounterIsLocked">
<a class="button is-dark field-lock" @click="hotpCounterIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
<div class="control" v-if="counterIsLocked">
<a class="button is-dark field-lock" @click="counterIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
<span class="icon">
<font-awesome-icon :icon="['fas', 'lock']" />
</span>
</a>
</div>
<div class="control" v-else>
<a class="button is-dark field-unlock" @click="hotpCounterIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
<a class="button is-dark field-unlock" @click="counterIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
<span class="icon has-text-danger">
<font-awesome-icon :icon="['fas', 'lock-open']" />
</span>
</a>
</div>
</div>
<field-error :form="form" field="uri" class="help-for-file" />
<p class="help" v-html="$t('twofaccounts.forms.hotpCounter.help_lock')"></p>
<field-error :form="form" field="counter" />
<p class="help" v-html="$t('twofaccounts.forms.counter.help_lock')"></p>
</div>
</div>
<!-- form buttons -->
@ -89,7 +89,7 @@
<p class="control">
<v-button :isLoading="form.isBusy" class="is-rounded" >{{ $t('commons.save') }}</v-button>
</p>
<p class="control" v-if="form.otpType && form.secret">
<p class="control" v-if="form.otp_type && form.secret">
<button type="button" class="button is-success is-rounded" @click="previewAccount">{{ $t('twofaccounts.forms.test') }}</button>
</p>
<p class="control">
@ -99,8 +99,8 @@
</form>
<!-- modal -->
<modal v-model="ShowTwofaccountInModal">
<token-displayer ref="AdvancedFormTokenDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
</token-displayer>
<otp-displayer ref="AdvancedFormOtpDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
</otp-displayer>
</modal>
</form-wrapper>
</template>
@ -109,30 +109,30 @@
import Modal from '../../components/Modal'
import Form from './../../components/Form'
import TokenDisplayer from '../../components/TokenDisplayer'
import OtpDisplayer from '../../components/OtpDisplayer'
export default {
data() {
return {
ShowTwofaccountInModal : false,
hotpCounterIsLocked: true,
counterIsLocked: true,
twofaccountExists: false,
tempIcon: '',
form: new Form({
service: '',
account: '',
otpType: '',
otp_type: '',
uri: '',
icon: '',
secret: '',
secretIsBase32Encoded: null,
algorithm: '',
digits: null,
hotpCounter: null,
totpPeriod: null,
imageLink: '',
counter: null,
period: null,
image: '',
}),
otpTypes: [
otp_types: [
{ text: 'TOTP', value: 'totp' },
{ text: 'HOTP', value: 'hotp' },
],
@ -161,7 +161,7 @@
// stop TOTP generation on modal close
this.$on('modalClose', function() {
this.$refs.AdvancedFormTokenDisplayer.stopLoop()
this.$refs.AdvancedFormOtpDisplayer.stopLoop()
});
},
@ -171,18 +171,16 @@
components: {
Modal,
TokenDisplayer,
OtpDisplayer,
},
methods: {
async getAccount () {
const { data } = await this.axios.get('/api/twofaccounts/' + this.$route.params.twofaccountId + '/withSensitive')
const { data } = await this.axios.get('/api/twofaccounts/' + this.$route.params.twofaccountId)
this.form.fill(data)
this.form.secretIsBase32Encoded = 1
this.form.uri = '' // we don't want the uri because the user can change any otp parameter in the form
this.twofaccountExists = true
// set account icon as temp icon
@ -212,7 +210,7 @@
},
previewAccount() {
this.$refs.AdvancedFormTokenDisplayer.getToken()
this.$refs.AdvancedFormOtpDisplayer.show()
},
cancelCreation: function() {
@ -230,7 +228,7 @@
let imgdata = new FormData();
imgdata.append('icon', this.$refs.iconInput.files[0]);
const { data } = await this.form.upload('/api/icon/upload', imgdata)
const { data } = await this.form.upload('/api/icons', imgdata)
this.tempIcon = data;
@ -239,7 +237,7 @@
deleteIcon(event) {
if( this.tempIcon && this.tempIcon !== this.form.icon ) {
this.axios.delete('/api/icon/delete/' + this.tempIcon)
this.axios.delete('/api/icons/' + this.tempIcon)
}
this.tempIcon = ''
@ -250,7 +248,7 @@
// the component.
// This could desynchronized the HOTP verification server and our local counter if the user never verified the HOTP but this
// is acceptable (and HOTP counter can be edited by the way)
this.form.hotpCounter = payload.nextHotpCounter
this.form.counter = payload.nextHotpCounter
this.form.uri = payload.nextUri
},

View File

@ -31,9 +31,12 @@
methods: {
/**
* Get a QR code image resource from backend
*/
async getQRcode () {
const { data } = await this.axios.get('/api/qrcode/' + this.$route.params.twofaccountId)
const { data } = await this.axios.get('/api/twofaccounts/' + this.$route.params.twofaccountId + '/qrcode')
this.qrcode = data.qrcode
},

View File

@ -30,11 +30,11 @@
'label' => 'Sprache',
'help' => 'Ändern Sie die Sprache, in der die App-Oberfläche angezeigt wird.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Generiertes Token als Punkte anzeigen',
'help' => 'Tokenzeichen werden als *** angezeigt, um die Vertraulichkeit zu gewährleisten. Dies beeinflusst nicht die Kopieren/Einfügen Funktion.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Token nach dem Kopieren schließen',
'help' => 'Schließe automatisch das Popup-Fenster mit dem generierten Token nachdem es kopiert wurde'
],
@ -77,7 +77,7 @@
'help' => 'Speichert den letzten Gruppenfilter und stellt ihn bei Ihrem nächsten Besuch wieder her',
],
'never' => 'Niemals',
'on_token_copy' => 'Beim Kopieren des Tokens',
'on_otp_copy' => 'Beim Kopieren des Tokens',
'1_minutes' => 'Nach 1 Minute',
'5_minutes' => 'Nach 5 Minuten',
'10_minutes' => 'Nach 10 Minuten',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithmus',
'help' => 'Der Algorithmus, der zur Sicherung Ihrer Sicherheitscodes verwendet wird'
],
'totpPeriod' => [
'period' => [
'label' => 'Gültigkeitsdauer',
'placeholder' => 'Standard ist 30',
'help' => 'Die Gültigkeitsdauer der generierten Sicherheitscodes in Sekunden'
],
'hotpCounter' => [
'counter' => [
'label' => 'Zähler',
'placeholder' => 'Standard ist 0',
'help' => 'Der Zählerwert am Anfang',
'help_lock' => 'Es ist riskant, den Zähler zu bearbeiten, da Sie das Konto mit dem Verifizierungsserver des Dienstes desynchronisieren könnten. Verwenden Sie das Schloss-Symbol, um die Änderung zu aktivieren, wenn Sie sich sicher sind, was Sie tun'
],
'image_link' => [
'image' => [
'label' => 'Bild',
'placeholder' => 'http://...',
'help' => 'Die URL eines externen Bildes, das als Kontosymbol benutzt wird'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'Kein Konto mit dieser E-Mail gefunden',
],
'otpType' => [
'otp_type' => [
'required_without' => 'Das Feld :attribute ist erforderlich.',
],
'secret' => [

View File

@ -35,7 +35,7 @@
'confirm_new_password' => 'Confirm new password',
'dont_have_account_yet' => 'Don\'t have your account yet?',
'already_register' => 'Already registered?',
'password_do_not_match' => 'Password do not match',
'password_do_not_match' => 'Password does not match',
'forgot_your_password' => 'Forgot your password?',
'request_password_reset' => 'Reset it',
'reset_password' => 'Reset password',

View File

@ -30,13 +30,13 @@
'label' => 'Language',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
'show_otp_as_dot' => [
'label' => 'Show generated one-time passwords as dot',
'help' => 'Replace generated password caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
'close_otp_on_copy' => [
'label' => 'Close OTP after copy',
'help' => 'Automatically close the popup showing the generated password after it has been copied'
],
'use_basic_qrcode_reader' => [
'label' => 'Use basic QR code reader',
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -22,6 +22,7 @@
'use_full_form' => 'Or use the full form',
'add_one' => 'Add one',
'show_qrcode' => 'Show QR code',
'no_service' => '- no service -',
'forms' => [
'service' => [
'placeholder' => 'example.com',
@ -67,18 +68,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -30,11 +30,11 @@
'label' => 'Idioma',
'help' => 'Cambiar el idioma utilizado para traducir la interfaz de la aplicación.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Mostrar tokens generados como punto',
'help' => 'Sustituya los carácteres de token generados por *** para asegurar la confidencialidad. No afecta a la función de copiar/pegar.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Cerrar token después de la copia',
'help' => 'Cerrar automáticamente la ventana emergente mostrando el token generado después de que ha sido copiado'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorítmo',
'help' => 'El algoritmo usado para proteger sus códigos de seguridad'
],
'totpPeriod' => [
'period' => [
'label' => 'Plazo',
'placeholder' => 'Por defecto es 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Contador',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'Langue',
'help' => 'Traduit l\'application dans la langue choisie'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Rendre illisibles les codes générés',
'help' => 'Remplace les caractères des codes générés par des ●●● pour garantir leur confidentialité. N\'affecte pas la fonction de copier/coller qui reste utilisable.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Ne plus afficher les codes copiés',
'help' => 'Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié.'
],
@ -77,7 +77,7 @@
'help' => 'Enregistre le dernier groupe affiché et le restaure lors de votre prochaine visite',
],
'never' => 'Jamais',
'on_token_copy' => 'Après copie d\'un code de sécurité',
'on_otp_copy' => 'Après copie d\'un code de sécurité',
'1_minutes' => 'Après 1 minute',
'5_minutes' => 'Après 5 minutes',
'10_minutes' => 'Après 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithme',
'help' => 'L\'algorithme utilisé pour sécuriser vos codes de sécurité'
],
'totpPeriod' => [
'period' => [
'label' => 'Durée de validité',
'placeholder' => '30s par défaut',
'help' => 'La durée de validité des codes de sécurité générés, en seconde'
],
'hotpCounter' => [
'counter' => [
'label' => 'Compteur',
'placeholder' => '0 par défaut',
'help' => 'La valeur initiale du compteur',
'help_lock' => 'Il est risqué de modifier le compteur car vous pouvez désynchroniser le compte avec le serveur de vérification du service. Utilisez l\'icône cadenas pour activer la modification, mais seulement si vous savez ce que vous faites'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'L\'url d\'une image externe à utiliser comme icône du compte'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'Aucun compte utilisateur n\'utilise cette email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'Le champ :attribute est obligatoire.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'Language',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'Language',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'Language',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'Language',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'භාෂාව',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [

View File

@ -30,11 +30,11 @@
'label' => 'Language',
'help' => 'Change the language used to translate the app interface.'
],
'show_token_as_dot' => [
'show_otp_as_dot' => [
'label' => 'Show generated tokens as dot',
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
],
'close_token_on_copy' => [
'close_otp_on_copy' => [
'label' => 'Close token after copy',
'help' => 'Automatically close the popup showing the generated token after it has been copied'
],
@ -77,7 +77,7 @@
'help' => 'Save the last group filter applied and restore it on your next visit',
],
'never' => 'Never',
'on_token_copy' => 'On security code copy',
'on_otp_copy' => 'On security code copy',
'1_minutes' => 'After 1 minute',
'5_minutes' => 'After 5 minutes',
'10_minutes' => 'After 10 minutes',

View File

@ -67,18 +67,18 @@
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'period' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'counter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value',
'help_lock' => 'It is risky to edit the counter as you can desynchronize the account with the verification server of the service. Use the lock icon to enable modification, but only if you know for you are doing'
],
'image_link' => [
'image' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'

View File

@ -142,7 +142,7 @@
'email' => [
'exists' => 'No account found using this email',
],
'otpType' => [
'otp_type' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [