Convert the standard Create form to an advanced form

This commit is contained in:
Bubka 2020-11-13 15:45:17 +01:00
parent d72292c60f
commit 207ee2d3fb
10 changed files with 289 additions and 148 deletions

View File

@ -31,19 +31,21 @@ public static function generate($twofaccount, $isPreview = false)
// $remainingTime = $nextOtpAt - time()
return $totp = [
'otp' => $twofaccount->token(),
'token' => $twofaccount->token(),
'position' => $positionInCurrentPeriod
];
}
else {
// It's a HOTP
$hotp = [
'otp' => $twofaccount->token(),
'counter' => $twofaccount->hotpCounter
'token' => $twofaccount->token(),
'hotpCounter' => $twofaccount->hotpCounter
];
// now we update the counter for the next OTP generation
$twofaccount->increaseCounter();
$twofaccount->increaseHotpCounter();
$hotp['nextHotpCounter'] = $twofaccount->hotpCounter;
$hotp['nextUri'] = $twofaccount->uri;
if( !$isPreview ) {

View File

@ -34,19 +34,36 @@ public function store(Request $request)
// see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
// for otpauth uri format validation
$this->validate($request, [
'service' => 'required',
'uri' => 'required|regex:/^otpauth:\/\/[h,t]otp\//i',
'service' => 'required|string',
'account' => 'nullable|string',
'icon' => 'nullable|string',
'uri' => 'nullable|string|regex:/^otpauth:\/\/[h,t]otp\//i',
'otpType' => 'required_without:uri|in:TOTP,HOTP',
'secret' => 'required_without:uri|string',
'digits' => 'nullable|integer|between:6,10',
'algorithm' => 'nullable|in:sha1,sha256,sha512,md5',
'totpPeriod' => 'nullable|integer|min:1',
'hotpCounter' => 'nullable|integer|min:0',
'imageLink' => 'nullable|url',
]);
OTP::get($request->uri);
// Two possible cases :
// - The most common case, the uri is provided thanks to a QR code live scan or file upload
// -> We use this uri to populate the account
// - The advanced form has been used and provide no uri but all individual parameters
// -> We use the parameters collection to populate the account
$twofaccount = new TwoFAccount;
$twofaccount = TwoFAccount::create([
'service' => $request->service,
'account' => $request->account,
'uri' => $request->uri,
'icon' => $request->icon
]);
if( $request->uri ) {
$twofaccount->populateFromUri($request->uri);
}
else {
$twofaccount->populate($request->all());
}
$twofaccount->save();
// Possible group association
$groupId = Options::get('defaultGroup') === '-1' ? (int) Options::get('activeGroup') : (int) Options::get('defaultGroup');
@ -103,10 +120,17 @@ public function generateOTP(Request $request)
// The request data is the Id of the account
$twofaccount = TwoFAccount::FindOrFail($request->id);
}
else {
// The request data is supposed to be a valid uri
else if( $request->otp['uri'] ) {
// The request data contain an uri
$twofaccount = new TwoFAccount;
$twofaccount->populateFromUri($request->uri);
$twofaccount->populateFromUri($request->otp['uri']);
$isPreview = true; // HOTP generated for preview (in the Create form) will not have its counter updated
}
else {
// The request data should contain all otp parameter
$twofaccount = new TwoFAccount;
$twofaccount->populate($request->otp);
$isPreview = true; // HOTP generated for preview (in the Create form) will not have its counter updated
}

View File

@ -306,20 +306,33 @@ public function populate(Array $attrib = [])
$this->otp = strtolower($attrib['otpType']) === 'totp' ? TOTP::create($secret) : HOTP::create($secret);
// and we change parameters if needed
if ($attrib['service']) {
if (array_key_exists('service', $attrib) && $attrib['service']) {
$this->service = $attrib['service'];
$this->otp->setIssuer( $attrib['service'] );
}
if ($attrib['account']) {
if (array_key_exists('account', $attrib) && $attrib['account']) {
$this->account = $attrib['account'];
$this->otp->setLabel( $attrib['account'] );
}
if ($attrib['icon']) { $this->account = $attrib['icon']; }
if ($attrib['digits'] > 0) { $this->otp->setParameter( 'digits', (int) $attrib['digits'] ); }
if ($attrib['algorithm']) { $this->otp->setParameter( 'digest', $attrib['algorithm'] ); }
if ($attrib['totpPeriod'] && $attrib['otpType'] !== 'totp') { $this->otp->setParameter( 'period', (int) $attrib['totpPeriod'] ); }
if ($attrib['hotpCounter'] && $attrib['otpType'] !== 'hotp') { $this->otp->setParameter( 'counter', (int) $attrib['hotpCounter'] ); }
if ($attrib['imageLink']) { $this->otp->setParameter( 'image', $attrib['imageLink'] ); }
if (array_key_exists('icon', $attrib) && $attrib['icon'])
{ $this->icon = $attrib['icon']; }
if (array_key_exists('digits', $attrib) && $attrib['digits'] > 0)
{ $this->otp->setParameter( 'digits', (int) $attrib['digits'] ); }
if (array_key_exists('digest', $attrib) && $attrib['algorithm'])
{ $this->otp->setParameter( 'digest', $attrib['algorithm'] ); }
if (array_key_exists('totpPeriod', $attrib) && $attrib['totpPeriod'] && $attrib['otpType'] !== 'totp')
{ $this->otp->setParameter( 'period', (int) $attrib['totpPeriod'] ); }
if (array_key_exists('hotpCounter', $attrib) && $attrib['hotpCounter'] && $attrib['otpType'] !== 'hotp')
{ $this->otp->setParameter( 'counter', (int) $attrib['hotpCounter'] ); }
if (array_key_exists('imageLink', $attrib) && $attrib['imageLink'])
{ $this->otp->setParameter( 'image', $attrib['imageLink'] ); }
// We can now generate a fresh URI
$this->uri = $this->otp->getProvisioningUri();
@ -327,7 +340,7 @@ public function populate(Array $attrib = [])
}
catch (\Exception $e) {
throw \Illuminate\Validation\ValidationException::withMessages([
'qrcode' => __('errors.cannot_create_otp_without_parameters')
'qrcode' => __('errors.cannot_create_otp_with_those_parameters')
]);
}

View File

@ -5,84 +5,107 @@
</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 id="otp" class="is-size-1 has-text-white" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => otp.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedOtp }}</p>
<ul class="dots" v-if="otpType === 'totp'">
<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'">
<li v-for="n in 30"></li>
</ul>
<ul v-else-if="otpType === 'hotp'">
<li>counter: {{ counter }}</li>
<ul v-else-if="internal_otpType === 'hotp'">
<li>counter: {{ internal_hotpCounter }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'TokenDisplayer',
data() {
return {
id: null,
internal_service: '',
internal_account: '',
internal_uri: '',
next_uri: '',
internal_icon: '',
otpType: '',
otp : '',
nextHotpCounter: null,
token : '',
timerID: null,
position: null,
counter: null,
internal_otpType: '',
internal_account: '',
internal_service: '',
internal_icon: '',
internal_hotpCounter: null,
}
},
props: {
service: '',
account: '',
uri : '',
icon: ''
account : String,
algorithm : String,
digits : Number,
hotpCounter : Number,
icon : String,
imageLink : String,
otpType : String,
qrcode : null,
secret : String,
secretIsBase32Encoded : Number,
service : String,
totpPeriod : Number,
uri : String
},
computed: {
displayedOtp() {
return this.$root.appSettings.showTokenAsDot ? this.otp.replace(/[0-9]/g, '●') : this.otp
displayedToken() {
return this.$root.appSettings.showTokenAsDot ? this.token.replace(/[0-9]/g, '●') : this.token
}
},
mounted: function() {
this.showAccount()
this.getToken()
},
methods: {
async showAccount(id) {
async getToken(id) {
// 2 possible cases :
// - ID is provided so we fetch the account data from db but without the uri.
// 3 possible cases :
// - Trigger when user ask for a token 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.
// - the URI prop has been set via the create form, we need to preview some OTP before storing the account.
// So this.otpType is set on client side from the provided URI
// - 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
try {
this.internal_otpType = this.otpType.toLowerCase()
}
catch(e) {
//do nothing
}
finally {
this.internal_account = this.account
this.internal_service = this.service
this.internal_icon = this.icon
this.internal_hotpCounter = this.hotpCounter
}
if( id ) {
this.id = id
if( this.id || this.uri ) {
if( this.id ) {
const { data } = await this.axios.get('api/twofaccounts/' + this.id)
this.internal_service = data.service
this.internal_account = data.account
this.internal_icon = data.icon
this.otpType = data.otpType
}
else {
this.internal_service = this.service
this.internal_account = this.account
this.internal_icon = this.icon
this.internal_uri = this.uri
this.otpType = this.internal_uri.slice(0, 15 ) === "otpauth://totp/" ? 'totp' : 'hotp';
this.internal_otpType = data.otpType
}
switch(this.otpType) {
// We force the otpType to be based on the uri
if( this.uri ) {
this.internal_otpType = 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
switch(this.internal_otpType) {
case 'totp':
await this.getTOTP()
break;
@ -97,12 +120,13 @@
}
},
getTOTP: function() {
this.axios.post('/api/twofaccounts/otp', { id: this.id, uri: this.internal_uri }).then(response => {
let spacePosition = Math.ceil(response.data.otp.length / 2);
this.axios.post('/api/twofaccounts/otp', { id: this.id, otp: this.$props }).then(response => {
let spacePosition = Math.ceil(response.data.token.length / 2);
this.otp = response.data.otp.substr(0, spacePosition) + " " + response.data.otp.substr(spacePosition);
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition);
this.position = response.data.position;
let dots = this.$el.querySelector('.dots');
@ -141,26 +165,31 @@
});
},
getHOTP: function() {
this.axios.post('/api/twofaccounts/otp', { id: this.id, uri: this.internal_uri }).then(response => {
let spacePosition = Math.ceil(response.data.otp.length / 2);
this.axios.post('/api/twofaccounts/otp', { id: this.id, otp: this.$props }).then(response => {
let spacePosition = Math.ceil(response.data.token.length / 2);
this.otp = response.data.otp.substr(0, spacePosition) + " " + response.data.otp.substr(spacePosition)
this.counter = response.data.counter
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition)
this.internal_hotpCounter = response.data.hotpCounter
this.nextHotpCounter = response.data.nextHotpCounter
this.next_uri = response.data.nextUri
this.$emit('update-hotp-counter', { nextHotpCounter: this.nextHotpCounter })
})
.catch(error => {
this.$router.push({ name: 'genericError', params: { err: error.response } });
});
},
clearOTP: function() {
this.stopLoop()
this.id = this.timerID = this.position = this.counter = null
this.internal_service = this.internal_account = this.internal_icon = this.internal_uri = ''
this.otp = '... ...'
this.id = this.timerID = this.position = this.internal_hotpCounter = null
this.internal_service = this.internal_account = this.internal_icon = this.internal_otpType = ''
this.token = '... ...'
try {
this.$el.querySelector('[data-is-active]').removeAttribute('data-is-active');
@ -171,12 +200,14 @@
}
},
stopLoop: function() {
if( this.otpType === 'totp' ) {
if( this.internal_otpType === 'totp' ) {
clearInterval(this.timerID)
}
},
clipboardSuccessHandler ({ value, event }) {
if(this.$root.appSettings.kickUserAfter == -1) {
@ -190,6 +221,7 @@
this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') })
},
clipboardErrorHandler ({ value, event }) {
console.log('error', value)
}

View File

@ -132,7 +132,7 @@
<quick-uploader v-if="showUploader" :directStreaming="accounts.length > 0" :showTrailer="accounts.length === 0" ref="QuickUploader"></quick-uploader>
<!-- modal -->
<modal v-model="showTwofaccountInModal">
<twofaccount-show ref="TwofaccountShow" ></twofaccount-show>
<token-displayer ref="TokenDisplayer" ></token-displayer>
</modal>
<!-- footer -->
<vue-footer v-if="showFooter && !showGroupSwitch" :showButtons="accounts.length > 0">
@ -188,7 +188,7 @@
<script>
import Modal from '../components/Modal'
import TwofaccountShow from '../components/TwofaccountShow'
import TokenDisplayer from '../components/TokenDisplayer'
import QuickUploader from './../components/QuickUploader'
// import vuePullRefresh from 'vue-pull-refresh';
import draggable from 'vuedraggable'
@ -257,7 +257,7 @@
// stop OTP generation on modal close
this.$on('modalClose', function() {
console.log('modalClose triggered')
this.$refs.TwofaccountShow.clearOTP()
this.$refs.TokenDisplayer.clearOTP()
});
// hide Footer when stream is on
@ -281,7 +281,7 @@
components: {
Modal,
TwofaccountShow,
TokenDisplayer,
// 'vue-pull-refresh': vuePullRefresh,
QuickUploader,
draggable,
@ -321,7 +321,7 @@
this.selectedAccounts.push(account.id)
}
else {
this.$refs.TwofaccountShow.showAccount(account.id)
this.$refs.TokenDisplayer.showAccount(account.id)
}
},

View File

@ -9,11 +9,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>
<twofaccount-show ref="TwofaccountShow"
:service="form.service"
:account="form.account"
:uri="form.uri">
</twofaccount-show>
<token-displayer ref="QuickFormTokenDisplayer" v-bind="form.data()">
</token-displayer>
</div>
</div>
<div class="columns is-mobile">
@ -33,45 +30,26 @@
<!-- Full form -->
<form-wrapper :title="$t('twofaccounts.forms.new_account')" v-else>
<form @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
<!-- qcode fileupload -->
<div class="field">
<div class="file is-dark is-boxed">
<div class="file is-black is-small">
<label class="file-label" :title="$t('twofaccounts.forms.use_qrcode.title')">
<input class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
<span class="file-cta">
<span class="file-icon">
<font-awesome-icon :icon="['fas', 'qrcode']" size="lg" />
</span>
<span class="file-label">{{ $t('twofaccounts.forms.use_qrcode.val') }}</span>
<span class="file-label">{{ $t('twofaccounts.forms.prefill_using_qrcode') }}</span>
</span>
</label>
</div>
</div>
<field-error :form="form" field="qrcode" class="help-for-file" />
<!-- service -->
<form-field :form="form" fieldName="service" inputType="text" :label="$t('twofaccounts.service')" :placeholder="$t('twofaccounts.forms.service.placeholder')" autofocus />
<!-- account -->
<form-field :form="form" fieldName="account" inputType="text" :label="$t('twofaccounts.account')" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
<div class="field" style="margin-bottom: 0.5rem;">
<label class="label">{{ $t('twofaccounts.forms.otp_uri') }}</label>
</div>
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" placeholder="otpauth://totp/..." v-model="form.uri" :disabled="uriIsLocked" />
</div>
<div class="control" v-if="uriIsLocked">
<a class="button is-dark field-lock" @click="uriIsLocked = 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="uriIsLocked = 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" />
<!-- icon upload -->
<div class="field">
<label class="label">{{ $t('twofaccounts.icon') }}</label>
<div class="file is-dark">
@ -91,26 +69,58 @@
</div>
</div>
<field-error :form="form" field="icon" class="help-for-file" />
<div class="field is-grouped">
<div class="control">
<v-button :isLoading="form.isBusy" >{{ $t('commons.create') }}</v-button>
<!-- otp type -->
<form-toggle :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">
<!-- secret -->
<label class="label" v-html="$t('twofaccounts.forms.secret.label')"></label>
<div class="field has-addons">
<p class="control">
<span class="select">
<select v-model="form.secretIsBase32Encoded">
<option v-for="format in secretFormats" :value="format.value">{{ format.text }}</option>
</select>
</span>
</p>
<p class="control is-expanded">
<input class="input" type="text" v-model="form.secret">
</p>
</div>
<div class="control" v-if="form.uri">
<button type="button" class="button is-success" @click="previewAccount">{{ $t('twofaccounts.forms.test') }}</button>
</div>
<div class="control">
<button type="button" class="button is-text" @click="cancelCreation">{{ $t('commons.cancel') }}</button>
<div class="field">
<field-error :form="form" field="secret" class="help-for-file" />
<p class="help" v-html="$t('twofaccounts.forms.secret.help')"></p>
</div>
<h2 class="title is-4 mt-5 mb-2">{{ $t('commons.options') }}</h2>
<p class="help mb-4">
{{ $t('twofaccounts.forms.options_help') }}
</p>
<!-- digits -->
<form-toggle :form="form" :choices="digitsChoices" fieldName="digits" :label="$t('twofaccounts.forms.digits.label')" :help="$t('twofaccounts.forms.digits.help')" />
<!-- 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')" />
<!-- 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')" />
<!-- image link -->
<form-field :form="form" fieldName="imageLink" inputType="text" :label="$t('twofaccounts.forms.image_link.label')" :placeholder="$t('twofaccounts.forms.image_link.placeholder')" :help="$t('twofaccounts.forms.image_link.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">
<button type="button" class="button is-success is-rounded" @click="previewAccount">{{ $t('twofaccounts.forms.test') }}</button>
</p>
<p class="control">
<button type="button" class="button is-text is-rounded" @click="cancelCreation">{{ $t('commons.cancel') }}</button>
</p>
</vue-footer>
</form>
<!-- modal -->
<modal v-model="ShowTwofaccountInModal">
<twofaccount-show ref="TwofaccountPreview"
:service="form.service"
:account="form.account"
:uri="form.uri"
:icon="tempIcon">
</twofaccount-show>
<token-displayer ref="AdvancedFormTokenDisplayer" v-bind="form.data()" @update-hotp-counter="updateHotpCounter">
</token-displayer>
</modal>
</form-wrapper>
</template>
@ -119,29 +129,43 @@
import Modal from '../../components/Modal'
import Form from './../../components/Form'
import TwofaccountShow from '../../components/TwofaccountShow'
import TokenDisplayer from '../../components/TokenDisplayer'
export default {
data() {
return {
isQuickForm: false,
ShowTwofaccountInModal : false,
uriIsLocked: true,
tempIcon: '',
form: new Form({
service: '',
account: '',
otpType: '',
uri: '',
icon: '',
qrcode: null
})
secret: '',
secretIsBase32Encoded: 0,
algorithm: '',
digits: null,
hotpCounter: null,
totpPeriod: null,
imageLink: '',
qrcode: null,
}),
otpTypes: ['TOTP', 'HOTP'],
digitsChoices: [6,7,8,9,10],
secretFormats: [
{ text: this.$t('twofaccounts.forms.plain_text'), value: 0 },
{ text: 'Base32', value: 1 }
],
algorithms: ['sha1', 'sha256', 'sha512', 'md5'],
}
},
watch: {
tempIcon: function(val) {
if( this.isQuickForm ) {
this.$refs.TwofaccountShow.internal_icon = val
this.$refs.QuickFormTokenDisplayer.internal_icon = val
}
},
},
@ -149,20 +173,21 @@
mounted: function () {
if( this.$route.params.qrAccount ) {
this.isQuickForm = true
this.form.fill(this.$route.params.qrAccount)
this.isQuickForm = true
}
// stop TOTP generation on modal close
this.$on('modalClose', function() {
this.$refs.TwofaccountPreview.stopLoop()
this.$refs.AdvancedFormTokenDisplayer.stopLoop()
});
},
components: {
Modal,
TwofaccountShow,
TokenDisplayer,
},
methods: {
@ -171,15 +196,15 @@
// set current temp icon as account icon
this.form.icon = this.tempIcon
// The quick form (possibly the preview feature too) has incremented the HOTP counter so the next_uri property
// must be used as the uri to store
// The quick form or the preview feature has incremented the HOTP counter so the next_uri property
// must be used as the uri to store.
// 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)
if( this.isQuickForm && this.$refs.TwofaccountShow.next_uri ) {
this.form.uri = this.$refs.TwofaccountShow.next_uri
if( this.isQuickForm && this.$refs.QuickFormTokenDisplayer.next_uri ) {
this.form.uri = this.$refs.QuickFormTokenDisplayer.next_uri
}
else if( this.$refs.TwofaccountPreview && this.$refs.TwofaccountPreview.next_uri ) {
this.form.uri = this.$refs.TwofaccountPreview.next_uri
else if( this.$refs.AdvancedFormTokenDisplayer && this.$refs.AdvancedFormTokenDisplayer.next_uri ) {
this.form.uri = this.$refs.AdvancedFormTokenDisplayer.next_uri
}
await this.form.post('/api/twofaccounts')
@ -191,10 +216,7 @@
},
previewAccount() {
// preview is possible only if we have an uri
if( this.form.uri ) {
this.$refs.TwofaccountPreview.showAccount()
}
this.$refs.AdvancedFormTokenDisplayer.getToken()
},
cancelCreation: function() {
@ -220,6 +242,9 @@
const { data } = await this.form.upload('/api/qrcode/decode', imgdata)
this.form.fill(data)
this.form.otpType = this.form.otpType.toUpperCase()
this.form.secretIsBase32Encoded = 1
this.form.uri = '' // we don't want an uri now because the user can change any otp parameter in the form
},
@ -244,6 +269,10 @@
}
},
updateHotpCounter(payload) {
this.form.hotpCounter = payload.nextHotpCounter
},
},
}

View File

@ -33,4 +33,5 @@
'move' => 'Move',
'all' => 'All',
'rename' => 'Rename',
'options' => 'Options',
];

View File

@ -26,7 +26,7 @@
'Unable_to_decrypt_uri' => 'Unable to decrypt uri',
'not_a_supported_otp_type' => 'This OTP format is not currently supported',
'cannot_create_otp_without_secret' => 'Cannot create an OTP without a secret',
'cannot_create_otp_without_parameters' => 'Cannot create an OTP with those parameters',
'cannot_create_otp_with_those_parameters' => 'Cannot create an OTP with those parameters',
'wrong_current_password' => 'Wrong current password, nothing has changed',
'error_during_encryption' => 'Encryption failed, your database remains unprotected.',
'error_during_decryption' => 'Decryption failed, your database is still protected. This is mainly caused by an integrity issue of encrypted data for one or more accounts.',

View File

@ -34,6 +34,7 @@
'otp_uri' => 'OTP Uri',
'hotp_counter' => 'HOTP Counter',
'scan_qrcode' => 'Scan a qrcode',
'prefill_using_qrcode' => 'Prefill using a QR Code',
'use_qrcode' => [
'val' => 'Use a qrcode',
'title' => 'Use a QR code to fill the form magically',
@ -48,6 +49,39 @@
],
'choose_image' => 'Choose an image…',
'test' => 'Test',
'secret' => [
'label' => 'Secret',
'help' => 'The key used to generate your security codes'
],
'plain_text' => 'Plain text',
'otp_type' => [
'label' => 'Choose the type of OTP to create',
'help' => 'Time-based OTP or HMAC-based OTP'
],
'digits' => [
'label' => 'Digits',
'help' => 'The number of digits of the generated security codes'
],
'algorithm' => [
'label' => 'Algorithm',
'help' => 'The algorithm used to secure your security codes'
],
'totpPeriod' => [
'label' => 'Period',
'placeholder' => 'Default is 30',
'help' => 'The period of validity of the generated security codes in second'
],
'hotpCounter' => [
'label' => 'Counter',
'placeholder' => 'Default is 0',
'help' => 'The initial counter value'
],
'image_link' => [
'label' => 'Image',
'placeholder' => 'http://...',
'help' => 'The url of an external image to use as the account icon'
],
'options_help' => 'You can leave the following options blank if you don\'t know how to set them. The most commonly used values will be applied.',
],
'stream' => [
'need_grant_permission' => 'You need to grant camera access permission',

View File

@ -115,7 +115,7 @@
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'url' => 'The :attribute format is invalid.',
'url' => 'The :attribute must be a valid url.',
'uuid' => 'The :attribute must be a valid UUID.',
/*
@ -141,7 +141,13 @@
],
'email' => [
'exists' => 'No account found using this email',
]
],
'otpType' => [
'required_without' => 'The :attribute field is required.',
],
'secret' => [
'required_without' => 'The :attribute field is required.',
],
],
/*