alepha 0.13.2 → 0.13.3
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/api-files/index.browser.js +80 -0
- package/dist/api-files/index.browser.js.map +1 -0
- package/dist/api-jobs/index.browser.js +56 -0
- package/dist/api-jobs/index.browser.js.map +1 -0
- package/dist/api-notifications/index.browser.js +382 -0
- package/dist/api-notifications/index.browser.js.map +1 -0
- package/dist/api-notifications/index.d.ts +124 -69
- package/dist/api-notifications/index.js +107 -55
- package/dist/api-notifications/index.js.map +1 -1
- package/dist/api-parameters/index.browser.js +29 -0
- package/dist/api-parameters/index.browser.js.map +1 -0
- package/dist/api-users/index.d.ts +16 -3
- package/dist/api-users/index.js +75 -28
- package/dist/api-users/index.js.map +1 -1
- package/dist/api-verifications/index.browser.js +52 -0
- package/dist/api-verifications/index.browser.js.map +1 -0
- package/dist/api-verifications/index.d.ts +117 -95
- package/dist/api-verifications/index.js +1 -1
- package/dist/api-verifications/index.js.map +1 -1
- package/dist/batch/index.js +0 -5
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.js +7 -5
- package/dist/bucket/index.js.map +1 -1
- package/dist/cli/{dist-Dl9Vl7Ur.js → dist-lGnqsKpu.js} +11 -15
- package/dist/cli/dist-lGnqsKpu.js.map +1 -0
- package/dist/cli/index.d.ts +26 -45
- package/dist/cli/index.js +40 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -0
- package/dist/command/index.js +9 -0
- package/dist/command/index.js.map +1 -1
- package/dist/email/index.js +5 -0
- package/dist/email/index.js.map +1 -1
- package/dist/orm/index.js +3 -3
- package/dist/orm/index.js.map +1 -1
- package/dist/redis/index.d.ts +10 -10
- package/dist/security/index.d.ts +28 -28
- package/dist/security/index.js +3 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/index.d.ts +9 -9
- package/dist/server-auth/index.d.ts +152 -152
- package/dist/server-cookies/index.js +2 -2
- package/dist/server-cookies/index.js.map +1 -1
- package/dist/server-links/index.d.ts +33 -33
- package/dist/server-static/index.js +18 -2
- package/dist/server-static/index.js.map +1 -1
- package/package.json +16 -6
- package/src/api-files/index.browser.ts +17 -0
- package/src/api-jobs/index.browser.ts +15 -0
- package/src/api-notifications/controllers/NotificationController.ts +26 -1
- package/src/api-notifications/index.browser.ts +17 -0
- package/src/api-notifications/index.ts +1 -0
- package/src/api-notifications/schemas/notificationQuerySchema.ts +13 -0
- package/src/api-notifications/services/NotificationService.ts +45 -2
- package/src/api-parameters/index.browser.ts +12 -0
- package/src/api-users/atoms/realmAuthSettingsAtom.ts +3 -1
- package/src/api-users/controllers/UserController.ts +21 -1
- package/src/api-users/primitives/$userRealm.ts +33 -10
- package/src/api-users/providers/UserRealmProvider.ts +1 -0
- package/src/api-users/services/SessionService.ts +2 -0
- package/src/api-users/services/UserService.ts +56 -16
- package/src/api-verifications/index.browser.ts +15 -0
- package/src/api-verifications/index.ts +1 -0
- package/src/batch/providers/BatchProvider.ts +0 -7
- package/src/bucket/index.ts +7 -5
- package/src/cli/apps/AlephaCli.ts +27 -1
- package/src/cli/apps/AlephaPackageBuilderCli.ts +3 -0
- package/src/cli/commands/CoreCommands.ts +6 -2
- package/src/cli/commands/ViteCommands.ts +2 -1
- package/src/cli/services/ProjectUtils.ts +40 -75
- package/src/command/helpers/Asker.ts +10 -0
- package/src/email/index.ts +13 -5
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -3
- package/src/server-cookies/providers/ServerCookiesProvider.ts +2 -1
- package/src/server-static/providers/ServerStaticProvider.ts +18 -3
- package/dist/cli/dist-Dl9Vl7Ur.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServerRouteSecure } from "alepha/server/security";
|
|
2
|
-
import * as
|
|
2
|
+
import * as alepha21 from "alepha";
|
|
3
3
|
import { Alepha, Async, KIND, Primitive, Static } from "alepha";
|
|
4
4
|
import * as alepha_server0 from "alepha/server";
|
|
5
5
|
import { ActionPrimitive, ClientRequestEntry, ClientRequestOptions, ClientRequestResponse, FetchResponse, HttpClient, RequestConfigSchema, ServerRequest, ServerRequestConfigEntry, ServerResponseBody, ServerTimingProvider } from "alepha/server";
|
|
@@ -9,23 +9,23 @@ import { ProxyPrimitiveOptions, ServerProxyProvider } from "alepha/server/proxy"
|
|
|
9
9
|
import { ServiceAccountPrimitive, UserAccountToken } from "alepha/security";
|
|
10
10
|
|
|
11
11
|
//#region src/server-links/schemas/apiLinksResponseSchema.d.ts
|
|
12
|
-
declare const apiLinkSchema:
|
|
13
|
-
name:
|
|
14
|
-
group:
|
|
15
|
-
path:
|
|
16
|
-
method:
|
|
17
|
-
requestBodyType:
|
|
18
|
-
service:
|
|
12
|
+
declare const apiLinkSchema: alepha21.TObject<{
|
|
13
|
+
name: alepha21.TString;
|
|
14
|
+
group: alepha21.TOptional<alepha21.TString>;
|
|
15
|
+
path: alepha21.TString;
|
|
16
|
+
method: alepha21.TOptional<alepha21.TString>;
|
|
17
|
+
requestBodyType: alepha21.TOptional<alepha21.TString>;
|
|
18
|
+
service: alepha21.TOptional<alepha21.TString>;
|
|
19
19
|
}>;
|
|
20
|
-
declare const apiLinksResponseSchema:
|
|
21
|
-
prefix:
|
|
22
|
-
links:
|
|
23
|
-
name:
|
|
24
|
-
group:
|
|
25
|
-
path:
|
|
26
|
-
method:
|
|
27
|
-
requestBodyType:
|
|
28
|
-
service:
|
|
20
|
+
declare const apiLinksResponseSchema: alepha21.TObject<{
|
|
21
|
+
prefix: alepha21.TOptional<alepha21.TString>;
|
|
22
|
+
links: alepha21.TArray<alepha21.TObject<{
|
|
23
|
+
name: alepha21.TString;
|
|
24
|
+
group: alepha21.TOptional<alepha21.TString>;
|
|
25
|
+
path: alepha21.TString;
|
|
26
|
+
method: alepha21.TOptional<alepha21.TString>;
|
|
27
|
+
requestBodyType: alepha21.TOptional<alepha21.TString>;
|
|
28
|
+
service: alepha21.TOptional<alepha21.TString>;
|
|
29
29
|
}>>;
|
|
30
30
|
}>;
|
|
31
31
|
type ApiLinksResponse = Static<typeof apiLinksResponseSchema>;
|
|
@@ -180,8 +180,8 @@ declare class RemotePrimitiveProvider {
|
|
|
180
180
|
protected readonly remotes: Array<ServerRemote>;
|
|
181
181
|
protected readonly log: alepha_logger0.Logger;
|
|
182
182
|
getRemotes(): ServerRemote[];
|
|
183
|
-
readonly configure:
|
|
184
|
-
readonly start:
|
|
183
|
+
readonly configure: alepha21.HookPrimitive<"configure">;
|
|
184
|
+
readonly start: alepha21.HookPrimitive<"start">;
|
|
185
185
|
registerRemote(value: RemotePrimitive): Promise<void>;
|
|
186
186
|
protected readonly fetchLinks: alepha_retry0.RetryPrimitiveFn<(opts: FetchLinksOptions) => Promise<ApiLinksResponse>>;
|
|
187
187
|
}
|
|
@@ -249,22 +249,22 @@ declare class ServerLinksProvider {
|
|
|
249
249
|
protected readonly remoteProvider: RemotePrimitiveProvider;
|
|
250
250
|
protected readonly serverTimingProvider: ServerTimingProvider;
|
|
251
251
|
get prefix(): string;
|
|
252
|
-
readonly onRoute:
|
|
252
|
+
readonly onRoute: alepha21.HookPrimitive<"configure">;
|
|
253
253
|
/**
|
|
254
254
|
* First API - Get all API links for the user.
|
|
255
255
|
*
|
|
256
256
|
* This is based on the user's permissions.
|
|
257
257
|
*/
|
|
258
258
|
readonly links: alepha_server0.RoutePrimitive<{
|
|
259
|
-
response:
|
|
260
|
-
prefix:
|
|
261
|
-
links:
|
|
262
|
-
name:
|
|
263
|
-
group:
|
|
264
|
-
path:
|
|
265
|
-
method:
|
|
266
|
-
requestBodyType:
|
|
267
|
-
service:
|
|
259
|
+
response: alepha21.TObject<{
|
|
260
|
+
prefix: alepha21.TOptional<alepha21.TString>;
|
|
261
|
+
links: alepha21.TArray<alepha21.TObject<{
|
|
262
|
+
name: alepha21.TString;
|
|
263
|
+
group: alepha21.TOptional<alepha21.TString>;
|
|
264
|
+
path: alepha21.TString;
|
|
265
|
+
method: alepha21.TOptional<alepha21.TString>;
|
|
266
|
+
requestBodyType: alepha21.TOptional<alepha21.TString>;
|
|
267
|
+
service: alepha21.TOptional<alepha21.TString>;
|
|
268
268
|
}>>;
|
|
269
269
|
}>;
|
|
270
270
|
}>;
|
|
@@ -275,10 +275,10 @@ declare class ServerLinksProvider {
|
|
|
275
275
|
* I mean for 150+ links, you got 50ms of serialization time.
|
|
276
276
|
*/
|
|
277
277
|
readonly schema: alepha_server0.RoutePrimitive<{
|
|
278
|
-
params:
|
|
279
|
-
name:
|
|
278
|
+
params: alepha21.TObject<{
|
|
279
|
+
name: alepha21.TString;
|
|
280
280
|
}>;
|
|
281
|
-
response:
|
|
281
|
+
response: alepha21.TRecord<string, alepha21.TAny>;
|
|
282
282
|
}>;
|
|
283
283
|
getSchemaByName(name: string, options?: GetApiLinksOptions): Promise<RequestConfigSchema>;
|
|
284
284
|
/**
|
|
@@ -315,7 +315,7 @@ declare module "alepha" {
|
|
|
315
315
|
* @see {@link $client}
|
|
316
316
|
* @module alepha.server.links
|
|
317
317
|
*/
|
|
318
|
-
declare const AlephaServerLinks:
|
|
318
|
+
declare const AlephaServerLinks: alepha21.Service<alepha21.Module>;
|
|
319
319
|
//#endregion
|
|
320
320
|
export { $client, $remote, AlephaServerLinks, ApiLink, ApiLinksResponse, ClientScope, FetchLinksOptions, GetApiLinksOptions, HttpClientLink, HttpVirtualClient, LinkProvider, RemotePrimitive, RemotePrimitiveOptions, RemotePrimitiveProvider, ServerLinksProvider, ServerRemote, VirtualAction, apiLinkSchema, apiLinksResponseSchema };
|
|
321
321
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -73,7 +73,15 @@ var ServerStaticProvider = class {
|
|
|
73
73
|
}
|
|
74
74
|
reply.headers["content-type"] = "text/html";
|
|
75
75
|
reply.status = 200;
|
|
76
|
-
return
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const stream = createReadStream(join(root, "index.html"));
|
|
78
|
+
stream.on("open", () => {
|
|
79
|
+
resolve(stream);
|
|
80
|
+
});
|
|
81
|
+
stream.on("error", (err) => {
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
77
85
|
}
|
|
78
86
|
});
|
|
79
87
|
}
|
|
@@ -111,7 +119,15 @@ var ServerStaticProvider = class {
|
|
|
111
119
|
reply.status = 304;
|
|
112
120
|
return;
|
|
113
121
|
}
|
|
114
|
-
return
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const stream = createReadStream(path);
|
|
124
|
+
stream.on("open", () => {
|
|
125
|
+
resolve(stream);
|
|
126
|
+
});
|
|
127
|
+
stream.on("error", (err) => {
|
|
128
|
+
reject(err);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
115
131
|
};
|
|
116
132
|
}
|
|
117
133
|
getCacheFileTypes() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/server-static/primitives/$serve.ts","../../src/server-static/providers/ServerStaticProvider.ts","../../src/server-static/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\n\n/**\n * Create a new static file handler.\n */\nexport const $serve = (options: ServePrimitiveOptions = {}): ServePrimitive => {\n return createPrimitive(ServePrimitive, options);\n};\n\nexport interface ServePrimitiveOptions {\n /**\n * Prefix for the served path.\n *\n * @default \"/\"\n */\n path?: string;\n\n /**\n * Path to the directory to serve.\n *\n * @default process.cwd()\n */\n root?: string;\n\n /**\n * If true, primitive will be ignored.\n *\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether to keep dot files (e.g. `.gitignore`, `.env`) in the served directory.\n *\n * @default true\n */\n ignoreDotEnvFiles?: boolean;\n\n /**\n * Whether to use the index.html file when the path is a directory.\n *\n * @default true\n */\n indexFallback?: boolean;\n\n /**\n * Force all requests \"not found\" to be served with the index.html file.\n * This is useful for single-page applications (SPAs) that use client-side only routing.\n */\n historyApiFallback?: boolean;\n\n /**\n * Optional name of the primitive.\n * This is used for logging and debugging purposes.\n *\n * @default Key name.\n */\n name?: string;\n\n /**\n * Whether to use cache control headers.\n *\n * @default {}\n */\n cacheControl?: Partial<CacheControlOptions> | false;\n}\n\nexport interface CacheControlOptions {\n /**\n * Whether to use cache control headers.\n *\n * @default [.js, .css]\n */\n fileTypes: string[];\n\n /**\n * The maximum age of the cache in seconds.\n *\n * @default 60 * 60 * 24 * 2 // 2 days\n */\n maxAge: DurationLike;\n\n /**\n * Whether to use immutable cache control headers.\n *\n * @default true\n */\n immutable: boolean;\n}\n\nexport class ServePrimitive extends Primitive<ServePrimitiveOptions> {}\n\n$serve[KIND] = ServePrimitive;\n","import { createReadStream } from \"node:fs\";\nimport { access, readdir, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join } from \"node:path\";\nimport type { Readable as NodeStream } from \"node:stream\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { FileDetector } from \"alepha/file\";\nimport { $logger } from \"alepha/logger\";\nimport { type ServerHandler, ServerRouterProvider } from \"alepha/server\";\nimport { $serve, type ServePrimitiveOptions } from \"../primitives/$serve.ts\";\n\nexport class ServerStaticProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly log = $logger();\n protected readonly directories: ServeDirectory[] = [];\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n await Promise.all(\n this.alepha\n .primitives($serve)\n .map((it) => this.createStaticServer(it.options)),\n );\n },\n });\n\n public async createStaticServer(\n options: ServePrimitiveOptions,\n ): Promise<void> {\n const prefix = options.path ?? \"/\";\n\n let root = options.root ?? process.cwd();\n if (!isAbsolute(root)) {\n root = join(process.cwd(), root);\n }\n\n this.log.debug(\"Serve static files\", { prefix, root });\n\n await stat(root);\n\n // 1. get all files in the root directory (recursively)\n const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);\n\n // 2. create a $route for each file (yes, this could be a lot of routes)\n const routes = await Promise.all(\n files.map(async (file) => {\n const path = file.replace(root, \"\").replace(/\\\\/g, \"/\");\n this.log.trace(`Mount ${join(prefix, path)} -> ${join(root, path)}`);\n return {\n path: join(prefix, encodeURI(path)),\n handler: await this.createFileHandler(join(root, path), options),\n };\n }),\n );\n\n for (const route of routes) {\n this.routerProvider.createRoute(route);\n\n // if route is for index.html, also create a route without it\n // e.g. /my/path/index.html -> /my/path/\n if (\n options.indexFallback !== false &&\n route.path.endsWith(\"index.html\")\n ) {\n this.routerProvider.createRoute({\n path: route.path.replace(/index\\.html$/, \"\"),\n handler: route.handler,\n });\n }\n }\n\n // 3. store the directory info for reference\n this.directories.push({\n options,\n files: files.map((file) => file.replace(root, \"\").replace(/\\\\/g, \"/\")),\n });\n\n // bonus! for SPAs, handle history API fallback\n if (options.historyApiFallback) {\n // meaning all unmatched routes should serve index.html\n this.routerProvider.createRoute({\n path: join(prefix, \"*\").replace(/\\\\/g, \"/\"),\n handler: async (request) => {\n const { reply } = request;\n\n if (request.url.pathname.includes(\".\")) {\n // If the request is for a file (e.g., /style.css), do not fall back\n reply.headers[\"content-type\"] = \"text/plain\";\n reply.body = \"Not Found\";\n reply.status = 404;\n return;\n }\n\n reply.headers[\"content-type\"] = \"text/html\";\n reply.status = 200;\n\n // Serve index.html for all unmatched routes\n return createReadStream(join(root, \"index.html\"));\n },\n });\n }\n }\n\n public async createFileHandler(\n filepath: string,\n options: ServePrimitiveOptions,\n ): Promise<ServerHandler> {\n const filename = basename(filepath);\n\n const hasGzip = await access(`${filepath}.gz`)\n .then(() => true)\n .catch(() => false);\n\n const hasBr = await access(`${filepath}.br`)\n .then(() => true)\n .catch(() => false);\n\n const fileStat = await stat(filepath);\n const lastModified = fileStat.mtime.toUTCString();\n const etag = `\"${fileStat.size}-${fileStat.mtime.getTime()}\"`;\n const contentType = this.fileDetector.getContentType(filename);\n const cacheControl = this.getCacheControl(filename, options);\n\n return async (request): Promise<NodeStream | undefined> => {\n const { headers, reply } = request;\n let path = filepath;\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding) {\n if (hasBr && encoding.includes(\"br\")) {\n reply.headers[\"content-encoding\"] = \"br\";\n path += \".br\";\n } else if (hasGzip && encoding.includes(\"gzip\")) {\n reply.headers[\"content-encoding\"] = \"gzip\";\n path += \".gz\";\n }\n }\n\n reply.headers[\"content-type\"] = contentType;\n reply.headers[\"accept-ranges\"] = \"bytes\";\n reply.headers[\"last-modified\"] = lastModified;\n\n if (cacheControl) {\n reply.headers[\"cache-control\"] =\n `public, max-age=${cacheControl.maxAge}`;\n if (cacheControl.immutable) {\n reply.headers[\"cache-control\"] += \", immutable\";\n }\n }\n\n reply.headers.etag = etag;\n if (\n headers[\"if-none-match\"] === etag ||\n headers[\"if-modified-since\"] === lastModified\n ) {\n reply.status = 304;\n return;\n }\n\n return createReadStream(path);\n };\n }\n\n protected getCacheFileTypes(): string[] {\n return [\n \".js\",\n \".css\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".otf\",\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".svg\",\n \".gif\",\n ];\n }\n\n protected getCacheControl(\n filename: string,\n options: ServePrimitiveOptions,\n ): { maxAge: number; immutable: boolean } | undefined {\n if (!options.cacheControl) {\n return;\n }\n\n const fileTypes =\n options.cacheControl.fileTypes ?? this.getCacheFileTypes();\n\n for (const type of fileTypes) {\n if (filename.endsWith(type)) {\n return {\n immutable: options.cacheControl.immutable ?? true,\n maxAge: this.dateTimeProvider\n .duration(options.cacheControl.maxAge ?? [30, \"days\"])\n .as(\"seconds\"),\n };\n }\n }\n }\n\n public async getAllFiles(\n dir: string,\n ignoreDotEnvFiles = true,\n ): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n const files = await Promise.all(\n entries.map((dirent) => {\n // skip .env & other dot files\n if (ignoreDotEnvFiles && dirent.name.startsWith(\".\")) {\n return [];\n }\n\n const fullPath = join(dir, dirent.name);\n return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;\n }),\n );\n\n return files.flat();\n }\n}\n\nexport interface ServeDirectory {\n options: ServePrimitiveOptions;\n files: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $serve } from \"./primitives/$serve.ts\";\nimport { ServerStaticProvider } from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$serve.ts\";\nexport * from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Create static file server with `$static()`.\n *\n * @see {@link ServerStaticProvider}\n * @module alepha.server.static\n */\nexport const AlephaServerStatic = $module({\n name: \"alepha.server.static\",\n primitives: [$serve],\n services: [AlephaServer, ServerStaticProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/server-static/primitives/$serve.ts","../../src/server-static/providers/ServerStaticProvider.ts","../../src/server-static/index.ts"],"sourcesContent":["import { createPrimitive, KIND, Primitive } from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\n\n/**\n * Create a new static file handler.\n */\nexport const $serve = (options: ServePrimitiveOptions = {}): ServePrimitive => {\n return createPrimitive(ServePrimitive, options);\n};\n\nexport interface ServePrimitiveOptions {\n /**\n * Prefix for the served path.\n *\n * @default \"/\"\n */\n path?: string;\n\n /**\n * Path to the directory to serve.\n *\n * @default process.cwd()\n */\n root?: string;\n\n /**\n * If true, primitive will be ignored.\n *\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether to keep dot files (e.g. `.gitignore`, `.env`) in the served directory.\n *\n * @default true\n */\n ignoreDotEnvFiles?: boolean;\n\n /**\n * Whether to use the index.html file when the path is a directory.\n *\n * @default true\n */\n indexFallback?: boolean;\n\n /**\n * Force all requests \"not found\" to be served with the index.html file.\n * This is useful for single-page applications (SPAs) that use client-side only routing.\n */\n historyApiFallback?: boolean;\n\n /**\n * Optional name of the primitive.\n * This is used for logging and debugging purposes.\n *\n * @default Key name.\n */\n name?: string;\n\n /**\n * Whether to use cache control headers.\n *\n * @default {}\n */\n cacheControl?: Partial<CacheControlOptions> | false;\n}\n\nexport interface CacheControlOptions {\n /**\n * Whether to use cache control headers.\n *\n * @default [.js, .css]\n */\n fileTypes: string[];\n\n /**\n * The maximum age of the cache in seconds.\n *\n * @default 60 * 60 * 24 * 2 // 2 days\n */\n maxAge: DurationLike;\n\n /**\n * Whether to use immutable cache control headers.\n *\n * @default true\n */\n immutable: boolean;\n}\n\nexport class ServePrimitive extends Primitive<ServePrimitiveOptions> {}\n\n$serve[KIND] = ServePrimitive;\n","import { createReadStream } from \"node:fs\";\nimport { access, readdir, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join } from \"node:path\";\nimport type { Readable as NodeStream } from \"node:stream\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { FileDetector } from \"alepha/file\";\nimport { $logger } from \"alepha/logger\";\nimport { type ServerHandler, ServerRouterProvider } from \"alepha/server\";\nimport { $serve, type ServePrimitiveOptions } from \"../primitives/$serve.ts\";\n\nexport class ServerStaticProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly fileDetector = $inject(FileDetector);\n protected readonly log = $logger();\n protected readonly directories: ServeDirectory[] = [];\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: async () => {\n await Promise.all(\n this.alepha\n .primitives($serve)\n .map((it) => this.createStaticServer(it.options)),\n );\n },\n });\n\n public async createStaticServer(\n options: ServePrimitiveOptions,\n ): Promise<void> {\n const prefix = options.path ?? \"/\";\n\n let root = options.root ?? process.cwd();\n if (!isAbsolute(root)) {\n root = join(process.cwd(), root);\n }\n\n this.log.debug(\"Serve static files\", { prefix, root });\n\n await stat(root);\n\n // 1. get all files in the root directory (recursively)\n const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);\n\n // 2. create a $route for each file (yes, this could be a lot of routes)\n const routes = await Promise.all(\n files.map(async (file) => {\n const path = file.replace(root, \"\").replace(/\\\\/g, \"/\");\n this.log.trace(`Mount ${join(prefix, path)} -> ${join(root, path)}`);\n return {\n path: join(prefix, encodeURI(path)),\n handler: await this.createFileHandler(join(root, path), options),\n };\n }),\n );\n\n for (const route of routes) {\n this.routerProvider.createRoute(route);\n\n // if route is for index.html, also create a route without it\n // e.g. /my/path/index.html -> /my/path/\n if (\n options.indexFallback !== false &&\n route.path.endsWith(\"index.html\")\n ) {\n this.routerProvider.createRoute({\n path: route.path.replace(/index\\.html$/, \"\"),\n handler: route.handler,\n });\n }\n }\n\n // 3. store the directory info for reference\n this.directories.push({\n options,\n files: files.map((file) => file.replace(root, \"\").replace(/\\\\/g, \"/\")),\n });\n\n // bonus! for SPAs, handle history API fallback\n if (options.historyApiFallback) {\n // meaning all unmatched routes should serve index.html\n this.routerProvider.createRoute({\n path: join(prefix, \"*\").replace(/\\\\/g, \"/\"),\n handler: async (request) => {\n const { reply } = request;\n\n if (request.url.pathname.includes(\".\")) {\n // If the request is for a file (e.g., /style.css), do not fall back\n reply.headers[\"content-type\"] = \"text/plain\";\n reply.body = \"Not Found\";\n reply.status = 404;\n return;\n }\n\n reply.headers[\"content-type\"] = \"text/html\";\n reply.status = 200;\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(join(root, \"index.html\"));\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n },\n });\n }\n }\n\n public async createFileHandler(\n filepath: string,\n options: ServePrimitiveOptions,\n ): Promise<ServerHandler> {\n const filename = basename(filepath);\n\n const hasGzip = await access(`${filepath}.gz`)\n .then(() => true)\n .catch(() => false);\n\n const hasBr = await access(`${filepath}.br`)\n .then(() => true)\n .catch(() => false);\n\n const fileStat = await stat(filepath);\n const lastModified = fileStat.mtime.toUTCString();\n const etag = `\"${fileStat.size}-${fileStat.mtime.getTime()}\"`;\n const contentType = this.fileDetector.getContentType(filename);\n const cacheControl = this.getCacheControl(filename, options);\n\n return async (request): Promise<NodeStream | undefined> => {\n const { headers, reply } = request;\n let path = filepath;\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding) {\n if (hasBr && encoding.includes(\"br\")) {\n reply.headers[\"content-encoding\"] = \"br\";\n path += \".br\";\n } else if (hasGzip && encoding.includes(\"gzip\")) {\n reply.headers[\"content-encoding\"] = \"gzip\";\n path += \".gz\";\n }\n }\n\n reply.headers[\"content-type\"] = contentType;\n reply.headers[\"accept-ranges\"] = \"bytes\";\n reply.headers[\"last-modified\"] = lastModified;\n\n if (cacheControl) {\n reply.headers[\"cache-control\"] =\n `public, max-age=${cacheControl.maxAge}`;\n if (cacheControl.immutable) {\n reply.headers[\"cache-control\"] += \", immutable\";\n }\n }\n\n reply.headers.etag = etag;\n if (\n headers[\"if-none-match\"] === etag ||\n headers[\"if-modified-since\"] === lastModified\n ) {\n reply.status = 304;\n return;\n }\n\n return new Promise<any>((resolve, reject) => {\n const stream = createReadStream(path);\n stream.on(\"open\", () => {\n resolve(stream);\n });\n stream.on(\"error\", (err) => {\n reject(err);\n });\n });\n };\n }\n\n protected getCacheFileTypes(): string[] {\n return [\n \".js\",\n \".css\",\n \".woff\",\n \".woff2\",\n \".ttf\",\n \".eot\",\n \".otf\",\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".svg\",\n \".gif\",\n ];\n }\n\n protected getCacheControl(\n filename: string,\n options: ServePrimitiveOptions,\n ): { maxAge: number; immutable: boolean } | undefined {\n if (!options.cacheControl) {\n return;\n }\n\n const fileTypes =\n options.cacheControl.fileTypes ?? this.getCacheFileTypes();\n\n for (const type of fileTypes) {\n if (filename.endsWith(type)) {\n return {\n immutable: options.cacheControl.immutable ?? true,\n maxAge: this.dateTimeProvider\n .duration(options.cacheControl.maxAge ?? [30, \"days\"])\n .as(\"seconds\"),\n };\n }\n }\n }\n\n public async getAllFiles(\n dir: string,\n ignoreDotEnvFiles = true,\n ): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n const files = await Promise.all(\n entries.map((dirent) => {\n // skip .env & other dot files\n if (ignoreDotEnvFiles && dirent.name.startsWith(\".\")) {\n return [];\n }\n\n const fullPath = join(dir, dirent.name);\n return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;\n }),\n );\n\n return files.flat();\n }\n}\n\nexport interface ServeDirectory {\n options: ServePrimitiveOptions;\n files: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $serve } from \"./primitives/$serve.ts\";\nimport { ServerStaticProvider } from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$serve.ts\";\nexport * from \"./providers/ServerStaticProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Create static file server with `$static()`.\n *\n * @see {@link ServerStaticProvider}\n * @module alepha.server.static\n */\nexport const AlephaServerStatic = $module({\n name: \"alepha.server.static\",\n primitives: [$serve],\n services: [AlephaServer, ServerStaticProvider],\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "alepha",
|
|
3
3
|
"description": "Easy-to-use modern TypeScript framework for building many kind of applications.",
|
|
4
4
|
"author": "Nicolas Foures",
|
|
5
|
-
"version": "0.13.
|
|
5
|
+
"version": "0.13.3",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.0.0"
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"drizzle-kit": "^0.31.7",
|
|
24
24
|
"drizzle-orm": "^0.44.7",
|
|
25
25
|
"postgres": "^3.4.7",
|
|
26
|
-
"tsx": "^4.
|
|
27
|
-
"typebox": "^1.0.
|
|
26
|
+
"tsx": "^4.21.0",
|
|
27
|
+
"typebox": "^1.0.61",
|
|
28
28
|
"typescript": "^5.9.3",
|
|
29
|
-
"vite": "^7.2.
|
|
29
|
+
"vite": "^7.2.6",
|
|
30
30
|
"vite-bundle-analyzer": "^1.2.3",
|
|
31
31
|
"ws": "^8.18.3"
|
|
32
32
|
},
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"@types/nodemailer": "^7.0.4",
|
|
38
38
|
"@types/ws": "^8.18.1",
|
|
39
39
|
"cron-schedule": "^6.0.0",
|
|
40
|
-
"jose": "^6.1.
|
|
40
|
+
"jose": "^6.1.3",
|
|
41
41
|
"nodemailer": "^7.0.11",
|
|
42
42
|
"openid-client": "^6.8.1",
|
|
43
43
|
"prom-client": "^15.1.3",
|
|
44
44
|
"swagger-ui-dist": "^5.30.3",
|
|
45
|
-
"vitest": "^4.0.
|
|
45
|
+
"vitest": "^4.0.15"
|
|
46
46
|
},
|
|
47
47
|
"scarfSettings": {
|
|
48
48
|
"enabled": false
|
|
@@ -70,21 +70,29 @@
|
|
|
70
70
|
"exports": {
|
|
71
71
|
"./api/files": {
|
|
72
72
|
"types": "./dist/api-files/index.js",
|
|
73
|
+
"react-native": "./dist/api-files/index.browser.js",
|
|
74
|
+
"browser": "./dist/api-files/index.browser.js",
|
|
73
75
|
"import": "./dist/api-files/index.js",
|
|
74
76
|
"default": "./dist/api-files/index.js"
|
|
75
77
|
},
|
|
76
78
|
"./api/jobs": {
|
|
77
79
|
"types": "./dist/api-jobs/index.js",
|
|
80
|
+
"react-native": "./dist/api-jobs/index.browser.js",
|
|
81
|
+
"browser": "./dist/api-jobs/index.browser.js",
|
|
78
82
|
"import": "./dist/api-jobs/index.js",
|
|
79
83
|
"default": "./dist/api-jobs/index.js"
|
|
80
84
|
},
|
|
81
85
|
"./api/notifications": {
|
|
82
86
|
"types": "./dist/api-notifications/index.js",
|
|
87
|
+
"react-native": "./dist/api-notifications/index.browser.js",
|
|
88
|
+
"browser": "./dist/api-notifications/index.browser.js",
|
|
83
89
|
"import": "./dist/api-notifications/index.js",
|
|
84
90
|
"default": "./dist/api-notifications/index.js"
|
|
85
91
|
},
|
|
86
92
|
"./api/parameters": {
|
|
87
93
|
"types": "./dist/api-parameters/index.js",
|
|
94
|
+
"react-native": "./dist/api-parameters/index.browser.js",
|
|
95
|
+
"browser": "./dist/api-parameters/index.browser.js",
|
|
88
96
|
"import": "./dist/api-parameters/index.js",
|
|
89
97
|
"default": "./dist/api-parameters/index.js"
|
|
90
98
|
},
|
|
@@ -97,6 +105,8 @@
|
|
|
97
105
|
},
|
|
98
106
|
"./api/verifications": {
|
|
99
107
|
"types": "./dist/api-verifications/index.js",
|
|
108
|
+
"react-native": "./dist/api-verifications/index.browser.js",
|
|
109
|
+
"browser": "./dist/api-verifications/index.browser.js",
|
|
100
110
|
"import": "./dist/api-verifications/index.js",
|
|
101
111
|
"default": "./dist/api-verifications/index.js"
|
|
102
112
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export type * from "./controllers/FileController.ts";
|
|
6
|
+
export type * from "./controllers/StorageStatsController.ts";
|
|
7
|
+
export * from "./entities/files.ts";
|
|
8
|
+
export * from "./schemas/fileQuerySchema.ts";
|
|
9
|
+
export * from "./schemas/fileResourceSchema.ts";
|
|
10
|
+
export * from "./schemas/storageStatsSchema.ts";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const AlephaApiFiles = $module({
|
|
15
|
+
name: "alepha.api.files",
|
|
16
|
+
services: [],
|
|
17
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export * from "./entities/jobExecutions.ts";
|
|
6
|
+
export * from "./schemas/jobExecutionQuerySchema.ts";
|
|
7
|
+
export * from "./schemas/jobExecutionResourceSchema.ts";
|
|
8
|
+
export * from "./schemas/triggerJobSchema.ts";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export const AlephaApiJobs = $module({
|
|
13
|
+
name: "alepha.api.jobs",
|
|
14
|
+
services: [],
|
|
15
|
+
});
|
|
@@ -1 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import { $inject } from "alepha";
|
|
2
|
+
import { pg } from "alepha/orm";
|
|
3
|
+
import { $action } from "alepha/server";
|
|
4
|
+
import { notifications } from "../entities/notifications.ts";
|
|
5
|
+
import { notificationQuerySchema } from "../schemas/notificationQuerySchema.ts";
|
|
6
|
+
import { NotificationService } from "../services/NotificationService.ts";
|
|
7
|
+
|
|
8
|
+
export class NotificationController {
|
|
9
|
+
protected readonly url = "/notifications";
|
|
10
|
+
protected readonly group = "notifications";
|
|
11
|
+
protected readonly notificationService = $inject(NotificationService);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find notifications with pagination and filtering.
|
|
15
|
+
*/
|
|
16
|
+
public readonly findNotifications = $action({
|
|
17
|
+
path: this.url,
|
|
18
|
+
group: this.group,
|
|
19
|
+
description: "Find notifications with pagination and filtering",
|
|
20
|
+
schema: {
|
|
21
|
+
query: notificationQuerySchema,
|
|
22
|
+
response: pg.page(notifications.schema),
|
|
23
|
+
},
|
|
24
|
+
handler: ({ query }) => this.notificationService.findNotifications(query),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export * from "./controllers/NotificationController.ts";
|
|
6
|
+
export * from "./entities/notifications.ts";
|
|
7
|
+
export * from "./schemas/notificationContactPreferencesSchema.ts";
|
|
8
|
+
export * from "./schemas/notificationContactSchema.ts";
|
|
9
|
+
export * from "./schemas/notificationCreateSchema.ts";
|
|
10
|
+
export * from "./schemas/notificationQuerySchema.ts";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const AlephaApiNotifications = $module({
|
|
15
|
+
name: "alepha.api.notifications",
|
|
16
|
+
services: [],
|
|
17
|
+
});
|
|
@@ -18,6 +18,7 @@ export * from "./primitives/$notification.ts";
|
|
|
18
18
|
export * from "./queues/NotificationQueues.ts";
|
|
19
19
|
export * from "./schemas/notificationContactPreferencesSchema.ts";
|
|
20
20
|
export * from "./schemas/notificationCreateSchema.ts";
|
|
21
|
+
export * from "./schemas/notificationQuerySchema.ts";
|
|
21
22
|
export * from "./services/NotificationSenderService.ts";
|
|
22
23
|
export * from "./services/NotificationService.ts";
|
|
23
24
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Static } from "alepha";
|
|
2
|
+
import { t } from "alepha";
|
|
3
|
+
import { pageQuerySchema } from "alepha/orm";
|
|
4
|
+
|
|
5
|
+
export const notificationQuerySchema = t.extend(pageQuerySchema, {
|
|
6
|
+
type: t.optional(t.enum(["email", "sms"])),
|
|
7
|
+
template: t.optional(t.string()),
|
|
8
|
+
contact: t.optional(t.string()),
|
|
9
|
+
category: t.optional(t.string()),
|
|
10
|
+
status: t.optional(t.enum(["pending", "sent", "failed"])),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type NotificationQuery = Static<typeof notificationQuerySchema>;
|
|
@@ -2,13 +2,17 @@ import { $env, $inject, Alepha, type Static, t } from "alepha";
|
|
|
2
2
|
import { $batch } from "alepha/batch";
|
|
3
3
|
import { DateTimeProvider } from "alepha/datetime";
|
|
4
4
|
import { $logger } from "alepha/logger";
|
|
5
|
-
import { $repository } from "alepha/orm";
|
|
6
|
-
import {
|
|
5
|
+
import { $repository, type Page } from "alepha/orm";
|
|
6
|
+
import {
|
|
7
|
+
type NotificationEntity,
|
|
8
|
+
notifications,
|
|
9
|
+
} from "../entities/notifications.ts";
|
|
7
10
|
import { NotificationQueues } from "../queues/NotificationQueues.ts";
|
|
8
11
|
import {
|
|
9
12
|
type NotificationCreate,
|
|
10
13
|
notificationCreateSchema,
|
|
11
14
|
} from "../schemas/notificationCreateSchema.ts";
|
|
15
|
+
import type { NotificationQuery } from "../schemas/notificationQuerySchema.ts";
|
|
12
16
|
import { NotificationSenderService } from "./NotificationSenderService.ts";
|
|
13
17
|
|
|
14
18
|
export const notificationServiceEnvSchema = t.object({
|
|
@@ -65,6 +69,45 @@ export class NotificationService {
|
|
|
65
69
|
return this.notificationRepository.findOne({ where: { id } });
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
public async findNotifications(
|
|
73
|
+
q: NotificationQuery = {},
|
|
74
|
+
): Promise<Page<NotificationEntity>> {
|
|
75
|
+
this.log.trace("Finding notifications", { query: q });
|
|
76
|
+
q.sort ??= "-createdAt";
|
|
77
|
+
|
|
78
|
+
const where = this.notificationRepository.createQueryWhere();
|
|
79
|
+
|
|
80
|
+
if (q.type) {
|
|
81
|
+
where.type = { eq: q.type };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (q.template) {
|
|
85
|
+
where.template = { like: `%${q.template}%` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (q.contact) {
|
|
89
|
+
where.contact = { like: `%${q.contact}%` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (q.category) {
|
|
93
|
+
where.category = { eq: q.category };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (q.status) {
|
|
97
|
+
if (q.status === "sent") {
|
|
98
|
+
where.sentAt = { isNotNull: true };
|
|
99
|
+
where.error = { isNull: true };
|
|
100
|
+
} else if (q.status === "failed") {
|
|
101
|
+
where.error = { isNotNull: true };
|
|
102
|
+
} else if (q.status === "pending") {
|
|
103
|
+
where.sentAt = { isNull: true };
|
|
104
|
+
where.error = { isNull: true };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return this.notificationRepository.paginate(q, { where }, { count: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
68
111
|
/**
|
|
69
112
|
* Create a new notification.
|
|
70
113
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export * from "./entities/parameters.ts";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export const AlephaApiParameters = $module({
|
|
10
|
+
name: "alepha.api.parameters",
|
|
11
|
+
services: [],
|
|
12
|
+
});
|
|
@@ -60,10 +60,11 @@ export const realmAuthSettingsAtom = $atom({
|
|
|
60
60
|
}),
|
|
61
61
|
}),
|
|
62
62
|
default: {
|
|
63
|
+
// for a fresh hello world setup, we accept registration and email login
|
|
63
64
|
registrationAllowed: true,
|
|
64
65
|
emailEnabled: true,
|
|
65
66
|
emailRequired: true,
|
|
66
|
-
usernameEnabled:
|
|
67
|
+
usernameEnabled: false,
|
|
67
68
|
usernameRequired: false,
|
|
68
69
|
phoneEnabled: false,
|
|
69
70
|
phoneRequired: false,
|
|
@@ -72,6 +73,7 @@ export const realmAuthSettingsAtom = $atom({
|
|
|
72
73
|
resetPasswordAllowed: false,
|
|
73
74
|
firstNameLastNameEnabled: false,
|
|
74
75
|
firstNameLastNameRequired: false,
|
|
76
|
+
// TODO: not implemented yet
|
|
75
77
|
passwordPolicy: {
|
|
76
78
|
minLength: 8,
|
|
77
79
|
requireUppercase: true,
|
|
@@ -310,6 +310,8 @@ export class UserController {
|
|
|
310
310
|
/**
|
|
311
311
|
* Request email verification.
|
|
312
312
|
* Generates a verification token using verification service and sends an email to the user.
|
|
313
|
+
* @param method - The verification method: "code" (default) sends a 6-digit code, "link" sends a clickable verification link.
|
|
314
|
+
* @param verifyUrl - Required when method is "link". The base URL for the verification link. Token and email will be appended as query params.
|
|
313
315
|
*/
|
|
314
316
|
public requestEmailVerification = $action({
|
|
315
317
|
path: "/users/email-verification/request",
|
|
@@ -317,6 +319,19 @@ export class UserController {
|
|
|
317
319
|
schema: {
|
|
318
320
|
query: t.object({
|
|
319
321
|
userRealmName: t.optional(t.string()),
|
|
322
|
+
method: t.optional(
|
|
323
|
+
t.enum(["code", "link"], {
|
|
324
|
+
default: "code",
|
|
325
|
+
description:
|
|
326
|
+
'Verification method: "code" sends a 6-digit code, "link" sends a clickable verification link.',
|
|
327
|
+
}),
|
|
328
|
+
),
|
|
329
|
+
verifyUrl: t.optional(
|
|
330
|
+
t.string({
|
|
331
|
+
description:
|
|
332
|
+
'Base URL for verification link. Required when method is "link". Token and email will be appended as query params.',
|
|
333
|
+
}),
|
|
334
|
+
),
|
|
320
335
|
}),
|
|
321
336
|
body: t.object({
|
|
322
337
|
email: t.email(),
|
|
@@ -327,15 +342,20 @@ export class UserController {
|
|
|
327
342
|
}),
|
|
328
343
|
},
|
|
329
344
|
handler: async ({ body, query }) => {
|
|
345
|
+
const method = query.method ?? "code";
|
|
330
346
|
await this.userService.requestEmailVerification(
|
|
331
347
|
body.email,
|
|
332
348
|
query.userRealmName,
|
|
349
|
+
method,
|
|
350
|
+
query.verifyUrl,
|
|
333
351
|
);
|
|
334
352
|
|
|
335
353
|
return {
|
|
336
354
|
success: true,
|
|
337
355
|
message:
|
|
338
|
-
|
|
356
|
+
method === "link"
|
|
357
|
+
? "If an account exists with this email, a verification link has been sent."
|
|
358
|
+
: "If an account exists with this email, a verification code has been sent.",
|
|
339
359
|
};
|
|
340
360
|
},
|
|
341
361
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { $context } from "alepha";
|
|
2
|
+
import { AlephaApiFiles } from "alepha/api/files";
|
|
2
3
|
import type { Repository } from "alepha/orm";
|
|
3
4
|
import {
|
|
4
5
|
$realm,
|
|
@@ -48,7 +49,14 @@ export const $userRealm = (
|
|
|
48
49
|
const userRealmProvider = alepha.inject(UserRealmProvider);
|
|
49
50
|
const name = options.realm?.name ?? DEFAULT_USER_REALM_NAME;
|
|
50
51
|
|
|
51
|
-
userRealmProvider.register(name, options);
|
|
52
|
+
const userRealm = userRealmProvider.register(name, options);
|
|
53
|
+
|
|
54
|
+
if (options.modules?.audits) {
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.modules?.files) {
|
|
58
|
+
alepha.with(AlephaApiFiles);
|
|
59
|
+
}
|
|
52
60
|
|
|
53
61
|
const realm: UserRealmPrimitive = $realm({
|
|
54
62
|
...options.realm,
|
|
@@ -103,18 +111,28 @@ export const $userRealm = (
|
|
|
103
111
|
sessionService.login(name, credentials.username, credentials.password);
|
|
104
112
|
};
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
const identities = options.identities ?? {
|
|
115
|
+
credentials: true,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (identities) {
|
|
119
|
+
const auth: Record<string, AuthPrimitive> = {};
|
|
120
|
+
if (identities.credentials) {
|
|
121
|
+
auth.credentials = $authCredentials(realm);
|
|
122
|
+
} else {
|
|
123
|
+
// if credentials auth is disabled, disable registration as well
|
|
124
|
+
userRealm.settings.registrationAllowed = false;
|
|
110
125
|
}
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
|
|
127
|
+
if (identities.google) {
|
|
128
|
+
auth.google = $authGoogle(realm);
|
|
113
129
|
}
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
|
|
131
|
+
if (identities.github) {
|
|
132
|
+
auth.github = $authGithub(realm);
|
|
116
133
|
}
|
|
117
|
-
|
|
134
|
+
|
|
135
|
+
alepha.with(() => auth);
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
return realm;
|
|
@@ -153,4 +171,9 @@ export interface UserRealmOptions {
|
|
|
153
171
|
google?: true;
|
|
154
172
|
github?: true;
|
|
155
173
|
};
|
|
174
|
+
|
|
175
|
+
modules?: {
|
|
176
|
+
files?: boolean;
|
|
177
|
+
audits?: boolean;
|
|
178
|
+
};
|
|
156
179
|
}
|
|
@@ -290,6 +290,8 @@ export class SessionService {
|
|
|
290
290
|
realm: realm.name,
|
|
291
291
|
username: profile.email.split("@")[0],
|
|
292
292
|
email: profile.email,
|
|
293
|
+
// we trust the OAuth2 provider
|
|
294
|
+
emailVerified: true,
|
|
293
295
|
roles: ["user"], // TODO: make default roles configurable via realm settings
|
|
294
296
|
});
|
|
295
297
|
|