@open-mercato/core 0.4.5-develop-0c30cb4b11 → 0.4.5-develop-974adb54b3

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.
@@ -2,7 +2,24 @@ import { NextResponse } from "next/server";
2
2
  import { toAbsoluteUrl } from "@open-mercato/shared/lib/url";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { signJwt } from "@open-mercato/shared/lib/auth/jwt";
5
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
6
+ import { refreshSessionRequestSchema } from "@open-mercato/core/modules/auth/data/validators";
7
+ import { checkAuthRateLimit } from "@open-mercato/core/modules/auth/lib/rateLimitCheck";
8
+ import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
9
+ import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
5
10
  import { z } from "zod";
11
+ const refreshRateLimitConfig = readEndpointRateLimitConfig("REFRESH", {
12
+ points: 15,
13
+ duration: 60,
14
+ blockDuration: 60,
15
+ keyPrefix: "refresh"
16
+ });
17
+ const refreshIpRateLimitConfig = readEndpointRateLimitConfig("REFRESH_IP", {
18
+ points: 60,
19
+ duration: 60,
20
+ blockDuration: 60,
21
+ keyPrefix: "refresh-ip"
22
+ });
6
23
  function parseCookie(req, name) {
7
24
  const cookie = req.headers.get("cookie") || "";
8
25
  const m = cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
@@ -37,23 +54,37 @@ async function GET(req) {
37
54
  return res;
38
55
  }
39
56
  async function POST(req) {
57
+ const { translate } = await resolveTranslations();
40
58
  let token = null;
41
59
  try {
42
60
  const body = await req.json();
43
- const parsed = refreshRequestSchema.safeParse(body);
61
+ const parsed = refreshSessionRequestSchema.safeParse(body);
44
62
  if (parsed.success) {
45
63
  token = parsed.data.refreshToken;
46
64
  }
47
65
  } catch {
48
66
  }
67
+ const { error: rateLimitError } = await checkAuthRateLimit({
68
+ req,
69
+ ipConfig: refreshIpRateLimitConfig,
70
+ compoundConfig: refreshRateLimitConfig,
71
+ compoundIdentifier: token ?? void 0
72
+ });
73
+ if (rateLimitError) return rateLimitError;
49
74
  if (!token) {
50
- return NextResponse.json({ ok: false, error: "Missing or invalid refresh token" }, { status: 400 });
75
+ return NextResponse.json({
76
+ ok: false,
77
+ error: translate("auth.session.refresh.errors.invalidPayload", "Missing or invalid refresh token")
78
+ }, { status: 400 });
51
79
  }
52
80
  const c = await createRequestContainer();
53
81
  const auth = c.resolve("authService");
54
82
  const ctx = await auth.refreshFromSessionToken(token);
55
83
  if (!ctx) {
56
- return NextResponse.json({ ok: false, error: "Invalid or expired refresh token" }, { status: 401 });
84
+ return NextResponse.json({
85
+ ok: false,
86
+ error: translate("auth.session.refresh.errors.invalidToken", "Invalid or expired refresh token")
87
+ }, { status: 401 });
57
88
  }
58
89
  const { user, roles } = ctx;
59
90
  const jwt = signJwt({
@@ -84,9 +115,6 @@ const metadata = {
84
115
  const refreshQuerySchema = z.object({
85
116
  redirect: z.string().optional().describe("Absolute or relative URL to redirect after refresh")
86
117
  });
87
- const refreshRequestSchema = z.object({
88
- refreshToken: z.string().min(1).describe("The refresh token obtained from login")
89
- });
90
118
  const refreshSuccessSchema = z.object({
91
119
  ok: z.literal(true),
92
120
  accessToken: z.string().describe("New JWT access token"),
@@ -111,13 +139,14 @@ const openApi = {
111
139
  POST: {
112
140
  summary: "Refresh access token (API/mobile)",
113
141
  description: "Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.",
114
- requestBody: { schema: refreshRequestSchema, contentType: "application/json" },
142
+ requestBody: { schema: refreshSessionRequestSchema, contentType: "application/json" },
115
143
  responses: [
116
144
  { status: 200, description: "New access token issued", schema: refreshSuccessSchema }
117
145
  ],
118
146
  errors: [
119
147
  { status: 400, description: "Missing refresh token", schema: refreshErrorSchema },
120
- { status: 401, description: "Invalid or expired token", schema: refreshErrorSchema }
148
+ { status: 401, description: "Invalid or expired token", schema: refreshErrorSchema },
149
+ { status: 429, description: "Too many refresh attempts", schema: rateLimitErrorSchema }
121
150
  ]
122
151
  }
123
152
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/session/refresh.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { z } from 'zod'\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction sanitizeRedirect(param: string | null, baseUrl: string): string {\n const value = param || '/'\n try {\n const base = new URL(baseUrl)\n const resolved = new URL(value, baseUrl)\n if (resolved.origin === base.origin && resolved.pathname.startsWith('/')) {\n return resolved.pathname + resolved.search + resolved.hash\n }\n } catch {}\n return '/'\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = process.env.APP_URL || `${url.protocol}//${url.host}`\n const redirectTo = sanitizeRedirect(url.searchParams.get('redirect'), baseUrl)\n const token = parseCookie(req, 'session_token')\n if (!token) return NextResponse.redirect(toAbsoluteUrl(req, '/login?redirect=' + encodeURIComponent(redirectTo)))\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) return NextResponse.redirect(toAbsoluteUrl(req, '/login?redirect=' + encodeURIComponent(redirectTo)))\n const { user, roles } = ctx\n const jwt = signJwt({ sub: String(user.id), tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })\n const res = NextResponse.redirect(toAbsoluteUrl(req, redirectTo))\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n if (!token) {\n return NextResponse.json({ ok: false, error: 'Missing or invalid refresh token' }, { status: 400 })\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired refresh token' }, { status: 401 })\n }\n\n const { user, roles } = ctx\n const jwt = signJwt({\n sub: String(user.id),\n tenantId: String(user.tenantId),\n orgId: String(user.organizationId),\n email: user.email,\n roles,\n })\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshRequestSchema = z.object({\n refreshToken: z.string().min(1).describe('The refresh token obtained from login'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction sanitizeRedirect(param: string | null, baseUrl: string): string {\n const value = param || '/'\n try {\n const base = new URL(baseUrl)\n const resolved = new URL(value, baseUrl)\n if (resolved.origin === base.origin && resolved.pathname.startsWith('/')) {\n return resolved.pathname + resolved.search + resolved.hash\n }\n } catch {}\n return '/'\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = process.env.APP_URL || `${url.protocol}//${url.host}`\n const redirectTo = sanitizeRedirect(url.searchParams.get('redirect'), baseUrl)\n const token = parseCookie(req, 'session_token')\n if (!token) return NextResponse.redirect(toAbsoluteUrl(req, '/login?redirect=' + encodeURIComponent(redirectTo)))\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) return NextResponse.redirect(toAbsoluteUrl(req, '/login?redirect=' + encodeURIComponent(redirectTo)))\n const { user, roles } = ctx\n const jwt = signJwt({ sub: String(user.id), tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })\n const res = NextResponse.redirect(toAbsoluteUrl(req, redirectTo))\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshSessionRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: refreshIpRateLimitConfig,\n compoundConfig: refreshRateLimitConfig,\n compoundIdentifier: token ?? undefined,\n })\n if (rateLimitError) return rateLimitError\n\n if (!token) {\n return NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n }\n\n const { user, roles } = ctx\n const jwt = signJwt({\n sub: String(user.id),\n tenantId: String(user.tenantId),\n orgId: String(user.organizationId),\n email: user.email,\n roles,\n })\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -14,6 +14,9 @@ const confirmPasswordResetSchema = z.object({
14
14
  token: z.string().min(10),
15
15
  password: passwordSchema
16
16
  });
17
+ const refreshSessionRequestSchema = z.object({
18
+ refreshToken: z.string().min(1)
19
+ });
17
20
  const sidebarPreferencesInputSchema = z.object({
18
21
  version: z.number().int().positive().optional(),
19
22
  groupOrder: z.array(z.string().min(1)).max(200).optional(),
@@ -32,6 +35,7 @@ const userCreateSchema = z.object({
32
35
  });
33
36
  export {
34
37
  confirmPasswordResetSchema,
38
+ refreshSessionRequestSchema,
35
39
  requestPasswordResetSchema,
36
40
  sidebarPreferencesInputSchema,
37
41
  userCreateSchema,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst passwordSchema = buildPasswordSchema()\n\n// Core auth validators\nexport const userLoginSchema = z.object({\n email: z.string().email(),\n password: z.string().min(6),\n requireRole: z.string().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const requestPasswordResetSchema = z.object({\n email: z.string().email(),\n})\n\nexport const confirmPasswordResetSchema = z.object({\n token: z.string().min(10),\n password: passwordSchema,\n})\n\nexport const sidebarPreferencesInputSchema = z.object({\n version: z.number().int().positive().optional(),\n groupOrder: z.array(z.string().min(1)).max(200).optional(),\n groupLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n itemLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n hiddenItems: z.array(z.string().min(1)).max(500).optional(),\n applyToRoles: z.array(z.string().uuid()).optional(),\n clearRoleIds: z.array(z.string().uuid()).optional(),\n})\n\n// Optional helpers for CLI or admin forms\nexport const userCreateSchema = z.object({\n email: z.string().email(),\n password: passwordSchema,\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid(),\n rolesCsv: z.string().optional(),\n})\n\nexport type UserLoginInput = z.infer<typeof userLoginSchema>\nexport type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>\nexport type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>\nexport type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>\nexport type UserCreateInput = z.infer<typeof userCreateSchema>\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,2BAA2B;AAEpC,MAAM,iBAAiB,oBAAoB;AAGpC,MAAM,kBAAkB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,MAAM;AAC1B,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;AAAA,EACxB,UAAU;AACZ,CAAC;AAEM,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACzD,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EAC9E,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EAC7E,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1D,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AACpD,CAAC;AAGM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU;AAAA,EACV,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst passwordSchema = buildPasswordSchema()\n\n// Core auth validators\nexport const userLoginSchema = z.object({\n email: z.string().email(),\n password: z.string().min(6),\n requireRole: z.string().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const requestPasswordResetSchema = z.object({\n email: z.string().email(),\n})\n\nexport const confirmPasswordResetSchema = z.object({\n token: z.string().min(10),\n password: passwordSchema,\n})\n\nexport const refreshSessionRequestSchema = z.object({\n refreshToken: z.string().min(1),\n})\n\nexport const sidebarPreferencesInputSchema = z.object({\n version: z.number().int().positive().optional(),\n groupOrder: z.array(z.string().min(1)).max(200).optional(),\n groupLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n itemLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n hiddenItems: z.array(z.string().min(1)).max(500).optional(),\n applyToRoles: z.array(z.string().uuid()).optional(),\n clearRoleIds: z.array(z.string().uuid()).optional(),\n})\n\n// Optional helpers for CLI or admin forms\nexport const userCreateSchema = z.object({\n email: z.string().email(),\n password: passwordSchema,\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid(),\n rolesCsv: z.string().optional(),\n})\n\nexport type UserLoginInput = z.infer<typeof userLoginSchema>\nexport type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>\nexport type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>\nexport type RefreshSessionRequestInput = z.infer<typeof refreshSessionRequestSchema>\nexport type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>\nexport type UserCreateInput = z.infer<typeof userCreateSchema>\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,2BAA2B;AAEpC,MAAM,iBAAiB,oBAAoB;AAGpC,MAAM,kBAAkB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,MAAM;AAC1B,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;AAAA,EACxB,UAAU;AACZ,CAAC;AAEM,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAClD,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC;AAChC,CAAC;AAEM,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACzD,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EAC9E,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EAC7E,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1D,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AACpD,CAAC;AAGM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU;AAAA,EACV,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
- import { resolveTranslationsRouteContext } from "@open-mercato/core/modules/translations/api/context";
3
+ import { resolveTranslationsRouteContext, requireTranslationFeatures } from "@open-mercato/core/modules/translations/api/context";
4
4
  import { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from "@open-mercato/core/modules/translations/data/validators";
5
5
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
6
  import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
@@ -16,6 +16,7 @@ const metadata = {
16
16
  async function GET(req, ctx) {
17
17
  try {
18
18
  const context = await resolveTranslationsRouteContext(req);
19
+ await requireTranslationFeatures(context, ["translations.view"]);
19
20
  const { entityType, entityId } = paramsSchema.parse({
20
21
  entityType: ctx.params?.entityType,
21
22
  entityId: ctx.params?.entityId
@@ -48,6 +49,7 @@ async function GET(req, ctx) {
48
49
  async function PUT(req, ctx) {
49
50
  try {
50
51
  const context = await resolveTranslationsRouteContext(req);
52
+ await requireTranslationFeatures(context, ["translations.manage"]);
51
53
  const { entityType, entityId } = paramsSchema.parse({
52
54
  entityType: ctx.params?.entityType,
53
55
  entityId: ctx.params?.entityId
@@ -102,6 +104,7 @@ async function PUT(req, ctx) {
102
104
  async function DELETE(req, ctx) {
103
105
  try {
104
106
  const context = await resolveTranslationsRouteContext(req);
107
+ await requireTranslationFeatures(context, ["translations.manage"]);
105
108
  const { entityType, entityId } = paramsSchema.parse({
106
109
  entityType: ctx.params?.entityType,
107
110
  entityId: ctx.params?.entityId
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/translations/api/%5BentityType%5D/%5BentityId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslationsRouteContext } from '@open-mercato/core/modules/translations/api/context'\nimport { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst paramsSchema = z.object({\n entityType: entityTypeParamSchema,\n entityId: entityIdParamSchema,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['translations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['translations.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['translations.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const row = await context.knex('entity_translations')\n .where({\n entity_type: entityType,\n entity_id: entityId,\n })\n .andWhereRaw('tenant_id is not distinct from ?', [context.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [context.organizationId])\n .first()\n\n if (!row) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const rawBody = await req.json().catch(() => ({}))\n const translations = translationBodySchema.parse(rawBody)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },\n { rowId: string }\n >('translations.translation.save', {\n input: {\n entityType,\n entityId,\n translations,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n const row = await context.knex('entity_translations')\n .where({ id: result.rowId })\n .first()\n\n const response = NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? result.rowId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.PUT] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; organizationId: string | null; tenantId: string },\n { deleted: boolean }\n >('translations.translation.delete', {\n input: {\n entityType,\n entityId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n const response = new NextResponse(null, { status: 204 })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst translationsTag = 'Translations'\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Get entity translations',\n description: 'Returns the full translation record for a single entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translation record found.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n { status: 404, description: 'No translations found for this entity' },\n ],\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Create or update entity translations',\n description: 'Full replacement of translations JSONB for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translations saved.' },\n ],\n errors: [\n { status: 400, description: 'Validation failed' },\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nconst deleteDoc: OpenApiMethodDoc = {\n summary: 'Delete entity translations',\n description: 'Removes all translations for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 204, description: 'Translations deleted.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: translationsTag,\n summary: 'Entity translation resource',\n pathParams: paramsSchema,\n methods: {\n GET: getDoc,\n PUT: putDoc,\n DELETE: deleteDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslationsRouteContext, requireTranslationFeatures } from '@open-mercato/core/modules/translations/api/context'\nimport { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst paramsSchema = z.object({\n entityType: entityTypeParamSchema,\n entityId: entityIdParamSchema,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['translations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['translations.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['translations.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.view'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const row = await context.knex('entity_translations')\n .where({\n entity_type: entityType,\n entity_id: entityId,\n })\n .andWhereRaw('tenant_id is not distinct from ?', [context.tenantId])\n .andWhereRaw('organization_id is not distinct from ?', [context.organizationId])\n .first()\n\n if (!row) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.manage'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const rawBody = await req.json().catch(() => ({}))\n const translations = translationBodySchema.parse(rawBody)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },\n { rowId: string }\n >('translations.translation.save', {\n input: {\n entityType,\n entityId,\n translations,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n const row = await context.knex('entity_translations')\n .where({ id: result.rowId })\n .first()\n\n const response = NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? result.rowId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.PUT] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.manage'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; organizationId: string | null; tenantId: string },\n { deleted: boolean }\n >('translations.translation.delete', {\n input: {\n entityType,\n entityId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n const response = new NextResponse(null, { status: 204 })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst translationsTag = 'Translations'\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Get entity translations',\n description: 'Returns the full translation record for a single entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translation record found.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n { status: 404, description: 'No translations found for this entity' },\n ],\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Create or update entity translations',\n description: 'Full replacement of translations JSONB for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translations saved.' },\n ],\n errors: [\n { status: 400, description: 'Validation failed' },\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nconst deleteDoc: OpenApiMethodDoc = {\n summary: 'Delete entity translations',\n description: 'Removes all translations for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 204, description: 'Translations deleted.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: translationsTag,\n summary: 'Entity translation resource',\n pathParams: paramsSchema,\n methods: {\n GET: getDoc,\n PUT: putDoc,\n DELETE: deleteDoc,\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,iCAAiC,kCAAkC;AAC5E,SAAS,uBAAuB,uBAAuB,2BAA2B;AAClF,SAAS,qBAAqB;AAE9B,SAAS,kCAAkC;AAG3C,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,YAAY;AAAA,EACZ,UAAU;AACZ,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EACjE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AAAA,EACnE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACxE;AAEA,eAAsB,IAAI,KAAc,KAA8D;AACpG,MAAI;AACF,UAAM,UAAU,MAAM,gCAAgC,GAAG;AACzD,UAAM,2BAA2B,SAAS,CAAC,mBAAmB,CAAC;AAC/D,UAAM,EAAE,YAAY,SAAS,IAAI,aAAa,MAAM;AAAA,MAClD,YAAY,IAAI,QAAQ;AAAA,MACxB,UAAU,IAAI,QAAQ;AAAA,IACxB,CAAC;AAED,UAAM,MAAM,MAAM,QAAQ,KAAK,qBAAqB,EACjD,MAAM;AAAA,MACL,aAAa;AAAA,MACb,WAAW;AAAA,IACb,CAAC,EACA,YAAY,oCAAoC,CAAC,QAAQ,QAAQ,CAAC,EAClE,YAAY,0CAA0C,CAAC,QAAQ,cAAc,CAAC,EAC9E,MAAM;AAET,QAAI,CAAC,KAAK;AACR,aAAO,aAAa,KAAK,EAAE,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClE;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,YAAY,IAAI;AAAA,MAChB,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,MAClB,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,eAAe,eAAe;AAChC,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,sBAAsB,SAAS,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChG;AACA,YAAQ,MAAM,6DAA6D,GAAG;AAC9E,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AACF;AAEA,eAAsB,IAAI,KAAc,KAA8D;AACpG,MAAI;AACF,UAAM,UAAU,MAAM,gCAAgC,GAAG;AACzD,UAAM,2BAA2B,SAAS,CAAC,qBAAqB,CAAC;AACjE,UAAM,EAAE,YAAY,SAAS,IAAI,aAAa,MAAM;AAAA,MAClD,YAAY,IAAI,QAAQ;AAAA,MACxB,UAAU,IAAI,QAAQ;AAAA,IACxB,CAAC;AAED,UAAM,UAAU,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACjD,UAAM,eAAe,sBAAsB,MAAM,OAAO;AAExD,UAAM,aAAa,QAAQ,UAAU,QAAQ,YAAY;AACzD,UAAM,EAAE,QAAQ,SAAS,IAAI,MAAM,WAAW,QAG5C,iCAAiC;AAAA,MACjC,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,gBAAgB,QAAQ;AAAA,QACxB,UAAU,QAAQ;AAAA,MACpB;AAAA,MACA,KAAK,QAAQ;AAAA,IACf,CAAC;AAED,UAAM,MAAM,MAAM,QAAQ,KAAK,qBAAqB,EACjD,MAAM,EAAE,IAAI,OAAO,MAAM,CAAC,EAC1B,MAAM;AAET,UAAM,WAAW,aAAa,KAAK;AAAA,MACjC,YAAY,IAAI;AAAA,MAChB,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,MAClB,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,IACjB,CAAC;AAED,QAAI,UAAU,aAAa,UAAU,MAAM,UAAU,WAAW;AAC9D,eAAS,QAAQ;AAAA,QACf;AAAA,QACA,2BAA2B;AAAA,UACzB,IAAI,SAAS;AAAA,UACb,WAAW,SAAS;AAAA,UACpB,WAAW,SAAS;AAAA,UACpB,aAAa,SAAS,eAAe;AAAA,UACrC,cAAc,SAAS,gBAAgB;AAAA,UACvC,YAAY,SAAS,cAAc,OAAO;AAAA,UAC1C,YAAY,SAAS,qBAAqB,OACtC,SAAS,UAAU,YAAY,IAC/B,OAAO,SAAS,cAAc,WAC5B,SAAS,aACT,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC/B,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,eAAe,eAAe;AAChC,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,SAAS,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC/F;AACA,YAAQ,MAAM,6DAA6D,GAAG;AAC9E,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AACF;AAEA,eAAsB,OAAO,KAAc,KAA8D;AACvG,MAAI;AACF,UAAM,UAAU,MAAM,gCAAgC,GAAG;AACzD,UAAM,2BAA2B,SAAS,CAAC,qBAAqB,CAAC;AACjE,UAAM,EAAE,YAAY,SAAS,IAAI,aAAa,MAAM;AAAA,MAClD,YAAY,IAAI,QAAQ;AAAA,MACxB,UAAU,IAAI,QAAQ;AAAA,IACxB,CAAC;AAED,UAAM,aAAa,QAAQ,UAAU,QAAQ,YAAY;AACzD,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW,QAGpC,mCAAmC;AAAA,MACnC,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,gBAAgB,QAAQ;AAAA,QACxB,UAAU,QAAQ;AAAA,MACpB;AAAA,MACA,KAAK,QAAQ;AAAA,IACf,CAAC;AAED,UAAM,WAAW,IAAI,aAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAEvD,QAAI,UAAU,aAAa,UAAU,MAAM,UAAU,WAAW;AAC9D,eAAS,QAAQ;AAAA,QACf;AAAA,QACA,2BAA2B;AAAA,UACzB,IAAI,SAAS;AAAA,UACb,WAAW,SAAS;AAAA,UACpB,WAAW,SAAS;AAAA,UACpB,aAAa,SAAS,eAAe;AAAA,UACrC,cAAc,SAAS,gBAAgB;AAAA,UACvC,YAAY,SAAS,cAAc;AAAA,UACnC,YAAY,SAAS,qBAAqB,OACtC,SAAS,UAAU,YAAY,IAC/B,OAAO,SAAS,cAAc,WAC5B,SAAS,aACT,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC/B,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,eAAe,eAAe;AAChC,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,sBAAsB,SAAS,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChG;AACA,YAAQ,MAAM,gEAAgE,GAAG;AACjF,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AACF;AAEA,MAAM,kBAAkB;AAExB,MAAM,SAA2B;AAAA,EAC/B,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,eAAe;AAAA,EACtB,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,4BAA4B;AAAA,EAC1D;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,IACtD,EAAE,QAAQ,KAAK,aAAa,wCAAwC;AAAA,EACtE;AACF;AAEA,MAAM,SAA2B;AAAA,EAC/B,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,eAAe;AAAA,EACtB,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,sBAAsB;AAAA,EACpD;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,IAChD,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,EACxD;AACF;AAEA,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,eAAe;AAAA,EACtB,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,EACtD;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,EACxD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,SAAS;AAAA,IACP,KAAK;AAAA,IACL,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AACF;",
6
6
  "names": []
7
7
  }
@@ -31,7 +31,23 @@ async function resolveTranslationsRouteContext(req) {
31
31
  commandCtx
32
32
  };
33
33
  }
34
+ async function requireTranslationFeatures(context, requiredFeatures) {
35
+ if (!requiredFeatures.length) return;
36
+ const subject = context.auth.sub;
37
+ if (!subject) {
38
+ throw new CrudHttpError(401, { error: "Unauthorized" });
39
+ }
40
+ const rbacService = context.container.resolve("rbacService");
41
+ const hasFeatures = await rbacService.userHasAllFeatures(subject, requiredFeatures, {
42
+ tenantId: context.tenantId,
43
+ organizationId: context.organizationId
44
+ });
45
+ if (!hasFeatures) {
46
+ throw new CrudHttpError(403, { error: "Forbidden" });
47
+ }
48
+ }
34
49
  export {
50
+ requireTranslationFeatures,
35
51
  resolveTranslationsRouteContext
36
52
  };
37
53
  //# sourceMappingURL=context.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/translations/api/context.ts"],
4
- "sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { Knex } from 'knex'\n\nexport type TranslationsRouteContext = {\n container: AwilixContainer\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n em: EntityManager\n knex: Knex\n organizationId: string | null\n tenantId: string\n commandCtx: CommandRuntimeContext\n}\n\nexport async function resolveTranslationsRouteContext(req: Request): Promise<TranslationsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = container.resolve('em') as EntityManager\n const knex = (em as unknown as { getConnection(): { getKnex(): Knex } }).getConnection().getKnex()\n const tenantId: string = scope?.tenantId ?? auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n const commandCtx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return {\n container,\n auth,\n em,\n knex,\n organizationId,\n tenantId,\n commandCtx,\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,qBAAqB;AAgB9B,eAAsB,gCAAgC,KAAiD;AACrG,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,EACxD;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,OAAQ,GAA2D,cAAc,EAAE,QAAQ;AACjG,QAAM,WAAmB,OAAO,YAAY,KAAK;AACjD,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAE1D,QAAM,aAAoC;AAAA,IACxC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { Knex } from 'knex'\n\nexport type TranslationsRouteContext = {\n container: AwilixContainer\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n em: EntityManager\n knex: Knex\n organizationId: string | null\n tenantId: string\n commandCtx: CommandRuntimeContext\n}\n\nexport async function resolveTranslationsRouteContext(req: Request): Promise<TranslationsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = container.resolve('em') as EntityManager\n const knex = (em as unknown as { getConnection(): { getKnex(): Knex } }).getConnection().getKnex()\n const tenantId: string = scope?.tenantId ?? auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n const commandCtx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return {\n container,\n auth,\n em,\n knex,\n organizationId,\n tenantId,\n commandCtx,\n }\n}\n\nexport async function requireTranslationFeatures(\n context: TranslationsRouteContext,\n requiredFeatures: string[],\n): Promise<void> {\n if (!requiredFeatures.length) return\n const subject = context.auth.sub\n if (!subject) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n const rbacService = context.container.resolve('rbacService') as {\n userHasAllFeatures(\n userId: string,\n required: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ): Promise<boolean>\n }\n const hasFeatures = await rbacService.userHasAllFeatures(subject, requiredFeatures, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n if (!hasFeatures) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,qBAAqB;AAgB9B,eAAsB,gCAAgC,KAAiD;AACrG,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,EACxD;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,OAAQ,GAA2D,cAAc,EAAE,QAAQ;AACjG,QAAM,WAAmB,OAAO,YAAY,KAAK;AACjD,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAE1D,QAAM,aAAoC;AAAA,IACxC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAsB,2BACpB,SACA,kBACe;AACf,MAAI,CAAC,iBAAiB,OAAQ;AAC9B,QAAM,UAAU,QAAQ,KAAK;AAC7B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAAA,EACxD;AACA,QAAM,cAAc,QAAQ,UAAU,QAAQ,aAAa;AAO3D,QAAM,cAAc,MAAM,YAAY,mBAAmB,SAAS,kBAAkB;AAAA,IAClF,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,EAC1B,CAAC;AACD,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,EACrD;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.5-develop-0c30cb4b11",
3
+ "version": "0.4.5-develop-974adb54b3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.5-develop-0c30cb4b11",
210
+ "@open-mercato/shared": "0.4.5-develop-974adb54b3",
211
211
  "@types/semver": "^7.5.8",
212
212
  "@xyflow/react": "^12.6.0",
213
213
  "ai": "^6.0.0",
@@ -4,8 +4,20 @@ import { toAbsoluteUrl } from '@open-mercato/shared/lib/url'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import { AuthService } from '@open-mercato/core/modules/auth/services/authService'
6
6
  import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
+ import { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'
9
+ import { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'
10
+ import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
11
+ import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
7
12
  import { z } from 'zod'
8
13
 
14
+ const refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {
15
+ points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',
16
+ })
17
+ const refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {
18
+ points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',
19
+ })
20
+
9
21
  function parseCookie(req: Request, name: string): string | null {
10
22
  const cookie = req.headers.get('cookie') || ''
11
23
  const m = cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]+)'))
@@ -42,11 +54,12 @@ export async function GET(req: Request) {
42
54
  }
43
55
 
44
56
  export async function POST(req: Request) {
57
+ const { translate } = await resolveTranslations()
45
58
  let token: string | null = null
46
59
 
47
60
  try {
48
61
  const body = await req.json()
49
- const parsed = refreshRequestSchema.safeParse(body)
62
+ const parsed = refreshSessionRequestSchema.safeParse(body)
50
63
  if (parsed.success) {
51
64
  token = parsed.data.refreshToken
52
65
  }
@@ -54,8 +67,19 @@ export async function POST(req: Request) {
54
67
  // Invalid JSON
55
68
  }
56
69
 
70
+ const { error: rateLimitError } = await checkAuthRateLimit({
71
+ req,
72
+ ipConfig: refreshIpRateLimitConfig,
73
+ compoundConfig: refreshRateLimitConfig,
74
+ compoundIdentifier: token ?? undefined,
75
+ })
76
+ if (rateLimitError) return rateLimitError
77
+
57
78
  if (!token) {
58
- return NextResponse.json({ ok: false, error: 'Missing or invalid refresh token' }, { status: 400 })
79
+ return NextResponse.json({
80
+ ok: false,
81
+ error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),
82
+ }, { status: 400 })
59
83
  }
60
84
 
61
85
  const c = await createRequestContainer()
@@ -63,7 +87,10 @@ export async function POST(req: Request) {
63
87
  const ctx = await auth.refreshFromSessionToken(token)
64
88
 
65
89
  if (!ctx) {
66
- return NextResponse.json({ ok: false, error: 'Invalid or expired refresh token' }, { status: 401 })
90
+ return NextResponse.json({
91
+ ok: false,
92
+ error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),
93
+ }, { status: 401 })
67
94
  }
68
95
 
69
96
  const { user, roles } = ctx
@@ -101,10 +128,6 @@ const refreshQuerySchema = z.object({
101
128
  redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),
102
129
  })
103
130
 
104
- const refreshRequestSchema = z.object({
105
- refreshToken: z.string().min(1).describe('The refresh token obtained from login'),
106
- })
107
-
108
131
  const refreshSuccessSchema = z.object({
109
132
  ok: z.literal(true),
110
133
  accessToken: z.string().describe('New JWT access token'),
@@ -131,13 +154,14 @@ export const openApi: OpenApiRouteDoc = {
131
154
  POST: {
132
155
  summary: 'Refresh access token (API/mobile)',
133
156
  description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',
134
- requestBody: { schema: refreshRequestSchema, contentType: 'application/json' },
157
+ requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },
135
158
  responses: [
136
159
  { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },
137
160
  ],
138
161
  errors: [
139
162
  { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },
140
163
  { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },
164
+ { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },
141
165
  ],
142
166
  },
143
167
  },
@@ -20,6 +20,10 @@ export const confirmPasswordResetSchema = z.object({
20
20
  password: passwordSchema,
21
21
  })
22
22
 
23
+ export const refreshSessionRequestSchema = z.object({
24
+ refreshToken: z.string().min(1),
25
+ })
26
+
23
27
  export const sidebarPreferencesInputSchema = z.object({
24
28
  version: z.number().int().positive().optional(),
25
29
  groupOrder: z.array(z.string().min(1)).max(200).optional(),
@@ -42,5 +46,6 @@ export const userCreateSchema = z.object({
42
46
  export type UserLoginInput = z.infer<typeof userLoginSchema>
43
47
  export type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>
44
48
  export type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>
49
+ export type RefreshSessionRequestInput = z.infer<typeof refreshSessionRequestSchema>
45
50
  export type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>
46
51
  export type UserCreateInput = z.infer<typeof userCreateSchema>
@@ -115,6 +115,8 @@
115
115
  "auth.roles.list.title": "Rollen",
116
116
  "auth.roles.superadmin": "Superadministrator",
117
117
  "auth.sendResetLink": "Link zum Zurücksetzen senden",
118
+ "auth.session.refresh.errors.invalidPayload": "Fehlendes oder ungültiges Refresh-Token",
119
+ "auth.session.refresh.errors.invalidToken": "Ungültiges oder abgelaufenes Refresh-Token",
118
120
  "auth.signIn": "Anmelden",
119
121
  "auth.users.errors.emailExists": "E-Mail-Adresse wird bereits verwendet",
120
122
  "auth.users.flash.created": "Benutzer erstellt",
@@ -115,6 +115,8 @@
115
115
  "auth.roles.list.title": "Roles",
116
116
  "auth.roles.superadmin": "Super Admin",
117
117
  "auth.sendResetLink": "Send reset link",
118
+ "auth.session.refresh.errors.invalidPayload": "Missing or invalid refresh token",
119
+ "auth.session.refresh.errors.invalidToken": "Invalid or expired refresh token",
118
120
  "auth.signIn": "Sign in",
119
121
  "auth.users.errors.emailExists": "Email already in use",
120
122
  "auth.users.flash.created": "User created",
@@ -115,6 +115,8 @@
115
115
  "auth.roles.list.title": "Roles",
116
116
  "auth.roles.superadmin": "Superadministrador",
117
117
  "auth.sendResetLink": "Enviar enlace de restablecimiento",
118
+ "auth.session.refresh.errors.invalidPayload": "Falta el token de actualización o no es válido",
119
+ "auth.session.refresh.errors.invalidToken": "El token de actualización no es válido o ha expirado",
118
120
  "auth.signIn": "Iniciar sesión",
119
121
  "auth.users.errors.emailExists": "El correo ya está en uso",
120
122
  "auth.users.flash.created": "Usuario creado",
@@ -115,6 +115,8 @@
115
115
  "auth.roles.list.title": "Role",
116
116
  "auth.roles.superadmin": "Superadministrator",
117
117
  "auth.sendResetLink": "Wyślij link resetujący",
118
+ "auth.session.refresh.errors.invalidPayload": "Brak tokenu odświeżania lub token jest nieprawidłowy",
119
+ "auth.session.refresh.errors.invalidToken": "Token odświeżania jest nieprawidłowy lub wygasł",
118
120
  "auth.signIn": "Zaloguj się",
119
121
  "auth.users.errors.emailExists": "Adres e-mail jest już używany",
120
122
  "auth.users.flash.created": "Użytkownik utworzony",
@@ -921,6 +921,68 @@
921
921
  "customers.people.list.noValue": "Nicht gesetzt",
922
922
  "customers.people.list.searchPlaceholder": "Personen durchsuchen",
923
923
  "customers.people.list.title": "Personen",
924
+ "customers.pipelines.actions.create": "Add pipeline",
925
+ "customers.pipelines.actions.delete": "Delete",
926
+ "customers.pipelines.actions.edit": "Edit",
927
+ "customers.pipelines.actions.hideStages": "Hide stages",
928
+ "customers.pipelines.actions.manageStages": "Manage stages",
929
+ "customers.pipelines.confirm.deleteConfirm": "Delete",
930
+ "customers.pipelines.confirm.deleteDesc": "Are you sure you want to delete this pipeline? This cannot be undone.",
931
+ "customers.pipelines.confirm.deleteTitle": "Delete pipeline",
932
+ "customers.pipelines.confirm.stageDeleteConfirm": "Delete",
933
+ "customers.pipelines.confirm.stageDeleteDesc": "Are you sure you want to delete this stage?",
934
+ "customers.pipelines.confirm.stageDeleteTitle": "Delete stage",
935
+ "customers.pipelines.defaultBadge": "Default",
936
+ "customers.pipelines.description": "Manage sales pipelines and their stages.",
937
+ "customers.pipelines.dialog.cancel": "Cancel",
938
+ "customers.pipelines.dialog.createTitle": "Create pipeline",
939
+ "customers.pipelines.dialog.editTitle": "Edit pipeline",
940
+ "customers.pipelines.dialog.save": "Save",
941
+ "customers.pipelines.empty": "No pipelines yet. Create one to get started.",
942
+ "customers.pipelines.errors.createFailed": "Failed to create pipeline",
943
+ "customers.pipelines.errors.deleteFailed": "Failed to delete pipeline",
944
+ "customers.pipelines.errors.loadFailed": "Failed to load pipelines",
945
+ "customers.pipelines.errors.reorderFailed": "Failed to reorder stages",
946
+ "customers.pipelines.errors.stageCreateFailed": "Failed to create stage",
947
+ "customers.pipelines.errors.stageDeleteFailed": "Failed to delete stage",
948
+ "customers.pipelines.errors.stageUpdateFailed": "Failed to update stage",
949
+ "customers.pipelines.errors.stagesLoadFailed": "Failed to load stages",
950
+ "customers.pipelines.errors.updateFailed": "Failed to update pipeline",
951
+ "customers.pipelines.flash.created": "Pipeline created",
952
+ "customers.pipelines.flash.deleted": "Pipeline deleted",
953
+ "customers.pipelines.flash.stageCreated": "Stage created",
954
+ "customers.pipelines.flash.stageDeleted": "Stage deleted",
955
+ "customers.pipelines.flash.stageUpdated": "Stage updated",
956
+ "customers.pipelines.flash.updated": "Pipeline updated",
957
+ "customers.pipelines.form.isDefault": "Set as default pipeline",
958
+ "customers.pipelines.form.name": "Name",
959
+ "customers.pipelines.form.namePlaceholder": "e.g. New Business",
960
+ "customers.pipelines.loading": "Loading pipelines…",
961
+ "customers.pipelines.stageDialog.cancel": "Cancel",
962
+ "customers.pipelines.stageDialog.createTitle": "Add stage",
963
+ "customers.pipelines.stageDialog.editTitle": "Edit stage",
964
+ "customers.pipelines.stageDialog.save": "Save",
965
+ "customers.pipelines.stageForm.color": "Color",
966
+ "customers.pipelines.stageForm.colorClear": "Remove color",
967
+ "customers.pipelines.stageForm.icon": "Icon",
968
+ "customers.pipelines.stageForm.iconClear": "Remove icon",
969
+ "customers.pipelines.stageForm.iconPicker": "Pick icon",
970
+ "customers.pipelines.stageForm.iconPlaceholder": "e.g. lucide:star",
971
+ "customers.pipelines.stageForm.iconSearch": "Search icons…",
972
+ "customers.pipelines.stageForm.iconSearchEmpty": "No icons found",
973
+ "customers.pipelines.stageForm.iconSuggestions": "Suggestions",
974
+ "customers.pipelines.stageForm.label": "Label",
975
+ "customers.pipelines.stageForm.labelPlaceholder": "e.g. Discovery",
976
+ "customers.pipelines.stageForm.previewEmpty": "No appearance set",
977
+ "customers.pipelines.stages.add": "Add stage",
978
+ "customers.pipelines.stages.delete": "Delete",
979
+ "customers.pipelines.stages.edit": "Edit",
980
+ "customers.pipelines.stages.empty": "No stages yet.",
981
+ "customers.pipelines.stages.loading": "Loading…",
982
+ "customers.pipelines.stages.moveDown": "Move down",
983
+ "customers.pipelines.stages.moveUp": "Move up",
984
+ "customers.pipelines.stages.title": "Stages",
985
+ "customers.pipelines.title": "Sales Pipelines",
924
986
  "customers.storage.nav.group": "Speicher",
925
987
  "customers.widgets.common.unknown": "Unbekannt",
926
988
  "customers.widgets.common.unknownDate": "Datum nicht verfügbar",
@@ -921,6 +921,68 @@
921
921
  "customers.people.list.noValue": "Not set",
922
922
  "customers.people.list.searchPlaceholder": "Search people",
923
923
  "customers.people.list.title": "People",
924
+ "customers.pipelines.actions.create": "Add pipeline",
925
+ "customers.pipelines.actions.delete": "Delete",
926
+ "customers.pipelines.actions.edit": "Edit",
927
+ "customers.pipelines.actions.hideStages": "Hide stages",
928
+ "customers.pipelines.actions.manageStages": "Manage stages",
929
+ "customers.pipelines.confirm.deleteConfirm": "Delete",
930
+ "customers.pipelines.confirm.deleteDesc": "Are you sure you want to delete this pipeline? This cannot be undone.",
931
+ "customers.pipelines.confirm.deleteTitle": "Delete pipeline",
932
+ "customers.pipelines.confirm.stageDeleteConfirm": "Delete",
933
+ "customers.pipelines.confirm.stageDeleteDesc": "Are you sure you want to delete this stage?",
934
+ "customers.pipelines.confirm.stageDeleteTitle": "Delete stage",
935
+ "customers.pipelines.defaultBadge": "Default",
936
+ "customers.pipelines.description": "Manage sales pipelines and their stages.",
937
+ "customers.pipelines.dialog.cancel": "Cancel",
938
+ "customers.pipelines.dialog.createTitle": "Create pipeline",
939
+ "customers.pipelines.dialog.editTitle": "Edit pipeline",
940
+ "customers.pipelines.dialog.save": "Save",
941
+ "customers.pipelines.empty": "No pipelines yet. Create one to get started.",
942
+ "customers.pipelines.errors.createFailed": "Failed to create pipeline",
943
+ "customers.pipelines.errors.deleteFailed": "Failed to delete pipeline",
944
+ "customers.pipelines.errors.loadFailed": "Failed to load pipelines",
945
+ "customers.pipelines.errors.reorderFailed": "Failed to reorder stages",
946
+ "customers.pipelines.errors.stageCreateFailed": "Failed to create stage",
947
+ "customers.pipelines.errors.stageDeleteFailed": "Failed to delete stage",
948
+ "customers.pipelines.errors.stageUpdateFailed": "Failed to update stage",
949
+ "customers.pipelines.errors.stagesLoadFailed": "Failed to load stages",
950
+ "customers.pipelines.errors.updateFailed": "Failed to update pipeline",
951
+ "customers.pipelines.flash.created": "Pipeline created",
952
+ "customers.pipelines.flash.deleted": "Pipeline deleted",
953
+ "customers.pipelines.flash.stageCreated": "Stage created",
954
+ "customers.pipelines.flash.stageDeleted": "Stage deleted",
955
+ "customers.pipelines.flash.stageUpdated": "Stage updated",
956
+ "customers.pipelines.flash.updated": "Pipeline updated",
957
+ "customers.pipelines.form.isDefault": "Set as default pipeline",
958
+ "customers.pipelines.form.name": "Name",
959
+ "customers.pipelines.form.namePlaceholder": "e.g. New Business",
960
+ "customers.pipelines.loading": "Loading pipelines…",
961
+ "customers.pipelines.stageDialog.cancel": "Cancel",
962
+ "customers.pipelines.stageDialog.createTitle": "Add stage",
963
+ "customers.pipelines.stageDialog.editTitle": "Edit stage",
964
+ "customers.pipelines.stageDialog.save": "Save",
965
+ "customers.pipelines.stageForm.color": "Color",
966
+ "customers.pipelines.stageForm.colorClear": "Remove color",
967
+ "customers.pipelines.stageForm.icon": "Icon",
968
+ "customers.pipelines.stageForm.iconClear": "Remove icon",
969
+ "customers.pipelines.stageForm.iconPicker": "Pick icon",
970
+ "customers.pipelines.stageForm.iconPlaceholder": "e.g. lucide:star",
971
+ "customers.pipelines.stageForm.iconSearch": "Search icons…",
972
+ "customers.pipelines.stageForm.iconSearchEmpty": "No icons found",
973
+ "customers.pipelines.stageForm.iconSuggestions": "Suggestions",
974
+ "customers.pipelines.stageForm.label": "Label",
975
+ "customers.pipelines.stageForm.labelPlaceholder": "e.g. Discovery",
976
+ "customers.pipelines.stageForm.previewEmpty": "No appearance set",
977
+ "customers.pipelines.stages.add": "Add stage",
978
+ "customers.pipelines.stages.delete": "Delete",
979
+ "customers.pipelines.stages.edit": "Edit",
980
+ "customers.pipelines.stages.empty": "No stages yet.",
981
+ "customers.pipelines.stages.loading": "Loading…",
982
+ "customers.pipelines.stages.moveDown": "Move down",
983
+ "customers.pipelines.stages.moveUp": "Move up",
984
+ "customers.pipelines.stages.title": "Stages",
985
+ "customers.pipelines.title": "Sales Pipelines",
924
986
  "customers.storage.nav.group": "Storage",
925
987
  "customers.widgets.common.unknown": "Unknown",
926
988
  "customers.widgets.common.unknownDate": "Date unavailable",
@@ -921,6 +921,68 @@
921
921
  "customers.people.list.noValue": "Sin datos",
922
922
  "customers.people.list.searchPlaceholder": "Buscar personas",
923
923
  "customers.people.list.title": "Personas",
924
+ "customers.pipelines.actions.create": "Add pipeline",
925
+ "customers.pipelines.actions.delete": "Delete",
926
+ "customers.pipelines.actions.edit": "Edit",
927
+ "customers.pipelines.actions.hideStages": "Hide stages",
928
+ "customers.pipelines.actions.manageStages": "Manage stages",
929
+ "customers.pipelines.confirm.deleteConfirm": "Delete",
930
+ "customers.pipelines.confirm.deleteDesc": "Are you sure you want to delete this pipeline? This cannot be undone.",
931
+ "customers.pipelines.confirm.deleteTitle": "Delete pipeline",
932
+ "customers.pipelines.confirm.stageDeleteConfirm": "Delete",
933
+ "customers.pipelines.confirm.stageDeleteDesc": "Are you sure you want to delete this stage?",
934
+ "customers.pipelines.confirm.stageDeleteTitle": "Delete stage",
935
+ "customers.pipelines.defaultBadge": "Default",
936
+ "customers.pipelines.description": "Manage sales pipelines and their stages.",
937
+ "customers.pipelines.dialog.cancel": "Cancel",
938
+ "customers.pipelines.dialog.createTitle": "Create pipeline",
939
+ "customers.pipelines.dialog.editTitle": "Edit pipeline",
940
+ "customers.pipelines.dialog.save": "Save",
941
+ "customers.pipelines.empty": "No pipelines yet. Create one to get started.",
942
+ "customers.pipelines.errors.createFailed": "Failed to create pipeline",
943
+ "customers.pipelines.errors.deleteFailed": "Failed to delete pipeline",
944
+ "customers.pipelines.errors.loadFailed": "Failed to load pipelines",
945
+ "customers.pipelines.errors.reorderFailed": "Failed to reorder stages",
946
+ "customers.pipelines.errors.stageCreateFailed": "Failed to create stage",
947
+ "customers.pipelines.errors.stageDeleteFailed": "Failed to delete stage",
948
+ "customers.pipelines.errors.stageUpdateFailed": "Failed to update stage",
949
+ "customers.pipelines.errors.stagesLoadFailed": "Failed to load stages",
950
+ "customers.pipelines.errors.updateFailed": "Failed to update pipeline",
951
+ "customers.pipelines.flash.created": "Pipeline created",
952
+ "customers.pipelines.flash.deleted": "Pipeline deleted",
953
+ "customers.pipelines.flash.stageCreated": "Stage created",
954
+ "customers.pipelines.flash.stageDeleted": "Stage deleted",
955
+ "customers.pipelines.flash.stageUpdated": "Stage updated",
956
+ "customers.pipelines.flash.updated": "Pipeline updated",
957
+ "customers.pipelines.form.isDefault": "Set as default pipeline",
958
+ "customers.pipelines.form.name": "Name",
959
+ "customers.pipelines.form.namePlaceholder": "e.g. New Business",
960
+ "customers.pipelines.loading": "Loading pipelines…",
961
+ "customers.pipelines.stageDialog.cancel": "Cancel",
962
+ "customers.pipelines.stageDialog.createTitle": "Add stage",
963
+ "customers.pipelines.stageDialog.editTitle": "Edit stage",
964
+ "customers.pipelines.stageDialog.save": "Save",
965
+ "customers.pipelines.stageForm.color": "Color",
966
+ "customers.pipelines.stageForm.colorClear": "Remove color",
967
+ "customers.pipelines.stageForm.icon": "Icon",
968
+ "customers.pipelines.stageForm.iconClear": "Remove icon",
969
+ "customers.pipelines.stageForm.iconPicker": "Pick icon",
970
+ "customers.pipelines.stageForm.iconPlaceholder": "e.g. lucide:star",
971
+ "customers.pipelines.stageForm.iconSearch": "Search icons…",
972
+ "customers.pipelines.stageForm.iconSearchEmpty": "No icons found",
973
+ "customers.pipelines.stageForm.iconSuggestions": "Suggestions",
974
+ "customers.pipelines.stageForm.label": "Label",
975
+ "customers.pipelines.stageForm.labelPlaceholder": "e.g. Discovery",
976
+ "customers.pipelines.stageForm.previewEmpty": "No appearance set",
977
+ "customers.pipelines.stages.add": "Add stage",
978
+ "customers.pipelines.stages.delete": "Delete",
979
+ "customers.pipelines.stages.edit": "Edit",
980
+ "customers.pipelines.stages.empty": "No stages yet.",
981
+ "customers.pipelines.stages.loading": "Loading…",
982
+ "customers.pipelines.stages.moveDown": "Move down",
983
+ "customers.pipelines.stages.moveUp": "Move up",
984
+ "customers.pipelines.stages.title": "Stages",
985
+ "customers.pipelines.title": "Sales Pipelines",
924
986
  "customers.storage.nav.group": "Almacenamiento",
925
987
  "customers.widgets.common.unknown": "Desconocido",
926
988
  "customers.widgets.common.unknownDate": "Fecha no disponible",
@@ -921,6 +921,68 @@
921
921
  "customers.people.list.noValue": "Brak",
922
922
  "customers.people.list.searchPlaceholder": "Szukaj osób",
923
923
  "customers.people.list.title": "Osoby",
924
+ "customers.pipelines.actions.create": "Dodaj lejek",
925
+ "customers.pipelines.actions.delete": "Usuń",
926
+ "customers.pipelines.actions.edit": "Edytuj",
927
+ "customers.pipelines.actions.hideStages": "Ukryj etapy",
928
+ "customers.pipelines.actions.manageStages": "Zarządzaj etapami",
929
+ "customers.pipelines.confirm.deleteConfirm": "Usuń",
930
+ "customers.pipelines.confirm.deleteDesc": "Czy na pewno chcesz usunąć ten lejek? Tej operacji nie można cofnąć.",
931
+ "customers.pipelines.confirm.deleteTitle": "Usunąć lejek",
932
+ "customers.pipelines.confirm.stageDeleteConfirm": "Usuń",
933
+ "customers.pipelines.confirm.stageDeleteDesc": "Czy na pewno chcesz usunąć ten etap?",
934
+ "customers.pipelines.confirm.stageDeleteTitle": "Usunąć etap",
935
+ "customers.pipelines.defaultBadge": "Domyślny",
936
+ "customers.pipelines.description": "Zarządzaj lejkami sprzedażowymi i ich etapami.",
937
+ "customers.pipelines.dialog.cancel": "Anuluj",
938
+ "customers.pipelines.dialog.createTitle": "Utwórz lejek",
939
+ "customers.pipelines.dialog.editTitle": "Edytuj lejek",
940
+ "customers.pipelines.dialog.save": "Zapisz",
941
+ "customers.pipelines.empty": "Brak lejków. Utwórz pierwszy, aby rozpocząć.",
942
+ "customers.pipelines.errors.createFailed": "Nie udało się utworzyć lejka",
943
+ "customers.pipelines.errors.deleteFailed": "Nie udało się usunąć lejka",
944
+ "customers.pipelines.errors.loadFailed": "Nie udało się wczytać lejków",
945
+ "customers.pipelines.errors.reorderFailed": "Nie udało się zmienić kolejności etapów",
946
+ "customers.pipelines.errors.stageCreateFailed": "Nie udało się utworzyć etapu",
947
+ "customers.pipelines.errors.stageDeleteFailed": "Nie udało się usunąć etapu",
948
+ "customers.pipelines.errors.stageUpdateFailed": "Nie udało się zaktualizować etapu",
949
+ "customers.pipelines.errors.stagesLoadFailed": "Nie udało się wczytać etapów",
950
+ "customers.pipelines.errors.updateFailed": "Nie udało się zaktualizować lejka",
951
+ "customers.pipelines.flash.created": "Utworzono lejek",
952
+ "customers.pipelines.flash.deleted": "Usunięto lejek",
953
+ "customers.pipelines.flash.stageCreated": "Utworzono etap",
954
+ "customers.pipelines.flash.stageDeleted": "Usunięto etap",
955
+ "customers.pipelines.flash.stageUpdated": "Zaktualizowano etap",
956
+ "customers.pipelines.flash.updated": "Zaktualizowano lejek",
957
+ "customers.pipelines.form.isDefault": "Ustaw jako domyślny lejek",
958
+ "customers.pipelines.form.name": "Nazwa",
959
+ "customers.pipelines.form.namePlaceholder": "np. Nowy biznes",
960
+ "customers.pipelines.loading": "Wczytywanie lejków…",
961
+ "customers.pipelines.stageDialog.cancel": "Anuluj",
962
+ "customers.pipelines.stageDialog.createTitle": "Dodaj etap",
963
+ "customers.pipelines.stageDialog.editTitle": "Edytuj etap",
964
+ "customers.pipelines.stageDialog.save": "Zapisz",
965
+ "customers.pipelines.stageForm.color": "Kolor",
966
+ "customers.pipelines.stageForm.colorClear": "Usuń kolor",
967
+ "customers.pipelines.stageForm.icon": "Ikona",
968
+ "customers.pipelines.stageForm.iconClear": "Usuń ikonę",
969
+ "customers.pipelines.stageForm.iconPicker": "Wybierz ikonę",
970
+ "customers.pipelines.stageForm.iconPlaceholder": "np. lucide:star",
971
+ "customers.pipelines.stageForm.iconSearch": "Szukaj ikon…",
972
+ "customers.pipelines.stageForm.iconSearchEmpty": "Nie znaleziono ikon",
973
+ "customers.pipelines.stageForm.iconSuggestions": "Sugestie",
974
+ "customers.pipelines.stageForm.label": "Nazwa etapu",
975
+ "customers.pipelines.stageForm.labelPlaceholder": "np. Kwalifikacja",
976
+ "customers.pipelines.stageForm.previewEmpty": "Brak ustawionego wyglądu",
977
+ "customers.pipelines.stages.add": "Dodaj etap",
978
+ "customers.pipelines.stages.delete": "Usuń",
979
+ "customers.pipelines.stages.edit": "Edytuj",
980
+ "customers.pipelines.stages.empty": "Brak etapów.",
981
+ "customers.pipelines.stages.loading": "Wczytywanie…",
982
+ "customers.pipelines.stages.moveDown": "Przenieś w dół",
983
+ "customers.pipelines.stages.moveUp": "Przenieś w górę",
984
+ "customers.pipelines.stages.title": "Etapy",
985
+ "customers.pipelines.title": "Lejki sprzedażowe",
924
986
  "customers.storage.nav.group": "Magazyn",
925
987
  "customers.widgets.common.unknown": "Nieznane",
926
988
  "customers.widgets.common.unknownDate": "Brak daty",
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
- import { resolveTranslationsRouteContext } from '@open-mercato/core/modules/translations/api/context'
3
+ import { resolveTranslationsRouteContext, requireTranslationFeatures } from '@open-mercato/core/modules/translations/api/context'
4
4
  import { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'
5
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
6
  import { CommandBus } from '@open-mercato/shared/lib/commands'
@@ -21,6 +21,7 @@ export const metadata = {
21
21
  export async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {
22
22
  try {
23
23
  const context = await resolveTranslationsRouteContext(req)
24
+ await requireTranslationFeatures(context, ['translations.view'])
24
25
  const { entityType, entityId } = paramsSchema.parse({
25
26
  entityType: ctx.params?.entityType,
26
27
  entityId: ctx.params?.entityId,
@@ -61,6 +62,7 @@ export async function GET(req: Request, ctx: { params?: { entityType?: string; e
61
62
  export async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {
62
63
  try {
63
64
  const context = await resolveTranslationsRouteContext(req)
65
+ await requireTranslationFeatures(context, ['translations.manage'])
64
66
  const { entityType, entityId } = paramsSchema.parse({
65
67
  entityType: ctx.params?.entityType,
66
68
  entityId: ctx.params?.entityId,
@@ -131,6 +133,7 @@ export async function PUT(req: Request, ctx: { params?: { entityType?: string; e
131
133
  export async function DELETE(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {
132
134
  try {
133
135
  const context = await resolveTranslationsRouteContext(req)
136
+ await requireTranslationFeatures(context, ['translations.manage'])
134
137
  const { entityType, entityId } = paramsSchema.parse({
135
138
  entityType: ctx.params?.entityType,
136
139
  entityId: ctx.params?.entityId,
@@ -49,3 +49,28 @@ export async function resolveTranslationsRouteContext(req: Request): Promise<Tra
49
49
  commandCtx,
50
50
  }
51
51
  }
52
+
53
+ export async function requireTranslationFeatures(
54
+ context: TranslationsRouteContext,
55
+ requiredFeatures: string[],
56
+ ): Promise<void> {
57
+ if (!requiredFeatures.length) return
58
+ const subject = context.auth.sub
59
+ if (!subject) {
60
+ throw new CrudHttpError(401, { error: 'Unauthorized' })
61
+ }
62
+ const rbacService = context.container.resolve('rbacService') as {
63
+ userHasAllFeatures(
64
+ userId: string,
65
+ required: string[],
66
+ scope: { tenantId: string | null; organizationId: string | null },
67
+ ): Promise<boolean>
68
+ }
69
+ const hasFeatures = await rbacService.userHasAllFeatures(subject, requiredFeatures, {
70
+ tenantId: context.tenantId,
71
+ organizationId: context.organizationId,
72
+ })
73
+ if (!hasFeatures) {
74
+ throw new CrudHttpError(403, { error: 'Forbidden' })
75
+ }
76
+ }