Merge branch 'oauth' into 'develop'

Move login to oauth.

See merge request pleroma/pleroma-fe!367
This commit is contained in:
lambda 2018-11-13 18:42:07 +00:00
commit 11f8a4f312
19 changed files with 1412 additions and 244 deletions

View File

@ -32,3 +32,9 @@ npm run unit
# Configuration # Configuration
Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings. Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings.
## Options
### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View File

@ -73,6 +73,7 @@ export default {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
logout () { logout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
} }
} }

176
src/boot/after_store.js Normal file
View File

@ -0,0 +1,176 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from '../App.vue'
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 Mentions from '../components/mentions/mentions.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'
const afterStoreSetup = ({store, i18n}) => {
window.fetch('/api/statusnet/config.json')
.then((res) => res.json())
.then((data) => {
const {name, closed: registrationClosed, textlimit, server} = 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 })
var apiConfig = data.site.pleromafe
window.fetch('/static/config.json')
.then((res) => res.json())
.catch((err) => {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(err)
return {}
})
.then((staticConfig) => {
// This takes static config and overrides properties that are present in apiConfig
var config = Object.assign({}, staticConfig, apiConfig)
var theme = (config.theme)
var background = (config.background)
var hidePostStats = (config.hidePostStats)
var hideUserStats = (config.hideUserStats)
var logo = (config.logo)
var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask)
var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin)
var redirectRootNoLogin = (config.redirectRootNoLogin)
var redirectRootLogin = (config.redirectRootLogin)
var chatDisabled = (config.chatDisabled)
var showInstanceSpecificPanel = (config.showInstanceSpecificPanel)
var scopeOptionsEnabled = (config.scopeOptionsEnabled)
var formattingOptionsEnabled = (config.formattingOptionsEnabled)
var collapseMessageWithSubject = (config.collapseMessageWithSubject)
var loginMethod = (config.loginMethod)
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
store.dispatch('setInstanceOption', { name: 'background', value: background })
store.dispatch('setInstanceOption', { name: 'hidePostStats', value: hidePostStats })
store.dispatch('setInstanceOption', { name: 'hideUserStats', value: hideUserStats })
store.dispatch('setInstanceOption', { name: 'logo', value: logo })
store.dispatch('setInstanceOption', { name: 'logoMask', value: logoMask })
store.dispatch('setInstanceOption', { name: 'logoMargin', value: logoMargin })
store.dispatch('setInstanceOption', { name: 'redirectRootNoLogin', value: redirectRootNoLogin })
store.dispatch('setInstanceOption', { name: 'redirectRootLogin', value: redirectRootLogin })
store.dispatch('setInstanceOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
store.dispatch('setInstanceOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
store.dispatch('setInstanceOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled })
store.dispatch('setInstanceOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject })
store.dispatch('setInstanceOption', { name: 'loginMethod', value: loginMethod })
if (chatDisabled) {
store.dispatch('disableChat')
}
const routes = [
{ name: 'root',
path: '/',
redirect: to => {
return (store.state.users.currentUser
? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all'
}},
{ path: '/main/all', component: PublicAndExternalTimeline },
{ path: '/main/public', component: PublicTimeline },
{ path: '/main/friends', component: FriendsTimeline },
{ path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'user-profile', path: '/users/:id', component: UserProfile },
{ name: 'mentions', path: '/:username/mentions', component: Mentions },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }
]
const router = new VueRouter({
mode: 'history',
routes,
scrollBehavior: (to, from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
}
})
/* eslint-disable no-new */
new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
})
})
})
window.fetch('/static/terms-of-service.html')
.then((res) => res.text())
.then((html) => {
store.dispatch('setInstanceOption', { name: 'tos', value: html })
})
window.fetch('/api/pleroma/emoji.json')
.then(
(res) => res.json()
.then(
(values) => {
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: values[key] }
})
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
},
(failure) => {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
}
),
(error) => console.log(error)
)
window.fetch('/static/emoji.json')
.then((res) => res.json())
.then((values) => {
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: false, 'utf': values[key] }
})
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
})
window.fetch('/instance/panel.html')
.then((res) => res.text())
.then((html) => {
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
})
window.fetch('/nodeinfo/2.0.json')
.then((res) => res.json())
.then((data) => {
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') })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
})
}
export default afterStoreSetup

