feat: extract cms from mp/cms — initial libreshop/cms
Some checks failed
Build and publish / build (push) Failing after 17s

Source moved verbatim from mp/cms/ on 2026-04-29; mp was the first
concrete adapter consuming the libreshop toolkit. Builds and publishes
git.librete.ch/libreshop/cms on every main / v* push via the standard
.gitea/workflows/build.yml shared across libreshop components.
This commit is contained in:
Michael Czechowski
2026-04-29 17:48:30 +02:00
commit 32a296baf2
127 changed files with 44618 additions and 0 deletions

22
config/admin.ts Normal file
View File

@@ -0,0 +1,22 @@
export default ({ env }) => ({
auth: {
secret: env("ADMIN_JWT_SECRET")
},
apiToken: {
salt: env("API_TOKEN_SALT")
},
transfer: {
token: {
salt: env("TRANSFER_TOKEN_SALT")
}
},
autoOpen: false,
// watchIgnoreFiles: [
// "**/controllers/**",
// "**/database/**",
// ],
flags: {
nps: false,
promoteEE: false
}
});

7
config/api.ts Normal file
View File

@@ -0,0 +1,7 @@
export default {
rest: {
defaultLimit: 100,
maxLimit: 1_000,
withCount: true
}
};

19
config/constants.ts Normal file
View File

@@ -0,0 +1,19 @@
import { Environment } from "@paypal/paypal-server-sdk";
export const pdfApiUrl = process.env.PDF_API_ADDRESS!;
export const mailApiUrl = process.env.MAIL_API_ADDRESS!;
export const baseUrl = process.env.BASE_URL!;
export const paypalClientId = process.env.PAYPAL_CLIENT_ID!;
export const paypalClientSecret = process.env.PAYPAL_CLIENT_SECRET!;
export const paypalEnvironment = process.env.PAYPAL_ENVIRONMENT! === "production" ? Environment.Production : Environment.Sandbox;
export const adminEmail = process.env.ADMIN_EMAIL_ADDRESS!;
// TODO: Should be retrieved from DepotApi
export const vatIncludedDecimal = 1.19;
// TODO: Should be retrieved from DepotApi
export const vatDecimal = 0.19;
export const vatDecimalExcluded = 1 - vatDecimal;
export const maxProductsSitemap = 500;

79
config/database.ts Normal file
View File

@@ -0,0 +1,79 @@
import path from "path";
export default ({ env }) => {
const client = env("DATABASE_CLIENT", "sqlite");
const connections = {
mysql: {
connection: {
connectionString: env("DATABASE_URL"),
host: env("DATABASE_HOST", "localhost"),
port: env.int("DATABASE_PORT", 3306),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool("DATABASE_SSL", false) && {
key: env("DATABASE_SSL_KEY", undefined),
cert: env("DATABASE_SSL_CERT", undefined),
ca: env("DATABASE_SSL_CA", undefined),
capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool("DATABASE_SSL_REJECT_UNAUTHORIZED", true)
}
},
pool: { min: env.int("DATABASE_POOL_MIN", 2), max: env.int("DATABASE_POOL_MAX", 10) }
},
mysql2: {
connection: {
host: env("DATABASE_HOST", "localhost"),
port: env.int("DATABASE_PORT", 3306),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool("DATABASE_SSL", false) && {
key: env("DATABASE_SSL_KEY", undefined),
cert: env("DATABASE_SSL_CERT", undefined),
ca: env("DATABASE_SSL_CA", undefined),
capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool("DATABASE_SSL_REJECT_UNAUTHORIZED", true)
}
},
pool: { min: env.int("DATABASE_POOL_MIN", 2), max: env.int("DATABASE_POOL_MAX", 10) }
},
postgres: {
connection: {
connectionString: env("DATABASE_URL"),
host: env("DATABASE_HOST", "localhost"),
port: env.int("DATABASE_PORT", 5432),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool("DATABASE_SSL", false) && {
key: env("DATABASE_SSL_KEY", undefined),
cert: env("DATABASE_SSL_CERT", undefined),
ca: env("DATABASE_SSL_CA", undefined),
capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool("DATABASE_SSL_REJECT_UNAUTHORIZED", true)
},
schema: env("DATABASE_SCHEMA", "public")
},
pool: { min: env.int("DATABASE_POOL_MIN", 2), max: env.int("DATABASE_POOL_MAX", 10) }
},
sqlite: {
connection: {
filename: path.join(__dirname, "..", "..", env("DATABASE_FILENAME", ".tmp/data.db"))
},
useNullAsDefault: true
}
};
return {
connection: {
client,
...connections[client],
acquireConnectionTimeout: env.int("DATABASE_CONNECTION_TIMEOUT", 60000)
}
};
};

