@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 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
- "An account with this email already exists. Please sign in with your password. You can link Google sign-in from your account settings afterwards."
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
- "An account with this email already exists. Please sign in with your password. You can link Google sign-in from your account settings afterwards."
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"] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridianjs/auth",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Meridian auth module — JWT authentication and middleware",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",