adonia 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/build/adonis_peers.d.ts +14 -0
- package/build/adonis_peers.js +56 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.js +111 -0
- package/build/commands/add.d.ts +18 -0
- package/build/commands/add.js +246 -0
- package/build/commands/check.d.ts +4 -0
- package/build/commands/check.js +159 -0
- package/build/commands/diff.d.ts +2 -0
- package/build/commands/diff.js +74 -0
- package/build/commands/init.d.ts +6 -0
- package/build/commands/init.js +124 -0
- package/build/commands/list.d.ts +2 -0
- package/build/commands/list.js +34 -0
- package/build/config_merge.d.ts +10 -0
- package/build/config_merge.js +71 -0
- package/build/constants_merge.d.ts +4 -0
- package/build/constants_merge.js +85 -0
- package/build/copy.d.ts +8 -0
- package/build/copy.js +33 -0
- package/build/host.d.ts +15 -0
- package/build/host.js +85 -0
- package/build/registry/index.d.ts +26 -0
- package/build/registry/index.js +116 -0
- package/build/registry/plan.d.ts +53 -0
- package/build/registry/plan.js +159 -0
- package/build/schema.d.ts +136 -0
- package/build/schema.js +58 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- package/build/wire.d.ts +16 -0
- package/build/wire.js +162 -0
- package/package.json +51 -0
- package/registry/README.md +86 -0
- package/registry/account/contracts/avatar_storage.ts +7 -0
- package/registry/account/contracts/index.ts +7 -0
- package/registry/account/contracts/types.ts +23 -0
- package/registry/account/events.ts +14 -0
- package/registry/account/module.json +31 -0
- package/registry/account/options.ts +11 -0
- package/registry/account/service.ts +155 -0
- package/registry/account/stubs/adapters/account.ts +21 -0
- package/registry/account/stubs/adapters/drive_avatar_storage.ts +15 -0
- package/registry/account/stubs/controllers/account_controller.ts +190 -0
- package/registry/account/stubs/routes/account.ts +53 -0
- package/registry/account/stubs/validators/account.ts +37 -0
- package/registry/account/tests/account_service.spec.ts +132 -0
- package/registry/account/tests/fakes/memory_avatar_storage.ts +12 -0
- package/registry/api/envelope.ts +66 -0
- package/registry/api/exception.ts +38 -0
- package/registry/api/exception_handler.ts +54 -0
- package/registry/api/module.json +27 -0
- package/registry/api/provider.ts +129 -0
- package/registry/api/service.ts +88 -0
- package/registry/api/tests/emit_safe.spec.ts +107 -0
- package/registry/api/tests/respond.spec.ts +40 -0
- package/registry/auth/contracts/index.ts +18 -0
- package/registry/auth/contracts/password_reset_store.ts +21 -0
- package/registry/auth/contracts/session_manager.ts +19 -0
- package/registry/auth/contracts/types.ts +32 -0
- package/registry/auth/events.ts +21 -0
- package/registry/auth/module.json +45 -0
- package/registry/auth/options.ts +23 -0
- package/registry/auth/service.ts +150 -0
- package/registry/auth/stubs/adapters/auth.ts +21 -0
- package/registry/auth/stubs/adapters/lucid_access_token_session.ts +33 -0
- package/registry/auth/stubs/adapters/lucid_password_reset_store.ts +68 -0
- package/registry/auth/stubs/adapters/lucid_user_store.ts +86 -0
- package/registry/auth/stubs/adapters/map_user.ts +19 -0
- package/registry/auth/stubs/controllers/auth_controller.ts +126 -0
- package/registry/auth/stubs/migrations/add_pending_email_to_users_table.ts +17 -0
- package/registry/auth/stubs/migrations/create_password_resets_table.ts +27 -0
- package/registry/auth/stubs/migrations/create_users_table.ts +31 -0
- package/registry/auth/stubs/models/password_reset.ts +29 -0
- package/registry/auth/stubs/models/user.ts +99 -0
- package/registry/auth/stubs/providers/vine_provider.ts +43 -0
- package/registry/auth/stubs/routes/auth.ts +36 -0
- package/registry/auth/stubs/start/limiter.ts +17 -0
- package/registry/auth/stubs/validators/auth.ts +48 -0
- package/registry/auth/stubs/validators/rules/exists.ts +21 -0
- package/registry/auth/stubs/validators/rules/unique.ts +21 -0
- package/registry/auth/stubs/validators/rules/valid_password.ts +20 -0
- package/registry/auth/stubs/validators/rules/valid_token.ts +27 -0
- package/registry/auth/stubs/validators/vine.d.ts +10 -0
- package/registry/auth/tests/auth_service.spec.ts +145 -0
- package/registry/auth/tests/auth_update_password.spec.ts +99 -0
- package/registry/auth/tests/fakes/index.ts +3 -0
- package/registry/auth/tests/fakes/memory_password_reset_store.ts +71 -0
- package/registry/auth/tests/fakes/memory_session_manager.ts +23 -0
- package/registry/auth/tests/fakes/memory_user_store.ts +89 -0
- package/registry/constants/exceptions.ts +64 -0
- package/registry/constants/index.ts +12 -0
- package/registry/constants/responseCodes.ts +87 -0
- package/registry/contracts/index.ts +5 -0
- package/registry/contracts/user.ts +78 -0
- package/registry/creem/contracts/creem_client.ts +34 -0
- package/registry/creem/contracts/index.ts +8 -0
- package/registry/creem/contracts/types.ts +30 -0
- package/registry/creem/events.ts +9 -0
- package/registry/creem/module.json +28 -0
- package/registry/creem/service.ts +87 -0
- package/registry/creem/stubs/adapters/creem.ts +23 -0
- package/registry/creem/stubs/adapters/creem_io_client.ts +70 -0
- package/registry/creem/tests/fakes/fake_creem_client.ts +68 -0
- package/registry/notification/contracts/app_notification_store.ts +8 -0
- package/registry/notification/contracts/index.ts +9 -0
- package/registry/notification/contracts/mail_transport.ts +7 -0
- package/registry/notification/contracts/types.ts +32 -0
- package/registry/notification/module.json +23 -0
- package/registry/notification/options.ts +11 -0
- package/registry/notification/service.ts +103 -0
- package/registry/notification/stubs/adapters/adonis_mail_transport.ts +15 -0
- package/registry/notification/stubs/adapters/lucid_app_notification_store.ts +21 -0
- package/registry/notification/stubs/adapters/notification.ts +36 -0
- package/registry/notification/stubs/migrations/create_notifications_table.ts +23 -0
- package/registry/notification/stubs/models/app_notification.ts +45 -0
- package/registry/notification/tests/fakes/memory_app_notification_store.ts +19 -0
- package/registry/notification/tests/fakes/memory_mail_transport.ts +16 -0
- package/registry/notification/tests/notification_service.spec.ts +63 -0
- package/registry/registry.json +4 -0
- package/registry/subscription/contracts/index.ts +9 -0
- package/registry/subscription/contracts/subscription_store.ts +15 -0
- package/registry/subscription/contracts/types.ts +39 -0
- package/registry/subscription/events.ts +13 -0
- package/registry/subscription/module.json +31 -0
- package/registry/subscription/options.ts +14 -0
- package/registry/subscription/service.ts +140 -0
- package/registry/subscription/stubs/adapters/lucid_subscription_store.ts +69 -0
- package/registry/subscription/stubs/adapters/subscription.ts +21 -0
- package/registry/subscription/stubs/migrations/create_subscriptions_table.ts +26 -0
- package/registry/subscription/stubs/migrations/subscriptions_user_creem_unique.ts +19 -0
- package/registry/subscription/stubs/models/subscription.ts +45 -0
- package/registry/subscription/tests/billing_service.spec.ts +189 -0
- package/registry/subscription/tests/fakes/memory_subscription_store.ts +63 -0
- package/registry/types/api.ts +41 -0
- package/registry/types/constants.ts +7 -0
- package/registry/types/index.ts +6 -0
- package/registry/types/models.ts +32 -0
- package/schema/adonia.schema.json +39 -0
package/build/wire.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const REQUIRED_IMPORTS = {
|
|
4
|
+
'#modules/*': './modules/*.js',
|
|
5
|
+
'#modules/types': './modules/types/index.js',
|
|
6
|
+
'#modules/contracts': './modules/contracts/index.js',
|
|
7
|
+
'#constants': './modules/constants/index.js',
|
|
8
|
+
'#constants/*': './modules/constants/*.js',
|
|
9
|
+
'#adapters/*': './app/adapters/*.js',
|
|
10
|
+
};
|
|
11
|
+
const EMPTY_MODULES_CONFIG = `const modulesConfig: Record<string, { description: string; emits: string[] }> = {}
|
|
12
|
+
|
|
13
|
+
export default modulesConfig
|
|
14
|
+
`;
|
|
15
|
+
const EXCEPTION_HANDLER_REEXPORT = `export { default } from '#modules/api/exception_handler'
|
|
16
|
+
`;
|
|
17
|
+
export function printWireChecklist() {
|
|
18
|
+
console.log(`
|
|
19
|
+
Host wiring checklist (run \`init --wire\` to apply automatically):
|
|
20
|
+
1. Merge #modules/*, #modules/types, #modules/contracts,
|
|
21
|
+
#constants, #constants/*, #adapters/* into package.json "imports".
|
|
22
|
+
2. Append () => import('#modules/api/provider') to adonisrc.ts providers.
|
|
23
|
+
3. Set server.errorHandler(() => import('#modules/api/exception_handler')) in start/kernel.ts.
|
|
24
|
+
4. Create app/exceptions/handler.ts re-export of #modules/api/exception_handler.
|
|
25
|
+
5. Create config/modules.ts with an empty Record if missing.
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
export function applyWire(cwd, dryRun = false) {
|
|
29
|
+
const actions = [];
|
|
30
|
+
actions.push(...mergePackageImports(cwd, dryRun));
|
|
31
|
+
actions.push(...ensureProvider(cwd, dryRun));
|
|
32
|
+
actions.push(...ensureErrorHandler(cwd, dryRun));
|
|
33
|
+
actions.push(...ensureExceptionHandlerFile(cwd, dryRun));
|
|
34
|
+
actions.push(...ensureModulesConfig(cwd, dryRun));
|
|
35
|
+
return actions;
|
|
36
|
+
}
|
|
37
|
+
function mergePackageImports(cwd, dryRun) {
|
|
38
|
+
const pkgPath = join(cwd, 'package.json');
|
|
39
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
40
|
+
pkg.imports ??= {};
|
|
41
|
+
const added = [];
|
|
42
|
+
for (const [key, value] of Object.entries(REQUIRED_IMPORTS)) {
|
|
43
|
+
if (pkg.imports[key] === value)
|
|
44
|
+
continue;
|
|
45
|
+
if (!(key in pkg.imports)) {
|
|
46
|
+
pkg.imports[key] = value;
|
|
47
|
+
added.push(key);
|
|
48
|
+
}
|
|
49
|
+
else if (pkg.imports[key] !== value) {
|
|
50
|
+
// leave host override; only fill missing
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (added.length && !dryRun) {
|
|
55
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
56
|
+
}
|
|
57
|
+
return added.map((k) => `imports: ${k}`);
|
|
58
|
+
}
|
|
59
|
+
function ensureProvider(cwd, dryRun) {
|
|
60
|
+
const path = join(cwd, 'adonisrc.ts');
|
|
61
|
+
if (!existsSync(path))
|
|
62
|
+
return [];
|
|
63
|
+
let content = readFileSync(path, 'utf8');
|
|
64
|
+
if (content.includes('#modules/api/provider'))
|
|
65
|
+
return [];
|
|
66
|
+
const providerLine = ` () => import('#modules/api/provider'),`;
|
|
67
|
+
if (/providers:\s*\[/.test(content)) {
|
|
68
|
+
content = content.replace(/providers:\s*\[/, (m) => `${m}\n${providerLine}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
content += `\nexport const providers = [\n${providerLine}\n]\n`;
|
|
72
|
+
}
|
|
73
|
+
if (!dryRun)
|
|
74
|
+
writeFileSync(path, content);
|
|
75
|
+
return ['adonisrc.ts providers'];
|
|
76
|
+
}
|
|
77
|
+
function ensureErrorHandler(cwd, dryRun) {
|
|
78
|
+
const path = join(cwd, 'start/kernel.ts');
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
if (!dryRun) {
|
|
81
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
+
writeFileSync(path, `import server from '@adonisjs/core/services/server'\n\nserver.errorHandler(() => import('#modules/api/exception_handler'))\n`);
|
|
83
|
+
}
|
|
84
|
+
return ['start/kernel.ts (created)'];
|
|
85
|
+
}
|
|
86
|
+
const original = readFileSync(path, 'utf8');
|
|
87
|
+
if (original.includes('#modules/api/exception_handler'))
|
|
88
|
+
return [];
|
|
89
|
+
// Decide before appending the errorHandler line — that line contains
|
|
90
|
+
// `server.` and would otherwise hide a missing import.
|
|
91
|
+
const needsServerImport = !original.includes("from '@adonisjs/core/services/server'") &&
|
|
92
|
+
!original.includes('server.');
|
|
93
|
+
let content = original + `\nserver.errorHandler(() => import('#modules/api/exception_handler'))\n`;
|
|
94
|
+
if (needsServerImport) {
|
|
95
|
+
content = `import server from '@adonisjs/core/services/server'\n${content}`;
|
|
96
|
+
}
|
|
97
|
+
if (!dryRun)
|
|
98
|
+
writeFileSync(path, content);
|
|
99
|
+
return ['start/kernel.ts errorHandler'];
|
|
100
|
+
}
|
|
101
|
+
function ensureExceptionHandlerFile(cwd, dryRun) {
|
|
102
|
+
const path = join(cwd, 'app/exceptions/handler.ts');
|
|
103
|
+
if (existsSync(path)) {
|
|
104
|
+
const content = readFileSync(path, 'utf8');
|
|
105
|
+
const isDefault = content.includes('@adonisjs/core/exceptions') ||
|
|
106
|
+
content.trim().length < 40 ||
|
|
107
|
+
/export default class HttpExceptionHandler/.test(content);
|
|
108
|
+
if (!isDefault && content.includes('#modules/api/exception_handler'))
|
|
109
|
+
return [];
|
|
110
|
+
if (!isDefault)
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
if (!dryRun) {
|
|
114
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
115
|
+
writeFileSync(path, EXCEPTION_HANDLER_REEXPORT);
|
|
116
|
+
}
|
|
117
|
+
return ['app/exceptions/handler.ts'];
|
|
118
|
+
}
|
|
119
|
+
function ensureModulesConfig(cwd, dryRun) {
|
|
120
|
+
const path = join(cwd, 'config/modules.ts');
|
|
121
|
+
if (existsSync(path))
|
|
122
|
+
return [];
|
|
123
|
+
if (!dryRun) {
|
|
124
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
125
|
+
writeFileSync(path, EMPTY_MODULES_CONFIG);
|
|
126
|
+
}
|
|
127
|
+
return ['config/modules.ts'];
|
|
128
|
+
}
|
|
129
|
+
export { REQUIRED_IMPORTS, EMPTY_MODULES_CONFIG };
|
|
130
|
+
/**
|
|
131
|
+
* Append a side-effect import of `start/routes/<module>.ts` into
|
|
132
|
+
* `start/routes.ts`. Idempotent. Does not create the module route file.
|
|
133
|
+
*/
|
|
134
|
+
export function wireModuleRoute(cwd, moduleName, dryRun = false) {
|
|
135
|
+
const routeFile = join(cwd, 'start/routes', `${moduleName}.ts`);
|
|
136
|
+
if (!existsSync(routeFile)) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `Cannot --wire-routes for "${moduleName}": missing start/routes/${moduleName}.ts. ` +
|
|
140
|
+
`Pass --with-routes first, or create the file.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const routesPath = join(cwd, 'start/routes.ts');
|
|
144
|
+
const importRelative = `./routes/${moduleName}.js`;
|
|
145
|
+
const importLine = `import '${importRelative}'`;
|
|
146
|
+
let content = existsSync(routesPath) ? readFileSync(routesPath, 'utf8') : '';
|
|
147
|
+
if (content.includes(`'${importRelative}'`) ||
|
|
148
|
+
content.includes(`"${importRelative}"`) ||
|
|
149
|
+
content.includes(`'#start/routes/${moduleName}'`) ||
|
|
150
|
+
content.includes(`"#start/routes/${moduleName}"`)) {
|
|
151
|
+
return { ok: true, actions: [] };
|
|
152
|
+
}
|
|
153
|
+
content = content.length ? `${content.trimEnd()}\n${importLine}\n` : `${importLine}\n`;
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
mkdirSync(dirname(routesPath), { recursive: true });
|
|
156
|
+
writeFileSync(routesPath, content);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
actions: [`start/routes.ts → ${importRelative}`],
|
|
161
|
+
};
|
|
162
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adonia",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Adonia modules for AdonisJS \u2014 CLI and bundled registry",
|
|
7
|
+
"bin": {
|
|
8
|
+
"adonia": "./build/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./build/cli.js",
|
|
12
|
+
"./schema/adonia.schema.json": "./schema/adonia.schema.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"build",
|
|
16
|
+
"schema",
|
|
17
|
+
"registry"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"check-types": "tsc --noEmit",
|
|
26
|
+
"sync-registry": "node --import tsx scripts/sync-registry.ts",
|
|
27
|
+
"prepack": "npm run sync-registry && npm run build",
|
|
28
|
+
"test": "npm run sync-registry && npm run build && node --import tsx --test tests/*.test.ts",
|
|
29
|
+
"adonia": "node build/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^13.1.0",
|
|
33
|
+
"fast-glob": "^3.3.3",
|
|
34
|
+
"zod": "^3.24.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.13.10",
|
|
38
|
+
"tsx": "^4.19.3",
|
|
39
|
+
"typescript": "^5.9.2"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/britzdylan/adonia.git",
|
|
44
|
+
"directory": "packages/modules"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/britzdylan/adonia#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/britzdylan/adonia/issues"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT"
|
|
51
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Modules package (authoring fixture)
|
|
2
|
+
|
|
3
|
+
This directory is the **authoring source** for Adonia modules. It lives
|
|
4
|
+
inside a fixture Adonis app used to design and test slices. Hosts do
|
|
5
|
+
**not** copy from here — they run:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx adonia@latest init --wire
|
|
9
|
+
npx adonia@latest add auth
|
|
10
|
+
npx adonia@latest add auth --with-adapters
|
|
11
|
+
npx adonia@latest add auth --with-stubs
|
|
12
|
+
npx adonia@latest add auth --with-routes --wire-routes
|
|
13
|
+
# or full kit:
|
|
14
|
+
npx adonia@latest init --scaffold
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Local authors iterating on a module can point the CLI at this tree:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx adonia add auth --registry .
|
|
21
|
+
# or from the monorepo root:
|
|
22
|
+
node packages/modules/build/cli.js add auth \
|
|
23
|
+
--registry apps/adonis-api-stater/modules
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`npm run sync-registry -w adonia` copies this tree into
|
|
27
|
+
`packages/modules/registry/` for the npm tarball.
|
|
28
|
+
|
|
29
|
+
## Layout
|
|
30
|
+
|
|
31
|
+
| Path | Role |
|
|
32
|
+
|------|------|
|
|
33
|
+
| `types/`, `constants/`, `contracts/` | Shared core (always copy with `api`) |
|
|
34
|
+
| `api/` | Envelopes, `ApiService`, `ctx.respond`, exception handler |
|
|
35
|
+
| `auth/`, `account/`, `notification/`, `creem/`, `subscription/` | Feature modules |
|
|
36
|
+
|
|
37
|
+
Import path: `#modules/<name>/…` (for example `#modules/auth/service`).
|
|
38
|
+
|
|
39
|
+
Public names are listed in `registry.json`.
|
|
40
|
+
|
|
41
|
+
## `module.json` schema
|
|
42
|
+
|
|
43
|
+
Each feature module (and `api`) ships a manifest:
|
|
44
|
+
|
|
45
|
+
- **files** — domain slice copy set (relative to the module folder).
|
|
46
|
+
- **stubs** — host files under `stubs/` (`models`, `migrations`,
|
|
47
|
+
`controllers`, `validators`, `adapters`, `providers`, `start`,
|
|
48
|
+
`routes`). Opt-in via CLI `--with-*` flags; plain `add` does not copy
|
|
49
|
+
them. `--with-adapters` copies Lucid/SDK adapters to `app/adapters/`.
|
|
50
|
+
`--with-controllers` and `--with-stubs` imply adapters. `--with-validators`
|
|
51
|
+
also copies `providers/`; `--with-routes` and `--with-stubs` also copy
|
|
52
|
+
`start/` (limiter example). Adonis kits (`@adonisjs/mail`,
|
|
53
|
+
`@adonisjs/drive`, `@adonisjs/limiter`) are not stubbed with no-ops.
|
|
54
|
+
`add` and `check` warn when a copied stub needs a kit that is missing
|
|
55
|
+
from `package.json` and print `node ace add @adonisjs/<pkg>`.
|
|
56
|
+
- **dependencies** — npm packages this module imports.
|
|
57
|
+
- **registryDependencies** — other modules that must already be present.
|
|
58
|
+
- **peerModels** — `#models/*` aliases Lucid adapters expect.
|
|
59
|
+
- **configKeys** — keys under `config/modules.ts`.
|
|
60
|
+
- **env** — environment variables the host must provide.
|
|
61
|
+
- **events** — names to list in `config/modules.ts` `emits`.
|
|
62
|
+
- **hostFacingExceptions** — codes kept for middleware/hosts (see
|
|
63
|
+
`constants/exceptions.ts`).
|
|
64
|
+
- **hostTests** — integration specs that stay in the host `tests/` tree.
|
|
65
|
+
|
|
66
|
+
## Install checklist (manual / without CLI)
|
|
67
|
+
|
|
68
|
+
Prefer `npx adonia`. If installing by hand:
|
|
69
|
+
|
|
70
|
+
1. Aliases: `#modules/*`, `#modules/types`, `#constants`, `#adapters/*`.
|
|
71
|
+
2. Copy `api` + `types` + `constants` + `contracts`.
|
|
72
|
+
3. Register `#modules/api/provider` and `#modules/api/exception_handler`.
|
|
73
|
+
4. Copy feature modules in `registryDependencies` order.
|
|
74
|
+
5. Optionally copy stubs (`--with-stubs` / `--with-adapters` /
|
|
75
|
+
`--with-routes`) into `app/`, `app/adapters/`, `database/`,
|
|
76
|
+
`providers/`, `start/`, and `start/routes/`. Register
|
|
77
|
+
`providers/vine_provider.ts` in `adonisrc.ts`. If a copied stub
|
|
78
|
+
imports `@adonisjs/mail`, `@adonisjs/drive`, or `@adonisjs/limiter`,
|
|
79
|
+
run `node ace add` for that package (the CLI prints the command).
|
|
80
|
+
6. Fill `config/modules.ts` from each module's `events` list.
|
|
81
|
+
|
|
82
|
+
## Authoring a new module
|
|
83
|
+
|
|
84
|
+
See [Adding a feature module](../../../docs/architecture/adding-modules.md)
|
|
85
|
+
for the service API, contracts, events, stubs, and `module.json`
|
|
86
|
+
checklist.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account module input types.
|
|
3
|
+
* User persistence uses shared #modules/contracts UserStore / User.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type UpdateProfileInput = {
|
|
7
|
+
firstName?: string
|
|
8
|
+
lastName?: string
|
|
9
|
+
/**
|
|
10
|
+
* New avatar storage key after the host uploaded the file.
|
|
11
|
+
* Pass null to clear. Omit to leave unchanged.
|
|
12
|
+
*/
|
|
13
|
+
avatarKey?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ReplaceEmailInput = {
|
|
17
|
+
email: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ReplacePasswordInput = {
|
|
21
|
+
currentPassword: string
|
|
22
|
+
newPassword: string
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account module events. Import this file so the EventsList augmentation loads.
|
|
3
|
+
*/
|
|
4
|
+
import type { User } from '#modules/contracts/index'
|
|
5
|
+
|
|
6
|
+
declare module '@adonisjs/core/types' {
|
|
7
|
+
interface EventsList {
|
|
8
|
+
'Account:UpdateUserProfile': User
|
|
9
|
+
'Account:UpdateUserEmail': { user: User; newEmail: string }
|
|
10
|
+
'Account:ConfirmEmailChange': User
|
|
11
|
+
'Account:UpdateUserPassword': User
|
|
12
|
+
'Account:DeleteAccount': { userId: number }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "account",
|
|
3
|
+
"description": "Authenticated profile, email change, password change, account deletion",
|
|
4
|
+
"files": [
|
|
5
|
+
"service.ts",
|
|
6
|
+
"events.ts",
|
|
7
|
+
"options.ts",
|
|
8
|
+
"contracts/**",
|
|
9
|
+
"module.json"
|
|
10
|
+
],
|
|
11
|
+
"stubs": [
|
|
12
|
+
"stubs/controllers/account_controller.ts",
|
|
13
|
+
"stubs/validators/account.ts",
|
|
14
|
+
"stubs/adapters/**/*",
|
|
15
|
+
"stubs/routes/account.ts"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {},
|
|
18
|
+
"registryDependencies": ["api", "auth"],
|
|
19
|
+
"peerModels": ["#models/user"],
|
|
20
|
+
"configKeys": ["modules.account"],
|
|
21
|
+
"env": ["APP_KEY"],
|
|
22
|
+
"events": [
|
|
23
|
+
"Account:UpdateUserProfile",
|
|
24
|
+
"Account:UpdateUserEmail",
|
|
25
|
+
"Account:ConfirmEmailChange",
|
|
26
|
+
"Account:UpdateUserPassword",
|
|
27
|
+
"Account:DeleteAccount"
|
|
28
|
+
],
|
|
29
|
+
"hostFacingExceptions": ["PAYLOAD_TOO_LARGE"],
|
|
30
|
+
"hostTests": ["tests/integration/lucid_adapters.spec.ts"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-editable defaults for AccountService.
|
|
3
|
+
*/
|
|
4
|
+
export type AccountOptions = {
|
|
5
|
+
/** Duration passed to encryption.encrypt for pending-email tokens. */
|
|
6
|
+
emailChangeTtl: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const defaultAccountOptions: AccountOptions = {
|
|
10
|
+
emailChangeTtl: '2 Hours',
|
|
11
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccountService
|
|
3
|
+
*
|
|
4
|
+
* Authenticated account actions: profile, email, password, delete.
|
|
5
|
+
* Returns DTOs / void; controllers build HTTP envelopes.
|
|
6
|
+
*/
|
|
7
|
+
import encryption from '@adonisjs/core/services/encryption'
|
|
8
|
+
import ApiService from '#modules/api/service'
|
|
9
|
+
import ApiException from '#modules/api/exception'
|
|
10
|
+
import { exceptions } from '#constants/exceptions'
|
|
11
|
+
import type { User, UserStore } from '#modules/contracts/index'
|
|
12
|
+
import type {
|
|
13
|
+
AvatarStorage,
|
|
14
|
+
ReplaceEmailInput,
|
|
15
|
+
ReplacePasswordInput,
|
|
16
|
+
UpdateProfileInput,
|
|
17
|
+
} from '#modules/account/contracts/index'
|
|
18
|
+
import { defaultAccountOptions, type AccountOptions } from '#modules/account/options'
|
|
19
|
+
import '#modules/account/events'
|
|
20
|
+
|
|
21
|
+
const noopAvatars: AvatarStorage = {
|
|
22
|
+
async delete() {},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AccountService extends ApiService {
|
|
26
|
+
namespace = 'account'
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private users: UserStore,
|
|
30
|
+
private avatars: AvatarStorage = noopAvatars,
|
|
31
|
+
private options: AccountOptions = defaultAccountOptions
|
|
32
|
+
) {
|
|
33
|
+
super()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private invalidTokenException() {
|
|
37
|
+
return ApiException.from(exceptions.INVALID_TOKEN)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private tokensMatch(incoming: string, stored: string): boolean {
|
|
41
|
+
const storedToken = encryption.decrypt(stored)
|
|
42
|
+
return encryption.decrypt(incoming) === storedToken && storedToken != null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private issuePendingEmailToken(user: User, pendingEmail: string): User {
|
|
46
|
+
return {
|
|
47
|
+
...user,
|
|
48
|
+
pendingEmail,
|
|
49
|
+
emailVerificationToken: encryption.encrypt(pendingEmail, this.options.emailChangeTtl),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async updateProfile(userId: number, data: UpdateProfileInput): Promise<User> {
|
|
54
|
+
const user = await this.users.findOrFail(userId)
|
|
55
|
+
const previousAvatarKey = user.avatarKey
|
|
56
|
+
|
|
57
|
+
if (data.firstName !== undefined) {
|
|
58
|
+
user.firstName = data.firstName
|
|
59
|
+
}
|
|
60
|
+
if (data.lastName !== undefined) {
|
|
61
|
+
user.lastName = data.lastName
|
|
62
|
+
}
|
|
63
|
+
if (data.avatarKey !== undefined) {
|
|
64
|
+
user.avatarKey = data.avatarKey
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const saved = await this.users.save(user)
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
data.avatarKey !== undefined &&
|
|
71
|
+
previousAvatarKey &&
|
|
72
|
+
previousAvatarKey !== data.avatarKey
|
|
73
|
+
) {
|
|
74
|
+
await this.avatars.delete(previousAvatarKey)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.emitSafe('Account:UpdateUserProfile', saved)
|
|
78
|
+
return saved
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stages a new email on pendingEmail. Current email stays valid until
|
|
83
|
+
* confirmEmailChange succeeds.
|
|
84
|
+
*/
|
|
85
|
+
public async replaceEmail(userId: number, data: ReplaceEmailInput): Promise<User> {
|
|
86
|
+
const user = await this.users.findOrFail(userId)
|
|
87
|
+
|
|
88
|
+
if (user.email === data.email) {
|
|
89
|
+
throw ApiException.from(exceptions.SAME_EMAIL)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const existing = await this.users.findByEmail(data.email)
|
|
93
|
+
if (existing) {
|
|
94
|
+
throw ApiException.from(exceptions.EMAIL_EXISTS)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const updated = await this.users.save(this.issuePendingEmailToken(user, data.email))
|
|
98
|
+
|
|
99
|
+
this.emitSafe('Account:UpdateUserEmail', {
|
|
100
|
+
user: updated,
|
|
101
|
+
newEmail: data.email,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return updated
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Confirms a pending email change using the verification token.
|
|
109
|
+
*/
|
|
110
|
+
public async confirmEmailChange(token: string): Promise<User> {
|
|
111
|
+
const user = await this.users.findByVerificationToken(token)
|
|
112
|
+
if (!user || !user.pendingEmail) {
|
|
113
|
+
throw this.invalidTokenException()
|
|
114
|
+
}
|
|
115
|
+
if (!user.emailVerificationToken || !this.tokensMatch(token, user.emailVerificationToken)) {
|
|
116
|
+
throw this.invalidTokenException()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const taken = await this.users.findByEmail(user.pendingEmail)
|
|
120
|
+
if (taken && taken.id !== user.id) {
|
|
121
|
+
throw ApiException.from(exceptions.EMAIL_EXISTS)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const confirmed = await this.users.save({
|
|
125
|
+
...user,
|
|
126
|
+
email: user.pendingEmail,
|
|
127
|
+
pendingEmail: null,
|
|
128
|
+
emailVerifiedAt: new Date(),
|
|
129
|
+
emailVerificationToken: null,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
this.emitSafe('Account:ConfirmEmailChange', confirmed)
|
|
133
|
+
return confirmed
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async replacePassword(userId: number, data: ReplacePasswordInput): Promise<void> {
|
|
137
|
+
const user = await this.users.findOrFail(userId)
|
|
138
|
+
const valid = await this.users.verifyPassword(userId, data.currentPassword)
|
|
139
|
+
if (!valid) {
|
|
140
|
+
throw ApiException.from(exceptions.INVALID_PASSWORD)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await this.users.updatePassword(user, data.newPassword)
|
|
144
|
+
this.emitSafe('Account:UpdateUserPassword', user)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async deleteAccount(userId: number): Promise<void> {
|
|
148
|
+
const user = await this.users.findOrFail(userId)
|
|
149
|
+
if (user.avatarKey) {
|
|
150
|
+
await this.avatars.delete(user.avatarKey)
|
|
151
|
+
}
|
|
152
|
+
await this.users.delete(userId)
|
|
153
|
+
this.emitSafe('Account:DeleteAccount', { userId })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Lucid factory for account. Copied to app/adapters/account.ts.
|
|
3
|
+
* Import as `#adapters/account`.
|
|
4
|
+
*
|
|
5
|
+
* Reuses `#adapters/lucid_user_store` from the auth adapter stubs.
|
|
6
|
+
* Avatar cleanup uses Drive (`node ace add @adonisjs/drive`).
|
|
7
|
+
*/
|
|
8
|
+
import { AccountService } from '#modules/account/service'
|
|
9
|
+
import { LucidUserStore } from '#adapters/lucid_user_store'
|
|
10
|
+
import { DriveAvatarStorage } from './drive_avatar_storage.ts'
|
|
11
|
+
import { defaultAccountOptions, type AccountOptions } from '#modules/account/options'
|
|
12
|
+
import type { AvatarStorage } from '#modules/account/contracts/index'
|
|
13
|
+
|
|
14
|
+
export { DriveAvatarStorage } from './drive_avatar_storage.ts'
|
|
15
|
+
|
|
16
|
+
export function createAccountService(
|
|
17
|
+
avatars: AvatarStorage = new DriveAvatarStorage(),
|
|
18
|
+
options: AccountOptions = defaultAccountOptions
|
|
19
|
+
) {
|
|
20
|
+
return new AccountService(new LucidUserStore(), avatars, options)
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive-backed AvatarStorage. Copied to app/adapters/drive_avatar_storage.ts.
|
|
3
|
+
* Requires `@adonisjs/drive` (`node ace add @adonisjs/drive`).
|
|
4
|
+
*/
|
|
5
|
+
import drive from '@adonisjs/drive/services/main'
|
|
6
|
+
import type { AvatarStorage } from '#modules/account/contracts/index'
|
|
7
|
+
|
|
8
|
+
export class DriveAvatarStorage implements AvatarStorage {
|
|
9
|
+
constructor(private diskName?: string) {}
|
|
10
|
+
|
|
11
|
+
async delete(key: string): Promise<void> {
|
|
12
|
+
const disk = this.diskName ? drive.use(this.diskName) : drive.use()
|
|
13
|
+
await disk.delete(key)
|
|
14
|
+
}
|
|
15
|
+
}
|