16
config/logger.ts Normal file
View File

@@ -0,0 +1,16 @@
import { winston } from "@strapi/logger";
export default {
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.printf(({ timestamp, level, message, ...rest }) => {
let restString = JSON.stringify(rest, undefined, 2);
restString = restString === "{}" ? "" : restString;
return `${timestamp}Z ${level}: ${message} ${restString}`;
})
)
})
]
};

12
config/middlewares.ts Normal file
View File

@@ -0,0 +1,12 @@
export default [
"strapi::errors",
"strapi::security",
"strapi::cors",
// 'strapi::poweredBy',
"strapi::logger",
"strapi::query",
"strapi::body",
"strapi::session",
"strapi::favicon",
"strapi::public"
];

749
config/plugins.ts Normal file
View File

@@ -0,0 +1,749 @@
export default ({ env }) => ({
upload: {
config: {
sizeLimit: 50 * 1024 * 1024
}
},
documentation: {
enabled: false,
config: {
openapi: "3.0.1",
info: {
version: "1.0.0",
title: "MUELLERPTINTS. Paperwork",
description: "API Documentation for MUELLERPRINTS. Paperwork",
termsOfService: false,
contact: {
name: "Michael W. Czechowski",
email: "mail@dailysh.it",
url: "https://dailysh.it"
},
license: "Copyright (C) 2024 Michael W. Czechowski",
externalDocs: false
},
"x-strapi-config": {
plugins: [],
path: "/documentation",
mutateDocumentation: (draft: any) => {
// Order endpoints - maintain existing modifications
// draft.paths["/orders/{uuid}/cart"].get.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/cart"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/generate-delivery-note"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/generate-invoice"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/add-product/{productId}"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/remove-product/{productId}"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/checkout"].post.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/send-invoice"].put.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}/send-delivery-note"].put.parameters[0].schema.type = "string";
//
// // Order request body modifications - maintain existing
// delete draft.paths["/orders/{uuid}/cart"].put.requestBody;
// delete draft.paths["/orders/{uuid}/add-product/{productId}"].put.requestBody;
// delete draft.paths["/orders/{uuid}/remove-product/{productId}"].put.requestBody;
// delete draft.paths["/orders/{uuid}/checkout"].post.requestBody;
//
// // Product publish endpoint - existing documentation
// draft.paths["/products/publish"].post.description = "Publish or unpublish products by filter";
// draft.paths["/products/publish"].post.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// required: ["filters"],
// properties: {
// filters: {
// type: "object",
// description: "Filters to select products (supports pattern, cover, ruling, pages)"
// },
// publish: {
// type: "boolean",
// description: "Whether to publish (true) or unpublish (false)",
// default: true
// },
// dryRun: {
// type: "boolean",
// description: "If true, no changes will be made but count will be returned",
// default: false
// }
// }
// }
// }
// }
// };
//
// draft.paths["/products/publish"].post.responses = {
// "200": {
// description: "Successfully published/unpublished products",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// published: {
// type: "integer",
// description: "Number of products published/unpublished"
// },
// dryRun: {
// type: "boolean",
// description: "Whether this was a dry run"
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// // Product allVariants endpoint
// if (draft.paths["/products/{id}/variants/all"]) {
// draft.paths["/products/{id}/variants/all"].get.description = "Get all variants for a product";
// draft.paths["/products/{id}/variants/all"].get.responses = {
// "200": {
// description: "Returns all variants of the product",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Product"
// }
// }
// }
// }
// },
// "404": {
// description: "Product not found",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// }
//
// // Product variantsByPattern endpoint
// if (draft.paths["/products/{id}/variants/pattern"]) {
// draft.paths["/products/{id}/variants/pattern"].get.description = "Get product variants grouped by pattern";
// draft.paths["/products/{id}/variants/pattern"].get.responses = {
// "200": {
// description: "Returns product variants grouped by pattern",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// allProductPattern: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductPattern"
// }
// },
// productVariants: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Product"
// }
// },
// patterns: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// description: {
// type: "string"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// }
//
// // Product variants endpoint
// if (draft.paths["/products/{id}/variants"]) {
// draft.paths["/products/{id}/variants"].get.description = "Get product variants grouped by pages, cover, and ruling";
// draft.paths["/products/{id}/variants"].get.responses = {
// "200": {
// description: "Returns product variants grouped by pages, cover, and ruling",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// pages: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// },
// cover: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// binding: {
// type: "string"
// },
// price: {
// type: "number"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// },
// ruling: {
// type: "array",
// items: {
// type: "object",
// properties: {
// id: {
// type: "string"
// },
// name: {
// type: "string"
// },
// productVariant: {
// oneOf: [
// {
// $ref: "#/components/schemas/Product"
// },
// {
// type: "null"
// }
// ]
// }
// }
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// error: {
// type: "string",
// description: "Error message"
// }
// }
// }
// }
// }
// }
// };
// }
//
// // Product Category endpoints
// if (draft.paths["/product-category"]) {
// draft.paths["/product-category"].get.description = "Get all product categories";
// draft.paths["/product-category"].get.responses = {
// "200": {
// description: "Returns all product categories",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// if (draft.paths["/product-category/corrupt"]) {
// draft.paths["/product-category/corrupt"].get.description = "Get all corrupted product categories";
// draft.paths["/product-category/corrupt"].get.responses = {
// "200": {
// description: "Returns all corrupted product categories",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// if (draft.paths["/product-category/{id}"]) {
// draft.paths["/product-category/{id}"].get.description = "Get a single product category";
// draft.paths["/product-category/{id}"].get.responses = {
// "200": {
// description: "Returns a single product category",
// content: {
// "application/json": {
// schema: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
//
// draft.paths["/product-category/{id}"].put.description = "Update a product category";
// draft.paths["/product-category/{id}"].put.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// description: "The data to update the category with"
// }
// }
// }
// };
// draft.paths["/product-category/{id}"].put.responses = {
// "200": {
// description: "Returns the updated product category",
// content: {
// "application/json": {
// schema: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// // Update endpoint to match the actual route from product-category.ts
// if (draft.paths["/product-category/bulk"]) {
// draft.paths["/product-category/bulk"].put.description = "Update multiple product categories";
// draft.paths["/product-category/bulk"].put.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// categories: {
// type: "array",
// description: "Array of categories to update"
// }
// }
// }
// }
// }
// };
// draft.paths["/product-category/bulk"].put.responses = {
// "200": {
// description: "Returns the updated product categories",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
// // Remove the incorrect "/product-category/update" documentation since it doesn't exist in routes
//
// if (draft.paths["/product-category/fix"]) {
// draft.paths["/product-category/fix"].put.description = "Fix categories with missing cover or pattern";
// draft.paths["/product-category/fix"].put.responses = {
// "200": {
// description: "Returns information about fixed and failed categories",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// fixed: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// },
// failed: {
// type: "array",
// items: {
// $ref: "#/components/schemas/ProductCategory"
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// if (draft.paths["/product-category/fix-images"]) {
// draft.paths["/product-category/fix-images"].put.description = "Fix product images in categories";
// draft.paths["/product-category/fix-images"].put.responses = {
// "200": {
// description: "Returns information about fixed and failed product images",
// content: {
// "application/json": {
// schema: {
// type: "object",
// properties: {
// fixed: {
// type: "array",
// items: {
// type: "object",
// properties: {
// categoryId: {
// type: "string"
// },
// productCount: {
// type: "integer"
// },
// products: {
// type: "array",
// items: {
// type: "string"
// }
// }
// }
// }
// },
// failed: {
// type: "array",
// items: {
// type: "object",
// properties: {
// categoryId: {
// type: "string"
// },
// reason: {
// type: "string"
// },
// error: {
// type: "string"
// }
// }
// }
// }
// }
// }
// }
// }
// },
// "400": {
// description: "Bad request"
// }
// };
// }
//
// // Content controller endpoints
// if (draft.paths["/content"]) {
// draft.paths["/content"].get.description = "Get all content entries";
// draft.paths["/content"].get.responses = {
// "200": {
// description: "Returns all content entries",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Content"
// }
// }
// }
// }
// }
// };
// }
//
// // Customer controller endpoints
// if (draft.paths["/customers"]) {
// draft.paths["/customers"].get.description = "Get all customers";
// draft.paths["/customers"].get.responses = {
// "200": {
// description: "Returns all customers",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Customer"
// }
// }
// }
// }
// }
// };
// }
//
// // Legal controller endpoints
// if (draft.paths["/legal"]) {
// draft.paths["/legal"].get.description = "Get all legal documents";
// draft.paths["/legal"].get.responses = {
// "200": {
// description: "Returns all legal documents",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Legal"
// }
// }
// }
// }
// }
// };
// }
//
// // Delivery controller endpoints
// if (draft.paths["/deliveries"]) {
// draft.paths["/deliveries"].get.description = "Get all deliveries";
// draft.paths["/deliveries"].get.responses = {
// "200": {
// description: "Returns all deliveries",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Delivery"
// }
// }
// }
// }
// }
// };
// }
//
// // Payment controller endpoints
// if (draft.paths["/payments"]) {
// draft.paths["/payments"].get.description = "Get all payments";
// draft.paths["/payments"].get.responses = {
// "200": {
// description: "Returns all payments",
// content: {
// "application/json": {
// schema: {
// type: "array",
// items: {
// $ref: "#/components/schemas/Payment"
// }
// }
// }
// }
// }
// };
// }
//
// if (draft.paths["/orders/{uuid}"]) {
// draft.paths["/orders/{uuid}"].get.description = "Get order by UUID";
// draft.paths["/orders/{uuid}"].get.parameters[0].schema.type = "string";
// draft.paths["/orders/{uuid}"].get.responses = {
// "200": {
// description: "Returns order details",
// content: {
// "application/json": {
// schema: {
// $ref: "#/components/schemas/Order"
// }
// }
// }
// },
// "404": {
// description: "Order not found"
// }
// };
// }
//
// if (draft.paths["/orders/webhook"]) {
// draft.paths["/orders/webhook"].post.description = "Handle PayPal webhook events";
// draft.paths["/orders/webhook"].post.requestBody = {
// required: true,
// content: {
// "application/json": {
// schema: {
// type: "object",
// description: "PayPal webhook event payload"
// }
// }
// }
// };
// draft.paths["/orders/webhook"].post.responses = {
// "200": {
// description: "Webhook processed successfully"
// },
// "500": {
// description: "Internal server error"
// }
// };
// }
}
},
servers: [
{
url: "/api",
description: "API server"
}
],
security: [
{
bearerAuth: []
}
]
}
},
"strapi-prometheus": {
enabled: false,
config: {
// add prefix to all the prometheus metrics names.
prefix: "cms",
// use full url instead of matched url
// true => path label: `/api/models/1`
// false => path label: `/api/models/:id`
fullURL: false,
// include url query in the url label
// true => path label: `/api/models?limit=1`
// false => path label: `/api/models`
includeQuery: false,
// metrics that will be enabled, by default they are all enabled.
enabledMetrics: {
koa: true, // koa metrics
process: true, // metrics regarding the running process
http: true, // http metrics like response time and size
apollo: false // metrics regarding graphql
},
// interval at which rate metrics are collected in ms
interval: 30_000,
// set custom/default labels to all the prometheus metrics
customLabels: {
name: "strapi-prometheus"
}
}
}
});

13
config/server.ts Normal file
View File

@@ -0,0 +1,13 @@
export default ({ env }) => ({
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 5555),
app: {
keys: env.array("APP_KEYS")
},
webhooks: {
populateRelations: env.bool("WEBHOOKS_POPULATE_RELATIONS", false)
},
logger: {
level: "info"
}
});