Introduce APL interface
This commit is contained in:
parent
a59519aede
commit
43f81f1af6
7 changed files with 166 additions and 6 deletions
68
docs/apl.md
Normal file
68
docs/apl.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
# APL - Auth Persistance Layer
|
||||
|
||||
APL is an interface for managing auth data of registered Apps. Implementing it does not restrict you from choosing the right technology for your project (for example Redis, Postgres, S3, etc.), and provides a common set of operations which can be used by your application and functions provided by App SDK.
|
||||
|
||||
## Available methods
|
||||
|
||||
- `get: (domain: string) => Promise<AuthData | undefined>` - If the entry for given domain exists, returns AuthData (`{ domain: string, token: string }`) object.
|
||||
|
||||
- `set: (authData: AuthData) => Promise<void>` - Save auth data.
|
||||
|
||||
- `delete: (domain: string) => Promise<void>` - Remove auth data fot the given domain.
|
||||
|
||||
- `getAll: () => Promise<AuthData[]>` - Returns all auth data available.
|
||||
|
||||
## Example implementation
|
||||
|
||||
Let's create an APL, which uses redis for data storage:
|
||||
|
||||
```ts
|
||||
import { createClient } from "redis";
|
||||
import { APL, AuthData } from "@saleor/app-sdk/types/apl";
|
||||
|
||||
const client = createClient();
|
||||
await client.connect();
|
||||
|
||||
const redisAPL: APL = {
|
||||
get: async (domain: string) => {
|
||||
const token = await client.get(domain);
|
||||
if (token) {
|
||||
return { token, domain };
|
||||
}
|
||||
return;
|
||||
},
|
||||
set: async (authData: AuthData) => {
|
||||
await client.set(authData.domain, authData.token);
|
||||
},
|
||||
delete: async (domain: string) => {
|
||||
await client.del(domain);
|
||||
},
|
||||
getAll: async () => {
|
||||
throw new Exception("Not implemented.");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
You'll be able to use it directly:
|
||||
|
||||
```ts
|
||||
import { redisAPL } from "./apl";
|
||||
|
||||
const createTestData = async () => {
|
||||
await redisAPL.set({ domain: "example.com ", token: "test-token" });
|
||||
};
|
||||
```
|
||||
|
||||
And with middleware from the SDK:
|
||||
|
||||
```ts
|
||||
import { withRegisteredSaleorDomainHeader } from "@saleor/app-sdk/middleware";
|
||||
import { redisAPL } from "./apl";
|
||||
|
||||
const handler = async (request) => {
|
||||
return Response.OK({ message: "If you see this, your app is registered!" });
|
||||
};
|
||||
|
||||
// the middleware will reject request if it's domain has not been registered
|
||||
export default withRegisteredSaleorDomainHeader({ apl: redisAPL })(handler);
|
||||
```
|
|
@ -53,6 +53,11 @@
|
|||
"import": "./const.mjs",
|
||||
"require": "./const.js"
|
||||
},
|
||||
"./apl": {
|
||||
"types": "./apl.d.ts",
|
||||
"import": "./apl.mjs",
|
||||
"require": "./apl.js"
|
||||
},
|
||||
"./middleware": {
|
||||
"types": "./middleware.d.ts",
|
||||
"import": "./middleware.mjs",
|
||||
|
|
11
src/APL/apl.ts
Normal file
11
src/APL/apl.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export interface AuthData {
|
||||
domain: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface APL {
|
||||
get: (domain: string) => Promise<AuthData | undefined>;
|
||||
set: (authData: AuthData) => Promise<void>;
|
||||
delete: (domain: string) => Promise<void>;
|
||||
getAll: () => Promise<AuthData[]>;
|
||||
}
|
1
src/APL/index.ts
Normal file
1
src/APL/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./apl";
|
|
@ -1,17 +1,19 @@
|
|||
import { Handler, Request } from "retes";
|
||||
import { Response } from "retes/types";
|
||||
import { Response } from "retes/response";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { withBaseURL } from "./middleware";
|
||||
import { APL } from "./APL";
|
||||
import { SALEOR_DOMAIN_HEADER } from "./const";
|
||||
import { withBaseURL, withRegisteredSaleorDomainHeader } from "./middleware";
|
||||
|
||||
const getMockEmptyResponse = async () => ({} as Response);
|
||||
const getMockSuccessResponse = async () => Response.OK({});
|
||||
|
||||
describe("middleware.test.ts", () => {
|
||||
describe("withBaseURL", () => {
|
||||
let mockHandlerFn: Handler = vi.fn(getMockEmptyResponse);
|
||||
let mockHandlerFn: Handler = vi.fn(getMockSuccessResponse);
|
||||
|
||||
beforeEach(() => {
|
||||
mockHandlerFn = vi.fn();
|
||||
mockHandlerFn = vi.fn(getMockSuccessResponse);
|
||||
});
|
||||
|
||||
it("Adds base URL from request header to context and calls handler", async () => {
|
||||
|
@ -29,4 +31,60 @@ describe("middleware.test.ts", () => {
|
|||
expect(mockHandlerFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("withRegisteredSaleorDomainHeader", () => {
|
||||
let mockHandlerFn: Handler = vi.fn(getMockSuccessResponse);
|
||||
|
||||
const mockAPL: APL = {
|
||||
get: async (domain: string) =>
|
||||
domain === "example.com"
|
||||
? {
|
||||
domain: "example.com",
|
||||
token: "mock-token",
|
||||
}
|
||||
: undefined,
|
||||
set: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockHandlerFn = vi.fn(getMockSuccessResponse);
|
||||
});
|
||||
|
||||
it("Pass request when auth data are available", async () => {
|
||||
const mockRequest = {
|
||||
context: {},
|
||||
headers: {
|
||||
host: "my-saleor-env.saleor.cloud",
|
||||
"x-forwarded-proto": "https",
|
||||
[SALEOR_DOMAIN_HEADER]: "example.com",
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
const response = await withRegisteredSaleorDomainHeader({ apl: mockAPL })(mockHandlerFn)(
|
||||
mockRequest
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockHandlerFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("Reject request when auth data are not available", async () => {
|
||||
const mockRequest = {
|
||||
context: {},
|
||||
headers: {
|
||||
host: "my-saleor-env.saleor.cloud",
|
||||
"x-forwarded-proto": "https",
|
||||
[SALEOR_DOMAIN_HEADER]: "not-registered.example.com",
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
const response = await withRegisteredSaleorDomainHeader({ apl: mockAPL })(mockHandlerFn)(
|
||||
mockRequest
|
||||
);
|
||||
expect(response.status).eq(403);
|
||||
expect(mockHandlerFn).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as jose from "jose";
|
|||
import type { Middleware, Request } from "retes";
|
||||
import { Response } from "retes/response";
|
||||
|
||||
import { APL } from "./APL";
|
||||
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_SIGNATURE_HEADER } from "./const";
|
||||
import { getSaleorHeaders } from "./headers";
|
||||
import { getJwksUrl } from "./urls";
|
||||
|
@ -187,3 +188,19 @@ export const withJWTVerified =
|
|||
|
||||
return handler(request);
|
||||
};
|
||||
|
||||
export const withRegisteredSaleorDomainHeader =
|
||||
({ apl }: { apl: APL }): Middleware =>
|
||||
(handler) =>
|
||||
async (request) => {
|
||||
const { domain: saleorDomain } = getSaleorHeaders(request.headers);
|
||||
const authData = await apl.get(saleorDomain);
|
||||
if (!authData) {
|
||||
return Response.Forbidden({
|
||||
success: false,
|
||||
message: `Domain ${saleorDomain} not registered.`,
|
||||
});
|
||||
}
|
||||
|
||||
return handler(request);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue