mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-01-23 06:38:34 +01:00
Convert the standard Create form to an advanced form
This commit is contained in:
parent
d72292c60f
commit
207ee2d3fb
@ -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 ) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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')
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
@ -33,4 +33,5 @@
|
||||
'move' => 'Move',
|
||||
'all' => 'All',
|
||||
'rename' => 'Rename',
|
||||
'options' => 'Options',
|
||||
];
|
@ -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.',
|
||||
|
@ -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',
|
||||
|
@ -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.',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
Loading…
Reference in New Issue
Block a user