move oauth api

This commit is contained in:
Henry Jameson 2026-06-17 19:04:05 +03:00
commit d2dcdfbd80
10 changed files with 204 additions and 281 deletions

View file

@ -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,
})

138
src/api/oauth.js Normal file
View file

@ -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,
})
}

View file

@ -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() {

View file

@ -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'

View file

@ -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'

View file

@ -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' })
})
}
},
}

View file

@ -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')

View file

@ -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

View file

@ -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) {

View file

@ -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) {