Merge branch 'api-refactor' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-06-17 19:10:07 +03:00
commit cfefb70b1f
14 changed files with 337 additions and 433 deletions

45
src/api/mfa.js Normal file
View file

@ -0,0 +1,45 @@
import { promisedRequest } from './helpers.js'
export const verifyOTPCode = ({
clientId,
clientSecret,
instance,
mfaToken,
code,
}) => {
const formData = new window.FormData()
formData.append('client_id', clientId)
formData.append('client_secret', clientSecret)
formData.append('mfa_token', mfaToken)
formData.append('code', code)
formData.append('challenge_type', 'totp')
return promisedRequest({
url: '/oauth/mfa/challenge',
method: 'POST',
formData,
})
}
export const verifyRecoveryCode = ({
clientId,
clientSecret,
instance,
mfaToken,
code,
}) => {
const formData = new window.FormData()
formData.append('client_id', clientId)
formData.append('client_secret', clientSecret)
formData.append('mfa_token', mfaToken)
formData.append('code', code)
formData.append('challenge_type', 'recovery')
return promisedRequest({
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

@ -14,9 +14,11 @@ import {
import { RegistrationError, StatusCodeError } from 'src/services/errors/errors'
const SUGGESTIONS_URL = '/api/v1/suggestions'
/* eslint-env browser */
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_PASSWORD_RESET_URL = ({ email }) =>
`/auth/password${paramsString({ email })}`
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
const MASTODON_FOLLOWING_URL = (
@ -110,7 +112,7 @@ export const fetchUserByName = ({ name, credentials }) =>
credentials,
params: { acct: name },
})
.then((data) => data.id)
.then(({ data }) => data.id)
.catch((error) => {
if (error && error.statusCode === 404) {
// Either the backend does not support lookup endpoint,
@ -313,6 +315,13 @@ export const verifyCredentials = ({ credentials }) =>
credentials,
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
export const resetPassword = ({ instance, email }) => {
return promisedRequest({
url: MASTODON_PASSWORD_RESET_URL({ email }),
method: 'POST',
})
}
export const suggestions = ({ credentials }) =>
promisedRequest({
url: SUGGESTIONS_URL,

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 mfaApi from '../../services/new_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'
@ -43,18 +43,18 @@ export default {
code: this.code,
}
mfaApi.verifyRecoveryCode(data).then((result) => {
if (result.error) {
this.error = result.error
verifyRecoveryCode(data)
.then((result) => {
this.login(result).then(() => {
this.$router.push({ name: 'friends' })
})
})
.catch((error) => {
this.error = error
this.code = null
this.focusOnCodeInput()
return
}
this.login(result).then(() => {
this.$router.push({ name: 'friends' })
})
})
},
},
}

View file

@ -1,11 +1,11 @@
import { mapActions, mapState, mapStores } from 'pinia'
import mfaApi from '../../services/new_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'
@ -46,18 +46,18 @@ export default {
code: this.code,
}
mfaApi.verifyOTPCode(data).then((result) => {
if (result.error) {
this.error = result.error
verifyOTPCode(data)
.then(({ data: result }) => {
this.login(result).then(() => {
this.$router.push({ name: 'friends' })
})
})
.catch((error) => {
this.error = error
this.code = null
this.focusOnCodeInput()
return
}
this.login(result).then(() => {
this.$router.push({ name: 'friends' })
})
})
},
},
}

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

@ -13,7 +13,7 @@ const fetchRelationship = (attempt, userId, store) =>
id: userId,
credentials: useOAuthStore().token,
})
.then((relationship) => {
.then(({ data: relationship }) => {
store.commit('updateUserRelationship', [relationship])
return relationship
})
@ -36,7 +36,7 @@ const fetchRelationship = (attempt, userId, store) =>
})
export const requestFollow = async (userId, store) => {
const updated = await followUser({
const { data: updated } = await followUser({
id: userId,
credentials: useOAuthStore().token,
})
@ -58,7 +58,7 @@ export const requestFollow = async (userId, store) => {
}
export const requestUnfollow = async (userId, store) => {
const updated = await unfollowUser({
const { data: updated } = await unfollowUser({
id: userId,
credentials: useOAuthStore().token,
})

View file

@ -1,54 +0,0 @@
const verifyOTPCode = ({
clientId,
clientSecret,
instance,
mfaToken,
code,
}) => {
const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData()
form.append('client_id', clientId)
form.append('client_secret', clientSecret)
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 = ({
clientId,
clientSecret,
instance,
mfaToken,
code,
}) => {
const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData()
form.append('client_id', clientId)
form.append('client_secret', clientSecret)
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 mfa = {
verifyOTPCode,
verifyRecoveryCode,
}
export default mfa

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

@ -1,22 +0,0 @@
import { reduce } from 'lodash'
const MASTODON_PASSWORD_RESET_URL = '/auth/password'
const resetPassword = ({ instance, email }) => {
const params = { email }
const query = reduce(
params,
(acc, v, k) => {
const encoded = `${k}=${encodeURIComponent(v)}`
return `${acc}&${encoded}`
},
'',
)
const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}`
return window.fetch(url, {
method: 'POST',
})
}
export default resetPassword

View file

@ -2,14 +2,7 @@ import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'
import {
getAnnouncements as adminGetAnnouncements,
deleteAnnouncement,
editAnnouncement,
postAnnouncement,
} from 'src/api/admin.js'
import { getAnnouncements } from 'src/api/public.js'
import { dismissAnnouncement } from 'src/api/user.js'
const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5
@ -18,6 +11,8 @@ export const useAnnouncementsStore = defineStore('announcements', {
announcements: [],
supportsAnnouncements: true,
fetchAnnouncementsTimer: undefined,
adminActions: {},
userActions: {},
}),
getters: {
unreadAnnouncementCount() {
@ -32,34 +27,41 @@ export const useAnnouncementsStore = defineStore('announcements', {
},
},
actions: {
fetchAnnouncements() {
if (!this.supportsAnnouncements) {
return Promise.resolve()
}
async fetchAnnouncements() {
if (!this.supportsAnnouncements) return
const currentUser = window.vuex.state.users.currentUser
const isAdmin =
currentUser &&
currentUser.privileges.has('announcements_manage_announcements')
const fetchAnnouncements = async () => {
if (!isAdmin) {
const result = await getAnnouncements({
credentials: useOAuthStore().token,
})
return result.data
try {
if (currentUser) {
this.userActions = await import('src/api/user.js')
}
const { data: all } = await adminGetAnnouncements({
if (isAdmin) {
this.adminActions = await import('src/api/admin.js')
} else {
const all = await getAnnouncements({
credentials: useOAuthStore().token,
})
return all.data
}
const { data: all } = await this.adminActions.getAnnouncements({
credentials: useOAuthStore().token,
})
const { data: visible } = await getAnnouncements({
credentials: useOAuthStore().token,
})
const visibleObject = visible.reduce((a, c) => {
a[c.id] = c
return a
}, {})
const getWithinVisible = (announcement) =>
visibleObject[announcement.id]
@ -72,36 +74,32 @@ export const useAnnouncementsStore = defineStore('announcements', {
}
})
return all
this.announcements = all
} catch (error) {
// If and only if backend does not support announcements, it would return 404.
// In this case, silently ignores it.
if (error && error.statusCode === 404) {
this.supportsAnnouncements = false
} else {
throw error
}
}
return fetchAnnouncements()
.then((announcements) => {
this.announcements = announcements
})
.catch((error) => {
// If and only if backend does not support announcements, it would return 404.
// In this case, silently ignores it.
if (error && error.statusCode === 404) {
this.supportsAnnouncements = false
} else {
throw error
}
})
},
markAnnouncementAsRead(id) {
return 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) {
@ -122,35 +120,41 @@ export const useAnnouncementsStore = defineStore('announcements', {
clearInterval(interval)
},
postAnnouncement({ content, startsAt, endsAt, allDay }) {
return postAnnouncement({
credentials: useOAuthStore().token,
content,
startsAt,
endsAt,
allDay,
}).then(() => {
return this.fetchAnnouncements()
})
return this.adminActions
.postAnnouncement({
credentials: useOAuthStore().token,
content,
startsAt,
endsAt,
allDay,
})
.then(() => {
return this.fetchAnnouncements()
})
},
editAnnouncement({ id, content, startsAt, endsAt, allDay }) {
return editAnnouncement({
id,
content,
startsAt,
endsAt,
allDay,
credentials: useOAuthStore().token,
}).then(() => {
return this.fetchAnnouncements()
})
return this.adminActions
.editAnnouncement({
id,
content,
startsAt,
endsAt,
allDay,
credentials: useOAuthStore().token,
})
.then(() => {
return this.fetchAnnouncements()
})
},
deleteAnnouncement(id) {
return deleteAnnouncement({
id,
credentials: useOAuthStore().token,
}).then(() => {
return this.fetchAnnouncements()
})
return this.adminActions
.deleteAnnouncement({
id,
credentials: useOAuthStore().token,
})
.then(() => {
return this.fetchAnnouncements()
})
},
},
})

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