@digitalpaws/starr 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.rst +75 -0
- package/data/.nfs.811bbb0d.43c34.4 +0 -0
- package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
- package/data/.nfs.82d127f7.fffff5704.4 +0 -0
- package/data/.nfs.82d127fe.fffff5704.4 +0 -0
- package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
- package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
- package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
- package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
- package/data/.nfs.82d18536.fffff5e24.4 +0 -0
- package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
- package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
- package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
- package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
- package/data/.nfs.8314122e.fffffb044.4 +0 -0
- package/data/.nfs.83141236.fffffb044.4 +0 -0
- package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
- package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
- package/data/logs/starr.log +66244 -0
- package/data/starr.db +0 -0
- package/data/starr.db-shm +0 -0
- package/data/starr.db-wal +0 -0
- package/data/starr.jwt +1 -0
- package/data/starr.key +1 -0
- package/dist/auth/auth-service.d.ts +42 -0
- package/dist/auth/auth-service.d.ts.map +1 -0
- package/dist/auth/auth-service.js +226 -0
- package/dist/auth/auth-service.js.map +1 -0
- package/dist/auth/http-server.d.ts +16 -0
- package/dist/auth/http-server.d.ts.map +1 -0
- package/dist/auth/http-server.js +207 -0
- package/dist/auth/http-server.js.map +1 -0
- package/dist/auth/jwt-secret.d.ts +6 -0
- package/dist/auth/jwt-secret.d.ts.map +1 -0
- package/dist/auth/jwt-secret.js +51 -0
- package/dist/auth/jwt-secret.js.map +1 -0
- package/dist/auth/oidc/oidc-flow.d.ts +60 -0
- package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-flow.js +201 -0
- package/dist/auth/oidc/oidc-flow.js.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.js +124 -0
- package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
- package/dist/auth/oidc/user-service.d.ts +67 -0
- package/dist/auth/oidc/user-service.d.ts.map +1 -0
- package/dist/auth/oidc/user-service.js +177 -0
- package/dist/auth/oidc/user-service.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +31 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +70 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/config/config-service.d.ts +83 -0
- package/dist/config/config-service.d.ts.map +1 -0
- package/dist/config/config-service.js +309 -0
- package/dist/config/config-service.js.map +1 -0
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +140 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/content-types/registry.d.ts +75 -0
- package/dist/content-types/registry.d.ts.map +1 -0
- package/dist/content-types/registry.js +171 -0
- package/dist/content-types/registry.js.map +1 -0
- package/dist/datastore/db.d.ts +9 -0
- package/dist/datastore/db.d.ts.map +1 -0
- package/dist/datastore/db.js +27 -0
- package/dist/datastore/db.js.map +1 -0
- package/dist/datastore/encrypt.d.ts +9 -0
- package/dist/datastore/encrypt.d.ts.map +1 -0
- package/dist/datastore/encrypt.js +84 -0
- package/dist/datastore/encrypt.js.map +1 -0
- package/dist/datastore/migrations.d.ts +10 -0
- package/dist/datastore/migrations.d.ts.map +1 -0
- package/dist/datastore/migrations.js +340 -0
- package/dist/datastore/migrations.js.map +1 -0
- package/dist/datastore/storage.d.ts +4 -0
- package/dist/datastore/storage.d.ts.map +1 -0
- package/dist/datastore/storage.js +60 -0
- package/dist/datastore/storage.js.map +1 -0
- package/dist/events/event-bus.d.ts +15 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +140 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/spine.d.ts +31 -0
- package/dist/events/spine.d.ts.map +1 -0
- package/dist/events/spine.js +68 -0
- package/dist/events/spine.js.map +1 -0
- package/dist/events/trace.d.ts +24 -0
- package/dist/events/trace.d.ts.map +1 -0
- package/dist/events/trace.js +33 -0
- package/dist/events/trace.js.map +1 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +94 -0
- package/dist/host.js.map +1 -0
- package/dist/library/importer.d.ts +29 -0
- package/dist/library/importer.d.ts.map +1 -0
- package/dist/library/importer.js +145 -0
- package/dist/library/importer.js.map +1 -0
- package/dist/library/library-cache.d.ts +46 -0
- package/dist/library/library-cache.d.ts.map +1 -0
- package/dist/library/library-cache.js +152 -0
- package/dist/library/library-cache.js.map +1 -0
- package/dist/library/library-config.d.ts +12 -0
- package/dist/library/library-config.d.ts.map +1 -0
- package/dist/library/library-config.js +10 -0
- package/dist/library/library-config.js.map +1 -0
- package/dist/library/library-scanner.d.ts +62 -0
- package/dist/library/library-scanner.d.ts.map +1 -0
- package/dist/library/library-scanner.js +336 -0
- package/dist/library/library-scanner.js.map +1 -0
- package/dist/library/library-service.d.ts +65 -0
- package/dist/library/library-service.d.ts.map +1 -0
- package/dist/library/library-service.js +197 -0
- package/dist/library/library-service.js.map +1 -0
- package/dist/library/metadata-matcher.d.ts +56 -0
- package/dist/library/metadata-matcher.d.ts.map +1 -0
- package/dist/library/metadata-matcher.js +207 -0
- package/dist/library/metadata-matcher.js.map +1 -0
- package/dist/library/naming.d.ts +8 -0
- package/dist/library/naming.d.ts.map +1 -0
- package/dist/library/naming.js +47 -0
- package/dist/library/naming.js.map +1 -0
- package/dist/library/scan-controller.d.ts +95 -0
- package/dist/library/scan-controller.d.ts.map +1 -0
- package/dist/library/scan-controller.js +379 -0
- package/dist/library/scan-controller.js.map +1 -0
- package/dist/library/source-resolver.d.ts +17 -0
- package/dist/library/source-resolver.d.ts.map +1 -0
- package/dist/library/source-resolver.js +75 -0
- package/dist/library/source-resolver.js.map +1 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +107 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/main.d.ts +36 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +229 -0
- package/dist/main.js.map +1 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +110 -0
- package/dist/plugins/context.js.map +1 -0
- package/dist/plugins/discovery.d.ts +2 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +32 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/loader.d.ts +9 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/state/request-intake.d.ts +26 -0
- package/dist/state/request-intake.d.ts.map +1 -0
- package/dist/state/request-intake.js +74 -0
- package/dist/state/request-intake.js.map +1 -0
- package/dist/state/state-store.d.ts +146 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +465 -0
- package/dist/state/state-store.js.map +1 -0
- package/dist/state/trace-store.d.ts +12 -0
- package/dist/state/trace-store.d.ts.map +1 -0
- package/dist/state/trace-store.js +24 -0
- package/dist/state/trace-store.js.map +1 -0
- package/dist/types/plugin.d.ts +93 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +4 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +153 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/routes/activity.d.ts +6 -0
- package/dist/web/routes/activity.d.ts.map +1 -0
- package/dist/web/routes/activity.js +31 -0
- package/dist/web/routes/activity.js.map +1 -0
- package/dist/web/routes/auth.d.ts +6 -0
- package/dist/web/routes/auth.d.ts.map +1 -0
- package/dist/web/routes/auth.js +142 -0
- package/dist/web/routes/auth.js.map +1 -0
- package/dist/web/routes/config.d.ts +7 -0
- package/dist/web/routes/config.d.ts.map +1 -0
- package/dist/web/routes/config.js +204 -0
- package/dist/web/routes/config.js.map +1 -0
- package/dist/web/routes/content-types.d.ts +6 -0
- package/dist/web/routes/content-types.d.ts.map +1 -0
- package/dist/web/routes/content-types.js +29 -0
- package/dist/web/routes/content-types.js.map +1 -0
- package/dist/web/routes/discovery.d.ts +6 -0
- package/dist/web/routes/discovery.d.ts.map +1 -0
- package/dist/web/routes/discovery.js +111 -0
- package/dist/web/routes/discovery.js.map +1 -0
- package/dist/web/routes/library-import.d.ts +6 -0
- package/dist/web/routes/library-import.d.ts.map +1 -0
- package/dist/web/routes/library-import.js +120 -0
- package/dist/web/routes/library-import.js.map +1 -0
- package/dist/web/routes/library-management.d.ts +6 -0
- package/dist/web/routes/library-management.d.ts.map +1 -0
- package/dist/web/routes/library-management.js +120 -0
- package/dist/web/routes/library-management.js.map +1 -0
- package/dist/web/routes/library.d.ts +6 -0
- package/dist/web/routes/library.d.ts.map +1 -0
- package/dist/web/routes/library.js +28 -0
- package/dist/web/routes/library.js.map +1 -0
- package/dist/web/routes/oidc.d.ts +9 -0
- package/dist/web/routes/oidc.d.ts.map +1 -0
- package/dist/web/routes/oidc.js +203 -0
- package/dist/web/routes/oidc.js.map +1 -0
- package/dist/web/routes/requests.d.ts +6 -0
- package/dist/web/routes/requests.d.ts.map +1 -0
- package/dist/web/routes/requests.js +183 -0
- package/dist/web/routes/requests.js.map +1 -0
- package/dist/web/routes/sse.d.ts +6 -0
- package/dist/web/routes/sse.d.ts.map +1 -0
- package/dist/web/routes/sse.js +54 -0
- package/dist/web/routes/sse.js.map +1 -0
- package/package.json +33 -0
- package/package.json.license +2 -0
- package/src/auth/auth-service.ts +333 -0
- package/src/auth/http-server.ts +249 -0
- package/src/auth/jwt-secret.ts +65 -0
- package/src/auth/oidc/oidc-flow.ts +266 -0
- package/src/auth/oidc/oidc-provider-service.ts +174 -0
- package/src/auth/oidc/user-service.ts +227 -0
- package/src/auth/rate-limiter.ts +85 -0
- package/src/config/config-service.ts +434 -0
- package/src/config/validate.ts +153 -0
- package/src/content-types/registry.ts +194 -0
- package/src/datastore/db.ts +38 -0
- package/src/datastore/encrypt.ts +107 -0
- package/src/datastore/migrations.ts +367 -0
- package/src/datastore/storage.ts +83 -0
- package/src/events/event-bus.ts +180 -0
- package/src/events/spine.ts +87 -0
- package/src/events/trace.ts +38 -0
- package/src/host.ts +130 -0
- package/src/library/importer.ts +174 -0
- package/src/library/library-cache.ts +212 -0
- package/src/library/library-config.ts +22 -0
- package/src/library/library-scanner.ts +404 -0
- package/src/library/library-service.ts +282 -0
- package/src/library/metadata-matcher.ts +285 -0
- package/src/library/naming.ts +59 -0
- package/src/library/scan-controller.ts +490 -0
- package/src/library/source-resolver.ts +92 -0
- package/src/logging/logger.ts +115 -0
- package/src/main.ts +364 -0
- package/src/plugins/context.ts +138 -0
- package/src/plugins/discovery.ts +29 -0
- package/src/plugins/loader.ts +309 -0
- package/src/state/request-intake.ts +101 -0
- package/src/state/state-store.ts +621 -0
- package/src/state/trace-store.ts +27 -0
- package/src/types/plugin.ts +110 -0
- package/src/web/index.ts +182 -0
- package/src/web/routes/activity.ts +41 -0
- package/src/web/routes/auth.ts +170 -0
- package/src/web/routes/config.ts +282 -0
- package/src/web/routes/content-types.ts +42 -0
- package/src/web/routes/discovery.ts +156 -0
- package/src/web/routes/library-import.ts +196 -0
- package/src/web/routes/library-management.ts +140 -0
- package/src/web/routes/library.ts +46 -0
- package/src/web/routes/oidc.ts +260 -0
- package/src/web/routes/requests.ts +250 -0
- package/src/web/routes/sse.ts +92 -0
- package/tests/auth.test.ts +510 -0
- package/tests/config-route.test.ts +345 -0
- package/tests/config.test.ts +612 -0
- package/tests/content-types.test.ts +376 -0
- package/tests/datastore.test.ts +345 -0
- package/tests/e2e.test.ts +430 -0
- package/tests/event-bus.test.ts +925 -0
- package/tests/host.test.ts +449 -0
- package/tests/library-cache.test.ts +171 -0
- package/tests/library-scanner.test.ts +177 -0
- package/tests/library.test.ts +210 -0
- package/tests/loader-context.test.ts +644 -0
- package/tests/logger-jsonl.test.ts +154 -0
- package/tests/metadata-matcher.test.ts +198 -0
- package/tests/oidc.test.ts +218 -0
- package/tests/scan-controller.test.ts +328 -0
- package/tests/source-resolver.test.ts +100 -0
- package/tests/starr-video-integration.test.ts +153 -0
- package/tests/state-store.test.ts +700 -0
- package/tests/trace-history.test.ts +356 -0
- package/tsconfig.json +8 -0
- package/tsconfig.json.license +2 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
5
|
+
import type { LibraryService, LibraryInput } from "../../library/library-service.js";
|
|
6
|
+
|
|
7
|
+
let libraryService: LibraryService | null = null;
|
|
8
|
+
|
|
9
|
+
export function initLibraryManagementRoutes(ls: LibraryService): void {
|
|
10
|
+
libraryService = ls;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireAdmin(request: any, reply: any): boolean {
|
|
14
|
+
const user = (request as any).user;
|
|
15
|
+
if (!user || user.role !== "admin") {
|
|
16
|
+
reply.status(403).send({ error: "Forbidden: admin role required" });
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const libraryManagementRoutes: FastifyPluginAsync = async (server) => {
|
|
23
|
+
// GET /api/libraries — list all libraries
|
|
24
|
+
server.get("/api/libraries", async (request, reply) => {
|
|
25
|
+
if (!requireAdmin(request, reply)) return;
|
|
26
|
+
if (!libraryService) {
|
|
27
|
+
reply.status(500).send({ error: "library service not available" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const libraries = libraryService.list();
|
|
31
|
+
return { libraries };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// POST /api/libraries — create a new library
|
|
35
|
+
server.post<{ Body: LibraryInput }>(
|
|
36
|
+
"/api/libraries",
|
|
37
|
+
async (request, reply) => {
|
|
38
|
+
if (!requireAdmin(request, reply)) return;
|
|
39
|
+
if (!libraryService) {
|
|
40
|
+
reply.status(500).send({ error: "library service not available" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const lib = libraryService.create(request.body);
|
|
45
|
+
reply.status(201).send(lib);
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
reply.status(400).send({ error: err.message });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// GET /api/libraries/:id — get a single library (including content type defaults)
|
|
53
|
+
server.get<{ Params: { id: string } }>(
|
|
54
|
+
"/api/libraries/:id",
|
|
55
|
+
async (request, reply) => {
|
|
56
|
+
if (!requireAdmin(request, reply)) return;
|
|
57
|
+
if (!libraryService) {
|
|
58
|
+
reply.status(500).send({ error: "library service not available" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const lib = libraryService.get(request.params.id);
|
|
62
|
+
if (!lib) {
|
|
63
|
+
reply.status(404).send({ error: "library not found" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const contentTypes = libraryService.getContentTypeDefaults(request.params.id);
|
|
67
|
+
return { ...lib, contentTypeDefaults: contentTypes };
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// PUT /api/libraries/:id — update a library
|
|
72
|
+
server.put<{ Params: { id: string }; Body: LibraryInput }>(
|
|
73
|
+
"/api/libraries/:id",
|
|
74
|
+
async (request, reply) => {
|
|
75
|
+
if (!requireAdmin(request, reply)) return;
|
|
76
|
+
if (!libraryService) {
|
|
77
|
+
reply.status(500).send({ error: "library service not available" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const lib = libraryService.update(request.params.id, request.body);
|
|
82
|
+
return lib;
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
reply.status(400).send({ error: err.message });
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// DELETE /api/libraries/:id — delete a library
|
|
90
|
+
server.delete<{ Params: { id: string } }>(
|
|
91
|
+
"/api/libraries/:id",
|
|
92
|
+
async (request, reply) => {
|
|
93
|
+
if (!requireAdmin(request, reply)) return;
|
|
94
|
+
if (!libraryService) {
|
|
95
|
+
reply.status(500).send({ error: "library service not available" });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
libraryService.delete(request.params.id);
|
|
100
|
+
return { deleted: true };
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
reply.status(400).send({ error: err.message });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// PUT /api/libraries/:id/content-types — set content type defaults for a library
|
|
108
|
+
server.put<{ Params: { id: string }; Body: { contentTypeIds: string[] } }>(
|
|
109
|
+
"/api/libraries/:id/content-types",
|
|
110
|
+
async (request, reply) => {
|
|
111
|
+
if (!requireAdmin(request, reply)) return;
|
|
112
|
+
if (!libraryService) {
|
|
113
|
+
reply.status(500).send({ error: "library service not available" });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
libraryService.setContentTypeDefaults(request.params.id, request.body.contentTypeIds);
|
|
118
|
+
return { ok: true };
|
|
119
|
+
} catch (err: any) {
|
|
120
|
+
reply.status(400).send({ error: err.message });
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// GET /api/libraries/:id/content-types — get content type defaults for a library
|
|
126
|
+
server.get<{ Params: { id: string } }>(
|
|
127
|
+
"/api/libraries/:id/content-types",
|
|
128
|
+
async (request, reply) => {
|
|
129
|
+
if (!requireAdmin(request, reply)) return;
|
|
130
|
+
if (!libraryService) {
|
|
131
|
+
reply.status(500).send({ error: "library service not available" });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const defaults = libraryService.getContentTypeDefaults(request.params.id);
|
|
135
|
+
return { contentTypeIds: defaults };
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default libraryManagementRoutes;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
5
|
+
import type { LibraryCache } from "../../library/library-cache.js";
|
|
6
|
+
|
|
7
|
+
let cache: LibraryCache | null = null;
|
|
8
|
+
|
|
9
|
+
export function initLibraryRoutes(lc: LibraryCache): void {
|
|
10
|
+
cache = lc;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const libraryRoutes: FastifyPluginAsync = async (server) => {
|
|
14
|
+
// GET /api/library/items — list imported/library items
|
|
15
|
+
server.get<{
|
|
16
|
+
Querystring: {
|
|
17
|
+
type?: string;
|
|
18
|
+
offset?: string;
|
|
19
|
+
limit?: string;
|
|
20
|
+
};
|
|
21
|
+
}>(
|
|
22
|
+
"/api/library/items",
|
|
23
|
+
async (request, reply) => {
|
|
24
|
+
if (!cache) {
|
|
25
|
+
reply.status(500).send({ error: "library cache not available" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const contentTypeId = request.query.type || undefined;
|
|
30
|
+
const offset = parseInt(request.query.offset ?? "0", 10) || 0;
|
|
31
|
+
const limit = parseInt(request.query.limit ?? "50", 10) || 50;
|
|
32
|
+
|
|
33
|
+
const allItems = cache.getItems(contentTypeId);
|
|
34
|
+
const paged = allItems.slice(offset, offset + limit);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
items: paged,
|
|
38
|
+
total: allItems.length,
|
|
39
|
+
offset,
|
|
40
|
+
limit,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default libraryRoutes;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
5
|
+
import type { AuthService } from "../../auth/auth-service.js";
|
|
6
|
+
import type { OidcProviderService } from "../../auth/oidc/oidc-provider-service.js";
|
|
7
|
+
import type { OidcFlow } from "../../auth/oidc/oidc-flow.js";
|
|
8
|
+
import type { UserService } from "../../auth/oidc/user-service.js";
|
|
9
|
+
import { rateLimit } from "../../auth/rate-limiter.js";
|
|
10
|
+
|
|
11
|
+
let authService: AuthService | null = null;
|
|
12
|
+
let oidcProviderService: OidcProviderService | null = null;
|
|
13
|
+
let oidcFlow: OidcFlow | null = null;
|
|
14
|
+
let userService: UserService | null = null;
|
|
15
|
+
|
|
16
|
+
export function initOidcRoutes(
|
|
17
|
+
auth: AuthService,
|
|
18
|
+
oidcProviders: OidcProviderService,
|
|
19
|
+
flow: OidcFlow,
|
|
20
|
+
users: UserService,
|
|
21
|
+
): void {
|
|
22
|
+
authService = auth;
|
|
23
|
+
oidcProviderService = oidcProviders;
|
|
24
|
+
oidcFlow = flow;
|
|
25
|
+
userService = users;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SESSION_COOKIE = "starr_sid";
|
|
29
|
+
const COOKIE_OPTS = {
|
|
30
|
+
httpOnly: true,
|
|
31
|
+
sameSite: "strict" as const,
|
|
32
|
+
path: "/",
|
|
33
|
+
secure: false,
|
|
34
|
+
maxAge: parseInt(process.env.STARR_SESSION_TTL_HOURS ?? "168", 10) * 3600,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const oidcRoutes: FastifyPluginAsync = async (server) => {
|
|
38
|
+
// ── Public OIDC endpoints (no auth required) ──
|
|
39
|
+
|
|
40
|
+
// GET /api/oidc/providers — list enabled OIDC providers (for login page)
|
|
41
|
+
server.get("/api/oidc/providers", async (_request, reply) => {
|
|
42
|
+
if (!oidcProviderService) {
|
|
43
|
+
reply.status(500).send({ error: "OIDC service not available" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return { providers: oidcProviderService.listEnabled() };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// GET /api/oidc/login/:providerId — initiate OIDC login (rate-limited)
|
|
50
|
+
server.get<{ Params: { providerId: string } }>(
|
|
51
|
+
"/api/oidc/login/:providerId",
|
|
52
|
+
{ preHandler: rateLimit(10, 1) },
|
|
53
|
+
async (request, reply) => {
|
|
54
|
+
if (!oidcFlow) {
|
|
55
|
+
reply.status(500).send({ error: "OIDC flow not available" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const query = request.query as Record<string, string>;
|
|
61
|
+
const redirectTo = query?.redirect ?? "/browse";
|
|
62
|
+
const authUrl = await oidcFlow.initiateLogin(
|
|
63
|
+
request.params.providerId,
|
|
64
|
+
redirectTo,
|
|
65
|
+
);
|
|
66
|
+
// Redirect the browser to the OIDC provider
|
|
67
|
+
reply.redirect(authUrl, 302);
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
reply.status(400).send({ error: err.message ?? "Failed to initiate OIDC login" });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// GET /api/oidc/callback/:providerId — OIDC callback (rate-limited)
|
|
75
|
+
server.get<{
|
|
76
|
+
Params: { providerId: string };
|
|
77
|
+
Querystring: { code?: string; state?: string };
|
|
78
|
+
}>(
|
|
79
|
+
"/api/oidc/callback/:providerId",
|
|
80
|
+
{ preHandler: rateLimit(20, 1) },
|
|
81
|
+
async (request, reply) => {
|
|
82
|
+
if (!oidcFlow || !authService) {
|
|
83
|
+
reply.status(500).send({ error: "OIDC service not available" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { code, state } = request.query;
|
|
88
|
+
if (!code || !state) {
|
|
89
|
+
reply.status(400).send({ error: "Missing code or state parameter" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const { user, redirectTo } = await oidcFlow.handleCallback(
|
|
95
|
+
request.params.providerId,
|
|
96
|
+
code,
|
|
97
|
+
state,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Create a Starr session (JWT) for the user
|
|
101
|
+
const jwt = authService.loginOidc(user.id, user.role);
|
|
102
|
+
reply.setCookie(SESSION_COOKIE, jwt, COOKIE_OPTS);
|
|
103
|
+
|
|
104
|
+
// Redirect to the original target page (or /browse by default)
|
|
105
|
+
reply.redirect(redirectTo, 302);
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
reply.status(401).send({ error: err.message ?? "OIDC login failed" });
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/** Reusable admin-role guard for admin-only routes. */
|
|
113
|
+
function requireAdmin(request: any, reply: any): boolean {
|
|
114
|
+
const user = (request as any).user;
|
|
115
|
+
if (!user) {
|
|
116
|
+
reply.status(401).send({ error: "Unauthorized" });
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (user.role !== "admin") {
|
|
120
|
+
reply.status(403).send({ error: "Forbidden: admin role required" });
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Admin OIDC provider management (auth required, admin-only) ──
|
|
127
|
+
|
|
128
|
+
// GET /api/auth/oidc/providers — list all providers (secrets masked)
|
|
129
|
+
server.get("/api/auth/oidc/providers", async (request, reply) => {
|
|
130
|
+
if (!requireAdmin(request, reply)) return;
|
|
131
|
+
if (!oidcProviderService) {
|
|
132
|
+
reply.status(500).send({ error: "Service not available" });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const providers = oidcProviderService.list();
|
|
136
|
+
const masked = providers.map((p) => ({
|
|
137
|
+
...p,
|
|
138
|
+
clientSecret: p.clientSecret ? "••••••••" : "",
|
|
139
|
+
}));
|
|
140
|
+
return { providers: masked };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// GET /api/auth/oidc/providers/:id — get single provider
|
|
144
|
+
server.get<{ Params: { id: string } }>(
|
|
145
|
+
"/api/auth/oidc/providers/:id",
|
|
146
|
+
async (request, reply) => {
|
|
147
|
+
if (!requireAdmin(request, reply)) return;
|
|
148
|
+
if (!oidcProviderService) {
|
|
149
|
+
reply.status(500).send({ error: "Service not available" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const provider = oidcProviderService.get(request.params.id);
|
|
153
|
+
if (!provider) {
|
|
154
|
+
reply.status(404).send({ error: "Provider not found" });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
...provider,
|
|
159
|
+
clientSecret: provider.clientSecret ? "••••••••" : "",
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// POST /api/auth/oidc/providers — create provider
|
|
165
|
+
server.post("/api/auth/oidc/providers", async (request, reply) => {
|
|
166
|
+
if (!requireAdmin(request, reply)) return;
|
|
167
|
+
if (!oidcProviderService) {
|
|
168
|
+
reply.status(500).send({ error: "Service not available" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const provider = oidcProviderService.create(request.body as any);
|
|
173
|
+
reply.status(201).send({ ...provider, clientSecret: "••••••••" });
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
reply.status(400).send({ error: err.message ?? "Failed to create provider" });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// PUT /api/auth/oidc/providers/:id — update provider
|
|
180
|
+
server.put<{ Params: { id: string } }>(
|
|
181
|
+
"/api/auth/oidc/providers/:id",
|
|
182
|
+
async (request, reply) => {
|
|
183
|
+
if (!requireAdmin(request, reply)) return;
|
|
184
|
+
if (!oidcProviderService) {
|
|
185
|
+
reply.status(500).send({ error: "Service not available" });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const provider = oidcProviderService.update(
|
|
189
|
+
request.params.id,
|
|
190
|
+
request.body as any,
|
|
191
|
+
);
|
|
192
|
+
if (!provider) {
|
|
193
|
+
reply.status(404).send({ error: "Provider not found" });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
return { ...provider, clientSecret: "••••••••" };
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// DELETE /api/auth/oidc/providers/:id — delete provider
|
|
201
|
+
server.delete<{ Params: { id: string } }>(
|
|
202
|
+
"/api/auth/oidc/providers/:id",
|
|
203
|
+
async (request, reply) => {
|
|
204
|
+
if (!requireAdmin(request, reply)) return;
|
|
205
|
+
if (!oidcProviderService) {
|
|
206
|
+
reply.status(500).send({ error: "Service not available" });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const deleted = oidcProviderService.delete(request.params.id);
|
|
210
|
+
if (!deleted) {
|
|
211
|
+
reply.status(404).send({ error: "Provider not found" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
return { ok: true };
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ── Admin user management ──
|
|
219
|
+
|
|
220
|
+
// GET /api/auth/users — list all users
|
|
221
|
+
server.get("/api/auth/users", async (request, reply) => {
|
|
222
|
+
if (!requireAdmin(request, reply)) return;
|
|
223
|
+
if (!userService) {
|
|
224
|
+
reply.status(500).send({ error: "Service not available" });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
return { users: userService.list() };
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// PATCH /api/auth/users/:id/role — change user role
|
|
231
|
+
server.patch<{ Params: { id: string }; Body: { role: "admin" | "user" } }>(
|
|
232
|
+
"/api/auth/users/:id/role",
|
|
233
|
+
async (request, reply) => {
|
|
234
|
+
if (!requireAdmin(request, reply)) return;
|
|
235
|
+
if (!userService) {
|
|
236
|
+
reply.status(500).send({ error: "Service not available" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { role } = request.body;
|
|
241
|
+
if (role !== "admin" && role !== "user") {
|
|
242
|
+
reply.status(400).send({ error: "Role must be 'admin' or 'user'" });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const user = userService.setRole(request.params.id, role);
|
|
248
|
+
if (!user) {
|
|
249
|
+
reply.status(404).send({ error: "User not found" });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
return { user };
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
reply.status(403).send({ error: err.message ?? "Failed to change role" });
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export default oidcRoutes;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
5
|
+
import type { ContentItemBase } from "@digitalpaws/starr-types-core";
|
|
6
|
+
import type { Starr } from "../../host.js";
|
|
7
|
+
import { requestIntake } from "../../state/request-intake.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory debounce map to prevent rapid retries.
|
|
11
|
+
* Maps requestId → timestamp of last retry.
|
|
12
|
+
*/
|
|
13
|
+
const retryDebounceMap = new Map<string, number>();
|
|
14
|
+
const RETRY_DEBOUNCE_MS = 10_000; // 10 seconds
|
|
15
|
+
|
|
16
|
+
class RouteError extends Error {
|
|
17
|
+
status: number;
|
|
18
|
+
constructor(status: number, message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retry a failed or missing content request.
|
|
26
|
+
*
|
|
27
|
+
* 1. Look up the TrackedRequest; 404 if not found.
|
|
28
|
+
* 2. Check status guards: must be 'failed' or 'downloaded'.
|
|
29
|
+
* 3. Debounce: reject if retried within 10 seconds.
|
|
30
|
+
* 4. Reset the request status and emit a fresh content.requested.
|
|
31
|
+
*
|
|
32
|
+
* Returns the updated request.
|
|
33
|
+
*/
|
|
34
|
+
async function retryRequest(
|
|
35
|
+
requestId: string,
|
|
36
|
+
userId: string,
|
|
37
|
+
starr: Starr,
|
|
38
|
+
): Promise<{ requestId: string; status: string }> {
|
|
39
|
+
const stateStore = starr.getStateStore();
|
|
40
|
+
const logger = starr.logger;
|
|
41
|
+
|
|
42
|
+
// 1. Look up the request
|
|
43
|
+
const req = stateStore.getByRequestId(requestId);
|
|
44
|
+
if (!req) {
|
|
45
|
+
throw new RouteError(404, "Request not found");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Status guard: only failed or downloaded can be retried
|
|
49
|
+
if (req.status === "searching" || req.status === "downloading" || req.status === "requested") {
|
|
50
|
+
throw new RouteError(
|
|
51
|
+
409,
|
|
52
|
+
`Cannot retry request in status "${req.status}". Wait for the current run to finish.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Debounce check
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const lastRetry = retryDebounceMap.get(requestId);
|
|
59
|
+
if (lastRetry && now - lastRetry < RETRY_DEBOUNCE_MS) {
|
|
60
|
+
const remaining = Math.ceil((RETRY_DEBOUNCE_MS - (now - lastRetry)) / 1000);
|
|
61
|
+
throw new RouteError(
|
|
62
|
+
429,
|
|
63
|
+
`Please wait ${remaining}s before retrying again.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
retryDebounceMap.set(requestId, now);
|
|
67
|
+
|
|
68
|
+
// Cleanup stale entries from debounce map (older than 1 minute)
|
|
69
|
+
for (const [key, ts] of retryDebounceMap) {
|
|
70
|
+
if (now - ts > 60_000) retryDebounceMap.delete(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.info("manual retry initiated", {
|
|
74
|
+
requestId,
|
|
75
|
+
previousStatus: req.status,
|
|
76
|
+
userId,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 4. Reset and re-emit
|
|
80
|
+
stateStore.resetAndRetry(requestId);
|
|
81
|
+
|
|
82
|
+
// Re-emit content.requested to restart the pipeline
|
|
83
|
+
starr.coreBus.emit("content.requested", {
|
|
84
|
+
requestId,
|
|
85
|
+
userId: req.ownerUserId,
|
|
86
|
+
contentTypeId: req.contentTypeId,
|
|
87
|
+
item: req.item,
|
|
88
|
+
libraryId: req.libraryId,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { requestId, status: "requested" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let starrInstance: Starr | null = null;
|
|
95
|
+
|
|
96
|
+
export function initRequestRoutes(starr: Starr): void {
|
|
97
|
+
starrInstance = starr;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const requestRoutes: FastifyPluginAsync = async (server) => {
|
|
101
|
+
// POST /api/requests — create a content request
|
|
102
|
+
server.post<{ Body: ContentItemBase }>(
|
|
103
|
+
"/api/requests",
|
|
104
|
+
async (request, reply) => {
|
|
105
|
+
if (!starrInstance) {
|
|
106
|
+
reply.status(500).send({ error: "host not available" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const item = request.body;
|
|
111
|
+
const user = (request as any).user;
|
|
112
|
+
|
|
113
|
+
if (!item || !item.contentTypeId || !item.title) {
|
|
114
|
+
reply.status(400).send({ error: "invalid content item" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const stateStore = starrInstance.getStateStore();
|
|
119
|
+
const registry = starrInstance.contentTypeRegistry;
|
|
120
|
+
const libraryService = starrInstance.libraryService ?? undefined;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const requestId = requestIntake(
|
|
124
|
+
{ userId: user.username, item },
|
|
125
|
+
stateStore,
|
|
126
|
+
registry,
|
|
127
|
+
starrInstance.coreBus,
|
|
128
|
+
starrInstance.logger,
|
|
129
|
+
libraryService,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Check if this was a new request or re-attach to existing
|
|
133
|
+
const req = stateStore.getByRequestId(requestId);
|
|
134
|
+
const existing = req && req.ownerUserId !== user.username;
|
|
135
|
+
|
|
136
|
+
reply.status(201).send({ requestId, existing });
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
reply.status(500).send({ error: String(err) });
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// GET /api/requests — list tracked requests
|
|
144
|
+
server.get("/api/requests", async (request, reply) => {
|
|
145
|
+
if (!starrInstance) {
|
|
146
|
+
reply.status(500).send({ error: "host not available" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const user = (request as any).user;
|
|
151
|
+
const stateStore = starrInstance.getStateStore();
|
|
152
|
+
|
|
153
|
+
if (user.role === "admin") {
|
|
154
|
+
// Admin sees all requests
|
|
155
|
+
const requests = stateStore.getAllRequests();
|
|
156
|
+
return { requests };
|
|
157
|
+
} else {
|
|
158
|
+
// Regular user sees only their own + watched
|
|
159
|
+
const requests = stateStore.listByUser(user.username);
|
|
160
|
+
return { requests };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// GET /api/requests/:id — get single request detail
|
|
165
|
+
server.get<{ Params: { id: string } }>(
|
|
166
|
+
"/api/requests/:id",
|
|
167
|
+
async (request, reply) => {
|
|
168
|
+
if (!starrInstance) {
|
|
169
|
+
reply.status(500).send({ error: "host not available" });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const req = starrInstance.getStateStore().getByRequestId(request.params.id);
|
|
174
|
+
if (!req) {
|
|
175
|
+
reply.status(404).send({ error: "request not found" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
return req;
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// POST /api/requests/:id/retry — manually retry a failed or missing request
|
|
183
|
+
server.post<{ Params: { id: string } }>(
|
|
184
|
+
"/api/requests/:id/retry",
|
|
185
|
+
async (request, reply) => {
|
|
186
|
+
if (!starrInstance) {
|
|
187
|
+
reply.status(500).send({ error: "host not available" });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const requestId = request.params.id;
|
|
192
|
+
const user = (request as any).user;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const result = await retryRequest(
|
|
196
|
+
requestId,
|
|
197
|
+
user.username,
|
|
198
|
+
starrInstance,
|
|
199
|
+
);
|
|
200
|
+
return result;
|
|
201
|
+
} catch (err: any) {
|
|
202
|
+
if (err.status && err.message) {
|
|
203
|
+
reply.status(err.status).send({ error: err.message });
|
|
204
|
+
} else {
|
|
205
|
+
reply.status(500).send({ error: String(err) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// DELETE /api/requests/:id — permanently remove a request
|
|
212
|
+
server.delete<{ Params: { id: string } }>(
|
|
213
|
+
"/api/requests/:id",
|
|
214
|
+
async (request, reply) => {
|
|
215
|
+
if (!starrInstance) {
|
|
216
|
+
reply.status(500).send({ error: "host not available" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const requestId = request.params.id;
|
|
221
|
+
const user = (request as any).user;
|
|
222
|
+
const stateStore = starrInstance.getStateStore();
|
|
223
|
+
|
|
224
|
+
const req = stateStore.getByRequestId(requestId);
|
|
225
|
+
if (!req) {
|
|
226
|
+
reply.status(404).send({ error: "Request not found" });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const previousStatus = req.status;
|
|
231
|
+
stateStore.deleteRequest(requestId);
|
|
232
|
+
|
|
233
|
+
// Notify plugins so they can clean up storage and cancel in-flight operations
|
|
234
|
+
starrInstance.coreBus.emit("request.deleted", {
|
|
235
|
+
requestId,
|
|
236
|
+
previousStatus,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
starrInstance.logger.info("request deleted via API", {
|
|
240
|
+
requestId,
|
|
241
|
+
userId: user.username,
|
|
242
|
+
previousStatus,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return { deleted: true, requestId };
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export default requestRoutes;
|