@gravito/satellite-membership 0.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.
Files changed (50) hide show
  1. package/.dockerignore +8 -0
  2. package/.env.example +19 -0
  3. package/ARCHITECTURE.md +14 -0
  4. package/CHANGELOG.md +14 -0
  5. package/Dockerfile +25 -0
  6. package/README.md +112 -0
  7. package/WHITEPAPER.md +20 -0
  8. package/dist/index.d.ts +26 -0
  9. package/dist/index.js +1121 -0
  10. package/docs/EXTENDING.md +99 -0
  11. package/docs/PASSKEYS.md +78 -0
  12. package/locales/en.json +30 -0
  13. package/locales/zh-TW.json +30 -0
  14. package/package.json +35 -0
  15. package/src/Application/DTOs/MemberDTO.ts +34 -0
  16. package/src/Application/Mail/ForgotPasswordMail.ts +42 -0
  17. package/src/Application/Mail/MemberLevelChangedMail.ts +41 -0
  18. package/src/Application/Mail/WelcomeMail.ts +45 -0
  19. package/src/Application/Services/PasskeysService.ts +198 -0
  20. package/src/Application/UseCases/ForgotPassword.ts +34 -0
  21. package/src/Application/UseCases/LoginMember.ts +64 -0
  22. package/src/Application/UseCases/RegisterMember.ts +65 -0
  23. package/src/Application/UseCases/ResetPassword.ts +30 -0
  24. package/src/Application/UseCases/UpdateMemberLevel.ts +47 -0
  25. package/src/Application/UseCases/UpdateSettings.ts +81 -0
  26. package/src/Application/UseCases/VerifyEmail.ts +23 -0
  27. package/src/Domain/Contracts/IMemberPasskeyRepository.ts +8 -0
  28. package/src/Domain/Contracts/IMemberRepository.ts +11 -0
  29. package/src/Domain/Entities/Member.ts +219 -0
  30. package/src/Domain/Entities/MemberPasskey.ts +97 -0
  31. package/src/Infrastructure/Auth/SentinelMemberProvider.ts +52 -0
  32. package/src/Infrastructure/Persistence/AtlasMemberPasskeyRepository.ts +63 -0
  33. package/src/Infrastructure/Persistence/AtlasMemberRepository.ts +91 -0
  34. package/src/Infrastructure/Persistence/Migrations/20250101_create_members_table.ts +30 -0
  35. package/src/Infrastructure/Persistence/Migrations/20250102_create_member_passkeys_table.ts +25 -0
  36. package/src/Interface/Http/Controllers/PasskeyController.ts +98 -0
  37. package/src/Interface/Http/Middleware/VerifySingleDevice.ts +51 -0
  38. package/src/index.ts +234 -0
  39. package/src/manifest.json +15 -0
  40. package/tests/PasskeysService.test.ts +113 -0
  41. package/tests/email-integration.test.ts +161 -0
  42. package/tests/grand-review.ts +176 -0
  43. package/tests/integration.test.ts +75 -0
  44. package/tests/member.test.ts +47 -0
  45. package/tests/unit.test.ts +7 -0
  46. package/tests/update-settings.test.ts +101 -0
  47. package/tsconfig.json +26 -0
  48. package/views/emails/level_changed.html +35 -0
  49. package/views/emails/reset_password.html +34 -0
  50. package/views/emails/welcome.html +31 -0
