@veap/auth 0.0.0-stage → 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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -2
  3. package/dist/actions/auth.actions.d.ts +23 -0
  4. package/dist/actions/auth.actions.js +40 -0
  5. package/dist/actions/index.d.ts +1 -0
  6. package/dist/actions/index.js +1 -0
  7. package/dist/client/auth.context.d.ts +29 -0
  8. package/dist/client/auth.context.js +64 -0
  9. package/dist/client/index.d.ts +1 -0
  10. package/dist/client/index.js +1 -0
  11. package/dist/common/augment.d.ts +26 -0
  12. package/dist/common/augment.js +63 -0
  13. package/dist/common/context.d.ts +30 -0
  14. package/dist/common/context.js +36 -0
  15. package/dist/common/index.d.ts +7 -0
  16. package/dist/common/index.js +7 -0
  17. package/dist/common/logic.d.ts +64 -0
  18. package/dist/common/logic.js +133 -0
  19. package/dist/common/registry.d.ts +28 -0
  20. package/dist/common/registry.js +76 -0
  21. package/dist/common/routes.d.ts +3 -0
  22. package/dist/common/routes.js +2 -0
  23. package/dist/common/tokens.d.ts +10 -0
  24. package/dist/common/tokens.js +8 -0
  25. package/dist/common/types.d.ts +178 -0
  26. package/dist/common/types.js +9 -0
  27. package/dist/crypto/aes-cipher.d.ts +8 -0
  28. package/dist/crypto/aes-cipher.js +12 -0
  29. package/dist/crypto/bcrypt-hasher.d.ts +12 -0
  30. package/dist/crypto/bcrypt-hasher.js +37 -0
  31. package/dist/crypto/contracts.d.ts +36 -0
  32. package/dist/crypto/contracts.js +3 -0
  33. package/dist/crypto/encryption.d.ts +18 -0
  34. package/dist/crypto/encryption.js +69 -0
  35. package/dist/crypto/index.d.ts +6 -0
  36. package/dist/crypto/index.js +6 -0
  37. package/dist/crypto/password.d.ts +19 -0
  38. package/dist/crypto/password.js +27 -0
  39. package/dist/crypto/token-generator.d.ts +10 -0
  40. package/dist/crypto/token-generator.js +25 -0
  41. package/dist/index.d.ts +11 -0
  42. package/dist/index.js +11 -0
  43. package/dist/migrations/0001_create_auth_tables.d.ts +4 -0
  44. package/dist/migrations/0001_create_auth_tables.js +122 -0
  45. package/dist/migrations/index.d.ts +4 -0
  46. package/dist/migrations/index.js +7 -0
  47. package/dist/models/EmailVerification.d.ts +23 -0
  48. package/dist/models/EmailVerification.js +19 -0
  49. package/dist/models/PasswordResetSession.d.ts +25 -0
  50. package/dist/models/PasswordResetSession.js +21 -0
  51. package/dist/models/Permission.d.ts +20 -0
  52. package/dist/models/Permission.js +19 -0
  53. package/dist/models/Role.d.ts +20 -0
  54. package/dist/models/Role.js +19 -0
  55. package/dist/models/Session.d.ts +22 -0
  56. package/dist/models/Session.js +21 -0
  57. package/dist/models/User.d.ts +32 -0
  58. package/dist/models/User.js +31 -0
  59. package/dist/models/index.d.ts +6 -0
  60. package/dist/models/index.js +6 -0
  61. package/dist/models.d.ts +1 -0
  62. package/dist/models.js +1 -0
  63. package/dist/passwords/contracts.d.ts +28 -0
  64. package/dist/passwords/contracts.js +2 -0
  65. package/dist/passwords/index.d.ts +4 -0
  66. package/dist/passwords/index.js +4 -0
  67. package/dist/passwords/password-reset.facade.d.ts +14 -0
  68. package/dist/passwords/password-reset.facade.js +57 -0
  69. package/dist/passwords/password-reset.repository.d.ts +10 -0
  70. package/dist/passwords/password-reset.repository.js +39 -0
  71. package/dist/passwords/password-reset.service.d.ts +38 -0
  72. package/dist/passwords/password-reset.service.js +242 -0
  73. package/dist/providers/auth.provider.d.ts +9 -0
  74. package/dist/providers/auth.provider.js +199 -0
  75. package/dist/providers/index.d.ts +2 -0
  76. package/dist/providers/index.js +2 -0
  77. package/dist/providers/setup.d.ts +7 -0
  78. package/dist/providers/setup.js +29 -0
  79. package/dist/rbac/contracts.d.ts +75 -0
  80. package/dist/rbac/contracts.js +4 -0
  81. package/dist/rbac/index.d.ts +4 -0
  82. package/dist/rbac/index.js +4 -0
  83. package/dist/rbac/rbac.facade.d.ts +23 -0
  84. package/dist/rbac/rbac.facade.js +85 -0
  85. package/dist/rbac/rbac.repository.d.ts +30 -0
  86. package/dist/rbac/rbac.repository.js +137 -0
  87. package/dist/rbac/rbac.service.d.ts +35 -0
  88. package/dist/rbac/rbac.service.js +143 -0
  89. package/dist/server.d.ts +3 -0
  90. package/dist/server.js +3 -0
  91. package/dist/session/auth.service.d.ts +35 -0
  92. package/dist/session/auth.service.js +207 -0
  93. package/dist/session/contracts.d.ts +33 -0
  94. package/dist/session/contracts.js +2 -0
  95. package/dist/session/index.d.ts +5 -0
  96. package/dist/session/index.js +5 -0
  97. package/dist/session/session.facade.d.ts +14 -0
  98. package/dist/session/session.facade.js +75 -0
  99. package/dist/session/session.repository.d.ts +15 -0
  100. package/dist/session/session.repository.js +55 -0
  101. package/dist/session/session.service.d.ts +63 -0
  102. package/dist/session/session.service.js +221 -0
  103. package/dist/users/contracts.d.ts +33 -0
  104. package/dist/users/contracts.js +2 -0
  105. package/dist/users/index.d.ts +4 -0
  106. package/dist/users/index.js +4 -0
  107. package/dist/users/user.facade.d.ts +28 -0
  108. package/dist/users/user.facade.js +92 -0
  109. package/dist/users/user.repository.d.ts +16 -0
  110. package/dist/users/user.repository.js +60 -0
  111. package/dist/users/user.service.d.ts +80 -0
  112. package/dist/users/user.service.js +201 -0
  113. package/dist/validation/index.d.ts +90 -0
  114. package/dist/validation/index.js +46 -0
  115. package/dist/verification/contracts.d.ts +25 -0
  116. package/dist/verification/contracts.js +2 -0
  117. package/dist/verification/index.d.ts +4 -0
  118. package/dist/verification/index.js +4 -0
  119. package/dist/verification/verification.facade.d.ts +10 -0
  120. package/dist/verification/verification.facade.js +49 -0
  121. package/dist/verification/verification.repository.d.ts +7 -0
  122. package/dist/verification/verification.repository.js +23 -0
  123. package/dist/verification/verification.service.d.ts +44 -0
  124. package/dist/verification/verification.service.js +122 -0
  125. package/package.json +84 -4
