@veap/auth 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +166 -2
- package/dist/actions/auth.actions.d.ts +23 -0
- package/dist/actions/auth.actions.js +40 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/client/auth.context.d.ts +29 -0
- package/dist/client/auth.context.js +64 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/common/augment.d.ts +26 -0
- package/dist/common/augment.js +63 -0
- package/dist/common/context.d.ts +29 -0
- package/dist/common/context.js +34 -0
- package/dist/common/index.d.ts +6 -0
- package/dist/common/index.js +6 -0
- package/dist/common/logic.d.ts +64 -0
- package/dist/common/logic.js +133 -0
- package/dist/common/registry.d.ts +28 -0
- package/dist/common/registry.js +76 -0
- package/dist/common/routes.d.ts +2 -0
- package/dist/common/routes.js +1 -0
- package/dist/common/types.d.ts +178 -0
- package/dist/common/types.js +9 -0
- package/dist/crypto/aes-cipher.d.ts +8 -0
- package/dist/crypto/aes-cipher.js +12 -0
- package/dist/crypto/bcrypt-hasher.d.ts +12 -0
- package/dist/crypto/bcrypt-hasher.js +37 -0
- package/dist/crypto/contracts.d.ts +36 -0
- package/dist/crypto/contracts.js +3 -0
- package/dist/crypto/encryption.d.ts +18 -0
- package/dist/crypto/encryption.js +69 -0
- package/dist/crypto/index.d.ts +6 -0
- package/dist/crypto/index.js +6 -0
- package/dist/crypto/password.d.ts +19 -0
- package/dist/crypto/password.js +27 -0
- package/dist/crypto/token-generator.d.ts +10 -0
- package/dist/crypto/token-generator.js +25 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/migrations/0001_create_auth_tables.d.ts +4 -0
- package/dist/migrations/0001_create_auth_tables.js +122 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.js +7 -0
- package/dist/models/EmailVerification.d.ts +23 -0
- package/dist/models/EmailVerification.js +19 -0
- package/dist/models/PasswordResetSession.d.ts +25 -0
- package/dist/models/PasswordResetSession.js +21 -0
- package/dist/models/Permission.d.ts +20 -0
- package/dist/models/Permission.js +19 -0
- package/dist/models/Role.d.ts +20 -0
- package/dist/models/Role.js +19 -0
- package/dist/models/Session.d.ts +22 -0
- package/dist/models/Session.js +21 -0
- package/dist/models/User.d.ts +32 -0
- package/dist/models/User.js +31 -0
- package/dist/models/index.d.ts +6 -0
- package/dist/models/index.js +6 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +1 -0
- package/dist/passwords/contracts.d.ts +28 -0
- package/dist/passwords/contracts.js +2 -0
- package/dist/passwords/index.d.ts +4 -0
- package/dist/passwords/index.js +4 -0
- package/dist/passwords/password-reset.facade.d.ts +14 -0
- package/dist/passwords/password-reset.facade.js +50 -0
- package/dist/passwords/password-reset.repository.d.ts +10 -0
- package/dist/passwords/password-reset.repository.js +39 -0
- package/dist/passwords/password-reset.service.d.ts +38 -0
- package/dist/passwords/password-reset.service.js +242 -0
- package/dist/providers/auth.provider.d.ts +9 -0
- package/dist/providers/auth.provider.js +196 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/setup.d.ts +7 -0
- package/dist/providers/setup.js +29 -0
- package/dist/rbac/contracts.d.ts +75 -0
- package/dist/rbac/contracts.js +4 -0
- package/dist/rbac/index.d.ts +4 -0
- package/dist/rbac/index.js +4 -0
- package/dist/rbac/rbac.facade.d.ts +23 -0
- package/dist/rbac/rbac.facade.js +78 -0
- package/dist/rbac/rbac.repository.d.ts +30 -0
- package/dist/rbac/rbac.repository.js +137 -0
- package/dist/rbac/rbac.service.d.ts +35 -0
- package/dist/rbac/rbac.service.js +143 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +3 -0
- package/dist/session/auth.service.d.ts +35 -0
- package/dist/session/auth.service.js +208 -0
- package/dist/session/contracts.d.ts +33 -0
- package/dist/session/contracts.js +2 -0
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.js +5 -0
- package/dist/session/session.facade.d.ts +14 -0
- package/dist/session/session.facade.js +75 -0
- package/dist/session/session.repository.d.ts +15 -0
- package/dist/session/session.repository.js +55 -0
- package/dist/session/session.service.d.ts +63 -0
- package/dist/session/session.service.js +219 -0
- package/dist/users/contracts.d.ts +32 -0
- package/dist/users/contracts.js +2 -0
- package/dist/users/index.d.ts +4 -0
- package/dist/users/index.js +4 -0
- package/dist/users/user.facade.d.ts +14 -0
- package/dist/users/user.facade.js +62 -0
- package/dist/users/user.repository.d.ts +15 -0
- package/dist/users/user.repository.js +55 -0
- package/dist/users/user.service.d.ts +68 -0
- package/dist/users/user.service.js +183 -0
- package/dist/validation/index.d.ts +95 -0
- package/dist/validation/index.js +55 -0
- package/dist/verification/contracts.d.ts +25 -0
- package/dist/verification/contracts.js +2 -0
- package/dist/verification/index.d.ts +4 -0
- package/dist/verification/index.js +4 -0
- package/dist/verification/verification.facade.d.ts +10 -0
- package/dist/verification/verification.facade.js +42 -0
- package/dist/verification/verification.repository.d.ts +7 -0
- package/dist/verification/verification.repository.js +23 -0
- package/dist/verification/verification.service.d.ts +44 -0
- package/dist/verification/verification.service.js +120 -0
- package/package.json +88 -4
|
@@ -0,0 +1,219 @@
|
|
|
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 { isIP } from "node:net";
|
|
14
|
+
import { COOKIE_STORE, Inject, Injectable, REQUEST_CONTEXT, } from "@veap/kernel";
|
|
15
|
+
import { addDays } from "date-fns";
|
|
16
|
+
import { augmentSession } from "../common/augment";
|
|
17
|
+
import { performFullUserAugmentation } from "../common/logic";
|
|
18
|
+
import { TOKEN_GENERATOR } from "../crypto/contracts";
|
|
19
|
+
import { SESSION_REPOSITORY } from "./contracts";
|
|
20
|
+
let SessionService = class SessionService {
|
|
21
|
+
sessions;
|
|
22
|
+
tokens;
|
|
23
|
+
cookieStore;
|
|
24
|
+
requestContext;
|
|
25
|
+
constructor(sessions, tokens, cookieStore, requestContext) {
|
|
26
|
+
this.sessions = sessions;
|
|
27
|
+
this.tokens = tokens;
|
|
28
|
+
this.cookieStore = cookieStore;
|
|
29
|
+
this.requestContext = requestContext;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns the user's validated IP address, checking trusted proxy headers first.
|
|
33
|
+
*/
|
|
34
|
+
async getIPAddress() {
|
|
35
|
+
const directHeaders = ["cf-connecting-ip", "x-real-ip", "x-client-ip"];
|
|
36
|
+
for (const header of directHeaders) {
|
|
37
|
+
const val = await this.requestContext.getHeader(header);
|
|
38
|
+
if (val && isIP(val.trim())) {
|
|
39
|
+
return val.trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const forwarded = await this.requestContext.getHeader("x-forwarded-for");
|
|
43
|
+
if (forwarded) {
|
|
44
|
+
const ips = forwarded.split(",").map((ip) => ip.trim());
|
|
45
|
+
for (const ip of ips) {
|
|
46
|
+
if (isIP(ip)) {
|
|
47
|
+
return ip;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validates the session token.
|
|
55
|
+
*/
|
|
56
|
+
async validateSessionToken(token) {
|
|
57
|
+
const sessionId = this.tokens.hashToken(token);
|
|
58
|
+
const record = await this.sessions.findWithUser(sessionId);
|
|
59
|
+
if (!record) {
|
|
60
|
+
return { session: null, user: null };
|
|
61
|
+
}
|
|
62
|
+
const baseUserAttributes = record.user;
|
|
63
|
+
const baseSessionAttributes = record.session;
|
|
64
|
+
// STRICTLY remove non-serializable and sensitive fields (including camelCase recoveryCode)
|
|
65
|
+
const { password, recovery_code, recoveryCode, ...safeUserRest } = baseUserAttributes;
|
|
66
|
+
const safeUser = {
|
|
67
|
+
...safeUserRest,
|
|
68
|
+
id: baseUserAttributes.id || safeUserRest.id,
|
|
69
|
+
};
|
|
70
|
+
const expiresAt = baseSessionAttributes.expiresAt ?? baseSessionAttributes.expires_at;
|
|
71
|
+
// Check if session is expired
|
|
72
|
+
if (new Date() > new Date(expiresAt)) {
|
|
73
|
+
await this.sessions.remove(sessionId);
|
|
74
|
+
return { session: null, user: null };
|
|
75
|
+
}
|
|
76
|
+
const metadata = baseSessionAttributes.metadata || {};
|
|
77
|
+
// Restore metadata into session object
|
|
78
|
+
const sessionWithFlags = {
|
|
79
|
+
...baseSessionAttributes,
|
|
80
|
+
id: baseSessionAttributes.id,
|
|
81
|
+
userId: baseSessionAttributes.userId ?? baseSessionAttributes.user_id,
|
|
82
|
+
expiresAt,
|
|
83
|
+
metadata,
|
|
84
|
+
...metadata,
|
|
85
|
+
};
|
|
86
|
+
// AUGMENT (EXTENSIBILITY POINTS)
|
|
87
|
+
const augmentedUser = await performFullUserAugmentation(safeUser, sessionWithFlags);
|
|
88
|
+
const augmentedSession = await augmentSession(sessionWithFlags);
|
|
89
|
+
// ENSURE PLAIN OBJECTS for Client Components
|
|
90
|
+
return {
|
|
91
|
+
session: augmentedSession ? { ...augmentedSession } : null,
|
|
92
|
+
user: augmentedUser ? { ...augmentedUser } : null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns the current user session from cookies.
|
|
97
|
+
*/
|
|
98
|
+
async getCurrentSession() {
|
|
99
|
+
const token = await this.cookieStore.get("session");
|
|
100
|
+
if (token === null) {
|
|
101
|
+
return { session: null, user: null };
|
|
102
|
+
}
|
|
103
|
+
return await this.validateSessionToken(token);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Invalidates a single session.
|
|
107
|
+
*/
|
|
108
|
+
async invalidateSession(sessionId) {
|
|
109
|
+
await this.sessions.remove(sessionId);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Invalidates all user sessions.
|
|
113
|
+
*/
|
|
114
|
+
async invalidateUserSessions(userId) {
|
|
115
|
+
await this.sessions.removeByUserId(userId);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Sets the session token in a cookie.
|
|
119
|
+
*/
|
|
120
|
+
async setSessionTokenCookie(token, expiresAt) {
|
|
121
|
+
await this.cookieStore.set("session", token, {
|
|
122
|
+
httpOnly: true,
|
|
123
|
+
path: "/",
|
|
124
|
+
secure: process.env.NODE_ENV === "production",
|
|
125
|
+
sameSite: "lax",
|
|
126
|
+
expires: expiresAt,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Removes the session token cookie.
|
|
131
|
+
*/
|
|
132
|
+
async deleteSessionTokenCookie() {
|
|
133
|
+
await this.cookieStore.delete("session");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Generates a new random session token.
|
|
137
|
+
*/
|
|
138
|
+
async generateSessionToken() {
|
|
139
|
+
return this.tokens.generateSessionToken();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Creates a new session in the database.
|
|
143
|
+
*/
|
|
144
|
+
async createSession(token, userId, flags) {
|
|
145
|
+
const sessionId = this.tokens.hashToken(token);
|
|
146
|
+
return await this.sessions.create({
|
|
147
|
+
id: sessionId,
|
|
148
|
+
expiresAt: new Date(addDays(new Date(), 7)),
|
|
149
|
+
userId,
|
|
150
|
+
metadata: flags,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Updates the current session metadata.
|
|
155
|
+
*/
|
|
156
|
+
async updateSessionMetadata(flags) {
|
|
157
|
+
const { session } = await this.getCurrentSession();
|
|
158
|
+
if (!session)
|
|
159
|
+
return;
|
|
160
|
+
const current = await this.sessions.findById(session.id);
|
|
161
|
+
if (!current)
|
|
162
|
+
return;
|
|
163
|
+
const newMetadata = {
|
|
164
|
+
...(current.metadata || {}),
|
|
165
|
+
...flags,
|
|
166
|
+
};
|
|
167
|
+
await this.sessions.updateMetadata(session.id, newMetadata);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Signs the user out and redirects to the sign-in page.
|
|
171
|
+
*/
|
|
172
|
+
async sessionSignOut() {
|
|
173
|
+
const { session } = await this.getCurrentSession();
|
|
174
|
+
if (session) {
|
|
175
|
+
await this.invalidateSession(session.id);
|
|
176
|
+
await this.deleteSessionTokenCookie();
|
|
177
|
+
}
|
|
178
|
+
await this.requestContext.redirect("/signin");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get all active sessions for a user.
|
|
182
|
+
*/
|
|
183
|
+
async getUserSessions(userId, currentSessionId) {
|
|
184
|
+
const sessions = await this.sessions.findByUserId(userId);
|
|
185
|
+
const augmentedSessions = await Promise.all(sessions.map(async (session) => {
|
|
186
|
+
const metadata = session.metadata || {};
|
|
187
|
+
const sessionWithFlags = {
|
|
188
|
+
...session,
|
|
189
|
+
id: session.id,
|
|
190
|
+
userId: session.userId ?? session.user_id,
|
|
191
|
+
expiresAt: session.expiresAt ?? session.expires_at,
|
|
192
|
+
metadata,
|
|
193
|
+
...metadata,
|
|
194
|
+
};
|
|
195
|
+
// Run augmenters
|
|
196
|
+
const augmented = await augmentSession(sessionWithFlags);
|
|
197
|
+
return {
|
|
198
|
+
...augmented,
|
|
199
|
+
isCurrent: augmented.id === currentSessionId,
|
|
200
|
+
};
|
|
201
|
+
}));
|
|
202
|
+
return augmentedSessions;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Invalidate all sessions for a user except the specified current one.
|
|
206
|
+
*/
|
|
207
|
+
async invalidateOtherSessions(userId, currentSessionId) {
|
|
208
|
+
await this.sessions.removeOtherUserSessions(userId, currentSessionId);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
SessionService = __decorate([
|
|
212
|
+
Injectable(),
|
|
213
|
+
__param(0, Inject(SESSION_REPOSITORY)),
|
|
214
|
+
__param(1, Inject(TOKEN_GENERATOR)),
|
|
215
|
+
__param(2, Inject(COOKIE_STORE)),
|
|
216
|
+
__param(3, Inject(REQUEST_CONTEXT)),
|
|
217
|
+
__metadata("design:paramtypes", [Object, Object, Object, Object])
|
|
218
|
+
], SessionService);
|
|
219
|
+
export { SessionService };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FullUser, User } from "../common";
|
|
2
|
+
export type { FullUser, User };
|
|
3
|
+
export interface CreateUserRecord {
|
|
4
|
+
email: string;
|
|
5
|
+
name: string;
|
|
6
|
+
password?: string | null;
|
|
7
|
+
image?: string | null;
|
|
8
|
+
emailVerifiedAt?: Date | null;
|
|
9
|
+
recoveryCode: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
export interface UpdateUserRecord {
|
|
12
|
+
name?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
image?: string | null;
|
|
15
|
+
password?: string;
|
|
16
|
+
emailVerifiedAt?: Date | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* User persistence port.
|
|
20
|
+
*/
|
|
21
|
+
export interface IUserRepository {
|
|
22
|
+
create(record: CreateUserRecord): Promise<User>;
|
|
23
|
+
findById(id: string): Promise<User | null>;
|
|
24
|
+
findByEmail(email: string): Promise<User | null>;
|
|
25
|
+
findPasswordHash(id: string): Promise<string | null | undefined>;
|
|
26
|
+
findRecoveryCode(id: string): Promise<Uint8Array | null | undefined>;
|
|
27
|
+
update(id: string, changes: UpdateUserRecord): Promise<void>;
|
|
28
|
+
setRecoveryCode(id: string, encrypted: Uint8Array): Promise<void>;
|
|
29
|
+
setEmailVerifiedIfEmailMatches(id: string, email: string): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
/** Injection token for the user repository port. */
|
|
32
|
+
export declare const USER_REPOSITORY: unique symbol;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { User } from "../common";
|
|
2
|
+
export declare function verifyUsernameInput(username: string): Promise<boolean>;
|
|
3
|
+
export declare function createUser(email: string, username: string, password: string): Promise<User>;
|
|
4
|
+
export declare function createOAuthUser(email: string, name: string, image?: string): Promise<User>;
|
|
5
|
+
export declare function getUserById(userId: string): Promise<User | null>;
|
|
6
|
+
export declare function getUserRecoverCode(userId: string): Promise<string>;
|
|
7
|
+
export declare function resetUserRecoveryCode(userId: string): Promise<string>;
|
|
8
|
+
export declare function updateUserPassword(userId: string, password: string): Promise<void>;
|
|
9
|
+
export declare function updateUserName(userId: string, name: string): Promise<void>;
|
|
10
|
+
export declare function updateUserAwatar(userId: string, image: string): Promise<void>;
|
|
11
|
+
export declare function updateUserEmailAndSetEmailAsVerified(userId: string, email: string): Promise<void>;
|
|
12
|
+
export declare function setUserAsEmailVerifiedIfEmailMatches(userId: string, email: string): Promise<boolean>;
|
|
13
|
+
export declare function getUserPasswordHash(userId: string): Promise<string | null>;
|
|
14
|
+
export declare function getUserFromEmail(email: string): Promise<User | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { app } from "@veap/kernel";
|
|
3
|
+
import { authContext, isAuthContextBound } from "../common/context";
|
|
4
|
+
import { UserService } from "./user.service";
|
|
5
|
+
async function resolveUserService() {
|
|
6
|
+
if (isAuthContextBound()) {
|
|
7
|
+
return authContext().user;
|
|
8
|
+
}
|
|
9
|
+
return await app(UserService);
|
|
10
|
+
}
|
|
11
|
+
export async function verifyUsernameInput(username) {
|
|
12
|
+
const service = await resolveUserService();
|
|
13
|
+
return service.verifyUsernameInput(username);
|
|
14
|
+
}
|
|
15
|
+
export async function createUser(email, username, password) {
|
|
16
|
+
const service = await resolveUserService();
|
|
17
|
+
return service.createUser(email, username, password);
|
|
18
|
+
}
|
|
19
|
+
export async function createOAuthUser(email, name, image) {
|
|
20
|
+
const service = await resolveUserService();
|
|
21
|
+
return service.createOAuthUser(email, name, image);
|
|
22
|
+
}
|
|
23
|
+
export async function getUserById(userId) {
|
|
24
|
+
const service = await resolveUserService();
|
|
25
|
+
return service.getUserById(userId);
|
|
26
|
+
}
|
|
27
|
+
export async function getUserRecoverCode(userId) {
|
|
28
|
+
const service = await resolveUserService();
|
|
29
|
+
return service.getUserRecoverCode(userId);
|
|
30
|
+
}
|
|
31
|
+
export async function resetUserRecoveryCode(userId) {
|
|
32
|
+
const service = await resolveUserService();
|
|
33
|
+
return service.resetUserRecoveryCode(userId);
|
|
34
|
+
}
|
|
35
|
+
export async function updateUserPassword(userId, password) {
|
|
36
|
+
const service = await resolveUserService();
|
|
37
|
+
return service.updateUserPassword(userId, password);
|
|
38
|
+
}
|
|
39
|
+
export async function updateUserName(userId, name) {
|
|
40
|
+
const service = await resolveUserService();
|
|
41
|
+
return service.updateUserName(userId, name);
|
|
42
|
+
}
|
|
43
|
+
export async function updateUserAwatar(userId, image) {
|
|
44
|
+
const service = await resolveUserService();
|
|
45
|
+
return service.updateUserAwatar(userId, image);
|
|
46
|
+
}
|
|
47
|
+
export async function updateUserEmailAndSetEmailAsVerified(userId, email) {
|
|
48
|
+
const service = await resolveUserService();
|
|
49
|
+
return service.updateUserEmailAndSetEmailAsVerified(userId, email);
|
|
50
|
+
}
|
|
51
|
+
export async function setUserAsEmailVerifiedIfEmailMatches(userId, email) {
|
|
52
|
+
const service = await resolveUserService();
|
|
53
|
+
return service.setUserAsEmailVerifiedIfEmailMatches(userId, email);
|
|
54
|
+
}
|
|
55
|
+
export async function getUserPasswordHash(userId) {
|
|
56
|
+
const service = await resolveUserService();
|
|
57
|
+
return service.getUserPasswordHash(userId);
|
|
58
|
+
}
|
|
59
|
+
export async function getUserFromEmail(email) {
|
|
60
|
+
const service = await resolveUserService();
|
|
61
|
+
return service.getUserFromEmail(email);
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { User as UserEntity } from "../common";
|
|
2
|
+
import type { CreateUserRecord, IUserRepository, UpdateUserRecord } from "./contracts";
|
|
3
|
+
/**
|
|
4
|
+
* `IUserRepository` adapter backed by the `User` ActiveRecord model.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ActiveRecordUserRepository implements IUserRepository {
|
|
7
|
+
create(record: CreateUserRecord): Promise<UserEntity>;
|
|
8
|
+
findById(id: string): Promise<UserEntity | null>;
|
|
9
|
+
findByEmail(email: string): Promise<UserEntity | null>;
|
|
10
|
+
findPasswordHash(id: string): Promise<string | null | undefined>;
|
|
11
|
+
findRecoveryCode(id: string): Promise<Uint8Array | null | undefined>;
|
|
12
|
+
update(id: string, changes: UpdateUserRecord): Promise<void>;
|
|
13
|
+
setRecoveryCode(id: string, encrypted: Uint8Array): Promise<void>;
|
|
14
|
+
setEmailVerifiedIfEmailMatches(id: string, email: string): Promise<boolean>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { User } from "../models/User";
|
|
2
|
+
/**
|
|
3
|
+
* `IUserRepository` adapter backed by the `User` ActiveRecord model.
|
|
4
|
+
*/
|
|
5
|
+
export class ActiveRecordUserRepository {
|
|
6
|
+
async create(record) {
|
|
7
|
+
const user = await User.create({
|
|
8
|
+
email: record.email,
|
|
9
|
+
name: record.name,
|
|
10
|
+
password: record.password ?? null,
|
|
11
|
+
image: record.image ?? null,
|
|
12
|
+
emailVerifiedAt: record.emailVerifiedAt ?? null,
|
|
13
|
+
recovery_code: record.recoveryCode,
|
|
14
|
+
});
|
|
15
|
+
return user.toJSON();
|
|
16
|
+
}
|
|
17
|
+
async findById(id) {
|
|
18
|
+
const user = await User.find(id);
|
|
19
|
+
return user ? user.toJSON() : null;
|
|
20
|
+
}
|
|
21
|
+
async findByEmail(email) {
|
|
22
|
+
const user = await User.where("email", email).first();
|
|
23
|
+
return user ? user.toJSON() : null;
|
|
24
|
+
}
|
|
25
|
+
async findPasswordHash(id) {
|
|
26
|
+
const user = await User.find(id);
|
|
27
|
+
if (!user)
|
|
28
|
+
return undefined;
|
|
29
|
+
return user.password ?? null;
|
|
30
|
+
}
|
|
31
|
+
async findRecoveryCode(id) {
|
|
32
|
+
const user = await User.find(id);
|
|
33
|
+
if (!user)
|
|
34
|
+
return undefined;
|
|
35
|
+
return user.recovery_code ?? null;
|
|
36
|
+
}
|
|
37
|
+
async update(id, changes) {
|
|
38
|
+
if (Object.keys(changes).length === 0)
|
|
39
|
+
return;
|
|
40
|
+
await User.where("id", id).update(changes);
|
|
41
|
+
}
|
|
42
|
+
async setRecoveryCode(id, encrypted) {
|
|
43
|
+
const user = await User.find(id);
|
|
44
|
+
if (!user)
|
|
45
|
+
return;
|
|
46
|
+
user.setAttribute("recovery_code", Buffer.from(encrypted));
|
|
47
|
+
await user.save();
|
|
48
|
+
}
|
|
49
|
+
async setEmailVerifiedIfEmailMatches(id, email) {
|
|
50
|
+
const updated = await User.where({ id, email }).update({
|
|
51
|
+
emailVerifiedAt: new Date(),
|
|
52
|
+
});
|
|
53
|
+
return updated > 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type IPasswordHasher, type ISecretCipher, type ITokenGenerator } from "../crypto/contracts";
|
|
2
|
+
import type { User } from "../common";
|
|
3
|
+
import { type IRoleRepository } from "../rbac/contracts";
|
|
4
|
+
import { type IUserRepository } from "./contracts";
|
|
5
|
+
export declare class UserService {
|
|
6
|
+
private readonly users;
|
|
7
|
+
private readonly roles;
|
|
8
|
+
private readonly hasher;
|
|
9
|
+
private readonly tokens;
|
|
10
|
+
private readonly cipher;
|
|
11
|
+
constructor(users: IUserRepository, roles: IRoleRepository, hasher: IPasswordHasher, tokens: ITokenGenerator, cipher: ISecretCipher);
|
|
12
|
+
/**
|
|
13
|
+
* Validates the username input.
|
|
14
|
+
*/
|
|
15
|
+
verifyUsernameInput(username: string): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new user with an initial recovery code and default 'user' role.
|
|
18
|
+
*/
|
|
19
|
+
createUser(email: string, username: string, password: string): Promise<User>;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new user from an OAuth provider.
|
|
22
|
+
*/
|
|
23
|
+
createOAuthUser(email: string, name: string, image?: string): Promise<User>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns a user by ID.
|
|
26
|
+
*/
|
|
27
|
+
getUserById(userId: string): Promise<User | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Decrypts and returns the user's recovery code.
|
|
30
|
+
*/
|
|
31
|
+
getUserRecoverCode(userId: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Generates and sets a new recovery code for the user.
|
|
34
|
+
*/
|
|
35
|
+
resetUserRecoveryCode(userId: string): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Updates the user's password.
|
|
38
|
+
*/
|
|
39
|
+
updateUserPassword(userId: string, password: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Updates the user's name.
|
|
42
|
+
*/
|
|
43
|
+
updateUserName(userId: string, name: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Updates the user's image.
|
|
46
|
+
*/
|
|
47
|
+
updateUserAwatar(userId: string, image: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Updates the user's email and marks it as verified.
|
|
50
|
+
*/
|
|
51
|
+
updateUserEmailAndSetEmailAsVerified(userId: string, email: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Sets the user as email verified if the provided email matches.
|
|
54
|
+
*/
|
|
55
|
+
setUserAsEmailVerifiedIfEmailMatches(userId: string, email: string): Promise<boolean>;
|
|
56
|
+
/**
|
|
57
|
+
* Returns the user's password hash.
|
|
58
|
+
*/
|
|
59
|
+
getUserPasswordHash(userId: string): Promise<string | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Returns a user by email.
|
|
62
|
+
*/
|
|
63
|
+
getUserFromEmail(email: string): Promise<User | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Ensures the default 'user' role exists and assigns it.
|
|
66
|
+
*/
|
|
67
|
+
private assignDefaultRole;
|
|
68
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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 { AppError, Inject, Injectable } from "@veap/kernel";
|
|
14
|
+
import { sendRecoveryCode } from "@veap/mail";
|
|
15
|
+
import { PASSWORD_HASHER, SECRET_CIPHER, TOKEN_GENERATOR, } from "../crypto/contracts";
|
|
16
|
+
import { ROLE_REPOSITORY } from "../rbac/contracts";
|
|
17
|
+
import { USER_REPOSITORY } from "./contracts";
|
|
18
|
+
const DEFAULT_ROLE = "user";
|
|
19
|
+
let UserService = class UserService {
|
|
20
|
+
users;
|
|
21
|
+
roles;
|
|
22
|
+
hasher;
|
|
23
|
+
tokens;
|
|
24
|
+
cipher;
|
|
25
|
+
constructor(users, roles, hasher, tokens, cipher) {
|
|
26
|
+
this.users = users;
|
|
27
|
+
this.roles = roles;
|
|
28
|
+
this.hasher = hasher;
|
|
29
|
+
this.tokens = tokens;
|
|
30
|
+
this.cipher = cipher;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validates the username input.
|
|
34
|
+
*/
|
|
35
|
+
async verifyUsernameInput(username) {
|
|
36
|
+
return (username.length > 3 &&
|
|
37
|
+
username.length < 32 &&
|
|
38
|
+
username.trim() === username);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new user with an initial recovery code and default 'user' role.
|
|
42
|
+
*/
|
|
43
|
+
async createUser(email, username, password) {
|
|
44
|
+
const passwordHash = await this.hasher.hash(password);
|
|
45
|
+
const recoveryCode = this.tokens.generateRecoveryCode();
|
|
46
|
+
const user = await this.users.create({
|
|
47
|
+
email,
|
|
48
|
+
name: username,
|
|
49
|
+
password: passwordHash,
|
|
50
|
+
recoveryCode: this.cipher.encrypt(recoveryCode),
|
|
51
|
+
});
|
|
52
|
+
await this.assignDefaultRole(user.id);
|
|
53
|
+
try {
|
|
54
|
+
await sendRecoveryCode(user.email, recoveryCode);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.warn(`[veap:auth] Failed to send recovery code email to ${user.email}:`, error?.message || error);
|
|
58
|
+
}
|
|
59
|
+
return user;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a new user from an OAuth provider.
|
|
63
|
+
*/
|
|
64
|
+
async createOAuthUser(email, name, image) {
|
|
65
|
+
const recoveryCode = this.tokens.generateRecoveryCode();
|
|
66
|
+
const user = await this.users.create({
|
|
67
|
+
email,
|
|
68
|
+
name,
|
|
69
|
+
image,
|
|
70
|
+
emailVerifiedAt: new Date(),
|
|
71
|
+
recoveryCode: this.cipher.encrypt(recoveryCode),
|
|
72
|
+
});
|
|
73
|
+
await this.assignDefaultRole(user.id);
|
|
74
|
+
return user;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns a user by ID.
|
|
78
|
+
*/
|
|
79
|
+
async getUserById(userId) {
|
|
80
|
+
return await this.users.findById(userId);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Decrypts and returns the user's recovery code.
|
|
84
|
+
*/
|
|
85
|
+
async getUserRecoverCode(userId) {
|
|
86
|
+
const stored = await this.users.findRecoveryCode(userId);
|
|
87
|
+
if (!stored) {
|
|
88
|
+
throw AppError.BadRequest("Recovery code not found for user");
|
|
89
|
+
}
|
|
90
|
+
return this.cipher.decryptToString(stored);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generates and sets a new recovery code for the user.
|
|
94
|
+
*/
|
|
95
|
+
async resetUserRecoveryCode(userId) {
|
|
96
|
+
const user = await this.users.findById(userId);
|
|
97
|
+
if (!user) {
|
|
98
|
+
throw AppError.BadRequest("User not found");
|
|
99
|
+
}
|
|
100
|
+
const recoveryCode = this.tokens.generateRecoveryCode();
|
|
101
|
+
await this.users.setRecoveryCode(userId, this.cipher.encrypt(recoveryCode));
|
|
102
|
+
try {
|
|
103
|
+
await sendRecoveryCode(user.email, recoveryCode);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.warn(`[veap:auth] Failed to send recovery code email to ${user.email}:`, error?.message || error);
|
|
107
|
+
}
|
|
108
|
+
return recoveryCode;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Updates the user's password.
|
|
112
|
+
*/
|
|
113
|
+
async updateUserPassword(userId, password) {
|
|
114
|
+
const passwordHash = await this.hasher.hash(password);
|
|
115
|
+
await this.users.update(userId, { password: passwordHash });
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Updates the user's name.
|
|
119
|
+
*/
|
|
120
|
+
async updateUserName(userId, name) {
|
|
121
|
+
await this.users.update(userId, { name });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Updates the user's image.
|
|
125
|
+
*/
|
|
126
|
+
async updateUserAwatar(userId, image) {
|
|
127
|
+
await this.users.update(userId, { image });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Updates the user's email and marks it as verified.
|
|
131
|
+
*/
|
|
132
|
+
async updateUserEmailAndSetEmailAsVerified(userId, email) {
|
|
133
|
+
await this.users.update(userId, {
|
|
134
|
+
email,
|
|
135
|
+
emailVerifiedAt: new Date(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sets the user as email verified if the provided email matches.
|
|
140
|
+
*/
|
|
141
|
+
async setUserAsEmailVerifiedIfEmailMatches(userId, email) {
|
|
142
|
+
return await this.users.setEmailVerifiedIfEmailMatches(userId, email);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Returns the user's password hash.
|
|
146
|
+
*/
|
|
147
|
+
async getUserPasswordHash(userId) {
|
|
148
|
+
const hash = await this.users.findPasswordHash(userId);
|
|
149
|
+
if (hash === undefined) {
|
|
150
|
+
throw AppError.BadRequest("User not found");
|
|
151
|
+
}
|
|
152
|
+
return hash;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns a user by email.
|
|
156
|
+
*/
|
|
157
|
+
async getUserFromEmail(email) {
|
|
158
|
+
return await this.users.findByEmail(email);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Ensures the default 'user' role exists and assigns it.
|
|
162
|
+
*/
|
|
163
|
+
async assignDefaultRole(userId) {
|
|
164
|
+
let role = await this.roles.findByName(DEFAULT_ROLE);
|
|
165
|
+
if (!role) {
|
|
166
|
+
role = await this.roles.create({
|
|
167
|
+
name: DEFAULT_ROLE,
|
|
168
|
+
description: "Default user role",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
await this.roles.assignToUser(userId, role.id);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
UserService = __decorate([
|
|
175
|
+
Injectable(),
|
|
176
|
+
__param(0, Inject(USER_REPOSITORY)),
|
|
177
|
+
__param(1, Inject(ROLE_REPOSITORY)),
|
|
178
|
+
__param(2, Inject(PASSWORD_HASHER)),
|
|
179
|
+
__param(3, Inject(TOKEN_GENERATOR)),
|
|
180
|
+
__param(4, Inject(SECRET_CIPHER)),
|
|
181
|
+
__metadata("design:paramtypes", [Object, Object, Object, Object, Object])
|
|
182
|
+
], UserService);
|
|
183
|
+
export { UserService };
|