mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-02-02 11:39:19 +01:00
Add OAuth Personal Access Token management
This commit is contained in:
parent
c9dee47be3
commit
55a47a75f4
@ -11,7 +11,7 @@
|
||||
<a class="has-text-grey" href="https://github.com/Bubka/2FAuth"><b>2FAuth</b> <font-awesome-icon :icon="['fab', 'github-alt']" /></a> - v{{ appVersion }}
|
||||
</div>
|
||||
<div v-else class="content has-text-centered">
|
||||
<router-link :to="{ name: 'settings' }" class="has-text-grey">{{ $t('settings.settings') }}</router-link> - <a class="has-text-grey" @click="logout">{{ $t('auth.sign_out') }}</a>
|
||||
<router-link :to="{ name: 'settings.options' }" class="has-text-grey">{{ $t('settings.settings') }}</router-link> - <a class="has-text-grey" @click="logout">{{ $t('auth.sign_out') }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
55
resources/js/components/SettingTabs.vue
Normal file
55
resources/js/components/SettingTabs.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="options-header has-background-black-ter">
|
||||
<div class="columns is-centered">
|
||||
<div class="form-column column is-two-thirds-tablet is-half-desktop is-one-third-widescreen is-one-third-fullhd">
|
||||
<div class="tabs is-centered is-fullwidth">
|
||||
<ul>
|
||||
<li v-for="tab in tabs" :key="tab.view" :class="{ 'is-active': tab.view === activeTab }">
|
||||
<a @click="selectTab(tab.view)">{{ tab.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'SettingTabs',
|
||||
|
||||
data(){
|
||||
return {
|
||||
tabs: [
|
||||
{
|
||||
'name' : this.$t('settings.options'),
|
||||
'view' : 'settings.options'
|
||||
},
|
||||
{
|
||||
'name' : this.$t('settings.account'),
|
||||
'view' : 'settings.account'
|
||||
},
|
||||
{
|
||||
'name' : this.$t('settings.oauth'),
|
||||
'view' : 'settings.oauth'
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
activeTab: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectTab(viewName) {
|
||||
this.$router.push({ name: viewName })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
4
resources/js/components/index.js
vendored
4
resources/js/components/index.js
vendored
@ -11,6 +11,7 @@ import FormCheckbox from './FormCheckbox'
|
||||
import FormButtons from './FormButtons'
|
||||
import VueFooter from './Footer'
|
||||
import Kicker from './Kicker'
|
||||
import SettingTabs from './SettingTabs'
|
||||
|
||||
// Components that are registered globaly.
|
||||
[
|
||||
@ -25,7 +26,8 @@ import Kicker from './Kicker'
|
||||
FormCheckbox,
|
||||
FormButtons,
|
||||
VueFooter,
|
||||
Kicker
|
||||
Kicker,
|
||||
SettingTabs
|
||||
].forEach(Component => {
|
||||
Vue.component(Component.name, Component)
|
||||
})
|
||||
|
7
resources/js/mixins.js
vendored
7
resources/js/mixins.js
vendored
@ -19,6 +19,13 @@ Vue.mixin({
|
||||
|
||||
this.$router.push({ name: 'login' })
|
||||
},
|
||||
|
||||
exitSettings: function(event) {
|
||||
if (event) {
|
||||
this.$notify({ clean: true })
|
||||
this.$router.push({ name: 'accounts' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
10
resources/js/routes.js
vendored
10
resources/js/routes.js
vendored
@ -16,7 +16,10 @@ import Login from './views/auth/Login'
|
||||
import Register from './views/auth/Register'
|
||||
import PasswordRequest from './views/auth/password/Request'
|
||||
import PasswordReset from './views/auth/password/Reset'
|
||||
import Settings from './views/settings/Index'
|
||||
import SettingsOptions from './views/settings/Options'
|
||||
import SettingsAccount from './views/settings/Account'
|
||||
import SettingsOAuth from './views/settings/OAuth'
|
||||
import GeneratePAT from './views/settings/PATokens/Create'
|
||||
import Errors from './views/Error'
|
||||
|
||||
const router = new Router({
|
||||
@ -34,7 +37,10 @@ const router = new Router({
|
||||
{ path: '/group/create', name: 'createGroup', component: CreateGroup, meta: { requiresAuth: true } },
|
||||
{ path: '/group/:groupId/edit', name: 'editGroup', component: EditGroup, meta: { requiresAuth: true }, props: true },
|
||||
|
||||
{ path: '/settings', name: 'settings', component: Settings, meta: { requiresAuth: true } },
|
||||
{ path: '/settings/options', name: 'settings.options', component: SettingsOptions, meta: { requiresAuth: true } },
|
||||
{ path: '/settings/account', name: 'settings.account', component: SettingsAccount, meta: { requiresAuth: true } },
|
||||
{ path: '/settings/oauth', name: 'settings.oauth', component: SettingsOAuth, meta: { requiresAuth: true } },
|
||||
{ path: '/settings/oauth/pat/create', name: 'settings.oauth.generatePAT', component: GeneratePAT, meta: { requiresAuth: true } },
|
||||
|
||||
{ path: '/login', name: 'login', component: Login },
|
||||
{ path: '/register', name: 'register', component: Register },
|
||||
|
@ -1,12 +1,33 @@
|
||||
<template>
|
||||
<form-wrapper>
|
||||
<form @submit.prevent="handleSubmit" @keydown="form.onKeydown($event)">
|
||||
<form-field :form="form" fieldName="name" :label="$t('auth.forms.name')" autofocus />
|
||||
<form-field :form="form" fieldName="email" inputType="email" :label="$t('auth.forms.email')" />
|
||||
<form-field :form="form" fieldName="password" inputType="password" :label="$t('auth.forms.current_password.label')" :help="$t('auth.forms.current_password.help')" :hasOffset="true" />
|
||||
<form-buttons :isBusy="form.isBusy" :caption="$t('commons.update')" />
|
||||
</form>
|
||||
</form-wrapper>
|
||||
<div>
|
||||
<setting-tabs :activeTab="'settings.account'"></setting-tabs>
|
||||
<div class="options-tabs">
|
||||
<form-wrapper>
|
||||
<form @submit.prevent="submitProfile" @keydown="formProfile.onKeydown($event)">
|
||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4>
|
||||
<form-field :form="formProfile" fieldName="name" :label="$t('auth.forms.name')" autofocus />
|
||||
<form-field :form="formProfile" fieldName="email" inputType="email" :label="$t('auth.forms.email')" />
|
||||
<form-field :form="formProfile" fieldName="password" inputType="password" :label="$t('auth.forms.current_password.label')" :help="$t('auth.forms.current_password.help')" />
|
||||
<form-buttons :isBusy="formProfile.isBusy" :caption="$t('commons.update')" />
|
||||
</form>
|
||||
<form @submit.prevent="submitPassword" @keydown="formPassword.onKeydown($event)">
|
||||
<h4 class="title is-4 pt-6 has-text-grey-light">{{ $t('settings.change_password') }}</h4>
|
||||
<form-field :form="formPassword" fieldName="password" inputType="password" :label="$t('auth.forms.new_password')" />
|
||||
<form-field :form="formPassword" fieldName="password_confirmation" inputType="password" :label="$t('auth.forms.confirm_new_password')" />
|
||||
<form-field :form="formPassword" fieldName="currentPassword" inputType="password" :label="$t('auth.forms.current_password.label')" :help="$t('auth.forms.current_password.help')" />
|
||||
<form-buttons :isBusy="formPassword.isBusy" :caption="$t('auth.forms.change_password')" />
|
||||
</form>
|
||||
</form-wrapper>
|
||||
</div>
|
||||
<vue-footer :showButtons="true">
|
||||
<!-- Cancel button -->
|
||||
<p class="control">
|
||||
<a class="button is-dark is-rounded" @click.stop="exitSettings">
|
||||
{{ $t('commons.close') }}
|
||||
</a>
|
||||
</p>
|
||||
</vue-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -16,25 +37,30 @@
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
form: new Form({
|
||||
formProfile: new Form({
|
||||
name : '',
|
||||
email : '',
|
||||
password : '',
|
||||
}),
|
||||
formPassword: new Form({
|
||||
currentPassword : '',
|
||||
password : '',
|
||||
password_confirmation : '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
const { data } = await this.form.get('/api/user')
|
||||
const { data } = await this.formProfile.get('/api/user')
|
||||
|
||||
this.form.fill(data)
|
||||
this.formProfile.fill(data)
|
||||
},
|
||||
|
||||
methods : {
|
||||
handleSubmit(e) {
|
||||
submitProfile(e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.form.put('/api/user', {returnError: true})
|
||||
this.formProfile.put('/api/user', {returnError: true})
|
||||
.then(response => {
|
||||
this.$notify({ type: 'is-success', text: this.$t('auth.forms.profile_saved') })
|
||||
})
|
||||
@ -44,6 +70,26 @@
|
||||
this.$notify({ type: 'is-danger', text: error.response.data.message })
|
||||
}
|
||||
else if( error.response.status !== 422 ) {
|
||||
this.$router.push({ name: 'genericError', params: { err: error.response } });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
submitPassword(e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.formPassword.patch('/api/user/password', {returnError: true})
|
||||
.then(response => {
|
||||
|
||||
this.$notify({ type: 'is-success', text: response.data.message })
|
||||
})
|
||||
.catch(error => {
|
||||
if( error.response.status === 400 ) {
|
||||
|
||||
this.$notify({ type: 'is-danger', text: error.response.data.message })
|
||||
}
|
||||
else if( error.response.status !== 422 ) {
|
||||
|
||||
this.$router.push({ name: 'genericError', params: { err: error.response } });
|
||||
}
|
||||
});
|
||||
|
@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="options-header has-background-black-ter">
|
||||
<div class="columns is-centered">
|
||||
<div class="form-column column is-two-thirds-tablet is-half-desktop is-one-third-widescreen is-one-third-fullhd">
|
||||
<div class="tabs is-centered is-fullwidth">
|
||||
<ul>
|
||||
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
|
||||
<a @click="selectTab(tab)">{{ tab.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-tabs">
|
||||
<options v-if="activeTab === $t('settings.options')"></options>
|
||||
<account v-if="activeTab === $t('settings.account')"></account>
|
||||
<password v-if="activeTab === $t('settings.password')"></password>
|
||||
</div>
|
||||
<vue-footer :showButtons="true">
|
||||
<!-- Cancel button -->
|
||||
<p class="control">
|
||||
<a class="button is-dark is-rounded" @click.stop="exitSettings">
|
||||
{{ $t('commons.close') }}
|
||||
</a>
|
||||
</p>
|
||||
</vue-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Options from './Options'
|
||||
import Account from './Account'
|
||||
import Password from './Password'
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
tabs: [
|
||||
{
|
||||
'name' : this.$t('settings.options'),
|
||||
'isActive': true
|
||||
},
|
||||
{
|
||||
'name' : this.$t('settings.account'),
|
||||
'isActive': false
|
||||
},
|
||||
{
|
||||
'name' : this.$t('settings.password'),
|
||||
'isActive': false
|
||||
},
|
||||
],
|
||||
activeTab: this.$t('settings.options')
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
Options,
|
||||
Account,
|
||||
Password
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectTab(selectedTab) {
|
||||
this.tabs.forEach(tab => {
|
||||
tab.isActive = (tab.name == selectedTab.name);
|
||||
if( tab.name == selectedTab.name ) {
|
||||
this.activeTab = selectedTab.name
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
exitSettings: function(event) {
|
||||
if (event) {
|
||||
this.$notify({ clean: true })
|
||||
this.$router.push({ name: 'accounts' })
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
120
resources/js/views/settings/OAuth.vue
Normal file
120
resources/js/views/settings/OAuth.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<setting-tabs :activeTab="'settings.oauth'"></setting-tabs>
|
||||
<div class="options-tabs">
|
||||
<div class="columns is-centered">
|
||||
<div class="form-column column is-two-thirds-tablet is-half-desktop is-one-third-widescreen is-one-third-fullhd">
|
||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.personal_access_tokens') }}</h4>
|
||||
<div class="is-size-7-mobile">
|
||||
{{ $t('settings.token_legend')}}
|
||||
</div>
|
||||
<div class="mt-3 mb-6">
|
||||
<router-link class="is-link mt-5" :to="{ name: 'settings.oauth.generatePAT' }">
|
||||
<font-awesome-icon :icon="['fas', 'plus-circle']" /> {{ $t('settings.generate_new_token')}}
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="tokens.length > 0">
|
||||
<div v-for="token in tokens" :key="token.id" class="group-item has-text-light is-size-5 is-size-6-mobile">
|
||||
<font-awesome-icon v-if="token.value" class="has-text-success" :icon="['fas', 'check']" /> {{ token.name }}
|
||||
<div class="tags is-pulled-right">
|
||||
<a v-if="token.value" class="tag" v-clipboard="() => token.value" v-clipboard:success="clipboardSuccessHandler">{{ $t('commons.copy') }}</a>
|
||||
<a class="tag is-dark " @click="revokeToken(token.id)">{{ $t('settings.revoke') }}</a>
|
||||
</div>
|
||||
<span v-if="token.value" class="is-size-7-mobile is-size-6 my-3">
|
||||
{{ $t('settings.make_sure_copy_token') }}
|
||||
</span>
|
||||
<span v-if="token.value" class="pat is-family-monospace is-size-6 is-size-7-mobile has-text-success">
|
||||
{{ token.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isFetching && tokens.length === 0" class="has-text-centered">
|
||||
<span class="is-size-4">
|
||||
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||
</span>
|
||||
</div>
|
||||
<!-- footer -->
|
||||
<vue-footer :showButtons="true">
|
||||
<!-- close button -->
|
||||
<p class="control">
|
||||
<router-link :to="{ name: 'accounts', params: { toRefresh: false } }" class="button is-dark is-rounded">{{ $t('commons.close') }}</router-link>
|
||||
</p>
|
||||
</vue-footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Form from './../../components/Form'
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
tokens : [],
|
||||
isFetching: false,
|
||||
form: new Form({
|
||||
token : '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchTokens()
|
||||
},
|
||||
|
||||
methods : {
|
||||
|
||||
/**
|
||||
* Get all groups from backend
|
||||
*/
|
||||
async fetchTokens() {
|
||||
|
||||
this.isFetching = true
|
||||
|
||||
await this.axios.get('/api/oauth/personal-access-tokens').then(response => {
|
||||
const tokens = []
|
||||
|
||||
response.data.forEach((data) => {
|
||||
if (data.id === this.$route.params.token_id) {
|
||||
data.value = this.$route.params.accessToken
|
||||
tokens.unshift(data)
|
||||
}
|
||||
else {
|
||||
tokens.push(data)
|
||||
}
|
||||
})
|
||||
|
||||
this.tokens = tokens
|
||||
})
|
||||
|
||||
this.isFetching = false
|
||||
},
|
||||
|
||||
clipboardSuccessHandler ({ value, event }) {
|
||||
|
||||
this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') })
|
||||
},
|
||||
|
||||
clipboardErrorHandler ({ value, event }) {
|
||||
console.log('error', value)
|
||||
},
|
||||
|
||||
/**
|
||||
* revoke a token (after confirmation)
|
||||
*/
|
||||
async revokeToken(tokenId) {
|
||||
if(confirm(this.$t('settings.confirm.revoke'))) {
|
||||
|
||||
await this.axios.delete('/api/oauth/personal-access-tokens/' + tokenId).then(response => {
|
||||
// Remove the revoked token from the collection
|
||||
this.tokens = this.tokens.filter(a => a.id !== tokenId)
|
||||
this.$notify({ type: 'is-success', text: this.$t('settings.token_revoked') })
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,40 +1,53 @@
|
||||
<template>
|
||||
<form-wrapper>
|
||||
<!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> -->
|
||||
<form>
|
||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
|
||||
<!-- Language -->
|
||||
<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 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 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')" />
|
||||
<div>
|
||||
<setting-tabs :activeTab="'settings.options'"></setting-tabs>
|
||||
<div class="options-tabs">
|
||||
<form-wrapper>
|
||||
<!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> -->
|
||||
<form>
|
||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
|
||||
<!-- Language -->
|
||||
<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 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 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 has-text-grey-light">{{ $t('groups.groups') }}</h4>
|
||||
<!-- default group -->
|
||||
<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 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 has-text-grey-light">{{ $t('groups.groups') }}</h4>
|
||||
<!-- default group -->
|
||||
<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 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 has-text-grey-light">{{ $t('settings.security') }}</h4>
|
||||
<!-- auto lock -->
|
||||
<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 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 has-text-grey-light">{{ $t('settings.security') }}</h4>
|
||||
<!-- auto lock -->
|
||||
<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 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 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
||||
<!-- basic qrcode -->
|
||||
<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 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 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>
|
||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
||||
<!-- basic qrcode -->
|
||||
<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 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 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>
|
||||
</div>
|
||||
<vue-footer :showButtons="true">
|
||||
<!-- Cancel button -->
|
||||
<p class="control">
|
||||
<a class="button is-dark is-rounded" @click.stop="exitSettings">
|
||||
{{ $t('commons.close') }}
|
||||
</a>
|
||||
</p>
|
||||
</vue-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -150,10 +163,10 @@
|
||||
|
||||
fetchGroups() {
|
||||
|
||||
this.axios.get('api/groups').then(response => {
|
||||
this.axios.get('/api/groups').then(response => {
|
||||
response.data.forEach((data) => {
|
||||
if( data.id >0 ) {
|
||||
this.groups.push({
|
||||
this.groups.push({
|
||||
text: data.name,
|
||||
value: data.id
|
||||
})
|
||||
|
50
resources/js/views/settings/PATokens/Create.vue
Normal file
50
resources/js/views/settings/PATokens/Create.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<form-wrapper :title="$t('settings.forms.new_token')">
|
||||
<form @submit.prevent="generatePAToken" @keydown="form.onKeydown($event)">
|
||||
<form-field :form="form" fieldName="name" inputType="text" :label="$t('commons.name')" autofocus />
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<v-button>{{ $t('commons.generate') }}</v-button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-text" @click="cancelGeneration">{{ $t('commons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Form from './../../../components/Form'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: new Form({
|
||||
name: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async generatePAToken() {
|
||||
|
||||
const { data } = await this.form.post('/api/oauth/personal-access-tokens')
|
||||
|
||||
if( this.form.errors.any() === false ) {
|
||||
this.$router.push({ name: 'settings.oauth', params: { accessToken: data.accessToken, token_id: data.token.id } });
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
cancelGeneration: function() {
|
||||
|
||||
this.$router.push({ name: 'settings.oauth' });
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<form-wrapper>
|
||||
<form @submit.prevent="handleSubmit" @keydown="form.onKeydown($event)">
|
||||
<form-field :form="form" fieldName="password" inputType="password" :label="$t('auth.forms.new_password')" />
|
||||
<form-field :form="form" fieldName="password_confirmation" inputType="password" :label="$t('auth.forms.confirm_new_password')" />
|
||||
<form-field :form="form" fieldName="currentPassword" inputType="password" :label="$t('auth.forms.current_password.label')" :help="$t('auth.forms.current_password.help')" :hasOffset="true" />
|
||||
<form-buttons :isBusy="form.isBusy" :caption="$t('auth.forms.change_password')" />
|
||||
</form>
|
||||
</form-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Form from './../../components/Form'
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
form: new Form({
|
||||
currentPassword : '',
|
||||
password : '',
|
||||
password_confirmation : '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods : {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.form.patch('/api/user/password', {returnError: true})
|
||||
.then(response => {
|
||||
|
||||
this.$notify({ type: 'is-success', text: response.data.message })
|
||||
})
|
||||
.catch(error => {
|
||||
if( error.response.status === 400 ) {
|
||||
|
||||
this.$notify({ type: 'is-danger', text: error.response.data.message })
|
||||
}
|
||||
else if( error.response.status !== 422 ) {
|
||||
|
||||
this.$router.push({ name: 'genericError', params: { err: error.response } });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
@ -15,6 +15,7 @@
|
||||
|
||||
'cancel' => 'Cancel',
|
||||
'update' => 'Update',
|
||||
'copy' => 'Copy',
|
||||
'copy_to_clipboard' => 'Copy to clipboard',
|
||||
'copied_to_clipboard' => 'Copied to clipboard',
|
||||
'profile' => 'Profile',
|
||||
@ -35,5 +36,6 @@
|
||||
'rename' => 'Rename',
|
||||
'options' => 'Options',
|
||||
'reload' => 'Reload',
|
||||
'some_data_have_changed' => 'Some data have changed. You should'
|
||||
'some_data_have_changed' => 'Some data have changed. You should',
|
||||
'generate' => 'Generate',
|
||||
];
|
@ -15,17 +15,30 @@
|
||||
|
||||
'settings' => 'Settings',
|
||||
'account' => 'Account',
|
||||
'password' => 'Password',
|
||||
'oauth' => 'OAuth',
|
||||
'tokens' => 'Tokens',
|
||||
'options' => 'Options',
|
||||
'confirm' => [
|
||||
|
||||
],
|
||||
'general' => 'General',
|
||||
'security' => 'Security',
|
||||
'profile' => 'Profile',
|
||||
'change_password' => 'Change password',
|
||||
'personal_access_tokens' => 'Personal access tokens',
|
||||
'token_legend' => 'Personal Access Tokens allow any app to authenticate to the 2Fauth API. You should specify the access token as a Bearer token in the authorization header of consumer apps requests.',
|
||||
'generate_new_token' => 'Generate a new token',
|
||||
'revoke' => 'Revoke',
|
||||
'token_revoked' => 'Token successfully revoked',
|
||||
'confirm' => [
|
||||
'revoke' => 'Are you sure you want to revoke this token?',
|
||||
],
|
||||
'make_sure_copy_token' => 'Make sure to copy your personal access token now. You won’t be able to see it again!',
|
||||
'data_input' => 'Data input',
|
||||
'forms' => [
|
||||
'edit_settings' => 'Edit settings',
|
||||
'setting_saved' => 'Settings saved',
|
||||
'new_token' => 'New token',
|
||||
'language' => [
|
||||
'label' => 'Language',
|
||||
'help' => 'Change the language used to translate the app interface.'
|
||||
@ -90,4 +103,4 @@
|
||||
'advanced_form' => 'Advanced form',
|
||||
],
|
||||
|
||||
];
|
||||
];
|
8
resources/sass/app.scss
vendored
8
resources/sass/app.scss
vendored
@ -76,6 +76,10 @@ a:hover {
|
||||
margin-top: 55px;
|
||||
}
|
||||
|
||||
.group-item .tags:not(:last-child) {
|
||||
margin-bottom: inherit;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.accounts {
|
||||
margin-top: 84px;
|
||||
@ -542,6 +546,10 @@ footer .field.is-grouped {
|
||||
background-image: url('data:image/png;base64,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');
|
||||
}
|
||||
|
||||
.pat {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
.fadeInOut-enter-active {
|
||||
animation: fadeIn 500ms
|
||||
|
Loading…
Reference in New Issue
Block a user