@@ -0,0 +1,57 @@
1
+ "use server";
2
+ import { app } from "@veap/kernel";
3
+ import { authContext, isAuthContextBound } from "../common/context.js";
4
+ import { PasswordResetService } from "./password-reset.service.js";
5
+ async function resolvePasswordResetService() {
6
+ const g = globalThis;
7
+ if (g.__VEAP_BOOTSTRAPPING_PROMISE__) {
8
+ try {
9
+ await g.__VEAP_BOOTSTRAPPING_PROMISE__;
10
+ }
11
+ catch { }
12
+ }
13
+ if (isAuthContextBound()) {
14
+ return authContext().passwordReset;
15
+ }
16
+ return await app(PasswordResetService);
17
+ }
18
+ export async function createPasswordResetSession(token, userId, email) {
19
+ const service = await resolvePasswordResetService();
20
+ return service.createPasswordResetSession(token, userId, email);
21
+ }
22
+ export async function validatePasswordResetSessionToken(token) {
23
+ const service = await resolvePasswordResetService();
24
+ return service.validatePasswordResetSessionToken(token);
25
+ }
26
+ export async function setPasswordResetSessionAsEmailVerified(sessionId) {
27
+ const service = await resolvePasswordResetService();
28
+ return service.setPasswordResetSessionAsEmailVerified(sessionId);
29
+ }
30
+ export async function invalidateUserPasswordResetSessions(userId) {
31
+ const service = await resolvePasswordResetService();
32
+ return service.invalidateUserPasswordResetSessions(userId);
33
+ }
34
+ export async function getCurrentPasswordResetSession() {
35
+ const service = await resolvePasswordResetService();
36
+ return service.getCurrentPasswordResetSession();
37
+ }
38
+ export async function setPasswordResetSessionTokenCookie(token, expiresAt) {
39
+ const service = await resolvePasswordResetService();
40
+ return service.setPasswordResetSessionTokenCookie(token, expiresAt);
41
+ }
42
+ export async function deletePasswordResetSessionTokenCookie() {
43
+ const service = await resolvePasswordResetService();
44
+ return service.deletePasswordResetSessionTokenCookie();
45
+ }
46
+ export async function sendPasswordResetEmail(email, code) {
47
+ const service = await resolvePasswordResetService();
48
+ return service.sendPasswordResetEmail(email, code);
49
+ }
50
+ export async function createDummyPasswordResetSession(token, email) {
51
+ const service = await resolvePasswordResetService();
52
+ return service.createDummyPasswordResetSession(token, email);
53
+ }
54
+ export async function verifyResetCode(sessionId, code) {
55
+ const service = await resolvePasswordResetService();
56
+ return service.verifyResetCode(sessionId, code);
57
+ }
@@ -0,0 +1,10 @@
1
+ import type { PasswordResetSession as PasswordResetSessionEntity } from "../common/index.js";
2
+ import type { CreatePasswordResetRecord, IPasswordResetRepository, PasswordResetWithUser } from "./contracts.js";
3
+ /** `IPasswordResetRepository` adapter backed by `PasswordResetSession`. */
4
+ export declare class ActiveRecordPasswordResetRepository implements IPasswordResetRepository {
5
+ create(record: CreatePasswordResetRecord): Promise<PasswordResetSessionEntity>;
6
+ findWithUser(id: string): Promise<PasswordResetWithUser | null>;
7
+ setEmailVerified(id: string): Promise<void>;
8
+ remove(id: string): Promise<void>;
9
+ removeByUserId(userId: string): Promise<void>;
10
+ }
@@ -0,0 +1,39 @@
1
+ import { PasswordResetSession } from "../models/PasswordResetSession.js";
2
+ /** `IPasswordResetRepository` adapter backed by `PasswordResetSession`. */
3
+ export class ActiveRecordPasswordResetRepository {
4
+ async create(record) {
5
+ const session = await PasswordResetSession.create({
6
+ id: record.id,
7
+ email: record.email,
8
+ code: record.code,
9
+ expiresAt: record.expiresAt,
10
+ userId: record.userId,
11
+ });
12
+ return session.toJSON();
13
+ }
14
+ async findWithUser(id) {
15
+ const session = await PasswordResetSession.query().with("user").find(id);
16
+ if (!session)
17
+ return null;
18
+ const user = session.getRelation("user");
19
+ if (!user)
20
+ return null;
21
+ return {
22
+ session: session.toJSON(),
23
+ user: (typeof user.toJSON === "function"
24
+ ? user.toJSON()
25
+ : user),
26
+ };
27
+ }
28
+ async setEmailVerified(id) {
29
+ await PasswordResetSession.where("id", id).update({
30
+ emailVerified: true,
31
+ });
32
+ }
33
+ async remove(id) {
34
+ await PasswordResetSession.destroy(id);
35
+ }
36
+ async removeByUserId(userId) {
37
+ await PasswordResetSession.where("user_id", userId).delete();
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { type ICacheProvider, type ICookieStore } from "@veap/kernel";
2
+ import type { PasswordResetAuthSession, PasswordResetSession as PasswordResetSessionType } from "../common/index.js";
3
+ import { type ITokenGenerator } from "../crypto/contracts.js";
4
+ import { type IPasswordResetRepository } from "./contracts.js";
5
+ export declare class PasswordResetService {
6
+ private readonly sessions;
7
+ private readonly tokens;
8
+ private readonly cookieStore;
9
+ private readonly cache?;
10
+ private inMemoryAttempts;
11
+ constructor(sessions: IPasswordResetRepository, tokens: ITokenGenerator, cookieStore: ICookieStore, cache?: ICacheProvider | undefined);
12
+ createPasswordResetSession(token: string, userId: string, email: string): Promise<PasswordResetSessionType>;
13
+ /**
14
+ * Creates an ephemeral dummy reset session for an unregistered email.
15
+ * This prevents user enumeration while providing an identical UX.
16
+ */
17
+ createDummyPasswordResetSession(token: string, email: string): Promise<PasswordResetSessionType>;
18
+ validatePasswordResetSessionToken(token: string): Promise<PasswordResetAuthSession>;
19
+ /**
20
+ * Verifies the OTP code for a password reset session.
21
+ * Enforces a maximum of 5 failed attempts before terminating the session.
22
+ */
23
+ verifyResetCode(sessionId: string, code: string): Promise<{
24
+ valid: boolean;
25
+ error?: string;
26
+ }>;
27
+ setPasswordResetSessionAsEmailVerified(sessionId: string): Promise<void>;
28
+ invalidateUserPasswordResetSessions(userId: string): Promise<void>;
29
+ getCurrentPasswordResetSession(): Promise<PasswordResetAuthSession>;
30
+ setPasswordResetSessionTokenCookie(token: string, expiresAt: Date): Promise<void>;
31
+ deletePasswordResetSessionTokenCookie(): Promise<void>;
32
+ validatePasswordResetSession(token: string): Promise<{
33
+ session: PasswordResetSessionType;
34
+ user: any;
35
+ }>;
36
+ verifyPasswordResetSessionCode(token: string, code: string): Promise<boolean>;
37
+ sendPasswordResetEmail(email: string, code: string): Promise<void>;
38
+ }
@@ -0,0 +1,242 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { CACHE_PROVIDER, COOKIE_STORE, Inject, Injectable, } from "@veap/kernel";
14
+ import { sendResetPassword } from "@veap/mail";
15
+ import { addMinutes } from "date-fns";
16
+ import { augmentPasswordResetSession } from "../common/augment.js";
17
+ import { performFullUserAugmentation } from "../common/logic.js";
18
+ import { TOKEN_GENERATOR } from "../crypto/contracts.js";
19
+ import { PASSWORD_RESET_REPOSITORY, } from "./contracts.js";
20
+ const dummySessions = new Map();
21
+ let PasswordResetService = class PasswordResetService {
22
+ sessions;
23
+ tokens;
24
+ cookieStore;
25
+ cache;
26
+ inMemoryAttempts = new Map();
27
+ constructor(sessions, tokens, cookieStore, cache) {
28
+ this.sessions = sessions;
29
+ this.tokens = tokens;
30
+ this.cookieStore = cookieStore;
31
+ this.cache = cache;
32
+ }
33
+ async createPasswordResetSession(token, userId, email) {
34
+ const sessionId = this.tokens.hashToken(token);
35
+ return await this.sessions.create({
36
+ id: sessionId,
37
+ email,
38
+ code: this.tokens.generateOtp(),
39
+ expiresAt: new Date(addMinutes(new Date(), 15)),
40
+ userId,
41
+ });
42
+ }
43
+ /**
44
+ * Creates an ephemeral dummy reset session for an unregistered email.
45
+ * This prevents user enumeration while providing an identical UX.
46
+ */
47
+ async createDummyPasswordResetSession(token, email) {
48
+ const sessionId = this.tokens.hashToken(token);
49
+ const expiresAt = new Date(addMinutes(new Date(), 15));
50
+ const session = {
51
+ id: sessionId,
52
+ email,
53
+ code: "------",
54
+ expiresAt,
55
+ userId: "dummy",
56
+ };
57
+ dummySessions.set(sessionId, {
58
+ id: sessionId,
59
+ email,
60
+ expiresAt,
61
+ attempts: 0,
62
+ });
63
+ return session;
64
+ }
65
+ async validatePasswordResetSessionToken(token) {
66
+ const sessionId = this.tokens.hashToken(token);
67
+ const record = await this.sessions.findWithUser(sessionId);
68
+ if (!record) {
69
+ // Check for dummy session to prevent timing and existence leakage
70
+ const dummy = dummySessions.get(sessionId);
71
+ if (dummy && new Date() <= new Date(dummy.expiresAt)) {
72
+ return {
73
+ session: {
74
+ id: dummy.id,
75
+ email: dummy.email,
76
+ code: "------",
77
+ expiresAt: dummy.expiresAt,
78
+ userId: "dummy",
79
+ },
80
+ user: {
81
+ id: "dummy",
82
+ email: dummy.email,
83
+ name: "User",
84
+ roles: [],
85
+ permissions: [],
86
+ emailVerifiedAt: null,
87
+ createdAt: new Date(),
88
+ updatedAt: null,
89
+ recovery_code: null,
90
+ image: null,
91
+ password: null,
92
+ },
93
+ };
94
+ }
95
+ return { session: null, user: null };
96
+ }
97
+ const baseSession = record.session;
98
+ const baseUser = record.user;
99
+ if (new Date() > new Date(baseSession.expiresAt)) {
100
+ await this.sessions.remove(sessionId);
101
+ return { session: null, user: null };
102
+ }
103
+ const { password, recovery_code, ...safeUser } = baseUser;
104
+ const user = await performFullUserAugmentation(safeUser);
105
+ const session = await augmentPasswordResetSession(baseSession);
106
+ return { session, user };
107
+ }
108
+ /**
109
+ * Verifies the OTP code for a password reset session.
110
+ * Enforces a maximum of 5 failed attempts before terminating the session.
111
+ */
112
+ async verifyResetCode(sessionId, code) {
113
+ // Handle dummy session
114
+ const dummy = dummySessions.get(sessionId);
115
+ if (dummy) {
116
+ dummy.attempts += 1;
117
+ if (dummy.attempts >= 5 || new Date() > new Date(dummy.expiresAt)) {
118
+ dummySessions.delete(sessionId);
119
+ await this.deletePasswordResetSessionTokenCookie();
120
+ return {
121
+ valid: false,
122
+ error: "Too many failed attempts. Password reset request has been cancelled.",
123
+ };
124
+ }
125
+ return { valid: false, error: "Incorrect code" };
126
+ }
127
+ const record = await this.sessions.findWithUser(sessionId);
128
+ if (!record) {
129
+ return { valid: false, error: "Invalid or expired session" };
130
+ }
131
+ if (new Date() > new Date(record.session.expiresAt)) {
132
+ await this.sessions.remove(sessionId);
133
+ await this.deletePasswordResetSessionTokenCookie();
134
+ return { valid: false, error: "Password reset code has expired" };
135
+ }
136
+ // Check attempts limit
137
+ const attemptKey = `pw_reset_attempts:${sessionId}`;
138
+ let attempts = 0;
139
+ if (this.cache) {
140
+ attempts = ((await this.cache.get(attemptKey)) || 0) + 1;
141
+ await this.cache.set(attemptKey, attempts, 15 * 60);
142
+ }
143
+ else {
144
+ const rec = this.inMemoryAttempts.get(sessionId);
145
+ attempts = (rec && Date.now() <= rec.expiresAt ? rec.count : 0) + 1;
146
+ this.inMemoryAttempts.set(sessionId, {
147
+ count: attempts,
148
+ expiresAt: Date.now() + 15 * 60 * 1000,
149
+ });
150
+ }
151
+ if (attempts >= 5) {
152
+ await this.sessions.remove(sessionId);
153
+ await this.deletePasswordResetSessionTokenCookie();
154
+ if (this.cache)
155
+ await this.cache.delete(attemptKey);
156
+ this.inMemoryAttempts.delete(sessionId);
157
+ return {
158
+ valid: false,
159
+ error: "Too many failed attempts. Password reset request has been cancelled.",
160
+ };
161
+ }
162
+ if (record.session.code !== code) {
163
+ return { valid: false, error: "Incorrect code" };
164
+ }
165
+ if (this.cache)
166
+ await this.cache.delete(attemptKey);
167
+ this.inMemoryAttempts.delete(sessionId);
168
+ await this.sessions.setEmailVerified(sessionId);
169
+ return { valid: true };
170
+ }
171
+ async setPasswordResetSessionAsEmailVerified(sessionId) {
172
+ await this.sessions.setEmailVerified(sessionId);
173
+ }
174
+ async invalidateUserPasswordResetSessions(userId) {
175
+ await this.sessions.removeByUserId(userId);
176
+ }
177
+ async getCurrentPasswordResetSession() {
178
+ const token = await this.cookieStore.get("password_reset_session");
179
+ if (token === null) {
180
+ return { session: null, user: null };
181
+ }
182
+ const result = await this.validatePasswordResetSessionToken(token);
183
+ if (result.session === null) {
184
+ await this.deletePasswordResetSessionTokenCookie();
185
+ }
186
+ return result;
187
+ }
188
+ async setPasswordResetSessionTokenCookie(token, expiresAt) {
189
+ await this.cookieStore.set("password_reset_session", token, {
190
+ expires: expiresAt,
191
+ sameSite: "lax",
192
+ httpOnly: true,
193
+ path: "/",
194
+ secure: process.env.NODE_ENV === "production",
195
+ });
196
+ }
197
+ async deletePasswordResetSessionTokenCookie() {
198
+ await this.cookieStore.delete("password_reset_session");
199
+ }
200
+ async validatePasswordResetSession(token) {
201
+ const sessionId = this.tokens.hashToken(token);
202
+ const record = await this.sessions.findWithUser(sessionId);
203
+ if (!record) {
204
+ throw new Error("Invalid or expired password reset session");
205
+ }
206
+ if (new Date() > new Date(record.session.expiresAt)) {
207
+ await this.sessions.remove(sessionId);
208
+ throw new Error("Invalid or expired password reset session");
209
+ }
210
+ if (!record.session.emailVerified) {
211
+ throw new Error("Invalid or expired password reset session");
212
+ }
213
+ return { session: record.session, user: record.user };
214
+ }
215
+ async verifyPasswordResetSessionCode(token, code) {
216
+ const sessionId = this.tokens.hashToken(token);
217
+ const record = await this.sessions.findWithUser(sessionId);
218
+ if (!record)
219
+ return false;
220
+ if (new Date() > new Date(record.session.expiresAt)) {
221
+ await this.sessions.remove(sessionId);
222
+ return false;
223
+ }
224
+ if (record.session.code !== code) {
225
+ return false;
226
+ }
227
+ await this.sessions.setEmailVerified(sessionId);
228
+ return true;
229
+ }
230
+ async sendPasswordResetEmail(email, code) {
231
+ await sendResetPassword(email, code);
232
+ }
233
+ };
234
+ PasswordResetService = __decorate([
235
+ Injectable(),
236
+ __param(0, Inject(PASSWORD_RESET_REPOSITORY)),
237
+ __param(1, Inject(TOKEN_GENERATOR)),
238
+ __param(2, Inject(COOKIE_STORE)),
239
+ __param(3, Inject(CACHE_PROVIDER)),
240
+ __metadata("design:paramtypes", [Object, Object, Object, Object])
241
+ ], PasswordResetService);
242
+ export { PasswordResetService };
@@ -0,0 +1,9 @@
1
+ import { Container, ServiceProvider } from "@veap/kernel";
2
+ import { type AuthConfig } from "../common/routes.js";
3
+ export declare class AuthServiceProvider extends ServiceProvider {
4
+ private config?;
5
+ constructor(container?: Container, config?: AuthConfig);
6
+ private resolveRoutes;
7
+ register(): void;
8
+ boot(): Promise<void>;
9
+ }
@@ -0,0 +1,199 @@
1
+ import { isDatabaseInitialized } from "@veap/database";
2
+ import { ConfigService, Container, container as defaultContainer, logger, ServiceProvider, } from "@veap/kernel";
3
+ import { bindAuthContext } from "../common/context.js";
4
+ import { AUTH_ROUTES, DEFAULT_AUTH_ROUTES, ROUTER_SERVICE, } from "../common/routes.js";
5
+ import { AesSecretCipher } from "../crypto/aes-cipher.js";
6
+ import { BcryptPasswordHasher } from "../crypto/bcrypt-hasher.js";
7
+ import { PASSWORD_HASHER, SECRET_CIPHER, TOKEN_GENERATOR, } from "../crypto/contracts.js";
8
+ import { OsloTokenGenerator } from "../crypto/token-generator.js";
9
+ import { runAuthMigrations } from "../migrations/index.js";
10
+ import { PASSWORD_RESET_REPOSITORY } from "../passwords/contracts.js";
11
+ import { ActiveRecordPasswordResetRepository } from "../passwords/password-reset.repository.js";
12
+ import { PasswordResetService } from "../passwords/password-reset.service.js";
13
+ import { PERMISSION_REPOSITORY, ROLE_REPOSITORY } from "../rbac/contracts.js";
14
+ import { ActiveRecordPermissionRepository, ActiveRecordRoleRepository, } from "../rbac/rbac.repository.js";
15
+ import { RbacService } from "../rbac/rbac.service.js";
16
+ import { AuthService } from "../session/auth.service.js";
17
+ import { SESSION_REPOSITORY } from "../session/contracts.js";
18
+ import { ActiveRecordSessionRepository } from "../session/session.repository.js";
19
+ import { SessionService } from "../session/session.service.js";
20
+ import { USER_REPOSITORY } from "../users/contracts.js";
21
+ import { ActiveRecordUserRepository } from "../users/user.repository.js";
22
+ import { UserService } from "../users/user.service.js";
23
+ import { EMAIL_VERIFICATION_REPOSITORY } from "../verification/contracts.js";
24
+ import { ActiveRecordEmailVerificationRepository } from "../verification/verification.repository.js";
25
+ import { EmailVerificationService } from "../verification/verification.service.js";
26
+ import { isSystemInstalled } from "./setup.js";
27
+ export class AuthServiceProvider extends ServiceProvider {
28
+ config;
29
+ constructor(container, config) {
30
+ super(container ?? defaultContainer);
31
+ this.config = config;
32
+ }
33
+ resolveRoutes() {
34
+ return {
35
+ ...DEFAULT_AUTH_ROUTES,
36
+ ...this.config?.routes,
37
+ };
38
+ }
39
+ register() {
40
+ const routes = this.resolveRoutes();
41
+ this.container.register({
42
+ token: AUTH_ROUTES,
43
+ useValue: routes,
44
+ });
45
+ // Cryptographic ports -> concrete adapters
46
+ this.container.register({
47
+ token: PASSWORD_HASHER,
48
+ useFactory: async (c) => {
49
+ let config;
50
+ try {
51
+ if (c && typeof c.resolve === "function" && c.has(ConfigService)) {
52
+ config = await c.resolve(ConfigService);
53
+ }
54
+ }
55
+ catch { }
56
+ const roundsRaw = config?.get("AUTH_BCRYPT_ROUNDS");
57
+ const minLengthRaw = config?.get("AUTH_PASSWORD_MIN_LENGTH");
58
+ const rounds = roundsRaw ? parseInt(roundsRaw, 10) : undefined;
59
+ const minLength = minLengthRaw ? parseInt(minLengthRaw, 10) : undefined;
60
+ return new BcryptPasswordHasher(rounds, minLength);
61
+ },
62
+ inject: [Container],
63
+ singleton: true,
64
+ });
65
+ this.container.register({
66
+ token: TOKEN_GENERATOR,
67
+ useClass: OsloTokenGenerator,
68
+ singleton: true,
69
+ });
70
+ this.container.register({
71
+ token: SECRET_CIPHER,
72
+ useClass: AesSecretCipher,
73
+ singleton: true,
74
+ });
75
+ // Repositories
76
+ this.container.register({
77
+ token: USER_REPOSITORY,
78
+ useClass: ActiveRecordUserRepository,
79
+ singleton: true,
80
+ });
81
+ this.container.register({
82
+ token: SESSION_REPOSITORY,
83
+ useClass: ActiveRecordSessionRepository,
84
+ singleton: true,
85
+ });
86
+ this.container.register({
87
+ token: ROLE_REPOSITORY,
88
+ useClass: ActiveRecordRoleRepository,
89
+ singleton: true,
90
+ });
91
+ this.container.register({
92
+ token: PERMISSION_REPOSITORY,
93
+ useClass: ActiveRecordPermissionRepository,
94
+ singleton: true,
95
+ });
96
+ this.container.register({
97
+ token: PASSWORD_RESET_REPOSITORY,
98
+ useClass: ActiveRecordPasswordResetRepository,
99
+ singleton: true,
100
+ });
101
+ this.container.register({
102
+ token: EMAIL_VERIFICATION_REPOSITORY,
103
+ useClass: ActiveRecordEmailVerificationRepository,
104
+ singleton: true,
105
+ });
106
+ // Services
107
+ this.container.register({
108
+ token: UserService,
109
+ useClass: UserService,
110
+ singleton: true,
111
+ });
112
+ this.container.register({
113
+ token: SessionService,
114
+ useClass: SessionService,
115
+ singleton: true,
116
+ });
117
+ this.container.register({
118
+ token: RbacService,
119
+ useClass: RbacService,
120
+ singleton: true,
121
+ });
122
+ this.container.register({
123
+ token: PasswordResetService,
124
+ useClass: PasswordResetService,
125
+ singleton: true,
126
+ });
127
+ this.container.register({
128
+ token: EmailVerificationService,
129
+ useClass: EmailVerificationService,
130
+ singleton: true,
131
+ });
132
+ this.container.register({
133
+ token: AuthService,
134
+ useClass: AuthService,
135
+ singleton: true,
136
+ });
137
+ }
138
+ async boot() {
139
+ // 0. Auto-bootstrap setup state
140
+ try {
141
+ isSystemInstalled();
142
+ }
143
+ catch { }
144
+ // 1. Resolve domain services and bind singleton auth context
145
+ const user = await this.container.resolve(UserService);
146
+ const session = await this.container.resolve(SessionService);
147
+ const rbac = await this.container.resolve(RbacService);
148
+ const passwordReset = await this.container.resolve(PasswordResetService);
149
+ const emailVerification = await this.container.resolve(EmailVerificationService);
150
+ const auth = await this.container.resolve(AuthService);
151
+ bindAuthContext({
152
+ user,
153
+ session,
154
+ rbac,
155
+ passwordReset,
156
+ emailVerification,
157
+ auth,
158
+ routes: this.resolveRoutes(),
159
+ });
160
+ // 2. Register route rewrites if RouterService is available
161
+ if (this.container.has(ROUTER_SERVICE)) {
162
+ const router = await this.container.resolve(ROUTER_SERVICE);
163
+ const routes = this.resolveRoutes();
164
+ const addRewrite = (from, to) => {
165
+ if (typeof router.addRewrite === "function") {
166
+ router.addRewrite(from, to);
167
+ }
168
+ else if (typeof router.registerRewrite === "function") {
169
+ router.registerRewrite(from, to);
170
+ }
171
+ };
172
+ if (routes.signIn !== DEFAULT_AUTH_ROUTES.signIn) {
173
+ addRewrite(routes.signIn, DEFAULT_AUTH_ROUTES.signIn);
174
+ }
175
+ if (routes.signUp !== DEFAULT_AUTH_ROUTES.signUp) {
176
+ addRewrite(routes.signUp, DEFAULT_AUTH_ROUTES.signUp);
177
+ }
178
+ if (routes.forgotPassword !== DEFAULT_AUTH_ROUTES.forgotPassword) {
179
+ addRewrite(routes.forgotPassword, DEFAULT_AUTH_ROUTES.forgotPassword);
180
+ }
181
+ if (routes.resetPassword !== DEFAULT_AUTH_ROUTES.resetPassword) {
182
+ addRewrite(routes.resetPassword, DEFAULT_AUTH_ROUTES.resetPassword);
183
+ }
184
+ if (routes.verifyEmail !== DEFAULT_AUTH_ROUTES.verifyEmail) {
185
+ addRewrite(routes.verifyEmail, DEFAULT_AUTH_ROUTES.verifyEmail);
186
+ }
187
+ }
188
+ // 3. Database migrations (when enabled and database is connected)
189
+ const shouldMigrate = this.config?.autoMigrate ?? true;
190
+ if (shouldMigrate && isDatabaseInitialized()) {
191
+ try {
192
+ await runAuthMigrations();
193
+ }
194
+ catch (error) {
195
+ logger.error("veap:auth", "Failed to run auth database migrations during boot:", error.message);
196
+ }
197
+ }
198
+ }
199
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./auth.provider.js";
2
+ export * from "./setup.js";
@@ -0,0 +1,2 @@
1
+ export * from "./auth.provider.js";
2
+ export * from "./setup.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Checks if the system is installed.
3
+ * A system is considered installed if at least one user exists in the database.
4
+ * This function also has a side-effect: it publishes the 'system:not-installed' event
5
+ * if it detects the system is not installed.
6
+ */
7
+ export declare function isSystemInstalled(): Promise<boolean>;
@@ -0,0 +1,29 @@
1
+ import { eventBus, logger } from "@veap/kernel";
2
+ import { User } from "../models/User.js";
3
+ /**
4
+ * Checks if the system is installed.
5
+ * A system is considered installed if at least one user exists in the database.
6
+ * This function also has a side-effect: it publishes the 'system:not-installed' event
7
+ * if it detects the system is not installed.
8
+ */
9
+ export async function isSystemInstalled() {
10
+ try {
11
+ const count = await User.query().count();
12
+ if (count === 0) {
13
+ logger.debug("veap:setup", "System not installed. Publishing event.");
14
+ await eventBus.publish("system:not-installed", { timestamp: Date.now() });
15
+ }
16
+ return count > 0;
17
+ }
18
+ catch (error) {
19
+ if (error?.digest?.startsWith("NEXT_REDIRECT")) {
20
+ // Re-throw redirect errors to let Next.js handle them
21
+ throw error;
22
+ }
23
+ // This typically happens on the very first run when the users table doesn't exist yet.
24
+ logger.debug("veap:setup", "isSystemInstalled check failed (most likely table does not exist)", error.message);
25
+ // If an error occurs (e.g., table not found), assume not installed and publish the event.
26
+ await eventBus.publish("system:not-installed", { timestamp: Date.now() });
27
+ return false;
28
+ }
29
+ }