chore: update TypeScript build info cache

This commit is contained in:
2025-09-18 22:20:52 -04:00
parent bcfb46ef7a
commit 41255a8126
22 changed files with 529 additions and 665 deletions
+34 -3
View File
@@ -4,9 +4,12 @@ import { randomBytes, randomUUID } from 'node:crypto';
import Database from 'better-sqlite3';
const root = process.cwd();
const dbDir = path.join(root, 'db');
const dbFile = path.join(dbDir, 'newtube.db');
const schemaFile = path.join(dbDir, 'schema.sql');
const overrideDbFile = process.env.NEWTUBE_DB_FILE && String(process.env.NEWTUBE_DB_FILE).trim().length
? path.resolve(String(process.env.NEWTUBE_DB_FILE))
: null;
const dbDir = overrideDbFile ? path.dirname(overrideDbFile) : path.join(root, 'db');
const dbFile = overrideDbFile || path.join(dbDir, 'newtube.db');
const schemaFile = path.join(root, 'db', 'schema.sql');
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
@@ -460,11 +463,39 @@ export function listPlaylists({ userId, limit = 50, offset = 0, q }) {
: db.prepare(sql).all(userId, limit, offset);
}
// List public playlists (visible to everyone)
export function listPublicPlaylists({ limit = 50, offset = 0, q }) {
const like = `%${(q || '').trim()}%`;
const hasQ = typeof q === 'string' && q.trim().length > 0;
const base = `SELECT id, user_id AS userId, name AS title, description, thumbnail, is_private AS isPrivate, created_at AS createdAt, updated_at AS updatedAt,
(SELECT COUNT(1) FROM playlist_items WHERE playlist_id = playlists.id) AS itemsCount
FROM playlists WHERE is_private = 0`;
const sql = hasQ ? `${base} AND (name LIKE ? OR COALESCE(description,'') LIKE ?)
ORDER BY updated_at DESC LIMIT ? OFFSET ?` :
`${base} ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
return hasQ
? db.prepare(sql).all(like, like, limit, offset)
: db.prepare(sql).all(limit, offset);
}
export function getPlaylistRaw(id) {
return db.prepare(`SELECT id, user_id AS userId, name AS title, description, thumbnail, is_private AS isPrivate, created_at AS createdAt, updated_at AS updatedAt
FROM playlists WHERE id = ?`).get(id);
}
// Get playlist with items if allowed for a given viewer (owner or public)
export function getPlaylistWithItemsIfAllowed({ viewerUserId, id, limit = 200, offset = 0 }) {
const pl = db.prepare(`SELECT id, user_id AS userId, name AS title, description, thumbnail, is_private AS isPrivate, created_at AS createdAt, updated_at AS updatedAt
FROM playlists WHERE id = ?`).get(id);
if (!pl) return null;
const isOwner = viewerUserId && pl.userId === viewerUserId;
if (!isOwner && Number(pl.isPrivate) === 1) return 'forbidden';
const items = db.prepare(`SELECT id, playlist_id AS playlistId, provider, video_id AS videoId, title, thumbnail, added_at AS addedAt, position
FROM playlist_items WHERE playlist_id = ?
ORDER BY position ASC LIMIT ? OFFSET ?`).all(id, limit, offset);
return { ...pl, items };
}
export function updatePlaylist({ userId, id, patch }) {
const cur = db.prepare(`SELECT * FROM playlists WHERE id = ?`).get(id);
if (!cur) return null;
+42 -1
View File
@@ -44,7 +44,9 @@ import {
// Playlists
createPlaylist,
listPlaylists,
listPublicPlaylists,
getPlaylistRaw,
getPlaylistWithItemsIfAllowed,
updatePlaylist,
deletePlaylist,
listPlaylistItems,
@@ -60,6 +62,46 @@ const ACCESS_TTL_MIN = Number(process.env.ACCESS_TTL_MIN || 15);
const REFRESH_TTL_DAYS = Number(process.env.REFRESH_TTL_DAYS || 2);
const REMEMBER_TTL_DAYS = Number(process.env.REMEMBER_TTL_DAYS || 30);
// Create router
const r = express.Router();
// Public: list public playlists (no auth required)
r.get('/playlists/public', (req, res) => {
try {
const limit = Math.min(200, Math.max(1, Number(req.query.limit || 50)));
const offset = Math.max(0, Number(req.query.offset || 0));
const q = typeof req.query.q === 'string' ? req.query.q : undefined;
const rows = listPublicPlaylists({ limit, offset, q });
return res.json(rows);
} catch (e) {
return res.status(500).json({ error: 'list_public_failed', details: String(e?.message || e) });
}
});
// Public: view a playlist if allowed (owner or public). Authorization header is optional.
r.get('/playlists/:id/view', (req, res) => {
try {
const id = String(req.params.id || '');
let viewerUserId = undefined;
try {
const auth = req.headers['authorization'] || '';
const [, token] = String(auth).split(' ');
if (token) {
const payload = jwt.verify(token, JWT_SECRET);
viewerUserId = payload?.sub;
}
} catch {}
const limit = Math.min(2000, Math.max(1, Number(req.query.limit || 500)));
const offset = Math.max(0, Number(req.query.offset || 0));
const result = getPlaylistWithItemsIfAllowed({ viewerUserId, id, limit, offset });
if (result === 'forbidden') return res.status(404).json({ error: 'not_found' });
if (!result) return res.status(404).json({ error: 'not_found' });
return res.json(result);
} catch (e) {
return res.status(500).json({ error: 'view_failed', details: String(e?.message || e) });
}
});
// Servir les fichiers statiques du dossier dist
app.use(express.static(path.join(process.cwd(), 'dist')));
app.use('/assets', express.static(path.join(process.cwd(), 'assets')));
@@ -528,7 +570,6 @@ function formatListFromMeta(meta) {
}
// Routes under /api
const r = express.Router();
// -------------------- YouTube simple cache (GET) --------------------
// Cache TTL defaults to 15 minutes, configurable via env YT_CACHE_TTL_MS
+94
View File
@@ -0,0 +1,94 @@
// Simple integration test for playlist public/private visibility
// Run with: npm run test:playlists
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
// Create isolated temp DB file
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'newtube-test-'));
const dbPath = path.join(tmpDir, 'test.db');
process.env.NEWTUBE_DB_FILE = dbPath;
// Dynamically import DB after setting env
const dbMod = await import('../db.mjs');
const {
insertUser,
cryptoRandomUUID,
createPlaylist,
listPublicPlaylists,
listPlaylists,
getPlaylistWithItemsIfAllowed,
} = dbMod;
function expect(cond, msg) {
if (!cond) {
throw new Error(`Assertion failed: ${msg}`);
}
}
function logOk(msg) {
console.log(`✓ ${msg}`);
}
// Seed users
const ownerId = cryptoRandomUUID();
const otherId = cryptoRandomUUID();
insertUser({ id: ownerId, username: 'owner', email: '[email protected]', passwordHash: 'x' });
insertUser({ id: otherId, username: 'other', email: '[email protected]', passwordHash: 'y' });
// Seed playlists
const pub1 = createPlaylist({ userId: ownerId, title: 'Pub A', description: 'public', thumbnail: null, isPrivate: false });
const pub2 = createPlaylist({ userId: ownerId, title: 'Pub B', description: 'public', thumbnail: null, isPrivate: 0 });
const priv1 = createPlaylist({ userId: ownerId, title: 'Priv A', description: 'private', thumbnail: null, isPrivate: true });
// 1) Anonymous lists public
{
const pubs = listPublicPlaylists({ limit: 50, offset: 0 });
expect(Array.isArray(pubs), 'listPublicPlaylists returns array');
const ids = new Set(pubs.map(p => p.id));
expect(ids.has(pub1.id) && ids.has(pub2.id), 'public playlists visible');
expect(!ids.has(priv1.id), 'private playlist not visible');
logOk('Anonymous sees public playlists only');
}
// 2) Other user lists own (none) + sees public via listPublicPlaylists
{
const ownOther = listPlaylists({ userId: otherId, limit: 50, offset: 0 });
expect(ownOther.length === 0, 'other user has no own playlists');
const pubs = listPublicPlaylists({ limit: 50, offset: 0 });
const ids = new Set(pubs.map(p => p.id));
expect(ids.has(pub1.id) && ids.has(pub2.id), 'other user sees public playlists');
logOk('Other user sees public playlists');
}
// 3) Owner lists own (public + private)
{
const ownOwner = listPlaylists({ userId: ownerId, limit: 50, offset: 0 });
const ids = new Set(ownOwner.map(p => p.id));
expect(ids.has(pub1.id) && ids.has(pub2.id) && ids.has(priv1.id), 'owner sees all own playlists, including private');
logOk('Owner sees own public + private playlists');
}
// 4) Public view endpoint logic: getPlaylistWithItemsIfAllowed
{
// Anonymous can view public
const viewPub = getPlaylistWithItemsIfAllowed({ viewerUserId: undefined, id: pub1.id, limit: 10, offset: 0 });
expect(viewPub && viewPub.id === pub1.id, 'anonymous can view public playlist details');
// Anonymous cannot view private
const viewPriv = getPlaylistWithItemsIfAllowed({ viewerUserId: undefined, id: priv1.id, limit: 10, offset: 0 });
expect(viewPriv === 'forbidden', 'anonymous cannot view private playlist');
// Other user can view public
const viewPubOther = getPlaylistWithItemsIfAllowed({ viewerUserId: otherId, id: pub2.id, limit: 10, offset: 0 });
expect(viewPubOther && viewPubOther.id === pub2.id, 'other user can view public playlist');
// Owner can view private
const viewPrivOwner = getPlaylistWithItemsIfAllowed({ viewerUserId: ownerId, id: priv1.id, limit: 10, offset: 0 });
expect(viewPrivOwner && viewPrivOwner.id === priv1.id, 'owner can view own private playlist');
logOk('Public view behavior correct for anonymous, other, and owner');
}
console.log('\nAll playlist visibility tests passed.');