@rebasepro/server-core 0.0.1-canary.4d4fb3e
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/LICENSE +6 -0
- package/README.md +40 -0
- package/build-errors.txt +52 -0
- package/coverage/clover.xml +3739 -0
- package/coverage/coverage-final.json +31 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
- package/coverage/lcov-report/src/api/errors.ts.html +472 -0
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
- package/coverage/lcov-report/src/api/graphql/index.html +116 -0
- package/coverage/lcov-report/src/api/index.html +176 -0
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
- package/coverage/lcov-report/src/api/rest/index.html +131 -0
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
- package/coverage/lcov-report/src/api/server.ts.html +823 -0
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
- package/coverage/lcov-report/src/auth/index.html +176 -0
- package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
- package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
- package/coverage/lcov-report/src/auth/password.ts.html +310 -0
- package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
- package/coverage/lcov-report/src/collections/index.html +116 -0
- package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
- package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
- package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
- package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
- package/coverage/lcov-report/src/db/index.html +146 -0
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
- package/coverage/lcov-report/src/db/services/index.html +176 -0
- package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
- package/coverage/lcov-report/src/services/index.html +131 -0
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
- package/coverage/lcov-report/src/storage/index.html +161 -0
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
- package/coverage/lcov-report/src/storage/types.ts.html +451 -0
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov.info +7179 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +36 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-BeMqpmfQ.js +239 -0
- package/dist/index-BeMqpmfQ.js.map +1 -0
- package/dist/index-bl4J3lNb.js +55823 -0
- package/dist/index-bl4J3lNb.js.map +1 -0
- package/dist/index.es.js +58 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +56062 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
- package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/server-core/src/api/errors.d.ts +35 -0
- package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
- package/dist/server-core/src/api/graphql/index.d.ts +1 -0
- package/dist/server-core/src/api/index.d.ts +9 -0
- package/dist/server-core/src/api/openapi-generator.d.ts +2 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
- package/dist/server-core/src/api/rest/index.d.ts +1 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
- package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
- package/dist/server-core/src/api/server.d.ts +40 -0
- package/dist/server-core/src/api/types.d.ts +90 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +7 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
- package/dist/server-core/src/auth/index.d.ts +12 -0
- package/dist/server-core/src/auth/interfaces.d.ts +270 -0
- package/dist/server-core/src/auth/jwt.d.ts +42 -0
- package/dist/server-core/src/auth/middleware.d.ts +56 -0
- package/dist/server-core/src/auth/password.d.ts +22 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +17 -0
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/server-core/src/collections/loader.d.ts +5 -0
- package/dist/server-core/src/db/interfaces.d.ts +18 -0
- package/dist/server-core/src/email/index.d.ts +6 -0
- package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
- package/dist/server-core/src/email/templates.d.ts +33 -0
- package/dist/server-core/src/email/types.d.ts +110 -0
- package/dist/server-core/src/functions/function-loader.d.ts +17 -0
- package/dist/server-core/src/functions/function-routes.d.ts +10 -0
- package/dist/server-core/src/functions/index.d.ts +3 -0
- package/dist/server-core/src/history/history-routes.d.ts +23 -0
- package/dist/server-core/src/history/index.d.ts +1 -0
- package/dist/server-core/src/index.d.ts +24 -0
- package/dist/server-core/src/init.d.ts +49 -0
- package/dist/server-core/src/serve-spa.d.ts +30 -0
- package/dist/server-core/src/services/driver-registry.d.ts +78 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
- package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
- package/dist/server-core/src/storage/index.d.ts +18 -0
- package/dist/server-core/src/storage/routes.d.ts +38 -0
- package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
- package/dist/server-core/src/storage/types.d.ts +91 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/sql.d.ts +27 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/index.d.ts +17 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/history_diff.log +385 -0
- package/jest.config.cjs +16 -0
- package/package.json +86 -0
- package/scratch.ts +8 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
- package/src/api/errors.ts +155 -0
- package/src/api/graphql/graphql-schema-generator.ts +334 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +160 -0
- package/src/api/rest/api-generator.ts +466 -0
- package/src/api/rest/index.ts +2 -0
- package/src/api/rest/query-parser.ts +155 -0
- package/src/api/schema-editor-routes.ts +39 -0
- package/src/api/server.ts +245 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +488 -0
- package/src/auth/google-oauth.ts +60 -0
- package/src/auth/index.ts +21 -0
- package/src/auth/interfaces.ts +316 -0
- package/src/auth/jwt.ts +164 -0
- package/src/auth/middleware.ts +235 -0
- package/src/auth/password.ts +75 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +730 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +17 -0
- package/src/email/smtp-email-service.ts +88 -0
- package/src/email/templates.ts +301 -0
- package/src/email/types.ts +112 -0
- package/src/functions/function-loader.ts +91 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +128 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +56 -0
- package/src/init.ts +309 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/storage/LocalStorageController.ts +368 -0
- package/src/storage/S3StorageController.ts +295 -0
- package/src/storage/index.ts +32 -0
- package/src/storage/routes.ts +247 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +122 -0
- package/src/types/index.ts +27 -0
- package/src/utils/logging.ts +35 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +591 -0
- package/test/api-generator.test.ts +458 -0
- package/test/ast-schema-editor.test.ts +61 -0
- package/test/auth-middleware-hono.test.ts +321 -0
- package/test/auth-routes.test.ts +868 -0
- package/test/driver-registry.test.ts +280 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +150 -0
- package/test/jwt-security.test.ts +173 -0
- package/test/jwt.test.ts +311 -0
- package/test/middleware.test.ts +295 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +258 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/storage-local.test.ts +278 -0
- package/test/storage-registry.test.ts +280 -0
- package/test/storage-routes.test.ts +218 -0
- package/test/storage-s3.test.ts +301 -0
- package/test-ast.ts +28 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +78 -0
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +46 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// PostgresBootstrapper was moved to @rebasepro/server-postgresql
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CollectionRegistry } from "@rebasepro/common";
|
|
2
|
+
import { CollectionRegistryInterface } from "../db/interfaces";
|
|
3
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Backend-agnostic collection registry.
|
|
7
|
+
* Satisfies CollectionRegistryInterface through inheritance from CollectionRegistry.
|
|
8
|
+
*/
|
|
9
|
+
export class BackendCollectionRegistry extends CollectionRegistry implements CollectionRegistryInterface {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the available relation keys for a given collection path.
|
|
13
|
+
* Maps from the collection's relation property names to the relation names.
|
|
14
|
+
*/
|
|
15
|
+
getRelationKeysForCollection(collectionPath: string): string[] {
|
|
16
|
+
const collection = this.getCollectionByPath(collectionPath) as (EntityCollection & { relations?: { relationName?: string, localKey?: string }[] }) | undefined;
|
|
17
|
+
if (!collection?.relations) return [];
|
|
18
|
+
return collection.relations.map(r => (r.relationName || r.localKey || "") as string).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Asynchronously load collection files from a directory for backend initialization
|
|
8
|
+
*/
|
|
9
|
+
export async function loadCollectionsFromDirectory(directory: string): Promise<EntityCollection[]> {
|
|
10
|
+
const collections: EntityCollection[] = [];
|
|
11
|
+
try {
|
|
12
|
+
if (!fs.existsSync(directory)) {
|
|
13
|
+
console.warn(`[loadCollectionsFromDirectory] Collections directory not found: ${directory}`);
|
|
14
|
+
return collections;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const files = fs.readdirSync(directory);
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
// Only load .ts and .js files, ignore test files and declaration files
|
|
20
|
+
if ((file.endsWith('.ts') || file.endsWith('.js')) &&
|
|
21
|
+
!file.includes('.test.') &&
|
|
22
|
+
!file.endsWith('.d.ts') &&
|
|
23
|
+
file !== 'index.ts' && file !== 'index.js') {
|
|
24
|
+
|
|
25
|
+
const filePath = path.join(directory, file);
|
|
26
|
+
try {
|
|
27
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
28
|
+
|
|
29
|
+
// Use new Function to compile dynamic import natively and bypass tsc converting import() to require()
|
|
30
|
+
const dynamicImport = new Function('url', 'return import(url)');
|
|
31
|
+
const module = await dynamicImport(fileUrl);
|
|
32
|
+
|
|
33
|
+
// Expect the collection to be the default export
|
|
34
|
+
if (module && module.default) {
|
|
35
|
+
collections.push(module.default);
|
|
36
|
+
} else {
|
|
37
|
+
console.warn(`[loadCollectionsFromDirectory] File ${file} does not have a default export. Skipping.`);
|
|
38
|
+
}
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
console.error(`[loadCollectionsFromDirectory] Failed to load collection from ${file}: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`[loadCollectionsFromDirectory] Error reading collections directory: ${err}`);
|
|
47
|
+
}
|
|
48
|
+
return collections;
|
|
49
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Abstraction Interfaces
|
|
3
|
+
*
|
|
4
|
+
* These interfaces define the contracts that any database backend must implement
|
|
5
|
+
* to be used with Rebase. This allows for pluggable database backends like
|
|
6
|
+
* PostgreSQL, MongoDB, MySQL, etc.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Entity,
|
|
11
|
+
EntityCollection,
|
|
12
|
+
FilterValues,
|
|
13
|
+
WhereFilterOp,
|
|
14
|
+
DatabaseConnection,
|
|
15
|
+
QueryFilter,
|
|
16
|
+
FetchCollectionOptions,
|
|
17
|
+
SearchOptions,
|
|
18
|
+
CountOptions,
|
|
19
|
+
ConditionBuilder,
|
|
20
|
+
ConditionBuilderStatic,
|
|
21
|
+
EntityRepository,
|
|
22
|
+
CollectionSubscriptionConfig,
|
|
23
|
+
EntitySubscriptionConfig,
|
|
24
|
+
RealtimeProvider,
|
|
25
|
+
CollectionRegistryInterface,
|
|
26
|
+
DataTransformer,
|
|
27
|
+
BackendConfig,
|
|
28
|
+
BackendInstance,
|
|
29
|
+
BackendFactory
|
|
30
|
+
} from "@rebasepro/types";
|
|
31
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
32
|
+
import { PgTransaction } from "drizzle-orm/pg-core";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Type representing either a direct database connection or a transaction.
|
|
36
|
+
* Used to allow services to operate within a transaction context.
|
|
37
|
+
* Note: `any` is intentional here — it represents a Drizzle client with
|
|
38
|
+
* a dynamic schema, enabling `db.query[tableName]` access without casts.
|
|
39
|
+
*/
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
export type DrizzleClient = NodePgDatabase<Record<string, unknown>> | PgTransaction<any, any, any>;
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
DatabaseConnection,
|
|
45
|
+
QueryFilter,
|
|
46
|
+
FetchCollectionOptions,
|
|
47
|
+
SearchOptions,
|
|
48
|
+
CountOptions,
|
|
49
|
+
ConditionBuilder,
|
|
50
|
+
ConditionBuilderStatic,
|
|
51
|
+
EntityRepository,
|
|
52
|
+
CollectionSubscriptionConfig,
|
|
53
|
+
EntitySubscriptionConfig,
|
|
54
|
+
RealtimeProvider,
|
|
55
|
+
CollectionRegistryInterface,
|
|
56
|
+
DataTransformer,
|
|
57
|
+
BackendConfig,
|
|
58
|
+
BackendInstance,
|
|
59
|
+
BackendFactory
|
|
60
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
EmailService,
|
|
7
|
+
EmailSendOptions,
|
|
8
|
+
SMTPConfig,
|
|
9
|
+
EmailConfig,
|
|
10
|
+
PasswordResetTemplateFunction,
|
|
11
|
+
EmailVerificationTemplateFunction,
|
|
12
|
+
UserInvitationTemplateFunction
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
export { SMTPEmailService, createEmailService } from "./smtp-email-service";
|
|
16
|
+
|
|
17
|
+
export { getPasswordResetTemplate, getEmailVerificationTemplate, getUserInvitationTemplate } from "./templates";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createTransport, Transporter } from "nodemailer";
|
|
2
|
+
import { EmailService, EmailSendOptions, EmailConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SMTP Email Service implementation using Nodemailer
|
|
6
|
+
*/
|
|
7
|
+
export class SMTPEmailService implements EmailService {
|
|
8
|
+
private transporter: Transporter | null = null;
|
|
9
|
+
private config: EmailConfig;
|
|
10
|
+
|
|
11
|
+
constructor(config: EmailConfig) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
|
|
14
|
+
if (config.smtp) {
|
|
15
|
+
this.transporter = createTransport({
|
|
16
|
+
host: config.smtp.host,
|
|
17
|
+
port: config.smtp.port,
|
|
18
|
+
secure: config.smtp.secure ?? (config.smtp.port === 465),
|
|
19
|
+
auth: config.smtp.auth ? {
|
|
20
|
+
user: config.smtp.auth.user,
|
|
21
|
+
pass: config.smtp.auth.pass
|
|
22
|
+
} : undefined
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if the email service is properly configured
|
|
29
|
+
*/
|
|
30
|
+
isConfigured(): boolean {
|
|
31
|
+
return !!(this.transporter || this.config.sendEmail);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Send an email using SMTP or custom send function
|
|
36
|
+
*/
|
|
37
|
+
async send(options: EmailSendOptions): Promise<void> {
|
|
38
|
+
// Use custom send function if provided
|
|
39
|
+
if (this.config.sendEmail) {
|
|
40
|
+
await this.config.sendEmail(options);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Use SMTP transporter
|
|
45
|
+
if (!this.transporter) {
|
|
46
|
+
throw new Error("Email service not configured. Provide SMTP config or sendEmail function.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await this.transporter.sendMail({
|
|
51
|
+
from: this.config.from,
|
|
52
|
+
to: options.to,
|
|
53
|
+
subject: options.subject,
|
|
54
|
+
html: options.html,
|
|
55
|
+
text: options.text
|
|
56
|
+
});
|
|
57
|
+
} catch (error: unknown) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
console.error("Failed to send email:", message);
|
|
60
|
+
throw new Error(`Failed to send email: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Verify SMTP connection (useful for startup checks)
|
|
66
|
+
*/
|
|
67
|
+
async verifyConnection(): Promise<boolean> {
|
|
68
|
+
if (!this.transporter) {
|
|
69
|
+
return !!this.config.sendEmail;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await this.transporter.verify();
|
|
74
|
+
return true;
|
|
75
|
+
} catch (error: unknown) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
console.error("SMTP connection verification failed:", message);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create an email service from configuration
|
|
85
|
+
*/
|
|
86
|
+
export function createEmailService(config: EmailConfig): EmailService {
|
|
87
|
+
return new SMTPEmailService(config);
|
|
88
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default email templates for authentication emails
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface TemplateUser {
|
|
6
|
+
email: string;
|
|
7
|
+
displayName?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a greeting name for the user
|
|
12
|
+
*/
|
|
13
|
+
function getGreeting(user: TemplateUser): string {
|
|
14
|
+
return user.displayName || user.email.split("@")[0];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Common email styles
|
|
19
|
+
*/
|
|
20
|
+
const styles = {
|
|
21
|
+
container: `
|
|
22
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
23
|
+
max-width: 600px;
|
|
24
|
+
margin: 0 auto;
|
|
25
|
+
padding: 40px 20px;
|
|
26
|
+
background-color: #f8fafc;
|
|
27
|
+
`,
|
|
28
|
+
card: `
|
|
29
|
+
background-color: #ffffff;
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
padding: 40px;
|
|
32
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
33
|
+
`,
|
|
34
|
+
heading: `
|
|
35
|
+
color: #1e293b;
|
|
36
|
+
font-size: 24px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
margin: 0 0 20px 0;
|
|
39
|
+
`,
|
|
40
|
+
paragraph: `
|
|
41
|
+
color: #475569;
|
|
42
|
+
font-size: 16px;
|
|
43
|
+
line-height: 1.6;
|
|
44
|
+
margin: 0 0 20px 0;
|
|
45
|
+
`,
|
|
46
|
+
button: `
|
|
47
|
+
display: inline-block;
|
|
48
|
+
background-color: #3b82f6;
|
|
49
|
+
color: #ffffff;
|
|
50
|
+
font-size: 16px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
text-decoration: none;
|
|
53
|
+
padding: 14px 28px;
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
margin: 20px 0;
|
|
56
|
+
`,
|
|
57
|
+
footer: `
|
|
58
|
+
color: #94a3b8;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
margin-top: 30px;
|
|
61
|
+
padding-top: 20px;
|
|
62
|
+
border-top: 1px solid #e2e8f0;
|
|
63
|
+
`,
|
|
64
|
+
warning: `
|
|
65
|
+
color: #64748b;
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
background-color: #fef3c7;
|
|
68
|
+
padding: 12px 16px;
|
|
69
|
+
border-radius: 6px;
|
|
70
|
+
margin-top: 20px;
|
|
71
|
+
`
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Default password reset email template
|
|
76
|
+
*/
|
|
77
|
+
export function getPasswordResetTemplate(
|
|
78
|
+
resetUrl: string,
|
|
79
|
+
user: TemplateUser,
|
|
80
|
+
appName: string = "Rebase"
|
|
81
|
+
): { subject: string; html: string; text: string } {
|
|
82
|
+
const greeting = getGreeting(user);
|
|
83
|
+
|
|
84
|
+
const subject = `Reset your ${appName} password`;
|
|
85
|
+
|
|
86
|
+
const html = `
|
|
87
|
+
<!DOCTYPE html>
|
|
88
|
+
<html>
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="utf-8">
|
|
91
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
92
|
+
<title>${subject}</title>
|
|
93
|
+
</head>
|
|
94
|
+
<body style="margin: 0; padding: 0; background-color: #f8fafc;">
|
|
95
|
+
<div style="${styles.container}">
|
|
96
|
+
<div style="${styles.card}">
|
|
97
|
+
<h1 style="${styles.heading}">Reset Your Password</h1>
|
|
98
|
+
|
|
99
|
+
<p style="${styles.paragraph}">
|
|
100
|
+
Hi ${greeting},
|
|
101
|
+
</p>
|
|
102
|
+
|
|
103
|
+
<p style="${styles.paragraph}">
|
|
104
|
+
We received a request to reset your password for your ${appName} account.
|
|
105
|
+
Click the button below to create a new password:
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<div style="text-align: center;">
|
|
109
|
+
<a href="${resetUrl}" style="${styles.button}">Reset Password</a>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<p style="${styles.paragraph}">
|
|
113
|
+
Or copy and paste this link into your browser:
|
|
114
|
+
</p>
|
|
115
|
+
<p style="color: #3b82f6; word-break: break-all; font-size: 14px;">
|
|
116
|
+
${resetUrl}
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<div style="${styles.warning}">
|
|
120
|
+
⏰ This link will expire in 1 hour for security reasons.
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style="${styles.footer}">
|
|
124
|
+
<p style="margin: 0;">
|
|
125
|
+
If you didn't request a password reset, you can safely ignore this email.
|
|
126
|
+
Your password will remain unchanged.
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`.trim();
|
|
134
|
+
|
|
135
|
+
const text = `
|
|
136
|
+
Reset Your Password
|
|
137
|
+
|
|
138
|
+
Hi ${greeting},
|
|
139
|
+
|
|
140
|
+
We received a request to reset your password for your ${appName} account.
|
|
141
|
+
|
|
142
|
+
Click this link to create a new password:
|
|
143
|
+
${resetUrl}
|
|
144
|
+
|
|
145
|
+
This link will expire in 1 hour for security reasons.
|
|
146
|
+
|
|
147
|
+
If you didn't request a password reset, you can safely ignore this email.
|
|
148
|
+
Your password will remain unchanged.
|
|
149
|
+
`.trim();
|
|
150
|
+
|
|
151
|
+
return { subject, html, text };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Default email verification template
|
|
156
|
+
*/
|
|
157
|
+
export function getEmailVerificationTemplate(
|
|
158
|
+
verifyUrl: string,
|
|
159
|
+
user: TemplateUser,
|
|
160
|
+
appName: string = "Rebase"
|
|
161
|
+
): { subject: string; html: string; text: string } {
|
|
162
|
+
const greeting = getGreeting(user);
|
|
163
|
+
|
|
164
|
+
const subject = `Verify your ${appName} email address`;
|
|
165
|
+
|
|
166
|
+
const html = `
|
|
167
|
+
<!DOCTYPE html>
|
|
168
|
+
<html>
|
|
169
|
+
<head>
|
|
170
|
+
<meta charset="utf-8">
|
|
171
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
172
|
+
<title>${subject}</title>
|
|
173
|
+
</head>
|
|
174
|
+
<body style="margin: 0; padding: 0; background-color: #f8fafc;">
|
|
175
|
+
<div style="${styles.container}">
|
|
176
|
+
<div style="${styles.card}">
|
|
177
|
+
<h1 style="${styles.heading}">Verify Your Email</h1>
|
|
178
|
+
|
|
179
|
+
<p style="${styles.paragraph}">
|
|
180
|
+
Hi ${greeting},
|
|
181
|
+
</p>
|
|
182
|
+
|
|
183
|
+
<p style="${styles.paragraph}">
|
|
184
|
+
Thanks for signing up for ${appName}! Please verify your email address
|
|
185
|
+
by clicking the button below:
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
<div style="text-align: center;">
|
|
189
|
+
<a href="${verifyUrl}" style="${styles.button}">Verify Email Address</a>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<p style="${styles.paragraph}">
|
|
193
|
+
Or copy and paste this link into your browser:
|
|
194
|
+
</p>
|
|
195
|
+
<p style="color: #3b82f6; word-break: break-all; font-size: 14px;">
|
|
196
|
+
${verifyUrl}
|
|
197
|
+
</p>
|
|
198
|
+
|
|
199
|
+
<div style="${styles.footer}">
|
|
200
|
+
<p style="margin: 0;">
|
|
201
|
+
If you didn't create an account with ${appName}, you can safely ignore this email.
|
|
202
|
+
</p>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</body>
|
|
207
|
+
</html>
|
|
208
|
+
`.trim();
|
|
209
|
+
|
|
210
|
+
const text = `
|
|
211
|
+
Verify Your Email
|
|
212
|
+
|
|
213
|
+
Hi ${greeting},
|
|
214
|
+
|
|
215
|
+
Thanks for signing up for ${appName}! Please verify your email address by clicking this link:
|
|
216
|
+
${verifyUrl}
|
|
217
|
+
|
|
218
|
+
If you didn't create an account with ${appName}, you can safely ignore this email.
|
|
219
|
+
`.trim();
|
|
220
|
+
|
|
221
|
+
return { subject, html, text };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Default user invitation email template
|
|
226
|
+
* Sent when an admin creates a new user account
|
|
227
|
+
*/
|
|
228
|
+
export function getUserInvitationTemplate(
|
|
229
|
+
setPasswordUrl: string,
|
|
230
|
+
user: TemplateUser,
|
|
231
|
+
appName: string = "Rebase"
|
|
232
|
+
): { subject: string; html: string; text: string } {
|
|
233
|
+
const greeting = getGreeting(user);
|
|
234
|
+
|
|
235
|
+
const subject = `You've been invited to ${appName}`;
|
|
236
|
+
|
|
237
|
+
const html = `
|
|
238
|
+
<!DOCTYPE html>
|
|
239
|
+
<html>
|
|
240
|
+
<head>
|
|
241
|
+
<meta charset="utf-8">
|
|
242
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
243
|
+
<title>${subject}</title>
|
|
244
|
+
</head>
|
|
245
|
+
<body style="margin: 0; padding: 0; background-color: #f8fafc;">
|
|
246
|
+
<div style="${styles.container}">
|
|
247
|
+
<div style="${styles.card}">
|
|
248
|
+
<h1 style="${styles.heading}">Welcome to ${appName}!</h1>
|
|
249
|
+
|
|
250
|
+
<p style="${styles.paragraph}">
|
|
251
|
+
Hi ${greeting},
|
|
252
|
+
</p>
|
|
253
|
+
|
|
254
|
+
<p style="${styles.paragraph}">
|
|
255
|
+
An account has been created for you on ${appName}.
|
|
256
|
+
Click the button below to set your password and get started:
|
|
257
|
+
</p>
|
|
258
|
+
|
|
259
|
+
<div style="text-align: center;">
|
|
260
|
+
<a href="${setPasswordUrl}" style="${styles.button}">Set Your Password</a>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<p style="${styles.paragraph}">
|
|
264
|
+
Or copy and paste this link into your browser:
|
|
265
|
+
</p>
|
|
266
|
+
<p style="color: #3b82f6; word-break: break-all; font-size: 14px;">
|
|
267
|
+
${setPasswordUrl}
|
|
268
|
+
</p>
|
|
269
|
+
|
|
270
|
+
<div style="${styles.warning}">
|
|
271
|
+
⏰ This link will expire in 1 hour for security reasons.
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div style="${styles.footer}">
|
|
275
|
+
<p style="margin: 0;">
|
|
276
|
+
If you weren't expecting this invitation, you can safely ignore this email.
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</body>
|
|
282
|
+
</html>
|
|
283
|
+
`.trim();
|
|
284
|
+
|
|
285
|
+
const text = `
|
|
286
|
+
Welcome to ${appName}!
|
|
287
|
+
|
|
288
|
+
Hi ${greeting},
|
|
289
|
+
|
|
290
|
+
An account has been created for you on ${appName}.
|
|
291
|
+
|
|
292
|
+
Click this link to set your password and get started:
|
|
293
|
+
${setPasswordUrl}
|
|
294
|
+
|
|
295
|
+
This link will expire in 1 hour for security reasons.
|
|
296
|
+
|
|
297
|
+
If you weren't expecting this invitation, you can safely ignore this email.
|
|
298
|
+
`.trim();
|
|
299
|
+
|
|
300
|
+
return { subject, html, text };
|
|
301
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email service types and interfaces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for sending an email
|
|
7
|
+
*/
|
|
8
|
+
export interface EmailSendOptions {
|
|
9
|
+
to: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
html: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Email service interface - abstraction for sending emails
|
|
17
|
+
*/
|
|
18
|
+
export interface EmailService {
|
|
19
|
+
/**
|
|
20
|
+
* Send an email
|
|
21
|
+
*/
|
|
22
|
+
send(options: EmailSendOptions): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if the email service is properly configured
|
|
26
|
+
*/
|
|
27
|
+
isConfigured(): boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SMTP server configuration
|
|
32
|
+
*/
|
|
33
|
+
export interface SMTPConfig {
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
secure?: boolean;
|
|
37
|
+
auth?: {
|
|
38
|
+
user: string;
|
|
39
|
+
pass: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Template function for password reset emails
|
|
45
|
+
*/
|
|
46
|
+
export type PasswordResetTemplateFunction = (
|
|
47
|
+
resetUrl: string,
|
|
48
|
+
user: { email: string; displayName?: string | null }
|
|
49
|
+
) => { subject: string; html: string; text?: string };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Template function for email verification emails
|
|
53
|
+
*/
|
|
54
|
+
export type EmailVerificationTemplateFunction = (
|
|
55
|
+
verifyUrl: string,
|
|
56
|
+
user: { email: string; displayName?: string | null }
|
|
57
|
+
) => { subject: string; html: string; text?: string };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Template function for user invitation emails
|
|
61
|
+
*/
|
|
62
|
+
export type UserInvitationTemplateFunction = (
|
|
63
|
+
setPasswordUrl: string,
|
|
64
|
+
user: { email: string; displayName?: string | null }
|
|
65
|
+
) => { subject: string; html: string; text?: string };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Complete email configuration
|
|
69
|
+
*/
|
|
70
|
+
export interface EmailConfig {
|
|
71
|
+
/**
|
|
72
|
+
* From address for all emails (e.g., "noreply@example.com" or "MyApp <noreply@example.com>")
|
|
73
|
+
*/
|
|
74
|
+
from: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SMTP configuration for sending emails via SMTP server
|
|
78
|
+
*/
|
|
79
|
+
smtp?: SMTPConfig;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Alternative: custom function to send emails
|
|
83
|
+
* Use this for custom email providers (AWS SES SDK, Resend, etc.)
|
|
84
|
+
*/
|
|
85
|
+
sendEmail?: (options: EmailSendOptions) => Promise<void>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Base URL for password reset links (e.g., "https://myapp.com")
|
|
89
|
+
* The reset link will be: {baseUrl}/reset-password?token={token}
|
|
90
|
+
*/
|
|
91
|
+
resetPasswordUrl?: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Base URL for email verification links (e.g., "https://myapp.com")
|
|
95
|
+
* The verification link will be: {baseUrl}/verify-email?token={token}
|
|
96
|
+
*/
|
|
97
|
+
verifyEmailUrl?: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Application name to use in email templates
|
|
101
|
+
*/
|
|
102
|
+
appName?: string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Custom email templates (optional - defaults are provided)
|
|
106
|
+
*/
|
|
107
|
+
templates?: {
|
|
108
|
+
passwordReset?: PasswordResetTemplateFunction;
|
|
109
|
+
emailVerification?: EmailVerificationTemplateFunction;
|
|
110
|
+
userInvitation?: UserInvitationTemplateFunction;
|
|
111
|
+
};
|
|
112
|
+
}
|