@infuro/cms-core 1.0.38 → 1.0.40

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.
@@ -12,7 +12,7 @@ import { isErpIntegrationEnabled } from './chunk-JC6DLWTE.js';
12
12
  import { __name } from './chunk-SHUYVCID.js';
13
13
  import { PrimaryGeneratedColumn, Column, Entity, ManyToOne, JoinColumn, OneToMany, Index, ManyToMany, JoinTable, Unique, CreateDateColumn, UpdateDateColumn, In, IsNull, ILike, Between, MoreThanOrEqual, LessThanOrEqual, Not, MoreThan } from 'typeorm';
14
14
  import { Country, State, City } from 'country-state-city';
15
- import crypto2, { randomUUID, createHmac, timingSafeEqual, randomInt } from 'crypto';
15
+ import crypto2, { randomBytes, randomUUID, createHmac, timingSafeEqual, randomInt } from 'crypto';
16
16
  import Parser from 'rss-parser';
17
17
  import fs from 'fs/promises';
18
18
  import path from 'path';
@@ -6179,6 +6179,225 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6179
6179
  };
6180
6180
  }
6181
6181
  __name(createCrudByIdHandler, "createCrudByIdHandler");
6182
+ var VENDOR_INVITE_METADATA_TOKEN = "inviteToken";
6183
+ var VENDOR_INVITE_METADATA_EXPIRES = "inviteExpiresAt";
6184
+ var VENDOR_INVITE_METADATA_SENT = "inviteSentAt";
6185
+ async function loadOwnerVendorForUser(dataSource, vendorEntity, userId) {
6186
+ const repo = dataSource.getRepository(vendorEntity);
6187
+ const row = await repo.findOne({
6188
+ where: {
6189
+ userId,
6190
+ deleted: false
6191
+ }
6192
+ });
6193
+ if (!row) return null;
6194
+ const v = row;
6195
+ return {
6196
+ id: v.id,
6197
+ inviteStatus: v.inviteStatus,
6198
+ metadata: v.metadata && typeof v.metadata === "object" && !Array.isArray(v.metadata) ? v.metadata : null
6199
+ };
6200
+ }
6201
+ __name(loadOwnerVendorForUser, "loadOwnerVendorForUser");
6202
+ function invalidatedInviteMessage() {
6203
+ return "This invitation is no longer valid. Ask an admin to resend the invite.";
6204
+ }
6205
+ __name(invalidatedInviteMessage, "invalidatedInviteMessage");
6206
+ async function validateInviteTokenForActivation(dataSource, entityMap, token) {
6207
+ const trimmedToken = String(token ?? "").trim();
6208
+ if (!trimmedToken) {
6209
+ return {
6210
+ ok: false,
6211
+ error: "Invalid invite link. Token is missing."
6212
+ };
6213
+ }
6214
+ const userRepo = entityMap.users ? dataSource.getRepository(entityMap.users) : null;
6215
+ if (entityMap.vendors && userRepo) {
6216
+ const vendorMatch = await findVendorByInviteToken(dataSource, entityMap.vendors, trimmedToken);
6217
+ if (vendorMatch) {
6218
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
6219
+ const freshVendor = await vendorRepo.findOne({
6220
+ where: {
6221
+ id: vendorMatch.id,
6222
+ deleted: false
6223
+ }
6224
+ });
6225
+ const v = freshVendor;
6226
+ if (!v?.userId) {
6227
+ return {
6228
+ ok: false,
6229
+ error: invalidatedInviteMessage()
6230
+ };
6231
+ }
6232
+ if (v.inviteStatus === "accepted") {
6233
+ return {
6234
+ ok: false,
6235
+ error: "This invitation has already been accepted"
6236
+ };
6237
+ }
6238
+ if (v.inviteStatus === "expired") {
6239
+ await vendorRepo.update(v.id, {
6240
+ inviteStatus: "pending"
6241
+ });
6242
+ v.inviteStatus = "pending";
6243
+ }
6244
+ const user2 = await userRepo.findOne({
6245
+ where: {
6246
+ id: v.userId
6247
+ },
6248
+ select: [
6249
+ "id",
6250
+ "email",
6251
+ "blocked",
6252
+ "password"
6253
+ ]
6254
+ });
6255
+ if (!user2) return {
6256
+ ok: false,
6257
+ error: "User not found"
6258
+ };
6259
+ const hasPassword2 = !!user2.password;
6260
+ if (!user2.blocked && hasPassword2) {
6261
+ return {
6262
+ ok: false,
6263
+ error: "User is already active"
6264
+ };
6265
+ }
6266
+ return {
6267
+ ok: true,
6268
+ target: {
6269
+ userId: user2.id,
6270
+ email: String(user2.email),
6271
+ vendorId: v.id
6272
+ }
6273
+ };
6274
+ }
6275
+ }
6276
+ let email;
6277
+ try {
6278
+ email = Buffer.from(trimmedToken, "base64").toString("utf8").trim();
6279
+ } catch {
6280
+ return {
6281
+ ok: false,
6282
+ error: invalidatedInviteMessage()
6283
+ };
6284
+ }
6285
+ if (!email.includes("@")) {
6286
+ return {
6287
+ ok: false,
6288
+ error: invalidatedInviteMessage()
6289
+ };
6290
+ }
6291
+ if (!userRepo) {
6292
+ return {
6293
+ ok: false,
6294
+ error: "User not found"
6295
+ };
6296
+ }
6297
+ const user = await userRepo.findOne({
6298
+ where: {
6299
+ email
6300
+ },
6301
+ select: [
6302
+ "id",
6303
+ "email",
6304
+ "blocked",
6305
+ "password"
6306
+ ]
6307
+ });
6308
+ if (!user) return {
6309
+ ok: false,
6310
+ error: "User not found"
6311
+ };
6312
+ const hasPassword = !!user.password;
6313
+ if (!user.blocked && hasPassword) {
6314
+ return {
6315
+ ok: false,
6316
+ error: "User is already active"
6317
+ };
6318
+ }
6319
+ if (entityMap.vendors) {
6320
+ const ownerVendor = await loadOwnerVendorForUser(dataSource, entityMap.vendors, user.id);
6321
+ if (ownerVendor && readVendorInviteToken(ownerVendor.metadata)) {
6322
+ return {
6323
+ ok: false,
6324
+ error: invalidatedInviteMessage()
6325
+ };
6326
+ }
6327
+ }
6328
+ return {
6329
+ ok: true,
6330
+ target: {
6331
+ userId: user.id,
6332
+ email: String(user.email)
6333
+ }
6334
+ };
6335
+ }
6336
+ __name(validateInviteTokenForActivation, "validateInviteTokenForActivation");
6337
+ function buildVendorInviteLink(baseUrl, token) {
6338
+ const base = baseUrl.replace(/\/+$/, "");
6339
+ return `${base}/admin/invite?token=${encodeURIComponent(token)}`;
6340
+ }
6341
+ __name(buildVendorInviteLink, "buildVendorInviteLink");
6342
+ function applyRotatingVendorInvite(existing) {
6343
+ const next = {
6344
+ ...existing ?? {}
6345
+ };
6346
+ next[VENDOR_INVITE_METADATA_TOKEN] = randomBytes(32).toString("hex");
6347
+ next[VENDOR_INVITE_METADATA_SENT] = (/* @__PURE__ */ new Date()).toISOString();
6348
+ delete next[VENDOR_INVITE_METADATA_EXPIRES];
6349
+ return next;
6350
+ }
6351
+ __name(applyRotatingVendorInvite, "applyRotatingVendorInvite");
6352
+ function clearVendorInviteMetadata(existing) {
6353
+ const next = {
6354
+ ...existing ?? {}
6355
+ };
6356
+ delete next[VENDOR_INVITE_METADATA_TOKEN];
6357
+ delete next[VENDOR_INVITE_METADATA_EXPIRES];
6358
+ delete next[VENDOR_INVITE_METADATA_SENT];
6359
+ return Object.keys(next).length > 0 ? next : null;
6360
+ }
6361
+ __name(clearVendorInviteMetadata, "clearVendorInviteMetadata");
6362
+ function readVendorInviteToken(metadata) {
6363
+ const raw = metadata?.[VENDOR_INVITE_METADATA_TOKEN];
6364
+ return typeof raw === "string" && raw.trim() ? raw.trim() : null;
6365
+ }
6366
+ __name(readVendorInviteToken, "readVendorInviteToken");
6367
+ async function findVendorByInviteToken(dataSource, vendorEntity, token) {
6368
+ const trimmed = token.trim();
6369
+ if (!trimmed) return null;
6370
+ const repo = dataSource.getRepository(vendorEntity);
6371
+ const row = await repo.createQueryBuilder("v").where("v.deleted = false").andWhere(`v.metadata->>'${VENDOR_INVITE_METADATA_TOKEN}' = :token`, {
6372
+ token: trimmed
6373
+ }).getOne();
6374
+ if (!row) return null;
6375
+ const v = row;
6376
+ return {
6377
+ id: v.id,
6378
+ userId: v.userId ?? null,
6379
+ inviteStatus: v.inviteStatus,
6380
+ metadata: v.metadata && typeof v.metadata === "object" && !Array.isArray(v.metadata) ? v.metadata : null
6381
+ };
6382
+ }
6383
+ __name(findVendorByInviteToken, "findVendorByInviteToken");
6384
+ async function completeVendorInviteAccept(dataSource, vendorEntity, vendorId) {
6385
+ const repo = dataSource.getRepository(vendorEntity);
6386
+ const row = await repo.findOne({
6387
+ where: {
6388
+ id: vendorId,
6389
+ deleted: false
6390
+ }
6391
+ });
6392
+ if (!row) return;
6393
+ const metadata = row.metadata;
6394
+ const cleared = clearVendorInviteMetadata(metadata);
6395
+ await repo.update(vendorId, {
6396
+ metadata: cleared,
6397
+ inviteStatus: "accepted"
6398
+ });
6399
+ }
6400
+ __name(completeVendorInviteAccept, "completeVendorInviteAccept");
6182
6401
 