@@ -0,0 +1,64 @@
1
+ import type { PlanetCore } from '@gravito/core'
2
+ import { UseCase } from '@gravito/enterprise'
3
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
4
+ import type { MemberDTO } from '../DTOs/MemberDTO'
5
+ import { MemberMapper } from '../DTOs/MemberDTO'
6
+
7
+ export interface LoginMemberInput {
8
+ email: string
9
+ passwordPlain: string
10
+ remember?: boolean
11
+ }
12
+
13
+ /**
14
+ * Login Member Use Case
15
+ *
16
+ * Uses Gravito Sentinel for robust authentication.
17
+ * Supports multi-device login restriction toggle via config.
18
+ */
19
+ export class LoginMember extends UseCase<LoginMemberInput, MemberDTO> {
20
+ constructor(
21
+ private repository: IMemberRepository,
22
+ private core: PlanetCore
23
+ ) {
24
+ super()
25
+ }
26
+
27
+ async execute(input: LoginMemberInput): Promise<MemberDTO> {
28
+ // 使用核心的 auth 管理器 (Sentinel) 進行驗證
29
+ const auth = this.core.container.make<any>('auth')
30
+
31
+ const result = await auth.guard('web').attempt(
32
+ {
33
+ email: input.email,
34
+ password: input.passwordPlain,
35
+ },
36
+ input.remember || false
37
+ )
38
+
39
+ if (!result) {
40
+ throw new Error('Invalid credentials')
41
+ }
42
+
43
+ const user = await auth.guard('web').user()
44
+
45
+ // 多設備限制邏輯
46
+ const singleDevice = this.core.config.get('membership.auth.single_device', false)
47
+ if (singleDevice) {
48
+ const session = this.core.container.make<any>('session')
49
+ if (session) {
50
+ // 獲取目前生成的 Session ID
51
+ const sessionId = session.id()
52
+
53
+ // 更新會員實體中的 Session ID
54
+ const member = await this.repository.findById(user.id)
55
+ if (member) {
56
+ member.bindSession(sessionId)
57
+ await this.repository.save(member)
58
+ }
59
+ }
60
+ }
61
+
62
+ return MemberMapper.toDTO(user)
63
+ }
64
+ }
@@ -0,0 +1,65 @@
1
+ import type { PlanetCore } from '@gravito/core'
2
+ import { UseCase } from '@gravito/enterprise'
3
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
4
+ import { Member } from '../../Domain/Entities/Member'
5
+ import type { MemberDTO } from '../DTOs/MemberDTO'
6
+ import { MemberMapper } from '../DTOs/MemberDTO'
7
+
8
+ /**
9
+ * Input for the member registration process
10
+ */
11
+ export interface RegisterMemberInput {
12
+ name: string
13
+ email: string
14
+ passwordPlain: string
15
+ }
16
+
17
+ /**
18
+ * Register Member Use Case
19
+ *
20
+ * Coordinates the registration of a new member including validation,
21
+ * password hashing, and domain event triggering.
22
+ */
23
+ export class RegisterMember extends UseCase<RegisterMemberInput, MemberDTO> {
24
+ constructor(
25
+ private repository: IMemberRepository,
26
+ private core: PlanetCore
27
+ ) {
28
+ super()
29
+ }
30
+
31
+ /**
32
+ * Execute the registration flow
33
+ */
34
+ async execute(input: RegisterMemberInput): Promise<MemberDTO> {
35
+ // 1. Check if email already exists
36
+ const existing = await this.repository.findByEmail(input.email)
37
+ if (existing) {
38
+ // Use i18n from core services
39
+ const i18n = this.core.container.make<any>('i18n')
40
+ throw new Error(i18n?.t('membership.errors.member_exists') || 'Member already exists')
41
+ }
42
+
43
+ // 2. Hash password using the core hasher
44
+ const passwordHash = await this.core.hasher.make(input.passwordPlain)
45
+
46
+ // 3. Create domain entity
47
+ const member = Member.create(crypto.randomUUID(), input.name, input.email, passwordHash)
48
+
49
+ // 4. Save to persistence
50
+ await this.repository.save(member)
51
+
52
+ // 5. Send Verification Email (via Signal hook)
53
+ await this.core.hooks.doAction('membership:send-verification', {
54
+ email: member.email,
55
+ token: member.verificationToken,
56
+ })
57
+
58
+ // 6. Trigger hooks for general extensions
59
+ await this.core.hooks.doAction('membership:registered', {
60
+ member: MemberMapper.toDTO(member),
61
+ })
62
+
63
+ return MemberMapper.toDTO(member)
64
+ }
65
+ }
@@ -0,0 +1,30 @@
1
+ import type { PlanetCore } from '@gravito/core'
2
+ import { UseCase } from '@gravito/enterprise'
3
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
4
+
5
+ export interface ResetPasswordInput {
6
+ token: string
7
+ newPasswordPlain: string
8
+ }
9
+
10
+ export class ResetPassword extends UseCase<ResetPasswordInput, void> {
11
+ constructor(
12
+ private repository: IMemberRepository,
13
+ private core: PlanetCore
14
+ ) {
15
+ super()
16
+ }
17
+
18
+ async execute(input: ResetPasswordInput): Promise<void> {
19
+ const member = await this.repository.findByResetToken(input.token)
20
+
21
+ if (!member || !member.passwordResetExpiresAt || member.passwordResetExpiresAt < new Date()) {
22
+ throw new Error('Invalid or expired reset token')
23
+ }
24
+
25
+ const newHash = await this.core.hasher.make(input.newPasswordPlain)
26
+ member.resetPassword(newHash)
27
+
28
+ await this.repository.save(member)
29
+ }
30
+ }
@@ -0,0 +1,47 @@
1
+ import type { PlanetCore } from '@gravito/core'
2
+ import { UseCase } from '@gravito/enterprise'
3
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
4
+ import type { MemberDTO } from '../DTOs/MemberDTO'
5
+ import { MemberMapper } from '../DTOs/MemberDTO'
6
+
7
+ export interface UpdateLevelInput {
8
+ memberId: string
9
+ newLevel: string
10
+ }
11
+
12
+ /**
13
+ * Update Member Level Use Case
14
+ *
15
+ * Typically used by administrative systems or loyalty programs to promote members.
16
+ */
17
+ export class UpdateMemberLevel extends UseCase<UpdateLevelInput, MemberDTO> {
18
+ constructor(
19
+ private repository: IMemberRepository,
20
+ private core: PlanetCore
21
+ ) {
22
+ super()
23
+ }
24
+
25
+ async execute(input: UpdateLevelInput): Promise<MemberDTO> {
26
+ const member = await this.repository.findById(input.memberId)
27
+
28
+ if (!member) {
29
+ throw new Error('Member not found')
30
+ }
31
+
32
+ const oldLevel = member.level
33
+ member.changeLevel(input.newLevel)
34
+
35
+ await this.repository.save(member)
36
+
37
+ // Trigger hook for tier change (e.g., to send congratulations or update discounts)
38
+ await this.core.hooks.doAction('membership:level-changed', {
39
+ memberId: member.id,
40
+ email: member.email,
41
+ oldLevel,
42
+ newLevel: input.newLevel,
43
+ })
44
+
45
+ return MemberMapper.toDTO(member)
46
+ }
47
+ }
@@ -0,0 +1,81 @@
1
+ import type { PlanetCore } from '@gravito/core'
2
+ import { UseCase } from '@gravito/enterprise'
3
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
4
+ import type { MemberDTO } from '../DTOs/MemberDTO'
5
+ import { MemberMapper } from '../DTOs/MemberDTO'
6
+
7
+ /**
8
+ * Input for updating member settings
9
+ */
10
+ export interface UpdateSettingsInput {
11
+ memberId: string
12
+ name?: string
13
+ currentPassword?: string
14
+ newPassword?: string
15
+ /** Custom metadata fields provided by extensions or specific apps */
16
+ metadata?: Record<string, any>
17
+ }
18
+
19
+ /**
20
+ * Update Settings Use Case
21
+ *
22
+ * Handles profile updates, password changes, and dynamic metadata enrichment.
23
+ */
24
+ export class UpdateSettings extends UseCase<UpdateSettingsInput, MemberDTO> {
25
+ constructor(
26
+ private repository: IMemberRepository,
27
+ private core: PlanetCore
28
+ ) {
29
+ super()
30
+ }
31
+
32
+ /**
33
+ * Execute the update flow
34
+ */
35
+ async execute(input: UpdateSettingsInput): Promise<MemberDTO> {
36
+ const member = await this.repository.findById(input.memberId)
37
+
38
+ if (!member) {
39
+ throw new Error('Member not found')
40
+ }
41
+
42
+ // 1. Handle Profile Update
43
+ if (input.name) {
44
+ member.updateProfile(input.name)
45
+ }
46
+
47
+ // 2. Handle Password Change (Secure Flow)
48
+ if (input.newPassword) {
49
+ if (!input.currentPassword) {
50
+ throw new Error('Current password is required to set a new password')
51
+ }
52
+
53
+ const isCurrentValid = await this.core.hasher.check(
54
+ input.currentPassword,
55
+ member.passwordHash
56
+ )
57
+ if (!isCurrentValid) {
58
+ throw new Error('Invalid current password')
59
+ }
60
+
61
+ const newHash = await this.core.hasher.make(input.newPassword)
62
+ member.changePassword(newHash)
63
+ }
64
+
65
+ // 3. Handle Dynamic Metadata (Custom Fields)
66
+ if (input.metadata) {
67
+ member.updateMetadata(input.metadata)
68
+ }
69
+
70
+ // 4. Persist changes
71
+ await this.repository.save(member)
72
+
73
+ // 5. Trigger update hook
74
+ await this.core.hooks.doAction('membership:updated', {
75
+ member: MemberMapper.toDTO(member),
76
+ updatedFields: Object.keys(input).filter((k) => k !== 'memberId' && k !== 'currentPassword'),
77
+ })
78
+
79
+ return MemberMapper.toDTO(member)
80
+ }
81
+ }
@@ -0,0 +1,23 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
3
+
4
+ export interface VerifyEmailInput {
5
+ token: string
6
+ }
7
+
8
+ export class VerifyEmail extends UseCase<VerifyEmailInput, void> {
9
+ constructor(private repository: IMemberRepository) {
10
+ super()
11
+ }
12
+
13
+ async execute(input: VerifyEmailInput): Promise<void> {
14
+ const member = await this.repository.findByVerificationToken(input.token)
15
+
16
+ if (!member) {
17
+ throw new Error('Invalid verification token')
18
+ }
19
+
20
+ member.verifyEmail()
21
+ await this.repository.save(member)
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ import type { Repository } from '@gravito/enterprise'
2
+ import type { MemberPasskey } from '../Entities/MemberPasskey'
3
+
4
+ export interface IMemberPasskeyRepository extends Repository<MemberPasskey, string> {
5
+ findByMemberId(memberId: string): Promise<MemberPasskey[]>
6
+ findByCredentialId(credentialId: string): Promise<MemberPasskey | null>
7
+ deleteByCredentialId(credentialId: string): Promise<void>
8
+ }
@@ -0,0 +1,11 @@
1
+ import type { Repository } from '@gravito/enterprise'
2
+ import type { Member } from '../Entities/Member'
3
+
4
+ /**
5
+ * Member Repository Contract
6
+ */
7
+ export interface IMemberRepository extends Repository<Member, string> {
8
+ findByEmail(email: string): Promise<Member | null>
9
+ findByVerificationToken(token: string): Promise<Member | null>
10
+ findByResetToken(token: string): Promise<Member | null>
11
+ }
@@ -0,0 +1,219 @@
1
+ import { Entity } from '@gravito/enterprise'
2
+
3
+ /**
4
+ * Member status enumeration
5
+ */
6
+ export enum MemberStatus {
7
+ ACTIVE = 'active',
8
+ PENDING = 'pending',
9
+ SUSPENDED = 'suspended',
10
+ }
11
+
12
+ /**
13
+ * Properties for the Member entity
14
+ */
15
+ export interface MemberProps {
16
+ email: string
17
+ name: string
18
+ passwordHash: string
19
+ status: MemberStatus
20
+ roles: string[]
21
+ verificationToken?: string
22
+ emailVerifiedAt?: Date
23
+ passwordResetToken?: string
24
+ passwordResetExpiresAt?: Date
25
+ currentSessionId?: string
26
+ rememberToken?: string
27
+ createdAt: Date
28
+ updatedAt: Date
29
+ /** Flexible metadata for extensions */
30
+ metadata?: Record<string, any>
31
+ }
32
+
33
+ /**
34
+ * Member Domain Entity
35
+ */
36
+ export class Member extends Entity<string> {
37
+ private constructor(
38
+ id: string,
39
+ private props: MemberProps
40
+ ) {
41
+ super(id)
42
+ }
43
+
44
+ static create(id: string, name: string, email: string, passwordHash: string): Member {
45
+ return new Member(id, {
46
+ name,
47
+ email,
48
+ passwordHash,
49
+ status: MemberStatus.PENDING,
50
+ roles: ['member'],
51
+ verificationToken: crypto.randomUUID(),
52
+ createdAt: new Date(),
53
+ updatedAt: new Date(),
54
+ })
55
+ }
56
+
57
+ static reconstitute(id: string, props: MemberProps): Member {
58
+ return new Member(id, props)
59
+ }
60
+
61
+ // Getters
62
+ get name() {
63
+ return this.props.name
64
+ }
65
+ get email() {
66
+ return this.props.email
67
+ }
68
+ get status() {
69
+ return this.props.status
70
+ }
71
+ get roles() {
72
+ return this.props.roles
73
+ }
74
+ get passwordHash() {
75
+ return this.props.passwordHash
76
+ }
77
+ get createdAt() {
78
+ return this.props.createdAt
79
+ }
80
+ get emailVerifiedAt() {
81
+ return this.props.emailVerifiedAt
82
+ }
83
+ get verificationToken() {
84
+ return this.props.verificationToken
85
+ }
86
+ get passwordResetToken() {
87
+ return this.props.passwordResetToken
88
+ }
89
+ get passwordResetExpiresAt() {
90
+ return this.props.passwordResetExpiresAt
91
+ }
92
+ get currentSessionId() {
93
+ return this.props.currentSessionId
94
+ }
95
+ get rememberToken() {
96
+ return this.props.rememberToken
97
+ }
98
+ get metadata() {
99
+ return this.props.metadata || {}
100
+ }
101
+
102
+ // Authenticatable implementation
103
+ getAuthIdentifier(): string {
104
+ return this.id
105
+ }
106
+ getAuthPassword(): string {
107
+ return this.props.passwordHash
108
+ }
109
+ getRememberToken(): string | null {
110
+ return this.props.rememberToken || null
111
+ }
112
+ setRememberToken(token: string): void {
113
+ this.props.rememberToken = token
114
+ this.props.updatedAt = new Date()
115
+ }
116
+
117
+ /**
118
+ * Bind a session ID to this member to restrict multi-device login.
119
+ */
120
+ public bindSession(sessionId: string): void {
121
+ this.props.currentSessionId = sessionId
122
+ this.props.updatedAt = new Date()
123
+ }
124
+
125
+ /**
126
+ * Get current membership level (from metadata or default)
127
+ */
128
+ get level(): string {
129
+ return this.metadata.level || 'standard'
130
+ }
131
+
132
+ /**
133
+ * Check if member has a specific role
134
+ */
135
+ public hasRole(role: string): boolean {
136
+ return this.props.roles.includes(role)
137
+ }
138
+
139
+ /**
140
+ * Assign a new role
141
+ */
142
+ public addRole(role: string): void {
143
+ if (!this.hasRole(role)) {
144
+ this.props.roles.push(role)
145
+ this.props.updatedAt = new Date()
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Remove a role
151
+ */
152
+ public removeRole(role: string): void {
153
+ this.props.roles = this.props.roles.filter((r) => r !== role)
154
+ this.props.updatedAt = new Date()
155
+ }
156
+
157
+ /**
158
+ * Update membership level
159
+ */
160
+ public changeLevel(newLevel: string): void {
161
+ this.updateMetadata({ level: newLevel })
162
+ }
163
+
164
+ /**
165
+ * Update core profile information
166
+ */
167
+ public updateProfile(name: string): void {
168
+ this.props.name = name
169
+ this.props.updatedAt = new Date()
170
+ }
171
+
172
+ /**
173
+ * Update member password
174
+ */
175
+ public changePassword(newPasswordHash: string): void {
176
+ this.props.passwordHash = newPasswordHash
177
+ this.props.updatedAt = new Date()
178
+ }
179
+
180
+ /**
181
+ * Merges new metadata into existing metadata
182
+ */
183
+ public updateMetadata(data: Record<string, any>): void {
184
+ this.props.metadata = {
185
+ ...(this.props.metadata || {}),
186
+ ...data,
187
+ }
188
+ this.props.updatedAt = new Date()
189
+ }
190
+
191
+ /**
192
+ * Mark email as verified
193
+ */
194
+ public verifyEmail(): void {
195
+ this.props.emailVerifiedAt = new Date()
196
+ this.props.status = MemberStatus.ACTIVE
197
+ this.props.verificationToken = undefined
198
+ this.props.updatedAt = new Date()
199
+ }
200
+
201
+ /**
202
+ * Generate a password reset token
203
+ */
204
+ public generatePasswordResetToken(): void {
205
+ this.props.passwordResetToken = crypto.randomUUID()
206
+ this.props.passwordResetExpiresAt = new Date(Date.now() + 3600000)
207
+ this.props.updatedAt = new Date()
208
+ }
209
+
210
+ /**
211
+ * Complete password reset
212
+ */
213
+ public resetPassword(newPasswordHash: string): void {
214
+ this.props.passwordHash = newPasswordHash
215
+ this.props.passwordResetToken = undefined
216
+ this.props.passwordResetExpiresAt = undefined
217
+ this.props.updatedAt = new Date()
218
+ }
219
+ }
@@ -0,0 +1,97 @@
1
+ import { Entity } from '@gravito/enterprise'
2
+
3
+ export interface MemberPasskeyProps {
4
+ memberId: string
5
+ credentialId: string
6
+ publicKey: string
7
+ counter: number
8
+ transports?: string[]
9
+ displayName?: string
10
+ createdAt: Date
11
+ updatedAt: Date
12
+ }
13
+
14
+ export class MemberPasskey extends Entity<string> {
15
+ private constructor(
16
+ id: string,
17
+ private props: MemberPasskeyProps
18
+ ) {
19
+ super(id)
20
+ }
21
+
22
+ static create(params: {
23
+ memberId: string
24
+ credentialId: string
25
+ publicKey: string
26
+ transports?: string[]
27
+ displayName?: string
28
+ counter?: number
29
+ }): MemberPasskey {
30
+ const now = new Date()
31
+ return new MemberPasskey(crypto.randomUUID(), {
32
+ memberId: params.memberId,
33
+ credentialId: params.credentialId,
34
+ publicKey: params.publicKey,
35
+ counter: params.counter ?? 0,
36
+ transports: params.transports,
37
+ displayName: params.displayName,
38
+ createdAt: now,
39
+ updatedAt: now,
40
+ })
41
+ }
42
+
43
+ static reconstitute(id: string, props: MemberPasskeyProps): MemberPasskey {
44
+ return new MemberPasskey(id, props)
45
+ }
46
+
47
+ get memberId(): string {
48
+ return this.props.memberId
49
+ }
50
+
51
+ get credentialId(): string {
52
+ return this.props.credentialId
53
+ }
54
+
55
+ get publicKey(): string {
56
+ return this.props.publicKey
57
+ }
58
+
59
+ get counter(): number {
60
+ return this.props.counter
61
+ }
62
+
63
+ get transports(): string[] | undefined {
64
+ return this.props.transports
65
+ }
66
+
67
+ get displayName(): string | undefined {
68
+ return this.props.displayName
69
+ }
70
+
71
+ get createdAt(): Date {
72
+ return this.props.createdAt
73
+ }
74
+
75
+ get updatedAt(): Date {
76
+ return this.props.updatedAt
77
+ }
78
+
79
+ public updateCounter(next: number): void {
80
+ this.props.counter = next
81
+ this.props.updatedAt = new Date()
82
+ }
83
+
84
+ toRecord(): Record<string, unknown> {
85
+ return {
86
+ id: this.id,
87
+ member_id: this.memberId,
88
+ credential_id: this.credentialId,
89
+ public_key: this.publicKey,
90
+ counter: this.counter,
91
+ transports: this.transports ? JSON.stringify(this.transports) : null,
92
+ display_name: this.displayName,
93
+ created_at: this.createdAt,
94
+ updated_at: this.updatedAt,
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,52 @@
1
+ import type { Authenticatable, UserProvider } from '@gravito/sentinel'
2
+ import type { IMemberRepository } from '../../Domain/Contracts/IMemberRepository'
3
+ import type { Member } from '../../Domain/Entities/Member'
4
+
5
+ /**
6
+ * Sentinel Member Provider
7
+ *
8
+ * Adapts our Member Repository to Gravito Sentinel's Auth system.
9
+ */
10
+ export class SentinelMemberProvider implements UserProvider {
11
+ constructor(private repository: IMemberRepository) {}
12
+
13
+ async retrieveById(id: string): Promise<Authenticatable | null> {
14
+ const member = await this.repository.findById(id)
15
+ return member ? this.toAuthenticatable(member) : null
16
+ }
17
+
18
+ async retrieveByToken(id: string, token: string): Promise<Authenticatable | null> {
19
+ const member = await this.repository.findById(id)
20
+ if (member && member.getRememberToken() === token) {
21
+ return member
22
+ }
23
+ return null
24
+ }
25
+
26
+ async updateRememberToken(user: Authenticatable, token: string): Promise<void> {
27
+ const member = await this.repository.findById(user.getAuthIdentifier() as string)
28
+ if (member) {
29
+ member.setRememberToken(token)
30
+ await this.repository.save(member)
31
+ }
32
+ }
33
+
34
+ async retrieveByCredentials(credentials: Record<string, any>): Promise<Authenticatable | null> {
35
+ if (!credentials.email) {
36
+ return null
37
+ }
38
+ return await this.repository.findByEmail(credentials.email)
39
+ }
40
+
41
+ async validateCredentials(
42
+ _user: Authenticatable,
43
+ _credentials: Record<string, any>
44
+ ): Promise<boolean> {
45
+ // Note: In a real app, you might check if account is active here
46
+ return true
47
+ }
48
+
49
+ private toAuthenticatable(member: Member): Authenticatable {
50
+ return member
51
+ }
52
+ }