import type {Express} from 'express'; import type {Logger} from "@foxxmd/logging"; import passport from "passport"; import type {ExpressHandler} from "../common/infrastructure/Atomic.ts"; import type LastfmScrobbler from "../scrobblers/LastfmScrobbler.ts"; import type ScrobbleClients from "../scrobblers/ScrobbleClients.ts"; import type LastfmSource from "../sources/LastfmSource.ts"; import type ScrobbleSources from "../sources/ScrobbleSources.ts"; import type SpotifySource from "../sources/SpotifySource.ts"; import type YTMusicSource from "../sources/YTMusicSource.ts"; import type LibrefmScrobbler from "../scrobblers/LibrefmScrobbler.ts"; import type LibrefmSource from "../sources/LibrefmSource.ts"; import AbstractSource from "../sources/AbstractSource.ts"; export const setupAuthRoutes = (app: Express, logger: Logger, sourceMiddle: ExpressHandler, clientMiddle: ExpressHandler, scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients) => { app.use('/api/client/auth', clientMiddle); app.get('/api/client/auth', async (req, res) => { const { scrobbleClient, } = req as any; switch (scrobbleClient.type) { case 'lastfm': case 'librefm': res.redirect(scrobbleClient.api.getAuthUrl()); break; case 'tealfm': const url = await scrobbleClient.getAuthorizeUrl(); res.redirect(url); break; default: return res.status(400).send(`Specified client does not have auth implemented (${scrobbleClient.type})`); } }); app.use('/api/source/auth', sourceMiddle); app.get('/api/source/auth', async (req, res, next) => { const { // @ts-expect-error TS(2339): Property 'scrobbleSource' does not exist on type '... Remove this comment to see the full error message scrobbleSource: source, // @ts-expect-error TS(2339): Property 'sourceName' does not exist on type 'Requ... Remove this comment to see the full error message sourceName: name, } = req; switch (source.type) { case 'spotify': if (source.spotifyApi === undefined) { res.status(400).send('Spotify configuration is not valid'); } else { logger.info('Redirecting to spotify authorization url'); res.redirect(source.createAuthUrl()); } break; case 'lastfm': case 'librefm': res.redirect(source.api.getAuthUrl()); break; case 'deezer': // @ts-expect-error TS(2339): Property 'deezerSource' does not exist on type 'Se... Remove this comment to see the full error message req.session.deezerSource = name; return passport.authenticate(`deezer-${source.name}`)(req,res,next); case 'ytmusic': await (source as YTMusicSource).reauthenticate(); res.redirect((source as YTMusicSource).verificationUrl); break; default: return res.status(400).send(`Specified source does not have auth implemented (${source.type})`); } }); app.get(/.*callback$/, async (req, res, next) => { if(req.url.indexOf('/api') !== 0) { return res.redirect(307, `/api${req.url}`); } const { query: { state, name } = {} } = req; const entityTypeHint = req.url.includes('lastfm') ? 'lastfm' : 'librefm'; if (req.url.includes('lastfm') || req.url.includes('librefm')) { const { query: { token } = {} } = req; let entity: LastfmScrobbler | LastfmSource | LibrefmScrobbler | LibrefmSource | undefined; if(req.url.includes('lastfm')) { entity = scrobbleClients.getByNameAndType(state as string, 'lastfm', true) as (LastfmScrobbler | LastfmSource | undefined); } else { entity = scrobbleClients.getByNameAndType(state as string, 'librefm', true) as (LibrefmScrobbler | LibrefmSource | undefined); } if(entity === undefined) { entity = scrobbleSources.getByName(state, true) as LastfmSource | LibrefmSource; } try { if(entity === undefined) { const error = new Error(`No source/client with type '${entityTypeHint}' and name ${state} was found`); logger.error(error); throw error; } await entity.api.authenticate(token); entity.authFailure = false; if(entity instanceof AbstractSource) { entity.poll().catch((e) => logger.error(e)); } else { entity.initialize() .catch((e) => logger.error(e)) .then(() => { entity.initScrobbleMonitoring().catch((e) => logger.error(e)); }) } return res.send('OK'); } catch (e) { return res.send(e.message); } } else if(req.url.includes('ytmusic')) { const entity: YTMusicSource | undefined = scrobbleSources.getByName(name) as (YTMusicSource | undefined); if(entity === undefined) { logger.error(`No YTMUsic source with name ${state} was found`); } const result = await entity.handleAuthCodeCallback(req.query); let responseContent = 'OK'; if(result === true) { entity.authFailure = false; entity.poll().catch((e) => logger.error(e)); } else { responseContent = result; } return res.send(responseContent); } else { // TODO right now all sources requiring source interaction are covered by logic branches (deezer above and spotify here) // but eventually should update all source callbacks to url specific URLS to avoid ambiguity... // wish we could use state param to identify name/source but not all auth strategies and auth provides may provide access to that logger.info({label: 'Spotify'}, 'Received auth code callback from Spotify'); const source = scrobbleSources.getByNameAndType(state as string, 'spotify', true) as SpotifySource; const tokenResult = await source.handleAuthCodeCallback(req.query); let responseContent = 'OK'; if (tokenResult === true) { source.authFailure = false; source.poll().catch((e) => logger.error(e)); } else { responseContent = tokenResult; } return res.send(responseContent); } }); }