create-nexwire 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/express/README.md +13 -7
- package/express/package.json +2 -2
- package/express/src/config/session.ts +1 -1
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/database/schema.ts +3 -3
- package/express/src/framework/cache/cache.ts +82 -81
- package/express/src/framework/database/seed.ts +1 -1
- package/express/src/framework/facade.ts +1 -24
- package/express/src/framework/http/app.ts +5 -5
- package/express/src/framework/http/openapi.ts +1 -1
- package/express/src/framework/http/router.ts +3 -18
- package/express/src/framework/http/types.ts +19 -0
- package/express/src/framework/http/validation.ts +18 -18
- package/express/src/framework/maker-cli/middleware/core.mjs +41 -47
- package/express/src/framework/maker-cli/module/core.mjs +920 -684
- package/express/src/framework/maker-cli/module/index.mjs +202 -140
- package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +40 -14
- package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +40 -15
- package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +42 -16
- package/express/src/framework/maker-cli/stubs/controller/standalone.ts.stub +47 -0
- package/express/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/express/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/express/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
- package/express/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
- package/express/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
- package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +50 -50
- package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
- package/express/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
- package/express/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
- package/express/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +113 -0
- package/express/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
- package/express/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
- package/express/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
- package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/express/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/express/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/express/src/framework/maker-cli/utils/naming.mjs +56 -13
- package/express/src/framework/modules/discover.ts +57 -54
- package/express/src/framework/notification/index.ts +1 -1
- package/express/src/framework/realtime/broadcast.ts +46 -46
- package/express/src/framework/server.ts +2 -1
- package/express/src/framework/session/session.ts +102 -98
- package/express/src/framework/support/mail.ts +48 -48
- package/express/src/framework/support/status-codes.ts +75 -0
- package/express/src/middlewares/auth-middleware.ts +1 -1
- package/express/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
- package/express/src/modules/auth/__tests__/login.test.ts +171 -0
- package/express/src/modules/auth/__tests__/logout.test.ts +60 -0
- package/express/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
- package/express/src/modules/auth/__tests__/me.test.ts +91 -0
- package/express/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/express/src/modules/auth/__tests__/register.test.ts +291 -0
- package/express/src/modules/auth/__tests__/resetpass.test.ts +206 -0
- package/express/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
- package/express/src/modules/auth/controllers/auth.ts +254 -0
- package/express/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -15
- package/{hono/src/modules/auth/database/models/notifications.ts → express/src/modules/auth/database/models/notification.ts} +1 -1
- package/express/src/modules/auth/database/models/role.ts +1 -1
- package/express/src/modules/auth/database/models/user.ts +1 -1
- package/express/src/modules/auth/facade.ts +12 -0
- package/express/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +3 -4
- package/express/src/modules/auth/jobs/registeruser.ts +23 -23
- package/express/src/modules/auth/routes/{api.ts → index.ts} +150 -150
- package/express/src/modules/auth/routes/role.ts +33 -33
- package/{hono/src/modules/auth/controllers/auth.schema.ts → express/src/modules/auth/schemas/auth.ts} +102 -102
- package/express/src/modules/auth/services/forgetpassword.ts +66 -0
- package/express/src/modules/auth/services/login.ts +63 -0
- package/express/src/modules/auth/services/logout.ts +25 -0
- package/express/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/express/src/modules/auth/services/me.ts +31 -0
- package/express/src/modules/auth/services/refreshtoken.ts +110 -0
- package/express/src/modules/auth/services/register.ts +141 -0
- package/express/src/modules/auth/services/resetpassword.ts +69 -0
- package/express/src/modules/auth/services/verifyemail.ts +77 -0
- package/express/src/modules/auth/types/auth.ts +31 -0
- package/express/src/resources/src/assets/images/logo.png +0 -0
- package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -0
- package/express/src/resources/src/pages/auth/login.vue +1 -1
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/hono/README.md +13 -7
- package/hono/package.json +1 -1
- package/hono/src/config/session.ts +1 -1
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/database/schema.ts +3 -3
- package/hono/src/framework/cache/cache.ts +82 -81
- package/hono/src/framework/database/seed.ts +1 -1
- package/hono/src/framework/http/app.ts +5 -5
- package/hono/src/framework/http/validation.ts +21 -21
- package/hono/src/framework/maker-cli/middleware/core.mjs +40 -46
- package/hono/src/framework/maker-cli/module/core.mjs +401 -168
- package/hono/src/framework/maker-cli/module/index.mjs +73 -11
- package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +28 -16
- package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +28 -17
- package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +28 -16
- package/hono/src/framework/maker-cli/stubs/controller/standalone.ts.stub +56 -0
- package/hono/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/hono/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/hono/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
- package/hono/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
- package/hono/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
- package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -49
- package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
- package/hono/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
- package/hono/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
- package/hono/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +112 -0
- package/hono/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
- package/hono/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
- package/hono/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
- package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/hono/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/hono/src/framework/maker-cli/utils/naming.mjs +56 -13
- package/hono/src/framework/modules/discover.ts +57 -54
- package/hono/src/framework/notification/index.ts +1 -1
- package/hono/src/framework/realtime/broadcast.ts +46 -46
- package/hono/src/framework/server.ts +1 -1
- package/hono/src/framework/session/session.ts +121 -116
- package/hono/src/middlewares/auth-middleware.ts +1 -1
- package/hono/src/modules/auth/__tests__/forgetpass.test.ts +170 -0
- package/hono/src/modules/auth/__tests__/login.test.ts +154 -0
- package/hono/src/modules/auth/__tests__/logout.test.ts +53 -0
- package/hono/src/modules/auth/__tests__/logoutalldevices.test.ts +34 -0
- package/hono/src/modules/auth/__tests__/me.test.ts +87 -0
- package/hono/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/hono/src/modules/auth/__tests__/register.test.ts +258 -0
- package/hono/src/modules/auth/__tests__/resetpass.test.ts +214 -0
- package/hono/src/modules/auth/__tests__/verifyemail.test.ts +189 -0
- package/hono/src/modules/auth/controllers/auth.ts +238 -0
- package/hono/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -25
- package/{express/src/modules/auth/database/models/notifications.ts → hono/src/modules/auth/database/models/notification.ts} +1 -1
- package/hono/src/modules/auth/database/models/role.ts +1 -1
- package/hono/src/modules/auth/database/models/user.ts +1 -1
- package/hono/src/modules/auth/facade.ts +12 -0
- package/hono/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +5 -6
- package/hono/src/modules/auth/jobs/registeruser.ts +23 -23
- package/hono/src/modules/auth/routes/{api.ts → index.ts} +150 -150
- package/hono/src/modules/auth/routes/role.ts +33 -33
- package/{express/src/modules/auth/controllers/auth.schema.ts → hono/src/modules/auth/schemas/auth.ts} +102 -102
- package/hono/src/modules/auth/services/forgetpassword.ts +58 -0
- package/hono/src/modules/auth/services/login.ts +61 -0
- package/hono/src/modules/auth/services/logout.ts +24 -0
- package/hono/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/hono/src/modules/auth/services/me.ts +31 -0
- package/hono/src/modules/auth/services/refreshtoken.ts +110 -0
- package/hono/src/modules/auth/services/register.ts +132 -0
- package/hono/src/modules/auth/services/resetpassword.ts +69 -0
- package/hono/src/modules/auth/services/verifyemail.ts +77 -0
- package/hono/src/modules/auth/types/auth.ts +31 -0
- package/hono/src/resources/src/assets/images/logo.png +0 -0
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +1 -1
- package/express/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
- package/express/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -418
- package/express/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
- package/express/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
- package/express/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
- package/express/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
- package/express/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
- package/express/src/modules/auth/controllers/auth.controller.ts +0 -358
- package/hono/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
- package/hono/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -505
- package/hono/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
- package/hono/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
- package/hono/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
- package/hono/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
- package/hono/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
- package/hono/src/modules/auth/controllers/auth.controller.ts +0 -413
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db } from "@/framework/facade.js";
|
|
3
|
+
import { {{tableVariable}} } from "@/modules/{{module}}/database/models/{{controller}}.js";
|
|
4
|
+
|
|
5
|
+
export const {{controller}}Service = {
|
|
6
|
+
/**
|
|
7
|
+
* Why: Returns every {{controller}} row for list screens.
|
|
8
|
+
* When: Called by the controller's index() handler.
|
|
9
|
+
* Where: GET /{{controller}} route.
|
|
10
|
+
*/
|
|
11
|
+
list: async () => {
|
|
12
|
+
return db.select().from({{tableVariable}});
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Why: Returns one {{controller}} row by primary key.
|
|
17
|
+
* When: Called by the controller's show/update/destroy handlers.
|
|
18
|
+
* Where: Reused across GET/PUT/DELETE /{{controller}}/{id} routes.
|
|
19
|
+
*/
|
|
20
|
+
findOne: async (id: number) => {
|
|
21
|
+
const [row] = await db
|
|
22
|
+
.select()
|
|
23
|
+
.from({{tableVariable}})
|
|
24
|
+
.where(eq({{tableVariable}}.id, id))
|
|
25
|
+
.limit(1);
|
|
26
|
+
return row ?? null;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Why: Persists a new {{controller}} row.
|
|
31
|
+
* When: Called by the controller's store() handler.
|
|
32
|
+
* Where: POST /{{controller}} route.
|
|
33
|
+
*/
|
|
34
|
+
create: async (data: Record<string, unknown>) => {
|
|
35
|
+
const [row] = await db.insert({{tableVariable}}).values(data).returning();
|
|
36
|
+
return row;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Why: Applies a partial change to one {{controller}} row.
|
|
41
|
+
* When: Called by the controller's update() handler.
|
|
42
|
+
* Where: PUT/PATCH /{{controller}}/{id} route.
|
|
43
|
+
*/
|
|
44
|
+
update: async (id: number, data: Record<string, unknown>) => {
|
|
45
|
+
const [row] = await db
|
|
46
|
+
.update({{tableVariable}})
|
|
47
|
+
.set(data)
|
|
48
|
+
.where(eq({{tableVariable}}.id, id))
|
|
49
|
+
.returning();
|
|
50
|
+
return row ?? null;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Why: Removes one {{controller}} row.
|
|
55
|
+
* When: Called by the controller's destroy() handler.
|
|
56
|
+
* Where: DELETE /{{controller}}/{id} route.
|
|
57
|
+
*/
|
|
58
|
+
remove: async (id: number) => {
|
|
59
|
+
const [row] = await db
|
|
60
|
+
.delete({{tableVariable}})
|
|
61
|
+
.where(eq({{tableVariable}}.id, id))
|
|
62
|
+
.returning();
|
|
63
|
+
return row ?? null;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why: Shared TypeScript types for the {{module}} module.
|
|
3
|
+
* When: Needed by controllers, services, or helpers that must agree on a shape.
|
|
4
|
+
* Where: src/modules/{{module}}/types/{{name}}.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Why: Base shape for every {{name}} record. When: Used as a constraint or return type. Where: This file. */
|
|
8
|
+
export interface {{Name}}Record {
|
|
9
|
+
id: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Why: Result shape for a single-item response. When: Returned by show-style handlers. Where: This file. */
|
|
13
|
+
export interface {{Name}}Result {
|
|
14
|
+
message: string;
|
|
15
|
+
data: {{Name}}Record;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Why: Result shape for list responses. When: Returned by index-style handlers. Where: This file. */
|
|
19
|
+
export interface {{Name}}ListResult {
|
|
20
|
+
message: string;
|
|
21
|
+
data: {{Name}}Record[];
|
|
22
|
+
}
|
|
@@ -1,13 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A name is safe when it can be used as a folder name, a URL segment, a table
|
|
3
|
+
* name and a TypeScript identifier without further quoting.
|
|
4
|
+
*
|
|
5
|
+
* Kebab-case, snake_case and camelCase all pass: `pascal` already folds `-` and
|
|
6
|
+
* `_`, and names are lowercased before this test. What fails is anything that
|
|
7
|
+
* would produce uncompilable code - spaces and dots break the generated
|
|
8
|
+
* `@/modules/...` import, a leading digit breaks the identifier.
|
|
9
|
+
*/
|
|
10
|
+
const NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Best-effort readable repair of a rejected name, used only in error text.
|
|
14
|
+
* Returns "" when no safe suggestion exists (e.g. a leading digit cannot be
|
|
15
|
+
* reordered without guessing the developer's intent).
|
|
16
|
+
*/
|
|
17
|
+
function suggestName(value) {
|
|
18
|
+
const cleaned = value
|
|
19
|
+
.trim()
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
22
|
+
.replace(/-{2,}/g, "-")
|
|
23
|
+
.replace(/^[-_]+|[-_]+$/g, "");
|
|
24
|
+
if (!cleaned || !/^[a-z]/.test(cleaned)) return "";
|
|
25
|
+
return cleaned;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate that a name value exists and is safe to use.
|
|
30
|
+
* Throws with the exact command to run instead when the value is unusable.
|
|
31
|
+
*/
|
|
32
|
+
export function assertName(value, label) {
|
|
33
|
+
if (!value) {
|
|
34
|
+
throw new Error(`${label} is required.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const name = value.trim().toLowerCase();
|
|
38
|
+
if (NAME_PATTERN.test(name)) return name;
|
|
39
|
+
|
|
40
|
+
const suggestion = suggestName(value);
|
|
41
|
+
const reason = /^[0-9]/.test(name)
|
|
42
|
+
? "must start with a lowercase letter"
|
|
43
|
+
: "may only contain lowercase letters, numbers, dashes and underscores";
|
|
44
|
+
throw new Error(`Invalid ${label.toLowerCase()}: "${value}" - a name ${reason}.${suggestion ? ` Use "${suggestion}" instead.` : ""}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Convert a kebab-case or snake_case string to PascalCase. */
|
|
48
|
+
export function pascal(input) {
|
|
49
|
+
return input.replace(/(^\w|[-_]\w)/g, (part) => part.replace(/[-_]/g, "").toUpperCase());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convert a kebab-case or snake_case string to camelCase. */
|
|
53
|
+
export function camelCase(input) {
|
|
54
|
+
const p = pascal(input);
|
|
55
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
56
|
+
}
|
|
@@ -1,54 +1,57 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { glob } from "glob";
|
|
5
|
-
|
|
6
|
-
function resolveModulesPath() {
|
|
7
|
-
const cwd = process.cwd();
|
|
8
|
-
const srcModules = path.resolve(cwd, "src/modules");
|
|
9
|
-
const distModules = path.resolve(cwd, "dist/src/modules");
|
|
10
|
-
const entry = String(process.argv[1] || "");
|
|
11
|
-
const runningDist = /[\\/]dist[\\/]/.test(entry);
|
|
12
|
-
|
|
13
|
-
if (runningDist && fs.existsSync(distModules)) return distModules;
|
|
14
|
-
return srcModules;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const modulesPath = resolveModulesPath();
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Why: Finds module files by glob from active src/dist modules path.
|
|
21
|
-
* When: Framework needs module routes/jobs/seeders/schedules discovery.
|
|
22
|
-
* Where: Route registrar, queue boot, scheduler boot, seeding.
|
|
23
|
-
* How: Runs glob with absolute paths and stable sorting.
|
|
24
|
-
*/
|
|
25
|
-
export async function discoverModuleFiles(pattern: string) {
|
|
26
|
-
const files = await glob(pattern, {
|
|
27
|
-
cwd: modulesPath,
|
|
28
|
-
absolute: true,
|
|
29
|
-
nodir: true,
|
|
30
|
-
windowsPathsNoEscape: true
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return files.sort();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Why: Derives module
|
|
38
|
-
* When: Mounting route prefixes and grouping module assets.
|
|
39
|
-
* Where: Route registration logic.
|
|
40
|
-
* How:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
|
|
6
|
+
function resolveModulesPath() {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const srcModules = path.resolve(cwd, "src/modules");
|
|
9
|
+
const distModules = path.resolve(cwd, "dist/src/modules");
|
|
10
|
+
const entry = String(process.argv[1] || "");
|
|
11
|
+
const runningDist = /[\\/]dist[\\/]/.test(entry);
|
|
12
|
+
|
|
13
|
+
if (runningDist && fs.existsSync(distModules)) return distModules;
|
|
14
|
+
return srcModules;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const modulesPath = resolveModulesPath();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Why: Finds module files by glob from active src/dist modules path.
|
|
21
|
+
* When: Framework needs module routes/jobs/seeders/schedules discovery.
|
|
22
|
+
* Where: Route registrar, queue boot, scheduler boot, seeding.
|
|
23
|
+
* How: Runs glob with absolute paths and stable sorting.
|
|
24
|
+
*/
|
|
25
|
+
export async function discoverModuleFiles(pattern: string) {
|
|
26
|
+
const files = await glob(pattern, {
|
|
27
|
+
cwd: modulesPath,
|
|
28
|
+
absolute: true,
|
|
29
|
+
nodir: true,
|
|
30
|
+
windowsPathsNoEscape: true
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return files.sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Why: Derives the module name from an absolute discovered file path.
|
|
38
|
+
* When: Mounting route prefixes and grouping module assets.
|
|
39
|
+
* Where: Route registration logic.
|
|
40
|
+
* How: Strips the artifact folder (e.g. `routes/`), keeping any panel segment so
|
|
41
|
+
* `admin/post/routes/index.ts` resolves to `admin/post` rather than `admin`.
|
|
42
|
+
* Returns POSIX separators because the result is used as a URL prefix.
|
|
43
|
+
*/
|
|
44
|
+
export function moduleNameFromPath(filePath: string) {
|
|
45
|
+
const relative = path.relative(modulesPath, filePath);
|
|
46
|
+
return path.dirname(path.dirname(relative)).split(path.sep).join("/");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Why: Dynamically imports a discovered module file.
|
|
51
|
+
* When: Bootstrapping pluggable module artifacts.
|
|
52
|
+
* Where: Discovery consumers across framework.
|
|
53
|
+
* How: Converts file path to file URL and imports it.
|
|
54
|
+
*/
|
|
55
|
+
export async function importFile(filePath: string) {
|
|
56
|
+
return await import(pathToFileURL(filePath).href);
|
|
57
|
+
}
|
|
@@ -3,7 +3,7 @@ import { db } from "@/framework/database/connection.js";
|
|
|
3
3
|
import { dispatchEvent } from "@/framework/events/dispatcher.js";
|
|
4
4
|
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
5
5
|
import { mail } from "@/framework/support/mail.js";
|
|
6
|
-
import { notifications } from "@/modules/auth/database/models/
|
|
6
|
+
import { notifications } from "@/modules/auth/database/models/notification.js";
|
|
7
7
|
import { users } from "@/modules/auth/database/models/user.js";
|
|
8
8
|
|
|
9
9
|
/** Extra JSON metadata attached to the notification (e.g. entity IDs, links). */
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { env } from "@/env.js";
|
|
2
|
-
import { socketServer } from "@/framework/realtime/
|
|
3
|
-
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
4
|
-
|
|
5
|
-
export type BroadcastOptions = {
|
|
6
|
-
users?: Array<string | number>;
|
|
7
|
-
roles?: string[];
|
|
8
|
-
rooms?: string[];
|
|
9
|
-
auth?: boolean;
|
|
10
|
-
all?: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Why: Emits realtime events to targeted audiences.
|
|
15
|
-
* When: Domain events need websocket fan-out.
|
|
16
|
-
* Where: Event dispatcher and module jobs.
|
|
17
|
-
* How: Emits via Socket.IO if available; otherwise relays through Redis pub/sub
|
|
18
|
-
* so the main process (which has Socket.IO) can broadcast it.
|
|
19
|
-
*/
|
|
20
|
-
function emitViaIo(event: string, payload: any, options: BroadcastOptions) {
|
|
21
|
-
const io = socketServer();
|
|
22
|
-
if (!io) return;
|
|
23
|
-
|
|
24
|
-
if (options.all) io.emit(event, payload);
|
|
25
|
-
if (options.auth) io.to("auth").emit(event, payload);
|
|
26
|
-
|
|
27
|
-
for (const user of options.users || []) io.to(`user:${user}`).emit(event, payload);
|
|
28
|
-
for (const role of options.roles || []) io.to(`role:${role}`).emit(event, payload);
|
|
29
|
-
for (const room of options.rooms || []) io.to(room).emit(event, payload);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function broadcast(event: string, payload: any, options: BroadcastOptions = {}) {
|
|
33
|
-
const io = socketServer();
|
|
34
|
-
|
|
35
|
-
if (io) {
|
|
36
|
-
emitViaIo(event, payload, options);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// No Socket.IO in this process — relay through Redis so the main process can broadcast
|
|
41
|
-
const redis = redisClientIfReady();
|
|
42
|
-
if (!redis) return;
|
|
43
|
-
|
|
44
|
-
const channel = `${env.REDIS_PREFIX}:broadcast`;
|
|
45
|
-
redis.publish(channel, JSON.stringify({ event, payload, options }));
|
|
46
|
-
}
|
|
1
|
+
import { env } from "@/env.js";
|
|
2
|
+
import { socketServer } from "@/framework/realtime/socket.js";
|
|
3
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
4
|
+
|
|
5
|
+
export type BroadcastOptions = {
|
|
6
|
+
users?: Array<string | number>;
|
|
7
|
+
roles?: string[];
|
|
8
|
+
rooms?: string[];
|
|
9
|
+
auth?: boolean;
|
|
10
|
+
all?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Why: Emits realtime events to targeted audiences.
|
|
15
|
+
* When: Domain events need websocket fan-out.
|
|
16
|
+
* Where: Event dispatcher and module jobs.
|
|
17
|
+
* How: Emits via Socket.IO if available; otherwise relays through Redis pub/sub
|
|
18
|
+
* so the main process (which has Socket.IO) can broadcast it.
|
|
19
|
+
*/
|
|
20
|
+
function emitViaIo(event: string, payload: any, options: BroadcastOptions) {
|
|
21
|
+
const io = socketServer();
|
|
22
|
+
if (!io) return;
|
|
23
|
+
|
|
24
|
+
if (options.all) io.emit(event, payload);
|
|
25
|
+
if (options.auth) io.to("auth").emit(event, payload);
|
|
26
|
+
|
|
27
|
+
for (const user of options.users || []) io.to(`user:${user}`).emit(event, payload);
|
|
28
|
+
for (const role of options.roles || []) io.to(`role:${role}`).emit(event, payload);
|
|
29
|
+
for (const room of options.rooms || []) io.to(room).emit(event, payload);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function broadcast(event: string, payload: any, options: BroadcastOptions = {}) {
|
|
33
|
+
const io = socketServer();
|
|
34
|
+
|
|
35
|
+
if (io) {
|
|
36
|
+
emitViaIo(event, payload, options);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// No Socket.IO in this process — relay through Redis so the main process can broadcast
|
|
41
|
+
const redis = redisClientIfReady();
|
|
42
|
+
if (!redis) return;
|
|
43
|
+
|
|
44
|
+
const channel = `${env.REDIS_PREFIX}:broadcast`;
|
|
45
|
+
redis.publish(channel, JSON.stringify({ event, payload, options }));
|
|
46
|
+
}
|
|
@@ -148,7 +148,7 @@ export async function startServer(): Promise<ServerHandle> {
|
|
|
148
148
|
const socketLine = !realtimeConfig.enabled
|
|
149
149
|
? "Realtime (Socket.IO) disabled"
|
|
150
150
|
: realtime
|
|
151
|
-
? `Realtime (Socket.IO) enabled: ${socketAdmin.enabled ? "Admin UI: https://admin.socket.io" : ""}
|
|
151
|
+
? `Realtime (Socket.IO) enabled: ${socketAdmin.enabled ? "Admin UI: https://admin.socket.io" : ""}${socketAdmin.enabled ? " | " : " "}${serverUrl(server, WS_PATH).replace(/^http/, "ws")}`
|
|
152
152
|
: "Realtime (Socket.IO) unavailable";
|
|
153
153
|
console.log(!realtimeConfig.enabled ? chalk.gray(socketLine) : realtime ? chalk.green(socketLine) : chalk.yellow(socketLine));
|
|
154
154
|
|
|
@@ -1,116 +1,121 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { Context, Next } from "hono";
|
|
3
|
-
import { getCookie, setCookie } from "hono/cookie";
|
|
4
|
-
import { appConfig, redisConfig, sessionConfig } from "@/config/index.js";
|
|
5
|
-
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
6
|
-
|
|
7
|
-
/** Extract origin from a URL string. Returns empty string on invalid/missing input. */
|
|
8
|
-
function originOf(url?: string) {
|
|
9
|
-
if (!url) return "";
|
|
10
|
-
try {
|
|
11
|
-
return new URL(url).origin;
|
|
12
|
-
} catch {
|
|
13
|
-
return "";
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Returns true when FRONTEND_URL and APP_URL point to different origins — requires SameSite=None; Secure for session cookies. */
|
|
18
|
-
function needsCrossSiteCookie() {
|
|
19
|
-
const appOrigin = originOf(appConfig.url);
|
|
20
|
-
const frontendOrigin = originOf(appConfig.frontendUrl);
|
|
21
|
-
return Boolean(appOrigin && frontendOrigin && appOrigin !== frontendOrigin);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Builds the Redis key for a session id using the configured prefix. */
|
|
25
|
-
function sessionKey(id: string) {
|
|
26
|
-
return `${sessionConfig.keyPrefix}:${id}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Global middleware that ensures every request has a session cookie.
|
|
31
|
-
* - Reads existing session cookie from request.
|
|
32
|
-
* - If missing: generates a UUID, sets a new cookie (SameSite=None; Secure if cross-origin, else Lax).
|
|
33
|
-
* - Stores sessionId in Hono context (`c.get("sessionId")`).
|
|
34
|
-
* - Refreshes the Redis session TTL on every request.
|
|
35
|
-
*/
|
|
36
|
-
export async function sessionMiddleware(c: Context, next: Next) {
|
|
37
|
-
let sessionId = getCookie(c, sessionConfig.cookieName);
|
|
38
|
-
|
|
39
|
-
if (!sessionId) {
|
|
40
|
-
sessionId = randomUUID();
|
|
41
|
-
const crossSiteCookie = needsCrossSiteCookie();
|
|
42
|
-
setCookie(c, sessionConfig.cookieName, sessionId, {
|
|
43
|
-
httpOnly: true,
|
|
44
|
-
sameSite: crossSiteCookie ? "None" : "Lax",
|
|
45
|
-
secure: crossSiteCookie,
|
|
46
|
-
path: "/",
|
|
47
|
-
maxAge: sessionConfig.ttlSeconds
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
c.set("sessionId", sessionId);
|
|
52
|
-
await session.refresh(sessionId);
|
|
53
|
-
await next();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Redis-backed server-side session store. Each session is a namespaced JSON document with configurable TTL. */
|
|
57
|
-
export const session = {
|
|
58
|
-
/** Create a new session document in Redis. Returns the generated session id, or empty string when Redis is unavailable. */
|
|
59
|
-
async start(data: Record<string, unknown> = {}) {
|
|
60
|
-
const client = redisClientIfReady();
|
|
61
|
-
if (!client) return "";
|
|
62
|
-
|
|
63
|
-
const id = randomUUID();
|
|
64
|
-
await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
|
|
65
|
-
return id;
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
/** Get the full session payload for a given id. Returns null when session doesn't exist or Redis is unavailable. */
|
|
69
|
-
async all<T = Record<string, unknown>>(id: string): Promise<T | null> {
|
|
70
|
-
const client = redisClientIfReady();
|
|
71
|
-
if (!client) return null;
|
|
72
|
-
|
|
73
|
-
const data = await client.get(sessionKey(id));
|
|
74
|
-
return data ? JSON.parse(data) : null;
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
/** Read a single key from the session payload. Returns null if the key or session doesn't exist. */
|
|
78
|
-
async get<T>(id: string, key: string): Promise<T | null> {
|
|
79
|
-
const data = await session.all<Record<string, T>>(id);
|
|
80
|
-
return data?.[key] ?? null;
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
/** Write or update a single key in the session payload. Rewrites the entire document with a fresh TTL. Returns false when Redis is unavailable. */
|
|
84
|
-
async
|
|
85
|
-
const client = redisClientIfReady();
|
|
86
|
-
if (!client) return false;
|
|
87
|
-
|
|
88
|
-
const data = (await session.all<Record<string, unknown>>(id)) || {};
|
|
89
|
-
data[key] = value;
|
|
90
|
-
await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
|
|
91
|
-
return true;
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
async
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Context, Next } from "hono";
|
|
3
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
4
|
+
import { appConfig, redisConfig, sessionConfig } from "@/config/index.js";
|
|
5
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
6
|
+
|
|
7
|
+
/** Extract origin from a URL string. Returns empty string on invalid/missing input. */
|
|
8
|
+
function originOf(url?: string) {
|
|
9
|
+
if (!url) return "";
|
|
10
|
+
try {
|
|
11
|
+
return new URL(url).origin;
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Returns true when FRONTEND_URL and APP_URL point to different origins — requires SameSite=None; Secure for session cookies. */
|
|
18
|
+
function needsCrossSiteCookie() {
|
|
19
|
+
const appOrigin = originOf(appConfig.url);
|
|
20
|
+
const frontendOrigin = originOf(appConfig.frontendUrl);
|
|
21
|
+
return Boolean(appOrigin && frontendOrigin && appOrigin !== frontendOrigin);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Builds the Redis key for a session id using the configured prefix. */
|
|
25
|
+
function sessionKey(id: string) {
|
|
26
|
+
return `${sessionConfig.keyPrefix}:${id}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Global middleware that ensures every request has a session cookie.
|
|
31
|
+
* - Reads existing session cookie from request.
|
|
32
|
+
* - If missing: generates a UUID, sets a new cookie (SameSite=None; Secure if cross-origin, else Lax).
|
|
33
|
+
* - Stores sessionId in Hono context (`c.get("sessionId")`).
|
|
34
|
+
* - Refreshes the Redis session TTL on every request.
|
|
35
|
+
*/
|
|
36
|
+
export async function sessionMiddleware(c: Context, next: Next) {
|
|
37
|
+
let sessionId = getCookie(c, sessionConfig.cookieName);
|
|
38
|
+
|
|
39
|
+
if (!sessionId) {
|
|
40
|
+
sessionId = randomUUID();
|
|
41
|
+
const crossSiteCookie = needsCrossSiteCookie();
|
|
42
|
+
setCookie(c, sessionConfig.cookieName, sessionId, {
|
|
43
|
+
httpOnly: true,
|
|
44
|
+
sameSite: crossSiteCookie ? "None" : "Lax",
|
|
45
|
+
secure: crossSiteCookie,
|
|
46
|
+
path: "/",
|
|
47
|
+
maxAge: sessionConfig.ttlSeconds
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
c.set("sessionId", sessionId);
|
|
52
|
+
await session.refresh(sessionId);
|
|
53
|
+
await next();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Redis-backed server-side session store. Each session is a namespaced JSON document with configurable TTL. */
|
|
57
|
+
export const session = {
|
|
58
|
+
/** Create a new session document in Redis. Returns the generated session id, or empty string when Redis is unavailable. */
|
|
59
|
+
async start(data: Record<string, unknown> = {}) {
|
|
60
|
+
const client = redisClientIfReady();
|
|
61
|
+
if (!client) return "";
|
|
62
|
+
|
|
63
|
+
const id = randomUUID();
|
|
64
|
+
await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
|
|
65
|
+
return id;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/** Get the full session payload for a given id. Returns null when session doesn't exist or Redis is unavailable. */
|
|
69
|
+
async all<T = Record<string, unknown>>(id: string): Promise<T | null> {
|
|
70
|
+
const client = redisClientIfReady();
|
|
71
|
+
if (!client) return null;
|
|
72
|
+
|
|
73
|
+
const data = await client.get(sessionKey(id));
|
|
74
|
+
return data ? JSON.parse(data) : null;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/** Read a single key from the session payload. Returns null if the key or session doesn't exist. */
|
|
78
|
+
async get<T>(id: string, key: string): Promise<T | null> {
|
|
79
|
+
const data = await session.all<Record<string, T>>(id);
|
|
80
|
+
return data?.[key] ?? null;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/** Write or update a single key in the session payload. Rewrites the entire document with a fresh TTL. Returns false when Redis is unavailable. */
|
|
84
|
+
async set(id: string, key: string, value: unknown) {
|
|
85
|
+
const client = redisClientIfReady();
|
|
86
|
+
if (!client) return false;
|
|
87
|
+
|
|
88
|
+
const data = (await session.all<Record<string, unknown>>(id)) || {};
|
|
89
|
+
data[key] = value;
|
|
90
|
+
await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** @deprecated Use set() instead. */
|
|
95
|
+
async put(id: string, key: string, value: unknown) {
|
|
96
|
+
return this.set(id, key, value);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/** Extend the TTL of an existing session. Called automatically by sessionMiddleware. Returns false when Redis is unavailable. */
|
|
100
|
+
async refresh(id: string) {
|
|
101
|
+
const client = redisClientIfReady();
|
|
102
|
+
if (!client) return false;
|
|
103
|
+
|
|
104
|
+
await client.expire(sessionKey(id), sessionConfig.ttlSeconds);
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** Delete a session document from Redis. Use on logout or session expiry. Returns false when Redis is unavailable. */
|
|
109
|
+
async destroy(id: string) {
|
|
110
|
+
const client = redisClientIfReady();
|
|
111
|
+
if (!client) return false;
|
|
112
|
+
|
|
113
|
+
await client.del(sessionKey(id));
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/** Returns true when Redis is enabled and the Redis client is ready — guards against using session store without Redis. */
|
|
118
|
+
isAvailable() {
|
|
119
|
+
return redisConfig.enabled && redisClientIfReady() !== null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import type { Context, Next } from "hono";
|
|
3
|
-
import { cookie, db,
|
|
3
|
+
import { cookie, db, HttpStatusCodes, jwt } from "@/framework/facade.js";
|
|
4
4
|
import { refreshTokens, users } from "@/modules/auth/database/models/user.js";
|
|
5
5
|
|
|
6
6
|
export async function authMiddleware(c: Context, next: Next) {
|