Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions example-app-invoices/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Saleor App Configuration
# ========================

# Required: Secret key for encrypting metadata (generate a random 32+ character string)
# Used by EncryptedMetadataManager for securing app configuration data
SECRET_KEY=your-secret-key-here-minimum-32-characters

# APL (Auth Persistence Layer) Configuration
# ==========================================

# APL Type: Choose one of "file", "upstash", or "rest"
# - "file": Single tenant, local development (default)
# - "upstash": Multi-tenant, production ready
# - "rest": Saleor Cloud APL
APL=file

# For APL type "rest" (Saleor Cloud):
# REST_APL_ENDPOINT=https://your-saleor-cloud-instance.saleor.cloud/graphql/
# REST_APL_TOKEN=your-saleor-cloud-token

# For APL type "upstash":
# UPSTASH_REDIS_REST_URL=your-upstash-redis-url
# UPSTASH_REDIS_REST_TOKEN=your-upstash-redis-token

# App URLs and Base Configuration
# ===============================

# Base URL for your app (used for manifest and webhooks)
# In development, this is typically http://localhost:3000
# In production, this should be your deployed app URL
APP_BASE_URL=http://localhost:3000

# Optional: Override iframe base URL (for embedded app interface)
# If not set, uses APP_BASE_URL
APP_IFRAME_BASE_URL=http://localhost:3000

# Optional: Override API base URL (for webhooks and API endpoints)
# If not set, uses APP_BASE_URL
APP_API_BASE_URL=http://localhost:3000
Comment on lines +25 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: These are actually not required to run the app. It's used when you need to have app work with Saleor running in a Docker container for example

It's described here in our docs: https://docs.saleor.io/developer/extending/apps/local-app-development#apps-url-overriding

I'm worried this description suggests you must provide this, which would be incorrect: it's used only for development, if you would use these env variables on production app it wouldn't have any multitenancy, since it would always try to connect to the same Saleor instance.


# Domain Security
# ===============

# Optional: Regex pattern to restrict which Saleor instances can install this app
# Example: ".*\\.saleor\\.cloud$" to only allow Saleor Cloud instances
# If not set, all domains are allowed
ALLOWED_DOMAIN_PATTERN=

# Server Configuration
# ====================

# Port for the development server (default: 3000)
PORT=3000

# Vercel deployment URL (automatically set by Vercel)
# VERCEL_URL=https://your-app.vercel.app

# Logging Configuration
# ====================

# Log level: debug, info, warn, error (default: info)
APP_LOG_LEVEL=info

# OpenTelemetry Configuration (optional)
# =====================================

# Enable OpenTelemetry logging (true/false)
OTEL_ENABLED=false

# Service name for OpenTelemetry
OTEL_SERVICE_NAME=saleor-invoice-app

# CI/CD Environment
# =================

# Set to "true" when running in CI/CD environment
CI=false

# File Storage Configuration
# ==========================

# Directory for temporary PDF storage (default: auto-generated temp directory)
# Ensure this directory is writable by the application
TEMP_PDF_STORAGE_DIR=

# Saleor Cloud Migration Scripts (optional)
# =========================================

# Required for running migration scripts with Saleor Cloud APL
# SALEOR_CLOUD_TOKEN=your-saleor-cloud-token
# SALEOR_CLOUD_RESOURCE_URL=https://your-saleor-cloud-instance.saleor.cloud/graphql/

# Development Notes:
# ==================
#
# 1. For local development, you only need:
# - SECRET_KEY (required)
# - APP_BASE_URL (optional, defaults to http://localhost:3000)
# - APL=file (default)
Comment on lines +96 to +99
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think if we only need these 3 we should probably have only these variables commented out, rest should be commented to avoid accidentaly using them

#
# 2. For production deployment:
# - Set APL to "upstash" or "rest"
# - Configure the corresponding APL environment variables
# - Set APP_BASE_URL to your production URL
# - Ensure SECRET_KEY is a strong, random string
#
# 3. For Saleor Cloud deployment:
# - Set APL=rest
# - Configure REST_APL_ENDPOINT and REST_APL_TOKEN
# - Set SALEOR_CLOUD_TOKEN and SALEOR_CLOUD_RESOURCE_URL for migrations
Comment on lines +107 to +110
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: could you please remove this part? 🙏🏻 we don't need instructions for Saleor Cloud deployment in example 😉

#
# 4. Security considerations:
# - Never commit SECRET_KEY to version control
# - Use strong, random values for all tokens
# - Restrict ALLOWED_DOMAIN_PATTERN in production
14 changes: 7 additions & 7 deletions example-app-invoices/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@saleor/app-sdk": "0.50.1",
"@saleor/app-sdk": "^1.3.0",
"@saleor/macaw-ui": "1.0.0-pre.7",
"@tanstack/react-query": "4.29.19",
"@trpc/client": "10.43.1",
Expand All @@ -23,10 +23,10 @@
"@urql/exchange-auth": "2.1.6",
"@web-std/file": "^3.0.2",
"dotenv": "16.3.1",
"graphql": "16.7.1",
"graphql": "^16.11.0",
"graphql-tag": "2.12.6",
"microinvoice": "^1.0.6",
"next": "14.2.3",
"next": "^14.2.32",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.41.0",
Expand All @@ -35,7 +35,7 @@
"tslog": "^4.9.3",
"urql": "4.0.7",
"usehooks-ts": "^2.9.1",
"zod": "3.21.4"
"zod": "^3.25.76"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.2",
Expand All @@ -54,10 +54,10 @@
"dotenv": "16.3.1",
"jsdom": "^20.0.3",
"rimraf": "^3.0.2",
"tsx": "4.7.1",
"tsx": "^4.20.5",
"typescript": "5.4.5",
"vite": "5.3.3",
"vitest": "1.6.0"
"vite": "^5.4.19",
"vitest": "^1.6.1"
},
"private": true,
"saleor": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { createGraphQLClient } from "../../src/lib/create-graphql-client";
import { createSettingsManager } from "../../src/modules/app-configuration/metadata-manager";
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
import { SaleorCloudAPL } from "@saleor/app-sdk/APL/saleor-cloud";

export const getMetadataManagerForEnv = (apiUrl: string, appToken: string) => {
const client = createGraphQLClient({
Expand Down
2 changes: 1 addition & 1 deletion example-app-invoices/src/logger-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
SALEOR_API_URL_HEADER,
SALEOR_EVENT_HEADER,
} from "@saleor/app-sdk/const";
} from "@saleor/app-sdk/headers";
import { AsyncLocalStorage } from "async_hooks";
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
FetchAppDetailsDocument,
FetchAppDetailsQuery,
UpdateAppMetadataDocument,
RemoveMetadataDocument,
} from "../../../generated/graphql";

gql`
Expand Down Expand Up @@ -96,6 +97,36 @@ export async function mutateMetadata(client: SimpleGraphqlClient, metadata: Meta
);
}

export async function deleteMetadata(client: SimpleGraphqlClient, keys: string[]) {
// to delete the metadata, ID is required
const { error: idQueryError, data: idQueryData } = await client
.query(FetchAppDetailsDocument, {})
.toPromise();

if (idQueryError) {
throw new Error(
"Could not fetch the app id. Please check if auth data for the client are valid."
);
}

const appId = idQueryData?.app?.id;

if (!appId) {
throw new Error("Could not fetch the app ID");
}

const { error: mutationError } = await client
.mutation(RemoveMetadataDocument, {
id: appId,
keys: keys,
})
.toPromise();

if (mutationError) {
throw new Error(`Delete mutation error: ${mutationError.message}`);
}
}

export const createSettingsManager = (client: SimpleGraphqlClient): SettingsManager => {
/*
* EncryptedMetadataManager gives you interface to manipulate metadata and cache values in memory.
Expand All @@ -107,5 +138,6 @@ export const createSettingsManager = (client: SimpleGraphqlClient): SettingsMana
encryptionKey: process.env.SECRET_KEY!,
fetchMetadata: () => fetchAllMetadata(client),
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
deleteMetadata: (keys) => deleteMetadata(client, keys),
});
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next";
import { verifyJWT } from "@saleor/app-sdk/verify-jwt";
// import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next";
// import { verifyJWT } from "@saleor/app-sdk/verify-jwt";
import { createGraphQLClient } from "../../lib/create-graphql-client";
import { TRPCError } from "@trpc/server";
import { createLogger } from "../../logger";
Expand Down Expand Up @@ -75,26 +75,27 @@ const validateClientToken = middleware(async ({ ctx, next, meta }) => {
});
}

try {
logger.debug("trying to verify JWT token from frontend");
logger.debug({ token: ctx.token ? `${ctx.token[0]}...` : undefined });

await verifyJWT({
appId: ctx.appId,
token: ctx.token,
saleorApiUrl: ctx.saleorApiUrl,
requiredPermissions: [
...REQUIRED_SALEOR_PERMISSIONS,
...(meta?.requiredClientPermissions || []),
],
});
} catch (e) {
logger.debug("JWT verification failed, throwing");
throw new ProtectedHandlerError(
"JWT verification failed: ",
"JWT_VERIFICATION_FAILED",
);
}
// TODO: JWT verification is temporarily disabled due to missing verifyJWT function
// try {
// logger.debug("trying to verify JWT token from frontend");
// logger.debug({ token: ctx.token ? `${ctx.token[0]}...` : undefined });

// await verifyJWT({
// appId: ctx.appId,
// token: ctx.token,
// saleorApiUrl: ctx.saleorApiUrl,
// requiredPermissions: [
// ...REQUIRED_SALEOR_PERMISSIONS,
// ...(meta?.requiredClientPermissions || []),
// ],
// });
// } catch (e) {
// logger.debug("JWT verification failed, throwing");
// throw new TRPCError({
// code: "UNAUTHORIZED",
// message: "JWT verification failed",
// });
// }
Comment on lines +78 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: we cannot merge this with missing JWT validation. It looks like there shouldn't be any issue here, since we also use this method in other Saleor apps. Example avatax app: https://github.com/saleor/apps/blob/3935a8bec907e05e94287bf330a4bd3b4fbe7849/apps/avatax/src/modules/trpc/protected-client-procedure.ts?plain=1#L1 (it uses app sdk v1.3.0)

Could you please add this back?


return next({
ctx: {
Expand Down
2 changes: 1 addition & 1 deletion example-app-invoices/src/modules/trpc/trpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { httpBatchLink } from "@trpc/client";
import {
SALEOR_API_URL_HEADER,
SALEOR_AUTHORIZATION_BEARER_HEADER,
} from "@saleor/app-sdk/const";
} from "@saleor/app-sdk/headers";

function getBaseUrl() {
if (typeof window !== "undefined") return "";
Expand Down
2 changes: 1 addition & 1 deletion example-app-invoices/src/modules/trpc/trpc-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as trpcNext from "@trpc/server/adapters/next";
import {
SALEOR_AUTHORIZATION_BEARER_HEADER,
SALEOR_API_URL_HEADER,
} from "@saleor/app-sdk/const";
} from "@saleor/app-sdk/headers";
import { inferAsyncReturnType } from "@trpc/server";

export const createTrpcContext = async ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/headers";
import {
NextWebhookApiHandler,
NextJsWebhookHandler,
SaleorAsyncWebhook,
} from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "../../../lib/create-graphql-client";
Expand Down Expand Up @@ -137,7 +137,7 @@ export const invoiceRequestedWebhook =
event: "INVOICE_REQUESTED",
apl: saleorApp.apl,
query: InvoiceRequestedSubscription,
onError(error, req, res) {
onError(error, req) {
const saleorApiUrl = req.headers[SALEOR_API_URL_HEADER] as string;

logger.error("Error during webhook handling", { error, saleorApiUrl });
Expand All @@ -153,7 +153,7 @@ const invoiceNumberGenerator = new InvoiceNumberGenerator();
* More logs
* Extract service
*/
export const handler: NextWebhookApiHandler<
export const handler: NextJsWebhookHandler<
InvoiceRequestedPayloadFragment
> = async (req, res, context) => {
const { authData, payload, baseUrl } = context;
Expand Down
14 changes: 12 additions & 2 deletions example-app-invoices/src/saleor-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { APL } from "@saleor/app-sdk/APL";
import { FileAPL } from "@saleor/app-sdk/APL/file";
import { SaleorCloudAPL } from "@saleor/app-sdk/APL/saleor-cloud";
import { UpstashAPL } from "@saleor/app-sdk/APL/upstash";
import { SaleorApp } from "@saleor/app-sdk/saleor-app";

const aplType = process.env.APL ?? "file";
Expand All @@ -7,7 +10,14 @@ let apl: APL;

switch (aplType) {
case "upstash":
apl = new UpstashAPL();
if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
throw new Error("Upstash APL is not configured - missing env variables. Check saleor-app.ts");
}

apl = new UpstashAPL({
restURL: process.env.UPSTASH_REDIS_REST_URL,
restToken: process.env.UPSTASH_REDIS_REST_TOKEN,
});

break;
case "file":
Expand Down