@open-mercato/core 0.4.8-develop-28cee031d6 → 0.4.8-develop-15259be22b
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/agentic/standalone-guide.md +235 -0
- package/dist/generated/entities/customer_role/index.js +27 -0
- package/dist/generated/entities/customer_role/index.js.map +7 -0
- package/dist/generated/entities/customer_role_acl/index.js +19 -0
- package/dist/generated/entities/customer_role_acl/index.js.map +7 -0
- package/dist/generated/entities/customer_user/index.js +37 -0
- package/dist/generated/entities/customer_user/index.js.map +7 -0
- package/dist/generated/entities/customer_user_acl/index.js +19 -0
- package/dist/generated/entities/customer_user_acl/index.js.map +7 -0
- package/dist/generated/entities/customer_user_email_verification/index.js +17 -0
- package/dist/generated/entities/customer_user_email_verification/index.js.map +7 -0
- package/dist/generated/entities/customer_user_invitation/index.js +33 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +7 -0
- package/dist/generated/entities/customer_user_password_reset/index.js +15 -0
- package/dist/generated/entities/customer_user_password_reset/index.js.map +7 -0
- package/dist/generated/entities/customer_user_role/index.js +13 -0
- package/dist/generated/entities/customer_user_role/index.js.map +7 -0
- package/dist/generated/entities/customer_user_session/index.js +21 -0
- package/dist/generated/entities/customer_user_session/index.js.map +7 -0
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +14 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +18 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +3 -9
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/customer_accounts/acl.js +12 -0
- package/dist/modules/customer_accounts/acl.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +87 -0
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +216 -0
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/roles.js +189 -0
- package/dist/modules/customer_accounts/api/admin/roles.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js +69 -0
- package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js +64 -0
- package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/users/[id].js +253 -0
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js +78 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/users.js +251 -0
- package/dist/modules/customer_accounts/api/admin/users.js.map +7 -0
- package/dist/modules/customer_accounts/api/email/verify.js +59 -0
- package/dist/modules/customer_accounts/api/email/verify.js.map +7 -0
- package/dist/modules/customer_accounts/api/interceptors.js +5 -0
- package/dist/modules/customer_accounts/api/interceptors.js.map +7 -0
- package/dist/modules/customer_accounts/api/invitations/accept.js +114 -0
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +7 -0
- package/dist/modules/customer_accounts/api/login.js +143 -0
- package/dist/modules/customer_accounts/api/login.js.map +7 -0
- package/dist/modules/customer_accounts/api/magic-link/request.js +78 -0
- package/dist/modules/customer_accounts/api/magic-link/request.js.map +7 -0
- package/dist/modules/customer_accounts/api/magic-link/verify.js +114 -0
- package/dist/modules/customer_accounts/api/magic-link/verify.js.map +7 -0
- package/dist/modules/customer_accounts/api/password/reset-confirm.js +59 -0
- package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +7 -0
- package/dist/modules/customer_accounts/api/password/reset-request.js +77 -0
- package/dist/modules/customer_accounts/api/password/reset-request.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/events/stream.js +163 -0
- package/dist/modules/customer_accounts/api/portal/events/stream.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/feature-check.js +57 -0
- package/dist/modules/customer_accounts/api/portal/feature-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/logout.js +64 -0
- package/dist/modules/customer_accounts/api/portal/logout.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/notifications/[id]/dismiss.js +49 -0
- package/dist/modules/customer_accounts/api/portal/notifications/[id]/dismiss.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/notifications/[id]/read.js +49 -0
- package/dist/modules/customer_accounts/api/portal/notifications/[id]/read.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/notifications/mark-all-read.js +46 -0
- package/dist/modules/customer_accounts/api/portal/notifications/mark-all-read.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/notifications/unread-count.js +42 -0
- package/dist/modules/customer_accounts/api/portal/notifications/unread-count.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/notifications.js +105 -0
- package/dist/modules/customer_accounts/api/portal/notifications.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/password-change.js +57 -0
- package/dist/modules/customer_accounts/api/portal/password-change.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/profile.js +135 -0
- package/dist/modules/customer_accounts/api/portal/profile.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/sessions/[id].js +62 -0
- package/dist/modules/customer_accounts/api/portal/sessions/[id].js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +75 -0
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/sessions.js +77 -0
- package/dist/modules/customer_accounts/api/portal/sessions.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +90 -0
- package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/users/[id].js +71 -0
- package/dist/modules/customer_accounts/api/portal/users/[id].js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js +92 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +7 -0
- package/dist/modules/customer_accounts/api/portal/users.js +79 -0
- package/dist/modules/customer_accounts/api/portal/users.js.map +7 -0
- package/dist/modules/customer_accounts/api/signup.js +121 -0
- package/dist/modules/customer_accounts/api/signup.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.js +491 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.js +15 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +343 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.js +16 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.js +180 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.js +16 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +176 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.meta.js +33 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/backend/page.js +466 -0
- package/dist/modules/customer_accounts/backend/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/page.meta.js +35 -0
- package/dist/modules/customer_accounts/backend/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/ce.js +26 -0
- package/dist/modules/customer_accounts/ce.js.map +7 -0
- package/dist/modules/customer_accounts/data/enrichers.js +85 -0
- package/dist/modules/customer_accounts/data/enrichers.js.map +7 -0
- package/dist/modules/customer_accounts/data/entities.js +377 -0
- package/dist/modules/customer_accounts/data/entities.js.map +7 -0
- package/dist/modules/customer_accounts/data/extensions.js +8 -0
- package/dist/modules/customer_accounts/data/extensions.js.map +7 -0
- package/dist/modules/customer_accounts/data/validators.js +111 -0
- package/dist/modules/customer_accounts/data/validators.js.map +7 -0
- package/dist/modules/customer_accounts/di.js +17 -0
- package/dist/modules/customer_accounts/di.js.map +7 -0
- package/dist/modules/customer_accounts/events.js +28 -0
- package/dist/modules/customer_accounts/events.js.map +7 -0
- package/dist/modules/customer_accounts/index.js +15 -0
- package/dist/modules/customer_accounts/index.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerAuth.js +71 -0
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +29 -0
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js +63 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js.map +7 -0
- package/dist/modules/customer_accounts/lib/tokenGenerator.js +12 -0
- package/dist/modules/customer_accounts/lib/tokenGenerator.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260313222043.js +49 -0
- package/dist/modules/customer_accounts/migrations/Migration20260313222043.js.map +7 -0
- package/dist/modules/customer_accounts/notifications.client.js +47 -0
- package/dist/modules/customer_accounts/notifications.client.js.map +7 -0
- package/dist/modules/customer_accounts/notifications.js +46 -0
- package/dist/modules/customer_accounts/notifications.js.map +7 -0
- package/dist/modules/customer_accounts/search.js +120 -0
- package/dist/modules/customer_accounts/search.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +87 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerRbacService.js +109 -0
- package/dist/modules/customer_accounts/services/customerRbacService.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js +75 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerTokenService.js +91 -0
- package/dist/modules/customer_accounts/services/customerTokenService.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerUserService.js +92 -0
- package/dist/modules/customer_accounts/services/customerUserService.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +179 -0
- package/dist/modules/customer_accounts/setup.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +54 -0
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js +68 -0
- package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyStaffOnSignup.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyStaffOnSignup.js.map +7 -0
- package/dist/modules/customer_accounts/translations.js +9 -0
- package/dist/modules/customer_accounts/translations.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +63 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +55 -0
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js +26 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +7 -0
- package/dist/modules/customer_accounts/workers/cleanupExpiredSessions.js +23 -0
- package/dist/modules/customer_accounts/workers/cleanupExpiredSessions.js.map +7 -0
- package/dist/modules/customer_accounts/workers/cleanupExpiredTokens.js +38 -0
- package/dist/modules/customer_accounts/workers/cleanupExpiredTokens.js.map +7 -0
- package/dist/modules/customers/components/AddressTiles.js +1 -1
- package/dist/modules/customers/components/AddressTiles.js.map +2 -2
- package/dist/modules/directory/api/get/organizations/lookup.js +83 -0
- package/dist/modules/directory/api/get/organizations/lookup.js.map +7 -0
- package/dist/modules/directory/commands/organizations.js +32 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +6 -2
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +3 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260314143323.js +15 -0
- package/dist/modules/directory/migrations/Migration20260314143323.js.map +7 -0
- package/dist/modules/directory/setup.js +36 -0
- package/dist/modules/directory/setup.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260313222043.js +15 -0
- package/dist/modules/payment_gateways/migrations/Migration20260313222043.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +131 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +96 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +94 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +89 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +104 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +7 -0
- package/dist/modules/portal/index.js +11 -0
- package/dist/modules/portal/index.js.map +7 -0
- package/dist/modules/portal/setup.js +23 -0
- package/dist/modules/portal/setup.js.map +7 -0
- package/generated/entities/customer_role/index.ts +12 -0
- package/generated/entities/customer_role_acl/index.ts +8 -0
- package/generated/entities/customer_user/index.ts +17 -0
- package/generated/entities/customer_user_acl/index.ts +8 -0
- package/generated/entities/customer_user_email_verification/index.ts +7 -0
- package/generated/entities/customer_user_invitation/index.ts +15 -0
- package/generated/entities/customer_user_password_reset/index.ts +6 -0
- package/generated/entities/customer_user_role/index.ts +5 -0
- package/generated/entities/customer_user_session/index.ts +9 -0
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entities.ids.generated.ts +14 -1
- package/generated/entity-fields-registry.ts +18 -0
- package/package.json +3 -3
- package/src/modules/auth/services/rbacService.ts +3 -9
- package/src/modules/customer_accounts/AGENTS.md +377 -0
- package/src/modules/customer_accounts/acl.ts +8 -0
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +98 -0
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +246 -0
- package/src/modules/customer_accounts/api/admin/roles.ts +212 -0
- package/src/modules/customer_accounts/api/admin/users/[id]/reset-password.ts +78 -0
- package/src/modules/customer_accounts/api/admin/users/[id]/verify-email.ts +72 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +289 -0
- package/src/modules/customer_accounts/api/admin/users-invite.ts +86 -0
- package/src/modules/customer_accounts/api/admin/users.ts +280 -0
- package/src/modules/customer_accounts/api/email/verify.ts +66 -0
- package/src/modules/customer_accounts/api/interceptors.ts +3 -0
- package/src/modules/customer_accounts/api/invitations/accept.ts +128 -0
- package/src/modules/customer_accounts/api/login.ts +163 -0
- package/src/modules/customer_accounts/api/magic-link/request.ts +87 -0
- package/src/modules/customer_accounts/api/magic-link/verify.ts +132 -0
- package/src/modules/customer_accounts/api/password/reset-confirm.ts +69 -0
- package/src/modules/customer_accounts/api/password/reset-request.ts +87 -0
- package/src/modules/customer_accounts/api/portal/events/stream.ts +209 -0
- package/src/modules/customer_accounts/api/portal/feature-check.ts +60 -0
- package/src/modules/customer_accounts/api/portal/logout.ts +71 -0
- package/src/modules/customer_accounts/api/portal/notifications/[id]/dismiss.ts +54 -0
- package/src/modules/customer_accounts/api/portal/notifications/[id]/read.ts +54 -0
- package/src/modules/customer_accounts/api/portal/notifications/mark-all-read.ts +49 -0
- package/src/modules/customer_accounts/api/portal/notifications/unread-count.ts +45 -0
- package/src/modules/customer_accounts/api/portal/notifications.ts +115 -0
- package/src/modules/customer_accounts/api/portal/password-change.ts +65 -0
- package/src/modules/customer_accounts/api/portal/profile.ts +151 -0
- package/src/modules/customer_accounts/api/portal/sessions/[id].ts +70 -0
- package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +87 -0
- package/src/modules/customer_accounts/api/portal/sessions.ts +84 -0
- package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +106 -0
- package/src/modules/customer_accounts/api/portal/users/[id].ts +81 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +103 -0
- package/src/modules/customer_accounts/api/portal/users.ts +86 -0
- package/src/modules/customer_accounts/api/signup.ts +136 -0
- package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.ts +11 -0
- package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.tsx +607 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.ts +12 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +385 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.ts +12 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.tsx +203 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.meta.ts +31 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +217 -0
- package/src/modules/customer_accounts/backend/page.meta.ts +33 -0
- package/src/modules/customer_accounts/backend/page.tsx +535 -0
- package/src/modules/customer_accounts/ce.ts +22 -0
- package/src/modules/customer_accounts/data/enrichers.ts +117 -0
- package/src/modules/customer_accounts/data/entities.ts +302 -0
- package/src/modules/customer_accounts/data/extensions.ts +4 -0
- package/src/modules/customer_accounts/data/validators.ts +128 -0
- package/src/modules/customer_accounts/di.ts +15 -0
- package/src/modules/customer_accounts/events.ts +28 -0
- package/src/modules/customer_accounts/i18n/de.json +176 -0
- package/src/modules/customer_accounts/i18n/en.json +176 -0
- package/src/modules/customer_accounts/i18n/es.json +176 -0
- package/src/modules/customer_accounts/i18n/pl.json +176 -0
- package/src/modules/customer_accounts/index.ts +13 -0
- package/src/modules/customer_accounts/lib/customerAuth.ts +85 -0
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +54 -0
- package/src/modules/customer_accounts/lib/rateLimiter.ts +36 -0
- package/src/modules/customer_accounts/lib/tokenGenerator.ts +9 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +1255 -0
- package/src/modules/customer_accounts/migrations/Migration20260313222043.ts +62 -0
- package/src/modules/customer_accounts/notifications.client.ts +46 -0
- package/src/modules/customer_accounts/notifications.ts +44 -0
- package/src/modules/customer_accounts/search.ts +134 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +109 -0
- package/src/modules/customer_accounts/services/customerRbacService.ts +144 -0
- package/src/modules/customer_accounts/services/customerSessionService.ts +90 -0
- package/src/modules/customer_accounts/services/customerTokenService.ts +98 -0
- package/src/modules/customer_accounts/services/customerUserService.ts +105 -0
- package/src/modules/customer_accounts/setup.ts +212 -0
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +65 -0
- package/src/modules/customer_accounts/subscribers/autoLinkCrmReverse.ts +78 -0
- package/src/modules/customer_accounts/subscribers/notifyStaffOnSignup.ts +32 -0
- package/src/modules/customer_accounts/translations.ts +5 -0
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +89 -0
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.ts +16 -0
- package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +78 -0
- package/src/modules/customer_accounts/widgets/injection/company-users/widget.ts +16 -0
- package/src/modules/customer_accounts/widgets/injection-table.ts +24 -0
- package/src/modules/customer_accounts/workers/cleanupExpiredSessions.ts +33 -0
- package/src/modules/customer_accounts/workers/cleanupExpiredTokens.ts +51 -0
- package/src/modules/customers/components/AddressTiles.tsx +1 -1
- package/src/modules/directory/api/get/organizations/lookup.ts +92 -0
- package/src/modules/directory/commands/organizations.ts +34 -1
- package/src/modules/directory/data/entities.ts +5 -1
- package/src/modules/directory/data/validators.ts +4 -0
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +20 -1
- package/src/modules/directory/migrations/Migration20260314143323.ts +15 -0
- package/src/modules/directory/setup.ts +41 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +4 -1
- package/src/modules/payment_gateways/migrations/Migration20260313222043.ts +17 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +158 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +120 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +118 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +112 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +138 -0
- package/src/modules/portal/i18n/de.json +93 -0
- package/src/modules/portal/i18n/en.json +93 -0
- package/src/modules/portal/i18n/es.json +93 -0
- package/src/modules/portal/i18n/pl.json +93 -0
- package/src/modules/portal/index.ts +9 -0
- package/src/modules/portal/setup.ts +23 -0
- package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +226 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import React, { useEffect, useMemo, useState, useCallback } from 'react'
|
|
3
|
+
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
7
|
+
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
8
|
+
import { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'
|
|
9
|
+
import { PortalCard, PortalCardHeader } from '@open-mercato/ui/portal/components/PortalCard'
|
|
10
|
+
import { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'
|
|
11
|
+
import { usePortalDashboardWidgets } from '@open-mercato/ui/portal/hooks/usePortalDashboardWidgets'
|
|
12
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
13
|
+
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
14
|
+
|
|
15
|
+
type Props = { params: { orgSlug: string } }
|
|
16
|
+
|
|
17
|
+
const HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'
|
|
18
|
+
|
|
19
|
+
function loadHiddenWidgets(): Set<string> {
|
|
20
|
+
try {
|
|
21
|
+
const raw = localStorage.getItem(HIDDEN_WIDGETS_KEY)
|
|
22
|
+
if (!raw) return new Set()
|
|
23
|
+
return new Set(JSON.parse(raw))
|
|
24
|
+
} catch {
|
|
25
|
+
return new Set()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveHiddenWidgets(hidden: Set<string>) {
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(HIDDEN_WIDGETS_KEY, JSON.stringify(Array.from(hidden)))
|
|
32
|
+
} catch {
|
|
33
|
+
// best effort
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function WidgetIcon({ className }: { className?: string }) {
|
|
38
|
+
return (
|
|
39
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
40
|
+
<rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="5" x="14" y="12" rx="1" /><rect width="7" height="9" x="3" y="16" rx="1" />
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function PortalDashboardPage({ params }: Props) {
|
|
46
|
+
const t = useT()
|
|
47
|
+
const router = useRouter()
|
|
48
|
+
const { auth } = usePortalContext()
|
|
49
|
+
const { user, loading } = auth
|
|
50
|
+
|
|
51
|
+
const [editing, setEditing] = useState(false)
|
|
52
|
+
const [hiddenWidgets, setHiddenWidgets] = useState<Set<string>>(() => loadHiddenWidgets())
|
|
53
|
+
|
|
54
|
+
const { widgets: dashboardWidgets, isLoading: widgetsLoading } = usePortalDashboardWidgets('portal:dashboard:sections' as any)
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!loading && !user) {
|
|
58
|
+
router.replace(`/${params.orgSlug}/portal/login`)
|
|
59
|
+
}
|
|
60
|
+
}, [loading, user, router, params.orgSlug])
|
|
61
|
+
|
|
62
|
+
const toggleWidget = useCallback((widgetId: string) => {
|
|
63
|
+
setHiddenWidgets((prev) => {
|
|
64
|
+
const next = new Set(prev)
|
|
65
|
+
if (next.has(widgetId)) {
|
|
66
|
+
next.delete(widgetId)
|
|
67
|
+
} else {
|
|
68
|
+
next.add(widgetId)
|
|
69
|
+
}
|
|
70
|
+
saveHiddenWidgets(next)
|
|
71
|
+
return next
|
|
72
|
+
})
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
const visibleWidgets = useMemo(
|
|
76
|
+
() => dashboardWidgets.filter((w) => !hiddenWidgets.has(w.metadata.id)),
|
|
77
|
+
[dashboardWidgets, hiddenWidgets],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const injectionContext = useMemo(
|
|
81
|
+
() => ({ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }),
|
|
82
|
+
[params.orgSlug, user, auth.roles, auth.resolvedFeatures],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (loading) {
|
|
86
|
+
return <div className="flex items-center justify-center py-20"><Spinner /></div>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!user) return null
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-col gap-8">
|
|
93
|
+
<PortalPageHeader
|
|
94
|
+
label={t('portal.dashboard.title', 'Dashboard')}
|
|
95
|
+
title={t('portal.dashboard.welcome', { name: user.displayName })}
|
|
96
|
+
action={
|
|
97
|
+
dashboardWidgets.length > 0 ? (
|
|
98
|
+
<Button
|
|
99
|
+
type="button"
|
|
100
|
+
variant={editing ? 'default' : 'outline'}
|
|
101
|
+
size="sm"
|
|
102
|
+
className="rounded-lg text-[13px]"
|
|
103
|
+
onClick={() => setEditing((prev) => !prev)}
|
|
104
|
+
>
|
|
105
|
+
{editing ? t('portal.dashboard.done', 'Done') : t('portal.dashboard.customize', 'Customize')}
|
|
106
|
+
</Button>
|
|
107
|
+
) : null
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageBefore('dashboard')} context={injectionContext} />
|
|
112
|
+
|
|
113
|
+
{editing && dashboardWidgets.length > 0 ? (
|
|
114
|
+
<PortalCard>
|
|
115
|
+
<PortalCardHeader
|
|
116
|
+
label={t('portal.dashboard.customize', 'Customize')}
|
|
117
|
+
title={t('portal.dashboard.widgets', 'Dashboard Widgets')}
|
|
118
|
+
/>
|
|
119
|
+
<div className="flex flex-wrap gap-2">
|
|
120
|
+
{dashboardWidgets.map((widget) => {
|
|
121
|
+
const isHidden = hiddenWidgets.has(widget.metadata.id)
|
|
122
|
+
return (
|
|
123
|
+
<Button key={widget.metadata.id} type="button" variant={isHidden ? 'outline' : 'default'} size="sm" className="rounded-lg text-[13px]" onClick={() => toggleWidget(widget.metadata.id)}>
|
|
124
|
+
{widget.metadata.title || widget.metadata.id}
|
|
125
|
+
</Button>
|
|
126
|
+
)
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
</PortalCard>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
{visibleWidgets.length > 0 ? (
|
|
133
|
+
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
|
134
|
+
{visibleWidgets.map((widget) => {
|
|
135
|
+
const WidgetComponent = widget.Widget
|
|
136
|
+
if (!WidgetComponent) return null
|
|
137
|
+
return (
|
|
138
|
+
<PortalCard key={widget.metadata.id}>
|
|
139
|
+
<PortalCardHeader title={widget.metadata.title || widget.metadata.id} />
|
|
140
|
+
<WidgetComponent context={{ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }} />
|
|
141
|
+
</PortalCard>
|
|
142
|
+
)
|
|
143
|
+
})}
|
|
144
|
+
</div>
|
|
145
|
+
) : null}
|
|
146
|
+
|
|
147
|
+
{dashboardWidgets.length === 0 && !widgetsLoading ? (
|
|
148
|
+
<PortalEmptyState
|
|
149
|
+
icon={<WidgetIcon className="size-5" />}
|
|
150
|
+
title={t('portal.dashboard.emptyWidgets', 'No dashboard widgets yet')}
|
|
151
|
+
description="Modules can inject widgets into this dashboard via the portal:dashboard:sections injection spot."
|
|
152
|
+
/>
|
|
153
|
+
) : null}
|
|
154
|
+
|
|
155
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageAfter('dashboard')} context={injectionContext} />
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
6
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
7
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
|
+
import { Notice } from '@open-mercato/ui/primitives/Notice'
|
|
9
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
10
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
|
+
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
12
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
13
|
+
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
14
|
+
|
|
15
|
+
type Props = { params: { orgSlug: string } }
|
|
16
|
+
|
|
17
|
+
export default function PortalLoginPage({ params }: Props) {
|
|
18
|
+
const t = useT()
|
|
19
|
+
const orgSlug = params.orgSlug
|
|
20
|
+
const { tenant } = usePortalContext()
|
|
21
|
+
|
|
22
|
+
const [email, setEmail] = useState('')
|
|
23
|
+
const [password, setPassword] = useState('')
|
|
24
|
+
const [error, setError] = useState<string | null>(null)
|
|
25
|
+
const [submitting, setSubmitting] = useState(false)
|
|
26
|
+
|
|
27
|
+
const handleSubmit = useCallback(
|
|
28
|
+
async (event: React.FormEvent) => {
|
|
29
|
+
event.preventDefault()
|
|
30
|
+
setError(null)
|
|
31
|
+
|
|
32
|
+
if (!tenant.tenantId) {
|
|
33
|
+
setError(t('portal.org.invalid', 'Organization not found.'))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setSubmitting(true)
|
|
38
|
+
try {
|
|
39
|
+
const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/login', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ email, password, tenantId: tenant.tenantId }),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (result.ok && result.result?.ok) {
|
|
46
|
+
window.location.assign(`/${orgSlug}/portal/dashboard`)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (result.status === 423) {
|
|
51
|
+
setError(t('portal.login.error.locked', 'Account locked. Try again later.'))
|
|
52
|
+
} else if (result.status === 401) {
|
|
53
|
+
setError(t('portal.login.error.invalidCredentials', 'Invalid email or password.'))
|
|
54
|
+
} else {
|
|
55
|
+
setError(result.result?.error || t('portal.login.error.generic', 'Login failed. Please try again.'))
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
setError(t('portal.login.error.generic', 'Login failed. Please try again.'))
|
|
59
|
+
} finally {
|
|
60
|
+
setSubmitting(false)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[email, password, tenant.tenantId, orgSlug, t],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const injectionContext = useMemo(
|
|
67
|
+
() => ({ orgSlug }),
|
|
68
|
+
[orgSlug],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (tenant.loading) {
|
|
72
|
+
return <div className="flex items-center justify-center py-20"><Spinner /></div>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (tenant.error) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="mx-auto w-full max-w-md py-12">
|
|
78
|
+
<Notice variant="error">{t('portal.org.invalid', 'Organization not found.')}</Notice>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="mx-auto w-full max-w-sm">
|
|
85
|
+
<div className="mb-8 text-center">
|
|
86
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('portal.login.title', 'Sign In')}</h1>
|
|
87
|
+
<p className="mt-1.5 text-sm text-muted-foreground">{t('portal.login.description', 'Enter your credentials to access the portal.')}</p>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageBefore('login')} context={injectionContext} />
|
|
91
|
+
|
|
92
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
93
|
+
{error ? <Notice variant="error">{error}</Notice> : null}
|
|
94
|
+
|
|
95
|
+
<div className="flex flex-col gap-1.5">
|
|
96
|
+
<Label htmlFor="login-email" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.login.email', 'Email')}</Label>
|
|
97
|
+
<Input id="login-email" type="email" autoComplete="email" required placeholder={t('portal.login.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="flex flex-col gap-1.5">
|
|
101
|
+
<Label htmlFor="login-password" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.login.password', 'Password')}</Label>
|
|
102
|
+
<Input id="login-password" type="password" autoComplete="current-password" required placeholder={t('portal.login.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<Button type="submit" disabled={submitting} className="mt-1 w-full rounded-lg">
|
|
106
|
+
{submitting ? t('portal.login.submitting', 'Signing in...') : t('portal.login.submit', 'Sign In')}
|
|
107
|
+
</Button>
|
|
108
|
+
|
|
109
|
+
<p className="text-center text-[13px] text-muted-foreground">
|
|
110
|
+
{t('portal.login.noAccount', "Don't have an account?")}{' '}
|
|
111
|
+
<Link href={`/${orgSlug}/portal/signup`} className="font-medium text-foreground underline underline-offset-4 hover:opacity-80">
|
|
112
|
+
{t('portal.login.signupLink', 'Sign up')}
|
|
113
|
+
</Link>
|
|
114
|
+
</p>
|
|
115
|
+
</form>
|
|
116
|
+
|
|
117
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageAfter('login')} context={injectionContext} />
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useEffect, useMemo } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
8
|
+
import { Notice } from '@open-mercato/ui/primitives/Notice'
|
|
9
|
+
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
10
|
+
import { PortalFeatureCard } from '@open-mercato/ui/portal/components/PortalFeatureCard'
|
|
11
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
12
|
+
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
13
|
+
|
|
14
|
+
type Props = { params: { orgSlug: string } }
|
|
15
|
+
|
|
16
|
+
function ShoppingBagIcon({ className }: { className?: string }) {
|
|
17
|
+
return (
|
|
18
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
19
|
+
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><line x1="3" x2="21" y1="6" y2="6" /><path d="M16 10a4 4 0 0 1-8 0" />
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function UserIcon({ className }: { className?: string }) {
|
|
25
|
+
return (
|
|
26
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
27
|
+
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
|
|
28
|
+
</svg>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ShieldIcon({ className }: { className?: string }) {
|
|
33
|
+
return (
|
|
34
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
35
|
+
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
|
36
|
+
</svg>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function PortalLandingPage({ params }: Props) {
|
|
41
|
+
const t = useT()
|
|
42
|
+
const router = useRouter()
|
|
43
|
+
const orgSlug = params.orgSlug
|
|
44
|
+
const { auth, tenant } = usePortalContext()
|
|
45
|
+
|
|
46
|
+
// Redirect authenticated users to dashboard
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!auth.loading && auth.user) {
|
|
49
|
+
router.replace(`/${orgSlug}/portal/dashboard`)
|
|
50
|
+
}
|
|
51
|
+
}, [auth.loading, auth.user, router, orgSlug])
|
|
52
|
+
|
|
53
|
+
const injectionContext = useMemo(
|
|
54
|
+
() => ({ orgSlug }),
|
|
55
|
+
[orgSlug],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (auth.loading || tenant.loading) {
|
|
59
|
+
return <div className="flex items-center justify-center py-20"><Spinner /></div>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (tenant.error) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="mx-auto w-full max-w-md py-12">
|
|
65
|
+
<Notice variant="error">{t('portal.org.invalid', 'Organization not found.')}</Notice>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Authenticated user — redirect is in progress
|
|
71
|
+
if (auth.user) return null
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageBefore('home')} context={injectionContext} />
|
|
76
|
+
|
|
77
|
+
<section className="flex flex-col items-center gap-5 py-8 text-center sm:py-16">
|
|
78
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground/60">
|
|
79
|
+
{t('portal.nav.home', 'Customer Portal')}
|
|
80
|
+
</p>
|
|
81
|
+
<h1 className="max-w-2xl text-4xl font-bold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
|
82
|
+
{t('portal.landing.hero.title', 'Welcome to your portal')}
|
|
83
|
+
</h1>
|
|
84
|
+
<p className="max-w-lg text-base leading-relaxed text-muted-foreground sm:text-lg">
|
|
85
|
+
{t('portal.landing.hero.description', 'Access your account, manage orders, and stay up to date.')}
|
|
86
|
+
</p>
|
|
87
|
+
<div className="mt-2 flex items-center gap-3">
|
|
88
|
+
<Button asChild size="lg" className="rounded-lg px-6 text-[14px]">
|
|
89
|
+
<Link href={`/${orgSlug}/portal/login`}>{t('portal.landing.cta.login', 'Sign In')}</Link>
|
|
90
|
+
</Button>
|
|
91
|
+
<Button asChild variant="outline" size="lg" className="rounded-lg px-6 text-[14px]">
|
|
92
|
+
<Link href={`/${orgSlug}/portal/signup`}>{t('portal.landing.cta.signup', 'Create Account')}</Link>
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
|
|
97
|
+
<section className="grid gap-4 sm:grid-cols-3">
|
|
98
|
+
<PortalFeatureCard
|
|
99
|
+
icon={<ShoppingBagIcon className="size-5" />}
|
|
100
|
+
title={t('portal.landing.feature.orders', 'Orders & Invoices')}
|
|
101
|
+
description={t('portal.landing.feature.orders.description', 'Track your orders, download invoices, and view delivery status in real time.')}
|
|
102
|
+
/>
|
|
103
|
+
<PortalFeatureCard
|
|
104
|
+
icon={<UserIcon className="size-5" />}
|
|
105
|
+
title={t('portal.landing.feature.account', 'Account Management')}
|
|
106
|
+
description={t('portal.landing.feature.account.description', 'Update your profile, manage team members, and configure your preferences.')}
|
|
107
|
+
/>
|
|
108
|
+
<PortalFeatureCard
|
|
109
|
+
icon={<ShieldIcon className="size-5" />}
|
|
110
|
+
title={t('portal.landing.feature.security', 'Secure Access')}
|
|
111
|
+
description={t('portal.landing.feature.security.description', 'Role-based permissions, session management, and full audit trail.')}
|
|
112
|
+
/>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageAfter('home')} context={injectionContext} />
|
|
116
|
+
</>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useEffect, useMemo } from 'react'
|
|
3
|
+
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
6
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
7
|
+
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
8
|
+
import { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'
|
|
9
|
+
import { PortalCard, PortalCardHeader, PortalStatRow, PortalCardDivider } from '@open-mercato/ui/portal/components/PortalCard'
|
|
10
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
11
|
+
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
12
|
+
|
|
13
|
+
type Props = { params: { orgSlug: string } }
|
|
14
|
+
|
|
15
|
+
export default function PortalProfilePage({ params }: Props) {
|
|
16
|
+
const t = useT()
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
const { auth } = usePortalContext()
|
|
19
|
+
const { user, roles, resolvedFeatures, isPortalAdmin, loading } = auth
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!loading && !user) {
|
|
23
|
+
router.replace(`/${params.orgSlug}/portal/login`)
|
|
24
|
+
}
|
|
25
|
+
}, [loading, user, router, params.orgSlug])
|
|
26
|
+
|
|
27
|
+
const injectionContext = useMemo(
|
|
28
|
+
() => ({ orgSlug: params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin }),
|
|
29
|
+
[params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if (loading) {
|
|
33
|
+
return <div className="flex items-center justify-center py-20"><Spinner /></div>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!user) return null
|
|
37
|
+
|
|
38
|
+
const formatDate = (dateString: string | null) => {
|
|
39
|
+
if (!dateString) return t('portal.dashboard.never', 'Never')
|
|
40
|
+
return new Date(dateString).toLocaleDateString(undefined, {
|
|
41
|
+
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col gap-8">
|
|
47
|
+
<PortalPageHeader
|
|
48
|
+
label={t('portal.profile.label', 'Account')}
|
|
49
|
+
title={t('portal.profile.title', 'Profile')}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageBefore('profile')} context={injectionContext} />
|
|
53
|
+
|
|
54
|
+
<div className="grid gap-5 md:grid-cols-2">
|
|
55
|
+
<PortalCard>
|
|
56
|
+
<PortalCardHeader label={t('portal.dashboard.profile', 'Profile')} title={user.displayName} />
|
|
57
|
+
<div className="flex flex-col">
|
|
58
|
+
<PortalStatRow
|
|
59
|
+
label={t('portal.dashboard.email', 'Email')}
|
|
60
|
+
value={
|
|
61
|
+
<span className="flex items-center gap-2">
|
|
62
|
+
<span className="truncate">{user.email}</span>
|
|
63
|
+
<Badge variant={user.emailVerified ? 'default' : 'outline'} className="shrink-0 text-[10px]">
|
|
64
|
+
{user.emailVerified ? t('portal.dashboard.emailVerified', 'Verified') : t('portal.dashboard.emailNotVerified', 'Unverified')}
|
|
65
|
+
</Badge>
|
|
66
|
+
</span>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
<PortalCardDivider />
|
|
70
|
+
<PortalStatRow label={t('portal.dashboard.lastLogin', 'Last login')} value={formatDate(user.lastLoginAt)} />
|
|
71
|
+
<PortalCardDivider />
|
|
72
|
+
<PortalStatRow label={t('portal.dashboard.memberSince', 'Member since')} value={formatDate(user.createdAt)} />
|
|
73
|
+
{isPortalAdmin ? (
|
|
74
|
+
<>
|
|
75
|
+
<PortalCardDivider />
|
|
76
|
+
<PortalStatRow label={t('portal.dashboard.roles', 'Role')} value={<Badge className="text-[10px]">{t('portal.dashboard.portalAdmin', 'Portal Admin')}</Badge>} />
|
|
77
|
+
</>
|
|
78
|
+
) : null}
|
|
79
|
+
</div>
|
|
80
|
+
</PortalCard>
|
|
81
|
+
|
|
82
|
+
<PortalCard>
|
|
83
|
+
<PortalCardHeader label={t('portal.dashboard.roles', 'Roles')} title={`${roles.length} assigned`} />
|
|
84
|
+
{roles.length > 0 ? (
|
|
85
|
+
<div className="flex flex-wrap gap-2">
|
|
86
|
+
{roles.map((role) => (
|
|
87
|
+
<span key={role.id} className="inline-flex items-center rounded-lg border px-3 py-1.5 text-[12px] font-medium">{role.name}</span>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
<p className="text-sm text-muted-foreground">{t('portal.dashboard.noRoles', 'No roles assigned')}</p>
|
|
92
|
+
)}
|
|
93
|
+
</PortalCard>
|
|
94
|
+
|
|
95
|
+
<PortalCard className="md:col-span-2">
|
|
96
|
+
<PortalCardHeader label={t('portal.dashboard.permissions', 'Permissions')} title={`${resolvedFeatures.length} features`} />
|
|
97
|
+
{resolvedFeatures.length > 0 ? (
|
|
98
|
+
<div className="flex flex-wrap gap-2">
|
|
99
|
+
{resolvedFeatures.map((feature) => (
|
|
100
|
+
<span key={feature} className="inline-flex items-center rounded-md border bg-muted/50 px-2 py-1 font-mono text-[11px] text-muted-foreground">{feature}</span>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<p className="text-sm text-muted-foreground">{t('portal.dashboard.noPermissions', 'No permissions')}</p>
|
|
105
|
+
)}
|
|
106
|
+
</PortalCard>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageAfter('profile')} context={injectionContext} />
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
6
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
7
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
|
+
import { Notice } from '@open-mercato/ui/primitives/Notice'
|
|
9
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
10
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
|
+
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
12
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
13
|
+
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
14
|
+
|
|
15
|
+
type Props = { params: { orgSlug: string } }
|
|
16
|
+
|
|
17
|
+
export default function PortalSignupPage({ params }: Props) {
|
|
18
|
+
const t = useT()
|
|
19
|
+
const orgSlug = params.orgSlug
|
|
20
|
+
const { tenant } = usePortalContext()
|
|
21
|
+
|
|
22
|
+
const [displayName, setDisplayName] = useState('')
|
|
23
|
+
const [email, setEmail] = useState('')
|
|
24
|
+
const [password, setPassword] = useState('')
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
const [success, setSuccess] = useState(false)
|
|
27
|
+
const [submitting, setSubmitting] = useState(false)
|
|
28
|
+
|
|
29
|
+
const handleSubmit = useCallback(
|
|
30
|
+
async (event: React.FormEvent) => {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
setError(null)
|
|
33
|
+
|
|
34
|
+
if (!tenant.tenantId || !tenant.organizationId) {
|
|
35
|
+
setError(t('portal.org.invalid', 'Organization not found.'))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setSubmitting(true)
|
|
40
|
+
try {
|
|
41
|
+
const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/signup', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ email, password, displayName, tenantId: tenant.tenantId, organizationId: tenant.organizationId }),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (result.status === 201 && result.result?.ok) {
|
|
48
|
+
setSuccess(true)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))
|
|
53
|
+
} catch {
|
|
54
|
+
setError(t('portal.signup.error.generic', 'Signup failed. Please try again.'))
|
|
55
|
+
} finally {
|
|
56
|
+
setSubmitting(false)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[displayName, email, password, tenant.tenantId, tenant.organizationId, t],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const injectionContext = useMemo(
|
|
63
|
+
() => ({ orgSlug }),
|
|
64
|
+
[orgSlug],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (tenant.loading) {
|
|
68
|
+
return <div className="flex items-center justify-center py-20"><Spinner /></div>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (tenant.error) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="mx-auto w-full max-w-md py-12">
|
|
74
|
+
<Notice variant="error">{t('portal.org.invalid', 'Organization not found.')}</Notice>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (success) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="mx-auto w-full max-w-sm text-center">
|
|
82
|
+
<div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-foreground text-background">
|
|
83
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="size-6">
|
|
84
|
+
<polyline points="20 6 9 17 4 12" />
|
|
85
|
+
</svg>
|
|
86
|
+
</div>
|
|
87
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('portal.signup.success.title', 'Account Created')}</h1>
|
|
88
|
+
<p className="mt-1.5 text-sm text-muted-foreground">{t('portal.signup.success.description', 'Your account has been created. You can now sign in.')}</p>
|
|
89
|
+
<Button asChild className="mt-6 w-full rounded-lg">
|
|
90
|
+
<Link href={`/${orgSlug}/portal/login`}>{t('portal.signup.success.loginLink', 'Sign In')}</Link>
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="mx-auto w-full max-w-sm">
|
|
98
|
+
<div className="mb-8 text-center">
|
|
99
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('portal.signup.title', 'Create Account')}</h1>
|
|
100
|
+
<p className="mt-1.5 text-sm text-muted-foreground">{t('portal.signup.description', 'Sign up for a portal account.')}</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageBefore('signup')} context={injectionContext} />
|
|
104
|
+
|
|
105
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
106
|
+
{error ? <Notice variant="error">{error}</Notice> : null}
|
|
107
|
+
|
|
108
|
+
<div className="flex flex-col gap-1.5">
|
|
109
|
+
<Label htmlFor="signup-name" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.displayName', 'Full Name')}</Label>
|
|
110
|
+
<Input id="signup-name" type="text" autoComplete="name" required placeholder={t('portal.signup.displayName.placeholder', 'Jane Smith')} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="flex flex-col gap-1.5">
|
|
114
|
+
<Label htmlFor="signup-email" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.email', 'Email')}</Label>
|
|
115
|
+
<Input id="signup-email" type="email" autoComplete="email" required placeholder={t('portal.signup.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="flex flex-col gap-1.5">
|
|
119
|
+
<Label htmlFor="signup-password" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.password', 'Password')}</Label>
|
|
120
|
+
<Input id="signup-password" type="password" autoComplete="new-password" required placeholder={t('portal.signup.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<Button type="submit" disabled={submitting} className="mt-1 w-full rounded-lg">
|
|
124
|
+
{submitting ? t('portal.signup.submitting', 'Creating account...') : t('portal.signup.submit', 'Create Account')}
|
|
125
|
+
</Button>
|
|
126
|
+
|
|
127
|
+
<p className="text-center text-[13px] text-muted-foreground">
|
|
128
|
+
{t('portal.signup.hasAccount', 'Already have an account?')}{' '}
|
|
129
|
+
<Link href={`/${orgSlug}/portal/login`} className="font-medium text-foreground underline underline-offset-4 hover:opacity-80">
|
|
130
|
+
{t('portal.signup.loginLink', 'Sign in')}
|
|
131
|
+
</Link>
|
|
132
|
+
</p>
|
|
133
|
+
</form>
|
|
134
|
+
|
|
135
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageAfter('signup')} context={injectionContext} />
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|