forked from AkkomaGang/akkoma-fe
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:
commit
1db3c785d8
10 changed files with 162 additions and 97 deletions
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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' })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue