@open-mercato/core 0.6.8-develop.6940.1.177ea30c6e → 0.6.8-develop.6942.1.dd382caa0f
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/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -2
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -2
- package/src/modules/customers/i18n/pl.json +34 -34
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
|
-
import { toAbsoluteUrl } from "@open-mercato/shared/lib/url";
|
|
3
|
+
import { getAppBaseUrl, toAbsoluteUrl } from "@open-mercato/shared/lib/url";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { createConnectedChannelRow, MailboxAlreadyConnectedError } from "../../../../../lib/connect-channel.js";
|
|
6
6
|
import { getChannelAdapter } from "../../../../../lib/adapter-registry-singleton.js";
|
|
@@ -23,7 +23,7 @@ const metadata = {
|
|
|
23
23
|
function redirectWithFlash(req, returnUrl, flash) {
|
|
24
24
|
const base = new URL(
|
|
25
25
|
normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),
|
|
26
|
-
|
|
26
|
+
getAppBaseUrl(req)
|
|
27
27
|
);
|
|
28
28
|
base.searchParams.set("flash", flash.type);
|
|
29
29
|
if (flash.code) base.searchParams.set("code", flash.code);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/communication_channels/api/get/oauth/%5Bprovider%5D/callback/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../../../../../lib/connect-channel'\nimport { getChannelAdapter } from '../../../../../lib/adapter-registry-singleton'\nimport { resolveOAuthClientCredentials } from '../../../../../lib/oauth-client-config'\nimport {\n COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n DEFAULT_OAUTH_RETURN_URL,\n normalizeOAuthReturnUrl,\n OAuthStateError,\n verifyOAuthState,\n} from '../../../../../lib/oauth-state'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'oauth-callback' })\n\nexport const metadata = {\n path: '/communication_channels/oauth/[provider]/callback',\n // No auth feature gate \u2014 the state cookie carries identity. The route still\n // verifies the session below to bind the callback to its initiator.\n GET: { requireAuth: true },\n}\n\ntype RouteContext = {\n params: Promise<{ provider: string }> | { provider: string }\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\nfunction redirectWithFlash(\n req: Request,\n returnUrl: string,\n flash: { type: 'connected' | 'error'; code?: string; provider?: string; channelId?: string },\n): Response {\n const base = new URL(\n normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),\n new URL(req.url).origin,\n )\n base.searchParams.set('flash', flash.type)\n if (flash.code) base.searchParams.set('code', flash.code)\n if (flash.provider) base.searchParams.set('provider', flash.provider)\n if (flash.channelId) base.searchParams.set('channelId', flash.channelId)\n const response = NextResponse.redirect(base.toString(), 302)\n response.cookies.set({\n name: COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n value: '',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { provider } = await context.params\n const url = new URL(req.url)\n const code = url.searchParams.get('code') ?? ''\n const stateParam = url.searchParams.get('state') ?? ''\n const error = url.searchParams.get('error')\n\n if (error) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, { type: 'error', code: error, provider })\n }\n if (!code || !stateParam) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'missing_code_or_state',\n provider,\n })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unauthorized',\n provider,\n })\n }\n\n const adapter = getChannelAdapter(provider)\n if (!adapter || typeof adapter.exchangeOAuthCode !== 'function') {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unknown_provider',\n provider,\n })\n }\n\n const cookieValue = req.headers.get('cookie') ?? ''\n const stateCookie = parseCookie(cookieValue, COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME)\n\n let statePayload\n try {\n statePayload = verifyOAuthState({\n cookie: stateCookie,\n expectedUserId: auth.sub as string,\n expectedProviderKey: provider,\n expectedState: stateParam,\n })\n } catch (err) {\n const errCode = err instanceof OAuthStateError ? err.code : 'invalid_state'\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: errCode,\n provider,\n })\n }\n\n // Defense-in-depth: the signed state is minted for a specific tenant at\n // initiate time and `verifyOAuthState` already binds it to `auth.sub`. Assert\n // the session tenant matches too, so a token/session anomaly can never create\n // a channel under a different tenant than the one that started the flow.\n if (statePayload.tenantId !== auth.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'tenant_mismatch',\n provider,\n })\n }\n\n const returnUrl = normalizeOAuthReturnUrl(statePayload.returnUrl, DEFAULT_OAUTH_RETURN_URL)\n\n // Exchange the code via the adapter.\n const container = await createRequestContainer()\n const credentialsService = (() => {\n try {\n return container.resolve('integrationCredentialsService') as CredentialsServiceLike\n } catch {\n return null\n }\n })()\n // Resolve the tenant's OAuth client app config from the `channel_<provider>`\n // integration (userId = null). Same source the `initiate` route uses; a\n // missing row means the provider was never configured, so bounce back with an\n // actionable code rather than attempting an exchange with empty credentials.\n const oauthClientCredentials = await resolveOAuthClientCredentials(credentialsService, provider, {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? null,\n })\n if (!oauthClientCredentials) {\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'oauth_client_not_configured',\n provider,\n })\n }\n\n // Must byte-for-byte match the redirect_uri sent at authorize time (the\n // `initiate` route), including behind a reverse proxy \u2014 derive it from the\n // configured app origin rather than the raw request URL.\n const redirectUri = toAbsoluteUrl(req, `/api/communication_channels/oauth/${provider}/callback`)\n\n let exchange\n try {\n exchange = await adapter.exchangeOAuthCode({\n code,\n redirectUri,\n credentials: oauthClientCredentials,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n },\n stateExtra: statePayload.extra,\n })\n } catch (err) {\n logger.warn(\n 'code exchange failed for provider',\n { provider, err },\n )\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'exchange_failed',\n provider,\n })\n }\n\n // Persist the encrypted credentials under a per-user `integration_credentials`\n // row, then create / update the per-user `CommunicationChannel`.\n const em = (container.resolve('em') as EntityManager).fork()\n // Per-user scope: pass `userId` so the credentials service writes to a\n // user-scoped row instead of overwriting the tenant-wide row. Without this,\n // two users on the same tenant share one credentials row (see review R2-C1\n // / N1, 2026-05-26).\n const credentialsScope = {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n userId: auth.sub as string,\n }\n let credentialsRefId: string | null = null\n let credentialsPersisted = false\n if (credentialsService?.save) {\n try {\n // Save under a per-provider integration id namespace; per-user scoping is\n // recorded on `IntegrationCredentials.user_id` via the `scope.userId`.\n // Arg order MUST be (integrationId, credentials, scope) \u2014 matches the real\n // CredentialsService signature; the legacy reversed order corrupted the\n // saved row by writing the scope object into the credentials field.\n await credentialsService.save(\n `channel_${provider}`,\n {\n ...exchange.credentials,\n userId: auth.sub,\n expiresAt: exchange.expiresAt ? exchange.expiresAt.toISOString() : undefined,\n },\n credentialsScope,\n )\n credentialsPersisted = true\n } catch (err) {\n logger.warn(\n 'persisting credentials failed for provider',\n { provider, err },\n )\n }\n }\n // Resolve the saved row's id so we can link `channel.credentialsRef` to it.\n // `credentialsService.save` is `void`-returning by contract, so we re-find the\n // row immediately afterwards. Best-effort \u2014 null is acceptable; the channel\n // creation below will downgrade to `requires_reauth` so workers don't poll\n // a credential-less channel.\n if (credentialsPersisted) {\n try {\n const { IntegrationCredentials } = await import('@open-mercato/core/modules/integrations/data/entities')\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId: `channel_${provider}`,\n tenantId: credentialsScope.tenantId,\n organizationId: credentialsScope.organizationId,\n userId: credentialsScope.userId,\n deletedAt: null,\n },\n undefined,\n credentialsScope,\n )\n credentialsRefId = (row as { id?: string } | null)?.id ?? null\n } catch {\n credentialsRefId = null\n }\n }\n\n const displayName =\n exchange.displayName ?? exchange.externalIdentifier ?? `${provider} channel`\n // Fail-safe: if credentials persistence failed (no `credentialsRef` available),\n // create the channel in `requires_reauth` so workers don't poll a channel that\n // has no usable credentials. The user can re-run the OAuth flow to recover\n // (see review R2-H4 / F6, 2026-05-26).\n const credentialsAvailable = credentialsRefId !== null\n let channel\n try {\n channel = await createConnectedChannelRow({\n em,\n adapter,\n providerKey: provider,\n displayName,\n externalIdentifier: exchange.externalIdentifier ?? null,\n credentialsRefId,\n userId: auth.sub as string,\n scope: { tenantId: statePayload.tenantId, organizationId: statePayload.organizationId ?? null },\n })\n } catch (err) {\n // Same mailbox already connected via another provider \u2014 don't create a second\n // channel that double-ingests every message; send the user back with a flash.\n if (err instanceof MailboxAlreadyConnectedError) {\n logger.warn('mailbox already connected via another provider', { existingProviderKey: err.existingProviderKey })\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'mailbox_already_connected',\n provider,\n })\n }\n throw err\n }\n\n // Spec C \u00A7 Phase C5 \u2014 best-effort push registration for OAuth providers\n // that support it (Gmail). Same shape as the credential-connect\n // path: failures persist as `pushStatus='failed'` on channelState and DO\n // NOT fail the connect \u2014 polling fallback covers until the operator clicks\n // \"Re-register push\" on the channels page.\n const adapterSupportsPush =\n typeof adapter.registerPush === 'function' && typeof adapter.unregisterPush === 'function'\n if (\n credentialsAvailable &&\n adapterSupportsPush &&\n statePayload.organizationId &&\n provider === 'gmail'\n ) {\n try {\n const { pushRegister } = await import('../../../../../commands/push-register')\n await pushRegister({\n container,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId,\n userId: auth.sub as string,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n logger.warn(\n 'best-effort pushRegister failed for channel',\n { channelId: channel.id, err },\n )\n }\n }\n\n return redirectWithFlash(req, returnUrl, {\n type: 'connected',\n provider,\n channelId: channel.id,\n })\n}\n\nfunction parseCookie(header: string, name: string): string | null {\n if (!header) return null\n const segments = header.split(';')\n for (const segment of segments) {\n const trimmed = segment.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return decodeURIComponent(trimmed.slice(name.length + 1))\n }\n }\n return null\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: 'OAuth callback \u2014 exchange code, persist credentials, create per-user channel',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 302, description: 'Redirect back to returnUrl with flash query params' },\n ],\n },\n },\n}\nexport default GET\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getAppBaseUrl, toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../../../../../lib/connect-channel'\nimport { getChannelAdapter } from '../../../../../lib/adapter-registry-singleton'\nimport { resolveOAuthClientCredentials } from '../../../../../lib/oauth-client-config'\nimport {\n COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n DEFAULT_OAUTH_RETURN_URL,\n normalizeOAuthReturnUrl,\n OAuthStateError,\n verifyOAuthState,\n} from '../../../../../lib/oauth-state'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'oauth-callback' })\n\nexport const metadata = {\n path: '/communication_channels/oauth/[provider]/callback',\n // No auth feature gate \u2014 the state cookie carries identity. The route still\n // verifies the session below to bind the callback to its initiator.\n GET: { requireAuth: true },\n}\n\ntype RouteContext = {\n params: Promise<{ provider: string }> | { provider: string }\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\nfunction redirectWithFlash(\n req: Request,\n returnUrl: string,\n flash: { type: 'connected' | 'error'; code?: string; provider?: string; channelId?: string },\n): Response {\n const base = new URL(\n normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),\n getAppBaseUrl(req),\n )\n base.searchParams.set('flash', flash.type)\n if (flash.code) base.searchParams.set('code', flash.code)\n if (flash.provider) base.searchParams.set('provider', flash.provider)\n if (flash.channelId) base.searchParams.set('channelId', flash.channelId)\n const response = NextResponse.redirect(base.toString(), 302)\n response.cookies.set({\n name: COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,\n value: '',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { provider } = await context.params\n const url = new URL(req.url)\n const code = url.searchParams.get('code') ?? ''\n const stateParam = url.searchParams.get('state') ?? ''\n const error = url.searchParams.get('error')\n\n if (error) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, { type: 'error', code: error, provider })\n }\n if (!code || !stateParam) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'missing_code_or_state',\n provider,\n })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unauthorized',\n provider,\n })\n }\n\n const adapter = getChannelAdapter(provider)\n if (!adapter || typeof adapter.exchangeOAuthCode !== 'function') {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'unknown_provider',\n provider,\n })\n }\n\n const cookieValue = req.headers.get('cookie') ?? ''\n const stateCookie = parseCookie(cookieValue, COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME)\n\n let statePayload\n try {\n statePayload = verifyOAuthState({\n cookie: stateCookie,\n expectedUserId: auth.sub as string,\n expectedProviderKey: provider,\n expectedState: stateParam,\n })\n } catch (err) {\n const errCode = err instanceof OAuthStateError ? err.code : 'invalid_state'\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: errCode,\n provider,\n })\n }\n\n // Defense-in-depth: the signed state is minted for a specific tenant at\n // initiate time and `verifyOAuthState` already binds it to `auth.sub`. Assert\n // the session tenant matches too, so a token/session anomaly can never create\n // a channel under a different tenant than the one that started the flow.\n if (statePayload.tenantId !== auth.tenantId) {\n return redirectWithFlash(req, DEFAULT_OAUTH_RETURN_URL, {\n type: 'error',\n code: 'tenant_mismatch',\n provider,\n })\n }\n\n const returnUrl = normalizeOAuthReturnUrl(statePayload.returnUrl, DEFAULT_OAUTH_RETURN_URL)\n\n // Exchange the code via the adapter.\n const container = await createRequestContainer()\n const credentialsService = (() => {\n try {\n return container.resolve('integrationCredentialsService') as CredentialsServiceLike\n } catch {\n return null\n }\n })()\n // Resolve the tenant's OAuth client app config from the `channel_<provider>`\n // integration (userId = null). Same source the `initiate` route uses; a\n // missing row means the provider was never configured, so bounce back with an\n // actionable code rather than attempting an exchange with empty credentials.\n const oauthClientCredentials = await resolveOAuthClientCredentials(credentialsService, provider, {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? null,\n })\n if (!oauthClientCredentials) {\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'oauth_client_not_configured',\n provider,\n })\n }\n\n // Must byte-for-byte match the redirect_uri sent at authorize time (the\n // `initiate` route), including behind a reverse proxy \u2014 derive it from the\n // configured app origin rather than the raw request URL.\n const redirectUri = toAbsoluteUrl(req, `/api/communication_channels/oauth/${provider}/callback`)\n\n let exchange\n try {\n exchange = await adapter.exchangeOAuthCode({\n code,\n redirectUri,\n credentials: oauthClientCredentials,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n },\n stateExtra: statePayload.extra,\n })\n } catch (err) {\n logger.warn(\n 'code exchange failed for provider',\n { provider, err },\n )\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'exchange_failed',\n provider,\n })\n }\n\n // Persist the encrypted credentials under a per-user `integration_credentials`\n // row, then create / update the per-user `CommunicationChannel`.\n const em = (container.resolve('em') as EntityManager).fork()\n // Per-user scope: pass `userId` so the credentials service writes to a\n // user-scoped row instead of overwriting the tenant-wide row. Without this,\n // two users on the same tenant share one credentials row (see review R2-C1\n // / N1, 2026-05-26).\n const credentialsScope = {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId ?? statePayload.tenantId,\n userId: auth.sub as string,\n }\n let credentialsRefId: string | null = null\n let credentialsPersisted = false\n if (credentialsService?.save) {\n try {\n // Save under a per-provider integration id namespace; per-user scoping is\n // recorded on `IntegrationCredentials.user_id` via the `scope.userId`.\n // Arg order MUST be (integrationId, credentials, scope) \u2014 matches the real\n // CredentialsService signature; the legacy reversed order corrupted the\n // saved row by writing the scope object into the credentials field.\n await credentialsService.save(\n `channel_${provider}`,\n {\n ...exchange.credentials,\n userId: auth.sub,\n expiresAt: exchange.expiresAt ? exchange.expiresAt.toISOString() : undefined,\n },\n credentialsScope,\n )\n credentialsPersisted = true\n } catch (err) {\n logger.warn(\n 'persisting credentials failed for provider',\n { provider, err },\n )\n }\n }\n // Resolve the saved row's id so we can link `channel.credentialsRef` to it.\n // `credentialsService.save` is `void`-returning by contract, so we re-find the\n // row immediately afterwards. Best-effort \u2014 null is acceptable; the channel\n // creation below will downgrade to `requires_reauth` so workers don't poll\n // a credential-less channel.\n if (credentialsPersisted) {\n try {\n const { IntegrationCredentials } = await import('@open-mercato/core/modules/integrations/data/entities')\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId: `channel_${provider}`,\n tenantId: credentialsScope.tenantId,\n organizationId: credentialsScope.organizationId,\n userId: credentialsScope.userId,\n deletedAt: null,\n },\n undefined,\n credentialsScope,\n )\n credentialsRefId = (row as { id?: string } | null)?.id ?? null\n } catch {\n credentialsRefId = null\n }\n }\n\n const displayName =\n exchange.displayName ?? exchange.externalIdentifier ?? `${provider} channel`\n // Fail-safe: if credentials persistence failed (no `credentialsRef` available),\n // create the channel in `requires_reauth` so workers don't poll a channel that\n // has no usable credentials. The user can re-run the OAuth flow to recover\n // (see review R2-H4 / F6, 2026-05-26).\n const credentialsAvailable = credentialsRefId !== null\n let channel\n try {\n channel = await createConnectedChannelRow({\n em,\n adapter,\n providerKey: provider,\n displayName,\n externalIdentifier: exchange.externalIdentifier ?? null,\n credentialsRefId,\n userId: auth.sub as string,\n scope: { tenantId: statePayload.tenantId, organizationId: statePayload.organizationId ?? null },\n })\n } catch (err) {\n // Same mailbox already connected via another provider \u2014 don't create a second\n // channel that double-ingests every message; send the user back with a flash.\n if (err instanceof MailboxAlreadyConnectedError) {\n logger.warn('mailbox already connected via another provider', { existingProviderKey: err.existingProviderKey })\n return redirectWithFlash(req, returnUrl, {\n type: 'error',\n code: 'mailbox_already_connected',\n provider,\n })\n }\n throw err\n }\n\n // Spec C \u00A7 Phase C5 \u2014 best-effort push registration for OAuth providers\n // that support it (Gmail). Same shape as the credential-connect\n // path: failures persist as `pushStatus='failed'` on channelState and DO\n // NOT fail the connect \u2014 polling fallback covers until the operator clicks\n // \"Re-register push\" on the channels page.\n const adapterSupportsPush =\n typeof adapter.registerPush === 'function' && typeof adapter.unregisterPush === 'function'\n if (\n credentialsAvailable &&\n adapterSupportsPush &&\n statePayload.organizationId &&\n provider === 'gmail'\n ) {\n try {\n const { pushRegister } = await import('../../../../../commands/push-register')\n await pushRegister({\n container,\n scope: {\n tenantId: statePayload.tenantId,\n organizationId: statePayload.organizationId,\n userId: auth.sub as string,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n logger.warn(\n 'best-effort pushRegister failed for channel',\n { channelId: channel.id, err },\n )\n }\n }\n\n return redirectWithFlash(req, returnUrl, {\n type: 'connected',\n provider,\n channelId: channel.id,\n })\n}\n\nfunction parseCookie(header: string, name: string): string | null {\n if (!header) return null\n const segments = header.split(';')\n for (const segment of segments) {\n const trimmed = segment.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return decodeURIComponent(trimmed.slice(name.length + 1))\n }\n }\n return null\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: 'OAuth callback \u2014 exchange code, persist credentials, create per-user channel',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 302, description: 'Redirect back to returnUrl with flash query params' },\n ],\n },\n },\n}\nexport default GET\n"],
|
|
5
|
+
"mappings": "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",
|
|
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.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6942.1.dd382caa0f",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6942.1.dd382caa0f",
|
|
258
|
+
"@open-mercato/shared": "0.6.8-develop.6942.1.dd382caa0f",
|
|
259
|
+
"@open-mercato/ui": "0.6.8-develop.6942.1.dd382caa0f",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6942.1.dd382caa0f",
|
|
265
|
+
"@open-mercato/shared": "0.6.8-develop.6942.1.dd382caa0f",
|
|
266
|
+
"@open-mercato/ui": "0.6.8-develop.6942.1.dd382caa0f",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^7.0.0",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
|
-
import { toAbsoluteUrl } from '@open-mercato/shared/lib/url'
|
|
3
|
+
import { getAppBaseUrl, toAbsoluteUrl } from '@open-mercato/shared/lib/url'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
6
|
import { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../../../../../lib/connect-channel'
|
|
@@ -47,7 +47,7 @@ function redirectWithFlash(
|
|
|
47
47
|
): Response {
|
|
48
48
|
const base = new URL(
|
|
49
49
|
normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),
|
|
50
|
-
|
|
50
|
+
getAppBaseUrl(req),
|
|
51
51
|
)
|
|
52
52
|
base.searchParams.set('flash', flash.type)
|
|
53
53
|
if (flash.code) base.searchParams.set('code', flash.code)
|
|
@@ -460,13 +460,13 @@
|
|
|
460
460
|
"customers.changelog.title": "Dziennik zmian",
|
|
461
461
|
"customers.changelog.user.system": "System",
|
|
462
462
|
"customers.companies.create.title": "Utwórz firmę",
|
|
463
|
-
"customers.companies.dashboard.activeDeal": "
|
|
463
|
+
"customers.companies.dashboard.activeDeal": "Aktywna szansa",
|
|
464
464
|
"customers.companies.dashboard.created": "Utworzono",
|
|
465
465
|
"customers.companies.dashboard.details": "Szczegóły",
|
|
466
466
|
"customers.companies.dashboard.dueOn": "Termin: {{date}}",
|
|
467
467
|
"customers.companies.dashboard.hiddenTiles": "Ukryte kafelki: {{count}}",
|
|
468
468
|
"customers.companies.dashboard.hideTile": "Ukryj kafelek",
|
|
469
|
-
"customers.companies.dashboard.kpi.activeDeals": "AKTYWNE
|
|
469
|
+
"customers.companies.dashboard.kpi.activeDeals": "AKTYWNE SZANSE",
|
|
470
470
|
"customers.companies.dashboard.kpi.activities": "AKTYWNOŚCI",
|
|
471
471
|
"customers.companies.dashboard.kpi.clientSince": "KLIENT OD",
|
|
472
472
|
"customers.companies.dashboard.kpi.completedDeals": "zakończonych szans",
|
|
@@ -507,7 +507,7 @@
|
|
|
507
507
|
"customers.companies.detail.actions.more": "Więcej",
|
|
508
508
|
"customers.companies.detail.actions.save": "Zapisz",
|
|
509
509
|
"customers.companies.detail.actions.sendMessage": "Wyślij wiadomość",
|
|
510
|
-
"customers.companies.detail.activeDeal": "
|
|
510
|
+
"customers.companies.detail.activeDeal": "AKTYWNA SZANSA",
|
|
511
511
|
"customers.companies.detail.activities.add": "Zaloguj aktywność",
|
|
512
512
|
"customers.companies.detail.activities.loading": "Ładowanie aktywności…",
|
|
513
513
|
"customers.companies.detail.addresses.add": "Dodaj adres",
|
|
@@ -973,8 +973,8 @@
|
|
|
973
973
|
"customers.deals.detail.badge.deal": "Szansa",
|
|
974
974
|
"customers.deals.detail.badge.lost": "Przegrana",
|
|
975
975
|
"customers.deals.detail.badge.won": "Wygrana",
|
|
976
|
-
"customers.deals.detail.closeLostError": "Nie udało się oznaczyć
|
|
977
|
-
"customers.deals.detail.closeWonError": "Nie udało się oznaczyć
|
|
976
|
+
"customers.deals.detail.closeLostError": "Nie udało się oznaczyć szansy jako przegranej.",
|
|
977
|
+
"customers.deals.detail.closeWonError": "Nie udało się oznaczyć szansy jako wygranej.",
|
|
978
978
|
"customers.deals.detail.closure.lost": "Przegrana",
|
|
979
979
|
"customers.deals.detail.closure.prompt": "Zamknij szansę — wybierz wynik",
|
|
980
980
|
"customers.deals.detail.closure.won": "Wygrana",
|
|
@@ -1088,10 +1088,10 @@
|
|
|
1088
1088
|
"customers.deals.detail.pipeline.stageOf": "Etap {{current}} z {{total}}",
|
|
1089
1089
|
"customers.deals.detail.saveError": "Nie udało się zaktualizować szansy.",
|
|
1090
1090
|
"customers.deals.detail.saveSuccess": "Szansa została zaktualizowana.",
|
|
1091
|
-
"customers.deals.detail.stageMenu.description": "Przesuń
|
|
1091
|
+
"customers.deals.detail.stageMenu.description": "Przesuń szansę dalej bez otwierania formularza.",
|
|
1092
1092
|
"customers.deals.detail.stageMenu.title": "Zmień etap pipeline",
|
|
1093
|
-
"customers.deals.detail.stageUpdateError": "Nie udało się zmienić etapu
|
|
1094
|
-
"customers.deals.detail.stageUpdateSuccess": "Etap
|
|
1093
|
+
"customers.deals.detail.stageUpdateError": "Nie udało się zmienić etapu szansy.",
|
|
1094
|
+
"customers.deals.detail.stageUpdateSuccess": "Etap szansy został zaktualizowany.",
|
|
1095
1095
|
"customers.deals.detail.summary": "{{status}} · {{pipeline}}",
|
|
1096
1096
|
"customers.deals.detail.tabs.activities": "Aktywności",
|
|
1097
1097
|
"customers.deals.detail.tabs.changelog": "Historia zmian",
|
|
@@ -1246,12 +1246,12 @@
|
|
|
1246
1246
|
"customers.deals.kanban.card.action.email": "Wyślij e-mail",
|
|
1247
1247
|
"customers.deals.kanban.card.action.note": "Notatka",
|
|
1248
1248
|
"customers.deals.kanban.card.aria": "Szansa: {title}",
|
|
1249
|
-
"customers.deals.kanban.card.aria.menu": "Akcje
|
|
1249
|
+
"customers.deals.kanban.card.aria.menu": "Akcje szansy",
|
|
1250
1250
|
"customers.deals.kanban.card.aria.moreCompanies": "{count} dodatkowych firm",
|
|
1251
1251
|
"customers.deals.kanban.card.aria.openActivities": "{count} otwartych aktywności",
|
|
1252
1252
|
"customers.deals.kanban.card.aria.probability": "Prawdopodobieństwo: {value}",
|
|
1253
|
-
"customers.deals.kanban.card.aria.roledescription": "karta
|
|
1254
|
-
"customers.deals.kanban.card.aria.select": "Zaznacz
|
|
1253
|
+
"customers.deals.kanban.card.aria.roledescription": "karta szansy",
|
|
1254
|
+
"customers.deals.kanban.card.aria.select": "Zaznacz szansę",
|
|
1255
1255
|
"customers.deals.kanban.card.daysInStage": "od {days} dni",
|
|
1256
1256
|
"customers.deals.kanban.card.noValue": "Brak wartości",
|
|
1257
1257
|
"customers.deals.kanban.card.overdueLabel": "Po terminie",
|
|
@@ -1259,21 +1259,21 @@
|
|
|
1259
1259
|
"customers.deals.kanban.card.statusStuck": "UTKNĘŁA",
|
|
1260
1260
|
"customers.deals.kanban.comingSoon": "{feature} pojawi się w kolejnej wersji.",
|
|
1261
1261
|
"customers.deals.kanban.cta.customize": "Dostosuj widok",
|
|
1262
|
-
"customers.deals.kanban.cta.newDeal": "
|
|
1262
|
+
"customers.deals.kanban.cta.newDeal": "Nowa szansa",
|
|
1263
1263
|
"customers.deals.kanban.cta.newStage": "Nowy etap",
|
|
1264
|
-
"customers.deals.kanban.cta.quickDeal": "
|
|
1264
|
+
"customers.deals.kanban.cta.quickDeal": "Szybka szansa",
|
|
1265
1265
|
"customers.deals.kanban.currencyBreakdown.basePill": "BAZOWA",
|
|
1266
1266
|
"customers.deals.kanban.currencyBreakdown.colCurrency": "Waluta",
|
|
1267
|
-
"customers.deals.kanban.currencyBreakdown.colDeals": "
|
|
1267
|
+
"customers.deals.kanban.currencyBreakdown.colDeals": "Szanse",
|
|
1268
1268
|
"customers.deals.kanban.currencyBreakdown.colNativeTotal": "Natywna suma",
|
|
1269
1269
|
"customers.deals.kanban.currencyBreakdown.footerCredit": "Kurs średni NBP · {date}",
|
|
1270
|
-
"customers.deals.kanban.currencyBreakdown.headingCount": "({count}
|
|
1270
|
+
"customers.deals.kanban.currencyBreakdown.headingCount": "({count} szans)",
|
|
1271
1271
|
"customers.deals.kanban.currencyBreakdown.title": "Podział wartości",
|
|
1272
1272
|
"customers.deals.kanban.currencyFilter.allCurrencies": "Wszystkie waluty",
|
|
1273
1273
|
"customers.deals.kanban.currencyFilter.apply": "Zastosuj filtr",
|
|
1274
1274
|
"customers.deals.kanban.currencyFilter.chipLabel": "Waluta",
|
|
1275
1275
|
"customers.deals.kanban.currencyFilter.footerCredit": "Kurs średni NBP · {date}",
|
|
1276
|
-
"customers.deals.kanban.currencyFilter.subtitle": "{label} · {count}
|
|
1276
|
+
"customers.deals.kanban.currencyFilter.subtitle": "{label} · {count} szans · {total} {currency}",
|
|
1277
1277
|
"customers.deals.kanban.currencyFilter.title": "Filtruj kanban według waluty",
|
|
1278
1278
|
"customers.deals.kanban.customize.actions": "AKCJE",
|
|
1279
1279
|
"customers.deals.kanban.customize.configCols": "Konfiguruj kolumny",
|
|
@@ -1358,7 +1358,7 @@
|
|
|
1358
1358
|
"customers.deals.kanban.menu.markWon": "Oznacz jako Won",
|
|
1359
1359
|
"customers.deals.kanban.menu.markWon.success": "Oznaczono szansę jako wygraną.",
|
|
1360
1360
|
"customers.deals.kanban.menu.moveStage": "Przenieś etap…",
|
|
1361
|
-
"customers.deals.kanban.menu.open": "Otwórz
|
|
1361
|
+
"customers.deals.kanban.menu.open": "Otwórz szansę",
|
|
1362
1362
|
"customers.deals.kanban.pageTitle": "Szanse",
|
|
1363
1363
|
"customers.deals.kanban.quickDeal.cancel": "Anuluj",
|
|
1364
1364
|
"customers.deals.kanban.quickDeal.company": "Firma",
|
|
@@ -1376,14 +1376,14 @@
|
|
|
1376
1376
|
"customers.deals.kanban.quickDeal.status.loose": "Wstrzymany",
|
|
1377
1377
|
"customers.deals.kanban.quickDeal.status.open": "Otwarty",
|
|
1378
1378
|
"customers.deals.kanban.quickDeal.status.win": "Wygrany",
|
|
1379
|
-
"customers.deals.kanban.quickDeal.submit": "+ Dodaj
|
|
1379
|
+
"customers.deals.kanban.quickDeal.submit": "+ Dodaj szansę",
|
|
1380
1380
|
"customers.deals.kanban.quickDeal.success": "Dodano szansę na etapie {stage}.",
|
|
1381
|
-
"customers.deals.kanban.quickDeal.title": "
|
|
1382
|
-
"customers.deals.kanban.quickDeal.title.label": "Tytuł
|
|
1381
|
+
"customers.deals.kanban.quickDeal.title": "Szybka szansa",
|
|
1382
|
+
"customers.deals.kanban.quickDeal.title.label": "Tytuł szansy",
|
|
1383
1383
|
"customers.deals.kanban.quickDeal.title.placeholder": "np. Ekspansja Q3 — pakiet oświetlenia",
|
|
1384
1384
|
"customers.deals.kanban.quickDeal.title.required": "Tytuł jest wymagany.",
|
|
1385
1385
|
"customers.deals.kanban.quickDeal.value": "Wartość",
|
|
1386
|
-
"customers.deals.kanban.search.placeholder": "Szukaj
|
|
1386
|
+
"customers.deals.kanban.search.placeholder": "Szukaj szans…",
|
|
1387
1387
|
"customers.deals.kanban.sort.default": "Domyślnie",
|
|
1388
1388
|
"customers.deals.kanban.sort.defaultHint": "Domyślnie: {label}",
|
|
1389
1389
|
"customers.deals.kanban.sort.label": "Sortuj",
|
|
@@ -1485,25 +1485,25 @@
|
|
|
1485
1485
|
"customers.deals.list.unknownOwner": "Nieznany właściciel",
|
|
1486
1486
|
"customers.deals.list.unnamedCompany": "Firma bez nazwy",
|
|
1487
1487
|
"customers.deals.list.unnamedPerson": "Osoba bez nazwy",
|
|
1488
|
-
"customers.deals.map.canvas.label": "Mapa
|
|
1488
|
+
"customers.deals.map.canvas.label": "Mapa szans",
|
|
1489
1489
|
"customers.deals.map.canvas.loading": "Ładowanie mapy…",
|
|
1490
1490
|
"customers.deals.map.legend.title": "Etapy",
|
|
1491
|
-
"customers.deals.map.loadError": "Nie udało się załadować
|
|
1491
|
+
"customers.deals.map.loadError": "Nie udało się załadować szans na mapę.",
|
|
1492
1492
|
"customers.deals.map.panel.count": "{count} z lokalizacją",
|
|
1493
|
-
"customers.deals.map.panel.empty.description": "Dodaj szerokość i długość geograficzną do adresów firm lub osób, aby ich
|
|
1494
|
-
"customers.deals.map.panel.empty.title": "Brak
|
|
1495
|
-
"customers.deals.map.panel.hint": "Kliknij
|
|
1493
|
+
"customers.deals.map.panel.empty.description": "Dodaj szerokość i długość geograficzną do adresów firm lub osób, aby ich szanse pojawiły się na mapie.",
|
|
1494
|
+
"customers.deals.map.panel.empty.title": "Brak szans na mapie",
|
|
1495
|
+
"customers.deals.map.panel.hint": "Kliknij szansę lub pinezkę na mapie, aby zobaczyć podgląd.",
|
|
1496
1496
|
"customers.deals.map.panel.noAddress": "Brak współrzędnych adresu",
|
|
1497
|
-
"customers.deals.map.panel.sort.label": "Sortowanie panelu
|
|
1497
|
+
"customers.deals.map.panel.sort.label": "Sortowanie panelu szans",
|
|
1498
1498
|
"customers.deals.map.panel.sort.listOrder": "Kolejność listy",
|
|
1499
1499
|
"customers.deals.map.panel.sort.proximity": "Najbliższe najpierw",
|
|
1500
|
-
"customers.deals.map.panel.title": "
|
|
1500
|
+
"customers.deals.map.panel.title": "Szanse z lokalizacją",
|
|
1501
1501
|
"customers.deals.map.preview.close": "Zamknij podgląd",
|
|
1502
1502
|
"customers.deals.map.preview.closeShort": "Zamknięcie {date}",
|
|
1503
|
-
"customers.deals.map.preview.openDeal": "Otwórz
|
|
1503
|
+
"customers.deals.map.preview.openDeal": "Otwórz szansę",
|
|
1504
1504
|
"customers.deals.map.preview.probabilityShort": "{value}%",
|
|
1505
1505
|
"customers.deals.map.retry": "Spróbuj ponownie",
|
|
1506
|
-
"customers.deals.map.truncated": "Wyświetlono pierwsze {count} z {total} zlokalizowanych
|
|
1506
|
+
"customers.deals.map.truncated": "Wyświetlono pierwsze {count} z {total} zlokalizowanych szans — zawęź filtry, aby ograniczyć mapę.",
|
|
1507
1507
|
"customers.deals.pipeline.actions.openDeal": "Otwórz szansę",
|
|
1508
1508
|
"customers.deals.pipeline.card.expectedClose": "Planowane zamknięcie",
|
|
1509
1509
|
"customers.deals.pipeline.card.probability": "Prawdopodobieństwo",
|
|
@@ -1619,7 +1619,7 @@
|
|
|
1619
1619
|
"customers.errors.person_not_found": "Nie znaleziono osoby",
|
|
1620
1620
|
"customers.errors.person_profile_not_found": "Nie znaleziono profilu osoby",
|
|
1621
1621
|
"customers.errors.person_required": "Wymagany identyfikator osoby",
|
|
1622
|
-
"customers.errors.primaryPersonMustBeLinked": "Główna osoba musi być powiązana z
|
|
1622
|
+
"customers.errors.primaryPersonMustBeLinked": "Główna osoba musi być powiązana z szansą",
|
|
1623
1623
|
"customers.errors.profile_must_be_object": "profil musi być obiektem",
|
|
1624
1624
|
"customers.errors.profile_unsupported_field": "Nieobsługiwane pole profilu: {{field}}",
|
|
1625
1625
|
"customers.errors.role_not_found": "Nie znaleziono roli",
|
|
@@ -2384,7 +2384,7 @@
|
|
|
2384
2384
|
"customers.personTags.activeCount": "Wybrane: {{count}}",
|
|
2385
2385
|
"customers.personTags.cancel": "Anuluj",
|
|
2386
2386
|
"customers.personTags.category.activity-types": "Aktywność",
|
|
2387
|
-
"customers.personTags.category.deal-statuses": "Status
|
|
2387
|
+
"customers.personTags.category.deal-statuses": "Status szansy",
|
|
2388
2388
|
"customers.personTags.category.industries": "Branża",
|
|
2389
2389
|
"customers.personTags.category.job-titles": "Stanowisko",
|
|
2390
2390
|
"customers.personTags.category.labels": "Etykiety",
|
|
@@ -2702,11 +2702,11 @@
|
|
|
2702
2702
|
"customers.tags.manage.customCategoryNoteTitle": "Niestandardowa kategoria",
|
|
2703
2703
|
"customers.tags.manage.defaultEntry": "domyślny przy tworzeniu nowych rekordów",
|
|
2704
2704
|
"customers.tags.manage.delete": "Usuń",
|
|
2705
|
-
"customers.tags.manage.description.customers.custom_tags": "Opcjonalne tagi specyficzne dla tenanta, dla osób, firm i powiązanych
|
|
2705
|
+
"customers.tags.manage.description.customers.custom_tags": "Opcjonalne tagi specyficzne dla tenanta, dla osób, firm i powiązanych szans.",
|
|
2706
2706
|
"customers.tags.manage.description.customers.lifecycle_stage": "Wartości cyklu życia wyrównane z pipeline, wspólne dla stron szczegółów CRM.",
|
|
2707
2707
|
"customers.tags.manage.description.customers.renewal_quarter": "Pomocniki kwartałów odnowień wyświetlane na kartach szczegółów CRM i podsumowaniach pipeline.",
|
|
2708
2708
|
"customers.tags.manage.description.customers.source": "Etykiety źródeł pozyskania używane w formularzach Zone 1 i podsumowaniach CRM.",
|
|
2709
|
-
"customers.tags.manage.description.customers.status": "Pojedynczy wybór · widoczne w hero karty firmy/osoby/
|
|
2709
|
+
"customers.tags.manage.description.customers.status": "Pojedynczy wybór · widoczne w hero karty firmy/osoby/szansy",
|
|
2710
2710
|
"customers.tags.manage.description.customers.temperature": "Wskaźniki gorący, ciepły i zimny używane w nagłówkach osób i firm.",
|
|
2711
2711
|
"customers.tags.manage.dragHandle": "Przeciągnij, aby zmienić kolejność",
|
|
2712
2712
|
"customers.tags.manage.entryNew": "Nowa wartość słownikowa",
|