@smi-digital/create-smi-app 1.0.6 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64,8 +64,7 @@ var FRAMEWORK_GENERATORS = {
64
64
  "--template",
65
65
  "minimal",
66
66
  "--install",
67
- "--git",
68
- "false",
67
+ "--no-git",
69
68
  "--yes"
70
69
  ];
71
70
  }
@@ -487,6 +486,7 @@ async function createApps(projectRoot, targets) {
487
486
  // src/modules/scaffoldActions/createIntegrations.ts
488
487
  import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
489
488
  import { dirname as dirname2, join as join4 } from "path";
489
+ import { randomBytes } from "crypto";
490
490
  function toTemplateToken(key) {
491
491
  const normalized = key.replaceAll(/[^a-zA-Z0-9]+/gv, "_").replaceAll(/^_+|_+$/gv, "").toUpperCase();
492
492
  return `__${normalized}__`;
@@ -530,18 +530,48 @@ async function createIntegrations(options, projectRoot, templatesDir) {
530
530
  integrationKey
531
531
  );
532
532
  const integrationRoot = join4(templatesDir, "integrations", integrationKey);
533
- const filesToApply = [integrationConfig.workflows.prValidation];
534
- if (options.configureCd && integrationConfig.workflows.deploy) {
535
- filesToApply.push(integrationConfig.workflows.deploy);
533
+ const filesToApply = [];
534
+ if (integrationConfig.workflows) {
535
+ if (integrationConfig.workflows.prValidation)
536
+ filesToApply.push(integrationConfig.workflows.prValidation);
537
+ if (options.configureCd && integrationConfig.workflows.deploy)
538
+ filesToApply.push(integrationConfig.workflows.deploy);
539
+ }
540
+ if (integrationConfig.files) {
541
+ for (const file of integrationConfig.files) {
542
+ if (!file.condition || file.condition === options.integrationInputs?.astro_mode) {
543
+ filesToApply.push(file);
544
+ }
545
+ }
546
+ }
547
+ if (options.configureCd && integrationConfig.cdFiles) {
548
+ for (const file of integrationConfig.cdFiles) {
549
+ if (!file.condition || file.condition === options.integrationInputs?.astro_mode) {
550
+ filesToApply.push(file);
551
+ }
552
+ }
536
553
  }
537
- const applyFile = async (file) => runStep(
538
- `Adding ${file.target}`,
539
- async () => copyTemplateFile(
554
+ const applyFile = async (file) => runStep(`Adding ${file.target}`, async () => {
555
+ const generateSecret = () => randomBytes(32).toString("base64url");
556
+ const templateValues = {
557
+ ...options.integrationInputs,
558
+ /* eslint-disable camelcase */
559
+ astro_port: "4321",
560
+ // Hardcoded to SSR port
561
+ vault_password: generateSecret(),
562
+ app_keys: `${generateSecret()},${generateSecret()}`,
563
+ api_token_salt: generateSecret(),
564
+ admin_jwt_secret: generateSecret(),
565
+ transfer_token_salt: generateSecret(),
566
+ jwt_secret: generateSecret()
567
+ /* eslint-enable camelcase */
568
+ };
569
+ return copyTemplateFile(
540
570
  join4(integrationRoot, file.template),
541
571
  join4(projectRoot, file.target),
542
- options.integrationInputs
543
- )
544
- );
572
+ templateValues
573
+ );
574
+ });
545
575
  const applySequentially = async (index) => {
546
576
  const file = filesToApply[index];
547
577
  if (!file) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smi-digital/create-smi-app",
3
- "version": "1.0.6",
3
+ "version": "2.1.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1 +1,28 @@
1
- npx lint-staged
1
+ #!/usr/bin/env sh
2
+
3
+ # 1. Run standard linters and formatters
4
+ npx lint-staged
5
+
6
+ # 2. Ansible-Vault Security Guard
7
+ # Find any staged files that end in .env and start with "production"
8
+ STAGED_ENV_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^production\..*\.env$')
9
+
10
+ for file in $STAGED_ENV_FILES; do
11
+ # Read the first line of the file
12
+ FIRST_LINE=$(head -n 1 "$file")
13
+
14
+ # Check if the first line indicates it is an Ansible Vault file
15
+ if [[ "$FIRST_LINE" != "\$ANSIBLE_VAULT;"* ]]; then
16
+ echo ""
17
+ echo "🚨 SECURITY ALERT: Unencrypted Production Secrets Detected! 🚨"
18
+ echo "File: $file"
19
+ echo ""
20
+ echo "You are attempting to commit a plain-text production .env file."
21
+ echo "Please encrypt it using Ansible-Vault before committing:"
22
+ echo ""
23
+ echo " ansible-vault encrypt $file"
24
+ echo ""
25
+ echo "Commit aborted."
26
+ exit 1
27
+ fi
28
+ done
@@ -1,2 +1,7 @@
1
1
  node_modules
2
- *.env
2
+ *.env
3
+ # Allow encrypted production env files to be committed
4
+ !production.*.env
5
+
6
+ # Ansible Vault Password (NEVER COMMIT THIS)
7
+ .vault-password
@@ -1,6 +1,3 @@
1
- # ==============================================================================
2
- # COPY THIS FILE TO: .github/workflows/deploy.yml in your project repository
3
- # ==============================================================================
4
1
  name: Deploy Production
5
2
 
6
3
  on:
@@ -11,16 +8,17 @@ on:
11
8
 
12
9
  jobs:
13
10
  call-central-deployer:
14
- uses: smi-digital/cd-library/.github/workflows/deploy-strapi-astro.yml@main
11
+ uses: smi-digital/cd-library/.github/workflows/deploy-ansible.yml@main
15
12
  with:
16
- # Replace the following Variables with the actual ones
17
13
  project_domain: __PROJECT_DOMAIN__
18
- frontend_domain: __FRONTEND_DOMAIN__
19
14
  backend_domain: __BACKEND_DOMAIN__
20
15
  app_name: __APP_NAME__
21
- include_www: __INCLUDE_WWW__
16
+ image_tag: latest
22
17
  secrets:
23
- # These secrets MUST be added to your repository settings!
24
- # Go to Settings > Secrets and variables > Actions
25
- ASTRO_FRONTEND_ENV: ${{ secrets.ASTRO_FRONTEND_ENV }}
26
- STRAPI_BACKEND_ENV: ${{ secrets.STRAPI_BACKEND_ENV }}
18
+ # These secrets must be configured at the Organization or Repository level:
19
+ SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
20
+ SERVER_IP: ${{ secrets.SERVER_IP }}
21
+ SERVER_DEPLOY_USER: ${{ secrets.SERVER_DEPLOY_USER }}
22
+ VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
23
+ ZOT_USERNAME: ${{ secrets.ZOT_USERNAME }}
24
+ ZOT_PASSWORD: ${{ secrets.ZOT_PASSWORD }}
@@ -0,0 +1 @@
1
+ __VAULT_PASSWORD__
@@ -0,0 +1,23 @@
1
+ # ========================================================
2
+ # EXAMPLE: Dockerfile for Strapi Backend
3
+ # Place this in your /backend folder
4
+ # ========================================================
5
+
6
+ FROM node:18-alpine
7
+ # Installing libvips-dev for sharp Compatibility
8
+ RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev > /dev/null 2>&1
9
+ ARG NODE_ENV=production
10
+ ENV NODE_ENV=${NODE_ENV}
11
+
12
+ WORKDIR /opt/
13
+ COPY package.json ./
14
+ RUN npm install -g node-gyp
15
+ RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install --only=production
16
+ ENV PATH /opt/node_modules/.bin:$PATH
17
+
18
+ WORKDIR /opt/app
19
+ COPY . .
20
+ RUN npm run build
21
+
22
+ EXPOSE 1337
23
+ CMD ["npm", "run", "start"]
@@ -0,0 +1,31 @@
1
+ # ========================================================
2
+ # EXAMPLE: Dockerfile for Astro SSR (Server-Side Rendering)
3
+ # Place this in your /frontend folder and name it 'Dockerfile'
4
+ # ========================================================
5
+
6
+ # Stage 1: Build with Bun
7
+ FROM oven/bun:1 as builder
8
+ WORKDIR /app
9
+ COPY package.json bun.lock ./
10
+ RUN bun install
11
+ COPY . .
12
+ RUN bun run build
13
+
14
+ # Stage 2: Serve dynamic application using Bun
15
+ FROM oven/bun:1-alpine
16
+ WORKDIR /app
17
+
18
+ # Copy the built SSR server bundle and node_modules
19
+ COPY --from=builder /app/node_modules ./node_modules
20
+ COPY --from=builder /app/dist ./dist
21
+ COPY --from=builder /app/package.json ./
22
+
23
+ # Expose port 4321 (Matches the 'frontend_port' logic in Ansible for SSR)
24
+ EXPOSE 4321
25
+
26
+ # Default environment variables
27
+ ENV HOST=0.0.0.0
28
+ ENV PORT=4321
29
+
30
+ # Start the server (Adjust entrypoint depending on your Astro Node/Bun adapter)
31
+ CMD ["bun", "run", "./dist/server/entry.mjs"]
@@ -0,0 +1,41 @@
1
+ export default {
2
+ register(/*{ strapi }*/) {},
3
+
4
+ async bootstrap({ strapi }) {
5
+ // 1. Find the Public Role
6
+ const publicRole = await strapi
7
+ .query('plugin::users-permissions.role')
8
+ .findOne({ where: { type: 'public' } });
9
+
10
+ if (!publicRole) return;
11
+
12
+ // 2. Dynamically find all Custom APIs created by the developer
13
+ // strapi.api contains all user-generated content types (ignoring system plugins)
14
+ const customApis = Object.keys(strapi.api);
15
+ const permissionsToOpen: string[] = [];
16
+
17
+ // 3. Build the permission strings (find and findOne) for every custom API
18
+ for (const apiName of customApis) {
19
+ permissionsToOpen.push(`api::${apiName}.${apiName}.find`);
20
+ permissionsToOpen.push(`api::${apiName}.${apiName}.findOne`);
21
+ }
22
+
23
+ // 4. Grant the permissions programmatically
24
+ for (const action of permissionsToOpen) {
25
+ const existingPermission = await strapi
26
+ .query('plugin::users-permissions.permission')
27
+ .findOne({ where: { role: publicRole.id, action } });
28
+
29
+ // If the permission doesn't exist yet, create it
30
+ if (!existingPermission) {
31
+ await strapi.query('plugin::users-permissions.permission').create({
32
+ data: {
33
+ action: action,
34
+ role: publicRole.id,
35
+ },
36
+ });
37
+ console.log(`[BOOTSTRAP] Automatically granted public access to: ${action}`);
38
+ }
39
+ }
40
+ },
41
+ };
@@ -0,0 +1,52 @@
1
+ services:
2
+ __APP_NAME__-strapi:
3
+ container_name: __APP_NAME__-strapi
4
+ # The image is built in CI and pushed to Zot
5
+ image: registry.smi-digital.com/__APP_NAME__-strapi:${IMAGE_TAG:-latest}
6
+ restart: unless-stopped
7
+ env_file:
8
+ - ./backend.env
9
+ volumes:
10
+ - ./data/uploads:/opt/app/public/uploads
11
+ - ./data/database:/opt/app/.tmp
12
+ networks:
13
+ - agency_shared_network
14
+ healthcheck:
15
+ test: ["CMD", "curl", "-f", "http://localhost:1337/admin"]
16
+ interval: 10s
17
+ timeout: 5s
18
+ retries: 15
19
+ labels:
20
+ - "traefik.enable=true"
21
+ # Route traffic from your backend domain to this container
22
+ - "traefik.http.routers.__APP_NAME__-strapi.rule=Host(`__BACKEND_DOMAIN__`)"
23
+ # Request an SSL certificate from the global Let's Encrypt resolver
24
+ - "traefik.http.routers.__APP_NAME__-strapi.tls.certresolver=letsencrypt"
25
+ # Tell Traefik which port to send traffic to inside the container
26
+ - "traefik.http.services.__APP_NAME__-strapi.loadbalancer.server.port=1337"
27
+ # Strapi requires larger body sizes for media uploads (25MB)
28
+ - "traefik.http.middlewares.__APP_NAME__-strapi-limit.buffering.maxRequestBodyBytes=25000000"
29
+ - "traefik.http.routers.__APP_NAME__-strapi.middlewares=__APP_NAME__-strapi-limit"
30
+
31
+ __APP_NAME__-astro:
32
+ container_name: __APP_NAME__-astro
33
+ image: registry.smi-digital.com/__APP_NAME__-astro:${IMAGE_TAG:-latest}
34
+ restart: unless-stopped
35
+ env_file:
36
+ - ./frontend.env
37
+ networks:
38
+ - agency_shared_network
39
+ depends_on:
40
+ __APP_NAME__-strapi:
41
+ condition: service_healthy
42
+ labels:
43
+ - "traefik.enable=true"
44
+ # Listen on both www and non-www (If include_www is selected during setup, you can manually adjust this)
45
+ - "traefik.http.routers.__APP_NAME__-astro.rule=Host(`__PROJECT_DOMAIN__`) || Host(`www.__PROJECT_DOMAIN__`)"
46
+ - "traefik.http.routers.__APP_NAME__-astro.tls.certresolver=letsencrypt"
47
+ # Port 4321 for Bun/SSR, Port 80 if pure SSG Nginx container
48
+ - "traefik.http.services.__APP_NAME__-astro.loadbalancer.server.port=__ASTRO_PORT__"
49
+
50
+ networks:
51
+ agency_shared_network:
52
+ external: true
@@ -0,0 +1,16 @@
1
+ // This middleware enforces Stale-While-Revalidate (SWR) caching for SSR pages.
2
+ // It ensures pages are fast for users while updating in the background.
3
+ import { defineMiddleware } from 'astro:middleware';
4
+
5
+ export const onRequest = defineMiddleware(async (context, next) => {
6
+ const response = await next();
7
+
8
+ // Only apply SWR caching to HTML responses (SSR pages)
9
+ // We do not want to aggressively cache API routes or dynamic forms
10
+ if (response.headers.get('content-type')?.includes('text/html')) {
11
+ // Cache for 10 seconds. For the next 1 hour (3600s), serve stale cache while fetching new data.
12
+ response.headers.set('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=3600');
13
+ }
14
+
15
+ return response;
16
+ });
@@ -15,13 +15,6 @@
15
15
  "default": "my-new-client.com",
16
16
  "required": true
17
17
  },
18
- {
19
- "key": "frontend_domain",
20
- "message": "What is the frontend domain?",
21
- "type": "input",
22
- "default": "my-new-client.com",
23
- "required": true
24
- },
25
18
  {
26
19
  "key": "backend_domain",
27
20
  "message": "What is the backend domain?",
@@ -53,5 +46,41 @@
53
46
  "template": ".github/workflows/deploy.yml.template",
54
47
  "target": ".github/workflows/deploy.yml"
55
48
  }
56
- }
49
+ },
50
+ "files": [
51
+ {
52
+ "template": "backend/src/index.ts.template",
53
+ "target": "backend/src/index.ts"
54
+ }
55
+ ],
56
+ "cdFiles": [
57
+ {
58
+ "template": "docker-compose.yml.template",
59
+ "target": "docker-compose.yml"
60
+ },
61
+ {
62
+ "template": "Dockerfile.backend.template",
63
+ "target": "backend/Dockerfile"
64
+ },
65
+ {
66
+ "template": "Dockerfile.frontend.ssr.template",
67
+ "target": "frontend/Dockerfile"
68
+ },
69
+ {
70
+ "template": "frontend/src/middleware.ts.template",
71
+ "target": "frontend/src/middleware.ts"
72
+ },
73
+ {
74
+ "template": ".vault-password.template",
75
+ "target": ".vault-password"
76
+ },
77
+ {
78
+ "template": "production.frontend.env.template",
79
+ "target": "production.frontend.env"
80
+ },
81
+ {
82
+ "template": "production.backend.env.template",
83
+ "target": "production.backend.env"
84
+ }
85
+ ]
57
86
  }
@@ -0,0 +1,19 @@
1
+ # ==============================================================================
2
+ # PRODUCTION BACKEND SECRETS (Strapi)
3
+ #
4
+ # 🚨 SECURITY WARNING 🚨
5
+ # This file MUST be encrypted before committing to Git!
6
+ # Run: ansible-vault encrypt production.backend.env --vault-password-file .vault-password
7
+ # ==============================================================================
8
+
9
+ # Strapi System Keys (Automatically generated by create-smi-app)
10
+ APP_KEYS=__APP_KEYS__
11
+ API_TOKEN_SALT=__API_TOKEN_SALT__
12
+ ADMIN_JWT_SECRET=__ADMIN_JWT_SECRET__
13
+ TRANSFER_TOKEN_SALT=__TRANSFER_TOKEN_SALT__
14
+ JWT_SECRET=__JWT_SECRET__
15
+
16
+ # Production Settings
17
+ HOST=0.0.0.0
18
+ PORT=1337
19
+ NODE_ENV=production
@@ -0,0 +1,14 @@
1
+ # ==============================================================================
2
+ # PRODUCTION FRONTEND SECRETS (Astro SSR)
3
+ #
4
+ # 🚨 SECURITY WARNING 🚨
5
+ # This file MUST be encrypted before committing to Git!
6
+ # Run: ansible-vault encrypt production.frontend.env --vault-password-file .vault-password
7
+ # ==============================================================================
8
+
9
+ # Internal Docker Network routing (Do not change)
10
+ PUBLIC_API_URL=http://__APP_NAME__-strapi:1337
11
+
12
+ # Add your client-specific 3rd party secrets below
13
+ # STRIPE_SECRET_KEY=sk_live_...
14
+ # SENDGRID_API_KEY=SG...