UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

1,986 lines (1,786 loc) 48.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.strapiTemplate = void 0; exports.strapiTemplate = { id: 'strapi', name: 'Strapi Headless CMS', displayName: 'Strapi', description: 'Flexible, open-source headless CMS with admin panel, Content-Type Builder, and REST/GraphQL APIs', author: 'Re-Shell Team', version: '4.15.0', tags: ['cms', 'headless', 'api', 'content-management', 'graphql', 'rest'], features: [ 'Admin Panel with intuitive interface', 'Content-Type Builder for dynamic API creation', 'REST & GraphQL APIs', 'Role-Based Access Control', 'Media Library with optimization', 'Internationalization support', 'Plugin System', 'Webhooks for real-time events', 'Draft/Publish system', 'Database support (PostgreSQL, MySQL, SQLite)' ], language: 'javascript', framework: 'strapi', featured_frameworks: ['strapi', 'koa', 'bookshelf', 'knex'], type: 'cms', complexity: 'advanced', keywords: [ 'cms', 'headless', 'content-management', 'admin-panel', 'api-builder', 'graphql', 'rest-api', 'media-library', 'authentication', 'rbac', 'i18n', 'webhooks', 'plugins' ], dependencies: { '@strapi/strapi': '^4.15.0', '@strapi/plugin-users-permissions': '^4.15.0', '@strapi/plugin-i18n': '^4.15.0', '@strapi/plugin-graphql': '^4.15.0', '@strapi/plugin-email': '^4.15.0', '@strapi/plugin-upload': '^4.15.0', '@strapi/plugin-sentry': '^4.15.0', '@strapi/provider-email-sendgrid': '^4.15.0', '@strapi/provider-upload-cloudinary': '^4.15.0', 'pg': '^8.11.3', 'mysql2': '^3.6.5', 'sqlite3': '^5.1.6', 'strapi-plugin-webhooks': '^4.0.0', 'strapi-plugin-import-export-entries': '^1.21.0', 'strapi-plugin-slugify': '^2.3.3', 'uuid': '^9.0.1' }, devDependencies: { '@types/node': '^20.10.0', 'typescript': '^5.3.0', '@strapi/typescript-utils': '^4.15.0', 'nodemon': '^3.0.1', 'jest': '^29.7.0', '@testing-library/jest-dom': '^6.1.5', 'supertest': '^6.3.3' }, port: 1337, files: { 'package.json': `{ "name": "strapi-cms", "private": true, "version": "0.1.0", "description": "A Strapi headless CMS application", "scripts": { "develop": "strapi develop", "start": "strapi start", "build": "strapi build", "strapi": "strapi", "test": "jest --forceExit --detectOpenHandles", "test:watch": "jest --watch", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "seed": "node scripts/seed.js", "generate:types": "strapi ts:generate-types", "docker:dev": "docker-compose up -d", "docker:prod": "docker-compose -f docker-compose.prod.yml up -d" }, "devDependencies": { "@types/node": "^20.10.0", "typescript": "^5.3.0", "@strapi/typescript-utils": "^4.15.0", "nodemon": "^3.0.1", "jest": "^29.7.0", "@testing-library/jest-dom": "^6.1.5", "supertest": "^6.3.3" }, "dependencies": { "@strapi/strapi": "4.15.0", "@strapi/plugin-users-permissions": "4.15.0", "@strapi/plugin-i18n": "4.15.0", "@strapi/plugin-graphql": "4.15.0", "@strapi/plugin-email": "4.15.0", "@strapi/plugin-upload": "4.15.0", "@strapi/plugin-sentry": "4.15.0", "@strapi/provider-email-sendgrid": "4.15.0", "@strapi/provider-upload-cloudinary": "4.15.0", "pg": "^8.11.3", "mysql2": "^3.6.5", "sqlite3": "^5.1.6", "strapi-plugin-webhooks": "^4.0.0", "strapi-plugin-import-export-entries": "^1.21.0", "strapi-plugin-slugify": "^2.3.3", "uuid": "^9.0.1" }, "strapi": { "uuid": "strapi-cms-template" }, "engines": { "node": ">=16.x.x <=20.x.x", "npm": ">=6.0.0" }, "license": "MIT" }`, 'config/server.ts': `export default ({ env }) => ({ host: env('HOST', '0.0.0.0'), port: env.int('PORT', 1337), app: { keys: env.array('APP_KEYS'), }, webhooks: { populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false), }, // Cron jobs configuration cron: { enabled: true, tasks: { /** * Simple example of cron job */ '0 0 * * *': async ({ strapi }) => { // Daily cleanup task console.log('Running daily cleanup task...'); // Example: Clean up old drafts const drafts = await strapi.entityService.findMany('api::article.article', { filters: { publishedAt: null, createdAt: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days old } } }); for (const draft of drafts) { await strapi.entityService.delete('api::article.article', draft.id); } }, /** * Send weekly newsletter */ '0 9 * * 1': async ({ strapi }) => { console.log('Sending weekly newsletter...'); const subscribers = await strapi.entityService.findMany('api::subscriber.subscriber', { filters: { active: true } }); for (const subscriber of subscribers) { await strapi.plugin('email').service('email').send({ to: subscriber.email, from: 'noreply@strapi.io', subject: 'Weekly Newsletter', text: 'Check out our latest content!', html: '<h1>Weekly Newsletter</h1><p>Check out our latest content!</p>' }); } } } } });`, 'config/database.ts': `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), }, }, 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: env('DATABASE_FILENAME', '.tmp/data.db'), }, useNullAsDefault: true, }, }; return { connection: { client, ...connections[client], acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000), }, }; };`, 'config/admin.ts': `export default ({ env }) => ({ auth: { secret: env('ADMIN_JWT_SECRET'), }, apiToken: { salt: env('API_TOKEN_SALT'), }, transfer: { token: { salt: env('TRANSFER_TOKEN_SALT'), }, }, // Admin panel customization head: { favicon: '/favicon.ico', }, // Theme customization theme: { light: { colors: { primary100: '#f6ecfc', primary200: '#e0c1f4', primary500: '#ac73e6', primary600: '#9736e8', primary700: '#8312d1', danger700: '#b72b1a' }, }, dark: { colors: { primary100: '#030415', primary200: '#151625', primary500: '#ac73e6', primary600: '#9736e8', primary700: '#8312d1', danger700: '#b72b1a' } } }, // Locales configuration locales: ['en', 'es', 'fr', 'de', 'pt', 'it', 'zh', 'ja'], // Disable video tutorials tutorials: false, // Disable notifications about new Strapi releases notifications: { releases: false }, });`, 'config/plugins.ts': `export default ({ env }) => ({ // GraphQL plugin graphql: { enabled: true, config: { endpoint: '/graphql', shadowCRUD: true, playgroundAlways: false, depthLimit: 7, amountLimit: 100, apolloServer: { tracing: false, }, }, }, // Email plugin with SendGrid email: { config: { provider: 'sendgrid', providerOptions: { apiKey: env('SENDGRID_API_KEY'), }, settings: { defaultFrom: env('EMAIL_DEFAULT_FROM', 'noreply@strapi.io'), defaultReplyTo: env('EMAIL_DEFAULT_REPLY_TO', 'noreply@strapi.io'), }, }, }, // Upload plugin with Cloudinary upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_NAME'), api_key: env('CLOUDINARY_KEY'), api_secret: env('CLOUDINARY_SECRET'), }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // Internationalization plugin i18n: { enabled: true, config: { defaultLocale: 'en', locales: ['en', 'es', 'fr', 'de', 'pt', 'it', 'zh', 'ja'], }, }, // Sentry plugin for error tracking sentry: { enabled: true, config: { dsn: env('SENTRY_DSN'), sendMetadata: true, }, }, // Webhooks plugin webhooks: { enabled: true, config: { defaultHeaders: { 'Content-Type': 'application/json', }, }, }, // Import/Export plugin 'import-export-entries': { enabled: true, config: { // Configure content types that can be imported/exported contentTypes: { article: { enabled: true, options: { populateCreatorFields: true, }, }, category: { enabled: true, }, tag: { enabled: true, }, }, }, }, // Slugify plugin slugify: { enabled: true, config: { contentTypes: { article: { field: 'slug', references: 'title', }, category: { field: 'slug', references: 'name', }, }, }, }, });`, 'config/middlewares.ts': `export default [ 'strapi::errors', 'strapi::security', 'strapi::cors', 'strapi::poweredBy', 'strapi::logger', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': ["'self'", 'data:', 'blob:', 'dl.airtable.com', 'strapi.io', 'res.cloudinary.com'], 'media-src': ["'self'", 'data:', 'blob:', 'dl.airtable.com', 'strapi.io', 'res.cloudinary.com'], upgradeInsecureRequests: null, }, }, }, }, { name: 'strapi::cors', config: { enabled: true, headers: '*', origin: ['http://localhost:3000', 'http://localhost:5173', 'https://yourdomain.com'], }, }, ];`, 'src/api/article/content-types/article/schema.json': `{ "kind": "collectionType", "collectionName": "articles", "info": { "singularName": "article", "pluralName": "articles", "displayName": "Article", "description": "Blog articles with rich content" }, "options": { "draftAndPublish": true }, "pluginOptions": { "i18n": { "localized": true } }, "attributes": { "title": { "type": "string", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "uid", "targetField": "title", "required": true }, "content": { "type": "richtext", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "excerpt": { "type": "text", "maxLength": 300, "pluginOptions": { "i18n": { "localized": true } } }, "featuredImage": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images"] }, "author": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.user", "inversedBy": "articles" }, "categories": { "type": "relation", "relation": "manyToMany", "target": "api::category.category", "inversedBy": "articles" }, "tags": { "type": "relation", "relation": "manyToMany", "target": "api::tag.tag", "inversedBy": "articles" }, "seo": { "type": "component", "repeatable": false, "component": "shared.seo" }, "views": { "type": "integer", "default": 0, "min": 0 }, "readingTime": { "type": "integer", "min": 1 } } }`, 'src/api/article/controllers/article.ts': `import { factories } from '@strapi/strapi'; export default factories.createCoreController('api::article.article', ({ strapi }) => ({ // Custom find method with view count increment async findOne(ctx) { const { id } = ctx.params; // Call default findOne const response = await super.findOne(ctx); // Increment view count if (response && response.data) { await strapi.entityService.update('api::article.article', id, { data: { views: (response.data.attributes.views || 0) + 1 } }); } return response; }, // Custom method to get trending articles async trending(ctx) { const { locale = 'en' } = ctx.query; const articles = await strapi.entityService.findMany('api::article.article', { sort: { views: 'desc' }, limit: 10, populate: ['featuredImage', 'author', 'categories'], locale, filters: { publishedAt: { $notNull: true } } }); return this.transformResponse(articles); }, // Custom method to get related articles async related(ctx) { const { id } = ctx.params; const { locale = 'en' } = ctx.query; // Get current article const article = await strapi.entityService.findOne('api::article.article', id, { populate: ['categories', 'tags'] }); if (!article) { return ctx.notFound('Article not found'); } // Find related articles by categories and tags const categoryIds = article.categories?.map(cat => cat.id) || []; const tagIds = article.tags?.map(tag => tag.id) || []; const relatedArticles = await strapi.entityService.findMany('api::article.article', { filters: { $and: [ { id: { $ne: id } }, { publishedAt: { $notNull: true } }, { $or: [ { categories: { id: { $in: categoryIds } } }, { tags: { id: { $in: tagIds } } } ] } ] }, limit: 5, populate: ['featuredImage', 'author', 'categories'], locale }); return this.transformResponse(relatedArticles); } }));`, 'src/api/article/services/article.ts': `import { factories } from '@strapi/strapi'; export default factories.createCoreService('api::article.article', ({ strapi }) => ({ // Calculate reading time based on content calculateReadingTime(content: string): number { const wordsPerMinute = 200; const wordCount = content.trim().split(/\\s+/).length; const readingTime = Math.ceil(wordCount / wordsPerMinute); return readingTime; }, // Generate excerpt from content generateExcerpt(content: string, maxLength = 300): string { const plainText = content.replace(/<[^>]*>/g, '').trim(); if (plainText.length <= maxLength) { return plainText; } return plainText.substring(0, maxLength).trim() + '...'; }, // Send notification on article publish async sendPublishNotification(article) { // Get all subscribers const subscribers = await strapi.entityService.findMany('api::subscriber.subscriber', { filters: { active: true } }); // Send email to each subscriber for (const subscriber of subscribers) { await strapi.plugin('email').service('email').send({ to: subscriber.email, from: 'noreply@strapi.io', subject: \`New Article: \${article.title}\`, text: \`Check out our new article: \${article.title}\`, html: \` <h2>New Article Published!</h2> <h3>\${article.title}</h3> <p>\${article.excerpt}</p> <a href="https://yoursite.com/articles/\${article.slug}">Read More</a> \` }); } }, // Search articles with full-text search async searchArticles(query: string, locale = 'en') { return await strapi.entityService.findMany('api::article.article', { filters: { $and: [ { publishedAt: { $notNull: true } }, { $or: [ { title: { $containsi: query } }, { content: { $containsi: query } }, { excerpt: { $containsi: query } } ] } ] }, populate: ['featuredImage', 'author', 'categories'], locale }); } }));`, 'src/api/article/routes/article.ts': `import { factories } from '@strapi/strapi'; export default factories.createCoreRouter('api::article.article', { config: { find: { middlewares: ['api::article.article-populate-middleware'] }, findOne: { middlewares: ['api::article.article-populate-middleware'] } } });`, 'src/api/article/routes/custom-article.ts': `export default { routes: [ { method: 'GET', path: '/articles/trending', handler: 'article.trending', config: { policies: [], middlewares: [], }, }, { method: 'GET', path: '/articles/:id/related', handler: 'article.related', config: { policies: [], middlewares: [], }, }, ], };`, 'src/api/article/middlewares/article-populate-middleware.ts': `export default (config, { strapi }) => { return async (ctx, next) => { // Add default population for articles ctx.query = { ...ctx.query, populate: { featuredImage: true, author: { fields: ['username', 'email'], populate: { avatar: true } }, categories: { fields: ['name', 'slug'] }, tags: { fields: ['name', 'slug'] }, seo: true } }; await next(); }; };`, 'src/api/category/content-types/category/schema.json': `{ "kind": "collectionType", "collectionName": "categories", "info": { "singularName": "category", "pluralName": "categories", "displayName": "Category", "description": "Article categories" }, "options": { "draftAndPublish": false }, "pluginOptions": { "i18n": { "localized": true } }, "attributes": { "name": { "type": "string", "required": true, "unique": true, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "uid", "targetField": "name", "required": true }, "description": { "type": "text", "pluginOptions": { "i18n": { "localized": true } } }, "articles": { "type": "relation", "relation": "manyToMany", "target": "api::article.article", "mappedBy": "categories" }, "icon": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images"] }, "color": { "type": "string", "regex": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" } } }`, 'src/api/subscriber/content-types/subscriber/schema.json': `{ "kind": "collectionType", "collectionName": "subscribers", "info": { "singularName": "subscriber", "pluralName": "subscribers", "displayName": "Subscriber", "description": "Newsletter subscribers" }, "options": { "draftAndPublish": false }, "attributes": { "email": { "type": "email", "required": true, "unique": true }, "name": { "type": "string" }, "active": { "type": "boolean", "default": true }, "preferences": { "type": "json", "default": { "frequency": "weekly", "categories": [] } }, "subscribedAt": { "type": "datetime", "default": "2023-01-01T00:00:00.000Z" }, "unsubscribeToken": { "type": "string", "unique": true, "private": true } } }`, 'src/api/subscriber/controllers/subscriber.ts': `import { factories } from '@strapi/strapi'; import { v4 as uuidv4 } from 'uuid'; export default factories.createCoreController('api::subscriber.subscriber', ({ strapi }) => ({ // Custom subscribe endpoint async subscribe(ctx) { const { email, name, preferences } = ctx.request.body; // Check if already subscribed const existing = await strapi.entityService.findMany('api::subscriber.subscriber', { filters: { email } }); if (existing.length > 0) { return ctx.badRequest('Email already subscribed'); } // Create subscriber with unsubscribe token const subscriber = await strapi.entityService.create('api::subscriber.subscriber', { data: { email, name, preferences, unsubscribeToken: uuidv4(), subscribedAt: new Date() } }); // Send welcome email await strapi.plugin('email').service('email').send({ to: email, from: 'noreply@strapi.io', subject: 'Welcome to our newsletter!', text: 'Thank you for subscribing to our newsletter.', html: \` <h2>Welcome!</h2> <p>Thank you for subscribing to our newsletter.</p> <p>You'll receive updates based on your preferences.</p> <p><a href="https://yoursite.com/unsubscribe?token=\${subscriber.unsubscribeToken}">Unsubscribe</a></p> \` }); return { data: { message: 'Successfully subscribed!', subscriber: { email: subscriber.email, name: subscriber.name } } }; }, // Custom unsubscribe endpoint async unsubscribe(ctx) { const { token } = ctx.query; if (!token) { return ctx.badRequest('Unsubscribe token required'); } const subscriber = await strapi.entityService.findMany('api::subscriber.subscriber', { filters: { unsubscribeToken: token } }); if (subscriber.length === 0) { return ctx.notFound('Invalid unsubscribe token'); } // Update subscriber status await strapi.entityService.update('api::subscriber.subscriber', subscriber[0].id, { data: { active: false } }); return { data: { message: 'Successfully unsubscribed' } }; } }));`, 'src/components/shared/seo.json': `{ "collectionName": "components_shared_seos", "info": { "displayName": "SEO", "description": "SEO metadata component", "icon": "search" }, "options": {}, "attributes": { "metaTitle": { "type": "string", "required": true, "maxLength": 60 }, "metaDescription": { "type": "text", "required": true, "maxLength": 160 }, "metaImage": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images"] }, "metaSocial": { "type": "component", "repeatable": true, "component": "shared.meta-social" }, "keywords": { "type": "text" }, "metaRobots": { "type": "string" }, "structuredData": { "type": "json" }, "metaViewport": { "type": "string", "default": "width=device-width, initial-scale=1" }, "canonicalURL": { "type": "string" } } }`, 'src/components/shared/meta-social.json': `{ "collectionName": "components_shared_meta_socials", "info": { "displayName": "Meta Social", "description": "Social media meta tags", "icon": "share" }, "options": {}, "attributes": { "socialNetwork": { "type": "enumeration", "enum": ["Facebook", "Twitter", "LinkedIn"], "required": true }, "title": { "type": "string", "required": true, "maxLength": 60 }, "description": { "type": "text", "required": true, "maxLength": 160 }, "image": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images"] } } }`, 'src/extensions/users-permissions/content-types/user/schema.json': `{ "kind": "collectionType", "collectionName": "up_users", "info": { "name": "user", "description": "", "singularName": "user", "pluralName": "users", "displayName": "User" }, "options": { "draftAndPublish": false }, "attributes": { "username": { "type": "string", "minLength": 3, "unique": true, "configurable": false, "required": true }, "email": { "type": "email", "minLength": 6, "configurable": false, "required": true }, "provider": { "type": "string", "configurable": false }, "password": { "type": "password", "minLength": 6, "configurable": false, "private": true }, "resetPasswordToken": { "type": "string", "configurable": false, "private": true }, "confirmationToken": { "type": "string", "configurable": false, "private": true }, "confirmed": { "type": "boolean", "default": false, "configurable": false }, "blocked": { "type": "boolean", "default": false, "configurable": false }, "role": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.role", "inversedBy": "users", "configurable": false }, "articles": { "type": "relation", "relation": "oneToMany", "target": "api::article.article", "mappedBy": "author" }, "avatar": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images"] }, "bio": { "type": "text", "maxLength": 500 }, "website": { "type": "string" }, "socialLinks": { "type": "json", "default": { "twitter": "", "linkedin": "", "github": "" } } } }`, 'src/policies/is-owner.ts': `export default async (policyContext, config, { strapi }) => { const { user, auth } = policyContext.state; const { params } = policyContext; // If no authenticated user, deny access if (!user) { return false; } // Get the model from the policy config const { model } = config; // Find the entity const entity = await strapi.entityService.findOne(model, params.id, { populate: ['author', 'user', 'owner'] }); if (!entity) { return false; } // Check if user is the owner/author const ownerId = entity.author?.id || entity.user?.id || entity.owner?.id; return ownerId === user.id; };`, 'src/policies/rate-limit.ts': `const rateLimit = new Map(); export default async (policyContext, config, { strapi }) => { const { user } = policyContext.state; const { request } = policyContext; // Configuration const { max = 100, windowMs = 60000 } = config; // Create unique key for rate limiting const key = user ? \`user-\${user.id}\` : \`ip-\${request.ip}\`; // Get current data const now = Date.now(); const userData = rateLimit.get(key) || { count: 0, resetTime: now + windowMs }; // Reset if window expired if (now > userData.resetTime) { userData.count = 0; userData.resetTime = now + windowMs; } // Increment count userData.count++; // Update map rateLimit.set(key, userData); // Set headers policyContext.set('X-RateLimit-Limit', max.toString()); policyContext.set('X-RateLimit-Remaining', Math.max(0, max - userData.count).toString()); policyContext.set('X-RateLimit-Reset', new Date(userData.resetTime).toISOString()); // Check if limit exceeded if (userData.count > max) { return false; } return true; };`, 'src/index.ts': `import { Strapi } from '@strapi/strapi'; export default { /** * An asynchronous register function that runs before * your application is initialized. * * This gives you an opportunity to extend code. */ register(/* { strapi }: { strapi: Strapi } */) { // Register custom fields strapi.customFields.register({ name: 'color', plugin: 'color-picker', type: 'string', }); }, /** * An asynchronous bootstrap function that runs before * your application gets started. * * This gives you an opportunity to set up your data model, * run jobs, or perform some special logic. */ async bootstrap({ strapi }: { strapi: Strapi }) { // Setup webhooks for content changes strapi.db.lifecycles.subscribe({ models: ['api::article.article'], async afterCreate(event) { const { result } = event; // Trigger webhook await strapi.service('plugin::webhooks.webhooks').trigger('article.created', { entry: result, model: 'article', event: 'created' }); }, async afterUpdate(event) { const { result, params } = event; // Check if article was just published if (result.publishedAt && !params.data.publishedAt) { // Send notification to subscribers await strapi.service('api::article.article').sendPublishNotification(result); // Trigger webhook await strapi.service('plugin::webhooks.webhooks').trigger('article.published', { entry: result, model: 'article', event: 'published' }); } }, async beforeCreate(event) { const { data } = event.params; // Auto-calculate reading time if (data.content && !data.readingTime) { data.readingTime = strapi.service('api::article.article').calculateReadingTime(data.content); } // Auto-generate excerpt if (data.content && !data.excerpt) { data.excerpt = strapi.service('api::article.article').generateExcerpt(data.content); } }, async beforeUpdate(event) { const { data } = event.params; // Update reading time if content changed if (data.content) { data.readingTime = strapi.service('api::article.article').calculateReadingTime(data.content); // Update excerpt if not manually set if (!data.excerpt) { data.excerpt = strapi.service('api::article.article').generateExcerpt(data.content); } } } }); // Register custom permissions await strapi.admin.services.permission.actionProvider.registerMany([ { section: 'plugins', displayName: 'Access the Import/Export Plugin', uid: 'plugin::import-export-entries.read', pluginName: 'import-export-entries', }, ]); }, };`, 'docker-compose.yml': `version: '3.8' services: strapi: image: strapi/strapi:4.15.0-alpine container_name: strapi-cms restart: unless-stopped env_file: .env environment: DATABASE_CLIENT: \${DATABASE_CLIENT} DATABASE_HOST: strapi-db DATABASE_PORT: \${DATABASE_PORT} DATABASE_NAME: \${DATABASE_NAME} DATABASE_USERNAME: \${DATABASE_USERNAME} DATABASE_PASSWORD: \${DATABASE_PASSWORD} JWT_SECRET: \${JWT_SECRET} ADMIN_JWT_SECRET: \${ADMIN_JWT_SECRET} API_TOKEN_SALT: \${API_TOKEN_SALT} APP_KEYS: \${APP_KEYS} NODE_ENV: \${NODE_ENV} volumes: - ./config:/opt/app/config - ./src:/opt/app/src - ./package.json:/opt/package.json - ./public/uploads:/opt/app/public/uploads ports: - '1337:1337' networks: - strapi-network depends_on: - strapi-db strapi-db: image: postgres:14-alpine container_name: strapi-database restart: unless-stopped env_file: .env environment: POSTGRES_USER: \${DATABASE_USERNAME} POSTGRES_PASSWORD: \${DATABASE_PASSWORD} POSTGRES_DB: \${DATABASE_NAME} volumes: - strapi-data:/var/lib/postgresql/data ports: - '5432:5432' networks: - strapi-network redis: image: redis:7-alpine container_name: strapi-redis restart: unless-stopped ports: - '6379:6379' networks: - strapi-network volumes: - redis-data:/data nginx: image: nginx:alpine container_name: strapi-nginx restart: unless-stopped ports: - '80:80' - '443:443' volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/ssl:/etc/nginx/ssl - ./public/uploads:/usr/share/nginx/html/uploads networks: - strapi-network depends_on: - strapi volumes: strapi-data: redis-data: networks: strapi-network: driver: bridge`, 'docker-compose.prod.yml': `version: '3.8' services: strapi: build: context: . dockerfile: Dockerfile.prod container_name: strapi-cms-prod restart: always env_file: .env.production environment: DATABASE_CLIENT: postgres DATABASE_HOST: strapi-db DATABASE_PORT: 5432 NODE_ENV: production volumes: - ./public/uploads:/opt/app/public/uploads networks: - strapi-network depends_on: - strapi-db - redis strapi-db: image: postgres:14-alpine container_name: strapi-database-prod restart: always env_file: .env.production environment: POSTGRES_USER: \${DATABASE_USERNAME} POSTGRES_PASSWORD: \${DATABASE_PASSWORD} POSTGRES_DB: \${DATABASE_NAME} volumes: - strapi-data:/var/lib/postgresql/data - ./scripts/backup:/backup networks: - strapi-network redis: image: redis:7-alpine container_name: strapi-redis-prod restart: always command: redis-server --requirepass \${REDIS_PASSWORD} networks: - strapi-network volumes: - redis-data:/data nginx: image: nginx:alpine container_name: strapi-nginx-prod restart: always ports: - '80:80' - '443:443' volumes: - ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf - ./nginx/ssl:/etc/nginx/ssl - ./public/uploads:/usr/share/nginx/html/uploads - ./nginx/cache:/var/cache/nginx networks: - strapi-network depends_on: - strapi volumes: strapi-data: redis-data: networks: strapi-network: driver: bridge`, 'Dockerfile.prod': `FROM node:18-alpine as build # Install dependencies RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev > /dev/null 2>&1 # Set working directory WORKDIR /opt/app # Copy package files COPY package*.json ./ # Install production dependencies RUN npm ci --only=production # Copy application files COPY . . # Build admin panel RUN npm run build # Production stage FROM node:18-alpine # Install runtime dependencies RUN apk add --no-cache vips-dev # Set working directory WORKDIR /opt/app # Copy from build stage COPY --from=build /opt/app ./ # Create upload directory RUN mkdir -p public/uploads # Set permissions RUN chown -R node:node /opt/app # Switch to node user USER node # Expose port EXPOSE 1337 # Start application CMD ["npm", "start"]`, '.env.example': `# Server HOST=0.0.0.0 PORT=1337 APP_KEYS=toBeModified1,toBeModified2 # Database DATABASE_CLIENT=postgres DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_NAME=strapi DATABASE_USERNAME=strapi DATABASE_PASSWORD=strapi DATABASE_SSL=false # JWT JWT_SECRET=toBeModified ADMIN_JWT_SECRET=toBeModified API_TOKEN_SALT=toBeModified TRANSFER_TOKEN_SALT=toBeModified # Email (SendGrid) SENDGRID_API_KEY=your-sendgrid-api-key EMAIL_DEFAULT_FROM=noreply@yoursite.com EMAIL_DEFAULT_REPLY_TO=noreply@yoursite.com # Upload (Cloudinary) CLOUDINARY_NAME=your-cloudinary-name CLOUDINARY_KEY=your-cloudinary-key CLOUDINARY_SECRET=your-cloudinary-secret # Sentry SENTRY_DSN=your-sentry-dsn # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=your-redis-password # OAuth Providers GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret # Frontend URL FRONTEND_URL=http://localhost:3000 # Admin URL ADMIN_URL=http://localhost:1337/admin`, 'nginx/nginx.conf': `user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # Gzip compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml application/atom+xml image/svg+xml; # Upload size client_max_body_size 100M; # Upstream upstream strapi { server strapi:1337; } # HTTP server server { listen 80; server_name localhost; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; # Strapi admin location /admin { proxy_pass http://strapi/admin; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } # Strapi API location / { proxy_pass http://strapi; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } # Static files location /uploads { alias /usr/share/nginx/html/uploads; expires 7d; add_header Cache-Control "public, immutable"; } } }`, 'scripts/seed.js': `const { createCoreService } = require('@strapi/strapi').factories; async function seedDatabase() { try { console.log('Starting database seeding...'); // Create categories const categories = [ { name: 'Technology', slug: 'technology', color: '#007bff' }, { name: 'Business', slug: 'business', color: '#28a745' }, { name: 'Design', slug: 'design', color: '#dc3545' }, { name: 'Marketing', slug: 'marketing', color: '#ffc107' }, { name: 'Development', slug: 'development', color: '#17a2b8' } ]; for (const category of categories) { await strapi.entityService.create('api::category.category', { data: category }); } console.log('Categories created successfully'); // Create tags const tags = [ { name: 'JavaScript', slug: 'javascript' }, { name: 'React', slug: 'react' }, { name: 'Node.js', slug: 'nodejs' }, { name: 'Python', slug: 'python' }, { name: 'Docker', slug: 'docker' }, { name: 'AWS', slug: 'aws' }, { name: 'DevOps', slug: 'devops' }, { name: 'UI/UX', slug: 'ui-ux' } ]; for (const tag of tags) { await strapi.entityService.create('api::tag.tag', { data: tag }); } console.log('Tags created successfully'); // Create sample articles const articles = [ { title: 'Getting Started with Strapi CMS', content: '<p>Strapi is a leading open-source headless CMS. It\'s 100% JavaScript, fully customizable and developer-first.</p>', excerpt: 'Learn how to get started with Strapi, the flexible and open-source headless CMS.', publishedAt: new Date() }, { title: 'Building APIs with Strapi', content: '<p>Strapi makes it easy to build powerful APIs. Learn how to create custom endpoints and controllers.</p>', excerpt: 'Discover how to build robust APIs using Strapi\'s powerful features.', publishedAt: new Date() } ]; for (const article of articles) { await strapi.entityService.create('api::article.article', { data: article }); } console.log('Articles created successfully'); console.log('Database seeding completed!'); } catch (error) { console.error('Error seeding database:', error); process.exit(1); } } // Only run if called directly if (require.main === module) { seedDatabase(); } module.exports = seedDatabase;`, 'tsconfig.json': `{ "extends": "@strapi/typescript-utils/tsconfigs/server", "compilerOptions": { "outDir": "dist", "rootDir": ".", "sourceMap": true, "allowJs": true }, "include": [ "./", "./**/*.ts", "./**/*.js", "src/**/*.json" ], "exclude": [ "node_modules/", "build/", "dist/", ".cache/", ".tmp/", "src/admin/", "**/*.test.*", "src/plugins/**" ] }`, '.gitignore': `############################ # OS X ############################ .DS_Store .AppleDouble .LSOverride Icon .Spotlight-V100 .Trashes ._* ############################ # Linux ############################ *~ ############################ # Windows ############################ Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ *.cab *.msi *.msm *.msp ############################ # Packages ############################ *.7z *.csv *.dat *.dmg *.gz *.iso *.jar *.rar *.tar *.zip *.com *.class *.dll *.exe *.o *.seed *.so *.swo *.swp *.swn *.swm *.out *.pid ############################ # Logs and databases ############################ .tmp *.log *.sql *.sqlite *.sqlite3 ############################ # Misc. ############################ *# ssl .idea nbproject public/uploads/* !public/uploads/.gitkeep ############################ # Node.js ############################ lib-cov lcov.info pids logs results node_modules .node_history ############################ # Tests ############################ coverage ############################ # Strapi ############################ .env license.txt exports *.cache dist build .strapi-updater.json .strapi .tmp public/uploads !public/uploads/.gitkeep`, 'README.md': `# Strapi Headless CMS A flexible, open-source headless CMS built with Strapi v4, featuring an admin panel, Content-Type Builder, and REST/GraphQL APIs. ## Features - **Admin Panel**: Intuitive interface for content management - **Content-Type Builder**: Dynamic API creation without coding - **REST & GraphQL APIs**: Automatic API generation - **Role-Based Access Control**: Fine-grained permissions - **Media Library**: Built-in asset management with optimization - **Internationalization**: Multi-language content support - **Plugin System**: Extensible architecture - **Webhooks**: Real-time event notifications - **Database Support**: PostgreSQL, MySQL, SQLite, MongoDB - **Authentication**: Local and OAuth providers - **Draft/Publish System**: Content workflow management ## Getting Started ### Prerequisites - Node.js 16.x - 20.x - npm/yarn - PostgreSQL/MySQL (or SQLite for development) ### Installation 1. Install dependencies: \`\`\`bash npm install \`\`\` 2. Set up environment variables: \`\`\`bash cp .env.example .env \`\`\` 3. Start development server: \`\`\`bash npm run develop \`\`\` 4. Access admin panel at: http://localhost:1337/admin ### Docker Setup Development: \`\`\`bash docker-compose up -d \`\`\` Production: \`\`\`bash docker-compose -f docker-compose.prod.yml up -d \`\`\` ## Project Structure \`\`\` ├── config/ # Configuration files ├── src/ │ ├── api/ # API endpoints and content types │ ├── components/ # Reusable components │ ├── extensions/ # Plugin extensions │ ├── middlewares/ # Custom middlewares │ ├── policies/ # Custom policies │ └── index.ts # Main application file ├── public/ # Public assets ├── scripts/ # Utility scripts └── docker/ # Docker configuration \`\`\` ## Content Types ### Article - Title, slug, content, excerpt - Featured image, author relation - Categories and tags - SEO metadata - Draft/publish workflow - View count tracking ### Category - Name, slug, description - Icon and color - Article relations ### Subscriber - Email newsletter management - Preferences and unsubscribe tokens ## Custom Features ### API Endpoints - \`GET /api/articles/trending\` - Get trending articles - \`GET /api/articles/:id/related\` - Get related articles - \`POST /api/subscribers/subscribe\` - Newsletter subscription - \`GET /api/subscribers/unsubscribe\` - Unsubscribe from newsletter ### Lifecycle Hooks - Auto-calculate reading time - Generate excerpts - Send publish notifications - Webhook triggers ### Policies - Rate limiting - Ownership verification ## Configuration ### Database Configure in \`config/database.ts\`: - PostgreSQL (recommended for production) - MySQL - SQLite (development) - MongoDB ### Email SendGrid integration in \`config/plugins.ts\` ### Media Storage Cloudinary integration for image optimization ### Authentication - Local authentication - OAuth providers (GitHub, Google) ## Testing Run tests: \`\`\`bash npm test \`\`\` ## Deployment 1. Build for production: \`\`\`bash npm run build \`\`\` 2. Start production server: \`\`\`bash npm start \`\`\` ## API Documentation ### REST API - Base URL: \`http://localhost:1337/api\` - Authentication: Bearer token or API key ### GraphQL - Endpoint: \`http://localhost:1337/graphql\` - Playground available in development ## Environment Variables See \`.env.example\` for all configuration options. ## License MIT` } };