6183
6402
  // src/api/auth-handlers.ts
6184
6403
  function createForgotPasswordHandler(config) {
@@ -6314,49 +6533,27 @@ function createInviteAcceptHandler(config) {
6314
6533
  }, {
6315
6534
  status: 400
6316
6535
  });
6317
- let email;
6318
- try {
6319
- email = Buffer.from(token, "base64").toString("utf8");
6320
- } catch {
6321
- return json({
6322
- error: "Invalid or expired invite token"
6323
- }, {
6324
- status: 400
6325
- });
6326
- }
6327
- const userRepo = dataSource.getRepository(entityMap.users);
6328
- const user = await userRepo.findOne({
6329
- where: {
6330
- email
6331
- },
6332
- select: [
6333
- "id",
6334
- "blocked",
6335
- "password"
6336
- ]
6337
- });
6338
- if (!user) return json({
6339
- error: "User not found"
6340
- }, {
6341
- status: 400
6342
- });
6343
- const hasPassword = !!user.password;
6344
- if (!user.blocked && hasPassword) {
6536
+ const validation = await validateInviteTokenForActivation(dataSource, entityMap, token);
6537
+ if (!validation.ok) {
6345
6538
  return json({
6346
- error: "User is already active"
6539
+ error: validation.error
6347
6540
  }, {
6348
6541
  status: 400
6349
6542
  });
6350
6543
  }
6544
+ const { userId, email, vendorId } = validation.target;
6351
6545
  if (entityMap.contacts) {
6352
- await linkUnclaimedContactToUser(dataSource, entityMap.contacts, user.id, email);
6546
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, userId, email);
6353
6547
  }
