feat(pos): add assertPosAuth for POS_API_KEY-gated routes
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f6bfb6f1eb
commit
af7a74b4fa
2 changed files with 75 additions and 0 deletions
53
src/lib/server/pos-auth.test.ts
Normal file
53
src/lib/server/pos-auth.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
import { assertPosAuth } from "./pos-auth";
|
||||
|
||||
describe("assertPosAuth", () => {
|
||||
const ORIGINAL_KEY = process.env.POS_API_KEY;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.POS_API_KEY = "test-pos-secret";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (ORIGINAL_KEY === undefined) {
|
||||
delete process.env.POS_API_KEY;
|
||||
} else {
|
||||
process.env.POS_API_KEY = ORIGINAL_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
function reqWith(authHeader: string | null) {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authHeader !== null) headers.authorization = authHeader;
|
||||
return new NextRequest("http://localhost/api/v1/pos/catalog", { headers });
|
||||
}
|
||||
|
||||
it("throws when POS_API_KEY env var is missing", () => {
|
||||
delete process.env.POS_API_KEY;
|
||||
expect(() => assertPosAuth(reqWith("Bearer test-pos-secret"))).toThrow(
|
||||
/POS_API_KEY is not configured/
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a matching bearer token", () => {
|
||||
expect(() => assertPosAuth(reqWith("Bearer test-pos-secret"))).not.toThrow();
|
||||
});
|
||||
|
||||
it("rejects a wrong bearer token", () => {
|
||||
expect(() => assertPosAuth(reqWith("Bearer wrong"))).toThrow(/Unauthorized/);
|
||||
});
|
||||
|
||||
it("rejects a request with no Authorization header", () => {
|
||||
expect(() => assertPosAuth(reqWith(null))).toThrow(/Unauthorized/);
|
||||
});
|
||||
|
||||
it("tags the thrown error with status 401", () => {
|
||||
try {
|
||||
assertPosAuth(reqWith("Bearer wrong"));
|
||||
throw new Error("expected to throw");
|
||||
} catch (err) {
|
||||
expect((err as Error & { status?: number }).status).toBe(401);
|
||||
}
|
||||
});
|
||||
});
|
||||
22
src/lib/server/pos-auth.ts
Normal file
22
src/lib/server/pos-auth.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* Device-scoped secret, distinct from AGENT_API_KEY — a POS device
|
||||
* only needs checkout/catalog/sales access, not the full agent API
|
||||
* surface, so it gets its own key rather than reusing the agent's.
|
||||
*/
|
||||
export function assertPosAuth(req: NextRequest) {
|
||||
const expected = process.env.POS_API_KEY;
|
||||
if (!expected) {
|
||||
throw new Error("POS_API_KEY is not configured.");
|
||||
}
|
||||
|
||||
const authHeader = req.headers.get("authorization") || "";
|
||||
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
||||
|
||||
if (token !== expected) {
|
||||
const error = new Error("Unauthorized");
|
||||
(error as Error & { status?: number }).status = 401;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue