92 lines
4.3 KiB
JavaScript
92 lines
4.3 KiB
JavaScript
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' });
|
|
}
|
|
});
|