@open-mercato/core 0.6.6-develop.6331.1.a33b8e99b7 → 0.6.6-develop.6332.1.6e73f0f55b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/auth/api/logout.js +2 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +5 -6
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/lib/requestRedirect.js +31 -3
- package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +2 -3
- package/src/modules/auth/api/session/refresh.ts +5 -6
- package/src/modules/auth/lib/requestRedirect.ts +44 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
1
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
2
|
import { verifyJwt } from "@open-mercato/shared/lib/auth/jwt";
|
|
4
|
-
import {
|
|
3
|
+
import { buildSafeRedirectResponse } from "@open-mercato/core/modules/auth/lib/requestRedirect";
|
|
5
4
|
function parseCookie(req, name) {
|
|
6
5
|
const cookie = req.headers.get("cookie") || "";
|
|
7
6
|
const m = cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
|
|
@@ -35,7 +34,7 @@ async function POST(req) {
|
|
|
35
34
|
} catch {
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
|
-
const res =
|
|
37
|
+
const res = buildSafeRedirectResponse(req, "/login");
|
|
39
38
|
res.cookies.set("auth_token", "", { path: "/", maxAge: 0 });
|
|
40
39
|
res.cookies.set("session_token", "", { path: "/", maxAge: 0 });
|
|
41
40
|
return res;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/api/logout.ts"],
|
|
4
|
-
"sourcesContent": ["import
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'\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 extractSessionIdFromAuthToken(token: string | null): string | null {\n if (!token) return null\n try {\n const payload = verifyJwt(token) as Record<string, unknown> | null\n if (!payload) return null\n const sid = payload.sid\n return typeof sid === 'string' && sid.length > 0 ? sid : null\n } catch {\n return null\n }\n}\n\nexport async function POST(req: Request) {\n const sessToken = parseCookie(req, 'session_token')\n const authToken = parseCookie(req, 'auth_token')\n const sessionId = extractSessionIdFromAuthToken(authToken)\n if (sessToken || sessionId) {\n try {\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n if (sessionId) {\n await auth.deleteSessionById(sessionId)\n }\n if (sessToken) {\n await auth.deleteSessionByToken(sessToken)\n }\n } catch {}\n }\n const res = buildSafeRedirectResponse(req, '/login')\n res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n res.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n return res\n}\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Log out current session',\n methods: {\n POST: {\n summary: 'Invalidate session and redirect',\n description: 'Clears authentication cookies and redirects the browser to the login page.',\n responses: [\n { status: 302, description: 'Redirect to login after successful logout', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,iBAAiB;AAC1B,SAAS,iCAAiC;AAE1C,SAAS,YAAY,KAAc,MAA6B;AAC9D,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC5C,QAAM,IAAI,OAAO,MAAM,IAAI,OAAO,gBAAgB,OAAO,UAAU,CAAC;AACpE,SAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,IAAI;AACxC;AAEA,SAAS,8BAA8B,OAAqC;AAC1E,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,MAAM,QAAQ;AACpB,WAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,MAAM;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,YAAY,YAAY,KAAK,eAAe;AAClD,QAAM,YAAY,YAAY,KAAK,YAAY;AAC/C,QAAM,YAAY,8BAA8B,SAAS;AACzD,MAAI,aAAa,WAAW;AAC1B,QAAI;AACF,YAAM,IAAI,MAAM,uBAAuB;AACvC,YAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,UAAI,WAAW;AACb,cAAM,KAAK,kBAAkB,SAAS;AAAA,MACxC;AACA,UAAI,WAAW;AACb,cAAM,KAAK,qBAAqB,SAAS;AAAA,MAC3C;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,QAAM,MAAM,0BAA0B,KAAK,QAAQ;AACnD,MAAI,QAAQ,IAAI,cAAc,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC1D,MAAI,QAAQ,IAAI,iBAAiB,IAAI,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC7D,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,6CAA6C,WAAW,YAAY;AAAA,MAClG;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,9 +4,8 @@ import { signJwt } from "@open-mercato/shared/lib/auth/jwt";
|
|
|
4
4
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
5
|
import { refreshSessionRequestSchema } from "@open-mercato/core/modules/auth/data/validators";
|
|
6
6
|
import { checkAuthRateLimit } from "@open-mercato/core/modules/auth/lib/rateLimitCheck";
|
|
7
|
-
import {
|
|
7
|
+
import { buildSafeRedirectResponse, resolveTrustedRedirectBase } from "@open-mercato/core/modules/auth/lib/requestRedirect";
|
|
8
8
|
import { sanitizeRedirectPath } from "@open-mercato/core/modules/auth/lib/safeRedirect";
|
|
9
|
-
import { getAppBaseUrl } from "@open-mercato/shared/lib/url";
|
|
10
9
|
import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
|
|
11
10
|
import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
12
11
|
import { z } from "zod";
|
|
@@ -46,12 +45,12 @@ function clearStaffAuthCookies(response) {
|
|
|
46
45
|
}
|
|
47
46
|
async function GET(req) {
|
|
48
47
|
const url = new URL(req.url);
|
|
49
|
-
const baseUrl =
|
|
48
|
+
const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin;
|
|
50
49
|
const redirectTo = sanitizeRedirectPath(url.searchParams.get("redirect"), baseUrl, "/");
|
|
51
50
|
const token = parseCookie(req, "session_token");
|
|
52
51
|
if (!token) {
|
|
53
52
|
return clearStaffAuthCookies(
|
|
54
|
-
|
|
53
|
+
buildSafeRedirectResponse(req, "/login?redirect=" + encodeURIComponent(redirectTo))
|
|
55
54
|
);
|
|
56
55
|
}
|
|
57
56
|
const c = await createRequestContainer();
|
|
@@ -59,12 +58,12 @@ async function GET(req) {
|
|
|
59
58
|
const ctx = await auth.refreshFromSessionToken(token);
|
|
60
59
|
if (!ctx) {
|
|
61
60
|
return clearStaffAuthCookies(
|
|
62
|
-
|
|
61
|
+
buildSafeRedirectResponse(req, "/login?redirect=" + encodeURIComponent(redirectTo))
|
|
63
62
|
);
|
|
64
63
|
}
|
|
65
64
|
const { user, roles, session } = ctx;
|
|
66
65
|
const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : void 0, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles });
|
|
67
|
-
const res =
|
|
66
|
+
const res = buildSafeRedirectResponse(req, redirectTo);
|
|
68
67
|
res.cookies.set("auth_token", jwt, { httpOnly: true, path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 60 * 60 * 8 });
|
|
69
68
|
return res;
|
|
70
69
|
}
|
|
@@ -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 { 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 {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,eAAe;AACxB,SAAS,2BAA2B;AACpC,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AACnC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\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 { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\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 clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const { user, roles, session } = ctx\n const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : undefined, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })\n const res = buildSafeRedirectResponse(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 clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n )\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 clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n const { user, roles, session } = ctx\n const jwt = signJwt({\n sub: String(user.id),\n sid: session ? String(session.id) : undefined,\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
|
}
|
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getSecurityEmailBaseUrl } from "@open-mercato/shared/lib/url";
|
|
3
|
+
function toRelativeRedirectPath(path) {
|
|
4
|
+
const withoutBackslashes = path.replace(/\\/g, "/");
|
|
5
|
+
const rooted = withoutBackslashes.startsWith("/") ? withoutBackslashes : `/${withoutBackslashes}`;
|
|
6
|
+
return rooted.replace(/^\/+/, "/");
|
|
7
|
+
}
|
|
8
|
+
function resolveTrustedRedirectBase(req) {
|
|
9
|
+
try {
|
|
10
|
+
return getSecurityEmailBaseUrl(req);
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function resolveSafeRedirectLocation(req, path) {
|
|
16
|
+
const safePath = toRelativeRedirectPath(path);
|
|
17
|
+
const base = resolveTrustedRedirectBase(req);
|
|
18
|
+
if (base) return new URL(safePath, base).toString();
|
|
19
|
+
return safePath;
|
|
20
|
+
}
|
|
21
|
+
function buildSafeRedirectResponse(req, path, status = 307) {
|
|
22
|
+
return new NextResponse(null, {
|
|
23
|
+
status,
|
|
24
|
+
headers: { Location: resolveSafeRedirectLocation(req, path) }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
2
27
|
function buildRequestOriginUrl(req, path) {
|
|
3
|
-
return
|
|
28
|
+
return resolveSafeRedirectLocation(req, path);
|
|
4
29
|
}
|
|
5
30
|
export {
|
|
6
|
-
buildRequestOriginUrl
|
|
31
|
+
buildRequestOriginUrl,
|
|
32
|
+
buildSafeRedirectResponse,
|
|
33
|
+
resolveSafeRedirectLocation,
|
|
34
|
+
resolveTrustedRedirectBase
|
|
7
35
|
};
|
|
8
36
|
//# sourceMappingURL=requestRedirect.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/lib/requestRedirect.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getSecurityEmailBaseUrl } from '@open-mercato/shared/lib/url'\n\nfunction toRelativeRedirectPath(path: string): string {\n // Strip any scheme/authority so the value is always treated as a path on the\n // current origin. Both `new URL(path, base)` and the browser would otherwise\n // honour a protocol-relative (`//host`) or backslash-smuggled value as a\n // cross-origin target supplied by a spoofed Host / X-Forwarded-Host.\n const withoutBackslashes = path.replace(/\\\\/g, '/')\n const rooted = withoutBackslashes.startsWith('/') ? withoutBackslashes : `/${withoutBackslashes}`\n return rooted.replace(/^\\/+/, '/')\n}\n\nexport function resolveTrustedRedirectBase(req: Request): string | null {\n try {\n return getSecurityEmailBaseUrl(req)\n } catch {\n return null\n }\n}\n\nexport function resolveSafeRedirectLocation(req: Request, path: string): string {\n const safePath = toRelativeRedirectPath(path)\n const base = resolveTrustedRedirectBase(req)\n if (base) return new URL(safePath, base).toString()\n return safePath\n}\n\nexport function buildSafeRedirectResponse(req: Request, path: string, status: number = 307): NextResponse {\n return new NextResponse(null, {\n status,\n headers: { Location: resolveSafeRedirectLocation(req, path) },\n })\n}\n\n/**\n * @deprecated Use {@link buildSafeRedirectResponse} or {@link resolveSafeRedirectLocation}.\n * Previously derived the redirect origin from raw request headers, which\n * allowed open redirects via a spoofed Host / X-Forwarded-Host. It now returns\n * an allowlist-validated app origin when the request origin is trusted, and a\n * host-relative path otherwise \u2014 the latter may be a relative URL, so callers\n * MUST NOT pass the result to `NextResponse.redirect` (use\n * {@link buildSafeRedirectResponse} instead).\n */\nexport function buildRequestOriginUrl(req: Request, path: string): string {\n return resolveSafeRedirectLocation(req, path)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,+BAA+B;AAExC,SAAS,uBAAuB,MAAsB;AAKpD,QAAM,qBAAqB,KAAK,QAAQ,OAAO,GAAG;AAClD,QAAM,SAAS,mBAAmB,WAAW,GAAG,IAAI,qBAAqB,IAAI,kBAAkB;AAC/F,SAAO,OAAO,QAAQ,QAAQ,GAAG;AACnC;AAEO,SAAS,2BAA2B,KAA6B;AACtE,MAAI;AACF,WAAO,wBAAwB,GAAG;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,4BAA4B,KAAc,MAAsB;AAC9E,QAAM,WAAW,uBAAuB,IAAI;AAC5C,QAAM,OAAO,2BAA2B,GAAG;AAC3C,MAAI,KAAM,QAAO,IAAI,IAAI,UAAU,IAAI,EAAE,SAAS;AAClD,SAAO;AACT;AAEO,SAAS,0BAA0B,KAAc,MAAc,SAAiB,KAAmB;AACxG,SAAO,IAAI,aAAa,MAAM;AAAA,IAC5B;AAAA,IACA,SAAS,EAAE,UAAU,4BAA4B,KAAK,IAAI,EAAE;AAAA,EAC9D,CAAC;AACH;AAWO,SAAS,sBAAsB,KAAc,MAAsB;AACxE,SAAO,4BAA4B,KAAK,IAAI;AAC9C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6332.1.6e73f0f55b",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
1
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
3
|
import { AuthService } from '@open-mercato/core/modules/auth/services/authService'
|
|
5
4
|
import { verifyJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
6
|
-
import {
|
|
5
|
+
import { buildSafeRedirectResponse } from '@open-mercato/core/modules/auth/lib/requestRedirect'
|
|
7
6
|
|
|
8
7
|
function parseCookie(req: Request, name: string): string | null {
|
|
9
8
|
const cookie = req.headers.get('cookie') || ''
|
|
@@ -39,7 +38,7 @@ export async function POST(req: Request) {
|
|
|
39
38
|
}
|
|
40
39
|
} catch {}
|
|
41
40
|
}
|
|
42
|
-
const res =
|
|
41
|
+
const res = buildSafeRedirectResponse(req, '/login')
|
|
43
42
|
res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })
|
|
44
43
|
res.cookies.set('session_token', '', { path: '/', maxAge: 0 })
|
|
45
44
|
return res
|
|
@@ -6,9 +6,8 @@ import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
|
6
6
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
7
7
|
import { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'
|
|
8
8
|
import { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'
|
|
9
|
-
import {
|
|
9
|
+
import { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'
|
|
10
10
|
import { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'
|
|
11
|
-
import { getAppBaseUrl } from '@open-mercato/shared/lib/url'
|
|
12
11
|
import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
|
|
13
12
|
import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
14
13
|
import { z } from 'zod'
|
|
@@ -46,12 +45,12 @@ function clearStaffAuthCookies(response: NextResponse) {
|
|
|
46
45
|
|
|
47
46
|
export async function GET(req: Request) {
|
|
48
47
|
const url = new URL(req.url)
|
|
49
|
-
const baseUrl =
|
|
48
|
+
const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin
|
|
50
49
|
const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')
|
|
51
50
|
const token = parseCookie(req, 'session_token')
|
|
52
51
|
if (!token) {
|
|
53
52
|
return clearStaffAuthCookies(
|
|
54
|
-
|
|
53
|
+
buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))
|
|
55
54
|
)
|
|
56
55
|
}
|
|
57
56
|
const c = await createRequestContainer()
|
|
@@ -59,12 +58,12 @@ export async function GET(req: Request) {
|
|
|
59
58
|
const ctx = await auth.refreshFromSessionToken(token)
|
|
60
59
|
if (!ctx) {
|
|
61
60
|
return clearStaffAuthCookies(
|
|
62
|
-
|
|
61
|
+
buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))
|
|
63
62
|
)
|
|
64
63
|
}
|
|
65
64
|
const { user, roles, session } = ctx
|
|
66
65
|
const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : undefined, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles })
|
|
67
|
-
const res =
|
|
66
|
+
const res = buildSafeRedirectResponse(req, redirectTo)
|
|
68
67
|
res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })
|
|
69
68
|
return res
|
|
70
69
|
}
|
|
@@ -1,5 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSecurityEmailBaseUrl } from '@open-mercato/shared/lib/url'
|
|
2
3
|
|
|
4
|
+
function toRelativeRedirectPath(path: string): string {
|
|
5
|
+
// Strip any scheme/authority so the value is always treated as a path on the
|
|
6
|
+
// current origin. Both `new URL(path, base)` and the browser would otherwise
|
|
7
|
+
// honour a protocol-relative (`//host`) or backslash-smuggled value as a
|
|
8
|
+
// cross-origin target supplied by a spoofed Host / X-Forwarded-Host.
|
|
9
|
+
const withoutBackslashes = path.replace(/\\/g, '/')
|
|
10
|
+
const rooted = withoutBackslashes.startsWith('/') ? withoutBackslashes : `/${withoutBackslashes}`
|
|
11
|
+
return rooted.replace(/^\/+/, '/')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveTrustedRedirectBase(req: Request): string | null {
|
|
15
|
+
try {
|
|
16
|
+
return getSecurityEmailBaseUrl(req)
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveSafeRedirectLocation(req: Request, path: string): string {
|
|
23
|
+
const safePath = toRelativeRedirectPath(path)
|
|
24
|
+
const base = resolveTrustedRedirectBase(req)
|
|
25
|
+
if (base) return new URL(safePath, base).toString()
|
|
26
|
+
return safePath
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildSafeRedirectResponse(req: Request, path: string, status: number = 307): NextResponse {
|
|
30
|
+
return new NextResponse(null, {
|
|
31
|
+
status,
|
|
32
|
+
headers: { Location: resolveSafeRedirectLocation(req, path) },
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Use {@link buildSafeRedirectResponse} or {@link resolveSafeRedirectLocation}.
|
|
38
|
+
* Previously derived the redirect origin from raw request headers, which
|
|
39
|
+
* allowed open redirects via a spoofed Host / X-Forwarded-Host. It now returns
|
|
40
|
+
* an allowlist-validated app origin when the request origin is trusted, and a
|
|
41
|
+
* host-relative path otherwise — the latter may be a relative URL, so callers
|
|
42
|
+
* MUST NOT pass the result to `NextResponse.redirect` (use
|
|
43
|
+
* {@link buildSafeRedirectResponse} instead).
|
|
44
|
+
*/
|
|
3
45
|
export function buildRequestOriginUrl(req: Request, path: string): string {
|
|
4
|
-
return
|
|
46
|
+
return resolveSafeRedirectLocation(req, path)
|
|
5
47
|
}
|