import { Router } from 'express'; import axios from 'axios'; import jwt from 'jsonwebtoken'; import { db } from '../lib/db.js'; export const authRouter = Router(); const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID || ''; const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET || ''; const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/callback'; const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change'; authRouter.post('/exchange', async (req, res) => { try { const { code } = req.body; if (!code) return res.status(400).json({ error: 'Missing code' }); const tokenResp = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI, }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`, }, }); const { access_token, refresh_token, expires_in } = tokenResp.data; const meResp = await axios.get('https://api.spotify.com/v1/me', { headers: { Authorization: `Bearer ${access_token}` }, }); const profile = meResp.data; const now = Date.now(); const tokenExpiresAt = now + expires_in * 1000 - 60000; // refresh a bit early db.db .prepare(`INSERT INTO users (id, display_name, email, avatar_url, country, product, created_at, updated_at, access_token, refresh_token, token_expires_at) VALUES (@id, @display_name, @email, @avatar_url, @country, @product, @created_at, @updated_at, @access_token, @refresh_token, @token_expires_at) ON CONFLICT(id) DO UPDATE SET display_name=excluded.display_name, email=excluded.email, avatar_url=excluded.avatar_url, country=excluded.country, product=excluded.product, updated_at=excluded.updated_at, access_token=excluded.access_token, refresh_token=excluded.refresh_token, token_expires_at=excluded.token_expires_at`) .run({ id: profile.id, display_name: profile.display_name, email: profile.email, avatar_url: profile.images?.[0]?.url || null, country: profile.country, product: profile.product, created_at: now, updated_at: now, access_token, refresh_token, token_expires_at: tokenExpiresAt, }); const jwtToken = jwt.sign({ uid: profile.id }, JWT_SECRET, { expiresIn: '30d' }); // Return Spotify tokens to enable client-side playback controls res.json({ token: jwtToken, uid: profile.id, access_token, refresh_token, expires_in }); } catch (err) { const detail = err?.response?.data || err?.message || 'Unknown error'; console.error('Auth exchange error:', detail); res.status(500).json({ error: 'Auth exchange failed', detail }); } }); authRouter.post('/refresh', async (req, res) => { try { const { uid } = req.body; if (!uid) return res.status(400).json({ error: 'Missing uid' }); const row = db.db.prepare('SELECT refresh_token FROM users WHERE id = ?').get(uid); if (!row?.refresh_token) return res.status(404).json({ error: 'User not found' }); const tokenResp = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'refresh_token', refresh_token: row.refresh_token }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`, }, }); const { access_token, expires_in } = tokenResp.data; const tokenExpiresAt = Date.now() + expires_in * 1000 - 60000; db.db.prepare('UPDATE users SET access_token=?, token_expires_at=?, updated_at=? WHERE id=?').run(access_token, tokenExpiresAt, Date.now(), uid); res.json({ ok: true }); } catch (err) { console.error(err.response?.data || err.message); res.status(500).json({ error: 'Refresh failed' }); } });