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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthService
|
|
3
|
+
*
|
|
4
|
+
* Domain auth use-cases. Depends on shared UserStore + auth-only contracts.
|
|
5
|
+
* Returns DTOs / void; controllers build HTTP envelopes and call ctx.respond.
|
|
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 { RegisterUserInput, User, UserStore } from '#modules/contracts/index'
|
|
12
|
+
import type {
|
|
13
|
+
AuthPasswordReset,
|
|
14
|
+
LoginCredentials,
|
|
15
|
+
LoginResult,
|
|
16
|
+
PasswordResetStore,
|
|
17
|
+
SessionManager,
|
|
18
|
+
UpdatePasswordInput,
|
|
19
|
+
} from '#modules/auth/contracts/index'
|
|
20
|
+
import { defaultAuthOptions, type AuthOptions } from '#modules/auth/options'
|
|
21
|
+
import '#modules/auth/events'
|
|
22
|
+
|
|
23
|
+
export class AuthService extends ApiService {
|
|
24
|
+
namespace = 'auth'
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private users: UserStore,
|
|
28
|
+
private passwordResets: PasswordResetStore,
|
|
29
|
+
private options: AuthOptions = defaultAuthOptions
|
|
30
|
+
) {
|
|
31
|
+
super()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private accountUnverifiedException() {
|
|
35
|
+
return ApiException.from(exceptions.ACCOUNT_UNVERIFIED)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private invalidTokenException() {
|
|
39
|
+
return ApiException.from(exceptions.INVALID_TOKEN)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private tokensMatch(incoming: string, stored: string): boolean {
|
|
43
|
+
const storedToken = encryption.decrypt(stored)
|
|
44
|
+
return encryption.decrypt(incoming) === storedToken && storedToken != null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private issueVerificationToken(user: User, email = user.email): User {
|
|
48
|
+
return {
|
|
49
|
+
...user,
|
|
50
|
+
emailVerificationToken: encryption.encrypt(email, this.options.verificationTtl),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private markVerified(user: User): User {
|
|
55
|
+
return {
|
|
56
|
+
...user,
|
|
57
|
+
emailVerifiedAt: new Date(),
|
|
58
|
+
emailVerificationToken: null,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async registerUser(data: RegisterUserInput): Promise<User> {
|
|
63
|
+
let user = await this.users.createEmailUser(data)
|
|
64
|
+
user = await this.users.save(this.issueVerificationToken(user))
|
|
65
|
+
this.emitSafe('Auth:RegisterUser', user)
|
|
66
|
+
return user
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async createNewVerificationToken(user: User): Promise<User> {
|
|
70
|
+
const updated = await this.users.save(this.issueVerificationToken(user))
|
|
71
|
+
this.emitSafe('Auth:CreateNewVerificationToken', updated)
|
|
72
|
+
return updated
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Authenticates and opens a session. Throws E_ACCOUNT_UNVERIFIED with no
|
|
77
|
+
* side effects when the account is unverified — hosts that want a resend
|
|
78
|
+
* call createNewVerificationToken behind their own rate limit.
|
|
79
|
+
*/
|
|
80
|
+
public async loginByEmail(
|
|
81
|
+
data: LoginCredentials,
|
|
82
|
+
session: SessionManager
|
|
83
|
+
): Promise<LoginResult> {
|
|
84
|
+
const user = await this.users.verifyCredentials(data.email, data.password)
|
|
85
|
+
if (user.emailVerifiedAt === null) {
|
|
86
|
+
throw this.accountUnverifiedException()
|
|
87
|
+
}
|
|
88
|
+
const sessionResult = await session.login(user)
|
|
89
|
+
this.emitSafe('Auth:Login', { user })
|
|
90
|
+
return { user, session: sessionResult }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async activateUserAccount(token: string): Promise<User> {
|
|
94
|
+
const user = await this.users.findByVerificationToken(token)
|
|
95
|
+
if (!user) {
|
|
96
|
+
throw this.invalidTokenException()
|
|
97
|
+
}
|
|
98
|
+
if (!user.emailVerificationToken || !this.tokensMatch(token, user.emailVerificationToken)) {
|
|
99
|
+
throw this.invalidTokenException()
|
|
100
|
+
}
|
|
101
|
+
const verified = await this.users.save(this.markVerified(user))
|
|
102
|
+
this.emitSafe('Auth:ActivateUserAccount', verified)
|
|
103
|
+
return verified
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public async requestPasswordReset(email: string): Promise<void> {
|
|
107
|
+
const user = await this.users.findByEmailOrFail(email)
|
|
108
|
+
const reset = await this.passwordResets.createForUser(
|
|
109
|
+
user.id,
|
|
110
|
+
this.options.passwordResetTtl
|
|
111
|
+
)
|
|
112
|
+
this.emitSafe('Auth:RequestPasswordReset', {
|
|
113
|
+
user,
|
|
114
|
+
token: reset.token,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public async validatePasswordResetToken(token: string): Promise<AuthPasswordReset> {
|
|
119
|
+
const passwordReset = await this.passwordResets.findByToken(token)
|
|
120
|
+
if (!passwordReset || passwordReset.isExpired) {
|
|
121
|
+
throw this.invalidTokenException()
|
|
122
|
+
}
|
|
123
|
+
return this.passwordResets.refreshToken(
|
|
124
|
+
passwordReset,
|
|
125
|
+
this.options.passwordResetValidateTtl
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async updatePassword(
|
|
130
|
+
data: UpdatePasswordInput,
|
|
131
|
+
session?: SessionManager
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const passwordReset = await this.passwordResets.findByToken(data.token)
|
|
134
|
+
if (!passwordReset || passwordReset.isExpired) {
|
|
135
|
+
throw this.invalidTokenException()
|
|
136
|
+
}
|
|
137
|
+
const user = await this.passwordResets.findUserByReset(passwordReset)
|
|
138
|
+
if (session) {
|
|
139
|
+
await session.logout()
|
|
140
|
+
}
|
|
141
|
+
await this.users.updatePassword(user, data.password)
|
|
142
|
+
await this.passwordResets.deleteForUser(user.id)
|
|
143
|
+
this.emitSafe('Auth:ResetPassword', { user })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async logout(session: SessionManager): Promise<void> {
|
|
147
|
+
await session.logout()
|
|
148
|
+
this.emitSafe('Auth:Logout', null)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Lucid factory for auth. Copied to app/adapters/auth.ts.
|
|
3
|
+
* Import as `#adapters/auth`.
|
|
4
|
+
*/
|
|
5
|
+
import { AuthService } from '#modules/auth/service'
|
|
6
|
+
import { LucidUserStore } from './lucid_user_store.ts'
|
|
7
|
+
import { LucidPasswordResetStore } from './lucid_password_reset_store.ts'
|
|
8
|
+
import { defaultAuthOptions, type AuthOptions } from '#modules/auth/options'
|
|
9
|
+
|
|
10
|
+
export { LucidUserStore } from './lucid_user_store.ts'
|
|
11
|
+
export { LucidPasswordResetStore } from './lucid_password_reset_store.ts'
|
|
12
|
+
export { LucidAccessTokenSession } from './lucid_access_token_session.ts'
|
|
13
|
+
export { toUser } from './map_user.ts'
|
|
14
|
+
|
|
15
|
+
export function createAuthService(options: AuthOptions = defaultAuthOptions) {
|
|
16
|
+
return new AuthService(
|
|
17
|
+
new LucidUserStore(),
|
|
18
|
+
new LucidPasswordResetStore(options.passwordResetTokenBytes),
|
|
19
|
+
options
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager using Adonis access tokens.
|
|
3
|
+
* Copied to app/adapters/lucid_access_token_session.ts.
|
|
4
|
+
*
|
|
5
|
+
* Controllers construct this per request and pass it into AuthService.
|
|
6
|
+
*/
|
|
7
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
8
|
+
import UserModel from '#models/user'
|
|
9
|
+
import type { User } from '#modules/contracts/index'
|
|
10
|
+
import type {
|
|
11
|
+
SessionManager,
|
|
12
|
+
SessionResult,
|
|
13
|
+
} from '#modules/auth/contracts/index'
|
|
14
|
+
|
|
15
|
+
export class LucidAccessTokenSession implements SessionManager {
|
|
16
|
+
constructor(private ctx: HttpContext) {}
|
|
17
|
+
|
|
18
|
+
async login(user: User): Promise<SessionResult> {
|
|
19
|
+
const model = await UserModel.findOrFail(user.id)
|
|
20
|
+
const token = await UserModel.accessTokens.create(model)
|
|
21
|
+
return {
|
|
22
|
+
token: token.value!.release(),
|
|
23
|
+
expiresAt: token.expiresAt ?? undefined,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async logout(): Promise<void> {
|
|
28
|
+
const user = this.ctx.auth.getUserOrFail()
|
|
29
|
+
if ('currentAccessToken' in user && user.currentAccessToken) {
|
|
30
|
+
await UserModel.accessTokens.delete(user, user.currentAccessToken.identifier)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lucid PasswordResetStore. Copied to app/adapters/lucid_password_reset_store.ts.
|
|
3
|
+
* Expects PasswordReset at #models/password_reset with userId, token, expiresAt,
|
|
4
|
+
* and a belongsTo user relation.
|
|
5
|
+
*/
|
|
6
|
+
import { DateTime } from 'luxon'
|
|
7
|
+
import string from '@adonisjs/core/helpers/string'
|
|
8
|
+
import encryption from '@adonisjs/core/services/encryption'
|
|
9
|
+
import PasswordReset from '#models/password_reset'
|
|
10
|
+
import { toUser } from './map_user.ts'
|
|
11
|
+
import { defaultAuthOptions } from '#modules/auth/options'
|
|
12
|
+
import type { User } from '#modules/contracts/index'
|
|
13
|
+
import type {
|
|
14
|
+
AuthPasswordReset,
|
|
15
|
+
PasswordResetStore,
|
|
16
|
+
TokenDuration,
|
|
17
|
+
} from '#modules/auth/contracts/index'
|
|
18
|
+
|
|
19
|
+
export class LucidPasswordResetStore implements PasswordResetStore {
|
|
20
|
+
constructor(private tokenBytes = defaultAuthOptions.passwordResetTokenBytes) {}
|
|
21
|
+
|
|
22
|
+
private toAuthPasswordReset(row: PasswordReset): AuthPasswordReset {
|
|
23
|
+
return {
|
|
24
|
+
id: row.id,
|
|
25
|
+
userId: row.userId,
|
|
26
|
+
token: row.token,
|
|
27
|
+
isExpired: row.expiresAt.toMillis() < DateTime.now().toMillis(),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private applyToken(row: PasswordReset, duration: TokenDuration) {
|
|
32
|
+
row.token = encryption.encrypt(string.generateRandom(this.tokenBytes))
|
|
33
|
+
row.expiresAt = DateTime.now().plus(duration)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async createForUser(userId: number, duration: TokenDuration): Promise<AuthPasswordReset> {
|
|
37
|
+
await this.deleteForUser(userId)
|
|
38
|
+
const row = new PasswordReset()
|
|
39
|
+
row.userId = userId
|
|
40
|
+
this.applyToken(row, duration)
|
|
41
|
+
await row.save()
|
|
42
|
+
return this.toAuthPasswordReset(row)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async findByToken(token: string): Promise<AuthPasswordReset | null> {
|
|
46
|
+
const row = await PasswordReset.findBy('token', token)
|
|
47
|
+
return row ? this.toAuthPasswordReset(row) : null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async deleteForUser(userId: number): Promise<void> {
|
|
51
|
+
await PasswordReset.query().where('user_id', userId).delete()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async refreshToken(
|
|
55
|
+
reset: AuthPasswordReset,
|
|
56
|
+
duration: TokenDuration
|
|
57
|
+
): Promise<AuthPasswordReset> {
|
|
58
|
+
const row = await PasswordReset.findOrFail(reset.id)
|
|
59
|
+
this.applyToken(row, duration)
|
|
60
|
+
await row.save()
|
|
61
|
+
return this.toAuthPasswordReset(row)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async findUserByReset(reset: AuthPasswordReset): Promise<User> {
|
|
65
|
+
const row = await PasswordReset.query().where('id', reset.id).preload('user').firstOrFail()
|
|
66
|
+
return toUser(row.user)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Lucid UserStore. Copied to app/adapters/lucid_user_store.ts.
|
|
3
|
+
* Import as `#adapters/lucid_user_store`. Auth and account both use this file.
|
|
4
|
+
*
|
|
5
|
+
* Expects a User model at #models/user with email, password, authMethod,
|
|
6
|
+
* firstName, lastName, avatarKey, emailVerifiedAt, and emailVerificationToken.
|
|
7
|
+
*/
|
|
8
|
+
import { DateTime } from 'luxon'
|
|
9
|
+
import hash from '@adonisjs/core/services/hash'
|
|
10
|
+
import UserModel from '#models/user'
|
|
11
|
+
import { toUser } from './map_user.ts'
|
|
12
|
+
import type { RegisterUserInput, User, UserStore } from '#modules/contracts/index'
|
|
13
|
+
|
|
14
|
+
export class LucidUserStore implements UserStore {
|
|
15
|
+
async createEmailUser(data: RegisterUserInput): Promise<User> {
|
|
16
|
+
const user = await UserModel.create({
|
|
17
|
+
email: data.email,
|
|
18
|
+
password: data.password,
|
|
19
|
+
firstName: data.firstName ?? data.fullName?.split(' ')[0] ?? '',
|
|
20
|
+
lastName: data.lastName ?? data.fullName?.split(' ').slice(1).join(' ') ?? '',
|
|
21
|
+
authMethod: 'email',
|
|
22
|
+
})
|
|
23
|
+
return toUser(user)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async findOrFail(id: number): Promise<User> {
|
|
27
|
+
const user = await UserModel.findOrFail(id)
|
|
28
|
+
return toUser(user)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
32
|
+
const user = await UserModel.findBy('email', email)
|
|
33
|
+
return user ? toUser(user) : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findByEmailOrFail(email: string): Promise<User> {
|
|
37
|
+
const user = await UserModel.findByOrFail('email', email)
|
|
38
|
+
return toUser(user)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findByVerificationToken(token: string): Promise<User | null> {
|
|
42
|
+
const user = await UserModel.findBy('emailVerificationToken', token)
|
|
43
|
+
return user ? toUser(user) : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async verifyCredentials(email: string, password: string): Promise<User> {
|
|
47
|
+
const user = await UserModel.verifyCredentials(email, password)
|
|
48
|
+
return toUser(user)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async verifyPassword(userId: number, password: string): Promise<boolean> {
|
|
52
|
+
const user = await UserModel.findOrFail(userId)
|
|
53
|
+
if (!user.password) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
return hash.verify(user.password, password)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async save(dto: User): Promise<User> {
|
|
60
|
+
const user = await UserModel.findOrFail(dto.id)
|
|
61
|
+
user.email = dto.email
|
|
62
|
+
user.pendingEmail = dto.pendingEmail
|
|
63
|
+
user.authMethod = dto.authMethod
|
|
64
|
+
user.firstName = dto.firstName
|
|
65
|
+
user.lastName = dto.lastName
|
|
66
|
+
user.avatarKey = dto.avatarKey
|
|
67
|
+
user.emailVerificationToken = dto.emailVerificationToken
|
|
68
|
+
user.emailVerifiedAt = dto.emailVerifiedAt
|
|
69
|
+
? DateTime.fromJSDate(dto.emailVerifiedAt)
|
|
70
|
+
: null
|
|
71
|
+
await user.save()
|
|
72
|
+
return toUser(user)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async updatePassword(dto: User, password: string): Promise<User> {
|
|
76
|
+
const user = await UserModel.findOrFail(dto.id)
|
|
77
|
+
user.password = password
|
|
78
|
+
await user.save()
|
|
79
|
+
return toUser(user)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async delete(userId: number): Promise<void> {
|
|
83
|
+
const user = await UserModel.findOrFail(userId)
|
|
84
|
+
await user.delete()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type UserModel from '#models/user'
|
|
2
|
+
import type { User } from '#modules/contracts/index'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Map a Lucid User row to the portable User DTO.
|
|
6
|
+
*/
|
|
7
|
+
export function toUser(user: UserModel): User {
|
|
8
|
+
return {
|
|
9
|
+
id: user.id,
|
|
10
|
+
email: user.email,
|
|
11
|
+
pendingEmail: user.pendingEmail ?? null,
|
|
12
|
+
authMethod: user.authMethod,
|
|
13
|
+
firstName: user.firstName,
|
|
14
|
+
lastName: user.lastName,
|
|
15
|
+
avatarKey: user.avatarKey ?? null,
|
|
16
|
+
emailVerifiedAt: user.emailVerifiedAt ? user.emailVerifiedAt.toJSDate() : null,
|
|
17
|
+
emailVerificationToken: user.emailVerificationToken,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example auth controller. Copy into app/controllers and register routes.
|
|
3
|
+
* Not registered by the starter.
|
|
4
|
+
*
|
|
5
|
+
* Copy Vine provider (`providers/vine_provider.ts`) and register it in
|
|
6
|
+
* adonisrc.ts so unique/exists/validToken/validPassword macros load.
|
|
7
|
+
* Logout needs named auth middleware from start/kernel.ts.
|
|
8
|
+
*/
|
|
9
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
10
|
+
import {
|
|
11
|
+
createAuthService,
|
|
12
|
+
LucidAccessTokenSession,
|
|
13
|
+
LucidUserStore,
|
|
14
|
+
} from '#adapters/auth'
|
|
15
|
+
import { responseCodes } from '#constants/responseCodes'
|
|
16
|
+
import {
|
|
17
|
+
loginValidator,
|
|
18
|
+
registerValidator,
|
|
19
|
+
activateValidator,
|
|
20
|
+
requestPasswordResetValidator,
|
|
21
|
+
updatePasswordValidator,
|
|
22
|
+
validatePasswordResetValidator,
|
|
23
|
+
resendActivationValidator,
|
|
24
|
+
} from '../validators/auth.ts'
|
|
25
|
+
|
|
26
|
+
const auth = createAuthService()
|
|
27
|
+
|
|
28
|
+
export default class AuthController {
|
|
29
|
+
async register(ctx: HttpContext) {
|
|
30
|
+
const data = await ctx.request.validateUsing(registerValidator)
|
|
31
|
+
const user = await auth.registerUser(data)
|
|
32
|
+
return ctx.response.created(
|
|
33
|
+
await ctx.respond({
|
|
34
|
+
success: true,
|
|
35
|
+
message: responseCodes.AUTH_REGISTER.code,
|
|
36
|
+
data: user,
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async login(ctx: HttpContext) {
|
|
42
|
+
const credentials = await ctx.request.validateUsing(loginValidator)
|
|
43
|
+
const session = new LucidAccessTokenSession(ctx)
|
|
44
|
+
const { user, session: sessionResult } = await auth.loginByEmail(credentials, session)
|
|
45
|
+
return ctx.response.ok(
|
|
46
|
+
await ctx.respond({
|
|
47
|
+
success: true,
|
|
48
|
+
message: responseCodes.AUTH_LOGIN.code,
|
|
49
|
+
data: { user, token: sessionResult.token ?? null },
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async logout(ctx: HttpContext) {
|
|
55
|
+
const session = new LucidAccessTokenSession(ctx)
|
|
56
|
+
await auth.logout(session)
|
|
57
|
+
return ctx.response.ok(
|
|
58
|
+
await ctx.respond({
|
|
59
|
+
success: true,
|
|
60
|
+
message: responseCodes.AUTH_LOGOUT.code,
|
|
61
|
+
data: null,
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async activate(ctx: HttpContext) {
|
|
67
|
+
const { token } = await ctx.request.validateUsing(activateValidator)
|
|
68
|
+
const user = await auth.activateUserAccount(token)
|
|
69
|
+
return ctx.response.ok(
|
|
70
|
+
await ctx.respond({
|
|
71
|
+
success: true,
|
|
72
|
+
message: responseCodes.AUTH_ACTIVATE_ACCOUNT.code,
|
|
73
|
+
data: user,
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async sendAccountActivationEmail(ctx: HttpContext) {
|
|
79
|
+
const { email } = await ctx.request.validateUsing(resendActivationValidator)
|
|
80
|
+
const user = await new LucidUserStore().findByEmailOrFail(email)
|
|
81
|
+
await auth.createNewVerificationToken(user)
|
|
82
|
+
return ctx.response.ok(
|
|
83
|
+
await ctx.respond({
|
|
84
|
+
success: true,
|
|
85
|
+
message: responseCodes.AUTH_ACTIVATE_ACCOUNT_REQUEST.code,
|
|
86
|
+
data: null,
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async requestPasswordReset(ctx: HttpContext) {
|
|
92
|
+
const { email } = await ctx.request.validateUsing(requestPasswordResetValidator)
|
|
93
|
+
await auth.requestPasswordReset(email)
|
|
94
|
+
return ctx.response.ok(
|
|
95
|
+
await ctx.respond({
|
|
96
|
+
success: true,
|
|
97
|
+
message: responseCodes.AUTH_RESET_PASSWORD_REQUEST.code,
|
|
98
|
+
data: null,
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async validatePasswordReset(ctx: HttpContext) {
|
|
104
|
+
const { token } = await ctx.request.validateUsing(validatePasswordResetValidator)
|
|
105
|
+
const reset = await auth.validatePasswordResetToken(token)
|
|
106
|
+
return ctx.response.ok(
|
|
107
|
+
await ctx.respond({
|
|
108
|
+
success: true,
|
|
109
|
+
message: responseCodes.AUTH_VALIDATE_PASSWORD_RESET.code,
|
|
110
|
+
data: reset,
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async updatePassword(ctx: HttpContext) {
|
|
116
|
+
const data = await ctx.request.validateUsing(updatePasswordValidator)
|
|
117
|
+
await auth.updatePassword(data)
|
|
118
|
+
return ctx.response.ok(
|
|
119
|
+
await ctx.respond({
|
|
120
|
+
success: true,
|
|
121
|
+
message: responseCodes.AUTH_UPDATE_PASSWORD.code,
|
|
122
|
+
data: null,
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
2
|
+
|
|
3
|
+
export default class extends BaseSchema {
|
|
4
|
+
protected tableName = 'users'
|
|
5
|
+
|
|
6
|
+
async up() {
|
|
7
|
+
this.schema.alterTable(this.tableName, (table) => {
|
|
8
|
+
table.string('pending_email', 254).nullable()
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async down() {
|
|
13
|
+
this.schema.alterTable(this.tableName, (table) => {
|
|
14
|
+
table.dropColumn('pending_email')
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
2
|
+
|
|
3
|
+
export default class extends BaseSchema {
|
|
4
|
+
protected tableName = 'password_resets'
|
|
5
|
+
|
|
6
|
+
async up() {
|
|
7
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
8
|
+
table.increments('id').primary()
|
|
9
|
+
table
|
|
10
|
+
.integer('user_id')
|
|
11
|
+
.unsigned()
|
|
12
|
+
.unique()
|
|
13
|
+
.notNullable()
|
|
14
|
+
.references('id')
|
|
15
|
+
.inTable('users')
|
|
16
|
+
.onDelete('CASCADE')
|
|
17
|
+
table.string('token').notNullable().index()
|
|
18
|
+
table.timestamp('created_at')
|
|
19
|
+
table.timestamp('updated_at')
|
|
20
|
+
table.timestamp('expires_at', { useTz: true })
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async down() {
|
|
25
|
+
this.schema.dropTable(this.tableName)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
2
|
+
|
|
3
|
+
export default class extends BaseSchema {
|
|
4
|
+
protected tableName = 'users'
|
|
5
|
+
|
|
6
|
+
async up() {
|
|
7
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
8
|
+
table.increments('id').notNullable()
|
|
9
|
+
table.string('auth_method').notNullable()
|
|
10
|
+
table.string('email', 254).notNullable().unique()
|
|
11
|
+
table.string('first_name').notNullable()
|
|
12
|
+
table.string('last_name').notNullable()
|
|
13
|
+
table.string('avatar_key').nullable()
|
|
14
|
+
table.string('password').nullable()
|
|
15
|
+
table.string('email_verification_token').nullable()
|
|
16
|
+
table.string('creem_customer_id').unique().nullable()
|
|
17
|
+
table.boolean('email_marketing').defaultTo(false)
|
|
18
|
+
table.boolean('email_product_updates').defaultTo(false)
|
|
19
|
+
table.boolean('email_security_alerts').defaultTo(true)
|
|
20
|
+
table.boolean('email_weekly_digest').defaultTo(false)
|
|
21
|
+
|
|
22
|
+
table.timestamp('created_at').notNullable()
|
|
23
|
+
table.timestamp('updated_at').nullable()
|
|
24
|
+
table.timestamp('email_verified_at').nullable()
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async down() {
|
|
29
|
+
this.schema.dropTable(this.tableName)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm'
|
|
3
|
+
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
|
|
4
|
+
import User from '#models/user'
|
|
5
|
+
|
|
6
|
+
export default class PasswordReset extends BaseModel {
|
|
7
|
+
static table = 'password_resets'
|
|
8
|
+
|
|
9
|
+
@column({ isPrimary: true })
|
|
10
|
+
declare id: number
|
|
11
|
+
|
|
12
|
+
@column()
|
|
13
|
+
declare userId: number
|
|
14
|
+
|
|
15
|
+
@column()
|
|
16
|
+
declare token: string
|
|
17
|
+
|
|
18
|
+
@column.dateTime({ autoCreate: true })
|
|
19
|
+
declare createdAt: DateTime
|
|
20
|
+
|
|
21
|
+
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
|
22
|
+
declare updatedAt: DateTime
|
|
23
|
+
|
|
24
|
+
@column.dateTime()
|
|
25
|
+
declare expiresAt: DateTime
|
|
26
|
+
|
|
27
|
+
@belongsTo(() => User)
|
|
28
|
+
declare user: BelongsTo<typeof User>
|
|
29
|
+
}
|