View File

@ -1,22 +1,40 @@
import oauthApi from '../../services/new_api/oauth.js'
const LoginForm = { const LoginForm = {
data: () => ({ data: () => ({
user: {}, user: {},
authError: false authError: false
}), }),
computed: { computed: {
loginMethod () { return this.$store.state.instance.loginMethod },
loggingIn () { return this.$store.state.users.loggingIn }, loggingIn () { return this.$store.state.users.loggingIn },
registrationOpen () { return this.$store.state.instance.registrationOpen } registrationOpen () { return this.$store.state.instance.registrationOpen }
}, },
methods: { methods: {
oAuthLogin () {
oauthApi.login({
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server,
commit: this.$store.commit
})
},
submit () { submit () {
this.$store.dispatch('loginUser', this.user).then( const data = {
() => {}, oauth: this.$store.state.oauth,
(error) => { instance: this.$store.state.instance.server
this.authError = error }
this.user.username = '' oauthApi.getOrCreateApp(data).then((app) => {
this.user.password = '' oauthApi.getTokenWithCredentials(
} {
) app,
instance: data.instance,
username: this.user.username,
password: this.user.password})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
})
} }
} }
} }

View File

@ -5,7 +5,7 @@
{{$t('login.login')}} {{$t('login.login')}}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form v-on:submit.prevent='submit(user)' class='login-form'> <form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
<div class='form-group'> <div class='form-group'>
<label for='username'>{{$t('login.username')}}</label> <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' v-bind:placeholder="$t('login.placeholder')">
@ -20,8 +20,17 @@
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button> <button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
</div> </div>
</div> </div>
<div v-if="authError" class='form-group'> </form>
<div class='alert error'>{{authError}}</div>
<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> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,20 @@
import oauth from '../../services/new_api/oauth.js'
const oac = {
props: ['code'],
mounted () {
if (this.code) {
oauth.getToken({
app: this.$store.state.oauth,
instance: this.$store.state.instance.server,
code: this.code
}).then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
}
}
}
export default oac

View File

@ -0,0 +1,5 @@
<template>
<h1>...</h1>
</template>
<script src="./oauth_callback.js"></script>

View File

