Merge pull request #35 from saleor/env-var-apl
Introduce new APLs: fileAPL and vercelAPL
This commit is contained in:
commit
cbaa2335b4
13 changed files with 549 additions and 9 deletions
|
@ -9,7 +9,7 @@
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier" // prettier *has* to be the last one, to avoid conflicting rules
|
"prettier" // prettier *has* to be the last one, to avoid conflicting rules
|
||||||
],
|
],
|
||||||
"ignorePatterns": ["pnpm-lock.yaml"],
|
"ignorePatterns": ["pnpm-lock.yaml", "dist"],
|
||||||
"plugins": ["simple-import-sort", "@typescript-eslint"],
|
"plugins": ["simple-import-sort", "@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||||
|
|
|
@ -3,3 +3,4 @@ saleor/api.tsx
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
graphql.schema.json
|
graphql.schema.json
|
||||||
lib/$path.ts
|
lib/$path.ts
|
||||||
|
dist
|
73
docs/apl.md
73
docs/apl.md
|
@ -14,7 +14,7 @@ APL is an interface for managing auth data of registered Apps. Implementing it d
|
||||||
|
|
||||||
## Example implementation
|
## Example implementation
|
||||||
|
|
||||||
Let's create an APL, which uses redis for data storage:
|
Let's create an APL, which uses Redis for data storage:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createClient } from "redis";
|
import { createClient } from "redis";
|
||||||
|
@ -66,3 +66,74 @@ const handler = async (request) => {
|
||||||
// the middleware will reject request if it's domain has not been registered
|
// the middleware will reject request if it's domain has not been registered
|
||||||
export default withRegisteredSaleorDomainHeader({ apl: redisAPL })(handler);
|
export default withRegisteredSaleorDomainHeader({ apl: redisAPL })(handler);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using different APL depending on the environment
|
||||||
|
|
||||||
|
Depending on the environment your app is working on, you may want to use a different APL. For example during local development you might like to use `FileAPL`, because it does not require any additional infrastructure. Deployed apps on the other hand need more robust solution.
|
||||||
|
|
||||||
|
To handle both scenarios, initialize the proper APLs in your code based on it's environment. In your application code:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// lib/saleorApp.ts
|
||||||
|
|
||||||
|
import { FileAPL, VercelAPL } from "@saleor/app-sdk/APL";
|
||||||
|
|
||||||
|
// Based on environment variable the app will use a different APL:
|
||||||
|
// - For local development store auth data in the `.auth-data.json`.
|
||||||
|
// - For app deployment on Vercel with Saleor CLI, use vercelAPL.
|
||||||
|
export const apl = process.env.VERCEL === "1" ? new VercelAPL() : new FileAPL();
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can use it for in your view:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SALEOR_DOMAIN_HEADER } from "@saleor/app-sdk/const";
|
||||||
|
import { withRegisteredSaleorDomainHeader } from "@saleor/app-sdk/middleware";
|
||||||
|
import type { Handler } from "retes";
|
||||||
|
import { toNextHandler } from "retes/adapter";
|
||||||
|
import { Response } from "retes/response";
|
||||||
|
|
||||||
|
// import created APL
|
||||||
|
import { apl } from "@lib/saleorApp";
|
||||||
|
|
||||||
|
const handler: Handler = async (request) => {
|
||||||
|
const saleorDomain = request.headers[SALEOR_DOMAIN_HEADER];
|
||||||
|
|
||||||
|
// Get auth data
|
||||||
|
const authData = apl.get(saleorDomain);
|
||||||
|
|
||||||
|
// view logic...
|
||||||
|
|
||||||
|
return Response.OK();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default toNextHandler([withRegisteredSaleorDomainHeader({ apl }), handler]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available APLs
|
||||||
|
|
||||||
|
### FileAPL
|
||||||
|
|
||||||
|
File based storage of auth data, intended for local development. Data are stored in the `.saleor-app-auth.json` file. You'll be able to develop app without additional dependencies or infrastructure.
|
||||||
|
|
||||||
|
Please note: this APL supports single tenant only (new registrations overwrite previous ones) and should not be used on production.
|
||||||
|
|
||||||
|
### VercelAPL
|
||||||
|
|
||||||
|
Single tenant APL dedicated for apps deployed on Vercel. Apps deployed from Marketplace and CLI automatically set up Vercel project for this APL (`SALEOR_REGISTER_APP_URL` and `SALEOR_DEPLOYMENT_TOKEN` variables).
|
||||||
|
|
||||||
|
The auth data are stored using environment variables:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant SI as Saleor Instance
|
||||||
|
participant A as App
|
||||||
|
participant SSI as Saleor x Vercel integration
|
||||||
|
participant V as Vercel
|
||||||
|
|
||||||
|
SI->>+A: Register
|
||||||
|
A->>SSI: Update auth data
|
||||||
|
A->>-SI: Register completed
|
||||||
|
SSI->>V: Set auth data as environment variables
|
||||||
|
V->>A: Redeploy the application
|
||||||
|
```
|
||||||
|
|
|
@ -50,3 +50,4 @@ Use the namespace name to enable debug logs for each module.
|
||||||
| \app-sdk:\* | Enable all |
|
| \app-sdk:\* | Enable all |
|
||||||
| app-sdk:AppBridge | Enable [AppBridge](./app-bridge.md) (browser only) |
|
| app-sdk:AppBridge | Enable [AppBridge](./app-bridge.md) (browser only) |
|
||||||
| app-sdk:Middleware:\* | Enable all middlewares (node only) |
|
| app-sdk:Middleware:\* | Enable all middlewares (node only) |
|
||||||
|
| app-sdk:APL:\* | Enable all APLs (node only) |
|
||||||
|
|
|
@ -22,11 +22,13 @@
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"graphql": "^16.5.0",
|
"graphql": "^16.5.0",
|
||||||
"jose": "^4.8.3",
|
"jose": "^4.8.3",
|
||||||
|
"node-fetch": "^3.2.10",
|
||||||
"retes": "^0.32.0",
|
"retes": "^0.32.0",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"release-it": "^15.4.1",
|
"release-it": "^15.4.1",
|
||||||
|
"@types/node-fetch": "^2.6.2",
|
||||||
"@testing-library/dom": "^8.17.1",
|
"@testing-library/dom": "^8.17.1",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/node": "^18.6.5",
|
"@types/node": "^18.6.5",
|
||||||
|
|
|
@ -4,6 +4,7 @@ specifiers:
|
||||||
'@testing-library/dom': ^8.17.1
|
'@testing-library/dom': ^8.17.1
|
||||||
'@types/debug': ^4.1.7
|
'@types/debug': ^4.1.7
|
||||||
'@types/node': ^18.6.5
|
'@types/node': ^18.6.5
|
||||||
|
'@types/node-fetch': ^2.6.2
|
||||||
'@types/uuid': ^8.3.4
|
'@types/uuid': ^8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': ^5.33.0
|
'@typescript-eslint/eslint-plugin': ^5.33.0
|
||||||
'@typescript-eslint/parser': ^5.33.0
|
'@typescript-eslint/parser': ^5.33.0
|
||||||
|
@ -24,6 +25,7 @@ specifiers:
|
||||||
husky: ^8.0.1
|
husky: ^8.0.1
|
||||||
jose: ^4.8.3
|
jose: ^4.8.3
|
||||||
jsdom: ^20.0.0
|
jsdom: ^20.0.0
|
||||||
|
node-fetch: ^3.2.10
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
release-it: ^15.4.1
|
release-it: ^15.4.1
|
||||||
retes: ^0.32.0
|
retes: ^0.32.0
|
||||||
|
@ -40,6 +42,7 @@ dependencies:
|
||||||
fast-glob: 3.2.11
|
fast-glob: 3.2.11
|
||||||
graphql: 16.5.0
|
graphql: 16.5.0
|
||||||
jose: 4.8.3
|
jose: 4.8.3
|
||||||
|
node-fetch: 3.2.10
|
||||||
retes: 0.32.0
|
retes: 0.32.0
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ devDependencies:
|
||||||
'@testing-library/dom': 8.17.1
|
'@testing-library/dom': 8.17.1
|
||||||
'@types/debug': 4.1.7
|
'@types/debug': 4.1.7
|
||||||
'@types/node': 18.7.1
|
'@types/node': 18.7.1
|
||||||
|
'@types/node-fetch': 2.6.2
|
||||||
'@types/uuid': 8.3.4
|
'@types/uuid': 8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': 5.33.0_njno5y7ry2l2lcmiu4tywxkwnq
|
'@typescript-eslint/eslint-plugin': 5.33.0_njno5y7ry2l2lcmiu4tywxkwnq
|
||||||
'@typescript-eslint/parser': 5.33.0_qugx7qdu5zevzvxaiqyxfiwquq
|
'@typescript-eslint/parser': 5.33.0_qugx7qdu5zevzvxaiqyxfiwquq
|
||||||
|
@ -686,6 +690,13 @@ packages:
|
||||||
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/node-fetch/2.6.2:
|
||||||
|
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.7.1
|
||||||
|
form-data: 3.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node/18.7.1:
|
/@types/node/18.7.1:
|
||||||
resolution: {integrity: sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==}
|
resolution: {integrity: sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1446,7 +1457,6 @@ packages:
|
||||||
/data-uri-to-buffer/4.0.0:
|
/data-uri-to-buffer/4.0.0:
|
||||||
resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==}
|
resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/data-urls/3.0.2:
|
/data-urls/3.0.2:
|
||||||
resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==}
|
resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==}
|
||||||
|
@ -2358,7 +2368,6 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-domexception: 1.0.0
|
node-domexception: 1.0.0
|
||||||
web-streams-polyfill: 3.2.1
|
web-streams-polyfill: 3.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/figures/4.0.1:
|
/figures/4.0.1:
|
||||||
resolution: {integrity: sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w==}
|
resolution: {integrity: sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w==}
|
||||||
|
@ -2418,6 +2427,15 @@ packages:
|
||||||
engines: {node: '>= 14.17'}
|
engines: {node: '>= 14.17'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/form-data/3.0.1:
|
||||||
|
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: true
|
||||||
|
|
||||||
/form-data/4.0.0:
|
/form-data/4.0.0:
|
||||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -2432,7 +2450,6 @@ packages:
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fs-extra/8.1.0:
|
/fs-extra/8.1.0:
|
||||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||||
|
@ -3494,7 +3511,6 @@ packages:
|
||||||
/node-domexception/1.0.0:
|
/node-domexception/1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/node-fetch/2.6.7:
|
/node-fetch/2.6.7:
|
||||||
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
|
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
|
||||||
|
@ -3515,7 +3531,6 @@ packages:
|
||||||
data-uri-to-buffer: 4.0.0
|
data-uri-to-buffer: 4.0.0
|
||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
formdata-polyfill: 4.0.10
|
formdata-polyfill: 4.0.10
|
||||||
dev: true
|
|
||||||
|
|
||||||
/node-releases/2.0.6:
|
/node-releases/2.0.6:
|
||||||
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
|
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
|
||||||
|
@ -4899,7 +4914,6 @@ packages:
|
||||||
/web-streams-polyfill/3.2.1:
|
/web-streams-polyfill/3.2.1:
|
||||||
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
|
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/webidl-conversions/3.0.1:
|
/webidl-conversions/3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
3
src/APL/apl-debug.ts
Normal file
3
src/APL/apl-debug.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { createDebug } from "../debug";
|
||||||
|
|
||||||
|
export const createAPLDebug = (namespace: string) => createDebug(`APL:${namespace}`);
|
119
src/APL/file-apl.test.ts
Normal file
119
src/APL/file-apl.test.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import { promises as fsPromises } from "fs";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { FileAPL } from "./file-apl";
|
||||||
|
|
||||||
|
const stubAuthData = {
|
||||||
|
domain: "example.com",
|
||||||
|
token: "example-token",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("APL", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FileAPL", () => {
|
||||||
|
describe("get", () => {
|
||||||
|
it("Should throw error when JSON parse fails", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue("Not a valid JSON");
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
await expect(apl.get(stubAuthData.domain)).rejects.toThrow(
|
||||||
|
"File APL could not read auth data from the .saleor-app-auth.json file"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns auth data for existing domain", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData));
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
expect(await apl.get(stubAuthData.domain)).toStrictEqual(stubAuthData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns undefined for unknown domain", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData));
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
expect(await apl.get("unknown-domain.example.com")).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set", () => {
|
||||||
|
it("Handle write file errors", async () => {
|
||||||
|
const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockImplementation(() => {
|
||||||
|
throw Error("Write error");
|
||||||
|
});
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
await expect(apl.set(stubAuthData)).rejects.toThrow(
|
||||||
|
"File APL was unable to save auth data"
|
||||||
|
);
|
||||||
|
expect(spyWriteFile).toBeCalledWith(".saleor-app-auth.json", JSON.stringify(stubAuthData));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Successfully save to file", async () => {
|
||||||
|
const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockResolvedValue();
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
await expect(apl.set(stubAuthData));
|
||||||
|
|
||||||
|
expect(spyWriteFile).toBeCalledWith(".saleor-app-auth.json", JSON.stringify(stubAuthData));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("delete", () => {
|
||||||
|
it("Should override file when called with known domain", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData));
|
||||||
|
const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockResolvedValue();
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
await apl.delete(stubAuthData.domain);
|
||||||
|
|
||||||
|
expect(spyWriteFile).toBeCalledWith(".saleor-app-auth.json", "{}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not delete data when called with unknown domain", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData));
|
||||||
|
|
||||||
|
const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockResolvedValue();
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
await apl.delete("unknown-domain.example.com");
|
||||||
|
|
||||||
|
expect(spyWriteFile).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAll", () => {
|
||||||
|
it("Should return list with one item when auth data are existing", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData));
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
expect(await apl.getAll()).toStrictEqual([stubAuthData]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return empty list when auth data are empty", async () => {
|
||||||
|
vi.spyOn(fsPromises, "access").mockResolvedValue();
|
||||||
|
vi.spyOn(fsPromises, "readFile").mockResolvedValue("{}");
|
||||||
|
|
||||||
|
const apl = new FileAPL();
|
||||||
|
|
||||||
|
expect(await apl.getAll()).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
101
src/APL/file-apl.ts
Normal file
101
src/APL/file-apl.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { promises as fsPromises } from "fs";
|
||||||
|
|
||||||
|
import { APL, AuthData } from "./apl";
|
||||||
|
import { createAPLDebug } from "./apl-debug";
|
||||||
|
|
||||||
|
const debug = createAPLDebug("FileAPL");
|
||||||
|
|
||||||
|
export type FileAPLConfig = {
|
||||||
|
fileName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File APL
|
||||||
|
*
|
||||||
|
* The APL store auth data in the json file.
|
||||||
|
*
|
||||||
|
* Before using this APL, please take in consideration:
|
||||||
|
* - only stores single auth data entry (setting up a new one will overwrite previous values)
|
||||||
|
* - it's not recommended for production use - redeployment of the application will override
|
||||||
|
* existing values, or data persistence will not be guaranteed at all depending on chosen
|
||||||
|
* hosting solution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class FileAPL implements APL {
|
||||||
|
private fileName: string;
|
||||||
|
|
||||||
|
constructor(config: FileAPLConfig = {}) {
|
||||||
|
this.fileName = config?.fileName || ".saleor-app-auth.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load auth data from a file and return it as AuthData format.
|
||||||
|
* In case of incomplete or invalid data, return `undefined`.
|
||||||
|
*
|
||||||
|
* @param {string} fileName
|
||||||
|
*/
|
||||||
|
private async loadDataFromFile(): Promise<AuthData | undefined> {
|
||||||
|
debug(`Load auth data from the ${this.fileName} file`);
|
||||||
|
let parsedData: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
await fsPromises.access(this.fileName);
|
||||||
|
parsedData = JSON.parse(await fsPromises.readFile(this.fileName, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
debug(`Could not read auth data from the ${this.fileName} file`, err);
|
||||||
|
throw new Error(`File APL could not read auth data from the ${this.fileName} file`);
|
||||||
|
}
|
||||||
|
const { token, domain } = parsedData;
|
||||||
|
if (token && domain) {
|
||||||
|
return { token, domain };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save auth data to file.
|
||||||
|
* When `authData` argument is empty, will overwrite file with empty values.
|
||||||
|
*
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {AuthData} [authData]
|
||||||
|
*/
|
||||||
|
private async saveDataToFile(authData?: AuthData) {
|
||||||
|
debug(`Save auth data to the ${this.fileName} file`);
|
||||||
|
const newData = authData ? JSON.stringify(authData) : "{}";
|
||||||
|
try {
|
||||||
|
await fsPromises.writeFile(this.fileName, newData);
|
||||||
|
} catch (err) {
|
||||||
|
debug(`Could not save auth data to the ${this.fileName} file`, err);
|
||||||
|
throw new Error("File APL was unable to save auth data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(domain: string) {
|
||||||
|
const authData = await this.loadDataFromFile();
|
||||||
|
if (domain === authData?.domain) {
|
||||||
|
return authData;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(authData: AuthData) {
|
||||||
|
await this.saveDataToFile(authData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(domain: string) {
|
||||||
|
const authData = await this.loadDataFromFile();
|
||||||
|
|
||||||
|
if (domain === authData?.domain) {
|
||||||
|
await this.saveDataToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
const authData = await this.loadDataFromFile();
|
||||||
|
|
||||||
|
if (!authData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [authData];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
export * from "./apl";
|
export * from "./apl";
|
||||||
|
export * from "./file-apl";
|
||||||
|
export * from "./vercel-apl";
|
||||||
|
|
112
src/APL/vercel-apl.test.ts
Normal file
112
src/APL/vercel-apl.test.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { VercelAPL, VercelAPLVariables } from "./vercel-apl";
|
||||||
|
|
||||||
|
const aplConfig = {
|
||||||
|
deploymentToken: "token",
|
||||||
|
registerAppURL: "http://example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubAuthData = {
|
||||||
|
domain: "example.com",
|
||||||
|
token: "example-token",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("APL", () => {
|
||||||
|
const initialEnv = { ...process.env };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...initialEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("VercelAPL", () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("Raise an error when configuration is missing", async () => {
|
||||||
|
delete process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL];
|
||||||
|
process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN] = "token";
|
||||||
|
|
||||||
|
expect(() => new VercelAPL()).toThrow();
|
||||||
|
|
||||||
|
process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL] = "http://example.com";
|
||||||
|
delete process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN];
|
||||||
|
|
||||||
|
expect(() => new VercelAPL()).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Constructs VercelAPL instance when deploymentToken and registerAppURL provided", async () => {
|
||||||
|
expect(() => new VercelAPL(aplConfig)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Constructs VercelAPL instance with config values from environment variables", async () => {
|
||||||
|
process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL] = aplConfig.registerAppURL;
|
||||||
|
process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN] = aplConfig.deploymentToken;
|
||||||
|
|
||||||
|
expect(() => new VercelAPL()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test if constructor use options over environment variables", async () => {
|
||||||
|
process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL] = "environment";
|
||||||
|
process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN] = "environment";
|
||||||
|
|
||||||
|
const apl = await new VercelAPL({ deploymentToken: "option", registerAppURL: "option" });
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(apl["deploymentToken"]).toBe("option");
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(apl["registerAppURL"]).toBe("option");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get", () => {
|
||||||
|
describe("Read existing auth data from env", () => {
|
||||||
|
it("Read existing auth data", async () => {
|
||||||
|
process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME] = stubAuthData.token;
|
||||||
|
process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME] = stubAuthData.domain;
|
||||||
|
|
||||||
|
const apl = new VercelAPL(aplConfig);
|
||||||
|
|
||||||
|
expect(await apl.get(stubAuthData.domain)).toStrictEqual(stubAuthData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Return undefined when unknown domain requested", async () => {
|
||||||
|
process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME] = stubAuthData.token;
|
||||||
|
process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME] = stubAuthData.domain;
|
||||||
|
|
||||||
|
const apl = new VercelAPL(aplConfig);
|
||||||
|
|
||||||
|
expect(await apl.get("unknown-domain.example.com")).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Return undefined when no data is defined", async () => {
|
||||||
|
delete process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME];
|
||||||
|
delete process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME];
|
||||||
|
|
||||||
|
const apl = new VercelAPL(aplConfig);
|
||||||
|
|
||||||
|
expect(await apl.get("example.com")).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAll", () => {
|
||||||
|
describe("Read existing auth data from env", () => {
|
||||||
|
it("Read existing auth data", async () => {
|
||||||
|
process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME] = stubAuthData.token;
|
||||||
|
process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME] = stubAuthData.domain;
|
||||||
|
|
||||||
|
const apl = new VercelAPL(aplConfig);
|
||||||
|
|
||||||
|
expect(await apl.getAll()).toStrictEqual([stubAuthData]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Return empty list when no auth data are existing", async () => {
|
||||||
|
delete process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME];
|
||||||
|
delete process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME];
|
||||||
|
|
||||||
|
const apl = new VercelAPL(aplConfig);
|
||||||
|
|
||||||
|
expect(await apl.getAll()).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
114
src/APL/vercel-apl.ts
Normal file
114
src/APL/vercel-apl.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
import { APL, AuthData } from "./apl";
|
||||||
|
import { createAPLDebug } from "./apl-debug";
|
||||||
|
|
||||||
|
const debug = createAPLDebug("VercelAPL");
|
||||||
|
|
||||||
|
export const VercelAPLVariables = {
|
||||||
|
TOKEN_VARIABLE_NAME: "SALEOR_AUTH_TOKEN",
|
||||||
|
DOMAIN_VARIABLE_NAME: "SALEOR_DOMAIN",
|
||||||
|
SALEOR_REGISTER_APP_URL: "SALEOR_REGISTER_APP_URL",
|
||||||
|
SALEOR_DEPLOYMENT_TOKEN: "SALEOR_DEPLOYMENT_TOKEN",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEnvAuth = (): AuthData | undefined => {
|
||||||
|
const token = process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME];
|
||||||
|
const domain = process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME];
|
||||||
|
if (!token || !domain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
domain,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelAPLConfig = {
|
||||||
|
registerAppURL?: string;
|
||||||
|
deploymentToken?: string;
|
||||||
|
};
|
||||||
|
/** Vercel APL
|
||||||
|
*
|
||||||
|
* Use environment variables for auth data storage. To update data on existing deployment,
|
||||||
|
* theres Saleor microservice which update new values with the Vercel API and restarts the instance.
|
||||||
|
*
|
||||||
|
* This APL should be used for single tenant purposes due to it's limitations:
|
||||||
|
* - only stores single auth data entry (setting up a new one will overwrite previous values)
|
||||||
|
* - changing the environment variables require server restart
|
||||||
|
*
|
||||||
|
* With this APL we recommend using the [Saleor CLI](https://docs.saleor.io/docs/3.x/cli),
|
||||||
|
* which automatically set up the required environment variables during deployment:
|
||||||
|
* - SALEOR_REGISTER_APP_URL: the URL for microservice which set up variables using Vercel API
|
||||||
|
* - SALEOR_DEPLOYMENT_TOKEN: token for your particular Vercel deployment
|
||||||
|
*/
|
||||||
|
export class VercelAPL implements APL {
|
||||||
|
private registerAppURL: string;
|
||||||
|
|
||||||
|
private deploymentToken: string;
|
||||||
|
|
||||||
|
constructor(config?: VercelAPLConfig) {
|
||||||
|
const registerAppURL =
|
||||||
|
config?.registerAppURL || process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL];
|
||||||
|
if (!registerAppURL) {
|
||||||
|
throw new Error("Misconfiguration: please provide registerAppUrl");
|
||||||
|
}
|
||||||
|
const deploymentToken =
|
||||||
|
config?.deploymentToken || process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN];
|
||||||
|
if (!deploymentToken) {
|
||||||
|
throw new Error("Misconfiguration: please provide deploymentToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerAppURL = registerAppURL;
|
||||||
|
this.deploymentToken = deploymentToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveDataToVercel(authData?: AuthData) {
|
||||||
|
debug(`saveDataToVercel with: ${authData}`);
|
||||||
|
try {
|
||||||
|
await fetch(this.registerAppURL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: this.deploymentToken,
|
||||||
|
envs: {
|
||||||
|
[VercelAPLVariables.TOKEN_VARIABLE_NAME]: authData?.token || "",
|
||||||
|
[VercelAPLVariables.DOMAIN_VARIABLE_NAME]: authData?.domain || "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debug("Error during saving the data:", error);
|
||||||
|
throw new Error(`VercelAPL was not able to save auth data${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(domain: string) {
|
||||||
|
const authData = getEnvAuth();
|
||||||
|
|
||||||
|
if (authData && domain === authData?.domain) {
|
||||||
|
return authData;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(authData: AuthData) {
|
||||||
|
await this.saveDataToVercel(authData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(domain: string) {
|
||||||
|
if (domain === getEnvAuth()?.domain) {
|
||||||
|
// Override existing data with the empty values
|
||||||
|
await this.saveDataToVercel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
const authData = getEnvAuth();
|
||||||
|
if (!authData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [authData];
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import { createDebug } from "../debug";
|
||||||
export const createMiddlewareDebug = (middleware: string) =>
|
export const createMiddlewareDebug = (middleware: string) =>
|
||||||
createDebug(`Middleware:${middleware}`);
|
createDebug(`Middleware:${middleware}`);
|
||||||
|
|
||||||
type DebugFactory = (handlerName: string) => (msg: string, ...args: any[]) => void;
|
type DebugFactory = (handlerName: string) => (msg: string, ...args: unknown[]) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental. Needs to be tested and evaluated on security
|
* Experimental. Needs to be tested and evaluated on security
|
||||||
|
|
Loading…
Reference in a new issue