@rebasepro/server-core 0.3.0 → 0.4.0
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/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/index.es.js +318 -282
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +318 -282
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/rest/query-parser.d.ts +2 -0
- package/dist/server-core/src/api/types.d.ts +2 -1
- package/dist/server-core/src/email/types.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +31 -1
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +21 -3
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +9 -5
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +5 -5
- package/src/api/rest/api-generator.ts +11 -9
- package/src/api/rest/query-parser.ts +184 -63
- package/src/api/types.ts +2 -1
- package/src/auth/admin-routes.ts +2 -91
- package/src/auth/builtin-auth-adapter.ts +5 -55
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/email/smtp-email-service.ts +31 -0
- package/src/email/types.ts +1 -0
- package/src/init.ts +136 -24
- package/src/storage/image-transform.ts +2 -1
- package/test/admin-routes.test.ts +0 -169
- package/test/backend-hooks-admin.test.ts +0 -25
- package/test/custom-auth-adapter.test.ts +2 -10
- package/test/smtp-email-service.test.ts +169 -0
- package/dist/types/src/users/roles.d.ts +0 -14
package/src/auth/admin-routes.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { requireAuth, requireAdmin, createRequireAuth } from "./middleware";
|
|
|
5
5
|
import type { AuthHooks } from "./auth-hooks";
|
|
6
6
|
import { resolveAuthHooks } from "./auth-hooks";
|
|
7
7
|
import { AuthModuleConfig } from "./routes";
|
|
8
|
-
import type { BackendHooks, AdminUser,
|
|
8
|
+
import type { BackendHooks, AdminUser, BackendHookContext } from "@rebasepro/types";
|
|
9
9
|
|
|
10
10
|
interface AdminRouteOptions extends AuthModuleConfig {
|
|
11
11
|
serviceKey?: string;
|
|
@@ -99,12 +99,7 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
99
99
|
return results.filter((u): u is AdminUser => u !== null);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
async function applyRoleAfterReadBatch(roles: AdminRole[], ctx: BackendHookContext): Promise<AdminRole[]> {
|
|
104
|
-
if (!hooks?.roles?.afterRead) return roles;
|
|
105
|
-
const results = await Promise.all(roles.map(r => hooks!.roles!.afterRead!(r, ctx)));
|
|
106
|
-
return results.filter((r): r is AdminRole => r !== null);
|
|
107
|
-
}
|
|
102
|
+
|
|
108
103
|
|
|
109
104
|
/** Convert a DB user record + role IDs into the AdminUser API shape */
|
|
110
105
|
function toAdminUser(u: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; createdAt?: Date | string; updatedAt?: Date | string }, roles: string[]): AdminUser {
|
|
@@ -533,91 +528,7 @@ displayName: existing.displayName }, appName);
|
|
|
533
528
|
return c.json({ success: true });
|
|
534
529
|
});
|
|
535
530
|
|
|
536
|
-
router.get("/roles", requireAdmin, async (c) => {
|
|
537
|
-
const roles = await authRepo.listRoles();
|
|
538
|
-
const hookCtx = buildHookContext(c, "GET");
|
|
539
|
-
|
|
540
|
-
let adminRoles: AdminRole[] = roles.map(r => ({
|
|
541
|
-
id: r.id,
|
|
542
|
-
name: r.name,
|
|
543
|
-
isAdmin: r.isAdmin,
|
|
544
|
-
defaultPermissions: r.defaultPermissions
|
|
545
|
-
}));
|
|
546
|
-
|
|
547
|
-
adminRoles = await applyRoleAfterReadBatch(adminRoles, hookCtx);
|
|
548
|
-
|
|
549
|
-
return c.json({ roles: adminRoles });
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
router.get("/roles/:roleId", requireAdmin, async (c) => {
|
|
553
|
-
const roleId = c.req.param("roleId");
|
|
554
|
-
const role = await authRepo.getRoleById(roleId);
|
|
555
|
-
|
|
556
|
-
if (!role) {
|
|
557
|
-
throw ApiError.notFound("Role not found");
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return c.json({ role });
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
router.post("/roles", requireAdmin, async (c) => {
|
|
564
|
-
const body = await c.req.json();
|
|
565
|
-
const { id, name, isAdmin, defaultPermissions } = body;
|
|
566
|
-
|
|
567
|
-
if (!id || !name) {
|
|
568
|
-
throw ApiError.badRequest("Role ID and name are required", "INVALID_INPUT");
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const existing = await authRepo.getRoleById(id);
|
|
572
|
-
if (existing) {
|
|
573
|
-
throw ApiError.conflict("Role already exists", "ROLE_EXISTS");
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const role = await authRepo.createRole({
|
|
577
|
-
id,
|
|
578
|
-
name,
|
|
579
|
-
isAdmin: isAdmin ?? false,
|
|
580
|
-
defaultPermissions: defaultPermissions ?? null
|
|
581
|
-
});
|
|
582
531
|
|
|
583
|
-
return c.json({ role }, 201);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
router.put("/roles/:roleId", requireAdmin, async (c) => {
|
|
587
|
-
const roleId = c.req.param("roleId");
|
|
588
|
-
const body = await c.req.json();
|
|
589
|
-
const { name, isAdmin, defaultPermissions } = body;
|
|
590
|
-
|
|
591
|
-
const existing = await authRepo.getRoleById(roleId);
|
|
592
|
-
if (!existing) {
|
|
593
|
-
throw ApiError.notFound("Role not found");
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const role = await authRepo.updateRole(roleId, {
|
|
597
|
-
name,
|
|
598
|
-
isAdmin,
|
|
599
|
-
defaultPermissions
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
return c.json({ role });
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
router.delete("/roles/:roleId", requireAdmin, async (c) => {
|
|
606
|
-
const roleId = c.req.param("roleId");
|
|
607
|
-
|
|
608
|
-
if (["admin", "editor", "viewer"].includes(roleId)) {
|
|
609
|
-
throw ApiError.badRequest("Cannot delete built-in roles", "BUILTIN_ROLE");
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const existing = await authRepo.getRoleById(roleId);
|
|
613
|
-
if (!existing) {
|
|
614
|
-
throw ApiError.notFound("Role not found");
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
await authRepo.deleteRole(roleId);
|
|
618
|
-
|
|
619
|
-
return c.json({ success: true });
|
|
620
|
-
});
|
|
621
532
|
|
|
622
533
|
return router;
|
|
623
534
|
}
|
|
@@ -15,13 +15,10 @@ import type {
|
|
|
15
15
|
AuthenticatedUser,
|
|
16
16
|
AuthAdapterCapabilities,
|
|
17
17
|
UserManagementAdapter,
|
|
18
|
-
RoleManagementAdapter,
|
|
19
18
|
AuthUserListOptions,
|
|
20
19
|
AuthUserListResult,
|
|
21
20
|
AuthUserData,
|
|
22
21
|
AuthCreateUserData,
|
|
23
|
-
AuthRoleData,
|
|
24
|
-
AuthCreateRoleData,
|
|
25
22
|
BootstrappedAuth,
|
|
26
23
|
BackendHooks,
|
|
27
24
|
} from "@rebasepro/types";
|
|
@@ -131,8 +128,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
131
128
|
// Resolve roles from the repository
|
|
132
129
|
let roles: string[] = payload.roles || [];
|
|
133
130
|
try {
|
|
134
|
-
|
|
135
|
-
roles = userRoles.map((r) => r.id);
|
|
131
|
+
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
136
132
|
} catch {
|
|
137
133
|
// Fall back to token roles if repository lookup fails
|
|
138
134
|
}
|
|
@@ -174,8 +170,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
174
170
|
|
|
175
171
|
let roles: string[] = payload.roles || [];
|
|
176
172
|
try {
|
|
177
|
-
|
|
178
|
-
roles = userRoles.map((r) => r.id);
|
|
173
|
+
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
179
174
|
} catch {
|
|
180
175
|
// Fall back to token roles if repository lookup fails
|
|
181
176
|
}
|
|
@@ -194,7 +189,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
194
189
|
|
|
195
190
|
userManagement: createUserManagementFromRepo(authRepository, resolvedOps, authHooks),
|
|
196
191
|
|
|
197
|
-
|
|
192
|
+
|
|
198
193
|
|
|
199
194
|
createAuthRoutes(): Hono<HonoEnv> | undefined {
|
|
200
195
|
return createAuthRoutes({
|
|
@@ -327,9 +322,8 @@ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: Resolve
|
|
|
327
322
|
}
|
|
328
323
|
},
|
|
329
324
|
|
|
330
|
-
async getUserRoles(userId: string): Promise<
|
|
331
|
-
|
|
332
|
-
return roles.map(toAuthRoleData);
|
|
325
|
+
async getUserRoles(userId: string): Promise<string[]> {
|
|
326
|
+
return repo.getUserRoleIds(userId);
|
|
333
327
|
},
|
|
334
328
|
|
|
335
329
|
async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
|
|
@@ -338,40 +332,6 @@ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: Resolve
|
|
|
338
332
|
};
|
|
339
333
|
}
|
|
340
334
|
|
|
341
|
-
function createRoleManagementFromRepo(repo: AuthRepository): RoleManagementAdapter {
|
|
342
|
-
return {
|
|
343
|
-
async listRoles(): Promise<AuthRoleData[]> {
|
|
344
|
-
const roles = await repo.listRoles();
|
|
345
|
-
return roles.map(toAuthRoleData);
|
|
346
|
-
},
|
|
347
|
-
|
|
348
|
-
async getRoleById(id: string): Promise<AuthRoleData | null> {
|
|
349
|
-
const role = await repo.getRoleById(id);
|
|
350
|
-
return role ? toAuthRoleData(role) : null;
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
async createRole(data: AuthCreateRoleData): Promise<AuthRoleData> {
|
|
354
|
-
const role = await repo.createRole({
|
|
355
|
-
id: data.id,
|
|
356
|
-
name: data.name,
|
|
357
|
-
isAdmin: data.isAdmin,
|
|
358
|
-
defaultPermissions: data.defaultPermissions,
|
|
359
|
-
collectionPermissions: data.collectionPermissions,
|
|
360
|
-
});
|
|
361
|
-
return toAuthRoleData(role);
|
|
362
|
-
},
|
|
363
|
-
|
|
364
|
-
async updateRole(id: string, data: Partial<AuthRoleData>): Promise<AuthRoleData | null> {
|
|
365
|
-
const role = await repo.updateRole(id, data);
|
|
366
|
-
return role ? toAuthRoleData(role) : null;
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
async deleteRole(id: string): Promise<void> {
|
|
370
|
-
await repo.deleteRole(id);
|
|
371
|
-
},
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
335
|
function toAuthUserData(user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; metadata?: Record<string, unknown>; createdAt?: Date; updatedAt?: Date }): AuthUserData {
|
|
376
336
|
return {
|
|
377
337
|
id: user.id,
|
|
@@ -384,13 +344,3 @@ function toAuthUserData(user: { id: string; email: string; displayName?: string
|
|
|
384
344
|
updatedAt: user.updatedAt,
|
|
385
345
|
};
|
|
386
346
|
}
|
|
387
|
-
|
|
388
|
-
function toAuthRoleData(role: { id: string; name: string; isAdmin: boolean; defaultPermissions?: unknown; collectionPermissions?: unknown }): AuthRoleData {
|
|
389
|
-
return {
|
|
390
|
-
id: role.id,
|
|
391
|
-
name: role.name,
|
|
392
|
-
isAdmin: role.isAdmin,
|
|
393
|
-
defaultPermissions: role.defaultPermissions as AuthRoleData["defaultPermissions"],
|
|
394
|
-
collectionPermissions: role.collectionPermissions as AuthRoleData["collectionPermissions"],
|
|
395
|
-
};
|
|
396
|
-
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { createTransport, Transporter } from "nodemailer";
|
|
2
2
|
import { EmailService, EmailSendOptions, EmailConfig } from "./types";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Safely parse a hostname from a URL string
|
|
6
|
+
*/
|
|
7
|
+
function getHostname(urlStr: string): string | undefined {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(urlStr.includes("://") ? urlStr : `https://${urlStr}`);
|
|
10
|
+
return url.hostname;
|
|
11
|
+
} catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
/**
|
|
5
17
|
* SMTP Email Service implementation using Nodemailer
|
|
6
18
|
*/
|
|
@@ -12,7 +24,26 @@ export class SMTPEmailService implements EmailService {
|
|
|
12
24
|
this.config = config;
|
|
13
25
|
|
|
14
26
|
if (config.smtp) {
|
|
27
|
+
let smtpName = config.smtp.name;
|
|
28
|
+
if (!smtpName) {
|
|
29
|
+
const urlsToTry = [
|
|
30
|
+
process.env.FRONTEND_URL,
|
|
31
|
+
config.resetPasswordUrl,
|
|
32
|
+
config.verifyEmailUrl
|
|
33
|
+
];
|
|
34
|
+
for (const urlStr of urlsToTry) {
|
|
35
|
+
if (urlStr) {
|
|
36
|
+
const hostname = getHostname(urlStr);
|
|
37
|
+
if (hostname) {
|
|
38
|
+
smtpName = hostname;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
this.transporter = createTransport({
|
|
46
|
+
name: smtpName,
|
|
16
47
|
host: config.smtp.host,
|
|
17
48
|
port: config.smtp.port,
|
|
18
49
|
secure: config.smtp.secure ?? (config.smtp.port === 465),
|
package/src/email/types.ts
CHANGED
package/src/init.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AuthAdapter,
|
|
3
|
+
BackendBootstrapper,
|
|
4
|
+
BackendHooks,
|
|
5
|
+
BootstrappedAuth,
|
|
6
|
+
DatabaseAdapter,
|
|
7
|
+
DataDriver,
|
|
8
|
+
EntityCollection,
|
|
9
|
+
HealthCheckResult,
|
|
10
|
+
InitializedDriver,
|
|
11
|
+
isPostgresCollection,
|
|
12
|
+
isSQLAdmin,
|
|
13
|
+
RealtimeProvider,
|
|
14
|
+
SecurityRule
|
|
15
|
+
} from "@rebasepro/types";
|
|
2
16
|
import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
|
|
3
17
|
import { loadCollectionsFromDirectory } from "./collections/loader";
|
|
4
|
-
import {
|
|
18
|
+
import { DEFAULT_DRIVER_ID, DefaultDriverRegistry, DriverRegistry } from "./services/driver-registry";
|
|
5
19
|
import { Server } from "http";
|
|
6
20
|
|
|
7
21
|
import { RestApiGenerator } from "./api/rest/api-generator";
|
|
@@ -16,21 +30,46 @@ import { HonoEnv } from "./api/types";
|
|
|
16
30
|
import { configureLogLevel } from "./utils/logging";
|
|
17
31
|
import { logger } from "./utils/logger";
|
|
18
32
|
import { requestLogger } from "./utils/request-logger";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
33
|
+
import { configureJwt, requireAdmin, requireAuth } from "./auth";
|
|
34
|
+
import {
|
|
35
|
+
BackendStorageConfig,
|
|
36
|
+
createStorageController,
|
|
37
|
+
createStorageRoutes,
|
|
38
|
+
DEFAULT_STORAGE_ID,
|
|
39
|
+
DefaultStorageRegistry,
|
|
40
|
+
StorageController,
|
|
41
|
+
StorageRegistry
|
|
42
|
+
} from "./storage";
|
|
43
|
+
import type { ApiKeyStore } from "./auth/api-keys/api-key-store";
|
|
21
44
|
import { createApiKeyStore } from "./auth/api-keys/api-key-store";
|
|
22
45
|
import { createApiKeyRoutes } from "./auth/api-keys/api-key-routes";
|
|
23
|
-
import type { ApiKeyStore } from "./auth/api-keys/api-key-store";
|
|
24
46
|
import { createApiKeyRateLimiter } from "./auth/rate-limiter";
|
|
25
47
|
import { createRebaseClient } from "@rebasepro/client";
|
|
48
|
+
|
|
26
49
|
import { createHistoryRoutes } from "./history";
|
|
27
|
-
import { EmailConfig, createEmailService } from "./email";
|
|
28
50
|
import type { EmailService } from "./email";
|
|
51
|
+
import { createEmailService, EmailConfig } from "./email";
|
|
29
52
|
import type { OAuthProvider } from "./auth/interfaces";
|
|
30
53
|
import type { AuthHooks } from "./auth/auth-hooks";
|
|
31
54
|
import { _initRebase } from "./singleton";
|
|
32
55
|
|
|
33
56
|
export interface RebaseAuthConfig {
|
|
57
|
+
/**
|
|
58
|
+
* The collection that represents auth users.
|
|
59
|
+
*
|
|
60
|
+
* When provided, this collection's underlying database table is used
|
|
61
|
+
* for all auth operations (login, registration, password reset, etc.).
|
|
62
|
+
*
|
|
63
|
+
* Import the built-in default:
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { defaultUsersCollection } from "@rebasepro/common";
|
|
66
|
+
* auth: { collection: defaultUsersCollection, jwtSecret: "..." }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Or pass your own collection with the required auth fields
|
|
70
|
+
* (email, passwordHash, displayName, etc.).
|
|
71
|
+
*/
|
|
72
|
+
collection?: EntityCollection;
|
|
34
73
|
jwtSecret?: string;
|
|
35
74
|
accessExpiresIn?: string;
|
|
36
75
|
refreshExpiresIn?: string;
|
|
@@ -83,6 +122,7 @@ export interface RebaseAuthConfig {
|
|
|
83
122
|
* ```
|
|
84
123
|
*/
|
|
85
124
|
hooks?: AuthHooks;
|
|
125
|
+
|
|
86
126
|
[key: string]: unknown;
|
|
87
127
|
}
|
|
88
128
|
|
|
@@ -135,6 +175,20 @@ export interface RebaseBackendConfig {
|
|
|
135
175
|
*/
|
|
136
176
|
storage?: BackendStorageConfig | StorageController | Record<string, BackendStorageConfig | StorageController>;
|
|
137
177
|
history?: unknown;
|
|
178
|
+
/**
|
|
179
|
+
* Default security rules applied to any collection that does not define
|
|
180
|
+
* its own `securityRules`. Opt-in — if not set, collections without
|
|
181
|
+
* explicit rules remain unrestricted (beyond `requireAuth`).
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* defaultSecurityRules: [
|
|
186
|
+
* { operation: "select", access: "public" },
|
|
187
|
+
* { operations: ["insert", "update", "delete"], roles: ["admin"] }
|
|
188
|
+
* ]
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
defaultSecurityRules?: SecurityRule[];
|
|
138
192
|
enableSwagger?: boolean;
|
|
139
193
|
functionsDir?: string;
|
|
140
194
|
cronsDir?: string;
|
|
@@ -218,11 +272,13 @@ export interface RebaseBackendInstance {
|
|
|
218
272
|
storageController?: StorageController;
|
|
219
273
|
collectionRegistry: BackendCollectionRegistry;
|
|
220
274
|
cronScheduler?: import("./cron").CronScheduler;
|
|
275
|
+
|
|
221
276
|
/**
|
|
222
277
|
* Deep health check that verifies database connectivity.
|
|
223
278
|
* Returns latency and component status.
|
|
224
279
|
*/
|
|
225
280
|
healthCheck(): Promise<HealthCheckResult>;
|
|
281
|
+
|
|
226
282
|
/**
|
|
227
283
|
* Graceful shutdown helper for the BaaS instance.
|
|
228
284
|
* Stops the cron scheduler and closes the HTTP server, allowing
|
|
@@ -288,8 +344,20 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
288
344
|
let activeCollections = config.collections || [];
|
|
289
345
|
if (config.collectionsDir && activeCollections.length === 0) {
|
|
290
346
|
activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
|
|
291
|
-
logger.info("Auto-discovered collections", {
|
|
292
|
-
|
|
347
|
+
logger.info("Auto-discovered collections", {
|
|
348
|
+
count: activeCollections.length,
|
|
349
|
+
dir: config.collectionsDir
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Apply default security rules to collections that don't define their own
|
|
354
|
+
if (config.defaultSecurityRules?.length) {
|
|
355
|
+
for (const collection of activeCollections) {
|
|
356
|
+
if (isPostgresCollection(collection) && (!collection.securityRules || collection.securityRules.length === 0)) {
|
|
357
|
+
collection.securityRules = config.defaultSecurityRules;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
logger.info("Default security rules applied to collections without explicit rules");
|
|
293
361
|
}
|
|
294
362
|
|
|
295
363
|
const realtimeServices: Record<string, RealtimeProvider> = {};
|
|
@@ -333,8 +401,10 @@ dir: config.collectionsDir });
|
|
|
333
401
|
defaultDriverId = b.id || bootstrapper.type;
|
|
334
402
|
}
|
|
335
403
|
|
|
336
|
-
const driverResult = await bootstrapper.initializeDriver({
|
|
337
|
-
|
|
404
|
+
const driverResult = await bootstrapper.initializeDriver({
|
|
405
|
+
collections: activeCollections,
|
|
406
|
+
collectionRegistry
|
|
407
|
+
});
|
|
338
408
|
delegates[b.id || bootstrapper.type] = driverResult.driver;
|
|
339
409
|
|
|
340
410
|
if ((b.id || bootstrapper.type) === defaultDriverId || !defaultDriverResult) {
|
|
@@ -354,7 +424,9 @@ collectionRegistry });
|
|
|
354
424
|
if (!defaultDriver || !defaultDriverResult) {
|
|
355
425
|
throw new Error("Default driver not initialized by bootstrappers");
|
|
356
426
|
}
|
|
357
|
-
const defaultBootstrapper = bootstrappers.find(b => (b as BackendBootstrapper & {
|
|
427
|
+
const defaultBootstrapper = bootstrappers.find(b => (b as BackendBootstrapper & {
|
|
428
|
+
id?: string
|
|
429
|
+
}).id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
|
|
358
430
|
const defaultRealtimeService = defaultDriverResult.realtimeProvider;
|
|
359
431
|
|
|
360
432
|
// 2. Initialize Auth & History via the default driver's bootstrapper
|
|
@@ -378,7 +450,6 @@ collectionRegistry });
|
|
|
378
450
|
// (the return type still exposes `auth?: BootstrappedAuth`)
|
|
379
451
|
authConfigResult = {
|
|
380
452
|
userService: authAdapter.userManagement ?? {},
|
|
381
|
-
roleService: authAdapter.roleManagement ?? {},
|
|
382
453
|
};
|
|
383
454
|
} else {
|
|
384
455
|
// ── RebaseAuthConfig — wrap in built-in adapter ──
|
|
@@ -520,7 +591,10 @@ collectionRegistry });
|
|
|
520
591
|
|
|
521
592
|
if (safeAuthConfig.linkedin?.clientId && safeAuthConfig.linkedin?.clientSecret) {
|
|
522
593
|
const { createLinkedinProvider } = await import("./auth");
|
|
523
|
-
oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as {
|
|
594
|
+
oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as {
|
|
595
|
+
clientId: string;
|
|
596
|
+
clientSecret: string
|
|
597
|
+
}));
|
|
524
598
|
}
|
|
525
599
|
|
|
526
600
|
if (safeAuthConfig.github?.clientId && safeAuthConfig.github?.clientSecret) {
|
|
@@ -790,6 +864,29 @@ collectionRegistry });
|
|
|
790
864
|
if (emailService) {
|
|
791
865
|
Object.assign(serverClient, { email: emailService });
|
|
792
866
|
logger.info("Email service attached to singleton", { configured: emailService.isConfigured() });
|
|
867
|
+
|
|
868
|
+
if (emailService.isConfigured() && typeof emailService.verifyConnection === "function") {
|
|
869
|
+
emailService.verifyConnection().then((success) => {
|
|
870
|
+
if (!success) {
|
|
871
|
+
logger.warn("Warning: SMTP connection verification failed. Email delivery may fail.");
|
|
872
|
+
} else {
|
|
873
|
+
logger.info("SMTP connection verified successfully.");
|
|
874
|
+
}
|
|
875
|
+
}).catch((err) => {
|
|
876
|
+
logger.warn("Warning: SMTP connection verification failed. Email delivery may fail.", { error: err });
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Attach raw SQL capability when the driver supports it (Postgres, MySQL).
|
|
882
|
+
// Document databases (MongoDB, Firestore) won't have this.
|
|
883
|
+
const driverAdmin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
|
|
884
|
+
if (isSQLAdmin(driverAdmin)) {
|
|
885
|
+
Object.assign(serverClient, {
|
|
886
|
+
sql: (query: string, options?: { database?: string; role?: string }) =>
|
|
887
|
+
driverAdmin.executeSql(query, options)
|
|
888
|
+
});
|
|
889
|
+
logger.info("SQL capability attached to singleton");
|
|
793
890
|
}
|
|
794
891
|
|
|
795
892
|
_initRebase(serverClient);
|
|
@@ -842,8 +939,10 @@ collectionRegistry });
|
|
|
842
939
|
const fnRoutes = createFunctionRoutes(loadedFunctions);
|
|
843
940
|
functionsRouter.route("/", fnRoutes);
|
|
844
941
|
config.app.route(`${basePath}/functions`, functionsRouter);
|
|
845
|
-
logger.info("Mounted custom functions", {
|
|
846
|
-
|
|
942
|
+
logger.info("Mounted custom functions", {
|
|
943
|
+
count: loadedFunctions.length,
|
|
944
|
+
path: `${basePath}/functions`
|
|
945
|
+
});
|
|
847
946
|
}
|
|
848
947
|
}
|
|
849
948
|
|
|
@@ -892,13 +991,19 @@ path: `${basePath}/functions` });
|
|
|
892
991
|
// Start the scheduler
|
|
893
992
|
cronScheduler.start();
|
|
894
993
|
|
|
895
|
-
logger.info("Mounted cron jobs", {
|
|
896
|
-
|
|
994
|
+
logger.info("Mounted cron jobs", {
|
|
995
|
+
count: loadedCronJobs.length,
|
|
996
|
+
path: `${basePath}/cron`
|
|
997
|
+
});
|
|
897
998
|
}
|
|
898
999
|
}
|
|
899
1000
|
|
|
900
|
-
if ((defaultBootstrapper as BackendBootstrapper & {
|
|
901
|
-
|
|
1001
|
+
if ((defaultBootstrapper as BackendBootstrapper & {
|
|
1002
|
+
initializeWebsockets?: (...args: unknown[]) => unknown
|
|
1003
|
+
}).initializeWebsockets) {
|
|
1004
|
+
await (defaultBootstrapper as BackendBootstrapper & {
|
|
1005
|
+
initializeWebsockets: (...args: unknown[]) => unknown
|
|
1006
|
+
}).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
902
1007
|
}
|
|
903
1008
|
|
|
904
1009
|
logger.info("Rebase Backend Initialized");
|
|
@@ -913,12 +1018,16 @@ path: `${basePath}/cron` });
|
|
|
913
1018
|
await admin.executeSql("SELECT 1");
|
|
914
1019
|
} else {
|
|
915
1020
|
// Fallback: try a lightweight fetch to confirm driver is responsive
|
|
916
|
-
await defaultDriver.fetchCollection({
|
|
917
|
-
|
|
1021
|
+
await defaultDriver.fetchCollection({
|
|
1022
|
+
path: "__health_check_nonexistent__",
|
|
1023
|
+
limit: 1
|
|
1024
|
+
});
|
|
918
1025
|
}
|
|
919
1026
|
const latencyMs = Math.round(performance.now() - start);
|
|
920
|
-
return {
|
|
921
|
-
|
|
1027
|
+
return {
|
|
1028
|
+
healthy: true,
|
|
1029
|
+
latencyMs
|
|
1030
|
+
};
|
|
922
1031
|
} catch (error: unknown) {
|
|
923
1032
|
const latencyMs = Math.round(performance.now() - start);
|
|
924
1033
|
logger.error("Health check failed", {
|
|
@@ -952,7 +1061,10 @@ latencyMs };
|
|
|
952
1061
|
// timer callbacks don't fire against a closed pool.
|
|
953
1062
|
for (const [key, rt] of Object.entries(realtimeServices)) {
|
|
954
1063
|
try {
|
|
955
|
-
const rtWithLifecycle = rt as RealtimeProvider & {
|
|
1064
|
+
const rtWithLifecycle = rt as RealtimeProvider & {
|
|
1065
|
+
destroy?: () => Promise<void>;
|
|
1066
|
+
stopListening?: () => Promise<void>
|
|
1067
|
+
};
|
|
956
1068
|
if (typeof rtWithLifecycle.destroy === "function") {
|
|
957
1069
|
await rtWithLifecycle.destroy();
|
|
958
1070
|
logger.info(`Realtime service "${key}" destroyed`);
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* in-memory cache to avoid redundant processing.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
10
|
let sharpFactory: ((input: Buffer | Uint8Array) => any) | undefined;
|
|
11
11
|
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
13
|
async function getSharp(): Promise<(input: Buffer | Uint8Array) => any> {
|
|
13
14
|
if (!sharpFactory) {
|
|
14
15
|
try {
|