@ -1,3 +1,5 @@
import oauthApi from '../../services/new_api/oauth.js'
const registration = { const registration = {
data: () => ({ data: () => ({
user: {}, user: {},
@ -25,9 +27,23 @@ const registration = {
this.$store.state.api.backendInteractor.register(this.user).then( this.$store.state.api.backendInteractor.register(this.user).then(
(response) => { (response) => {
if (response.ok) { if (response.ok) {
this.$store.dispatch('loginUser', this.user) const data = {
this.$router.push('/main/all') oauth: this.$store.state.oauth,
this.registering = false instance: this.$store.state.instance.server
}
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
app,
instance: data.instance,
username: this.user.username,
password: this.user.password})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
})
} else { } else {
this.registering = false this.registering = false
response.json().then((data) => { response.json().then((data) => {

View File

@ -21,6 +21,7 @@
}, },
"login": { "login": {
"login": "Anmelden", "login": "Anmelden",
"description": "Mit OAuth anmelden",
"logout": "Abmelden", "logout": "Abmelden",
"password": "Passwort", "password": "Passwort",
"placeholder": "z.B. lain", "placeholder": "z.B. lain",

View File

@ -21,6 +21,7 @@
}, },
"login": { "login": {
"login": "Log in", "login": "Log in",
"description": "Log in with OAuth",
"logout": "Log out", "logout": "Log out",
"password": "Password", "password": "Password",
"placeholder": "e.g. lain", "placeholder": "e.g. lain",

View File

@ -17,7 +17,9 @@ const saveImmedeatelyActions = [
'clearCurrentUser', 'clearCurrentUser',
'setCurrentUser', 'setCurrentUser',
'setHighlight', 'setHighlight',
'setOption' 'setOption',
'setClientData',
'setToken'
] ]
const defaultStorage = (() => { const defaultStorage = (() => {
@ -43,8 +45,8 @@ export default function createPersistedState ({
storage = defaultStorage, storage = defaultStorage,
subscriber = store => handler => store.subscribe(handler) subscriber = store => handler => store.subscribe(handler)
} = {}) { } = {}) {
return store => { return getState(key, storage).then((savedState) => {
getState(key, storage).then((savedState) => { return store => {
try { try {
if (typeof savedState === 'object') { if (typeof savedState === 'object') {
// build user cache // build user cache
@ -67,36 +69,35 @@ export default function createPersistedState ({
value: store.state.config.customTheme value: store.state.config.customTheme
}) })
} }
if (store.state.users.lastLoginName) { if (store.state.oauth.token) {
store.dispatch('loginUser', {username: store.state.users.lastLoginName, password: 'xxx'}) store.dispatch('loginUser', store.state.oauth.token)
} }
loaded = true loaded = true
} catch (e) { } catch (e) {
console.log("Couldn't load state") console.log("Couldn't load state")
loaded = true loaded = true
} }
}) subscriber(store)((mutation, state) => {
try {
subscriber(store)((mutation, state) => { if (saveImmedeatelyActions.includes(mutation.type)) {
try { setState(key, reducer(state, paths), storage)
if (saveImmedeatelyActions.includes(mutation.type)) { .then(success => {
setState(key, reducer(state, paths), storage) if (typeof success !== 'undefined') {
.then(success => { if (mutation.type === 'setOption') {
if (typeof success !== 'undefined') { store.dispatch('settingsSaved', { success })
if (mutation.type === 'setOption') { }
store.dispatch('settingsSaved', { success })
} }
} }, error => {
}, error => { if (mutation.type === 'setOption') {
if (mutation.type === 'setOption') { store.dispatch('settingsSaved', { error })
store.dispatch('settingsSaved', { error }) }
} })
}) }
} catch (e) {
console.log("Couldn't persist state:")
console.log(e)
} }
} catch (e) { })
console.log("Couldn't persist state:") }
console.log(e) })
}
})
}
} }

View File

@ -1,18 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Vuex from 'vuex' import Vuex from 'vuex'
import App from './App.vue'
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 Mentions from './components/mentions/mentions.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 interfaceModule from './modules/interface.js' import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js' import instanceModule from './modules/instance.js'
@ -21,6 +9,7 @@ import usersModule from './modules/users.js'
import apiModule from './modules/api.js' import apiModule from './modules/api.js'
import configModule from './modules/config.js' import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -31,6 +20,8 @@ import messages from './i18n/messages.js'
import VueChatScroll from 'vue-chat-scroll' import VueChatScroll from 'vue-chat-scroll'
import afterStoreSetup from './boot/after_store.js'
const currentLocale = (window.navigator.language || 'en').split('-')[0] const currentLocale = (window.navigator.language || 'en').split('-')[0]
Vue.use(Vuex) Vue.use(Vuex)
@ -45,29 +36,6 @@ Vue.use(VueTimeago, {
Vue.use(VueI18n) Vue.use(VueI18n)
Vue.use(VueChatScroll) Vue.use(VueChatScroll)
const persistedStateOptions = {
paths: [
'config',
'users.lastLoginName',
'statuses.notifications.maxSavedId'
]
}
const store = new Vuex.Store({
modules: {
interface: interfaceModule,
instance: instanceModule,
statuses: statusesModule,
users: usersModule,
api: apiModule,
config: configModule,
chat: chatModule
},
plugins: [createPersistedState(persistedStateOptions)],
strict: false // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production'
})
const i18n = new VueI18n({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary
locale: currentLocale, locale: currentLocale,
@ -75,155 +43,30 @@ const i18n = new VueI18n({
messages messages
}) })
window.fetch('/api/statusnet/config.json') const persistedStateOptions = {
.then((res) => res.json()) paths: [
.then((data) => { 'config',
const {name, closed: registrationClosed, textlimit, server} = data.site 'users.lastLoginName',
'statuses.notifications.maxSavedId',
store.dispatch('setInstanceOption', { name: 'name', value: name }) 'oauth'
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) ]
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) }
store.dispatch('setInstanceOption', { name: 'server', value: server }) createPersistedState(persistedStateOptions).then((persistedState) => {
const store = new Vuex.Store({
var apiConfig = data.site.pleromafe modules: {
interface: interfaceModule,
window.fetch('/static/config.json') instance: instanceModule,
.then((res) => res.json()) statuses: statusesModule,
.catch((err) => { users: usersModule,
console.warn('Failed to load static/config.json, continuing without it.') api: apiModule,
console.warn(err) config: configModule,
return {} chat: chatModule,
}) oauth: oauthModule
.then((staticConfig) => { },
// This takes static config and overrides properties that are present in apiConfig plugins: [persistedState],
var config = Object.assign({}, staticConfig, apiConfig) strict: false // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production'
var theme = (config.theme)
var background = (config.background)
var hidePostStats = (config.hidePostStats)
var hideUserStats = (config.hideUserStats)
var logo = (config.logo)
var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask)
var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin)
var redirectRootNoLogin = (config.redirectRootNoLogin)
var redirectRootLogin = (config.redirectRootLogin)
var chatDisabled = (config.chatDisabled)
var showInstanceSpecificPanel = (config.showInstanceSpecificPanel)
var scopeOptionsEnabled = (config.scopeOptionsEnabled)
var formattingOptionsEnabled = (config.formattingOptionsEnabled)
var collapseMessageWithSubject = (config.collapseMessageWithSubject)
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
store.dispatch('setInstanceOption', { name: 'background', value: background })
store.dispatch('setInstanceOption', { name: 'hidePostStats', value: hidePostStats })
store.dispatch('setInstanceOption', { name: 'hideUserStats', value: hideUserStats })
store.dispatch('setInstanceOption', { name: 'logo', value: logo })
store.dispatch('setInstanceOption', { name: 'logoMask', value: logoMask })
store.dispatch('setInstanceOption', { name: 'logoMargin', value: logoMargin })
store.dispatch('setInstanceOption', { name: 'redirectRootNoLogin', value: redirectRootNoLogin })
store.dispatch('setInstanceOption', { name: 'redirectRootLogin', value: redirectRootLogin })
store.dispatch('setInstanceOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
store.dispatch('setInstanceOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
store.dispatch('setInstanceOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled })
store.dispatch('setInstanceOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject })
if (chatDisabled) {
store.dispatch('disableChat')
}
const routes = [
{ name: 'root',
path: '/',
redirect: to => {
return (store.state.users.currentUser
? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all'
}},
{ path: '/main/all', component: PublicAndExternalTimeline },
{ path: '/main/public', component: PublicTimeline },
{ path: '/main/friends', component: FriendsTimeline },
{ path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'user-profile', path: '/users/:id', component: UserProfile },
{ name: 'mentions', path: '/:username/mentions', component: Mentions },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }
]
const router = new VueRouter({
mode: 'history',
routes,
scrollBehavior: (to, from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
}
})
/* eslint-disable no-new */
new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
})
})
}) })
window.fetch('/static/terms-of-service.html') afterStoreSetup({store, i18n})
.then((res) => res.text()) })
.then((html) => {
store.dispatch('setInstanceOption', { name: 'tos', value: html })
})
window.fetch('/api/pleroma/emoji.json')
.then(
(res) => res.json()
.then(
(values) => {
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: values[key] }
})
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
},
(failure) => {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
}
),
(error) => console.log(error)
)
window.fetch('/static/emoji.json')
.then((res) => res.json())
.then((values) => {
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: false, 'utf': values[key] }
})
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
})
window.fetch('/instance/panel.html')
.then((res) => res.text())
.then((html) => {
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
})
window.fetch('/nodeinfo/2.0.json')
.then((res) => res.json())
.then((data) => {
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') })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
})

View File

@ -21,6 +21,7 @@ const defaultState = {
hidePostStats: false, hidePostStats: false,
hideUserStats: false, hideUserStats: false,
disableChat: false, disableChat: false,
loginMethod: 'password',
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,

18
src/modules/oauth.js Normal file
View File

@ -0,0 +1,18 @@
const oauth = {
state: {
client_id: false,
client_secret: false,
token: false
},
mutations: {
setClientData (state, data) {
state.client_id = data.client_id
state.client_secret = data.client_secret
},
setToken (state, token) {
state.token = token
}
}
}
export default oauth

View File

@ -82,24 +82,26 @@ const users = {
}, },
logout (store) { logout (store) {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.commit('setToken', false)
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService()) store.commit('setBackendInteractor', backendInteractorService())
}, },
loginUser (store, userCredentials) { loginUser (store, accessToken) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const commit = store.commit const commit = store.commit
commit('beginLogin') commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(userCredentials) store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((response) => { .then((response) => {
if (response.ok) { if (response.ok) {
response.json() response.json()
.then((user) => { .then((user) => {
user.credentials = userCredentials // user.credentials = userCredentials
user.credentials = accessToken
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
// Set our new backend interactor // Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(userCredentials)) commit('setBackendInteractor', backendInteractorService(accessToken))
if (user.token) { if (user.token) {
store.dispatch('initializeSocket', user.token) store.dispatch('initializeSocket', user.token)

View File

@ -52,16 +52,6 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options) return oldfetch(fullUrl, options)
} }
// from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
let utoa = (str) => {
// first we use encodeURIComponent to get percent-encoded UTF-8,
// then we convert the percent encodings into raw bytes which
// can be fed into btoa.
return btoa(encodeURIComponent(str)
.replace(/%([0-9A-F]{2})/g,
(match, p1) => { return String.fromCharCode('0x' + p1) }))
}
// Params // Params
// cropH // cropH
// cropW // cropW
@ -175,9 +165,9 @@ const register = (params) => {
}) })
} }
const authHeaders = (user) => { const authHeaders = (accessToken) => {
if (user && user.username && user.password) { if (accessToken) {
return { 'Authorization': `Basic ${utoa(`${user.username}:${user.password}`)}` } return { 'Authorization': `Bearer ${accessToken}` }
} else { } else {
return { } return { }
} }

View File

@ -0,0 +1,82 @@
import {reduce} from 'lodash'
const getOrCreateApp = ({oauth, instance}) => {
const url = `${instance}/api/v1/apps`
const form = new window.FormData()
form.append('client_name', `PleromaFE_${Math.random()}`)
form.append('redirect_uris', `${window.location.origin}/oauth-callback`)
form.append('scopes', 'read write follow')
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
}
const login = (args) => {
getOrCreateApp(args).then((app) => {
args.commit('setClientData', app)
const data = {
response_type: 'code',
client_id: app.client_id,
redirect_uri: app.redirect_uri,
scope: 'read write follow'
}
const dataString = reduce(data, (acc, v, k) => {
const encoded = `${k}=${encodeURIComponent(v)}`
if (!acc) {
return encoded
} else {
return `${acc}&${encoded}`
}
}, false)
// Do the redirect...
const url = `${args.instance}/oauth/authorize?${dataString}`
window.location.href = url
})
}
const getTokenWithCredentials = ({app, instance, username, password}) => {
const url = `${instance}/oauth/token`
const form = new window.FormData()
form.append('client_id', app.client_id)
form.append('client_secret', app.client_secret)
form.append('grant_type', 'password')
form.append('username', username)
form.append('password', password)
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
}
const getToken = ({app, instance, code}) => {
const url = `${instance}/oauth/token`
const form = new window.FormData()
form.append('client_id', app.client_id)
form.append('client_secret', app.client_secret)
form.append('grant_type', 'authorization_code')
form.append('code', code)
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
}
const oauth = {
login,
getToken,
getTokenWithCredentials,
getOrCreateApp
}
export default oauth

View File

@ -12,5 +12,6 @@
"formattingOptionsEnabled": false, "formattingOptionsEnabled": false,
"collapseMessageWithSubject": false, "collapseMessageWithSubject": false,
"hidePostStats": false, "hidePostStats": false,
"hideUserStats": false "hideUserStats": false,
"loginMethod": "password"
} }

977
yarn.lock

File diff suppressed because it is too large Load Diff