initial commit

This commit is contained in:
2026-09-11 02:46:56 +02:00
commit 20b384039c
288 changed files with 46557 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
import type { Core } from '@strapi/strapi';
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Admin => ({
auth: {
secret: env('ADMIN_JWT_SECRET')!,
},
apiToken: {
salt: env('API_TOKEN_SALT')!,
},
transfer: {
token: {
salt: env('TRANSFER_TOKEN_SALT')!,
},
},
secrets: {
encryptionKey: env('ENCRYPTION_KEY')!,
},
/**
* Path the admin panel is served from, relative to `server.url`.
* Set ADMIN_URL to an absolute URL only when the panel is hosted separately
* from the API (in which case also set SERVE_ADMIN_PANEL=false).
*/
url: env('ADMIN_URL', '/admin'),
serveAdminPanel: env.bool('SERVE_ADMIN_PANEL', true),
// Do not pop a browser open on `strapi develop` - agents and CI run headless.
autoOpen: env.bool('ADMIN_AUTO_OPEN', false),
flags: {
nps: env.bool('FLAG_NPS', true),
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
docLinks: env.bool('FLAG_DOC_LINKS', true),
},
});
export default config;
+16
View File
@@ -0,0 +1,16 @@
import type { Core } from '@strapi/strapi';
const config: Core.Config.Api = {
rest: {
defaultLimit: 25,
maxLimit: 100,
withCount: true,
strictParams: true,
},
documents: {
strictParams: true,
strictRelations: true,
},
};
export default config;
+72
View File
@@ -0,0 +1,72 @@
import path from 'path';
import type { Core } from '@strapi/strapi';
import { isDatabaseClientKind } from '@strapi/database';
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Database => {
const client = env('DATABASE_CLIENT', 'sqlite');
if (!isDatabaseClientKind(client)) {
throw new Error(
`Unsupported DATABASE_CLIENT: ${client}. Use "postgres", "mysql", or "sqlite".`
);
}
const connections: Record<Core.Config.Database.ClientKind, Core.Config.Database['connection']> = {
mysql: {
client: 'mysql',
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: {
client: '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: {
client: 'sqlite',
connection: {
filename: path.join(__dirname, '..', '..', env('DATABASE_FILENAME', '.tmp/data.db')),
},
useNullAsDefault: true,
},
};
return {
connection: {
...connections[client],
acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000),
},
};
};
export default config;
+63
View File
@@ -0,0 +1,63 @@
import type { Core } from '@strapi/strapi';
/**
* Origins that are always allowed outside production: the React Router dev
* server (Vite) on both hostnames it is commonly reached on.
*/
const DEV_ORIGINS = [
'http://localhost:5173',
'http://127.0.0.1:5173',
];
/**
* The default Strapi middleware stack, in order, with CORS configured for the
* SHARP-ER web app.
*
* Order matters: `strapi::errors` must wrap everything after it, `strapi::cors`
* must run before routing so pre-flight requests are answered, and
* `strapi::public` stays last so it only handles what nothing else claimed.
*/
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Middlewares => {
const isProduction = env('NODE_ENV', 'development') === 'production';
/**
* Production origins come from the environment, e.g.
* CORS_ORIGINS=https://sharper.ly,https://www.sharper.ly
* Outside production the local dev server is allowed as well.
*/
const configuredOrigins = env.array('CORS_ORIGINS', []) as string[];
const origin = Array.from(
new Set([...(isProduction ? [] : DEV_ORIGINS), ...configuredOrigins].filter(Boolean))
);
return [
'strapi::logger',
'strapi::errors',
// If uploads move to S3/CDN, add the bucket host to img-src and media-src
// here - see the S3 notes in config/plugins.ts.
'strapi::security',
{
name: 'strapi::cors',
config: {
// An empty list blocks every cross-origin browser request, which is the
// safe default if CORS_ORIGINS was forgotten in production.
origin,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'],
// The web app sends the locale it wants and reads pagination metadata.
expose: ['Content-Range', 'Content-Length'],
credentials: true,
maxAge: 31536000,
keepHeadersOnError: false,
},
},
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];
};
export default config;
+143
View File
@@ -0,0 +1,143 @@
import type { Core } from '@strapi/strapi';
/**
* Media types the Media Library accepts.
*
* The site publishes reports, guidance notes and event material, so office
* documents and PDFs are allowed next to the usual image/video/audio types.
*/
const allowedMediaTypes = [
'image/*',
'video/*',
'audio/*',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.*',
'text/plain',
'text/csv',
];
/**
* Types that are never accepted, even if they slip past `allowedMediaTypes`.
* SVG is denied on purpose: it can carry script and is served from the same
* origin as the API.
*/
const deniedTypes = [
'image/svg+xml',
'application/vnd.microsoft.portable-executable',
'application/x-msdownload',
'application/x-msdos-program',
'application/x-executable',
'application/x-dosexec',
'application/x-sh',
'text/x-shellscript',
'application/x-mach-binary',
];
/**
* The locales the site ships with.
*
* `en` is the default (LTR) and `ar` is the right-to-left translation. The web
* app mirrors these codes in its URL prefixes (`/en/...`, `/ar/...`), so the
* codes here are part of the public contract - do not rename them.
*
* The i18n plugin itself does not read this key; it is our own declaration and
* is read back at boot by `src/bootstrap/locales.ts`, which creates any missing
* locale and pins the default one. Adding a locale is therefore a one-line edit
* here plus a restart.
*/
const locales = [
{ code: 'en', name: 'English (en)', isDefault: true },
{ code: 'ar', name: 'Arabic (ar)' },
];
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
/**
* Internationalisation - core plugin, listed explicitly so the two supported
* locales live in version control rather than only in the database.
*/
i18n: {
enabled: true,
config: {
defaultLocale: 'en',
locales,
},
},
'users-permissions': {
config: {
jwtSecret: env('JWT_SECRET'),
/**
* "refresh" uses the SessionManager (short-lived access token + refresh
* token) instead of the legacy long-lived plugin JWT.
*/
jwtManagement: 'refresh',
sessions: {
// Refresh token is stored in a cookie that JavaScript cannot read.
httpOnly: true,
},
/**
* Rate limit on the auth endpoints (login, forgot-password, ...).
* Tightened from the Strapi default of 10 requests per minute.
*/
ratelimit: {
interval: env.int('AUTH_RATE_LIMIT_INTERVAL', 60_000),
max: env.int('AUTH_RATE_LIMIT_MAX', 5),
},
},
},
/**
* Uploads stay on the default `local` provider: files land in
* `apps/cms/public/uploads` and are served by `strapi::public`.
*
* TO SWITCH TO S3 IN PRODUCTION
* -----------------------------
* 1. Add the provider package to apps/cms/package.json:
* "@strapi/provider-upload-aws-s3": "5.53.0"
* 2. Replace the `provider` / `providerOptions` keys below with:
*
* provider: 'aws-s3',
* providerOptions: {
* baseUrl: env('CDN_URL'), // optional CloudFront/CDN origin
* rootPath: env('S3_ROOT_PATH'), // optional key prefix
* s3Options: {
* credentials: {
* accessKeyId: env('AWS_ACCESS_KEY_ID'),
* secretAccessKey: env('AWS_ACCESS_SECRET'),
* },
* region: env('AWS_REGION'),
* params: {
* ACL: env('AWS_ACL', 'private'),
* signedUrlExpires: env.int('AWS_SIGNED_URL_EXPIRES', 15 * 60),
* Bucket: env('AWS_BUCKET'),
* },
* },
* },
* actionOptions: { upload: {}, uploadStream: {}, delete: {} },
*
* 3. Add the bucket/CDN host to the Content Security Policy in
* `config/middlewares.ts` (`img-src` and `media-src`), otherwise the admin
* panel shows broken thumbnails.
* 4. Set the AWS_* / CDN_URL variables listed in `.env.example`.
*/
upload: {
config: {
provider: 'local',
providerOptions: {
// Cache-Control max-age (seconds) sent for locally served uploads.
localServer: {
maxage: env.int('UPLOAD_LOCAL_MAX_AGE', 300_000),
},
},
// Hard ceiling per file, in bytes. Defaults to 50 MB.
sizeLimit: env.int('UPLOAD_MAX_FILE_SIZE', 50 * 1024 * 1024),
security: {
allowedTypes: allowedMediaTypes,
deniedTypes,
},
},
},
});
export default config;
+30
View File
@@ -0,0 +1,30 @@
import type { Core } from '@strapi/strapi';
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
/**
* Public URL of the API. Required behind a reverse proxy / custom domain so
* that generated links (media URLs, admin redirects) are absolute and https.
* Leave unset in local development.
*/
url: env('PUBLIC_URL'),
// Trust X-Forwarded-* headers when Strapi runs behind nginx / a load balancer.
proxy: env.bool('IS_PROXIED', false),
app: {
keys: env.array('APP_KEYS')!,
},
logger: {
config: {
level: env('LOG_LEVEL', 'info'),
},
},
cron: {
enabled: env.bool('CRON_ENABLED', false),
},
webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
},
});
export default config;