@open-mercato/shared 0.4.2-canary-e6bf6a353e → 0.4.2-canary-49d47ff90e
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/lib/api/context.js +18 -0
- package/dist/lib/api/context.js.map +7 -0
- package/dist/lib/auth/passwordPolicy.js +111 -0
- package/dist/lib/auth/passwordPolicy.js.map +7 -0
- package/dist/lib/auth/server.js +3 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/email/send.js +9 -2
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/frontend/notificationEvents.js +48 -0
- package/dist/lib/frontend/notificationEvents.js.map +7 -0
- package/dist/lib/lib/email/send.js +2 -2
- package/dist/lib/lib/email/send.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/notifications/index.js +2 -0
- package/dist/modules/notifications/index.js.map +7 -0
- package/dist/modules/notifications/types.js +1 -0
- package/dist/modules/notifications/types.js.map +7 -0
- package/package.json +1 -1
- package/src/lib/api/context.ts +36 -0
- package/src/lib/auth/passwordPolicy.ts +150 -0
- package/src/lib/auth/server.ts +5 -0
- package/src/lib/email/__tests__/send.test.ts +60 -0
- package/src/lib/email/send.ts +10 -3
- package/src/lib/frontend/notificationEvents.ts +48 -0
- package/src/lib/lib/email/send.ts +3 -3
- package/src/modules/notifications/index.ts +1 -0
- package/src/modules/notifications/types.ts +106 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequestContainer } from "../di/container.js";
|
|
2
|
+
import { getAuthFromRequest } from "../auth/server.js";
|
|
3
|
+
import { resolveTranslations } from "../i18n/server.js";
|
|
4
|
+
async function resolveRequestContext(req) {
|
|
5
|
+
const container = await createRequestContainer();
|
|
6
|
+
const auth = await getAuthFromRequest(req);
|
|
7
|
+
const { translate } = await resolveTranslations();
|
|
8
|
+
const ctx = {
|
|
9
|
+
container,
|
|
10
|
+
auth,
|
|
11
|
+
translate
|
|
12
|
+
};
|
|
13
|
+
return { ctx };
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
resolveRequestContext
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/api/context.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { createRequestContainer } from '../di/container'\nimport { getAuthFromRequest, type AuthContext } from '../auth/server'\nimport { resolveTranslations } from '../i18n/server'\n\nexport type RequestContext = {\n container: AwilixContainer\n auth: AuthContext\n organizationScope?: unknown\n selectedOrganizationId?: string | null\n organizationIds?: string[] | null\n translate: (key: string, fallback?: string) => string\n}\n\nexport type ResolveRequestContextResult = {\n ctx: RequestContext\n}\n\n/**\n * Resolves the request context for API routes.\n * This includes container, auth, and translations.\n * For organization-scoped routes, use resolveOrganizationScopeForRequest separately.\n */\nexport async function resolveRequestContext(req: Request): Promise<ResolveRequestContextResult> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n const ctx: RequestContext = {\n container,\n auth,\n translate,\n }\n\n return { ctx }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,0BAA4C;AACrD,SAAS,2BAA2B;AAoBpC,eAAsB,sBAAsB,KAAoD;AAC9F,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAEhD,QAAM,MAAsB;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,EAAE,IAAI;AACf;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
3
|
+
const DEFAULT_POLICY = {
|
|
4
|
+
minLength: 6,
|
|
5
|
+
requireDigit: true,
|
|
6
|
+
requireUppercase: true,
|
|
7
|
+
requireSpecial: true
|
|
8
|
+
};
|
|
9
|
+
const ENV_KEYS = {
|
|
10
|
+
minLength: "OM_PASSWORD_MIN_LENGTH",
|
|
11
|
+
requireDigit: "OM_PASSWORD_REQUIRE_DIGIT",
|
|
12
|
+
requireUppercase: "OM_PASSWORD_REQUIRE_UPPERCASE",
|
|
13
|
+
requireSpecial: "OM_PASSWORD_REQUIRE_SPECIAL"
|
|
14
|
+
};
|
|
15
|
+
const PUBLIC_PREFIX = "NEXT_PUBLIC_";
|
|
16
|
+
function readEnvValue(env, key) {
|
|
17
|
+
const rawKey = ENV_KEYS[key];
|
|
18
|
+
const publicKey = `${PUBLIC_PREFIX}${rawKey}`;
|
|
19
|
+
const rawValue = env[rawKey];
|
|
20
|
+
if (typeof rawValue === "string" && rawValue.trim().length > 0) return rawValue;
|
|
21
|
+
const publicValue = env[publicKey];
|
|
22
|
+
if (typeof publicValue === "string" && publicValue.trim().length > 0) return publicValue;
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function parsePositiveInt(raw, fallback, min = 1) {
|
|
26
|
+
if (!raw) return fallback;
|
|
27
|
+
const parsed = Number.parseInt(raw, 10);
|
|
28
|
+
if (Number.isNaN(parsed)) return fallback;
|
|
29
|
+
return Math.max(min, parsed);
|
|
30
|
+
}
|
|
31
|
+
function getPasswordPolicy(env = process.env) {
|
|
32
|
+
const minLength = parsePositiveInt(
|
|
33
|
+
readEnvValue(env, "minLength"),
|
|
34
|
+
DEFAULT_POLICY.minLength,
|
|
35
|
+
1
|
|
36
|
+
);
|
|
37
|
+
return {
|
|
38
|
+
minLength,
|
|
39
|
+
requireDigit: parseBooleanWithDefault(
|
|
40
|
+
readEnvValue(env, "requireDigit"),
|
|
41
|
+
DEFAULT_POLICY.requireDigit
|
|
42
|
+
),
|
|
43
|
+
requireUppercase: parseBooleanWithDefault(
|
|
44
|
+
readEnvValue(env, "requireUppercase"),
|
|
45
|
+
DEFAULT_POLICY.requireUppercase
|
|
46
|
+
),
|
|
47
|
+
requireSpecial: parseBooleanWithDefault(
|
|
48
|
+
readEnvValue(env, "requireSpecial"),
|
|
49
|
+
DEFAULT_POLICY.requireSpecial
|
|
50
|
+
)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getPasswordRequirements(policy = getPasswordPolicy()) {
|
|
54
|
+
const requirements = [{ id: "minLength", value: policy.minLength }];
|
|
55
|
+
if (policy.requireDigit) requirements.push({ id: "digit" });
|
|
56
|
+
if (policy.requireUppercase) requirements.push({ id: "uppercase" });
|
|
57
|
+
if (policy.requireSpecial) requirements.push({ id: "special" });
|
|
58
|
+
return requirements;
|
|
59
|
+
}
|
|
60
|
+
function formatPasswordRequirements(policy, translate, keyPrefix = "auth.password.requirements") {
|
|
61
|
+
const items = getPasswordRequirements(policy).map((requirement) => {
|
|
62
|
+
switch (requirement.id) {
|
|
63
|
+
case "minLength":
|
|
64
|
+
return translate(
|
|
65
|
+
`${keyPrefix}.minLength`,
|
|
66
|
+
"At least {min} characters",
|
|
67
|
+
{ min: requirement.value ?? policy.minLength }
|
|
68
|
+
);
|
|
69
|
+
case "digit":
|
|
70
|
+
return translate(`${keyPrefix}.digit`, "One number");
|
|
71
|
+
case "uppercase":
|
|
72
|
+
return translate(`${keyPrefix}.uppercase`, "One uppercase letter");
|
|
73
|
+
case "special":
|
|
74
|
+
return translate(`${keyPrefix}.special`, "One special character");
|
|
75
|
+
default:
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}).filter((value) => value && value.trim().length > 0);
|
|
79
|
+
if (!items.length) return "";
|
|
80
|
+
const separator = translate(`${keyPrefix}.separator`, ", ");
|
|
81
|
+
return items.join(separator);
|
|
82
|
+
}
|
|
83
|
+
function validatePassword(password, policy = getPasswordPolicy()) {
|
|
84
|
+
const violations = [];
|
|
85
|
+
if (password.length < policy.minLength) violations.push("minLength");
|
|
86
|
+
if (policy.requireDigit && !/[0-9]/.test(password)) violations.push("digit");
|
|
87
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) violations.push("uppercase");
|
|
88
|
+
if (policy.requireSpecial && !/[^A-Za-z0-9]/.test(password)) violations.push("special");
|
|
89
|
+
return { ok: violations.length === 0, violations };
|
|
90
|
+
}
|
|
91
|
+
function buildPasswordSchema(options) {
|
|
92
|
+
const policy = options?.policy ?? getPasswordPolicy();
|
|
93
|
+
const maxLength = options?.maxLength;
|
|
94
|
+
const message = options?.message ?? "Password does not meet the requirements.";
|
|
95
|
+
let schema = z.string().min(policy.minLength, message);
|
|
96
|
+
if (typeof maxLength === "number") schema = schema.max(maxLength, message);
|
|
97
|
+
return schema.superRefine((value, ctx) => {
|
|
98
|
+
const result = validatePassword(value, policy);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
buildPasswordSchema,
|
|
106
|
+
formatPasswordRequirements,
|
|
107
|
+
getPasswordPolicy,
|
|
108
|
+
getPasswordRequirements,
|
|
109
|
+
validatePassword
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=passwordPolicy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/auth/passwordPolicy.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\n\nexport type PasswordPolicy = {\n minLength: number\n requireDigit: boolean\n requireUppercase: boolean\n requireSpecial: boolean\n}\n\nexport type PasswordRequirementId = 'minLength' | 'digit' | 'uppercase' | 'special'\n\nexport type PasswordRequirement = {\n id: PasswordRequirementId\n value?: number\n}\n\nexport type PasswordValidationResult = {\n ok: boolean\n violations: PasswordRequirementId[]\n}\n\nexport type PasswordRequirementFormatter = (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n) => string\n\nconst DEFAULT_POLICY: PasswordPolicy = {\n minLength: 6,\n requireDigit: true,\n requireUppercase: true,\n requireSpecial: true,\n}\n\nconst ENV_KEYS = {\n minLength: 'OM_PASSWORD_MIN_LENGTH',\n requireDigit: 'OM_PASSWORD_REQUIRE_DIGIT',\n requireUppercase: 'OM_PASSWORD_REQUIRE_UPPERCASE',\n requireSpecial: 'OM_PASSWORD_REQUIRE_SPECIAL',\n} as const\n\nconst PUBLIC_PREFIX = 'NEXT_PUBLIC_'\n\nfunction readEnvValue(env: NodeJS.ProcessEnv, key: keyof typeof ENV_KEYS): string | undefined {\n const rawKey = ENV_KEYS[key]\n const publicKey = `${PUBLIC_PREFIX}${rawKey}`\n const rawValue = env[rawKey]\n if (typeof rawValue === 'string' && rawValue.trim().length > 0) return rawValue\n const publicValue = env[publicKey]\n if (typeof publicValue === 'string' && publicValue.trim().length > 0) return publicValue\n return undefined\n}\n\nfunction parsePositiveInt(raw: string | undefined, fallback: number, min = 1): number {\n if (!raw) return fallback\n const parsed = Number.parseInt(raw, 10)\n if (Number.isNaN(parsed)) return fallback\n return Math.max(min, parsed)\n}\n\nexport function getPasswordPolicy(env: NodeJS.ProcessEnv = process.env): PasswordPolicy {\n const minLength = parsePositiveInt(\n readEnvValue(env, 'minLength'),\n DEFAULT_POLICY.minLength,\n 1,\n )\n return {\n minLength,\n requireDigit: parseBooleanWithDefault(\n readEnvValue(env, 'requireDigit'),\n DEFAULT_POLICY.requireDigit,\n ),\n requireUppercase: parseBooleanWithDefault(\n readEnvValue(env, 'requireUppercase'),\n DEFAULT_POLICY.requireUppercase,\n ),\n requireSpecial: parseBooleanWithDefault(\n readEnvValue(env, 'requireSpecial'),\n DEFAULT_POLICY.requireSpecial,\n ),\n }\n}\n\nexport function getPasswordRequirements(policy: PasswordPolicy = getPasswordPolicy()): PasswordRequirement[] {\n const requirements: PasswordRequirement[] = [{ id: 'minLength', value: policy.minLength }]\n if (policy.requireDigit) requirements.push({ id: 'digit' })\n if (policy.requireUppercase) requirements.push({ id: 'uppercase' })\n if (policy.requireSpecial) requirements.push({ id: 'special' })\n return requirements\n}\n\nexport function formatPasswordRequirements(\n policy: PasswordPolicy,\n translate: PasswordRequirementFormatter,\n keyPrefix = 'auth.password.requirements',\n): string {\n const items = getPasswordRequirements(policy).map((requirement) => {\n switch (requirement.id) {\n case 'minLength':\n return translate(\n `${keyPrefix}.minLength`,\n 'At least {min} characters',\n { min: requirement.value ?? policy.minLength },\n )\n case 'digit':\n return translate(`${keyPrefix}.digit`, 'One number')\n case 'uppercase':\n return translate(`${keyPrefix}.uppercase`, 'One uppercase letter')\n case 'special':\n return translate(`${keyPrefix}.special`, 'One special character')\n default:\n return ''\n }\n }).filter((value) => value && value.trim().length > 0)\n\n if (!items.length) return ''\n const separator = translate(`${keyPrefix}.separator`, ', ')\n return items.join(separator)\n}\n\nexport function validatePassword(\n password: string,\n policy: PasswordPolicy = getPasswordPolicy(),\n): PasswordValidationResult {\n const violations: PasswordRequirementId[] = []\n if (password.length < policy.minLength) violations.push('minLength')\n if (policy.requireDigit && !/[0-9]/.test(password)) violations.push('digit')\n if (policy.requireUppercase && !/[A-Z]/.test(password)) violations.push('uppercase')\n if (policy.requireSpecial && !/[^A-Za-z0-9]/.test(password)) violations.push('special')\n return { ok: violations.length === 0, violations }\n}\n\nexport function buildPasswordSchema(options?: {\n policy?: PasswordPolicy\n maxLength?: number\n message?: string\n}): z.ZodType<string> {\n const policy = options?.policy ?? getPasswordPolicy()\n const maxLength = options?.maxLength\n const message = options?.message ?? 'Password does not meet the requirements.'\n let schema = z.string().min(policy.minLength, message)\n if (typeof maxLength === 'number') schema = schema.max(maxLength, message)\n return schema.superRefine((value, ctx) => {\n const result = validatePassword(value, policy)\n if (!result.ok) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message })\n }\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/auth/server.js
CHANGED
|
@@ -90,6 +90,7 @@ async function resolveApiKeyAuth(secret) {
|
|
|
90
90
|
await em.persistAndFlush(record);
|
|
91
91
|
} catch {
|
|
92
92
|
}
|
|
93
|
+
const actualUserId = record.sessionUserId ?? record.createdBy ?? null;
|
|
93
94
|
return {
|
|
94
95
|
sub: `api_key:${record.id}`,
|
|
95
96
|
tenantId: record.tenantId ?? null,
|
|
@@ -97,7 +98,8 @@ async function resolveApiKeyAuth(secret) {
|
|
|
97
98
|
roles: roleNames,
|
|
98
99
|
isApiKey: true,
|
|
99
100
|
keyId: record.id,
|
|
100
|
-
keyName: record.name
|
|
101
|
+
keyName: record.name,
|
|
102
|
+
...actualUserId ? { userId: actualUserId } : {}
|
|
101
103
|
};
|
|
102
104
|
} catch {
|
|
103
105
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/auth/server.ts"],
|
|
4
|
-
"sourcesContent": ["import { cookies } from 'next/headers'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n if ((auth as Record<string, unknown>).isSuperAdmin === true) return true\n const roles = Array.isArray(auth?.roles) ? auth.roles : []\n return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role } = await import('@open-mercato/core/modules/auth/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) return null\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds } })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n try {\n record.lastUsedAt = new Date()\n await em.persistAndFlush(record)\n } catch {\n // best-effort update; ignore write failures\n }\n\n return {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n keyId: record.id,\n keyName: record.name,\n }\n } catch {\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return null\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return null\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n return null\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload) return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n // fall back to API key detection\n }\n }\n\n const apiKey = extractApiKey(req)\n if (!apiKey) return null\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) return null\n return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,iBAAiB;AAE1B,MAAM,qBAAqB;AAC3B,MAAM,2BAA2B;AACjC,MAAM,iCAAiC;AACvC,MAAM,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { cookies } from 'next/headers'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n userId?: string\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n if ((auth as Record<string, unknown>).isSuperAdmin === true) return true\n const roles = Array.isArray(auth?.roles) ? auth.roles : []\n return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role } = await import('@open-mercato/core/modules/auth/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) return null\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds } })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n try {\n record.lastUsedAt = new Date()\n await em.persistAndFlush(record)\n } catch {\n // best-effort update; ignore write failures\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n return {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\n } catch {\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return null\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return null\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n return null\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload) return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n // fall back to API key detection\n }\n }\n\n const apiKey = extractApiKey(req)\n if (!apiKey) return null\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) return null\n return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/email/send.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Resend } from "resend";
|
|
2
|
-
async function sendEmail({ to, subject, react, from }) {
|
|
2
|
+
async function sendEmail({ to, subject, react, from, replyTo }) {
|
|
3
3
|
const apiKey = process.env.RESEND_API_KEY;
|
|
4
4
|
if (!apiKey) throw new Error("RESEND_API_KEY is not set");
|
|
5
5
|
const resend = new Resend(apiKey);
|
|
6
6
|
const fromAddr = from || process.env.EMAIL_FROM || "no-reply@localhost";
|
|
7
|
-
|
|
7
|
+
const payload = {
|
|
8
|
+
to,
|
|
9
|
+
subject,
|
|
10
|
+
from: fromAddr,
|
|
11
|
+
react,
|
|
12
|
+
...replyTo ? { reply_to: replyTo } : {}
|
|
13
|
+
};
|
|
14
|
+
await resend.emails.send(payload);
|
|
8
15
|
}
|
|
9
16
|
export {
|
|
10
17
|
sendEmail
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/email/send.ts"],
|
|
4
|
-
"sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n}\n\nexport async function sendEmail({ to, subject, react, from }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;
|
|
4
|
+
"sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n replyTo?: string\n}\n\nexport async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n const payload = {\n to,\n subject,\n from: fromAddr,\n react,\n ...(replyTo ? { reply_to: replyTo } : {}),\n }\n await resend.emails.send(payload)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAAc;AAWvB,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,MAAM,QAAQ,GAAqB;AACvF,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,QAAQ,IAAI,cAAc;AACnD,QAAM,UAAU;AAAA,IACd;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,GAAI,UAAU,EAAE,UAAU,QAAQ,IAAI,CAAC;AAAA,EACzC;AACA,QAAM,OAAO,OAAO,KAAK,OAAO;AAClC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const NOTIFICATION_DOM_EVENTS = {
|
|
2
|
+
NEW: "om:notifications:new",
|
|
3
|
+
ACTIONED: "om:notifications:actioned",
|
|
4
|
+
COUNT_CHANGED: "om:notifications:count-changed"
|
|
5
|
+
};
|
|
6
|
+
function emitNotificationNew(detail) {
|
|
7
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
|
|
8
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.NEW, { detail }));
|
|
9
|
+
}
|
|
10
|
+
function emitNotificationActioned(notificationId) {
|
|
11
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
|
|
12
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.ACTIONED, { detail: { notificationId } }));
|
|
13
|
+
}
|
|
14
|
+
function emitNotificationCountChanged(count) {
|
|
15
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
|
|
16
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, { detail: { count } }));
|
|
17
|
+
}
|
|
18
|
+
function subscribeNotificationNew(handler) {
|
|
19
|
+
if (typeof window === "undefined") return () => {
|
|
20
|
+
};
|
|
21
|
+
const listener = (event) => handler(event.detail);
|
|
22
|
+
window.addEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener);
|
|
23
|
+
return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener);
|
|
24
|
+
}
|
|
25
|
+
function subscribeNotificationActioned(handler) {
|
|
26
|
+
if (typeof window === "undefined") return () => {
|
|
27
|
+
};
|
|
28
|
+
const listener = (event) => handler(event.detail.notificationId);
|
|
29
|
+
window.addEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener);
|
|
30
|
+
return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener);
|
|
31
|
+
}
|
|
32
|
+
function subscribeNotificationCountChanged(handler) {
|
|
33
|
+
if (typeof window === "undefined") return () => {
|
|
34
|
+
};
|
|
35
|
+
const listener = (event) => handler(event.detail.count);
|
|
36
|
+
window.addEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener);
|
|
37
|
+
return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
NOTIFICATION_DOM_EVENTS,
|
|
41
|
+
emitNotificationActioned,
|
|
42
|
+
emitNotificationCountChanged,
|
|
43
|
+
emitNotificationNew,
|
|
44
|
+
subscribeNotificationActioned,
|
|
45
|
+
subscribeNotificationCountChanged,
|
|
46
|
+
subscribeNotificationNew
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=notificationEvents.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/frontend/notificationEvents.ts"],
|
|
4
|
+
"sourcesContent": ["export const NOTIFICATION_DOM_EVENTS = {\n NEW: 'om:notifications:new',\n ACTIONED: 'om:notifications:actioned',\n COUNT_CHANGED: 'om:notifications:count-changed',\n} as const\n\nexport type NotificationNewDetail = {\n id: string\n type: string\n title: string\n severity: string\n}\n\nexport function emitNotificationNew(detail: NotificationNewDetail): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.NEW, { detail }))\n}\n\nexport function emitNotificationActioned(notificationId: string): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.ACTIONED, { detail: { notificationId } }))\n}\n\nexport function emitNotificationCountChanged(count: number): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, { detail: { count } }))\n}\n\nexport function subscribeNotificationNew(handler: (detail: NotificationNewDetail) => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => handler((event as CustomEvent<NotificationNewDetail>).detail)\n window.addEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)\n return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)\n}\n\nexport function subscribeNotificationActioned(handler: (notificationId: string) => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => handler((event as CustomEvent<{ notificationId: string }>).detail.notificationId)\n window.addEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)\n return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)\n}\n\nexport function subscribeNotificationCountChanged(handler: (count: number) => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => handler((event as CustomEvent<{ count: number }>).detail.count)\n window.addEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)\n return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)\n}\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,0BAA0B;AAAA,EACrC,KAAK;AAAA,EACL,UAAU;AAAA,EACV,eAAe;AACjB;AASO,SAAS,oBAAoB,QAAqC;AACvE,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,SAAO,cAAc,IAAI,YAAY,wBAAwB,KAAK,EAAE,OAAO,CAAC,CAAC;AAC/E;AAEO,SAAS,yBAAyB,gBAA8B;AACrE,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,SAAO,cAAc,IAAI,YAAY,wBAAwB,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAC;AACxG;AAEO,SAAS,6BAA6B,OAAqB;AAChE,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,SAAO,cAAc,IAAI,YAAY,wBAAwB,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;AACpG;AAEO,SAAS,yBAAyB,SAA8D;AACrG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB,QAAS,MAA6C,MAAM;AAC/F,SAAO,iBAAiB,wBAAwB,KAAK,QAAQ;AAC7D,SAAO,MAAM,OAAO,oBAAoB,wBAAwB,KAAK,QAAQ;AAC/E;AAEO,SAAS,8BAA8B,SAAuD;AACnG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB,QAAS,MAAkD,OAAO,cAAc;AACnH,SAAO,iBAAiB,wBAAwB,UAAU,QAAQ;AAClE,SAAO,MAAM,OAAO,oBAAoB,wBAAwB,UAAU,QAAQ;AACpF;AAEO,SAAS,kCAAkC,SAA8C;AAC9F,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB,QAAS,MAAyC,OAAO,KAAK;AACjG,SAAO,iBAAiB,wBAAwB,eAAe,QAAQ;AACvE,SAAO,MAAM,OAAO,oBAAoB,wBAAwB,eAAe,QAAQ;AACzF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Resend } from "resend";
|
|
2
|
-
async function sendEmail({ to, subject, react, from }) {
|
|
2
|
+
async function sendEmail({ to, subject, react, from, replyTo }) {
|
|
3
3
|
const apiKey = process.env.RESEND_API_KEY;
|
|
4
4
|
if (!apiKey) throw new Error("RESEND_API_KEY is not set");
|
|
5
5
|
const resend = new Resend(apiKey);
|
|
6
6
|
const fromAddr = from || process.env.EMAIL_FROM || "no-reply@localhost";
|
|
7
|
-
await resend.emails.send({ to, subject, from: fromAddr, react });
|
|
7
|
+
await resend.emails.send({ to, subject, from: fromAddr, react, replyTo });
|
|
8
8
|
}
|
|
9
9
|
export {
|
|
10
10
|
sendEmail
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/lib/email/send.ts"],
|
|
4
|
-
"sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n}\n\nexport async function sendEmail({ to, subject, react, from }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n await resend.emails.send({ to, subject, from: fromAddr, react })\n}\n
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;
|
|
4
|
+
"sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n replyTo?: string\n}\n\nexport async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n await resend.emails.send({ to, subject, from: fromAddr, react, replyTo })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAAc;AAWvB,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,MAAM,QAAQ,GAAqB;AACvF,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,QAAQ,IAAI,cAAc;AACnD,QAAM,OAAO,OAAO,KAAK,EAAE,IAAI,SAAS,MAAM,UAAU,OAAO,QAAQ,CAAC;AAC1E;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.2-canary-
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.2-canary-49d47ff90e'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import { createRequestContainer } from '../di/container'
|
|
3
|
+
import { getAuthFromRequest, type AuthContext } from '../auth/server'
|
|
4
|
+
import { resolveTranslations } from '../i18n/server'
|
|
5
|
+
|
|
6
|
+
export type RequestContext = {
|
|
7
|
+
container: AwilixContainer
|
|
8
|
+
auth: AuthContext
|
|
9
|
+
organizationScope?: unknown
|
|
10
|
+
selectedOrganizationId?: string | null
|
|
11
|
+
organizationIds?: string[] | null
|
|
12
|
+
translate: (key: string, fallback?: string) => string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ResolveRequestContextResult = {
|
|
16
|
+
ctx: RequestContext
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the request context for API routes.
|
|
21
|
+
* This includes container, auth, and translations.
|
|
22
|
+
* For organization-scoped routes, use resolveOrganizationScopeForRequest separately.
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveRequestContext(req: Request): Promise<ResolveRequestContextResult> {
|
|
25
|
+
const container = await createRequestContainer()
|
|
26
|
+
const auth = await getAuthFromRequest(req)
|
|
27
|
+
const { translate } = await resolveTranslations()
|
|
28
|
+
|
|
29
|
+
const ctx: RequestContext = {
|
|
30
|
+
container,
|
|
31
|
+
auth,
|
|
32
|
+
translate,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { ctx }
|
|
36
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
3
|
+
|
|
4
|
+
export type PasswordPolicy = {
|
|
5
|
+
minLength: number
|
|
6
|
+
requireDigit: boolean
|
|
7
|
+
requireUppercase: boolean
|
|
8
|
+
requireSpecial: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type PasswordRequirementId = 'minLength' | 'digit' | 'uppercase' | 'special'
|
|
12
|
+
|
|
13
|
+
export type PasswordRequirement = {
|
|
14
|
+
id: PasswordRequirementId
|
|
15
|
+
value?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PasswordValidationResult = {
|
|
19
|
+
ok: boolean
|
|
20
|
+
violations: PasswordRequirementId[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PasswordRequirementFormatter = (
|
|
24
|
+
key: string,
|
|
25
|
+
fallback: string,
|
|
26
|
+
params?: Record<string, string | number>,
|
|
27
|
+
) => string
|
|
28
|
+
|
|
29
|
+
const DEFAULT_POLICY: PasswordPolicy = {
|
|
30
|
+
minLength: 6,
|
|
31
|
+
requireDigit: true,
|
|
32
|
+
requireUppercase: true,
|
|
33
|
+
requireSpecial: true,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ENV_KEYS = {
|
|
37
|
+
minLength: 'OM_PASSWORD_MIN_LENGTH',
|
|
38
|
+
requireDigit: 'OM_PASSWORD_REQUIRE_DIGIT',
|
|
39
|
+
requireUppercase: 'OM_PASSWORD_REQUIRE_UPPERCASE',
|
|
40
|
+
requireSpecial: 'OM_PASSWORD_REQUIRE_SPECIAL',
|
|
41
|
+
} as const
|
|
42
|
+
|
|
43
|
+
const PUBLIC_PREFIX = 'NEXT_PUBLIC_'
|
|
44
|
+
|
|
45
|
+
function readEnvValue(env: NodeJS.ProcessEnv, key: keyof typeof ENV_KEYS): string | undefined {
|
|
46
|
+
const rawKey = ENV_KEYS[key]
|
|
47
|
+
const publicKey = `${PUBLIC_PREFIX}${rawKey}`
|
|
48
|
+
const rawValue = env[rawKey]
|
|
49
|
+
if (typeof rawValue === 'string' && rawValue.trim().length > 0) return rawValue
|
|
50
|
+
const publicValue = env[publicKey]
|
|
51
|
+
if (typeof publicValue === 'string' && publicValue.trim().length > 0) return publicValue
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parsePositiveInt(raw: string | undefined, fallback: number, min = 1): number {
|
|
56
|
+
if (!raw) return fallback
|
|
57
|
+
const parsed = Number.parseInt(raw, 10)
|
|
58
|
+
if (Number.isNaN(parsed)) return fallback
|
|
59
|
+
return Math.max(min, parsed)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getPasswordPolicy(env: NodeJS.ProcessEnv = process.env): PasswordPolicy {
|
|
63
|
+
const minLength = parsePositiveInt(
|
|
64
|
+
readEnvValue(env, 'minLength'),
|
|
65
|
+
DEFAULT_POLICY.minLength,
|
|
66
|
+
1,
|
|
67
|
+
)
|
|
68
|
+
return {
|
|
69
|
+
minLength,
|
|
70
|
+
requireDigit: parseBooleanWithDefault(
|
|
71
|
+
readEnvValue(env, 'requireDigit'),
|
|
72
|
+
DEFAULT_POLICY.requireDigit,
|
|
73
|
+
),
|
|
74
|
+
requireUppercase: parseBooleanWithDefault(
|
|
75
|
+
readEnvValue(env, 'requireUppercase'),
|
|
76
|
+
DEFAULT_POLICY.requireUppercase,
|
|
77
|
+
),
|
|
78
|
+
requireSpecial: parseBooleanWithDefault(
|
|
79
|
+
readEnvValue(env, 'requireSpecial'),
|
|
80
|
+
DEFAULT_POLICY.requireSpecial,
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getPasswordRequirements(policy: PasswordPolicy = getPasswordPolicy()): PasswordRequirement[] {
|
|
86
|
+
const requirements: PasswordRequirement[] = [{ id: 'minLength', value: policy.minLength }]
|
|
87
|
+
if (policy.requireDigit) requirements.push({ id: 'digit' })
|
|
88
|
+
if (policy.requireUppercase) requirements.push({ id: 'uppercase' })
|
|
89
|
+
if (policy.requireSpecial) requirements.push({ id: 'special' })
|
|
90
|
+
return requirements
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatPasswordRequirements(
|
|
94
|
+
policy: PasswordPolicy,
|
|
95
|
+
translate: PasswordRequirementFormatter,
|
|
96
|
+
keyPrefix = 'auth.password.requirements',
|
|
97
|
+
): string {
|
|
98
|
+
const items = getPasswordRequirements(policy).map((requirement) => {
|
|
99
|
+
switch (requirement.id) {
|
|
100
|
+
case 'minLength':
|
|
101
|
+
return translate(
|
|
102
|
+
`${keyPrefix}.minLength`,
|
|
103
|
+
'At least {min} characters',
|
|
104
|
+
{ min: requirement.value ?? policy.minLength },
|
|
105
|
+
)
|
|
106
|
+
case 'digit':
|
|
107
|
+
return translate(`${keyPrefix}.digit`, 'One number')
|
|
108
|
+
case 'uppercase':
|
|
109
|
+
return translate(`${keyPrefix}.uppercase`, 'One uppercase letter')
|
|
110
|
+
case 'special':
|
|
111
|
+
return translate(`${keyPrefix}.special`, 'One special character')
|
|
112
|
+
default:
|
|
113
|
+
return ''
|
|
114
|
+
}
|
|
115
|
+
}).filter((value) => value && value.trim().length > 0)
|
|
116
|
+
|
|
117
|
+
if (!items.length) return ''
|
|
118
|
+
const separator = translate(`${keyPrefix}.separator`, ', ')
|
|
119
|
+
return items.join(separator)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validatePassword(
|
|
123
|
+
password: string,
|
|
124
|
+
policy: PasswordPolicy = getPasswordPolicy(),
|
|
125
|
+
): PasswordValidationResult {
|
|
126
|
+
const violations: PasswordRequirementId[] = []
|
|
127
|
+
if (password.length < policy.minLength) violations.push('minLength')
|
|
128
|
+
if (policy.requireDigit && !/[0-9]/.test(password)) violations.push('digit')
|
|
129
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) violations.push('uppercase')
|
|
130
|
+
if (policy.requireSpecial && !/[^A-Za-z0-9]/.test(password)) violations.push('special')
|
|
131
|
+
return { ok: violations.length === 0, violations }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildPasswordSchema(options?: {
|
|
135
|
+
policy?: PasswordPolicy
|
|
136
|
+
maxLength?: number
|
|
137
|
+
message?: string
|
|
138
|
+
}): z.ZodType<string> {
|
|
139
|
+
const policy = options?.policy ?? getPasswordPolicy()
|
|
140
|
+
const maxLength = options?.maxLength
|
|
141
|
+
const message = options?.message ?? 'Password does not meet the requirements.'
|
|
142
|
+
let schema = z.string().min(policy.minLength, message)
|
|
143
|
+
if (typeof maxLength === 'number') schema = schema.max(maxLength, message)
|
|
144
|
+
return schema.superRefine((value, ctx) => {
|
|
145
|
+
const result = validatePassword(value, policy)
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message })
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
}
|
package/src/lib/auth/server.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type AuthContext = {
|
|
|
14
14
|
email?: string
|
|
15
15
|
roles?: string[]
|
|
16
16
|
isApiKey?: boolean
|
|
17
|
+
userId?: string
|
|
17
18
|
keyId?: string
|
|
18
19
|
keyName?: string
|
|
19
20
|
[k: string]: unknown
|
|
@@ -132,6 +133,9 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
|
|
|
132
133
|
// best-effort update; ignore write failures
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// For session keys, use sessionUserId; for regular keys, use createdBy
|
|
137
|
+
const actualUserId = record.sessionUserId ?? record.createdBy ?? null
|
|
138
|
+
|
|
135
139
|
return {
|
|
136
140
|
sub: `api_key:${record.id}`,
|
|
137
141
|
tenantId: record.tenantId ?? null,
|
|
@@ -140,6 +144,7 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
|
|
|
140
144
|
isApiKey: true,
|
|
141
145
|
keyId: record.id,
|
|
142
146
|
keyName: record.name,
|
|
147
|
+
...(actualUserId ? { userId: actualUserId } : {}),
|
|
143
148
|
}
|
|
144
149
|
} catch {
|
|
145
150
|
return null
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { sendEmail } from '../send'
|
|
3
|
+
|
|
4
|
+
const sendMock = jest.fn().mockResolvedValue({ id: 'email-1' })
|
|
5
|
+
const ResendMock = jest.fn().mockImplementation(() => ({
|
|
6
|
+
emails: { send: sendMock },
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
jest.mock('resend', () => ({
|
|
10
|
+
Resend: ResendMock,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
describe('sendEmail', () => {
|
|
14
|
+
const originalEnv = process.env
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
process.env = {
|
|
18
|
+
...originalEnv,
|
|
19
|
+
RESEND_API_KEY: 'test-key',
|
|
20
|
+
EMAIL_FROM: 'from@example.com',
|
|
21
|
+
}
|
|
22
|
+
sendMock.mockClear()
|
|
23
|
+
ResendMock.mockClear()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
process.env = originalEnv
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('maps replyTo to reply_to in Resend payload', async () => {
|
|
31
|
+
await sendEmail({
|
|
32
|
+
to: 'user@example.com',
|
|
33
|
+
subject: 'Hello',
|
|
34
|
+
react: React.createElement('div', null, 'Hi'),
|
|
35
|
+
replyTo: 'reply@example.com',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(ResendMock).toHaveBeenCalledWith('test-key')
|
|
39
|
+
expect(sendMock).toHaveBeenCalledWith(
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
to: 'user@example.com',
|
|
42
|
+
subject: 'Hello',
|
|
43
|
+
from: 'from@example.com',
|
|
44
|
+
reply_to: 'reply@example.com',
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('omits reply_to when replyTo is not provided', async () => {
|
|
50
|
+
await sendEmail({
|
|
51
|
+
to: 'user@example.com',
|
|
52
|
+
subject: 'Hello',
|
|
53
|
+
react: React.createElement('div', null, 'Hi'),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const payload = sendMock.mock.calls[0]?.[0] as Record<string, unknown>
|
|
57
|
+
expect(payload).toBeDefined()
|
|
58
|
+
expect(payload.reply_to).toBeUndefined()
|
|
59
|
+
})
|
|
60
|
+
})
|
package/src/lib/email/send.ts
CHANGED
|
@@ -6,13 +6,20 @@ export type SendEmailOptions = {
|
|
|
6
6
|
subject: string
|
|
7
7
|
react: React.ReactElement
|
|
8
8
|
from?: string
|
|
9
|
+
replyTo?: string
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
|
|
12
|
+
export async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {
|
|
12
13
|
const apiKey = process.env.RESEND_API_KEY
|
|
13
14
|
if (!apiKey) throw new Error('RESEND_API_KEY is not set')
|
|
14
15
|
const resend = new Resend(apiKey)
|
|
15
16
|
const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'
|
|
16
|
-
|
|
17
|
+
const payload = {
|
|
18
|
+
to,
|
|
19
|
+
subject,
|
|
20
|
+
from: fromAddr,
|
|
21
|
+
react,
|
|
22
|
+
...(replyTo ? { reply_to: replyTo } : {}),
|
|
23
|
+
}
|
|
24
|
+
await resend.emails.send(payload)
|
|
17
25
|
}
|
|
18
|
-
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const NOTIFICATION_DOM_EVENTS = {
|
|
2
|
+
NEW: 'om:notifications:new',
|
|
3
|
+
ACTIONED: 'om:notifications:actioned',
|
|
4
|
+
COUNT_CHANGED: 'om:notifications:count-changed',
|
|
5
|
+
} as const
|
|
6
|
+
|
|
7
|
+
export type NotificationNewDetail = {
|
|
8
|
+
id: string
|
|
9
|
+
type: string
|
|
10
|
+
title: string
|
|
11
|
+
severity: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function emitNotificationNew(detail: NotificationNewDetail): void {
|
|
15
|
+
if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
|
|
16
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.NEW, { detail }))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function emitNotificationActioned(notificationId: string): void {
|
|
20
|
+
if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
|
|
21
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.ACTIONED, { detail: { notificationId } }))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function emitNotificationCountChanged(count: number): void {
|
|
25
|
+
if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
|
|
26
|
+
window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, { detail: { count } }))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function subscribeNotificationNew(handler: (detail: NotificationNewDetail) => void): () => void {
|
|
30
|
+
if (typeof window === 'undefined') return () => {}
|
|
31
|
+
const listener = (event: Event) => handler((event as CustomEvent<NotificationNewDetail>).detail)
|
|
32
|
+
window.addEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)
|
|
33
|
+
return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function subscribeNotificationActioned(handler: (notificationId: string) => void): () => void {
|
|
37
|
+
if (typeof window === 'undefined') return () => {}
|
|
38
|
+
const listener = (event: Event) => handler((event as CustomEvent<{ notificationId: string }>).detail.notificationId)
|
|
39
|
+
window.addEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)
|
|
40
|
+
return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function subscribeNotificationCountChanged(handler: (count: number) => void): () => void {
|
|
44
|
+
if (typeof window === 'undefined') return () => {}
|
|
45
|
+
const listener = (event: Event) => handler((event as CustomEvent<{ count: number }>).detail.count)
|
|
46
|
+
window.addEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)
|
|
47
|
+
return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)
|
|
48
|
+
}
|
|
@@ -6,13 +6,13 @@ export type SendEmailOptions = {
|
|
|
6
6
|
subject: string
|
|
7
7
|
react: React.ReactElement
|
|
8
8
|
from?: string
|
|
9
|
+
replyTo?: string
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
|
|
12
|
+
export async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {
|
|
12
13
|
const apiKey = process.env.RESEND_API_KEY
|
|
13
14
|
if (!apiKey) throw new Error('RESEND_API_KEY is not set')
|
|
14
15
|
const resend = new Resend(apiKey)
|
|
15
16
|
const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'
|
|
16
|
-
await resend.emails.send({ to, subject, from: fromAddr, react })
|
|
17
|
+
await resend.emails.send({ to, subject, from: fromAddr, react, replyTo })
|
|
17
18
|
}
|
|
18
|
-
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './types'
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
|
|
3
|
+
export type NotificationStatus = 'unread' | 'read' | 'actioned' | 'dismissed'
|
|
4
|
+
export type NotificationSeverity = 'info' | 'warning' | 'success' | 'error'
|
|
5
|
+
|
|
6
|
+
export type NotificationAction = {
|
|
7
|
+
id: string
|
|
8
|
+
label: string
|
|
9
|
+
labelKey?: string
|
|
10
|
+
variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'
|
|
11
|
+
icon?: string
|
|
12
|
+
commandId?: string
|
|
13
|
+
href?: string
|
|
14
|
+
confirmRequired?: boolean
|
|
15
|
+
confirmMessage?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type NotificationActionData = {
|
|
19
|
+
actions: NotificationAction[]
|
|
20
|
+
primaryActionId?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type NotificationTypeAction = {
|
|
24
|
+
id: string
|
|
25
|
+
labelKey: string
|
|
26
|
+
variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'
|
|
27
|
+
icon?: string
|
|
28
|
+
commandId?: string
|
|
29
|
+
href?: string
|
|
30
|
+
confirmRequired?: boolean
|
|
31
|
+
confirmMessageKey?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type NotificationRendererProps = {
|
|
35
|
+
notification: {
|
|
36
|
+
id: string
|
|
37
|
+
type: string
|
|
38
|
+
title: string
|
|
39
|
+
body?: string | null
|
|
40
|
+
titleKey?: string | null
|
|
41
|
+
bodyKey?: string | null
|
|
42
|
+
titleVariables?: Record<string, string> | null
|
|
43
|
+
bodyVariables?: Record<string, string> | null
|
|
44
|
+
icon?: string | null
|
|
45
|
+
severity: string
|
|
46
|
+
status: string
|
|
47
|
+
sourceModule?: string | null
|
|
48
|
+
sourceEntityType?: string | null
|
|
49
|
+
sourceEntityId?: string | null
|
|
50
|
+
linkHref?: string | null
|
|
51
|
+
createdAt: string
|
|
52
|
+
}
|
|
53
|
+
onAction: (actionId: string) => Promise<void>
|
|
54
|
+
onDismiss: () => Promise<void>
|
|
55
|
+
actions: NotificationTypeAction[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type NotificationTypeDefinition = {
|
|
59
|
+
type: string
|
|
60
|
+
module: string
|
|
61
|
+
titleKey: string
|
|
62
|
+
bodyKey?: string
|
|
63
|
+
icon: string
|
|
64
|
+
severity: NotificationSeverity
|
|
65
|
+
actions: NotificationTypeAction[]
|
|
66
|
+
primaryActionId?: string
|
|
67
|
+
linkHref?: string
|
|
68
|
+
Renderer?: ComponentType<NotificationRendererProps>
|
|
69
|
+
expiresAfterHours?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type NotificationDto = {
|
|
73
|
+
id: string
|
|
74
|
+
type: string
|
|
75
|
+
title: string
|
|
76
|
+
body?: string | null
|
|
77
|
+
titleKey?: string | null
|
|
78
|
+
bodyKey?: string | null
|
|
79
|
+
titleVariables?: Record<string, string> | null
|
|
80
|
+
bodyVariables?: Record<string, string> | null
|
|
81
|
+
icon?: string | null
|
|
82
|
+
severity: string
|
|
83
|
+
status: string
|
|
84
|
+
actions: Array<{
|
|
85
|
+
id: string
|
|
86
|
+
label: string
|
|
87
|
+
labelKey?: string
|
|
88
|
+
variant?: string
|
|
89
|
+
icon?: string
|
|
90
|
+
}>
|
|
91
|
+
primaryActionId?: string
|
|
92
|
+
sourceModule?: string | null
|
|
93
|
+
sourceEntityType?: string | null
|
|
94
|
+
sourceEntityId?: string | null
|
|
95
|
+
linkHref?: string | null
|
|
96
|
+
createdAt: string
|
|
97
|
+
readAt?: string | null
|
|
98
|
+
actionTaken?: string | null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type NotificationPollData = {
|
|
102
|
+
unreadCount: number
|
|
103
|
+
recent: NotificationDto[]
|
|
104
|
+
hasNew: boolean
|
|
105
|
+
lastId?: string
|
|
106
|
+
}
|