Bladeren bron

Add OAuth Personal Access Token management

Bubka 3 jaren geleden
bovenliggende
commit
55a47a75f4

+ 1 - 1
resources/js/components/Footer.vue

@@ -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 - 0
resources/js/components/SettingTabs.vue

@@ -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>

+ 3 - 1
resources/js/components/index.js

@@ -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 - 0
resources/js/mixins.js

@@ -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' })
+            }
+        }
     }
 
 })

+ 8 - 2
resources/js/routes.js

@@ -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 },

+ 59 - 13
resources/js/views/settings/Account.vue

@@ -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 } });
                     }
                 });

+ 0 - 84
resources/js/views/settings/Index.vue

@@ -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 - 0
resources/js/views/settings/OAuth.vue

@@ -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>

+ 51 - 38
resources/js/views/settings/Options.vue

@@ -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')" />
-
-            <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.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>
+        <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('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>
+        </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 - 0
resources/js/views/settings/PATokens/Create.vue

@@ -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>

+ 0 - 49
resources/js/views/settings/Password.vue

@@ -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>

+ 3 - 1
resources/lang/en/commons.php

@@ -15,6 +15,7 @@ return [
 
     'cancel' => 'Cancel',
     'update' => 'Update',
+    'copy' => 'Copy',
     'copy_to_clipboard' => 'Copy to clipboard',
     'copied_to_clipboard' => 'Copied to clipboard',
     'profile' => 'Profile',
@@ -35,5 +36,6 @@ return [
     '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 - 2
resources/lang/en/settings.php

@@ -15,17 +15,30 @@ return [
 
     '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 @@ return [
         'advanced_form' => 'Advanced form',
     ],
 
-];
+];

File diff suppressed because it is too large
+ 4 - 0
resources/sass/app.scss


Some files were not shown because too many files changed in this diff