From d2dcdfbd80268532f093268f39a2baf9e711ea3a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 17 Jun 2026 19:04:05 +0300 Subject: [PATCH] move oauth api --- src/api/mfa.js | 5 +- src/api/oauth.js | 138 ++++++++++++ src/components/login_form/login_form.js | 64 +++--- src/components/mfa_form/recovery_form.js | 4 +- src/components/mfa_form/totp_form.js | 4 +- .../oauth_callback/oauth_callback.js | 26 ++- src/modules/users.js | 4 +- src/services/new_api/oauth.js | 198 ------------------ src/stores/announcements.js | 22 +- src/stores/oauth.js | 20 +- 10 files changed, 204 insertions(+), 281 deletions(-) create mode 100644 src/api/oauth.js delete mode 100644 src/services/new_api/oauth.js diff --git a/src/api/mfa.js b/src/api/mfa.js index ff5df6560..8c1677fbe 100644 --- a/src/api/mfa.js +++ b/src/api/mfa.js @@ -16,7 +16,7 @@ export const verifyOTPCode = ({ formData.append('challenge_type', 'totp') return promisedRequest({ - url: '/oauth/mfa/challenge' + url: '/oauth/mfa/challenge', method: 'POST', formData, }) @@ -29,7 +29,6 @@ export const verifyRecoveryCode = ({ mfaToken, code, }) => { - const url = `${instance}` const formData = new window.FormData() formData.append('client_id', clientId) @@ -39,7 +38,7 @@ export const verifyRecoveryCode = ({ formData.append('challenge_type', 'recovery') return promisedRequest({ - url: '/oauth/mfa/challenge' + url: `${instance}/oauth/mfa/challenge`, method: 'POST', formData, }) diff --git a/src/api/oauth.js b/src/api/oauth.js new file mode 100644 index 000000000..ce4c2f6fc --- /dev/null +++ b/src/api/oauth.js @@ -0,0 +1,138 @@ +import { reduce } from 'lodash' + +import { paramsString, promisedRequest } from './helpers.js' + +import { StatusCodeError } from 'src/services/errors/errors.js' + +const REDIRECT_URI = `${window.location.origin}/oauth-callback` + +export const createApp = ({ instance }) => { + const formData = new window.FormData() + + formData.append('client_name', 'PleromaFE') + formData.append('website', 'https://pleroma.social') + formData.append('redirect_uris', REDIRECT_URI) + formData.append('scopes', 'read write follow push admin') + + return promisedRequest({ + url: `${instance}/api/v1/apps`, + method: 'POST', + formData, + }).then(({ data, ...rest }) => ({ + ...rest, + data: { + ...data, + clientId: data.client_id, + clientSecret: data.client_secret, + }, + })) +} + +export const getLoginUrl = ({ instance, clientId }) => { + const data = { + responseType: 'code', + clientId, + redirectUri: REDIRECT_URI, + scope: 'read write follow push admin', + } + + return `${instance}/oauth/authorize${paramsString(data)}` +} + +export const getTokenWithCredentials = ({ + clientId, + clientSecret, + instance, + username, + password, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'password') + formData.append('username', username) + formData.append('password', password) + + return promisedRequest({ + url: `${instance}/oauth/token`, + method: 'POST', + formData, + }) +} + +export const getToken = ({ clientId, clientSecret, instance, code }) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'authorization_code') + formData.append('code', code) + formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return promisedRequest({ + url: `${instance}/oauth/token`, + method: 'POST', + formData, + }) +} + +export const getClientToken = ({ clientId, clientSecret, instance }) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'client_credentials') + formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return promisedRequest({ + url: `${instance}/oauth/token`, + method: 'POST', + formData, + }) +} +export const verifyOTPCode = ({ app, instance, mfaToken, code }) => { + const formData = new window.FormData() + + formData.append('client_id', app.client_id) + formData.append('client_secret', app.client_secret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'totp') + + return promisedRequest({ + url: `${instance}/oauth/mfa/challenge`, + method: 'POST', + formData, + }) +} + +export const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => { + const formData = new window.FormData() + + formData.append('client_id', app.client_id) + formData.append('client_secret', app.client_secret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'recovery') + + return promisedRequest({ + url: `${instance}/oauth/mfa/challenge`, + method: 'POST', + formData, + }) +} + +export const revokeToken = ({ app, instance, token }) => { + const formData = new window.FormData() + + formData.append('client_id', app.clientId) + formData.append('client_secret', app.clientSecret) + formData.append('token', token) + + return promisedRequest({ + url: `${instance}/oauth/revoke`, + method: 'POST', + formData, + }) +} diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index b16fb8d2f..8f705f269 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,12 +1,12 @@ import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia' import { mapState } from 'vuex' -import oauthApi from '../../services/new_api/oauth.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -35,19 +35,13 @@ const LoginForm = { this.isTokenAuth ? this.submitToken() : this.submitPassword() }, submitToken() { - const data = { - instance: this.server, - commit: this.$store.commit, - } - // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. this.ensureAppToken().then(() => { - const app = { + window.location.href = getLoginUrl({ clientId: this.clientId, - clientSecret: this.clientSecret, - } - oauthApi.login({ ...app, ...data }) + instance: this.server, + }) }) }, submitPassword() { @@ -56,37 +50,31 @@ const LoginForm = { // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. this.ensureAppToken().then(() => { - const app = { + getTokenWithCredentials({ clientId: this.clientId, clientSecret: this.clientSecret, - } - - oauthApi - .getTokenWithCredentials({ - ...app, - instance: this.server, - username: this.user.username, - password: this.user.password, - }) - .then((result) => { - if (result.error) { - if (result.error === 'mfa_required') { - this.requireMFA({ settings: result }) - } else if (result.identifier === 'password_reset_required') { - this.$router.push({ - name: 'password-reset', - params: { passwordResetRequested: true }, - }) - } else { - this.error = result.error - this.focusOnPasswordInput() - } - return + instance: this.server, + username: this.user.username, + password: this.user.password, + }).then((result) => { + if (result.error) { + if (result.error === 'mfa_required') { + this.requireMFA({ settings: result }) + } else if (result.identifier === 'password_reset_required') { + this.$router.push({ + name: 'password-reset', + params: { passwordResetRequested: true }, + }) + } else { + this.error = result.error + this.focusOnPasswordInput() } - this.login(result).then(() => { - this.$router.push({ name: 'friends' }) - }) + return + } + this.login(result).then(() => { + this.$router.push({ name: 'friends' }) }) + }) }) }, clearError() { diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js index 46e38d29c..e04218bd8 100644 --- a/src/components/mfa_form/recovery_form.js +++ b/src/components/mfa_form/recovery_form.js @@ -1,11 +1,11 @@ import { mapActions, mapState, mapStores } from 'pinia' -import { verifyRecoveryCode } from 'src/api/mfa.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { verifyRecoveryCode } from 'src/api/mfa.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js index 4cd635550..056098c25 100644 --- a/src/components/mfa_form/totp_form.js +++ b/src/components/mfa_form/totp_form.js @@ -1,11 +1,11 @@ import { mapActions, mapState, mapStores } from 'pinia' -import { verifyOTPCode } from 'src/api/mfa.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { verifyOTPCode } from 'src/api/mfa.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js index 4bcb9803e..04f79425e 100644 --- a/src/components/oauth_callback/oauth_callback.js +++ b/src/components/oauth_callback/oauth_callback.js @@ -1,8 +1,8 @@ -import oauth from '../../services/new_api/oauth.js' - import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { getToken } from 'src/api/oauth.js' + const oac = { props: ['code'], mounted() { @@ -10,18 +10,16 @@ const oac = { const oauthStore = useOAuthStore() const { clientId, clientSecret } = oauthStore - oauth - .getToken({ - clientId, - clientSecret, - instance: useInstanceStore().server, - code: this.code, - }) - .then((result) => { - oauthStore.setToken(result.access_token) - this.$store.dispatch('loginUser', result.access_token) - this.$router.push({ name: 'friends' }) - }) + getToken({ + clientId, + clientSecret, + instance: useInstanceStore().server, + code: this.code, + }).then(({ data: result }) => { + oauthStore.setToken(result.access_token) + this.$store.dispatch('loginUser', result.access_token) + this.$router.push({ name: 'friends' }) + }) } }, } diff --git a/src/modules/users.js b/src/modules/users.js index 2eee21333..5573928b4 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -9,7 +9,6 @@ import { uniq, } from 'lodash' -import oauthApi from '../services/new_api/oauth.js' import { registerPushNotifications, unregisterPushNotifications, @@ -30,6 +29,7 @@ import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' +import { revokeToken } from 'src/api/oauth.js' import { fetchFollowers, fetchFriends, @@ -711,7 +711,7 @@ const users = { token: oauth.userToken, } - return oauthApi.revokeToken(params) + return revokeToken(params) }) .then(() => { store.commit('clearCurrentUser') diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js deleted file mode 100644 index b803e2146..000000000 --- a/src/services/new_api/oauth.js +++ /dev/null @@ -1,198 +0,0 @@ -import { reduce } from 'lodash' - -import { StatusCodeError } from 'src/services/errors/errors.js' - -const REDIRECT_URI = `${window.location.origin}/oauth-callback` - -export const getJsonOrError = async (response) => { - if (response.ok) { - return response.json().catch((error) => { - throw new StatusCodeError(response.status, error, {}, response) - }) - } else { - throw new StatusCodeError( - response.status, - await response.text(), - {}, - response, - ) - } -} - -export const createApp = (instance) => { - const url = `${instance}/api/v1/apps` - const form = new window.FormData() - - form.append('client_name', 'PleromaFE') - form.append('website', 'https://pleroma.social') - form.append('redirect_uris', REDIRECT_URI) - form.append('scopes', 'read write follow push admin') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then(getJsonOrError) - .then((app) => ({ - clientId: app.client_id, - clientSecret: app.client_secret, - })) -} - -export const verifyAppToken = ({ instance, appToken }) => { - return window - .fetch(`${instance}/api/v1/apps/verify_credentials`, { - method: 'GET', - headers: { Authorization: `Bearer ${appToken}` }, - }) - .then(getJsonOrError) -} - -const login = ({ instance, clientId }) => { - const data = { - response_type: 'code', - client_id: clientId, - redirect_uri: REDIRECT_URI, - scope: 'read write follow push admin', - } - - 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', clientId) - form.append('client_secret', clientSecret) - 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 = ({ clientId, clientSecret, instance, code }) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - 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, - }) - .then(getJsonOrError) -} -const verifyOTPCode = ({ app, instance, mfaToken, code }) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'totp') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'recovery') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const revokeToken = ({ app, instance, token }) => { - const url = `${instance}/oauth/revoke` - const form = new window.FormData() - - form.append('client_id', app.clientId) - form.append('client_secret', app.clientSecret) - form.append('token', token) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const oauth = { - login, - getToken, - getTokenWithCredentials, - verifyOTPCode, - verifyRecoveryCode, - revokeToken, -} - -export default oauth diff --git a/src/stores/announcements.js b/src/stores/announcements.js index f780f6dd7..4668b286a 100644 --- a/src/stores/announcements.js +++ b/src/stores/announcements.js @@ -86,18 +86,20 @@ export const useAnnouncementsStore = defineStore('announcements', { } }, markAnnouncementAsRead(id) { - return this.userActions.dismissAnnouncement({ - id, - credentials: useOAuthStore().token, - }).then(() => { - const index = this.announcements.findIndex((a) => a.id === id) + return this.userActions + .dismissAnnouncement({ + id, + credentials: useOAuthStore().token, + }) + .then(() => { + const index = this.announcements.findIndex((a) => a.id === id) - if (index < 0) { - return - } + if (index < 0) { + return + } - this.announcements[index].read = true - }) + this.announcements[index].read = true + }) }, startFetchingAnnouncements() { if (this.fetchAnnouncementsTimer) { diff --git a/src/stores/oauth.js b/src/stores/oauth.js index 116169b81..55e8e759c 100644 --- a/src/stores/oauth.js +++ b/src/stores/oauth.js @@ -2,11 +2,8 @@ import { defineStore } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' -import { - createApp, - getClientToken, - verifyAppToken, -} from 'src/services/new_api/oauth.js' +import { createApp, getClientToken } from 'src/api/oauth.js' +import { verifyCredentials } from 'src/api/public.js' // status codes about verifyAppToken (GET /api/v1/apps/verify_credentials) const isAppTokenRejected = (error) => @@ -61,9 +58,9 @@ export const useOAuthStore = defineStore('oauth', { }, async createApp() { const instance = useInstanceStore().server - const app = await createApp(instance) - this.setClientData(app) - return app + const app = await createApp({ instance }) + this.setClientData(app.data) + return app.data }, /// Use this if you want to get the client id and secret but are not interested /// in whether they are valid. @@ -85,7 +82,7 @@ export const useOAuthStore = defineStore('oauth', { clientSecret: this.clientSecret, instance, }) - this.setAppToken(res.access_token) + this.setAppToken(res.data.access_token) return res.access_token }, /// Use this if you want to ensure the app is still valid to use. @@ -93,9 +90,8 @@ export const useOAuthStore = defineStore('oauth', { async ensureAppToken() { if (this.appToken) { try { - await verifyAppToken({ - instance: useInstanceStore().server, - appToken: this.appToken, + await verifyCredentials({ + credentials: this.appToken, }) return this.appToken } catch (e) {