@open-mercato/core 0.6.6-develop.6133.1.f01540250e → 0.6.6-develop.6135.1.95b98004bd
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/modules/customer_accounts/api/admin/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +1 -0
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +6 -3
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/AGENTS.md +1 -0
- package/src/modules/customer_accounts/api/admin/users-invite.ts +10 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +10 -0
- package/src/modules/customer_accounts/events.ts +1 -0
- package/src/modules/notifications/api/unread-count/route.ts +6 -4
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
5
6
|
import { inviteUserSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
6
7
|
import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
7
8
|
import {
|
|
@@ -51,6 +52,14 @@ async function POST(req) {
|
|
|
51
52
|
displayName: parsed.data.displayName || null
|
|
52
53
|
}
|
|
53
54
|
);
|
|
55
|
+
void emitCustomerAccountsEvent("customer_accounts.user.invited", {
|
|
56
|
+
invitationId: invitation.id,
|
|
57
|
+
email: invitation.email,
|
|
58
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
59
|
+
invitedByType: "staff",
|
|
60
|
+
tenantId: auth.tenantId,
|
|
61
|
+
organizationId: auth.orgId
|
|
62
|
+
}).catch(() => void 0);
|
|
54
63
|
return NextResponse.json({
|
|
55
64
|
ok: true,
|
|
56
65
|
invitation: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/admin/users-invite.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata = {}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.invite'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId!, organizationId: auth.orgId! },\n {\n customerEntityId: parsed.data.customerEntityId || null,\n roleIds: parsed.data.roleIds,\n invitedByUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite customer user (admin)',\n description: 'Creates a staff-initiated invitation for a new customer user. The invitedByUserId is set from the staff auth context.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite customer user (admin)',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata = {}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.invite'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId!, organizationId: auth.orgId! },\n {\n customerEntityId: parsed.data.customerEntityId || null,\n roleIds: parsed.data.roleIds,\n invitedByUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n void emitCustomerAccountsEvent('customer_accounts.user.invited', {\n invitationId: invitation.id,\n email: invitation.email,\n customerEntityId: invitation.customerEntityId || null,\n invitedByType: 'staff',\n tenantId: auth.tenantId!,\n organizationId: auth.orgId!,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite customer user (admin)',\n description: 'Creates a staff-initiated invitation for a new customer user. The invitedByUserId is set from the staff auth context.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite customer user (admin)',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getCustomerAuthFromRequest, requireCustomerFeature } from "@open-mercato/core/modules/customer_accounts/lib/customerAuth";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
5
6
|
import { CustomerRole } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
6
7
|
import { inviteUserSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
7
8
|
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
@@ -76,6 +77,14 @@ async function POST(req) {
|
|
|
76
77
|
displayName: parsed.data.displayName || null
|
|
77
78
|
}
|
|
78
79
|
);
|
|
80
|
+
void emitCustomerAccountsEvent("customer_accounts.user.invited", {
|
|
81
|
+
invitationId: invitation.id,
|
|
82
|
+
email: invitation.email,
|
|
83
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
84
|
+
invitedByType: "portal",
|
|
85
|
+
tenantId: auth.tenantId,
|
|
86
|
+
organizationId: auth.orgId
|
|
87
|
+
}).catch(() => void 0);
|
|
79
88
|
return NextResponse.json({
|
|
80
89
|
ok: true,
|
|
81
90
|
invitation: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/portal/users-invite.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, ['portal.users.manage'], customerRbacService)\n } catch (response) {\n return response as NextResponse\n }\n\n if (!auth.customerEntityId) {\n return NextResponse.json({ ok: false, error: 'No company association' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n // Validate all roles are customer_assignable\n const requestedRoleIds = parsed.data.roleIds\n const roles = requestedRoleIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerRole,\n { id: { $in: requestedRoleIds }, tenantId: auth.tenantId, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n : []\n const rolesById = new Map(roles.map((role) => [role.id, role]))\n for (const roleId of requestedRoleIds) {\n const role = rolesById.get(roleId)\n if (!role) {\n return NextResponse.json({ ok: false, error: `Role ${roleId} not found` }, { status: 400 })\n }\n if (!role.customerAssignable) {\n return NextResponse.json({ ok: false, error: `Role \"${role.name}\" cannot be assigned by portal users` }, { status: 403 })\n }\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n {\n customerEntityId: auth.customerEntityId,\n roleIds: parsed.data.roleIds,\n invitedByCustomerUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite a user to the company portal',\n description: 'Creates an invitation for a new user to join the company portal.',\n tags: ['Customer Portal'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions or non-assignable role', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite portal user',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,4BAA4B,8BAA8B;AACnE,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerInviteRateLimitConfig,\n customerInviteIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerInviteIpRateLimitConfig,\n compoundConfig: customerInviteRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, ['portal.users.manage'], customerRbacService)\n } catch (response) {\n return response as NextResponse\n }\n\n if (!auth.customerEntityId) {\n return NextResponse.json({ ok: false, error: 'No company association' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = inviteUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n // Validate all roles are customer_assignable\n const requestedRoleIds = parsed.data.roleIds\n const roles = requestedRoleIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerRole,\n { id: { $in: requestedRoleIds }, tenantId: auth.tenantId, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n : []\n const rolesById = new Map(roles.map((role) => [role.id, role]))\n for (const roleId of requestedRoleIds) {\n const role = rolesById.get(roleId)\n if (!role) {\n return NextResponse.json({ ok: false, error: `Role ${roleId} not found` }, { status: 400 })\n }\n if (!role.customerAssignable) {\n return NextResponse.json({ ok: false, error: `Role \"${role.name}\" cannot be assigned by portal users` }, { status: 403 })\n }\n }\n\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n\n const { invitation } = await customerInvitationService.createInvitation(\n parsed.data.email,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n {\n customerEntityId: auth.customerEntityId,\n roleIds: parsed.data.roleIds,\n invitedByCustomerUserId: auth.sub,\n displayName: parsed.data.displayName || null,\n },\n )\n\n void emitCustomerAccountsEvent('customer_accounts.user.invited', {\n invitationId: invitation.id,\n email: invitation.email,\n customerEntityId: invitation.customerEntityId || null,\n invitedByType: 'portal',\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n invitation: {\n id: invitation.id,\n email: invitation.email,\n expiresAt: invitation.expiresAt,\n },\n }, { status: 201 })\n}\n\nconst successSchema = z.object({\n ok: z.literal(true),\n invitation: z.object({\n id: z.string().uuid(),\n email: z.string(),\n expiresAt: z.string().datetime(),\n }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Invite a user to the company portal',\n description: 'Creates an invitation for a new user to join the company portal.',\n tags: ['Customer Portal'],\n requestBody: { schema: inviteUserSchema },\n responses: [{ status: 201, description: 'Invitation created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions or non-assignable role', schema: errorSchema },\n { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Invite portal user',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,6 +15,7 @@ const events = [
|
|
|
15
15
|
{ id: "customer_accounts.role.created", label: "Customer Role Created", entity: "role", category: "crud" },
|
|
16
16
|
{ id: "customer_accounts.role.updated", label: "Customer Role Updated", entity: "role", category: "crud", portalBroadcast: true },
|
|
17
17
|
{ id: "customer_accounts.role.deleted", label: "Customer Role Deleted", entity: "role", category: "crud" },
|
|
18
|
+
{ id: "customer_accounts.user.invited", label: "Customer User Invited", entity: "user", category: "lifecycle", clientBroadcast: true },
|
|
18
19
|
{ id: "customer_accounts.invitation.accepted", label: "Customer Invitation Accepted", category: "lifecycle", clientBroadcast: true },
|
|
19
20
|
{ id: "customer_accounts.password_reset.requested", label: "Customer Password Reset Requested", category: "lifecycle" },
|
|
20
21
|
// Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/customer_accounts/events.ts"],
|
|
4
|
-
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA;AAAA,EAEtH,EAAE,IAAI,4CAA4C,OAAO,4BAA4B,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACvJ,EAAE,IAAI,6CAA6C,OAAO,8BAA8B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC/J,EAAE,IAAI,8CAA8C,OAAO,wBAAwB,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC1J,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,4CAA4C,OAAO,yBAAyB,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACpJ,EAAE,IAAI,6CAA6C,OAAO,0BAA0B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAC7J;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,4BAA4B,aAAa;AAItD,IAAO,iBAAQ;",
|
|
4
|
+
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.user.invited', label: 'Customer User Invited', entity: 'user', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACrI,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA;AAAA,EAEtH,EAAE,IAAI,4CAA4C,OAAO,4BAA4B,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACvJ,EAAE,IAAI,6CAA6C,OAAO,8BAA8B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC/J,EAAE,IAAI,8CAA8C,OAAO,wBAAwB,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC1J,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,4CAA4C,OAAO,yBAAyB,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACpJ,EAAE,IAAI,6CAA6C,OAAO,0BAA0B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAC7J;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,4BAA4B,aAAa;AAItD,IAAO,iBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,9 +22,12 @@ async function GET(req) {
|
|
|
22
22
|
const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null;
|
|
23
23
|
const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null;
|
|
24
24
|
if (cache && cacheKey) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
try {
|
|
26
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey));
|
|
27
|
+
if (typeof cached === "number") {
|
|
28
|
+
return Response.json({ unreadCount: cached });
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
const count = await em.count(Notification, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/notifications/api/unread-count/route.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(userId: string): string {\n return `notifications:unread-count:u=${userId}`\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null\n const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAAwB;AACxD,SAAO,gCAAgC,MAAM;AAC/C;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,QAAQ,UAAU,mBAAmB,IAAI,iBAAiB,IAAI,SAAS,IAAI;AACjF,QAAM,WAAW,SAAS,SAAS,yBAAyB,MAAM,IAAI;AAEtE,MAAI,SAAS,UAAU;AACrB,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { Notification } from '../../data/entities'\nimport { unreadCountResponseSchema } from '../openapi'\nimport { resolveNotificationContext } from '../../lib/routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nconst UNREAD_COUNT_RESOURCE = 'notifications.notification'\nconst UNREAD_COUNT_TTL_MS = 10_000\n\nfunction buildUnreadCountCacheKey(userId: string): string {\n return `notifications:unread-count:u=${userId}`\n}\n\nexport async function GET(req: Request) {\n const { scope, ctx } = await resolveNotificationContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n\n const userId = scope.userId\n const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null\n const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null\n\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (typeof cached === 'number') {\n return Response.json({ unreadCount: cached })\n }\n } catch {}\n }\n\n const count = await em.count(Notification, {\n recipientUserId: userId,\n tenantId: scope.tenantId,\n status: 'unread',\n })\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, count, {\n ttl: UNREAD_COUNT_TTL_MS,\n tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [null]),\n }),\n )\n } catch {}\n }\n\n return Response.json({ unreadCount: count })\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get unread notification count',\n tags: ['Notifications'],\n responses: {\n 200: {\n description: 'Unread count',\n content: {\n 'application/json': {\n schema: unreadCountResponseSchema,\n },\n },\n },\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAEpC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAC3B;AAEA,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,SAAS,yBAAyB,QAAwB;AACxD,SAAO,gCAAgC,MAAM;AAC/C;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAC3D,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AAErC,QAAM,SAAS,MAAM;AACrB,QAAM,QAAQ,UAAU,mBAAmB,IAAI,iBAAiB,IAAI,SAAS,IAAI;AACjF,QAAM,WAAW,SAAS,SAAS,yBAAyB,MAAM,IAAI;AAEtE,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM,SAAS,MAAM,mBAAmB,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,CAAC;AACjF,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO,SAAS,KAAK,EAAE,aAAa,OAAO,CAAC;AAAA,MAC9C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,QAAM,QAAQ,MAAM,GAAG,MAAM,cAAc;AAAA,IACzC,iBAAiB;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,SAAS,UAAU;AACrB,QAAI;AACF,YAAM;AAAA,QAAmB,MAAM;AAAA,QAAU,MACvC,MAAM,IAAI,UAAU,OAAO;AAAA,UACzB,KAAK;AAAA,UACL,MAAM,oBAAoB,uBAAuB,MAAM,UAAU,CAAC,IAAI,CAAC;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AAEA,SAAO,SAAS,KAAK,EAAE,aAAa,MAAM,CAAC;AAC7C;AAEO,MAAM,UAAU;AAAA,EACrB,KAAK;AAAA,IACH,SAAS;AAAA,IACT,MAAM,CAAC,eAAe;AAAA,IACtB,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6135.1.95b98004bd",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6135.1.95b98004bd",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6135.1.95b98004bd",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6135.1.95b98004bd",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6135.1.95b98004bd",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6135.1.95b98004bd",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6135.1.95b98004bd",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -262,6 +262,7 @@ Declared in `events.ts` via `createModuleEvents`. Emit with `emitCustomerAccount
|
|
|
262
262
|
| `customer_accounts.role.created` | crud | No |
|
|
263
263
|
| `customer_accounts.role.updated` | crud | No |
|
|
264
264
|
| `customer_accounts.role.deleted` | crud | No |
|
|
265
|
+
| `customer_accounts.user.invited` | lifecycle | Yes |
|
|
265
266
|
| `customer_accounts.invitation.accepted` | lifecycle | Yes |
|
|
266
267
|
|
|
267
268
|
## Subscribers
|
|
@@ -5,6 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
|
|
8
|
+
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
8
9
|
import { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
9
10
|
import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
10
11
|
import {
|
|
@@ -63,6 +64,15 @@ export async function POST(req: Request) {
|
|
|
63
64
|
},
|
|
64
65
|
)
|
|
65
66
|
|
|
67
|
+
void emitCustomerAccountsEvent('customer_accounts.user.invited', {
|
|
68
|
+
invitationId: invitation.id,
|
|
69
|
+
email: invitation.email,
|
|
70
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
71
|
+
invitedByType: 'staff',
|
|
72
|
+
tenantId: auth.tenantId!,
|
|
73
|
+
organizationId: auth.orgId!,
|
|
74
|
+
}).catch(() => undefined)
|
|
75
|
+
|
|
66
76
|
return NextResponse.json({
|
|
67
77
|
ok: true,
|
|
68
78
|
invitation: {
|
|
@@ -4,6 +4,7 @@ import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib
|
|
|
4
4
|
import { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
|
|
7
|
+
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
7
8
|
import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
|
|
8
9
|
import { CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
9
10
|
import { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
@@ -95,6 +96,15 @@ export async function POST(req: Request) {
|
|
|
95
96
|
},
|
|
96
97
|
)
|
|
97
98
|
|
|
99
|
+
void emitCustomerAccountsEvent('customer_accounts.user.invited', {
|
|
100
|
+
invitationId: invitation.id,
|
|
101
|
+
email: invitation.email,
|
|
102
|
+
customerEntityId: invitation.customerEntityId || null,
|
|
103
|
+
invitedByType: 'portal',
|
|
104
|
+
tenantId: auth.tenantId,
|
|
105
|
+
organizationId: auth.orgId,
|
|
106
|
+
}).catch(() => undefined)
|
|
107
|
+
|
|
98
108
|
return NextResponse.json({
|
|
99
109
|
ok: true,
|
|
100
110
|
invitation: {
|
|
@@ -16,6 +16,7 @@ const events = [
|
|
|
16
16
|
{ id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },
|
|
17
17
|
{ id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },
|
|
18
18
|
{ id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },
|
|
19
|
+
{ id: 'customer_accounts.user.invited', label: 'Customer User Invited', entity: 'user', category: 'lifecycle', clientBroadcast: true },
|
|
19
20
|
{ id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },
|
|
20
21
|
{ id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },
|
|
21
22
|
// Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
|
|
@@ -29,10 +29,12 @@ export async function GET(req: Request) {
|
|
|
29
29
|
const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null
|
|
30
30
|
|
|
31
31
|
if (cache && cacheKey) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
try {
|
|
33
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
|
|
34
|
+
if (typeof cached === 'number') {
|
|
35
|
+
return Response.json({ unreadCount: cached })
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const count = await em.count(Notification, {
|