Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 9e97918fdf..8219ce778d 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -1,293 +1,295 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'
import App from '../App.vue'
import { windowWidth } from '../services/window_utils/window_utils'
const getStatusnetConfig = async ({ store }) => {
try {
const res = await window.fetch('/api/statusnet/config.json')
if (res.ok) {
const data = await res.json()
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
store.dispatch('setInstanceOption', { name: 'name', value: name })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setInstanceOption', { name: 'server', value: server })
store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
// TODO: default values for this stuff, added if to not make it break on
// my dev config out of the box.
if (uploadlimit) {
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
}
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
}
return data.site.pleromafe
} else {
throw (res)
}
} catch (error) {
console.error('Could not load statusnet config, potentially fatal')
console.error(error)
}
}
const getStaticConfig = async () => {
try {
const res = await window.fetch('/static/config.json')
if (res.ok) {
return res.json()
} else {
throw (res)
}
} catch (error) {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(error)
return {}
}
}
const setSettings = async ({ apiConfig, staticConfig, store }) => {
const overrides = window.___pleromafe_dev_overrides || {}
const env = window.___pleromafe_mode.NODE_ENV
// This takes static config and overrides properties that are present in apiConfig
let config = {}
if (overrides.staticConfigPreference && env === 'development') {
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
config = Object.assign({}, apiConfig, staticConfig)
} else {
config = Object.assign({}, staticConfig, apiConfig)
}
const copyInstanceOption = (name) => {
store.dispatch('setInstanceOption', { name, value: config[name] })
}
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
store.dispatch('setInstanceOption', {
name: 'logoMask',
value: typeof config.logoMask === 'undefined'
? true
: config.logoMask
})
store.dispatch('setInstanceOption', {
name: 'logoMargin',
value: typeof config.logoMargin === 'undefined'
? 0
: config.logoMargin
})
store.commit('authFlow/setInitialStrategy', config['loginMethod'])
copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin')
copyInstanceOption('showInstanceSpecificPanel')
copyInstanceOption('minimalScopesMode')
copyInstanceOption('formattingOptionsEnabled')
copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('noAttachmentLinks')
copyInstanceOption('showFeaturesPanel')
if ((config.chatDisabled)) {
store.dispatch('disableChat')
} else {
store.dispatch('initializeSocket')
}
return store.dispatch('setTheme', config['theme'])
}
const getTOS = async ({ store }) => {
try {
const res = await window.fetch('/static/terms-of-service.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load TOS")
console.warn(e)
}
}
const getInstancePanel = async ({ store }) => {
try {
const res = await window.fetch('/instance/panel.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load instance panel")
console.warn(e)
}
}
const getStaticEmoji = async ({ store }) => {
try {
const res = await window.fetch('/static/emoji.json')
if (res.ok) {
const values = await res.json()
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: false, 'utf': values[key] }
})
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load static emoji")
console.warn(e)
}
}
// This is also used to indicate if we have a 'pleroma backend' or not.
// Somewhat weird, should probably be somewhere else.
const getCustomEmoji = async ({ store }) => {
try {
const res = await window.fetch('/api/pleroma/emoji.json')
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: values[key].image_url || values[key] }
})
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
} else {
throw (res)
}
} catch (e) {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
console.warn("Can't load custom emojis, maybe not a Pleroma instance?")
console.warn(e)
}
}
const getNodeInfo = async ({ store }) => {
try {
const res = await window.fetch('/nodeinfo/2.0.json')
if (res.ok) {
const data = await res.json()
const metadata = data.metadata
const features = metadata.features
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
} else {
throw (res)
}
} catch (e) {
console.warn('Could not load nodeinfo')
console.warn(e)
}
}
const setConfig = async ({ store }) => {
// apiConfig, staticConfig
const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()])
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig })
}
const checkOAuthToken = async ({ store }) => {
return new Promise(async (resolve, reject) => {
if (store.state.oauth.token) {
try {
await store.dispatch('loginUser', store.state.oauth.token)
} catch (e) {
console.log(e)
}
}
resolve()
})
}
const afterStoreSetup = async ({ store, i18n }) => {
if (store.state.config.customTheme) {
// This is a hack to deal with async loading of config.json and themes
// See: style_setter.js, setPreset()
window.themeLoaded = true
store.dispatch('setOption', {
name: 'customTheme',
value: store.state.config.customTheme
})
}
const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800)
// Now we can try getting the server settings and logging in
await Promise.all([
checkOAuthToken({ store }),
setConfig({ store }),
getTOS({ store }),
getInstancePanel({ store }),
getStaticEmoji({ store }),
getCustomEmoji({ store }),
getNodeInfo({ store })
])
const router = new VueRouter({
mode: 'history',
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
}
})
+ store.commit('authFlow/setRouter', router)
+
/* eslint-disable no-new */
return new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
})
}
export default afterStoreSetup
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 1a17909911..4c66c8950b 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -1,53 +1,57 @@
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import Settings from 'components/settings/settings.vue'
import Registration from 'components/registration/registration.vue'
import UserSettings from 'components/user_settings/user_settings.vue'
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 TOTPForm from 'components/mfa_form/totp_form.vue'
+import RecoveryForm from 'components/mfa_form/recovery_form.vue'
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'
export default (store) => {
return [
{ name: 'root',
path: '/',
redirect: _to => {
return (store.state.users.currentUser
? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all'
}
},
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
{ name: 'friends', path: '/main/friends', component: FriendsTimeline },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions },
{ name: 'dms', path: '/users/:username/dms', component: DMs },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-token', path: '/registration/:token', component: Registration },
{ 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: 'mfa-totp', path: '/sessions/two-factor', component: TOTPForm },
+ { name: 'mfa-recovery', path: '/sessions/two-factor/recovery', component: RecoveryForm },
{ 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 }) },
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
{ name: 'about', path: '/about', component: About },
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
]
}
diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js
index fbe9b437a9..630bda2808 100644
--- a/src/components/mfa_form/recovery_form.js
+++ b/src/components/mfa_form/recovery_form.js
@@ -1,41 +1,46 @@
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' })
},
+ beforeRouteEnter (to, from, next) {
+ next(vm => {
+ if (!vm.authApp) { vm.$router.push({name: 'login'}) }
+ })
+ },
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'})
})
})
}
}
}
diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue
index 806746f476..e0e2d65bd5 100644
--- a/src/components/mfa_form/recovery_form.vue
+++ b/src/components/mfa_form/recovery_form.vue
@@ -1,41 +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>
diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js
index 6c94fe52bf..e235eecc14 100644
--- a/src/components/mfa_form/totp_form.js
+++ b/src/components/mfa_form/totp_form.js
@@ -1,40 +1,45 @@
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' })
},
+ beforeRouteEnter (to, from, next) {
+ next(vm => {
+ if (!vm.authApp) { vm.$router.push({name: 'login'}) }
+ })
+ },
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'})
})
})
}
}
}
diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue
index d39caa6d1a..c547785e2b 100644
--- a/src/components/mfa_form/totp_form.vue
+++ b/src/components/mfa_form/totp_form.vue
@@ -1,44 +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>
diff --git a/src/modules/auth_flow.js b/src/modules/auth_flow.js
index 86328cf3f6..5fb50efe53 100644
--- a/src/modules/auth_flow.js
+++ b/src/modules/auth_flow.js
@@ -1,89 +1,107 @@
const PASSWORD_STRATEGY = 'password'
const TOKEN_STRATEGY = 'token'
// MFA strategies
const TOTP_STRATEGY = 'totp'
const RECOVERY_STRATEGY = 'recovery'
// initial state
const state = {
+ router: null,
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 = {
+ setRouter (state, router) {
+ if (router) { state.router = router }
+ },
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
+
+ if (state.router.currentRoute.name === 'login') {
+ state.router.push({name: 'mfa-totp'})
+ }
},
requireRecovery (state) {
state.strategy = RECOVERY_STRATEGY
+ if (state.router.currentRoute.name === 'mfa-totp') {
+ state.router.push({name: 'mfa-recovery'})
+ }
},
requireTOTP (state) {
state.strategy = TOTP_STRATEGY
+ if (state.router.currentRoute.name === 'mfa-recovery') {
+ state.router.push({name: 'mfa-totp'})
+ }
},
abortMFA (state) {
resetState(state)
+ if (state.router.currentRoute.name === 'mfa-totp' ||
+ state.router.currentRoute.name === 'mfa-recovery') {
+ state.router.push({name: 'login'})
+ }
}
}
// 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
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 31, 6:04 PM (19 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801642
Default Alt Text
(22 KB)

Event Timeline