290 lines
8.3 KiB
TypeScript
290 lines
8.3 KiB
TypeScript
import type { BackendError, User } from '~/types'
|
|
import type { Module } from 'vuex'
|
|
import type { RootState } from '~/store/index'
|
|
import type { RouteLocationRaw } from 'vue-router'
|
|
|
|
import axios from 'axios'
|
|
import useLogger from '~/composables/useLogger'
|
|
import useFormData from '~/composables/useFormData'
|
|
|
|
import { clear as clearIDB } from 'idb-keyval'
|
|
import { useQueue } from '~/composables/audio/queue'
|
|
|
|
export type Permission = 'settings' | 'library' | 'moderation'
|
|
export interface State {
|
|
authenticated: boolean
|
|
username: string
|
|
fullUsername: string
|
|
availablePermissions: Record<Permission, boolean>,
|
|
profile: null | User
|
|
oauth: OAuthTokens
|
|
scopedTokens: ScopedTokens
|
|
|
|
applicationSecret: string | undefined
|
|
}
|
|
|
|
interface ScopedTokens {
|
|
listen: null | string
|
|
}
|
|
|
|
interface OAuthTokens {
|
|
clientId: null | string
|
|
clientSecret: null | string
|
|
accessToken: null | string
|
|
refreshToken: null | string
|
|
}
|
|
|
|
const NEEDED_SCOPES = 'read write'
|
|
|
|
const logger = useLogger()
|
|
|
|
function getDefaultScopedTokens (): ScopedTokens {
|
|
return {
|
|
listen: null
|
|
}
|
|
}
|
|
|
|
function getDefaultOauth (): OAuthTokens {
|
|
return {
|
|
clientId: null,
|
|
clientSecret: null,
|
|
accessToken: null,
|
|
refreshToken: null
|
|
}
|
|
}
|
|
|
|
async function createOauthApp () {
|
|
const payload = {
|
|
name: `Funkwhale web client at ${window.location.hostname}`,
|
|
website: location.origin,
|
|
scopes: NEEDED_SCOPES,
|
|
redirect_uris: `${location.origin}/auth/callback`
|
|
}
|
|
return (await axios.post('oauth/apps/', payload)).data
|
|
}
|
|
|
|
const store: Module<State, RootState> = {
|
|
namespaced: true,
|
|
state: {
|
|
authenticated: false,
|
|
username: '',
|
|
fullUsername: '',
|
|
availablePermissions: {
|
|
settings: false,
|
|
library: false,
|
|
moderation: false
|
|
},
|
|
profile: null,
|
|
oauth: getDefaultOauth(),
|
|
scopedTokens: getDefaultScopedTokens(),
|
|
|
|
applicationSecret: undefined
|
|
},
|
|
getters: {
|
|
header: state => {
|
|
if (state.oauth.accessToken) {
|
|
return 'Bearer ' + state.oauth.accessToken
|
|
}
|
|
}
|
|
},
|
|
mutations: {
|
|
reset (state) {
|
|
state.authenticated = false
|
|
state.profile = null
|
|
state.username = ''
|
|
state.fullUsername = ''
|
|
state.scopedTokens = getDefaultScopedTokens()
|
|
state.oauth = getDefaultOauth()
|
|
state.availablePermissions = {
|
|
settings: false,
|
|
library: false,
|
|
moderation: false
|
|
}
|
|
|
|
state.applicationSecret = undefined
|
|
},
|
|
profile: (state, value) => {
|
|
state.profile = value
|
|
},
|
|
authenticated: (state, value) => {
|
|
state.authenticated = value
|
|
if (value === false) {
|
|
state.username = ''
|
|
state.fullUsername = ''
|
|
state.profile = null
|
|
state.scopedTokens = getDefaultScopedTokens()
|
|
state.availablePermissions = {
|
|
settings: false,
|
|
library: false,
|
|
moderation: false
|
|
}
|
|
}
|
|
},
|
|
username: (state, value) => {
|
|
state.username = value
|
|
},
|
|
fullUsername: (state, value) => {
|
|
state.fullUsername = value
|
|
},
|
|
avatar: (state, value) => {
|
|
if (state.profile) {
|
|
state.profile.avatar = value
|
|
}
|
|
},
|
|
scopedTokens: (state, value) => {
|
|
state.scopedTokens = { ...value }
|
|
},
|
|
permission: (state, { key, status }: { key: Permission, status: boolean }) => {
|
|
state.availablePermissions[key] = status
|
|
},
|
|
profilePartialUpdate: (state, payload: User) => {
|
|
if (!state.profile) {
|
|
state.profile = {} as User
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(payload)) {
|
|
state.profile[key as keyof User] = value as never
|
|
}
|
|
},
|
|
oauthApp: (state, payload) => {
|
|
state.oauth.clientId = payload.client_id
|
|
state.oauth.clientSecret = payload.client_secret
|
|
},
|
|
oauthToken: (state, payload) => {
|
|
state.oauth.accessToken = payload.access_token
|
|
state.oauth.refreshToken = payload.refresh_token
|
|
}
|
|
},
|
|
actions: {
|
|
// Send a request to the login URL and save the returned JWT
|
|
async login ({ dispatch }, { credentials }) {
|
|
const form = useFormData(credentials)
|
|
await axios.post('users/login', form)
|
|
|
|
logger.info('Successfully logged in as', credentials.username)
|
|
await dispatch('fetchUser')
|
|
},
|
|
async logout ({ commit }) {
|
|
try {
|
|
await axios.post('users/logout')
|
|
} catch (error) {
|
|
logger.error('Error while logging out, probably logged in via oauth', error)
|
|
}
|
|
|
|
const modules = [
|
|
'auth',
|
|
'favorites',
|
|
'player',
|
|
'playlists',
|
|
'queue',
|
|
'radios'
|
|
]
|
|
|
|
for (const module of modules) {
|
|
commit(`${module}/reset`, null, { root: true })
|
|
}
|
|
|
|
// Clear session storage
|
|
sessionStorage.clear()
|
|
|
|
// Clear track queue
|
|
await useQueue().clear()
|
|
|
|
// Clear all indexedDB data
|
|
await clearIDB()
|
|
|
|
logger.info('Log out, goodbye!')
|
|
},
|
|
|
|
async fetchNotifications ({ dispatch, state }) {
|
|
return Promise.all([
|
|
dispatch('ui/fetchUnreadNotifications', null, { root: true }),
|
|
state.availablePermissions.library && dispatch('ui/fetchPendingReviewEdits', null, { root: true }),
|
|
state.availablePermissions.moderation && dispatch('ui/fetchPendingReviewReports', null, { root: true }),
|
|
state.availablePermissions.moderation && dispatch('ui/fetchPendingReviewRequests', null, { root: true })
|
|
])
|
|
},
|
|
async fetchUser ({ dispatch }) {
|
|
try {
|
|
const response = await axios.get('users/me/')
|
|
logger.info('Successfully fetched user profile')
|
|
|
|
dispatch('updateUser', response.data)
|
|
|
|
await Promise.all([
|
|
dispatch('fetchNotifications'),
|
|
dispatch('favorites/fetch', null, { root: true }),
|
|
dispatch('playlists/fetchOwn', null, { root: true }),
|
|
dispatch('libraries/fetchFollows', null, { root: true }),
|
|
dispatch('channels/fetchSubscriptions', null, { root: true }),
|
|
dispatch('moderation/fetchContentFilters', null, { root: true })
|
|
])
|
|
} catch (error) {
|
|
if ((error as BackendError).response?.status === 401) {
|
|
logger.info('User is not authenticated')
|
|
return
|
|
}
|
|
|
|
logger.error('Error while fetching user profile', error)
|
|
}
|
|
},
|
|
updateUser ({ commit }, data) {
|
|
commit('authenticated', true)
|
|
commit('profile', data)
|
|
commit('username', data.username)
|
|
commit('fullUsername', data.full_username)
|
|
|
|
if (data.tokens) {
|
|
commit('scopedTokens', data.tokens)
|
|
}
|
|
|
|
for (const [permission, hasPermission] of Object.entries(data.permissions)) {
|
|
// this makes it easier to check for permissions in templates
|
|
commit('permission', { key: permission, status: hasPermission })
|
|
}
|
|
},
|
|
async oauthLogin ({ state, rootState, commit }, next: RouteLocationRaw) {
|
|
const app = await createOauthApp()
|
|
commit('oauthApp', app)
|
|
const redirectUri = encodeURIComponent(`${location.origin}/auth/callback`)
|
|
const params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUri}&state=${next}&client_id=${state.oauth.clientId}`
|
|
const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`
|
|
logger.log('Redirecting user...', authorizeUrl)
|
|
window.location.href = authorizeUrl
|
|
},
|
|
async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
|
|
logger.log('Fetching token...')
|
|
const payload = {
|
|
client_id: state.oauth.clientId,
|
|
client_secret: state.oauth.clientSecret,
|
|
grant_type: 'authorization_code',
|
|
code: authorizationCode,
|
|
redirect_uri: `${location.origin}/auth/callback`
|
|
}
|
|
const response = await axios.post(
|
|
'oauth/token/',
|
|
useFormData(payload),
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
|
)
|
|
commit('oauthToken', response.data)
|
|
await dispatch('fetchUser')
|
|
},
|
|
async refreshOauthToken ({ state, commit }) {
|
|
const payload = {
|
|
client_id: state.oauth.clientId,
|
|
client_secret: state.oauth.clientSecret,
|
|
grant_type: 'refresh_token',
|
|
refresh_token: state.oauth.refreshToken
|
|
}
|
|
const response = await axios.post(
|
|
'oauth/token/',
|
|
useFormData(payload),
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
|
)
|
|
commit('oauthToken', response.data)
|
|
}
|
|
}
|
|
}
|
|
|
|
export default store
|