6354
- if (beforeActivate) await beforeActivate(email, user.id);
6548
+ if (beforeActivate) await beforeActivate(email, userId);
6355
6549
  const hashedPassword = await hashPassword(password);
6356
- await userRepo.update(user.id, {
6550
+ await dataSource.getRepository(entityMap.users).update(userId, {
6357
6551
  password: hashedPassword,
6358
6552
  blocked: false
6359
6553
  });
6554
+ if (vendorId != null && entityMap.vendors) {
6555
+ await completeVendorInviteAccept(dataSource, entityMap.vendors, vendorId);
6556
+ }
6360
6557
  return json({
6361
6558
  message: "User account activated successfully"
6362
6559
  }, {
@@ -6372,6 +6569,35 @@ function createInviteAcceptHandler(config) {
6372
6569
  }, "POST");
6373
6570
  }
6374
6571
  __name(createInviteAcceptHandler, "createInviteAcceptHandler");
6572
+ function createInviteValidateHandler(config) {
6573
+ const { dataSource, entityMap, json } = config;
6574
+ return /* @__PURE__ */ __name(async function GET(request) {
6575
+ try {
6576
+ const token = new URL(request.url).searchParams.get("token");
6577
+ const validation = await validateInviteTokenForActivation(dataSource, entityMap, token ?? "");
6578
+ if (!validation.ok) {
6579
+ return json({
6580
+ valid: false,
6581
+ error: validation.error
6582
+ }, {
6583
+ status: 400
6584
+ });
6585
+ }
6586
+ return json({
6587
+ valid: true
6588
+ });
6589
+ } catch (err) {
6590
+ console.error("[users/invite GET] validate failed", err);
6591
+ return json({
6592
+ valid: false,
6593
+ error: "Server Error"
6594
+ }, {
6595
+ status: 500
6596
+ });
6597
+ }
6598
+ }, "GET");
6599
+ }
6600
+ __name(createInviteValidateHandler, "createInviteValidateHandler");
6375
6601
  function createChangePasswordHandler(config) {
6376
6602
  const { dataSource, entityMap, json, comparePassword, hashPassword, getSession, minPasswordLength = 6, beforeUpdate } = config;
6377
6603
  return /* @__PURE__ */ __name(async function POST(request) {
@@ -6450,12 +6676,22 @@ function createUserAuthApiRouter(config) {
6450
6676
  const forgot = createForgotPasswordHandler(config);
6451
6677
  const setPass = createSetPasswordHandler(config);
6452
6678
  const invite = createInviteAcceptHandler(config);
6679
+ const inviteValidate = createInviteValidateHandler(config);
6453
6680
  const changePass = config.getSession ? createChangePasswordHandler({
6454
6681
  ...config,
6455
6682
  getSession: config.getSession,
6456
6683
  beforeUpdate: config.beforeChangePasswordUpdate
6457
6684
  }) : null;
6458
6685
  return {
6686
+ async GET(req, pathname) {
6687
+ const path2 = pathname.replace(/\/$/, "");
6688
+ if (path2 === "invite") return inviteValidate(req);
6689
+ return config.json({
6690
+ error: "Not found"
6691
+ }, {
6692
+ status: 404
6693
+ });
6694
+ },
6459
6695
  async POST(req, pathname) {
6460
6696
  const path2 = pathname.replace(/\/$/, "");
6461
6697
  if (!USER_AUTH_PATHS.includes(path2)) {
@@ -8861,13 +9097,31 @@ function createUsersApiHandlers(config) {
8861
9097
  }, {
8862
9098
  status: 404
8863
9099
  });
8864
- const emailToken = Buffer.from(user.email).toString("base64");
8865
- const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
8866
- await trySendInviteEmail(user.email, inviteLink, user.name ?? "");
9100
+ let inviteLink = `${baseUrl}/admin/invite?token=${Buffer.from(user.email).toString("base64")}`;
8867
9101
  if (entityMap.vendors) {
9102
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
9103
+ const vendor = await vendorRepo.findOne({
9104
+ where: {
9105
+ userId: user.id,
9106
+ deleted: false
9107
+ }
9108
+ });
9109
+ const v = vendor;
9110
+ if (v && (v.inviteStatus === "pending" || v.inviteStatus === "expired")) {
9111
+ const rotatedMetadata = applyRotatingVendorInvite(v.metadata);
9112
+ await vendorRepo.update(v.id, {
9113
+ metadata: rotatedMetadata,
9114
+ inviteStatus: "pending"
9115
+ });
9116
+ const token = readVendorInviteToken(rotatedMetadata);
9117
+ if (token) {
9118
+ inviteLink = buildVendorInviteLink(baseUrl, token);
9119
+ }
9120
+ }
8868
9121
  const { markVendorInvitePendingForUser } = await import('./vendor-invite-status-PWJ3DE76.js');
8869
9122
  await markVendorInvitePendingForUser(dataSource, entityMap.vendors, user.id);
8870
9123
  }
9124
+ await trySendInviteEmail(user.email, inviteLink, user.name ?? "");
8871
9125
  return json({
8872
9126
  message: "New invite link generated successfully",
8873
9127
  inviteLink
@@ -11811,6 +12065,84 @@ function slugify(input) {
11811
12065
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
11812
12066
  }
11813
12067
  __name(slugify, "slugify");
12068
+ function vendorOnboardErrorResponse(json, err) {
12069
+ const msg = err instanceof Error ? err.message : String(err);
12070
+ console.error("[vendor-onboard]", err);
12071
+ if (msg === "VENDOR_SLUG_EXISTS") {
12072
+ return json({
12073
+ error: "Vendor slug already exists"
12074
+ }, {
12075
+ status: 400
12076
+ });
12077
+ }
12078
+ if (msg === "USER_EMAIL_EXISTS") {
12079
+ return json({
12080
+ error: "User with this email already exists"
12081
+ }, {
12082
+ status: 400
12083
+ });
12084
+ }
12085
+ if (msg === "VENDOR_OWNER_GROUP_MISSING") {
12086
+ return json({
12087
+ error: "Vendor Owner group not found. Run migrations (VendorOwnerRbacSeed)."
12088
+ }, {
12089
+ status: 500
12090
+ });
12091
+ }
12092
+ if (msg === "VENDOR_ROLES_SEED_FAILED") {
12093
+ return json({
12094
+ error: "Failed to seed store roles for vendor. Run migrations."
12095
+ }, {
12096
+ status: 500
12097
+ });
12098
+ }
12099
+ if (/vendor_roles|vendor_role_permissions/i.test(msg)) {
12100
+ return json({
12101
+ error: "Vendor roles tables missing. Run migrations."
12102
+ }, {
12103
+ status: 500
12104
+ });
12105
+ }
12106
+ if (/relation .* does not exist|column .* does not exist/i.test(msg)) {
12107
+ return json({
12108
+ error: "Database schema is outdated on this environment. Run migrations.",
12109
+ detail: msg
12110
+ }, {
12111
+ status: 500
12112
+ });
12113
+ }
12114
+ if (/duplicate key|unique constraint/i.test(msg)) {
12115
+ if (/email/i.test(msg)) {
12116
+ return json({
12117
+ error: "A user or record with this email already exists",
12118
+ detail: msg
12119
+ }, {
12120
+ status: 409
12121
+ });
12122
+ }
12123
+ if (/slug/i.test(msg)) {
12124
+ return json({
12125
+ error: "Vendor slug already exists",
12126
+ detail: msg
12127
+ }, {
12128
+ status: 409
12129
+ });
12130
+ }
12131
+ return json({
12132
+ error: "Duplicate value conflicts with an existing record",
12133
+ detail: msg
12134
+ }, {
12135
+ status: 409
12136
+ });
12137
+ }
12138
+ return json({
12139
+ error: "Server error",
12140
+ detail: msg
12141
+ }, {
12142
+ status: 500
12143
+ });
12144
+ }
12145
+ __name(vendorOnboardErrorResponse, "vendorOnboardErrorResponse");
11814
12146
  function createVendorOnboardHandlers(config) {
11815
12147
  const { dataSource, entityMap, json, getSessionUser, baseUrl, getCms, hashPassword, minPasswordLength = 6 } = config;
11816
12148
  async function gateAdmin() {
@@ -11979,12 +12311,28 @@ function createVendorOnboardHandlers(config) {
11979
12311
  }
11980
12312
  });
11981
12313
  if (dupVendor) throw new Error("VENDOR_SLUG_EXISTS");
11982
- const ownerGroup = await groupRepo.findOne({
12314
+ let ownerGroup = await groupRepo.findOne({
11983
12315
  where: {
11984
12316
  name: VENDOR_OWNER_GROUP_NAME,
11985
12317
  deleted: false
11986
12318
  }
11987
12319
  });
12320
+ if (!ownerGroup) {
12321
+ try {
12322
+ await groupRepo.save(groupRepo.create({
12323
+ name: VENDOR_OWNER_GROUP_NAME,
12324
+ deleted: false
12325
+ }));
12326
+ ownerGroup = await groupRepo.findOne({
12327
+ where: {
12328
+ name: VENDOR_OWNER_GROUP_NAME,
12329
+ deleted: false
12330
+ }
12331
+ });
12332
+ } catch (seedErr) {
12333
+ console.error("[vendor-onboard] failed to seed Vendor Owner group", seedErr);
12334
+ }
12335
+ }
11988
12336
  if (!ownerGroup) throw new Error("VENDOR_OWNER_GROUP_MISSING");
11989
12337
  const existingUser = await userRepo.findOne({
11990
12338
  where: {
@@ -12022,6 +12370,11 @@ function createVendorOnboardHandlers(config) {
12022
12370
  groupId: ownerGroup.id,
12023
12371
  adminAccess: true
12024
12372
  }));
12373
+ const baseMetadata = buildVendorMetadata(null, {
12374
+ ownerDesignation,
12375
+ termsAccepted: true
12376
+ });
12377
+ const metadata = activation === "invite" ? applyRotatingVendorInvite(baseMetadata) : baseMetadata;
12025
12378
  const vendor = await vendorRepo.save(vendorRepo.create({
12026
12379
  name: vendorName,
12027
12380
  slug,
@@ -12041,10 +12394,7 @@ function createVendorOnboardHandlers(config) {
12041
12394
  gstin: profile.gstin,
12042
12395
  pan: profile.pan,
12043
12396
  registrationStatus: profile.registrationStatus,
12044
- metadata: buildVendorMetadata(null, {
12045
- ownerDesignation,
12046
- termsAccepted: true
12047
- }),
12397
+ metadata,
12048
12398
  active: body.vendor?.active !== false,
12049
12399
  deleted: false,
12050
12400
  userId: newUser.id,
@@ -12073,14 +12423,18 @@ function createVendorOnboardHandlers(config) {
12073
12423
  });
12074
12424
  }
12075
12425
  if (entityMap.customer) {
12076
- await ensureCustomerForUser(em, entityMap.customer, {
12077
- id: newUser.id,
12078
- name: userName,
12079
- email: userEmail,
12080
- phone: trimOrNull(body.vendor?.phone) ?? ownerPhone
12081
- }, {
12082
- phone: trimOrNull(body.vendor?.phone) ?? ownerPhone
12083
- });
12426
+ try {
12427
+ await ensureCustomerForUser(em, entityMap.customer, {
12428
+ id: newUser.id,
12429
+ name: userName,
12430
+ email: userEmail,
12431
+ phone: trimOrNull(body.vendor?.phone) ?? ownerPhone
12432
+ }, {
12433
+ phone: trimOrNull(body.vendor?.phone) ?? ownerPhone
12434
+ });
12435
+ } catch (customerErr) {
12436
+ console.error("[vendor-onboard] ensureCustomerForUser skipped", customerErr);
12437
+ }
12084
12438
  }
12085
12439
  return {
12086
12440
  vendor,
@@ -12090,8 +12444,10 @@ function createVendorOnboardHandlers(config) {
12090
12444
  let inviteLink;
12091
12445
  let emailSent = false;
12092
12446
  if (activation === "invite") {
12093
- const emailToken = Buffer.from(result.user.email).toString("base64");
12094
- inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
12447
+ const token = readVendorInviteToken(result.vendor.metadata);
12448
+ if (token) {
12449
+ inviteLink = buildVendorInviteLink(baseUrl, token);
12450
+ }
12095
12451
  }
12096
12452
  if (sendOwnerEmail) {
12097
12453
  emailSent = await trySendVendorOnboardEmails({
@@ -12130,46 +12486,131 @@ function createVendorOnboardHandlers(config) {
12130
12486
  status: 201
12131
12487
  });
12132
12488
  } catch (e) {
12133
- const msg = e instanceof Error ? e.message : String(e);
12134
- if (process.env.NODE_ENV === "development") {
12135
- console.error("[vendor-onboard]", e);
12136
- }
12137
- if (msg === "VENDOR_SLUG_EXISTS") return json({
12138
- error: "Vendor slug already exists"
12489
+ return vendorOnboardErrorResponse(json, e);
12490
+ }
12491
+ },
12492
+ /** POST /api/admin/vendors/:id/resend-invite — rotate owner invite token and resend email */
12493
+ async resendInvite(req, vendorIdStr) {
12494
+ const err = await gateAdmin();
12495
+ if (err) return err;
12496
+ if (!entityMap.vendors || !entityMap.users) {
12497
+ return json({
12498
+ error: "Vendor entities not configured"
12139
12499
  }, {
12140
- status: 400
12500
+ status: 500
12141
12501
  });
12142
- if (msg === "USER_EMAIL_EXISTS") return json({
12143
- error: "User with this email already exists"
12502
+ }
12503
+ const vendorId = Number(vendorIdStr);
12504
+ if (!Number.isFinite(vendorId) || vendorId <= 0) {
12505
+ return json({
12506
+ error: "Invalid vendor id"
12144
12507
  }, {
12145
12508
  status: 400
12146
12509
  });
12147
- if (msg === "VENDOR_OWNER_GROUP_MISSING") {
12510
+ }
12511
+ let sendEmail = true;
12512
+ try {
12513
+ const body = await req.json().catch(() => ({}));
12514
+ if (body.sendEmail === false) sendEmail = false;
12515
+ } catch {
12516
+ }
12517
+ try {
12518
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
12519
+ const vendor = await vendorRepo.findOne({
12520
+ where: {
12521
+ id: vendorId,
12522
+ deleted: false
12523
+ }
12524
+ });
12525
+ if (!vendor) return json({
12526
+ error: "Vendor not found"
12527
+ }, {
12528
+ status: 404
12529
+ });
12530
+ const v = vendor;
12531
+ if (v.inviteStatus === "accepted") {
12148
12532
  return json({
12149
- error: "Vendor Owner group not found. Run migrations."
12533
+ error: "Owner has already accepted the invite"
12150
12534
  }, {
12151
- status: 500
12535
+ status: 400
12152
12536
  });
12153
12537
  }
12154
- if (msg === "VENDOR_ROLES_SEED_FAILED") {
12538
+ if (v.inviteStatus === "none") {
12155
12539
  return json({
12156
- error: "Failed to seed store roles for vendor. Run migrations."
12540
+ error: "This vendor was not created with an invite link"
12157
12541
  }, {
12158
- status: 500
12542
+ status: 400
12543
+ });
12544
+ }
12545
+ if (v.inviteStatus !== "pending" && v.inviteStatus !== "expired") {
12546
+ return json({
12547
+ error: "Invite cannot be resent for this vendor"
12548
+ }, {
12549
+ status: 400
12550
+ });
12551
+ }
12552
+ if (!v.userId) {
12553
+ return json({
12554
+ error: "Vendor has no owner user linked"
12555
+ }, {
12556
+ status: 400
12557
+ });
12558
+ }
12559
+ const userRepo = dataSource.getRepository(entityMap.users);
12560
+ const owner = await userRepo.findOne({
12561
+ where: {
12562
+ id: v.userId,
12563
+ deleted: false
12564
+ }
12565
+ });
12566
+ if (!owner) return json({
12567
+ error: "Owner user not found"
12568
+ }, {
12569
+ status: 404
12570
+ });
12571
+ const ownerRow = owner;
12572
+ const hasPassword = !!(ownerRow.password && String(ownerRow.password).trim());
12573
+ if (!ownerRow.blocked && hasPassword) {
12574
+ return json({
12575
+ error: "Owner account is already active"
12576
+ }, {
12577
+ status: 400
12159
12578
  });
12160
12579
  }
12161
- if (/vendor_roles|vendor_role_permissions/i.test(msg)) {
12580
+ const rotatedMetadata = applyRotatingVendorInvite(v.metadata);
12581
+ await vendorRepo.update(vendorId, {
12582
+ metadata: rotatedMetadata,
12583
+ inviteStatus: "pending"
12584
+ });
12585
+ const token = readVendorInviteToken(rotatedMetadata);
12586
+ if (!token) {
12162
12587
  return json({
12163
- error: "Vendor roles tables missing. Run migrations."
12588
+ error: "Failed to generate invite token"
12164
12589
  }, {
12165
12590
  status: 500
12166
12591
  });
12167
12592
  }
12593
+ const inviteLink = buildVendorInviteLink(baseUrl, token);
12594
+ let emailSent = false;
12595
+ if (sendEmail) {
12596
+ emailSent = await trySendVendorOnboardEmails({
12597
+ vendorName: v.name,
12598
+ vendorSlug: v.slug,
12599
+ ownerName: ownerRow.name,
12600
+ ownerEmail: ownerRow.email,
12601
+ activation: "invite",
12602
+ inviteLink,
12603
+ sendToOwner: true
12604
+ });
12605
+ }
12168
12606
  return json({
12169
- error: "Server error"
12170
- }, {
12171
- status: 500
12607
+ message: emailSent ? "Invite resent. Previous invite link is no longer valid." : sendEmail ? "New invite link created (email may not have been sent \u2014 check email plugin). Previous link is invalid." : "New invite link created. Previous invite link is no longer valid.",
12608
+ emailSent,
12609
+ inviteLink,
12610
+ inviteStatus: "pending"
12172
12611
  });
12612
+ } catch (e) {
12613
+ return vendorOnboardErrorResponse(json, e);
12173
12614
  }
12174
12615
  },
12175
12616
  /** POST /api/admin/vendor/switch — set active vendor in session (JWT update via client) */
@@ -25533,6 +25974,14 @@ function createCmsApiHandler(config) {
25533
25974
  });
25534
25975
  return vendorHandlers.onboard(req);
25535
25976
  }
25977
+ if (path2[0] === "admin" && path2[1] === "vendors" && path2.length === 4 && path2[3] === "resend-invite" && m === "POST") {
25978
+ if (!vendorHandlers) return config.json({
25979
+ error: "Not found"
25980
+ }, {
25981
+ status: 404
25982
+ });
25983
+ return vendorHandlers.resendInvite(req, path2[2]);
25984
+ }
25536
25985
  if (path2[0] === "admin" && path2[1] === "vendor" && path2[2] === "switch" && path2.length === 3 && m === "POST") {
25537
25986
  if (!vendorHandlers) return config.json({
25538
25987
  error: "Not found"
@@ -26124,6 +26573,15 @@ function createCmsApiHandler(config) {
26124
26573
  if (m === "PUT" || m === "PATCH") return formSaveHandlers.PUT(req, path2[1]);
26125
26574
  }
26126
26575
  }
26576
+ if (path2[0] === "users" && path2.length === 2 && userAuthRouter) {
26577
+ const authSegment = path2[1];
26578
+ if (m === "GET" && authSegment === "invite" && userAuthRouter.GET) {
26579
+ return userAuthRouter.GET(req, authSegment);
26580
+ }
26581
+ if (m === "POST" && (authSegment === "invite" || authSegment === "forgot-password" || authSegment === "set-password" || authSegment === "reset-password")) {
26582
+ return userAuthRouter.POST(req, authSegment);
26583
+ }
26584
+ }
26127
26585
  if (path2[0] === "users" && usersHandlers) {
26128
26586
  if (path2.length === 1) {
26129
26587
  if (m === "GET") return usersHandlers.list(req);
@@ -26144,9 +26602,6 @@ function createCmsApiHandler(config) {
26144
26602
  return usersHandlers.regenerateInvite(req, path2[1]);
26145
26603
  }
26146
26604
  }
26147
- if (path2[0] === "users" && path2.length === 2 && userAuthRouter && m === "POST") {
26148
- return userAuthRouter.POST(req, path2[1]);
26149
- }
26150
26605
  if (path2[0] === "social-media" && path2[1] === "linkedin" && socialMediaHandlers && path2.length === 3) {
26151
26606
  const tail = path2[2];
26152
26607
  if (tail === "status" && m === "GET") {
@@ -30529,4 +30984,4 @@ function createStorefrontApiHandler(config) {
30529
30984
  }
30530
30985
  __name(createStorefrontApiHandler, "createStorefrontApiHandler");
30531
30986
 
30532
- export { Address, Attendee, Attribute, BLOG_GENERATOR_AGENT_NAME, BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION, BLOG_GENERATOR_DEFAULT_VALIDATION_RULES, BLOG_GENERATOR_LLM_AGENT_SLUG, BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR, BLOG_METADATA_ENRICHER_AGENT_NAME, BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION, BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES, BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, Blog, BlogGeneratorService, Brand, CMS_ENTITY_MAP, Cart, CartItem, Category, ChatConversation, ChatMessage, Collection, Combo, ComboItem, Comment, Config, Contact, Currency, CurrencyExchange, Customer, Customer_Contacts, Discount, DiscountRules, Event, EventProduct, Form, FormField, FormSubmission, JobSchedule, JobScheduleRun, KnowledgeBaseChunk, KnowledgeBaseDocument, LlmAgent, Media, MessageTemplate, Order, OrderAddresses, OrderDiscounts, OrderItem, OrderNotificationBinding, OrderNotificationTrigger, OtpChallenge, Page, PasswordResetToken, Payment, Permission, Product, ProductAttribute, ProductCategory, ProductConfig, ProductVariant, RefundPolicy, RefundRequest, RssArticle, RssFeed, Seo, Tag, Tax, User, UserGroup, Vendor, VendorCustomer, VendorRole, VendorRolePermission, VendorUser, Wishlist, WishlistItem, ZIP_MIME_TYPES, applyVendorCustomersContactFilter, assertCaptchaOk, assertContactAllowedForVendorOrder, buildBlogMetadataUserPrompt, buildCronFromSchedule, buildRssUserPromptFromFeeds, calculateOrderRefundPreview, calculateRefundFromPolicy, checkEventsEnabled, checkMultiVendorEnabled, contactIsVendorCustomer, countRecentOtpSends, createAnalyticsHandlers, createBlogBySlugHandler, createChangePasswordHandler, createCmsApiHandler, createCmsApp, createCmsAppWithMessaging, createCrudByIdHandler, createCrudHandler, createDashboardStatsHandler, createEcommerceAnalyticsHandler, createEventOrderMessageTemplateHandlers, createForgotPasswordHandler, createFormBySlugHandler, createInviteAcceptHandler, createJobScheduleHandlers, createLlmAgentKnowledgeHandlers, createMediaZipExtractHandler, createMessageTemplateRowLoader, createOtpChallenge, createSetPasswordHandler, createSettingsApiHandlers, createSocialMediaHandlers, createStorefrontApiHandler, createUploadHandler, createUserAuthApiRouter, createUserAvatarHandler, createUserProfileHandler, createUsersApiHandlers, createVendorDashboardHandler, createVendorOnboardHandlers, customerPhoneForUser, daysBeforeEventStart, describeEventTierPolicy, ensureCustomerForUser, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, findActiveRefundPolicyForVendor, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRssArticleSummaryFromItem, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, isCustomerTypeContact, isZipMedia, linkUnclaimedContactToUser, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging };
30987
+ export { Address, Attendee, Attribute, BLOG_GENERATOR_AGENT_NAME, BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION, BLOG_GENERATOR_DEFAULT_VALIDATION_RULES, BLOG_GENERATOR_LLM_AGENT_SLUG, BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR, BLOG_METADATA_ENRICHER_AGENT_NAME, BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION, BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES, BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, Blog, BlogGeneratorService, Brand, CMS_ENTITY_MAP, Cart, CartItem, Category, ChatConversation, ChatMessage, Collection, Combo, ComboItem, Comment, Config, Contact, Currency, CurrencyExchange, Customer, Customer_Contacts, Discount, DiscountRules, Event, EventProduct, Form, FormField, FormSubmission, JobSchedule, JobScheduleRun, KnowledgeBaseChunk, KnowledgeBaseDocument, LlmAgent, Media, MessageTemplate, Order, OrderAddresses, OrderDiscounts, OrderItem, OrderNotificationBinding, OrderNotificationTrigger, OtpChallenge, Page, PasswordResetToken, Payment, Permission, Product, ProductAttribute, ProductCategory, ProductConfig, ProductVariant, RefundPolicy, RefundRequest, RssArticle, RssFeed, Seo, Tag, Tax, User, UserGroup, Vendor, VendorCustomer, VendorRole, VendorRolePermission, VendorUser, Wishlist, WishlistItem, ZIP_MIME_TYPES, applyRotatingVendorInvite, applyVendorCustomersContactFilter, assertCaptchaOk, assertContactAllowedForVendorOrder, buildBlogMetadataUserPrompt, buildCronFromSchedule, buildRssUserPromptFromFeeds, buildVendorInviteLink, calculateOrderRefundPreview, calculateRefundFromPolicy, checkEventsEnabled, checkMultiVendorEnabled, contactIsVendorCustomer, countRecentOtpSends, createAnalyticsHandlers, createBlogBySlugHandler, createChangePasswordHandler, createCmsApiHandler, createCmsApp, createCmsAppWithMessaging, createCrudByIdHandler, createCrudHandler, createDashboardStatsHandler, createEcommerceAnalyticsHandler, createEventOrderMessageTemplateHandlers, createForgotPasswordHandler, createFormBySlugHandler, createInviteAcceptHandler, createJobScheduleHandlers, createLlmAgentKnowledgeHandlers, createMediaZipExtractHandler, createMessageTemplateRowLoader, createOtpChallenge, createSetPasswordHandler, createSettingsApiHandlers, createSocialMediaHandlers, createStorefrontApiHandler, createUploadHandler, createUserAuthApiRouter, createUserAvatarHandler, createUserProfileHandler, createUsersApiHandlers, createVendorDashboardHandler, createVendorOnboardHandlers, customerPhoneForUser, daysBeforeEventStart, describeEventTierPolicy, ensureCustomerForUser, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, findActiveRefundPolicyForVendor, findVendorByInviteToken, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRssArticleSummaryFromItem, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, isCustomerTypeContact, isZipMedia, linkUnclaimedContactToUser, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging };