Add APL for storing installation data
This commit is contained in:
parent
cd65676aa5
commit
2eea13fe81
8 changed files with 2138 additions and 13 deletions
51
deno-apl.ts
Normal file
51
deno-apl.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
APL,
|
||||
AuthData,
|
||||
AplReadyResult,
|
||||
AplConfiguredResult,
|
||||
} from "npm:@saleor/app-sdk@0.43.0/APL";
|
||||
|
||||
const kv = await Deno.openKv();
|
||||
|
||||
const KV_KEY = "authData";
|
||||
|
||||
export class DenoAPL implements APL {
|
||||
async get(saleorApiUrl: string) {
|
||||
const authData = await kv.get<AuthData>([KV_KEY, saleorApiUrl]);
|
||||
if (authData.value) {
|
||||
return authData.value;
|
||||
}
|
||||
}
|
||||
|
||||
async set(authData: AuthData) {
|
||||
await kv.set([KV_KEY, authData.saleorApiUrl], authData);
|
||||
}
|
||||
|
||||
async delete(saleorApiUrl: string) {
|
||||
return await kv.delete([KV_KEY, saleorApiUrl]);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const list = kv.list<AuthData>({ prefix: [KV_KEY] });
|
||||
|
||||
const authDataList: AuthData[] = [];
|
||||
|
||||
for await (const res of list) {
|
||||
authDataList.push(res.value);
|
||||
}
|
||||
|
||||
return authDataList;
|
||||
}
|
||||
|
||||
async isReady(): Promise<AplReadyResult> {
|
||||
return {
|
||||
ready: true,
|
||||
};
|
||||
}
|
||||
|
||||
async isConfigured(): Promise<AplConfiguredResult> {
|
||||
return {
|
||||
configured: true,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"importMap": "./import_map.json",
|
||||
"tasks": {
|
||||
"start": "deno run -A -r main.ts"
|
||||
}
|
||||
"importMap": "./import_map.json",
|
||||
"tasks": {
|
||||
"start": "deno run -A --unstable main.ts"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"imports": {
|
||||
"http/": "https://deno.land/std@0.160.0/http/",
|
||||
"wren/": "https://deno.land/x/wren@0.8.0/"
|
||||
}
|
||||
"imports": {
|
||||
"http/": "https://deno.land/std@0.160.0/http/",
|
||||
"wren/": "https://deno.land/x/wren@0.8.0/"
|
||||
}
|
||||
}
|
||||
|
|
36
invariant.ts
Normal file
36
invariant.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
// From https://github.com/alexreardon/tiny-invariant/blob/master/src/tiny-invariant.ts
|
||||
|
||||
const isDenoDeploy: boolean = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined;
|
||||
const prefix: string = "Invariant failed";
|
||||
|
||||
// Throw an error if the condition fails
|
||||
// Strip out error messages for production
|
||||
// > Not providing an inline default argument for message as the result is smaller
|
||||
export default function invariant(
|
||||
condition: any,
|
||||
// Can provide a string, or a function that returns a string for cases where
|
||||
// the message takes a fair amount of effort to compute
|
||||
message?: string | (() => string)
|
||||
): asserts condition {
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
// Condition not passed
|
||||
|
||||
// In production we strip the message but still throw
|
||||
if (isDenoDeploy) {
|
||||
throw new Error(prefix);
|
||||
}
|
||||
|
||||
// When not in production we allow the message to pass through
|
||||
// *This block will be removed in production builds*
|
||||
|
||||
const provided: string | undefined =
|
||||
typeof message === "function" ? message() : message;
|
||||
|
||||
// Options:
|
||||
// 1. message provided: `${prefix}: ${provided}`
|
||||
// 2. message not provided: prefix
|
||||
const value: string = provided ? `${prefix}: ${provided}` : prefix;
|
||||
throw new Error(value);
|
||||
}
|
2
justfile
2
justfile
|
@ -1,2 +1,2 @@
|
|||
run:
|
||||
deno run -A main.ts
|
||||
deno run -A --unstable main.ts
|
||||
|
|
63
main.ts
63
main.ts
|
@ -1,3 +1,5 @@
|
|||
/// <reference lib="deno.unstable" />
|
||||
|
||||
import { serve } from "wren/mod.ts";
|
||||
import { GET, POST } from "wren/route.ts";
|
||||
import * as Response from "wren/response.ts";
|
||||
|
@ -10,6 +12,15 @@ import {
|
|||
transactionInitialize,
|
||||
transactionProcess,
|
||||
} from "./subscriptions.ts";
|
||||
import {
|
||||
SALEOR_DOMAIN_HEADER,
|
||||
SALEOR_API_URL_HEADER,
|
||||
} from "npm:@saleor/app-sdk@0.43.0/const";
|
||||
import { DenoAPL } from "./deno-apl.ts";
|
||||
import { fetchRemoteJwks, getAppId } from "./utils.ts";
|
||||
import { AuthData } from "npm:@saleor/app-sdk@0.43.0/APL";
|
||||
|
||||
const apl = new DenoAPL();
|
||||
|
||||
interface ActionRequestResponse {
|
||||
pspReference: string;
|
||||
|
@ -102,17 +113,61 @@ const routes = [
|
|||
} satisfies AppManifest);
|
||||
}),
|
||||
POST("/install", async (req) => {
|
||||
console.log("install");
|
||||
const json = await req.json();
|
||||
console.log("install", json);
|
||||
const authToken = json.auth_token;
|
||||
const saleorDomain = req.headers.get(SALEOR_DOMAIN_HEADER);
|
||||
const saleorApiUrl = req.headers.get(SALEOR_API_URL_HEADER);
|
||||
|
||||
if (!authToken || !saleorDomain || !saleorApiUrl) {
|
||||
return Response.BadRequest({
|
||||
code: "MISSING_HEADER",
|
||||
message: "One of requried headers is missing",
|
||||
});
|
||||
}
|
||||
|
||||
const appId = await getAppId({ saleorApiUrl, token: authToken });
|
||||
|
||||
if (!appId) {
|
||||
return Response.BadRequest({
|
||||
code: "UNKNOWN_APP_ID",
|
||||
message: `The auth data given during registration request could not be used to fetch app ID.
|
||||
This usually means that App could not connect to Saleor during installation. Saleor URL that App tried to connect: ${saleorApiUrl}`,
|
||||
});
|
||||
}
|
||||
|
||||
const jwks = await fetchRemoteJwks(saleorApiUrl);
|
||||
if (!jwks) {
|
||||
return Response.BadRequest({
|
||||
code: "JWKS_NOT_AVAILABLE",
|
||||
message: "Can't fetch the remote JWKS.",
|
||||
});
|
||||
}
|
||||
|
||||
const authData: AuthData = {
|
||||
domain: saleorDomain,
|
||||
token: authToken,
|
||||
saleorApiUrl,
|
||||
appId,
|
||||
jwks,
|
||||
};
|
||||
|
||||
try {
|
||||
apl.set(authData);
|
||||
} catch (_e) {
|
||||
return Response.InternalServerError({
|
||||
code: "APL_SAVE_ERROR",
|
||||
message: "Cannot save APL",
|
||||
});
|
||||
}
|
||||
|
||||
return Response.OK({
|
||||
success: true,
|
||||
});
|
||||
}),
|
||||
POST("/gateway-initialize", async (req: Request) => {
|
||||
const json = await req.json();
|
||||
console.log("gateway initialize", json);
|
||||
console.log("headers", req.headers);
|
||||
console.log("/gateway-initialize - json", json);
|
||||
console.log("/gateway-initialize - headers", req.headers);
|
||||
return Response.OK({
|
||||
data: {
|
||||
some: "data",
|
||||
|
|
55
utils.ts
Normal file
55
utils.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { getJwksUrlFromSaleorApiUrl } from "npm:@saleor/app-sdk@0.43.0/urls";
|
||||
|
||||
export const fetchRemoteJwks = async (saleorApiUrl: string) => {
|
||||
const jwksResponse = await fetch(getJwksUrlFromSaleorApiUrl(saleorApiUrl));
|
||||
return jwksResponse.text();
|
||||
};
|
||||
|
||||
type GetIdResponseType = {
|
||||
data?: {
|
||||
app?: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export interface GetAppIdProperties {
|
||||
saleorApiUrl: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const getAppId = async ({
|
||||
saleorApiUrl,
|
||||
token,
|
||||
}: GetAppIdProperties): Promise<string | undefined> => {
|
||||
try {
|
||||
const response = await fetch(saleorApiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
{
|
||||
app{
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
console.error(
|
||||
`Could not get the app ID: Saleor API has response code ${response.status}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
const body = (await response.json()) as GetIdResponseType;
|
||||
const appId = body.data?.app?.id;
|
||||
return appId;
|
||||
} catch (e) {
|
||||
console.error("Could not get the app ID: %O", e);
|
||||
return undefined;
|
||||
}
|
||||
};
|
Loading…
Reference in a new issue