@meridianjs/auth 0.1.8 → 0.1.10
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/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +89 -1
- package/dist/index.mjs +90 -2
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -61,6 +61,31 @@ declare class AuthModuleService extends AuthModuleService_base {
|
|
|
61
61
|
* 3. Create new user
|
|
62
62
|
*/
|
|
63
63
|
loginOrRegisterWithGoogle(input: GoogleAuthInput): Promise<AuthResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Restore a soft-deleted user via an invite link.
|
|
66
|
+
* Updates their name, password, and role, then issues a fresh session token.
|
|
67
|
+
* The user's ID — and all history tied to it — is preserved.
|
|
68
|
+
*/
|
|
69
|
+
restoreFromInvite(userId: string, input: {
|
|
70
|
+
password: string;
|
|
71
|
+
first_name?: string;
|
|
72
|
+
last_name?: string;
|
|
73
|
+
role?: UserRole;
|
|
74
|
+
}): Promise<AuthResult>;
|
|
75
|
+
/** Set (or reset) password for a user. Used after OTP verification. */
|
|
76
|
+
setPassword(userId: string, newPassword: string): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Generate a password reset token for the given email.
|
|
79
|
+
* Returns the token and user info so the caller can emit an event / send an email.
|
|
80
|
+
* Returns null (instead of throwing) when the email isn't found — prevents enumeration.
|
|
81
|
+
*/
|
|
82
|
+
requestPasswordReset(email: string): Promise<{
|
|
83
|
+
token: string;
|
|
84
|
+
userId: string;
|
|
85
|
+
email: string;
|
|
86
|
+
} | null>;
|
|
87
|
+
/** Validate a password reset token and set the new password. */
|
|
88
|
+
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
64
89
|
/** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
|
|
65
90
|
verifyToken(token: string, secret: string): JwtPayload;
|
|
66
91
|
/** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
|
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,31 @@ declare class AuthModuleService extends AuthModuleService_base {
|
|
|
61
61
|
* 3. Create new user
|
|
62
62
|
*/
|
|
63
63
|
loginOrRegisterWithGoogle(input: GoogleAuthInput): Promise<AuthResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Restore a soft-deleted user via an invite link.
|
|
66
|
+
* Updates their name, password, and role, then issues a fresh session token.
|
|
67
|
+
* The user's ID — and all history tied to it — is preserved.
|
|
68
|
+
*/
|
|
69
|
+
restoreFromInvite(userId: string, input: {
|
|
70
|
+
password: string;
|
|
71
|
+
first_name?: string;
|
|
72
|
+
last_name?: string;
|
|
73
|
+
role?: UserRole;
|
|
74
|
+
}): Promise<AuthResult>;
|
|
75
|
+
/** Set (or reset) password for a user. Used after OTP verification. */
|
|
76
|
+
setPassword(userId: string, newPassword: string): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Generate a password reset token for the given email.
|
|
79
|
+
* Returns the token and user info so the caller can emit an event / send an email.
|
|
80
|
+
* Returns null (instead of throwing) when the email isn't found — prevents enumeration.
|
|
81
|
+
*/
|
|
82
|
+
requestPasswordReset(email: string): Promise<{
|
|
83
|
+
token: string;
|
|
84
|
+
userId: string;
|
|
85
|
+
email: string;
|
|
86
|
+
} | null>;
|
|
87
|
+
/** Validate a password reset token and set the new password. */
|
|
88
|
+
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
64
89
|
/** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
|
|
65
90
|
verifyToken(token: string, secret: string): JwtPayload;
|
|
66
91
|
/** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ var import_crypto = require("crypto");
|
|
|
49
49
|
var BCRYPT_ROUNDS = 12;
|
|
50
50
|
var JWT_EXPIRES_IN = "7d";
|
|
51
51
|
var JWT_EXPIRES_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
52
|
+
var RESET_TOKEN_EXPIRES_MS = 30 * 60 * 1e3;
|
|
52
53
|
var AuthModuleService = class extends (0, import_framework_utils.MeridianService)({}) {
|
|
53
54
|
container;
|
|
54
55
|
constructor(container) {
|
|
@@ -99,6 +100,9 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
99
100
|
if (!user) {
|
|
100
101
|
throw Object.assign(new Error("Invalid credentials"), { status: 401 });
|
|
101
102
|
}
|
|
103
|
+
if (user.deleted_at) {
|
|
104
|
+
throw Object.assign(new Error("Invalid credentials"), { status: 401 });
|
|
105
|
+
}
|
|
102
106
|
if (!user.is_active) {
|
|
103
107
|
throw Object.assign(new Error("Account deactivated"), { status: 403 });
|
|
104
108
|
}
|
|
@@ -137,16 +141,23 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
137
141
|
if (existingByEmail) {
|
|
138
142
|
throw Object.assign(
|
|
139
143
|
new Error(
|
|
140
|
-
"
|
|
144
|
+
"GOOGLE_NOT_LINKED: Your Google account is not connected yet. Please sign in with your email and password, then connect Google from your Profile settings to use Google sign-in."
|
|
141
145
|
),
|
|
142
146
|
{ status: 409 }
|
|
143
147
|
);
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
if (user) {
|
|
151
|
+
if (user.deleted_at) {
|
|
152
|
+
throw Object.assign(new Error("Invalid credentials"), { status: 401 });
|
|
153
|
+
}
|
|
147
154
|
if (!user.is_active) {
|
|
148
155
|
throw Object.assign(new Error("Account deactivated"), { status: 403 });
|
|
149
156
|
}
|
|
157
|
+
if (!user.avatar_url && input.picture) {
|
|
158
|
+
await userService.updateUser(user.id, { avatar_url: input.picture }).catch(() => {
|
|
159
|
+
});
|
|
160
|
+
}
|
|
150
161
|
await userService.recordLogin(user.id).catch(() => {
|
|
151
162
|
});
|
|
152
163
|
const permissions2 = await this.resolvePermissions(user.app_role_id);
|
|
@@ -181,7 +192,9 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
181
192
|
last_name: input.lastName ?? null,
|
|
182
193
|
role,
|
|
183
194
|
is_active: true,
|
|
195
|
+
has_password: false,
|
|
184
196
|
google_id: input.googleId,
|
|
197
|
+
avatar_url: input.picture ?? null,
|
|
185
198
|
...invite?.app_role_id ? { app_role_id: invite.app_role_id } : {}
|
|
186
199
|
});
|
|
187
200
|
if (invite) {
|
|
@@ -201,6 +214,81 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
201
214
|
token
|
|
202
215
|
};
|
|
203
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Restore a soft-deleted user via an invite link.
|
|
219
|
+
* Updates their name, password, and role, then issues a fresh session token.
|
|
220
|
+
* The user's ID — and all history tied to it — is preserved.
|
|
221
|
+
*/
|
|
222
|
+
async restoreFromInvite(userId, input) {
|
|
223
|
+
const userService = this.container.resolve("userModuleService");
|
|
224
|
+
const config = this.container.resolve("config");
|
|
225
|
+
const password_hash = await import_bcrypt.default.hash(input.password, BCRYPT_ROUNDS);
|
|
226
|
+
const user = await userService.restoreUser(userId, {
|
|
227
|
+
password_hash,
|
|
228
|
+
first_name: input.first_name ?? null,
|
|
229
|
+
last_name: input.last_name ?? null,
|
|
230
|
+
role: input.role ?? "member"
|
|
231
|
+
});
|
|
232
|
+
const permissions = await this.resolvePermissions(user.app_role_id);
|
|
233
|
+
const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role], permissions, config.projectConfig.jwtSecret);
|
|
234
|
+
await userService.createSession(jti, user.id, expiresAt).catch(() => {
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
user: {
|
|
238
|
+
id: user.id,
|
|
239
|
+
email: user.email,
|
|
240
|
+
first_name: user.first_name ?? null,
|
|
241
|
+
last_name: user.last_name ?? null
|
|
242
|
+
},
|
|
243
|
+
token
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/** Set (or reset) password for a user. Used after OTP verification. */
|
|
247
|
+
async setPassword(userId, newPassword) {
|
|
248
|
+
const userService = this.container.resolve("userModuleService");
|
|
249
|
+
const password_hash = await import_bcrypt.default.hash(newPassword, BCRYPT_ROUNDS);
|
|
250
|
+
await userService.updateUser(userId, { password_hash, has_password: true });
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Generate a password reset token for the given email.
|
|
254
|
+
* Returns the token and user info so the caller can emit an event / send an email.
|
|
255
|
+
* Returns null (instead of throwing) when the email isn't found — prevents enumeration.
|
|
256
|
+
*/
|
|
257
|
+
async requestPasswordReset(email) {
|
|
258
|
+
const userService = this.container.resolve("userModuleService");
|
|
259
|
+
const config = this.container.resolve("config");
|
|
260
|
+
const user = await userService.retrieveUserByEmail(email.toLowerCase().trim());
|
|
261
|
+
if (!user || user.deleted_at || !user.is_active) return null;
|
|
262
|
+
const resetToken = (0, import_crypto.randomBytes)(32).toString("hex");
|
|
263
|
+
const payload = { sub: user.id, purpose: "password_reset", jti: resetToken };
|
|
264
|
+
const signedToken = import_jsonwebtoken.default.sign(payload, config.projectConfig.jwtSecret, { expiresIn: "30m" });
|
|
265
|
+
return { token: signedToken, userId: user.id, email: user.email };
|
|
266
|
+
}
|
|
267
|
+
/** Validate a password reset token and set the new password. */
|
|
268
|
+
async resetPassword(token, newPassword) {
|
|
269
|
+
const config = this.container.resolve("config");
|
|
270
|
+
const userService = this.container.resolve("userModuleService");
|
|
271
|
+
let payload;
|
|
272
|
+
try {
|
|
273
|
+
payload = import_jsonwebtoken.default.verify(token, config.projectConfig.jwtSecret, { algorithms: ["HS256"] });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err.name === "TokenExpiredError") {
|
|
276
|
+
throw Object.assign(new Error("Reset link has expired. Please request a new one."), { status: 400 });
|
|
277
|
+
}
|
|
278
|
+
throw Object.assign(new Error("Invalid reset link"), { status: 400 });
|
|
279
|
+
}
|
|
280
|
+
if (payload.purpose !== "password_reset") {
|
|
281
|
+
throw Object.assign(new Error("Invalid reset link"), { status: 400 });
|
|
282
|
+
}
|
|
283
|
+
const user = await userService.retrieveUser(payload.sub);
|
|
284
|
+
if (!user || user.deleted_at || !user.is_active) {
|
|
285
|
+
throw Object.assign(new Error("Account not found or inactive"), { status: 400 });
|
|
286
|
+
}
|
|
287
|
+
const password_hash = await import_bcrypt.default.hash(newPassword, BCRYPT_ROUNDS);
|
|
288
|
+
await userService.updateUser(payload.sub, { password_hash });
|
|
289
|
+
await userService.revokeAllUserSessions(payload.sub).catch(() => {
|
|
290
|
+
});
|
|
291
|
+
}
|
|
204
292
|
/** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
|
|
205
293
|
verifyToken(token, secret) {
|
|
206
294
|
return import_jsonwebtoken.default.verify(token, secret, { algorithms: ["HS256"] });
|
package/dist/index.mjs
CHANGED
|
@@ -5,10 +5,11 @@ import { Module } from "@meridianjs/framework-utils";
|
|
|
5
5
|
import { MeridianService } from "@meridianjs/framework-utils";
|
|
6
6
|
import bcrypt from "bcrypt";
|
|
7
7
|
import jwt from "jsonwebtoken";
|
|
8
|
-
import { randomUUID } from "crypto";
|
|
8
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
9
9
|
var BCRYPT_ROUNDS = 12;
|
|
10
10
|
var JWT_EXPIRES_IN = "7d";
|
|
11
11
|
var JWT_EXPIRES_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
12
|
+
var RESET_TOKEN_EXPIRES_MS = 30 * 60 * 1e3;
|
|
12
13
|
var AuthModuleService = class extends MeridianService({}) {
|
|
13
14
|
container;
|
|
14
15
|
constructor(container) {
|
|
@@ -59,6 +60,9 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
59
60
|
if (!user) {
|
|
60
61
|
throw Object.assign(new Error("Invalid credentials"), { status: 401 });
|
|
61
62
|
}
|
|
63
|
+
if (user.deleted_at) {
|
|
64
|
+
throw Object.assign(new Error("Invalid credentials"), { status: 401 });
|
|
65
|
+
}
|
|
62
66
|
if (!user.is_active) {
|
|
63
67
|
throw Object.assign(new Error("Account deactivated"), { status: 403 });
|
|
64
68
|
}
|
|
@@ -97,16 +101,23 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
97
101
|
if (existingByEmail) {
|
|
98
102
|
throw Object.assign(
|
|
99
103
|
new Error(
|
|
100
|
-
"
|
|
104
|
+
"GOOGLE_NOT_LINKED: Your Google account is not connected yet. Please sign in with your email and password, then connect Google from your Profile settings to use Google sign-in."
|
|
101
105
|
),
|
|
102
106
|
{ status: 409 }
|
|
103
107
|
);
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
if (user) {
|
|
111
|
+
if (user.deleted_at) {
|
|
112
|
+
throw Object.assign(new Error("Invalid credentials"), { status: 401 });
|
|
113
|
+
}
|
|
107
114
|
if (!user.is_active) {
|
|
108
115
|
throw Object.assign(new Error("Account deactivated"), { status: 403 });
|
|
109
116
|
}
|
|
117
|
+
if (!user.avatar_url && input.picture) {
|
|
118
|
+
await userService.updateUser(user.id, { avatar_url: input.picture }).catch(() => {
|
|
119
|
+
});
|
|
120
|
+
}
|
|
110
121
|
await userService.recordLogin(user.id).catch(() => {
|
|
111
122
|
});
|
|
112
123
|
const permissions2 = await this.resolvePermissions(user.app_role_id);
|
|
@@ -141,7 +152,9 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
141
152
|
last_name: input.lastName ?? null,
|
|
142
153
|
role,
|
|
143
154
|
is_active: true,
|
|
155
|
+
has_password: false,
|
|
144
156
|
google_id: input.googleId,
|
|
157
|
+
avatar_url: input.picture ?? null,
|
|
145
158
|
...invite?.app_role_id ? { app_role_id: invite.app_role_id } : {}
|
|
146
159
|
});
|
|
147
160
|
if (invite) {
|
|
@@ -161,6 +174,81 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
161
174
|
token
|
|
162
175
|
};
|
|
163
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Restore a soft-deleted user via an invite link.
|
|
179
|
+
* Updates their name, password, and role, then issues a fresh session token.
|
|
180
|
+
* The user's ID — and all history tied to it — is preserved.
|
|
181
|
+
*/
|
|
182
|
+
async restoreFromInvite(userId, input) {
|
|
183
|
+
const userService = this.container.resolve("userModuleService");
|
|
184
|
+
const config = this.container.resolve("config");
|
|
185
|
+
const password_hash = await bcrypt.hash(input.password, BCRYPT_ROUNDS);
|
|
186
|
+
const user = await userService.restoreUser(userId, {
|
|
187
|
+
password_hash,
|
|
188
|
+
first_name: input.first_name ?? null,
|
|
189
|
+
last_name: input.last_name ?? null,
|
|
190
|
+
role: input.role ?? "member"
|
|
191
|
+
});
|
|
192
|
+
const permissions = await this.resolvePermissions(user.app_role_id);
|
|
193
|
+
const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role], permissions, config.projectConfig.jwtSecret);
|
|
194
|
+
await userService.createSession(jti, user.id, expiresAt).catch(() => {
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
user: {
|
|
198
|
+
id: user.id,
|
|
199
|
+
email: user.email,
|
|
200
|
+
first_name: user.first_name ?? null,
|
|
201
|
+
last_name: user.last_name ?? null
|
|
202
|
+
},
|
|
203
|
+
token
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Set (or reset) password for a user. Used after OTP verification. */
|
|
207
|
+
async setPassword(userId, newPassword) {
|
|
208
|
+
const userService = this.container.resolve("userModuleService");
|
|
209
|
+
const password_hash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
|
|
210
|
+
await userService.updateUser(userId, { password_hash, has_password: true });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Generate a password reset token for the given email.
|
|
214
|
+
* Returns the token and user info so the caller can emit an event / send an email.
|
|
215
|
+
* Returns null (instead of throwing) when the email isn't found — prevents enumeration.
|
|
216
|
+
*/
|
|
217
|
+
async requestPasswordReset(email) {
|
|
218
|
+
const userService = this.container.resolve("userModuleService");
|
|
219
|
+
const config = this.container.resolve("config");
|
|
220
|
+
const user = await userService.retrieveUserByEmail(email.toLowerCase().trim());
|
|
221
|
+
if (!user || user.deleted_at || !user.is_active) return null;
|
|
222
|
+
const resetToken = randomBytes(32).toString("hex");
|
|
223
|
+
const payload = { sub: user.id, purpose: "password_reset", jti: resetToken };
|
|
224
|
+
const signedToken = jwt.sign(payload, config.projectConfig.jwtSecret, { expiresIn: "30m" });
|
|
225
|
+
return { token: signedToken, userId: user.id, email: user.email };
|
|
226
|
+
}
|
|
227
|
+
/** Validate a password reset token and set the new password. */
|
|
228
|
+
async resetPassword(token, newPassword) {
|
|
229
|
+
const config = this.container.resolve("config");
|
|
230
|
+
const userService = this.container.resolve("userModuleService");
|
|
231
|
+
let payload;
|
|
232
|
+
try {
|
|
233
|
+
payload = jwt.verify(token, config.projectConfig.jwtSecret, { algorithms: ["HS256"] });
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err.name === "TokenExpiredError") {
|
|
236
|
+
throw Object.assign(new Error("Reset link has expired. Please request a new one."), { status: 400 });
|
|
237
|
+
}
|
|
238
|
+
throw Object.assign(new Error("Invalid reset link"), { status: 400 });
|
|
239
|
+
}
|
|
240
|
+
if (payload.purpose !== "password_reset") {
|
|
241
|
+
throw Object.assign(new Error("Invalid reset link"), { status: 400 });
|
|
242
|
+
}
|
|
243
|
+
const user = await userService.retrieveUser(payload.sub);
|
|
244
|
+
if (!user || user.deleted_at || !user.is_active) {
|
|
245
|
+
throw Object.assign(new Error("Account not found or inactive"), { status: 400 });
|
|
246
|
+
}
|
|
247
|
+
const password_hash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
|
|
248
|
+
await userService.updateUser(payload.sub, { password_hash });
|
|
249
|
+
await userService.revokeAllUserSessions(payload.sub).catch(() => {
|
|
250
|
+
});
|
|
251
|
+
}
|
|
164
252
|
/** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
|
|
165
253
|
verifyToken(token, secret) {
|
|
166
254
|
return jwt.verify(token, secret, { algorithms: ["HS256"] });
|