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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types'
|
|
2
|
+
import { HttpContext } from '@adonisjs/core/http'
|
|
3
|
+
import { BaseSerializer } from '@adonisjs/core/transformers'
|
|
4
|
+
import { type SimplePaginatorMetaKeys } from '@adonisjs/lucid/types/querybuilder'
|
|
5
|
+
import {
|
|
6
|
+
isApiError,
|
|
7
|
+
isApiResponse,
|
|
8
|
+
isPaginatedResponse,
|
|
9
|
+
} from '#modules/api/envelope'
|
|
10
|
+
import type {
|
|
11
|
+
ApiResponse,
|
|
12
|
+
ApiResult,
|
|
13
|
+
PaginatedResponse,
|
|
14
|
+
} from '#modules/types/api'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom serializer for API responses that ensures consistent JSON structure
|
|
18
|
+
* across all API endpoints. Wraps response data in a 'data' property and handles
|
|
19
|
+
* pagination metadata for Lucid ORM query results.
|
|
20
|
+
*/
|
|
21
|
+
class ApiSerializer extends BaseSerializer<{
|
|
22
|
+
Wrap: 'data'
|
|
23
|
+
PaginationMetaData: SimplePaginatorMetaKeys
|
|
24
|
+
}> {
|
|
25
|
+
/**
|
|
26
|
+
* Wraps all serialized data under this key in the response object.
|
|
27
|
+
* Example: { data: [...] } instead of returning raw arrays/objects
|
|
28
|
+
*/
|
|
29
|
+
wrap: 'data' = 'data'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates and defines pagination metadata structure for paginated responses.
|
|
33
|
+
* Ensures that pagination info from Lucid queries is properly formatted.
|
|
34
|
+
*
|
|
35
|
+
* @throws Error if metadata doesn't match Lucid's pagination structure
|
|
36
|
+
*/
|
|
37
|
+
definePaginationMetaData(metaData: unknown): SimplePaginatorMetaKeys {
|
|
38
|
+
if (!this.isLucidPaginatorMetaData(metaData)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Invalid pagination metadata. Expected metadata to contain Lucid pagination keys'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return metaData
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Single instance of ApiSerializer used across the application
|
|
49
|
+
*/
|
|
50
|
+
const serializer = new ApiSerializer()
|
|
51
|
+
|
|
52
|
+
const serialize = Object.assign(
|
|
53
|
+
function (this: HttpContext, ...[data, resolver]: Parameters<ApiSerializer['serialize']>) {
|
|
54
|
+
return serializer.serialize(data, resolver ?? this.containerResolver)
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
withoutWrapping(
|
|
58
|
+
this: HttpContext,
|
|
59
|
+
...[data, resolver]: Parameters<ApiSerializer['serializeWithoutWrapping']>
|
|
60
|
+
) {
|
|
61
|
+
return serializer.serializeWithoutWrapping(data, resolver ?? this.containerResolver)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
) as ApiSerializer['serialize'] & { withoutWrapping: ApiSerializer['serializeWithoutWrapping'] }
|
|
65
|
+
|
|
66
|
+
type Respondable =
|
|
67
|
+
| ApiResponse<unknown>
|
|
68
|
+
| ApiResult<unknown>
|
|
69
|
+
| PaginatedResponse<unknown>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Bridges ApiService envelopes to HTTP JSON without double-wrapping `data`.
|
|
73
|
+
* Serializes the payload via transformers, then restores the shared envelope.
|
|
74
|
+
*/
|
|
75
|
+
export async function respond(this: HttpContext, result: Respondable) {
|
|
76
|
+
if (isApiError(result)) {
|
|
77
|
+
this.response.status(result.status ?? 400)
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isPaginatedResponse(result)) {
|
|
82
|
+
const data = await serializer.serializeWithoutWrapping(
|
|
83
|
+
result.data,
|
|
84
|
+
this.containerResolver
|
|
85
|
+
)
|
|
86
|
+
return {
|
|
87
|
+
success: true as const,
|
|
88
|
+
message: result.message,
|
|
89
|
+
data,
|
|
90
|
+
meta: result.meta,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isApiResponse(result)) {
|
|
95
|
+
const data = await serializer.serializeWithoutWrapping(
|
|
96
|
+
result.data,
|
|
97
|
+
this.containerResolver
|
|
98
|
+
)
|
|
99
|
+
return {
|
|
100
|
+
success: true as const,
|
|
101
|
+
message: result.message,
|
|
102
|
+
data,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Registers HTTP helpers used by all API controllers.
|
|
111
|
+
*/
|
|
112
|
+
export default class ApiProvider {
|
|
113
|
+
constructor(protected app: ApplicationService) {}
|
|
114
|
+
|
|
115
|
+
async boot() {
|
|
116
|
+
HttpContext.instanceProperty('serialize', serialize)
|
|
117
|
+
HttpContext.instanceProperty('respond', respond)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Module augmentation for HttpContext API helpers.
|
|
123
|
+
*/
|
|
124
|
+
declare module '@adonisjs/core/http' {
|
|
125
|
+
export interface HttpContext {
|
|
126
|
+
serialize: typeof serialize
|
|
127
|
+
respond: typeof respond
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Domain base for feature modules.
|
|
3
|
+
*
|
|
4
|
+
* Envelope helpers (prepareResponse / prepareError / preparePaginatedResponse)
|
|
5
|
+
* are for controllers and hosts — domain services under modules/ return DTOs
|
|
6
|
+
* and throw ApiException instead of wrapping results.
|
|
7
|
+
*
|
|
8
|
+
* HTTP serialization lives on HttpContext via the api provider (ctx.respond).
|
|
9
|
+
*/
|
|
10
|
+
import emitter from '@adonisjs/core/services/emitter'
|
|
11
|
+
import logger from '@adonisjs/core/services/logger'
|
|
12
|
+
import config from '@adonisjs/core/services/config'
|
|
13
|
+
import type { EventsList } from '@adonisjs/core/types'
|
|
14
|
+
import { ModelsTypes, ApiTypes } from '#modules/types'
|
|
15
|
+
import { prepareError } from '#modules/api/envelope'
|
|
16
|
+
|
|
17
|
+
export default class ApiService {
|
|
18
|
+
namespace: ModelsTypes.NameSpace = null
|
|
19
|
+
|
|
20
|
+
resolveConfig() {
|
|
21
|
+
if (!this.namespace) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
return config.get<ModelsTypes.ModuleActionConfig>(`modules.${this.namespace}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a success envelope. Prefer calling this from controllers/hosts,
|
|
29
|
+
* not from domain services under modules/.
|
|
30
|
+
*/
|
|
31
|
+
prepareResponse<T>(data: T, message: string | null = null): ApiTypes.ApiResponse<T> {
|
|
32
|
+
return {
|
|
33
|
+
data,
|
|
34
|
+
message,
|
|
35
|
+
success: true,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
prepareError(message: string, status?: number): ApiTypes.ApiResult<never> {
|
|
40
|
+
return prepareError(message, status)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a paginated success envelope with meta. Controllers/hosts only.
|
|
45
|
+
*/
|
|
46
|
+
preparePaginatedResponse<T>(
|
|
47
|
+
data: T[],
|
|
48
|
+
total: number,
|
|
49
|
+
params: ApiTypes.PaginationParams = {}
|
|
50
|
+
): ApiTypes.PaginatedResponse<T> {
|
|
51
|
+
const page = params.page ?? 1
|
|
52
|
+
const pageSize = params.pageSize ?? data.length
|
|
53
|
+
return {
|
|
54
|
+
data,
|
|
55
|
+
message: null,
|
|
56
|
+
success: true,
|
|
57
|
+
meta: {
|
|
58
|
+
total,
|
|
59
|
+
page,
|
|
60
|
+
pageSize,
|
|
61
|
+
totalPages: pageSize > 0 ? Math.ceil(total / pageSize) : 0,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Emit a typed domain event when listed in config/modules.ts.
|
|
68
|
+
* Types come from EventsList augmentations imported by each feature module.
|
|
69
|
+
*/
|
|
70
|
+
public async emitSafe<K extends keyof EventsList>(
|
|
71
|
+
eventName: K,
|
|
72
|
+
payload: EventsList[K]
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const actionConfig = this.resolveConfig()
|
|
75
|
+
if (!actionConfig) {
|
|
76
|
+
logger.warn(`No modules.${this.namespace} config; skipping ${String(eventName)}`)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (!actionConfig.emits.includes(eventName as string)) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await emitter.emit(eventName, payload)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error(`Event emit failed for ${String(eventName)}: %o`, err)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import emitter from '@adonisjs/core/services/emitter'
|
|
4
|
+
import config from '@adonisjs/core/services/config'
|
|
5
|
+
import { AuthService } from '#modules/auth/service'
|
|
6
|
+
import type { User, UserStore } from '#modules/contracts/index'
|
|
7
|
+
import type { PasswordResetStore } from '#modules/auth/contracts/index'
|
|
8
|
+
import '#modules/auth/events'
|
|
9
|
+
|
|
10
|
+
function makeUser(): User {
|
|
11
|
+
return {
|
|
12
|
+
id: 1,
|
|
13
|
+
email: 'user@test.com',
|
|
14
|
+
pendingEmail: null,
|
|
15
|
+
authMethod: 'email',
|
|
16
|
+
firstName: 'Test',
|
|
17
|
+
lastName: 'User',
|
|
18
|
+
avatarKey: null,
|
|
19
|
+
emailVerifiedAt: new Date(),
|
|
20
|
+
emailVerificationToken: null,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeUserStore(): UserStore {
|
|
25
|
+
return {
|
|
26
|
+
createEmailUser: async () => makeUser(),
|
|
27
|
+
findOrFail: async () => makeUser(),
|
|
28
|
+
findByEmail: async () => null,
|
|
29
|
+
findByEmailOrFail: async () => makeUser(),
|
|
30
|
+
findByVerificationToken: async () => null,
|
|
31
|
+
verifyCredentials: async () => makeUser(),
|
|
32
|
+
verifyPassword: async () => true,
|
|
33
|
+
save: async (u) => u,
|
|
34
|
+
updatePassword: async (u) => u,
|
|
35
|
+
delete: async () => {},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makePasswordResetStore(): PasswordResetStore {
|
|
40
|
+
return {
|
|
41
|
+
createForUser: async () => ({ id: 1, userId: 1, token: 't', isExpired: false }),
|
|
42
|
+
findByToken: async () => null,
|
|
43
|
+
deleteForUser: async () => {},
|
|
44
|
+
refreshToken: async (r) => r,
|
|
45
|
+
findUserByReset: async () => makeUser(),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test.group('ApiService emitSafe', () => {
|
|
50
|
+
test('emits listed events and skips unlisted ones', async () => {
|
|
51
|
+
const service = new AuthService(makeUserStore(), makePasswordResetStore())
|
|
52
|
+
const emitted: string[] = []
|
|
53
|
+
const originalEmit = emitter.emit.bind(emitter)
|
|
54
|
+
const originalGet = config.get.bind(config)
|
|
55
|
+
|
|
56
|
+
;(emitter as any).emit = async (event: string, payload: unknown) => {
|
|
57
|
+
emitted.push(event)
|
|
58
|
+
return originalEmit(event as any, payload as any)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
;(config as any).get = (key: string) => {
|
|
62
|
+
if (key === 'modules.auth') {
|
|
63
|
+
return {
|
|
64
|
+
description: 'test',
|
|
65
|
+
emits: ['Auth:Logout'],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return originalGet(key)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await service.emitSafe('Auth:Logout', null)
|
|
73
|
+
await service.emitSafe('Auth:Login', { user: makeUser() })
|
|
74
|
+
assert.deepEqual(emitted, ['Auth:Logout'])
|
|
75
|
+
} finally {
|
|
76
|
+
;(emitter as any).emit = originalEmit
|
|
77
|
+
;(config as any).get = originalGet
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('resolves when a listener throws', async () => {
|
|
82
|
+
const service = new AuthService(makeUserStore(), makePasswordResetStore())
|
|
83
|
+
const originalEmit = emitter.emit.bind(emitter)
|
|
84
|
+
const originalGet = config.get.bind(config)
|
|
85
|
+
|
|
86
|
+
;(config as any).get = (key: string) => {
|
|
87
|
+
if (key === 'modules.auth') {
|
|
88
|
+
return {
|
|
89
|
+
description: 'test',
|
|
90
|
+
emits: ['Auth:Logout'],
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return originalGet(key)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
;(emitter as any).emit = async () => {
|
|
97
|
+
throw new Error('listener boom')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await service.emitSafe('Auth:Logout', null)
|
|
102
|
+
} finally {
|
|
103
|
+
;(emitter as any).emit = originalEmit
|
|
104
|
+
;(config as any).get = originalGet
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
4
|
+
import { respond } from '#modules/api/provider'
|
|
5
|
+
import { prepareError } from '#modules/api/envelope'
|
|
6
|
+
|
|
7
|
+
test.group('ctx.respond error status', () => {
|
|
8
|
+
test('sets HTTP status from ApiErrorBody', async () => {
|
|
9
|
+
let statusCode = 200
|
|
10
|
+
const ctx = {
|
|
11
|
+
response: {
|
|
12
|
+
status(code: number) {
|
|
13
|
+
statusCode = code
|
|
14
|
+
return this
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
} as unknown as HttpContext
|
|
18
|
+
|
|
19
|
+
const body = prepareError('E_X', 409)
|
|
20
|
+
const result = await respond.call(ctx, body)
|
|
21
|
+
|
|
22
|
+
assert.equal(statusCode, 409)
|
|
23
|
+
assert.deepEqual(result, { success: false, message: 'E_X', status: 409 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('defaults to 400 when status is omitted', async () => {
|
|
27
|
+
let statusCode = 200
|
|
28
|
+
const ctx = {
|
|
29
|
+
response: {
|
|
30
|
+
status(code: number) {
|
|
31
|
+
statusCode = code
|
|
32
|
+
return this
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} as unknown as HttpContext
|
|
36
|
+
|
|
37
|
+
await respond.call(ctx, prepareError('E_X'))
|
|
38
|
+
assert.equal(statusCode, 400)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module contracts.
|
|
3
|
+
* Shared User / UserStore: #modules/contracts
|
|
4
|
+
*/
|
|
5
|
+
export type {
|
|
6
|
+
AuthPasswordReset,
|
|
7
|
+
LoginCredentials,
|
|
8
|
+
LoginResult,
|
|
9
|
+
UpdatePasswordInput,
|
|
10
|
+
TokenDuration,
|
|
11
|
+
SessionResult,
|
|
12
|
+
} from './types.ts'
|
|
13
|
+
|
|
14
|
+
export type { PasswordResetStore } from './password_reset_store.ts'
|
|
15
|
+
export type { SessionManager } from './session_manager.ts'
|
|
16
|
+
|
|
17
|
+
/** Re-export shared user port for convenience inside the auth module. */
|
|
18
|
+
export type { AuthMethod, User, RegisterUserInput, UserStore } from '#modules/contracts/index'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { User } from '#modules/contracts/index'
|
|
2
|
+
import type { AuthPasswordReset, TokenDuration } from './types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Host must implement this for password-reset flows.
|
|
6
|
+
*/
|
|
7
|
+
export interface PasswordResetStore {
|
|
8
|
+
/** Invalidate existing reset rows for the user, then create a new token. */
|
|
9
|
+
createForUser(userId: number, duration: TokenDuration): Promise<AuthPasswordReset>
|
|
10
|
+
|
|
11
|
+
findByToken(token: string): Promise<AuthPasswordReset | null>
|
|
12
|
+
|
|
13
|
+
deleteForUser(userId: number): Promise<void>
|
|
14
|
+
|
|
15
|
+
/** Rotate/replace the token with a new expiry window. */
|
|
16
|
+
refreshToken(reset: AuthPasswordReset, duration: TokenDuration): Promise<AuthPasswordReset>
|
|
17
|
+
|
|
18
|
+
findUserByReset(reset: AuthPasswordReset): Promise<User>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type { AuthPasswordReset, TokenDuration }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { User } from '#modules/contracts/index'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of opening a session. Access-token hosts return token + expiry;
|
|
5
|
+
* cookie hosts may return {}.
|
|
6
|
+
*/
|
|
7
|
+
export type SessionResult = {
|
|
8
|
+
token?: string
|
|
9
|
+
expiresAt?: Date
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Host HTTP/session (or access-token) bridge.
|
|
14
|
+
* Controllers pass an implementation; AuthService stays HTTP-free.
|
|
15
|
+
*/
|
|
16
|
+
export interface SessionManager {
|
|
17
|
+
login(user: User): Promise<SessionResult>
|
|
18
|
+
logout(): Promise<void>
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth-only DTOs and input types.
|
|
3
|
+
* Shared User / UserStore live in #modules/contracts.
|
|
4
|
+
*/
|
|
5
|
+
import type { User } from '#modules/contracts/index'
|
|
6
|
+
import type { SessionResult } from './session_manager.ts'
|
|
7
|
+
|
|
8
|
+
export type AuthPasswordReset = {
|
|
9
|
+
id: number
|
|
10
|
+
userId: number
|
|
11
|
+
token: string
|
|
12
|
+
isExpired: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type LoginCredentials = {
|
|
16
|
+
email: string
|
|
17
|
+
password: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type UpdatePasswordInput = {
|
|
21
|
+
token: string
|
|
22
|
+
password: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TokenDuration = { hours: number } | { minutes: number }
|
|
26
|
+
|
|
27
|
+
export type { SessionResult }
|
|
28
|
+
|
|
29
|
+
export type LoginResult = {
|
|
30
|
+
user: User
|
|
31
|
+
session: SessionResult
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module events. Import this file so the EventsList augmentation loads.
|
|
3
|
+
*/
|
|
4
|
+
import type { User } from '#modules/contracts/index'
|
|
5
|
+
|
|
6
|
+
type RequestPasswordReset = {
|
|
7
|
+
user: User
|
|
8
|
+
token: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '@adonisjs/core/types' {
|
|
12
|
+
interface EventsList {
|
|
13
|
+
'Auth:RegisterUser': User
|
|
14
|
+
'Auth:CreateNewVerificationToken': User
|
|
15
|
+
'Auth:ActivateUserAccount': User
|
|
16
|
+
'Auth:RequestPasswordReset': RequestPasswordReset
|
|
17
|
+
'Auth:ResetPassword': { user: User }
|
|
18
|
+
'Auth:Login': { user: User }
|
|
19
|
+
'Auth:Logout': null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"description": "Email registration, login, verification, password reset",
|
|
4
|
+
"files": [
|
|
5
|
+
"service.ts",
|
|
6
|
+
"events.ts",
|
|
7
|
+
"options.ts",
|
|
8
|
+
"contracts/**",
|
|
9
|
+
"module.json"
|
|
10
|
+
],
|
|
11
|
+
"stubs": [
|
|
12
|
+
"stubs/models/user.ts",
|
|
13
|
+
"stubs/models/password_reset.ts",
|
|
14
|
+
"stubs/migrations/*.ts",
|
|
15
|
+
"stubs/controllers/auth_controller.ts",
|
|
16
|
+
"stubs/validators/**/*",
|
|
17
|
+
"stubs/providers/**/*",
|
|
18
|
+
"stubs/start/**/*",
|
|
19
|
+
"stubs/adapters/**/*",
|
|
20
|
+
"stubs/routes/auth.ts"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {},
|
|
23
|
+
"registryDependencies": ["api"],
|
|
24
|
+
"peerModels": ["#models/user", "#models/password_reset"],
|
|
25
|
+
"configKeys": ["modules.auth"],
|
|
26
|
+
"env": ["APP_KEY"],
|
|
27
|
+
"events": [
|
|
28
|
+
"Auth:RegisterUser",
|
|
29
|
+
"Auth:CreateNewVerificationToken",
|
|
30
|
+
"Auth:ActivateUserAccount",
|
|
31
|
+
"Auth:RequestPasswordReset",
|
|
32
|
+
"Auth:ResetPassword",
|
|
33
|
+
"Auth:Login",
|
|
34
|
+
"Auth:Logout"
|
|
35
|
+
],
|
|
36
|
+
"hostFacingExceptions": [
|
|
37
|
+
"INVALID_CREDENTIALS",
|
|
38
|
+
"UNAUTHENTICATED",
|
|
39
|
+
"SESSION_EXPIRED",
|
|
40
|
+
"FORBIDDEN",
|
|
41
|
+
"TOO_MANY_ATTEMPTS",
|
|
42
|
+
"TOO_MANY_REQUESTS"
|
|
43
|
+
],
|
|
44
|
+
"hostTests": ["tests/integration/lucid_adapters.spec.ts"]
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TokenDuration } from '#modules/auth/contracts/index'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host-editable defaults for AuthService.
|
|
5
|
+
* Do not read env here — pass values in when constructing the service.
|
|
6
|
+
*/
|
|
7
|
+
export type AuthOptions = {
|
|
8
|
+
/** Duration passed to encryption.encrypt for email verification tokens. */
|
|
9
|
+
verificationTtl: string
|
|
10
|
+
/** Lifetime of a newly requested password-reset token. */
|
|
11
|
+
passwordResetTtl: TokenDuration
|
|
12
|
+
/** Lifetime after validatePasswordResetToken rotates the token. */
|
|
13
|
+
passwordResetValidateTtl: TokenDuration
|
|
14
|
+
/** Byte length for string.generateRandom in LucidPasswordResetStore. */
|
|
15
|
+
passwordResetTokenBytes: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const defaultAuthOptions: AuthOptions = {
|
|
19
|
+
verificationTtl: '2 Hours',
|
|
20
|
+
passwordResetTtl: { hours: 2 },
|
|
21
|
+
passwordResetValidateTtl: { minutes: 5 },
|
|
22
|
+
passwordResetTokenBytes: 40,
|
|
23
|
+
}
|