feat(subscriptions): gestion des abonnements par groupes façon PocketTube
CI / build-and-test (push) Failing after 8m18s

- Tables subscription_groups + subscription_group_members (migration + schema)
- API CRUD groupes, membres par groupe, groupes par abonnement
- Page Abonnements: onglets Groupes/Chaines, modale creation/edition (icone/couleur), assignation unitaire + lot, filtre par groupe + recherche
- Sidebar: section Groupes avec navigation directe (queryParams)
- i18n nav.groups FR/EN + 4 tests unitaires service
This commit is contained in:
2026-09-26 10:45:34 -04:00
parent 95e4358b01
commit 49c3b5dfdf
11 changed files with 1058 additions and 28 deletions
+95
View File
@@ -66,6 +66,13 @@ import {
subscribeChannel,
unsubscribeChannel,
isSubscribed,
listSubscriptionGroups,
createSubscriptionGroup,
updateSubscriptionGroup,
deleteSubscriptionGroup,
setSubscriptionGroupMembers,
setSubscriptionGroups,
listSubscriptionGroupMembersByUser,
insertDownloadJob,
getDownloadJob,
listDownloadJobs,
@@ -334,6 +341,94 @@ r.delete('/subscriptions/:subscriptionId', authMiddlewareCookieAware, subscripti
}
});
// -------------------- Subscription groups APIs (façon PocketTube) --------------------
r.get('/subscription-groups', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const groups = listSubscriptionGroups(req.user.id);
const members = listSubscriptionGroupMembersByUser(req.user.id);
return res.json({ groups, members });
} catch (error) {
return res.status(500).json({ error: 'groups_list_failed', details: String(error?.message || error) });
}
});
r.post('/subscription-groups', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const group = createSubscriptionGroup({
userId: req.user.id,
name: req.body?.name,
color: req.body?.color,
icon: req.body?.icon,
});
return res.status(201).json(group);
} catch (error) {
const msg = error?.message || 'group_create_failed';
if (msg === 'group_name_required') return res.status(400).json({ error: msg });
if (msg === 'group_name_taken') return res.status(409).json({ error: msg });
return res.status(500).json({ error: 'group_create_failed' });
}
});
r.patch('/subscription-groups/:groupId', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const group = updateSubscriptionGroup({
userId: req.user.id,
groupId: req.params.groupId,
name: req.body?.name,
color: req.body?.color,
icon: req.body?.icon,
});
if (!group) return res.status(404).json({ error: 'not_found' });
return res.json(group);
} catch (error) {
const msg = error?.message || 'group_update_failed';
if (msg === 'group_name_required') return res.status(400).json({ error: msg });
if (msg === 'group_name_taken') return res.status(409).json({ error: msg });
return res.status(500).json({ error: 'group_update_failed' });
}
});
r.delete('/subscription-groups/:groupId', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const info = deleteSubscriptionGroup({ userId: req.user.id, groupId: req.params.groupId });
if ((info?.changes || 0) === 0) return res.status(404).json({ error: 'not_found' });
return res.status(204).end();
} catch (error) {
return res.status(500).json({ error: 'group_delete_failed' });
}
});
// Remplace les chaînes d'un groupe : { subscriptionIds: number[] }
r.put('/subscription-groups/:groupId/members', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const group = setSubscriptionGroupMembers({
userId: req.user.id,
groupId: req.params.groupId,
subscriptionIds: req.body?.subscriptionIds,
});
if (!group) return res.status(404).json({ error: 'not_found' });
return res.json(group);
} catch (error) {
return res.status(500).json({ error: 'group_members_failed' });
}
});
// Remplace les groupes d'un abonnement : { groupIds: string[] }
r.put('/subscriptions/:subscriptionId/groups', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const groupIds = setSubscriptionGroups({
userId: req.user.id,
subscriptionId: Number(req.params.subscriptionId),
groupIds: req.body?.groupIds,
});
if (!groupIds) return res.status(404).json({ error: 'not_found' });
return res.json({ subscriptionId: Number(req.params.subscriptionId), groupIds });
} catch (error) {
return res.status(500).json({ error: 'subscription_groups_failed' });
}
});
// Public: view a playlist if allowed (owner or public). Authorization header is optional.
r.get('/playlists/:id/view', (req, res) => {
try {