Merge branch 'feature/2fa' into 'develop'
adds 2FA/two_factor_authentication support See merge request pleroma/pleroma-fe!556
This commit is contained in:
commit
e53f11c30f
@ -15,6 +15,7 @@
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"chromatism": "^3.0.0",
|
||||
|
@ -92,6 +92,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
@ -100,7 +101,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||
copyInstanceOption('formattingOptionsEnabled')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('loginMethod')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
|
@ -13,7 +13,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import UserSearch from 'components/user_search/user_search.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import LoginForm from 'components/login_form/login_form.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
import About from 'components/about/about.vue'
|
||||
@ -42,7 +42,7 @@ export default (store) => {
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
||||
{ name: 'login', path: '/login', component: LoginForm },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
|
||||
|
26
src/components/auth_form/auth_form.js
Normal file
26
src/components/auth_form/auth_form.js
Normal file
@ -0,0 +1,26 @@
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render (createElement) {
|
||||
return createElement('component', { is: this.authForm })
|
||||
},
|
||||
computed: {
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
MFATOTPForm,
|
||||
LoginForm
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthForm
|
@ -1,28 +1,44 @@
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
|
||||
const LoginForm = {
|
||||
data: () => ({
|
||||
user: {},
|
||||
authError: false
|
||||
error: false
|
||||
}),
|
||||
computed: {
|
||||
loginMethod () { return this.$store.state.instance.loginMethod },
|
||||
loggingIn () { return this.$store.state.users.loggingIn },
|
||||
registrationOpen () { return this.$store.state.instance.registrationOpen }
|
||||
isPasswordAuth () { return this.requiredPassword },
|
||||
isTokenAuth () { return this.requiredToken },
|
||||
...mapState({
|
||||
registrationOpen: state => state.instance.registrationOpen,
|
||||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
oauth: state => state.oauth
|
||||
}),
|
||||
...mapGetters(
|
||||
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
oAuthLogin () {
|
||||
...mapMutations('authFlow', ['requireMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
submit () {
|
||||
this.isTokenMethod ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
submitToken () {
|
||||
oauthApi.login({
|
||||
oauth: this.$store.state.oauth,
|
||||
instance: this.$store.state.instance.server,
|
||||
oauth: this.oauth,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
})
|
||||
},
|
||||
submit () {
|
||||
submitPassword () {
|
||||
const data = {
|
||||
oauth: this.$store.state.oauth,
|
||||
instance: this.$store.state.instance.server
|
||||
oauth: this.oauth,
|
||||
instance: this.instance.server
|
||||
}
|
||||
this.clearError()
|
||||
this.error = false
|
||||
|
||||
oauthApi.getOrCreateApp(data).then((app) => {
|
||||
oauthApi.getTokenWithCredentials(
|
||||
{
|
||||
@ -31,24 +47,27 @@ const LoginForm = {
|
||||
username: this.user.username,
|
||||
password: this.user.password
|
||||
}
|
||||
).then(async (result) => {
|
||||
).then((result) => {
|
||||
if (result.error) {
|
||||
this.authError = result.error
|
||||
this.user.password = ''
|
||||
if (result.error === 'mfa_required') {
|
||||
this.requireMFA({app: app, settings: result})
|
||||
} else {
|
||||
this.error = result.error
|
||||
this.focusOnPasswordInput()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
try {
|
||||
await this.$store.dispatch('loginUser', result.access_token)
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({name: 'friends'})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
this.authError = false
|
||||
clearError () { this.error = false },
|
||||
focusOnPasswordInput () {
|
||||
let passwordInput = this.$refs.passwordInput
|
||||
passwordInput.focus()
|
||||
passwordInput.setSelectionRange(0, passwordInput.value.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,53 @@
|
||||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
<div class="panel-heading">
|
||||
{{$t('login.login')}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">{{$t('login.login')}}</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<template v-if="isPasswordAuth">
|
||||
<div class='form-group'>
|
||||
<label for='username'>{{$t('login.username')}}</label>
|
||||
<input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
|
||||
<input :disabled="loggingIn" v-model='user.username'
|
||||
class='form-control' id='username'
|
||||
:placeholder="$t('login.placeholder')">
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='password'>{{$t('login.password')}}</label>
|
||||
<input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'>
|
||||
<input :disabled="loggingIn" v-model='user.password'
|
||||
ref='passwordInput' class='form-control' id='password' type='password'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form">
|
||||
<div class="form-group">
|
||||
<p>{{$t('login.description')}}</p>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-group" v-if="isTokenAuth">
|
||||
<p>{{$t('login.description')}}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="authError" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{authError}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<router-link :to="{name: 'registration'}"
|
||||
v-if='registrationOpen'
|
||||
class='register'>
|
||||
{{$t('login.register')}}
|
||||
</router-link>
|
||||
</div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>
|
||||
{{$t('login.login')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login_form.js" ></script>
|
||||
|
41
src/components/mfa_form/recovery_form.js
Normal file
41
src/components/mfa_form/recovery_form.js
Normal file
@ -0,0 +1,41 @@
|
||||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
code: null,
|
||||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authApp: 'authFlow/app',
|
||||
authSettings: 'authFlow/settings'
|
||||
}),
|
||||
...mapState({ instance: 'instance' })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
clearError () { this.error = false },
|
||||
submit () {
|
||||
const data = {
|
||||
app: this.authApp,
|
||||
instance: this.instance.server,
|
||||
mfaToken: this.authSettings.mfa_token,
|
||||
code: this.code
|
||||
}
|
||||
|
||||
mfaApi.verifyRecoveryCode(data).then((result) => {
|
||||
if (result.error) {
|
||||
this.error = result.error
|
||||
this.code = null
|
||||
return
|
||||
}
|
||||
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
42
src/components/mfa_form/recovery_form.vue
Normal file
42
src/components/mfa_form/recovery_form.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">{{$t('login.heading.recovery')}}</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<div class='form-group'>
|
||||
<label for='code'>{{$t('login.recovery_code')}}</label>
|
||||
<input v-model='code' class='form-control' id='code'>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<a href="#" @click.prevent="requireTOTP">
|
||||
{{$t('login.enter_two_factor_code')}}
|
||||
</a>
|
||||
<br />
|
||||
<a href="#" @click.prevent="abortMFA">
|
||||
{{$t('general.cancel')}}
|
||||
</a>
|
||||
</div>
|
||||
<button type='submit' class='btn btn-default'>
|
||||
{{$t('general.verify')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./recovery_form.js" ></script>
|
40
src/components/mfa_form/totp_form.js
Normal file
40
src/components/mfa_form/totp_form.js
Normal file
@ -0,0 +1,40 @@
|
||||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
export default {
|
||||
data: () => ({
|
||||
code: null,
|
||||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authApp: 'authFlow/app',
|
||||
authSettings: 'authFlow/settings'
|
||||
}),
|
||||
...mapState({ instance: 'instance' })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
clearError () { this.error = false },
|
||||
submit () {
|
||||
const data = {
|
||||
app: this.authApp,
|
||||
instance: this.instance.server,
|
||||
mfaToken: this.authSettings.mfa_token,
|
||||
code: this.code
|
||||
}
|
||||
|
||||
mfaApi.verifyOTPCode(data).then((result) => {
|
||||
if (result.error) {
|
||||
this.error = result.error
|
||||
this.code = null
|
||||
return
|
||||
}
|
||||
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
45
src/components/mfa_form/totp_form.vue
Normal file
45
src/components/mfa_form/totp_form.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">
|
||||
{{$t('login.heading.totp')}}
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<div class='form-group'>
|
||||
<label for='code'>
|
||||
{{$t('login.authentication_code')}}
|
||||
</label>
|
||||
<input v-model='code' class='form-control' id='code'>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<a href="#" @click.prevent="requireRecovery">
|
||||
{{$t('login.enter_recovery_code')}}
|
||||
</a>
|
||||
<br />
|
||||
<a href="#" @click.prevent="abortMFA">
|
||||
{{$t('general.cancel')}}
|
||||
</a>
|
||||
</div>
|
||||
<button type='submit' class='btn btn-default'>
|
||||
{{$t('general.verify')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./totp_form.js"></script>
|
@ -1,13 +1,15 @@
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import AuthForm from '../auth_form/auth_form.js'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const UserPanel = {
|
||||
computed: {
|
||||
user () { return this.$store.state.users.currentUser }
|
||||
signedIn () { return this.user },
|
||||
...mapState({ user: state => state.users.currentUser })
|
||||
},
|
||||
components: {
|
||||
LoginForm,
|
||||
AuthForm,
|
||||
PostStatusForm,
|
||||
UserCard
|
||||
}
|
||||
|
@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<div class="user-panel">
|
||||
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
||||
|
||||
<div v-if="signedIn" key="user-panel" class="panel panel-default signed-in">
|
||||
<UserCard :user="user" :hideBio="true" rounded="top"/>
|
||||
<div class="panel-footer">
|
||||
<post-status-form v-if='user'></post-status-form>
|
||||
</div>
|
||||
</div>
|
||||
<login-form v-if='!user'></login-form>
|
||||
<auth-form v-else key="user-panel"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-panel .signed-in {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
9
src/components/user_settings/confirm.js
Normal file
9
src/components/user_settings/confirm.js
Normal file
@ -0,0 +1,9 @@
|
||||
const Confirm = {
|
||||
props: ['disabled'],
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
confirm () { this.$emit('confirm') },
|
||||
cancel () { this.$emit('cancel') }
|
||||
}
|
||||
}
|
||||
export default Confirm
|
14
src/components/user_settings/confirm.vue
Normal file
14
src/components/user_settings/confirm.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<button class="btn btn-default" @click="confirm" :disabled="disabled">
|
||||
{{$t('general.confirm')}}
|
||||
</button>
|
||||
<button class="btn btn-default" @click="cancel" :disabled="disabled">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./confirm.js">
|
||||
</script>
|
152
src/components/user_settings/mfa.js
Normal file
152
src/components/user_settings/mfa.js
Normal file
@ -0,0 +1,152 @@
|
||||
import RecoveryCodes from './mfa_backup_codes.vue'
|
||||
import TOTP from './mfa_totp.vue'
|
||||
import Confirm from './confirm.vue'
|
||||
import VueQrcode from '@chenfengyuan/vue-qrcode'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const Mfa = {
|
||||
data: () => ({
|
||||
settings: { // current settings of MFA
|
||||
enabled: false,
|
||||
totp: false
|
||||
},
|
||||
setupState: { // setup mfa
|
||||
state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
|
||||
setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
|
||||
},
|
||||
backupCodes: {
|
||||
getNewCodes: false,
|
||||
inProgress: false, // progress of fetch codes
|
||||
codes: []
|
||||
},
|
||||
otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
|
||||
provisioning_uri: '',
|
||||
key: ''
|
||||
},
|
||||
currentPassword: null,
|
||||
otpConfirmToken: null,
|
||||
error: null,
|
||||
readyInit: false
|
||||
}),
|
||||
components: {
|
||||
'recovery-codes': RecoveryCodes,
|
||||
'totp-item': TOTP,
|
||||
'qrcode': VueQrcode,
|
||||
'confirm': Confirm
|
||||
},
|
||||
computed: {
|
||||
canSetupOTP () {
|
||||
return (
|
||||
(this.setupInProgress && this.backupCodesPrepared) ||
|
||||
this.settings.enabled
|
||||
) && !this.settings.totp && !this.setupOTPInProgress
|
||||
},
|
||||
setupInProgress () {
|
||||
return this.setupState.state !== '' && this.setupState.state !== 'complete'
|
||||
},
|
||||
setupOTPInProgress () {
|
||||
return this.setupState.state === 'setupOTP' && !this.completedOTP
|
||||
},
|
||||
prepareOTP () {
|
||||
return this.setupState.setupOTPState === 'prepare'
|
||||
},
|
||||
confirmOTP () {
|
||||
return this.setupState.setupOTPState === 'confirm'
|
||||
},
|
||||
completedOTP () {
|
||||
return this.setupState.setupOTPState === 'completed'
|
||||
},
|
||||
backupCodesPrepared () {
|
||||
return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
|
||||
},
|
||||
confirmNewBackupCodes () {
|
||||
return this.backupCodes.getNewCodes
|
||||
},
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
activateOTP () {
|
||||
if (!this.settings.enabled) {
|
||||
this.setupState.state = 'getBackupcodes'
|
||||
this.fetchBackupCodes()
|
||||
}
|
||||
},
|
||||
fetchBackupCodes () {
|
||||
this.backupCodes.inProgress = true
|
||||
this.backupCodes.codes = []
|
||||
|
||||
return this.backendInteractor.generateMfaBackupCodes()
|
||||
.then((res) => {
|
||||
this.backupCodes.codes = res.codes
|
||||
this.backupCodes.inProgress = false
|
||||
})
|
||||
},
|
||||
getBackupCodes () { // get a new backup codes
|
||||
this.backupCodes.getNewCodes = true
|
||||
},
|
||||
confirmBackupCodes () { // confirm getting new backup codes
|
||||
this.fetchBackupCodes().then((res) => {
|
||||
this.backupCodes.getNewCodes = false
|
||||
})
|
||||
},
|
||||
cancelBackupCodes () { // cancel confirm form of new backup codes
|
||||
this.backupCodes.getNewCodes = false
|
||||
},
|
||||
|
||||
// Setup OTP
|
||||
setupOTP () { // prepare setup OTP
|
||||
this.setupState.state = 'setupOTP'
|
||||
this.setupState.setupOTPState = 'prepare'
|
||||
this.backendInteractor.mfaSetupOTP()
|
||||
.then((res) => {
|
||||
this.otpSettings = res
|
||||
this.setupState.setupOTPState = 'confirm'
|
||||
})
|
||||
},
|
||||
doConfirmOTP () { // handler confirm enable OTP
|
||||
this.error = null
|
||||
this.backendInteractor.mfaConfirmOTP({
|
||||
token: this.otpConfirmToken,
|
||||
password: this.currentPassword
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
this.error = res.error
|
||||
return
|
||||
}
|
||||
this.completeSetup()
|
||||
})
|
||||
},
|
||||
|
||||
completeSetup () {
|
||||
this.setupState.setupOTPState = 'complete'
|
||||
this.setupState.state = 'complete'
|
||||
this.currentPassword = null
|
||||
this.error = null
|
||||
this.fetchSettings()
|
||||
},
|
||||
cancelSetup () { // cancel setup
|
||||
this.setupState.setupOTPState = ''
|
||||
this.setupState.state = ''
|
||||
this.currentPassword = null
|
||||
this.error = null
|
||||
},
|
||||
// end Setup OTP
|
||||
|
||||
// fetch settings from server
|
||||
async fetchSettings () {
|
||||
let result = await this.backendInteractor.fetchSettingsMFA()
|
||||
this.settings = result.settings
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchSettings().then(() => {
|
||||
this.readyInit = true
|
||||
})
|
||||
}
|
||||
}
|
||||
export default Mfa
|
121
src/components/user_settings/mfa.vue
Normal file
121
src/components/user_settings/mfa.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="setting-item mfa-settings" v-if="readyInit">
|
||||
|
||||
<div class="mfa-heading">
|
||||
<h2>{{$t('settings.mfa.title')}}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="setting-item" v-if="!setupInProgress">
|
||||
<!-- Enabled methods -->
|
||||
<h3>{{$t('settings.mfa.authentication_methods')}}</h3>
|
||||
<totp-item :settings="settings" @deactivate="fetchSettings" @activate="activateOTP"/>
|
||||
<br />
|
||||
|
||||
<div v-if="settings.enabled"> <!-- backup codes block-->
|
||||
<recovery-codes :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" />
|
||||
<button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes">
|
||||
{{$t('settings.mfa.generate_new_recovery_codes')}}
|
||||
</button>
|
||||
|
||||
<div v-if="confirmNewBackupCodes">
|
||||
<confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes"
|
||||
:disabled="backupCodes.inProgress">
|
||||
<p class="warning">{{$t('settings.mfa.warning_of_generate_new_codes')}}</p>
|
||||
</confirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="setupInProgress"> <!-- setup block-->
|
||||
|
||||
<h3>{{$t('settings.mfa.setup_otp')}}</h3>
|
||||
|
||||
<recovery-codes :backup-codes="backupCodes" v-if="!setupOTPInProgress"/>
|
||||
|
||||
|
||||
<button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" v-if="canSetupOTP" @click="setupOTP">
|
||||
{{$t('settings.mfa.setup_otp')}}
|
||||
</button>
|
||||
|
||||
<template v-if="setupOTPInProgress">
|
||||
<i v-if="prepareOTP">{{$t('settings.mfa.wait_pre_setup_otp')}}</i>
|
||||
|
||||
<div v-if="confirmOTP">
|
||||
<div class="setup-otp">
|
||||
<div class="qr-code">
|
||||
<h4>{{$t('settings.mfa.scan.title')}}</h4>
|
||||
<p>{{$t('settings.mfa.scan.desc')}}</p>
|
||||
<qrcode :value="otpSettings.provisioning_uri" :options="{ width: 200 }"></qrcode>
|
||||
<p>
|
||||
{{$t('settings.mfa.scan.secret_code')}}:
|
||||
{{otpSettings.key}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="verify">
|
||||
<h4>{{$t('general.verify')}}</h4>
|
||||
<p>{{$t('settings.mfa.verify.desc')}}</p>
|
||||
<input type="text" v-model="otpConfirmToken">
|
||||
|
||||
<p>{{$t('settings.enter_current_password_to_confirm')}}:</p>
|
||||
<input type="password" v-model="currentPassword">
|
||||
<div class="confirm-otp-actions">
|
||||
<button class="btn btn-default" @click="doConfirmOTP">
|
||||
{{$t('settings.mfa.confirm_and_enable')}}
|
||||
</button>
|
||||
<button class="btn btn-default" @click="cancelSetup">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert error" v-if="error">{{error}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mfa.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.setup-otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
.qr-code {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.verify { flex: 1; }
|
||||
.error { margin: 4px 0 0 0; }
|
||||
.confirm-otp-actions {
|
||||
button {
|
||||
width: 15em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
17
src/components/user_settings/mfa_backup_codes.js
Normal file
17
src/components/user_settings/mfa_backup_codes.js
Normal file
@ -0,0 +1,17 @@
|
||||
export default {
|
||||
props: {
|
||||
backupCodes: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
inProgress: false,
|
||||
codes: []
|
||||
})
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
computed: {
|
||||
inProgress () { return this.backupCodes.inProgress },
|
||||
ready () { return this.backupCodes.codes.length > 0 },
|
||||
displayTitle () { return this.inProgress || this.ready }
|
||||
}
|
||||
}
|
22
src/components/user_settings/mfa_backup_codes.vue
Normal file
22
src/components/user_settings/mfa_backup_codes.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4 v-if="displayTitle">{{$t('settings.mfa.recovery_codes')}}</h4>
|
||||
<i v-if="inProgress">{{$t('settings.mfa.waiting_a_recovery_codes')}}</i>
|
||||
<template v-if="ready">
|
||||
<p class="alert warning">{{$t('settings.mfa.recovery_codes_warning')}}</p>
|
||||
<ul class="backup-codes"><li v-for="code in backupCodes.codes">{{code}}</li></ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
</style>
|
49
src/components/user_settings/mfa_totp.js
Normal file
49
src/components/user_settings/mfa_totp.js
Normal file
@ -0,0 +1,49 @@
|
||||
import Confirm from './confirm.vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['settings'],
|
||||
data: () => ({
|
||||
error: false,
|
||||
currentPassword: '',
|
||||
deactivate: false,
|
||||
inProgress: false // progress peform request to disable otp method
|
||||
}),
|
||||
components: {
|
||||
'confirm': Confirm
|
||||
},
|
||||
computed: {
|
||||
isActivated () {
|
||||
return this.settings.totp
|
||||
},
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
doActivate () {
|
||||
this.$emit('activate')
|
||||
},
|
||||
cancelDeactivate () { this.deactivate = false },
|
||||
doDeactivate () {
|
||||
this.error = null
|
||||
this.deactivate = true
|
||||
},
|
||||
confirmDeactivate () { // confirm deactivate TOTP method
|
||||
this.error = null
|
||||
this.inProgress = true
|
||||
this.backendInteractor.mfaDisableOTP({
|
||||
password: this.currentPassword
|
||||
})
|
||||
.then((res) => {
|
||||
this.inProgress = false
|
||||
if (res.error) {
|
||||
this.error = res.error
|
||||
return
|
||||
}
|
||||
this.deactivate = false
|
||||
this.$emit('deactivate')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
23
src/components/user_settings/mfa_totp.vue
Normal file
23
src/components/user_settings/mfa_totp.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="method-item">
|
||||
<strong>{{$t('settings.mfa.otp')}}</strong>
|
||||
<button class="btn btn-default" v-if="!isActivated" @click="doActivate">
|
||||
{{$t('general.enable')}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" :disabled="deactivate" @click="doDeactivate"
|
||||
v-if="isActivated">
|
||||
{{$t('general.disable')}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<confirm @confirm="confirmDeactivate" @cancel="cancelDeactivate"
|
||||
:disabled="inProgress" v-if="deactivate">
|
||||
{{$t('settings.enter_current_password_to_confirm')}}:
|
||||
<input type="password" v-model="currentPassword">
|
||||
</confirm>
|
||||
<div class="alert error" v-if="error">{{error}}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_totp.js"></script>
|
@ -17,6 +17,7 @@ import Importer from '../importer/importer.vue'
|
||||
import Exporter from '../exporter/exporter.vue'
|
||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||
import userSearchApi from '../../services/new_api/user_search.js'
|
||||
import Mfa from './mfa.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
@ -75,7 +76,8 @@ const UserSettings = {
|
||||
MuteCard,
|
||||
ProgressButton,
|
||||
Importer,
|
||||
Exporter
|
||||
Exporter,
|
||||
Mfa
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
@ -152,7 +152,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mfa />
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.delete_account')}}</h2>
|
||||
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
|
||||
|
@ -27,7 +27,11 @@
|
||||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"confirm": "Confirm",
|
||||
"verify": "Verify"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
@ -48,7 +52,15 @@
|
||||
"placeholder": "e.g. lain",
|
||||
"register": "Register",
|
||||
"username": "Username",
|
||||
"hint": "Log in to join the discussion"
|
||||
"hint": "Log in to join the discussion",
|
||||
"authentication_code": "Authentication code",
|
||||
"enter_recovery_code": "Enter a recovery code",
|
||||
"enter_two_factor_code": "Enter a two-factor code",
|
||||
"recovery_code": "Recovery code",
|
||||
"heading" : {
|
||||
"totp" : "Two-factor authentication",
|
||||
"recovery" : "Two-factor recovery"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Previous",
|
||||
@ -138,6 +150,29 @@
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "App name",
|
||||
"security": "Security",
|
||||
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Setup OTP",
|
||||
"wait_pre_setup_otp" : "presetting OTP",
|
||||
"confirm_and_enable" : "Confirm & enable OTP",
|
||||
"title": "Two-factor Authentication",
|
||||
"generate_new_recovery_codes" : "Generate new recovery codes",
|
||||
"warning_of_generate_new_codes" : "When you generate new recovery codes, your old codes won’t work anymore.",
|
||||
"recovery_codes" : "Recovery codes.",
|
||||
"waiting_a_recovery_codes": "Receiving backup codes...",
|
||||
"recovery_codes_warning" : "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.",
|
||||
"authentication_methods" : "Authentication methods",
|
||||
"scan": {
|
||||
"title": "Scan",
|
||||
"desc": "Using your two-factor app, scan this QR code or enter text key:",
|
||||
"secret_code": "Key"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "Attachments",
|
||||
"attachments": "Attachments",
|
||||
"autoload": "Enable automatic loading when scrolled to the bottom",
|
||||
|
@ -9,7 +9,11 @@
|
||||
"general": {
|
||||
"apply": "Применить",
|
||||
"submit": "Отправить",
|
||||
"cancel": "Отмена"
|
||||
"cancel": "Отмена",
|
||||
"disable": "Оключить",
|
||||
"enable": "Включить",
|
||||
"confirm": "Подтвердить",
|
||||
"verify": "Проверить"
|
||||
},
|
||||
"login": {
|
||||
"login": "Войти",
|
||||
@ -17,7 +21,15 @@
|
||||
"password": "Пароль",
|
||||
"placeholder": "e.c. lain",
|
||||
"register": "Зарегистрироваться",
|
||||
"username": "Имя пользователя"
|
||||
"username": "Имя пользователя",
|
||||
"authentication_code": "Код аутентификации",
|
||||
"enter_recovery_code": "Ввести код восстановления",
|
||||
"enter_two_factor_code": "Ввести код аутентификации",
|
||||
"recovery_code": "Код восстановления",
|
||||
"heading" : {
|
||||
"TotpForm" : "Двухфакторная аутентификация",
|
||||
"RecoveryForm" : "Two-factor recovery"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"back": "Назад",
|
||||
@ -79,6 +91,28 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"enter_current_password_to_confirm": "Введите свой текущий пароль",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Настройка OTP",
|
||||
"wait_pre_setup_otp" : "предварительная настройка OTP",
|
||||
"confirm_and_enable" : "Подтвердить и включить OTP",
|
||||
"title": "Двухфакторная аутентификация",
|
||||
"generate_new_recovery_codes" : "Получить новые коды востановления",
|
||||
"warning_of_generate_new_codes" : "После получения новых кодов восстановления, старые больше не будут работать.",
|
||||
"recovery_codes" : "Коды восстановления.",
|
||||
"waiting_a_recovery_codes": "Получение кодов восстановления ...",
|
||||
"recovery_codes_warning" : "Запишите эти коды и держите в безопасном месте - иначе вы их больше не увидите. Если вы потеряете доступ к OTP приложению - без резервных кодов вы больше не сможете залогиниться.",
|
||||
"authentication_methods" : "Методы аутентификации",
|
||||
"scan": {
|
||||
"title": "Сканирование",
|
||||
"desc": "Используйте приложение для двухэтапной аутентификации для сканирования этого QR-код или введите текстовый ключ:",
|
||||
"secret_code": "Ключ"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "Чтобы включить двухэтапную аутентификации, введите код из вашего приложение для двухэтапной аутентификации:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "Прикреплённые файлы",
|
||||
"attachments": "Вложения",
|
||||
"autoload": "Включить автоматическую загрузку при прокрутке вниз",
|
||||
|
@ -10,6 +10,7 @@ import apiModule from './modules/api.js'
|
||||
import configModule from './modules/config.js'
|
||||
import chatModule from './modules/chat.js'
|
||||
import oauthModule from './modules/oauth.js'
|
||||
import authFlowModule from './modules/auth_flow.js'
|
||||
import mediaViewerModule from './modules/media_viewer.js'
|
||||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
import reportsModule from './modules/reports.js'
|
||||
@ -77,6 +78,7 @@ const persistedStateOptions = {
|
||||
config: configModule,
|
||||
chat: chatModule,
|
||||
oauth: oauthModule,
|
||||
authFlow: authFlowModule,
|
||||
mediaViewer: mediaViewerModule,
|
||||
oauthTokens: oauthTokensModule,
|
||||
reports: reportsModule
|
||||
|
89
src/modules/auth_flow.js
Normal file
89
src/modules/auth_flow.js
Normal file
@ -0,0 +1,89 @@
|
||||
const PASSWORD_STRATEGY = 'password'
|
||||
const TOKEN_STRATEGY = 'token'
|
||||
|
||||
// MFA strategies
|
||||
const TOTP_STRATEGY = 'totp'
|
||||
const RECOVERY_STRATEGY = 'recovery'
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
app: null,
|
||||
settings: {},
|
||||
strategy: PASSWORD_STRATEGY,
|
||||
initStrategy: PASSWORD_STRATEGY // default strategy from config
|
||||
}
|
||||
|
||||
const resetState = (state) => {
|
||||
state.strategy = state.initStrategy
|
||||
state.settings = {}
|
||||
state.app = null
|
||||
}
|
||||
|
||||
// getters
|
||||
const getters = {
|
||||
app: (state, getters) => {
|
||||
return state.app
|
||||
},
|
||||
settings: (state, getters) => {
|
||||
return state.settings
|
||||
},
|
||||
requiredPassword: (state, getters, rootState) => {
|
||||
return state.strategy === PASSWORD_STRATEGY
|
||||
},
|
||||
requiredToken: (state, getters, rootState) => {
|
||||
return state.strategy === TOKEN_STRATEGY
|
||||
},
|
||||
requiredTOTP: (state, getters, rootState) => {
|
||||
return state.strategy === TOTP_STRATEGY
|
||||
},
|
||||
requiredRecovery: (state, getters, rootState) => {
|
||||
return state.strategy === RECOVERY_STRATEGY
|
||||
}
|
||||
}
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
setInitialStrategy (state, strategy) {
|
||||
if (strategy) {
|
||||
state.initStrategy = strategy
|
||||
state.strategy = strategy
|
||||
}
|
||||
},
|
||||
requirePassword (state) {
|
||||
state.strategy = PASSWORD_STRATEGY
|
||||
},
|
||||
requireToken (state) {
|
||||
state.strategy = TOKEN_STRATEGY
|
||||
},
|
||||
requireMFA (state, {app, settings}) {
|
||||
state.settings = settings
|
||||
state.app = app
|
||||
state.strategy = TOTP_STRATEGY // default strategy of MFA
|
||||
},
|
||||
requireRecovery (state) {
|
||||
state.strategy = RECOVERY_STRATEGY
|
||||
},
|
||||
requireTOTP (state) {
|
||||
state.strategy = TOTP_STRATEGY
|
||||
},
|
||||
abortMFA (state) {
|
||||
resetState(state)
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
async login ({state, dispatch, commit}, {access_token}) {
|
||||
commit('setToken', access_token, { root: true })
|
||||
await dispatch('loginUser', access_token, { root: true })
|
||||
resetState(state)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
@ -27,7 +27,6 @@ const defaultState = {
|
||||
scopeCopy: true,
|
||||
subjectLineBehavior: 'email',
|
||||
postContentType: 'text/plain',
|
||||
loginMethod: 'password',
|
||||
nsfwCensorImage: undefined,
|
||||
vapidPublicKey: undefined,
|
||||
noAttachmentLinks: false,
|
||||
|
@ -17,6 +17,13 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
|
||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||
|
||||
const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa'
|
||||
const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes'
|
||||
|
||||
const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp'
|
||||
const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp'
|
||||
const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
|
||||
|
||||
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
|
||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
||||
@ -649,6 +656,51 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
|
||||
.then((response) => response.json())
|
||||
}
|
||||
|
||||
const settingsMFA = ({credentials}) => {
|
||||
return fetch(MFA_SETTINGS_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'GET'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const mfaDisableOTP = ({credentials, password}) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
|
||||
return fetch(MFA_DISABLE_OTP_URL, {
|
||||
body: form,
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(credentials)
|
||||
})
|
||||
.then((response) => response.json())
|
||||
}
|
||||
|
||||
const mfaConfirmOTP = ({credentials, password, token}) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
form.append('code', token)
|
||||
|
||||
return fetch(MFA_CONFIRM_OTP_URL, {
|
||||
body: form,
|
||||
headers: authHeaders(credentials),
|
||||
method: 'POST'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const mfaSetupOTP = ({credentials}) => {
|
||||
return fetch(MFA_SETUP_OTP_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'GET'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const generateMfaBackupCodes = ({credentials}) => {
|
||||
return fetch(MFA_BACKUP_CODES_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'GET'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const fetchMutes = ({credentials}) => {
|
||||
return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
|
||||
.then((users) => users.map(parseUser))
|
||||
@ -776,6 +828,11 @@ const apiService = {
|
||||
importFollows,
|
||||
deleteAccount,
|
||||
changePassword,
|
||||
settingsMFA,
|
||||
mfaDisableOTP,
|
||||
generateMfaBackupCodes,
|
||||
mfaSetupOTP,
|
||||
mfaConfirmOTP,
|
||||
fetchFollowRequests,
|
||||
approveUser,
|
||||
denyUser,
|
||||
|
@ -116,6 +116,12 @@ const backendInteractorService = (credentials) => {
|
||||
const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password})
|
||||
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
|
||||
|
||||
const fetchSettingsMFA = () => apiService.settingsMFA({credentials})
|
||||
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({credentials})
|
||||
const mfaSetupOTP = () => apiService.mfaSetupOTP({credentials})
|
||||
const mfaConfirmOTP = ({password, token}) => apiService.mfaConfirmOTP({credentials, password, token})
|
||||
const mfaDisableOTP = ({password}) => apiService.mfaDisableOTP({credentials, password})
|
||||
|
||||
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id})
|
||||
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id})
|
||||
const reportUser = (params) => apiService.reportUser({credentials, ...params})
|
||||
@ -166,6 +172,11 @@ const backendInteractorService = (credentials) => {
|
||||
importFollows,
|
||||
deleteAccount,
|
||||
changePassword,
|
||||
fetchSettingsMFA,
|
||||
generateMfaBackupCodes,
|
||||
mfaSetupOTP,
|
||||
mfaConfirmOTP,
|
||||
mfaDisableOTP,
|
||||
fetchFollowRequests,
|
||||
approveUser,
|
||||
denyUser,
|
||||
|
38
src/services/new_api/mfa.js
Normal file
38
src/services/new_api/mfa.js
Normal file
@ -0,0 +1,38 @@
|
||||
const verifyOTPCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const mfa = {
|
||||
verifyOTPCode,
|
||||
verifyRecoveryCode
|
||||
}
|
||||
|
||||
export default mfa
|
@ -71,12 +71,45 @@ const getToken = ({app, instance, code}) => {
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const verifyOTPCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const oauth = {
|
||||
login,
|
||||
getToken,
|
||||
getTokenWithCredentials,
|
||||
getOrCreateApp
|
||||
getOrCreateApp,
|
||||
verifyOTPCode,
|
||||
verifyRecoveryCode
|
||||
}
|
||||
|
||||
export default oauth
|
||||
|
Loading…
Reference in New Issue
Block a user