streamby-core 0.27.4 → 0.28.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/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { createStreamByRouter } from './middleware/createRouter';
2
2
  export { emitToUser } from './services/wsHub';
3
3
  export { createNotification } from './services/notification';
4
+ export type { UserPlan, UserSubscription } from './types';
@@ -23,6 +23,7 @@ const connection_1 = require("./routes/connection");
23
23
  const dbConnection_1 = require("./routes/dbConnection");
24
24
  const storageConnection_1 = require("./routes/storageConnection");
25
25
  const notification_1 = require("./routes/notification");
26
+ const subscription_1 = require("./routes/subscription");
26
27
  function createStreamByRouter(config) {
27
28
  const router = express_1.default.Router();
28
29
  if (config.encrypt)
@@ -42,6 +43,7 @@ function createStreamByRouter(config) {
42
43
  const mainDb = config.databases.find(db => db.main);
43
44
  if (mainDb) {
44
45
  (0, manager_1.registerModel)('users', [mainDb.id], 'users', 'accounts');
46
+ (0, manager_1.registerModel)('user_subscriptions', [mainDb.id], 'user_subscriptions', 'streamby');
45
47
  }
46
48
  }
47
49
  if (config.websocket?.server) {
@@ -50,6 +52,7 @@ function createStreamByRouter(config) {
50
52
  router.use((0, user_1.userRouter)(config));
51
53
  router.use((0, auth_2.authRouter)(config));
52
54
  router.use((0, auth_1.authenticate)(config));
55
+ router.use(subscription_1.ensureSubscription);
53
56
  router.use((0, export_1.exportRouter)(config));
54
57
  router.use((0, member_1.memberRouter)(config));
55
58
  router.use((0, storage_2.storageRouter)(config));
@@ -60,5 +63,6 @@ function createStreamByRouter(config) {
60
63
  router.use((0, dbConnection_1.dbConnectionRouter)(config));
61
64
  router.use((0, storageConnection_1.storageConnectionRouter)(config));
62
65
  router.use((0, notification_1.notificationRouter)(config));
66
+ router.use((0, subscription_1.subscriptionRouter)(config));
63
67
  return router;
64
68
  }
@@ -295,48 +295,51 @@ function exportRouter(config) {
295
295
  if (!exportMetadata) {
296
296
  return res.status(404).json({ message: 'Export not found in this project' });
297
297
  }
298
- const origin = req.headers.origin;
299
- let effectiveAllowedOrigins = exportMetadata.allowedOrigin;
300
- const projectOrigins = project.allowedOrigin;
301
- // If export's allowedOrigin is ['*'] or it's not set, it inherits from the project.
302
- if (!effectiveAllowedOrigins || (effectiveAllowedOrigins.length === 1 && effectiveAllowedOrigins[0] === '*')) {
303
- effectiveAllowedOrigins = projectOrigins;
304
- }
305
- else {
306
- // If the export has its own list, ensure it's a subset of the project's list (if project is not public).
307
- if (projectOrigins && !projectOrigins.includes('*')) {
308
- const isSubset = effectiveAllowedOrigins.every((o) => projectOrigins.includes(o));
309
- if (!isSubset) {
310
- return res.status(403).json({ message: 'Unauthorized: Export origins are not allowed by the parent project.' });
311
- }
298
+ // Authenticated project members bypass origin checks (e.g. dev environments).
299
+ const auth = await config.authProvider(req).catch(() => null);
300
+ const isMember = auth?.userId && (0, auth_1.isProjectMember)(project, auth.userId);
301
+ if (!isMember) {
302
+ const origin = req.headers.origin;
303
+ let effectiveAllowedOrigins = exportMetadata.allowedOrigin;
304
+ const projectOrigins = project.allowedOrigin;
305
+ // If export's allowedOrigin is empty, ['*'], or not set, it inherits from the project.
306
+ if (!effectiveAllowedOrigins || effectiveAllowedOrigins.length === 0 || (effectiveAllowedOrigins.length === 1 && effectiveAllowedOrigins[0] === '*')) {
307
+ effectiveAllowedOrigins = projectOrigins;
312
308
  }
313
- }
314
- // Dev mode: auto-allow localhost requests on configured ports.
315
- let devModeAllowed = false;
316
- if (exportMetadata.devMode && origin) {
317
- const match = origin.match(/^https?:\/\/localhost:(\d+)$/);
318
- if (match) {
319
- const port = parseInt(match[1]);
320
- const allowedPorts = exportMetadata.devPorts?.length
321
- ? exportMetadata.devPorts
322
- : [3000, 5173, 8080, 4200];
323
- if (allowedPorts.includes(port)) {
324
- devModeAllowed = true;
325
- res.header('Access-Control-Allow-Origin', origin);
326
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
327
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
309
+ else {
310
+ // If the export has its own list, ensure it's a subset of the project's list (if project is not public).
311
+ if (projectOrigins && !projectOrigins.includes('*')) {
312
+ const isSubset = effectiveAllowedOrigins.every((o) => projectOrigins.includes(o));
313
+ if (!isSubset) {
314
+ return res.status(403).json({ message: 'Unauthorized: Export origins are not allowed by the parent project.' });
315
+ }
328
316
  }
329
317
  }
330
- }
331
- if (!devModeAllowed) {
332
- // If there's no origin header, deny access unless the effective scope is public.
333
- if (!origin && !(effectiveAllowedOrigins && effectiveAllowedOrigins.includes('*'))) {
334
- return res.status(403).json({ message: 'Origin header required' });
318
+ // Dev mode: auto-allow localhost requests on configured ports.
319
+ let devModeAllowed = false;
320
+ if (exportMetadata.devMode && origin) {
321
+ const match = origin.match(/^https?:\/\/localhost:(\d+)$/);
322
+ if (match) {
323
+ const port = parseInt(match[1]);
324
+ const allowedPorts = exportMetadata.devPorts?.length
325
+ ? exportMetadata.devPorts
326
+ : [3000, 5173, 8080, 4200];
327
+ if (allowedPorts.includes(port)) {
328
+ devModeAllowed = true;
329
+ res.header('Access-Control-Allow-Origin', origin);
330
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
331
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
332
+ }
333
+ }
335
334
  }
336
- // Check for public access ('*') or if the request's origin is in the effective list.
337
- const isAllowed = effectiveAllowedOrigins && (effectiveAllowedOrigins.includes('*') || (origin && effectiveAllowedOrigins.includes(origin)));
338
- if (!isAllowed) {
339
- return res.status(403).json({ message: 'Unauthorized' });
335
+ if (!devModeAllowed) {
336
+ if (!origin && !(effectiveAllowedOrigins && effectiveAllowedOrigins.includes('*'))) {
337
+ return res.status(403).json({ message: 'Origin header required' });
338
+ }
339
+ const isAllowed = effectiveAllowedOrigins && (effectiveAllowedOrigins.includes('*') || (origin && effectiveAllowedOrigins.includes(origin)));
340
+ if (!isAllowed) {
341
+ return res.status(403).json({ message: 'Unauthorized' });
342
+ }
340
343
  }
341
344
  }
342
345
  let data;
@@ -197,6 +197,88 @@ function memberRouter(config) {
197
197
  res.status(500).json({ message: 'Failed to reject invitation', details: err.message });
198
198
  }
199
199
  });
200
+ router.post('/projects/:id/request-join', async (req, res) => {
201
+ try {
202
+ const auth = req.auth;
203
+ const projectId = req.params.id;
204
+ const project = await Project.findOne({ _id: projectId });
205
+ if (!project) {
206
+ return res.status(404).json({ message: 'Project not found' });
207
+ }
208
+ if (project.public === false) {
209
+ return res.status(403).json({ message: 'This project is not open for join requests' });
210
+ }
211
+ const alreadyMember = project.members?.some((m) => m.userId?.toString() === auth.userId?.toString());
212
+ if (alreadyMember) {
213
+ return res.status(400).json({ message: 'You are already a member or have a pending request' });
214
+ }
215
+ const User = (0, manager_1.getModel)('users');
216
+ const user = await User.findOne({ _id: auth.userId });
217
+ const username = user?.username || auth.username;
218
+ if (project.dbType === 'sql') {
219
+ const connection = getSqlConnection();
220
+ if (!connection) {
221
+ return res.status(503).json({ message: 'SQL connection not available' });
222
+ }
223
+ await sql_1.sqlAdapter.create(connection, 'project_members', {
224
+ projectId,
225
+ userId: auth.userId,
226
+ role: 'viewer',
227
+ archived: false,
228
+ status: 'pending',
229
+ invitedBy: auth.userId,
230
+ }, 'streamby');
231
+ }
232
+ else {
233
+ await Project.update({ _id: projectId }, {
234
+ $push: { members: { userId: auth.userId, username, role: 'viewer', archived: false, status: 'pending', invitedBy: auth.userId } },
235
+ });
236
+ }
237
+ const admins = (project.members || []).filter((m) => m.role === 'admin' && m.status === 'active');
238
+ await Promise.all(admins.map((admin) => (0, notification_1.createNotification)(admin.userId, 'join_request', `${username} has requested to join "${project.name}"`, { projectId, userId: auth.userId })));
239
+ res.status(201).json({ message: 'Join request sent successfully' });
240
+ }
241
+ catch (err) {
242
+ res.status(500).json({ message: 'Failed to send join request', details: err.message });
243
+ }
244
+ });
245
+ router.patch('/projects/:id/members/:userId/approve', async (req, res) => {
246
+ try {
247
+ const auth = req.auth;
248
+ const { id: projectId, userId: targetUserId } = req.params;
249
+ const project = await Project.findOne({ _id: projectId });
250
+ if (!project) {
251
+ return res.status(404).json({ message: 'Project not found' });
252
+ }
253
+ const requestingMember = project.members?.find((m) => m.userId?.toString() === auth.userId?.toString());
254
+ if (!requestingMember || requestingMember.status !== 'active' || requestingMember.role !== 'admin') {
255
+ return res.status(403).json({ message: 'Only project admins can approve requests' });
256
+ }
257
+ const targetMember = project.members?.find((m) => m.userId?.toString() === targetUserId?.toString());
258
+ if (!targetMember) {
259
+ return res.status(404).json({ message: 'Member not found in project' });
260
+ }
261
+ if (targetMember.status !== 'pending') {
262
+ return res.status(400).json({ message: 'No pending request for this user' });
263
+ }
264
+ if (project.dbType === 'sql') {
265
+ const connection = getSqlConnection();
266
+ if (!connection) {
267
+ return res.status(503).json({ message: 'SQL connection not available' });
268
+ }
269
+ await sql_1.sqlAdapter.update(connection, 'project_members', { projectId, userId: targetUserId }, { status: 'active' }, 'streamby');
270
+ }
271
+ else {
272
+ const updatedMembers = project.members.map((m) => m.userId?.toString() === targetUserId ? { ...m, status: 'active' } : m);
273
+ await Project.update({ _id: projectId }, { members: updatedMembers });
274
+ }
275
+ await (0, notification_1.createNotification)(targetUserId, 'join_approved', `Your request to join "${project.name}" has been approved`, { projectId });
276
+ res.status(200).json({ message: 'Join request approved' });
277
+ }
278
+ catch (err) {
279
+ res.status(500).json({ message: 'Failed to approve join request', details: err.message });
280
+ }
281
+ });
200
282
  router.delete('/projects/:id/members/:userId', async (req, res) => {
201
283
  try {
202
284
  const auth = req.auth;
@@ -44,13 +44,40 @@ function projectRouter(config) {
44
44
  res.status(500).json({ message: 'Failed to list projects', details: err });
45
45
  }
46
46
  });
47
+ router.get('/projects/explore', async (req, res) => {
48
+ try {
49
+ const auth = req.auth;
50
+ const allProjects = await Project.find({});
51
+ const result = allProjects
52
+ .filter((p) => p.public !== false)
53
+ .map((p) => ({
54
+ id: p._id || p.id,
55
+ name: p.name,
56
+ description: p.description || '',
57
+ image: p.image || '',
58
+ memberCount: (p.members || []).filter((m) => m.status === 'active').length,
59
+ isMember: (p.members || []).some((m) => m.userId?.toString() === auth.userId && m.status === 'active'),
60
+ hasPendingRequest: (p.members || []).some((m) => m.userId?.toString() === auth.userId && m.status === 'pending'),
61
+ }));
62
+ res.json({ projects: result });
63
+ }
64
+ catch (err) {
65
+ res.status(500).json({ message: 'Failed to explore projects', details: err.message });
66
+ }
67
+ });
47
68
  router.post('/projects/create', async (req, res) => {
48
69
  try {
49
70
  const auth = req.auth;
71
+ if (req.subscription === 'freemium') {
72
+ return res.status(403).json({ error: 'Freemium users cannot create projects' });
73
+ }
50
74
  if (auth.role !== 'admin' && auth.role !== 'editor') {
51
75
  return res.status(403).json({ message: 'Permission denied' });
52
76
  }
53
- const { name, description, image, allowedOrigin } = req.body;
77
+ const { name, description, image, allowedOrigin, public: isPublic = true } = req.body;
78
+ if (isPublic === false && req.subscription === 'freemium') {
79
+ return res.status(403).json({ error: 'Private projects require a subscriber plan' });
80
+ }
54
81
  const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
55
82
  if (!mainDb) {
56
83
  return res.status(500).json({ message: 'No database configured' });
@@ -67,6 +94,7 @@ function projectRouter(config) {
67
94
  description: description || '',
68
95
  image: image || '',
69
96
  allowedOrigin: allowedOrigin || [],
97
+ public: isPublic,
70
98
  members: [{ userId: auth.userId, username: user.username, role: "admin", status: "active", archived: false }],
71
99
  });
72
100
  const allProjects = await Project.find({});
@@ -0,0 +1,4 @@
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare const ensureSubscription: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4
+ export declare function subscriptionRouter(config: StreamByConfig): Router;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureSubscription = void 0;
4
+ exports.subscriptionRouter = subscriptionRouter;
5
+ const express_1 = require("express");
6
+ const manager_1 = require("../../models/manager");
7
+ const resolveSubscription = async (auth) => {
8
+ const Subscription = (0, manager_1.getModel)('user_subscriptions');
9
+ const existing = await Subscription.findOne({ user_id: auth.userId });
10
+ if (existing)
11
+ return existing.plan;
12
+ const plan = auth.role === 'admin' ? 'admin' : 'freemium';
13
+ await Subscription.create({ user_id: auth.userId, plan, dbType: 'sql' });
14
+ return plan;
15
+ };
16
+ const ensureSubscription = async (req, res, next) => {
17
+ try {
18
+ const auth = req.auth;
19
+ const plan = await resolveSubscription(auth);
20
+ req.subscription = plan;
21
+ next();
22
+ }
23
+ catch (err) {
24
+ res.status(403).json({ error: 'Unauthorized', details: err.message });
25
+ }
26
+ };
27
+ exports.ensureSubscription = ensureSubscription;
28
+ function subscriptionRouter(config) {
29
+ const router = (0, express_1.Router)();
30
+ router.get('/user/subscription', async (req, res) => {
31
+ res.json({ subscription: req.subscription });
32
+ });
33
+ return router;
34
+ }
@@ -180,8 +180,10 @@ class Model {
180
180
  if (this.tableName === 'projects' && key === 'members') {
181
181
  membersToInsert = data[key];
182
182
  }
183
- else if (this.tableName === 'projects' && key.toLowerCase() === 'dbtype') {
184
- dataToInsert["dbType"] = data[key];
183
+ else if (key.toLowerCase() === 'dbtype') {
184
+ if (this.tableName === 'projects')
185
+ dataToInsert["dbType"] = data[key];
186
+ // else: routing hint only — never a real column
185
187
  }
186
188
  else {
187
189
  dataToInsert[key] = data[key];
@@ -54,6 +54,7 @@ export interface ProjectInfo {
54
54
  dbType?: DatabaseType;
55
55
  name: string;
56
56
  image?: string;
57
+ public?: boolean;
57
58
  members?: {
58
59
  role: 'viewer' | 'editor' | 'admin';
59
60
  userId: string;
@@ -135,6 +136,14 @@ export interface Auth {
135
136
  role: 'viewer' | 'editor' | 'admin';
136
137
  }
137
138
  export type AuthProvider = (req: Request) => Promise<Auth>;
139
+ export type UserPlan = 'freemium' | 'subscriber' | 'admin';
140
+ export interface UserSubscription {
141
+ id: string;
142
+ user_id: string;
143
+ plan: UserPlan;
144
+ created_at: Date;
145
+ updated_at: Date;
146
+ }
138
147
  export type ExternalDbType = 'postgresql' | 'mongodb';
139
148
  export interface DbConnection {
140
149
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamby-core",
3
- "version": "0.27.4",
3
+ "version": "0.28.1",
4
4
  "description": "StreamBy middleware framework for media storage management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",