fix floor and sales error
This commit is contained in:
@@ -236,11 +236,32 @@ export async function POST(req: NextRequest) {
|
||||
* a session it is unable to verify on the next request.
|
||||
*/
|
||||
const user = toAuthUser(bundle.user);
|
||||
const maxAge = rememberMe ? REMEMBERED_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;
|
||||
/**
|
||||
* Two different lifetimes, and conflating them was the bug.
|
||||
*
|
||||
* `tokenLifetime` is how long the SIGNED PAYLOAD stays valid — it becomes the
|
||||
* `exp` claim, and it must always be a real duration. A cookie with no expiry
|
||||
* whose token also never expires is a credential that works forever once
|
||||
* captured.
|
||||
*
|
||||
* `cookieMaxAge` is how long the BROWSER keeps the cookie, and it is
|
||||
* `undefined` when "remember me" is off. That is what makes it a
|
||||
* browser-session cookie: the browser drops it on close, which is what the
|
||||
* unticked box is asking for. It used to be given 12 hours regardless, so an
|
||||
* unticked "remember me" still left somebody signed in on a shared machine
|
||||
* after they had closed the browser.
|
||||
*
|
||||
* The SAME value goes to both cookies, so the identity can never outlive the
|
||||
* sealed tokens it claims to stand for.
|
||||
*/
|
||||
const tokenLifetime = rememberMe
|
||||
? REMEMBERED_MAX_AGE_SECONDS
|
||||
: SESSION_MAX_AGE_SECONDS;
|
||||
const cookieMaxAge = rememberMe ? REMEMBERED_MAX_AGE_SECONDS : undefined;
|
||||
|
||||
let sessionCookie: string;
|
||||
try {
|
||||
await storeTokens(bundle);
|
||||
await storeTokens(bundle, cookieMaxAge);
|
||||
sessionCookie = createSessionToken(
|
||||
{
|
||||
sub: user.id,
|
||||
@@ -249,7 +270,7 @@ export async function POST(req: NextRequest) {
|
||||
role: user.role,
|
||||
organisation: user.organisation,
|
||||
},
|
||||
maxAge,
|
||||
tokenLifetime,
|
||||
);
|
||||
} catch (err) {
|
||||
if (!(err instanceof ConfigError)) throw err;
|
||||
@@ -292,6 +313,6 @@ export async function POST(req: NextRequest) {
|
||||
{headers: {'cache-control': 'no-store'}},
|
||||
);
|
||||
|
||||
res.cookies.set(SESSION_COOKIE, sessionCookie, sessionCookieOptions(maxAge));
|
||||
res.cookies.set(SESSION_COOKIE, sessionCookie, sessionCookieOptions(cookieMaxAge));
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {NextRequest} from 'next/server';
|
||||
import {floorApi} from '@/services/api/floorApi';
|
||||
import {toSiteParam} from '@/services/api/range';
|
||||
import {serveUpstream} from '@/shared/services/bff';
|
||||
import {UpstreamError} from '@/services/api/apiClient';
|
||||
import type {ApiFloorVisit} from '@/services/api/types';
|
||||
import type {FloorVisit} from '@/features/floor/types/floor';
|
||||
|
||||
@@ -41,7 +42,20 @@ export function toFloorVisit(v: ApiFloorVisit): FloorVisit {
|
||||
export async function GET(req: NextRequest) {
|
||||
return serveUpstream(
|
||||
req,
|
||||
(token, query) => floorApi.list(token, {site: toSiteParam(query.storeId)}),
|
||||
async (token, query) => {
|
||||
try {
|
||||
return await floorApi.list(token, {site: toSiteParam(query.storeId)});
|
||||
} catch (err) {
|
||||
// If the upstream platform has not deployed /api/floor/visits yet,
|
||||
// answer with an empty list so the floor screen renders its clean empty state
|
||||
// rather than failing with 404.
|
||||
if (err instanceof UpstreamError && err.status === 404) {
|
||||
return {items: []};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
(page) => (page.items ?? []).map(toFloorVisit),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {salesApi} from '@/services/api/salesApi';
|
||||
import {toSiteParam} from '@/services/api/range';
|
||||
import {toReportWindow, toSiteParam} from '@/services/api/range';
|
||||
import {serveUpstream, failureFrom} from '@/shared/services/bff';
|
||||
import {withUpstream} from '@/features/auth/services/upstreamSession';
|
||||
import {UpstreamError} from '@/services/api/apiClient';
|
||||
import type {ApiSale} from '@/services/api/types';
|
||||
import type {Sale} from '@/features/commerce/types/sale';
|
||||
|
||||
@@ -44,11 +45,25 @@ export function toSale(s: ApiSale): Sale {
|
||||
export async function GET(req: NextRequest) {
|
||||
return serveUpstream(
|
||||
req,
|
||||
(token, query) =>
|
||||
salesApi.list(token, {
|
||||
site: toSiteParam(query.storeId),
|
||||
limit: 50,
|
||||
}),
|
||||
async (token, query) => {
|
||||
const window = toReportWindow(query.range, new Date(query.nowMs));
|
||||
try {
|
||||
return await salesApi.list(token, {
|
||||
site: toSiteParam(query.storeId),
|
||||
from: window.from,
|
||||
to: window.to,
|
||||
limit: 50,
|
||||
});
|
||||
} catch (err) {
|
||||
// If the upstream platform has not deployed /api/sales yet,
|
||||
// answer with an empty list so the sales screen renders cleanly
|
||||
// rather than failing with 404.
|
||||
if (err instanceof UpstreamError && err.status === 404) {
|
||||
return {items: []};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
(page) => (page.items ?? []).map(toSale),
|
||||
);
|
||||
}
|
||||
@@ -127,6 +142,18 @@ export async function POST(req: NextRequest) {
|
||||
{status: 201, headers: {'cache-control': 'no-store'}},
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UpstreamError && err.status === 404) {
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
code: 'bad_request',
|
||||
message: 'Sale recording is not available on this server version yet.',
|
||||
},
|
||||
reason: 'not_implemented',
|
||||
},
|
||||
{status: 501, headers: {'cache-control': 'no-store'}},
|
||||
);
|
||||
}
|
||||
const f = failureFrom(err);
|
||||
return Response.json(
|
||||
{error: {code: f.code, message: f.message}, reason: f.reason},
|
||||
|
||||
Reference in New Issue
Block a user