From e7a2a7267dbd8a4ee3d266d22249459d028569d6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 22 May 2019 19:13:41 +0300 Subject: [PATCH] Proper clientId/secret/token caching, MastoAPI registration --- .eslintrc.js | 3 +- src/boot/after_store.js | 15 ++- src/components/login_form/login_form.js | 16 ++-- .../oauth_callback/oauth_callback.js | 4 +- src/modules/oauth.js | 21 +++-- src/modules/users.js | 37 +++----- src/services/api/api.service.js | 34 ++++--- .../backend_interactor_service.js | 2 +- src/services/new_api/oauth.js | 94 ++++++++++++------- src/services/new_api/utils.js | 4 +- 10 files changed, 138 insertions(+), 92 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b526df67..7d090208 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,8 +31,7 @@ module.exports = { 'vue/require-prop-types': 1, 'vue/no-use-v-if-with-v-for': 1, 'indent': 1, - 'import/first': 1, // ???? - 'import-first': 1, + 'import/first': 1, 'object-curly-spacing': 1, 'prefer-promise-reject-errors': 1, 'eol-last': 1, diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 603de348..caaede59 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -3,6 +3,8 @@ import VueRouter from 'vue-router' import routes from './routes' import App from '../App.vue' 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 }) => { 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('setClientToken', token.access_token) + commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) + }) +} + const getNodeInfo = async ({ store }) => { try { const res = await window.fetch('/nodeinfo/2.0.json') @@ -228,7 +241,7 @@ const setConfig = async ({ store }) => { const apiConfig = configInfos[0] const staticConfig = configInfos[1] - await setSettings({ store, apiConfig, staticConfig }) + await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store })) } const checkOAuthToken = async ({ store }) => { diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index dc917e47..0097e18a 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -11,22 +11,26 @@ const LoginForm = { }, methods: { oAuthLogin () { - oauthApi.login({ - oauth: this.$store.state.oauth, + const data = { + ...this.$store.state.oauth, instance: this.$store.state.instance.server, commit: this.$store.commit - }) + } + + oauthApi.getOrCreateApp(data) + .then((app) => { oauthApi.login({ ...app, ...data }) }) }, submit () { const data = { - oauth: this.$store.state.oauth, - instance: this.$store.state.instance.server + ...this.$store.state.oauth, + instance: this.$store.state.instance.server, + commit: this.$store.commit } this.clearError() oauthApi.getOrCreateApp(data).then((app) => { oauthApi.getTokenWithCredentials( { - app, + ...app, instance: data.instance, username: this.user.username, password: this.user.password diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js index e3d45ee1..48ddd10d 100644 --- a/src/components/oauth_callback/oauth_callback.js +++ b/src/components/oauth_callback/oauth_callback.js @@ -5,13 +5,13 @@ const oac = { mounted () { if (this.code) { oauth.getToken({ - app: this.$store.state.oauth, + ...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({name: 'friends'}) + this.$router.push({ name: 'friends' }) }) } } diff --git a/src/modules/oauth.js b/src/modules/oauth.js index 144ff830..242e29c3 100644 --- a/src/modules/oauth.js +++ b/src/modules/oauth.js @@ -1,17 +1,26 @@ const oauth = { state: { - client_id: false, - client_secret: false, - token: false + clientId: false, + clientSecret: false, + token: false, + clientToken: false }, mutations: { - setClientData (state, data) { - state.client_id = data.client_id - state.client_secret = data.client_secret + setClientData (state, { clientId, clientSecret }) { + state.clientId = clientId + state.clientSecret = clientSecret + }, + setClientToken (state, token) { + state.clientToken = token }, setToken (state, token) { state.token = token } + }, + getters: { + getToken: state => () => { + return state.token || state.clientToken + } } } diff --git a/src/modules/users.js b/src/modules/users.js index e72a657c..739b8b92 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -3,7 +3,6 @@ import userSearchApi from '../services/new_api/user_search.js' import { compact, map, each, merge, last, concat, uniq } from 'lodash' import { set } from 'vue' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' -import oauthApi from '../services/new_api/oauth' import { humanizeErrors } from './errors' // TODO: Unify with mergeOrAdd in statuses.js @@ -368,31 +367,21 @@ const users = { let rootState = store.rootState - let response = await rootState.api.backendInteractor.register(userInfo) - if (response.ok) { - 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 - }) + try { + let data = await rootState.api.backendInteractor.register(userInfo) store.commit('signUpSuccess') - store.commit('setToken', result.access_token) - store.dispatch('loginUser', result.access_token) - } else { - const data = await response.json() - let errors = JSON.parse(data.error) + store.commit('setToken', data.access_token) + store.dispatch('loginUser', data.access_token) + } catch (e) { + let errors = e.message // replace ap_id with username - if (errors.ap_id) { - errors.username = errors.ap_id - delete errors.ap_id + if (typeof errors === 'object') { + if (errors.ap_id) { + errors.username = errors.ap_id + delete errors.ap_id + } + errors = humanizeErrors(errors) } - errors = humanizeErrors(errors) store.commit('signUpFailure', errors) throw Error(errors) } @@ -406,7 +395,7 @@ const users = { store.dispatch('disconnectFromChat') store.commit('setToken', false) store.dispatch('stopFetching', 'friends') - store.commit('setBackendInteractor', backendInteractorService()) + store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.dispatch('stopFetching', 'notifications') store.commit('clearNotifications') store.commit('resetStatuses') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index c67eccf1..559eb25d 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,6 +1,5 @@ /* eslint-env browser */ const LOGIN_URL = '/api/account/verify_credentials.json' -const REGISTRATION_URL = '/api/account/register.json' const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' @@ -17,6 +16,7 @@ const ACTIVATION_STATUS_URL = screenName => `/api/pleroma/admin/users/${screenNa const ADMIN_USERS_URL = '/api/pleroma/admin/users' const SUGGESTIONS_URL = '/api/v1/suggestions' +const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` @@ -161,19 +161,29 @@ const updateProfile = ({credentials, params}) => { // homepage // location // token -const register = (params) => { - const form = new FormData() - - each(params, (value, key) => { - if (value) { - form.append(key, value) - } - }) - - return fetch(REGISTRATION_URL, { +const register = ({ params, credentials }) => { + const { nickname, ...rest } = params + return fetch(MASTODON_REGISTRATION_URL, { method: 'POST', - body: form + headers: { + ...authHeaders(credentials), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + nickname, + locale: 'xx_XX', + 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()) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 639bcabc..3d6edfc3 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -99,7 +99,7 @@ const backendInteractorService = (credentials) => { const unpinOwnStatus = (id) => apiService.unpinOwnStatus({credentials, id}) 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 updateBg = ({params}) => apiService.updateBg({credentials, params}) const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner}) diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js index 9e656507..4730427d 100644 --- a/src/services/new_api/oauth.js +++ b/src/services/new_api/oauth.js @@ -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 form = new window.FormData() - form.append('client_name', `PleromaFE_${Math.random()}`) - form.append('redirect_uris', `${window.location.origin}/oauth-callback`) + form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`) + form.append('redirect_uris', REDIRECT_URI) 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 }) + .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 form = new window.FormData() - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) + form.append('client_id', clientId) + form.append('client_secret', clientSecret) form.append('grant_type', 'password') form.append('username', username) form.append('password', password) @@ -56,16 +62,32 @@ const getTokenWithCredentials = ({app, instance, username, password}) => { }).then((data) => data.json()) } -const getToken = ({app, instance, code}) => { +const getToken = ({ clientId, clientSecret, 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('client_id', clientId) + form.append('client_secret', clientSecret) 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()) +} + +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, { method: 'POST', body: form diff --git a/src/services/new_api/utils.js b/src/services/new_api/utils.js index 078f392f..6696573b 100644 --- a/src/services/new_api/utils.js +++ b/src/services/new_api/utils.js @@ -5,9 +5,9 @@ const queryParams = (params) => { } const headers = (store) => { - const accessToken = store.state.oauth.token + const accessToken = store.getters.getToken() if (accessToken) { - return {'Authorization': `Bearer ${accessToken}`} + return { 'Authorization': `Bearer ${accessToken}` } } else { return {} }