Merge branch 'masto-register-app-secret' into 'develop'

Proper clientId/secret/token caching, MastoAPI registration

Closes #554

See merge request pleroma/pleroma-fe!806
This commit is contained in:
HJ 2019-06-16 11:18:21 +00:00
commit 1db3c785d8
10 changed files with 162 additions and 97 deletions

View File

@ -31,8 +31,7 @@ module.exports = {
'vue/require-prop-types': 1, 'vue/require-prop-types': 1,
'vue/no-use-v-if-with-v-for': 1, 'vue/no-use-v-if-with-v-for': 1,
'indent': 1, 'indent': 1,
'import/first': 1, // ???? 'import/first': 1,
'import-first': 1,
'object-curly-spacing': 1, 'object-curly-spacing': 1,
'prefer-promise-reject-errors': 1, 'prefer-promise-reject-errors': 1,
'eol-last': 1, 'eol-last': 1,

View File

@ -3,6 +3,8 @@ import VueRouter from 'vue-router'
import routes from './routes' import routes from './routes'
import App from '../App.vue' import App from '../App.vue'
import { windowWidth } from '../services/window_utils/window_utils' import { windowWidth } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
const getStatusnetConfig = async ({ store }) => { const getStatusnetConfig = async ({ store }) => {
try { try {
@ -188,6 +190,17 @@ const getCustomEmoji = async ({ store }) => {
} }
} }
const getAppSecret = async ({ store }) => {
const { state, commit } = store
const { oauth, instance } = state
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
.then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => {
commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
})
}
const getNodeInfo = async ({ store }) => { const getNodeInfo = async ({ store }) => {
try { try {
const res = await window.fetch('/nodeinfo/2.0.json') const res = await window.fetch('/nodeinfo/2.0.json')
@ -228,14 +241,14 @@ const setConfig = async ({ store }) => {
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig }) await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
} }
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (store.state.oauth.token) { if (store.getters.getUserToken()) {
try { try {
await store.dispatch('loginUser', store.state.oauth.token) await store.dispatch('loginUser', store.getters.getUserToken())
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }

View File

@ -26,23 +26,30 @@ const LoginForm = {
this.isTokenAuth ? this.submitToken() : this.submitPassword() this.isTokenAuth ? this.submitToken() : this.submitPassword()
}, },
submitToken () { submitToken () {
oauthApi.login({ const { clientId } = this.oauth
const data = {
clientId,
instance: this.instance.server,
commit: this.$store.commit
}
oauthApi.getOrCreateApp(data)
.then((app) => { oauthApi.login({ ...app, ...data }) })
},
submitPassword () {
const { clientId } = this.oauth
const data = {
clientId,
oauth: this.oauth, oauth: this.oauth,
instance: this.instance.server, instance: this.instance.server,
commit: this.$store.commit commit: this.$store.commit
})
},
submitPassword () {
const data = {
oauth: this.oauth,
instance: this.instance.server
} }
this.error = false this.error = false
oauthApi.getOrCreateApp(data).then((app) => { oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials( oauthApi.getTokenWithCredentials(
{ {
app, ...app,
instance: data.instance, instance: data.instance,
username: this.user.username, username: this.user.username,
password: this.user.password password: this.user.password

View File

@ -4,14 +4,16 @@ const oac = {
props: ['code'], props: ['code'],
mounted () { mounted () {
if (this.code) { if (this.code) {
const { clientId } = this.$store.state.oauth
oauth.getToken({ oauth.getToken({
app: this.$store.state.oauth, clientId,
instance: this.$store.state.instance.server, instance: this.$store.state.instance.server,
code: this.code code: this.code
}).then((result) => { }).then((result) => {
this.$store.commit('setToken', result.access_token) this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token) this.$store.dispatch('loginUser', result.access_token)
this.$router.push({name: 'friends'}) this.$router.push({ name: 'friends' })
}) })
} }
} }

View File

@ -1,16 +1,39 @@
const oauth = { const oauth = {
state: { state: {
client_id: false, clientId: false,
client_secret: false, clientSecret: false,
token: false /* App token is authentication for app without any user, used mostly for
* MastoAPI's registration of new users, stored so that we can fall back to
* it on logout
*/
appToken: false,
/* User token is authentication for app with user, this is for every calls
* that need authorized user to be successful (i.e. posting, liking etc)
*/
userToken: false
}, },
mutations: { mutations: {
setClientData (state, data) { setClientData (state, { clientId, clientSecret }) {
state.client_id = data.client_id state.clientId = clientId
state.client_secret = data.client_secret state.clientSecret = clientSecret
},
setAppToken (state, token) {
state.appToken = token
}, },
setToken (state, token) { setToken (state, token) {
state.token = token state.userToken = token
}
},
getters: {
getToken: state => () => {
// state.token is userToken with older name, coming from persistent state
// added here for smoother transition, otherwise user will be logged out
return state.userToken || state.token || state.appToken
},
getUserToken: state => () => {
// state.token is userToken with older name, coming from persistent state
// added here for smoother transition, otherwise user will be logged out
return state.userToken || state.token
} }
} }
} }

View File

@ -3,7 +3,6 @@ import userSearchApi from '../services/new_api/user_search.js'
import { compact, map, each, merge, last, concat, uniq } from 'lodash' import { compact, map, each, merge, last, concat, uniq } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import oauthApi from '../services/new_api/oauth'
import { humanizeErrors } from './errors' import { humanizeErrors } from './errors'
// TODO: Unify with mergeOrAdd in statuses.js // TODO: Unify with mergeOrAdd in statuses.js
@ -368,31 +367,21 @@ const users = {
let rootState = store.rootState let rootState = store.rootState
let response = await rootState.api.backendInteractor.register(userInfo) try {
if (response.ok) { let data = await rootState.api.backendInteractor.register(userInfo)
const data = {
oauth: rootState.oauth,
instance: rootState.instance.server
}
let app = await oauthApi.getOrCreateApp(data)
let result = await oauthApi.getTokenWithCredentials({
app,
instance: data.instance,
username: userInfo.username,
password: userInfo.password
})
store.commit('signUpSuccess') store.commit('signUpSuccess')
store.commit('setToken', result.access_token) store.commit('setToken', data.access_token)
store.dispatch('loginUser', result.access_token) store.dispatch('loginUser', data.access_token)
} else { } catch (e) {
const data = await response.json() let errors = e.message
let errors = JSON.parse(data.error)
// replace ap_id with username // replace ap_id with username
if (errors.ap_id) { if (typeof errors === 'object') {
errors.username = errors.ap_id if (errors.ap_id) {
delete errors.ap_id errors.username = errors.ap_id
delete errors.ap_id
}
errors = humanizeErrors(errors)
} }
errors = humanizeErrors(errors)
store.commit('signUpFailure', errors) store.commit('signUpFailure', errors)
throw Error(errors) throw Error(errors)
} }
@ -406,7 +395,7 @@ const users = {
store.dispatch('disconnectFromChat') store.dispatch('disconnectFromChat')
store.commit('setToken', false) store.commit('setToken', false)
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService()) store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetching', 'notifications') store.dispatch('stopFetching', 'notifications')
store.commit('clearNotifications') store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')

View File

@ -1,5 +1,4 @@
/* eslint-env browser */ /* eslint-env browser */
const REGISTRATION_URL = '/api/account/register.json'
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
@ -25,6 +24,7 @@ const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp'
const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp' const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const GET_BACKGROUND_HACK = '/api/account/verify_credentials.json' const GET_BACKGROUND_HACK = '/api/account/verify_credentials.json'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
@ -185,19 +185,29 @@ const updateProfile = ({credentials, params}) => {
// homepage // homepage
// location // location
// token // token
const register = (params) => { const register = ({ params, credentials }) => {
const form = new FormData() const { nickname, ...rest } = params
return fetch(MASTODON_REGISTRATION_URL, {
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
return fetch(REGISTRATION_URL, {
method: 'POST', method: 'POST',
body: form headers: {
...authHeaders(credentials),
'Content-Type': 'application/json'
},
body: JSON.stringify({
nickname,
locale: 'en_US',
agreement: true,
...rest
})
}) })
.then((response) => [response.ok, response])
.then(([ok, response]) => {
if (ok) {
return response.json()
} else {
return response.json().then((error) => { throw new Error(error) })
}
})
} }
const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json()) const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())

View File

@ -103,7 +103,7 @@ const backendInteractorService = (credentials) => {
const unpinOwnStatus = (id) => apiService.unpinOwnStatus({credentials, id}) const unpinOwnStatus = (id) => apiService.unpinOwnStatus({credentials, id})
const getCaptcha = () => apiService.getCaptcha() const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params) const register = (params) => apiService.register({ credentials, params })
const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar}) const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar})
const updateBg = ({params}) => apiService.updateBg({credentials, params}) const updateBg = ({params}) => apiService.updateBg({credentials, params})
const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner}) const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})

View File

@ -1,51 +1,57 @@
import {reduce} from 'lodash' import { reduce } from 'lodash'
const REDIRECT_URI = `${window.location.origin}/oauth-callback`
export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) => {
if (clientId && clientSecret) {
return Promise.resolve({ clientId, clientSecret })
}
const getOrCreateApp = ({oauth, instance}) => {
const url = `${instance}/api/v1/apps` const url = `${instance}/api/v1/apps`
const form = new window.FormData() const form = new window.FormData()
form.append('client_name', `PleromaFE_${Math.random()}`) form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`)
form.append('redirect_uris', `${window.location.origin}/oauth-callback`) form.append('redirect_uris', REDIRECT_URI)
form.append('scopes', 'read write follow') form.append('scopes', 'read write follow')
return window.fetch(url, { return window.fetch(url, {
method: 'POST', method: 'POST',
body: form 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
}) })
.then((data) => data.json())
.then((app) => ({ clientId: app.client_id, clientSecret: app.client_secret }))
.then((app) => commit('setClientData', app) || app)
} }
const getTokenWithCredentials = ({app, instance, username, password}) => { const login = ({ instance, clientId }) => {
const data = {
response_type: 'code',
client_id: clientId,
redirect_uri: 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 = `${instance}/oauth/authorize?${dataString}`
window.location.href = url
}
const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, password }) => {
const url = `${instance}/oauth/token` const url = `${instance}/oauth/token`
const form = new window.FormData() const form = new window.FormData()
form.append('client_id', app.client_id) form.append('client_id', clientId)
form.append('client_secret', app.client_secret) form.append('client_secret', clientSecret)
form.append('grant_type', 'password') form.append('grant_type', 'password')
form.append('username', username) form.append('username', username)
form.append('password', password) form.append('password', password)
@ -56,16 +62,32 @@ const getTokenWithCredentials = ({app, instance, username, password}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const getToken = ({app, instance, code}) => { const getToken = ({ clientId, clientSecret, instance, code }) => {
const url = `${instance}/oauth/token` const url = `${instance}/oauth/token`
const form = new window.FormData() const form = new window.FormData()
form.append('client_id', app.client_id) form.append('client_id', clientId)
form.append('client_secret', app.client_secret) form.append('client_secret', clientSecret)
form.append('grant_type', 'authorization_code') form.append('grant_type', 'authorization_code')
form.append('code', code) form.append('code', code)
form.append('redirect_uri', `${window.location.origin}/oauth-callback`) form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
return window.fetch(url, {
method: 'POST',
body: form
})
.then((data) => data.json())
}
export const getClientToken = ({ clientId, clientSecret, instance }) => {
const url = `${instance}/oauth/token`
const form = new window.FormData()
form.append('client_id', clientId)
form.append('client_secret', clientSecret)
form.append('grant_type', 'client_credentials')
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
return window.fetch(url, { return window.fetch(url, {
method: 'POST', method: 'POST',
body: form body: form

View File

@ -5,9 +5,9 @@ const queryParams = (params) => {
} }
const headers = (store) => { const headers = (store) => {
const accessToken = store.state.oauth.token const accessToken = store.getters.getToken()
if (accessToken) { if (accessToken) {
return {'Authorization': `Bearer ${accessToken}`} return { 'Authorization': `Bearer ${accessToken}` }
} else { } else {
return {} return {}
} }