create-tigra 2.4.0 → 2.5.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/package.json +1 -1
- package/template/client/.dockerignore +63 -0
- package/template/client/Dockerfile +98 -0
- package/template/client/next.config.ts +1 -0
- package/template/client/src/app/layout.tsx +1 -1
- package/template/client/src/app/providers.tsx +1 -1
- package/template/client/src/features/auth/components/AuthInitializer.tsx +8 -2
- package/template/client/src/lib/api/axios.config.ts +1 -1
- package/template/server/.dockerignore +0 -1
- package/template/server/.env.example +21 -0
- package/template/server/.env.example.production +26 -0
- package/template/server/Dockerfile +8 -0
- package/template/server/package.json +1 -1
- package/template/server/src/server.ts +1 -1
- package/template/server/tsconfig.build.json +4 -0
- package/template/server/tsconfig.json +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Dependencies (will be installed fresh in Docker)
|
|
2
|
+
node_modules
|
|
3
|
+
npm-debug.log*
|
|
4
|
+
yarn-debug.log*
|
|
5
|
+
yarn-error.log*
|
|
6
|
+
pnpm-debug.log*
|
|
7
|
+
|
|
8
|
+
# Build output (will be built fresh in Docker)
|
|
9
|
+
.next
|
|
10
|
+
out
|
|
11
|
+
*.tsbuildinfo
|
|
12
|
+
|
|
13
|
+
# Environment files (use Docker env vars instead)
|
|
14
|
+
.env
|
|
15
|
+
.env.local
|
|
16
|
+
.env.*.local
|
|
17
|
+
.env.development
|
|
18
|
+
.env.test
|
|
19
|
+
|
|
20
|
+
# Git
|
|
21
|
+
.git
|
|
22
|
+
.gitignore
|
|
23
|
+
.gitattributes
|
|
24
|
+
|
|
25
|
+
# IDE
|
|
26
|
+
.vscode
|
|
27
|
+
.idea
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
*~
|
|
31
|
+
.DS_Store
|
|
32
|
+
|
|
33
|
+
# Testing
|
|
34
|
+
coverage
|
|
35
|
+
.nyc_output
|
|
36
|
+
**/__tests__
|
|
37
|
+
**/*.test.ts
|
|
38
|
+
**/*.test.tsx
|
|
39
|
+
**/*.spec.ts
|
|
40
|
+
**/*.spec.tsx
|
|
41
|
+
vitest.config.ts
|
|
42
|
+
jest.config.ts
|
|
43
|
+
|
|
44
|
+
# Documentation
|
|
45
|
+
docs
|
|
46
|
+
CHANGELOG.md
|
|
47
|
+
LICENSE
|
|
48
|
+
|
|
49
|
+
# Docker
|
|
50
|
+
Dockerfile
|
|
51
|
+
.dockerignore
|
|
52
|
+
docker-compose.yml
|
|
53
|
+
|
|
54
|
+
# CI/CD
|
|
55
|
+
.github
|
|
56
|
+
.gitlab-ci.yml
|
|
57
|
+
.circleci
|
|
58
|
+
|
|
59
|
+
# Misc
|
|
60
|
+
.prettierrc
|
|
61
|
+
.eslintrc
|
|
62
|
+
.eslintignore
|
|
63
|
+
.editorconfig
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ===================================================================
|
|
2
|
+
# Multi-stage Dockerfile for Next.js Production Deployment
|
|
3
|
+
# Optimized for Coolify, Docker Compose, and Kubernetes
|
|
4
|
+
# Requires `output: "standalone"` in next.config.ts
|
|
5
|
+
# ===================================================================
|
|
6
|
+
|
|
7
|
+
# ===================================================================
|
|
8
|
+
# Stage 1: Dependencies (cached layer)
|
|
9
|
+
# ===================================================================
|
|
10
|
+
FROM node:20-alpine AS dependencies
|
|
11
|
+
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
|
|
14
|
+
# Copy package files
|
|
15
|
+
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
|
|
16
|
+
|
|
17
|
+
# Install dependencies (use the lockfile that exists)
|
|
18
|
+
RUN if [ -f pnpm-lock.yaml ]; then \
|
|
19
|
+
npm install -g pnpm && pnpm install --frozen-lockfile; \
|
|
20
|
+
elif [ -f yarn.lock ]; then \
|
|
21
|
+
yarn install --frozen-lockfile; \
|
|
22
|
+
else \
|
|
23
|
+
npm ci; \
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# ===================================================================
|
|
27
|
+
# Stage 2: Build (compile Next.js)
|
|
28
|
+
# ===================================================================
|
|
29
|
+
FROM node:20-alpine AS builder
|
|
30
|
+
|
|
31
|
+
WORKDIR /app
|
|
32
|
+
|
|
33
|
+
# Copy dependencies from previous stage
|
|
34
|
+
COPY --from=dependencies /app/node_modules ./node_modules
|
|
35
|
+
|
|
36
|
+
# Copy source code
|
|
37
|
+
COPY . .
|
|
38
|
+
|
|
39
|
+
# Build arguments for env vars needed at build time
|
|
40
|
+
# Next.js inlines NEXT_PUBLIC_* values during build, so they must
|
|
41
|
+
# be available here. Pass them via --build-arg in Coolify/Docker.
|
|
42
|
+
ARG NEXT_PUBLIC_API_BASE_URL
|
|
43
|
+
ARG NEXT_PUBLIC_APP_NAME
|
|
44
|
+
|
|
45
|
+
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
|
46
|
+
ENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME
|
|
47
|
+
|
|
48
|
+
# Disable Next.js telemetry during build
|
|
49
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
50
|
+
|
|
51
|
+
# Build the application (outputs to .next/standalone)
|
|
52
|
+
RUN npm run build
|
|
53
|
+
|
|
54
|
+
# ===================================================================
|
|
55
|
+
# Stage 3: Production Runtime
|
|
56
|
+
# ===================================================================
|
|
57
|
+
FROM node:20-alpine AS production
|
|
58
|
+
|
|
59
|
+
# Install dumb-init for proper signal handling
|
|
60
|
+
RUN apk add --no-cache dumb-init
|
|
61
|
+
|
|
62
|
+
# Create non-root user for security
|
|
63
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
64
|
+
adduser -S nextjs -u 1001
|
|
65
|
+
|
|
66
|
+
WORKDIR /app
|
|
67
|
+
|
|
68
|
+
# Copy the standalone server (includes only required node_modules)
|
|
69
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
70
|
+
|
|
71
|
+
# Copy static assets (not included in standalone output)
|
|
72
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
73
|
+
|
|
74
|
+
# Copy public folder (favicon, images, etc.)
|
|
75
|
+
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
76
|
+
|
|
77
|
+
# Switch to non-root user
|
|
78
|
+
USER nextjs
|
|
79
|
+
|
|
80
|
+
# Disable telemetry at runtime
|
|
81
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
82
|
+
|
|
83
|
+
# Set hostname to listen on all interfaces (required in containers)
|
|
84
|
+
ENV HOSTNAME="0.0.0.0"
|
|
85
|
+
|
|
86
|
+
# Default port (override via PORT env var in Coolify)
|
|
87
|
+
ENV PORT=3000
|
|
88
|
+
EXPOSE 3000
|
|
89
|
+
|
|
90
|
+
# Health check for container orchestration
|
|
91
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
92
|
+
CMD node -e "const p=process.env.PORT||3000;require('http').get('http://localhost:'+p,(r)=>{process.exit(r.statusCode===200?0:1)})"
|
|
93
|
+
|
|
94
|
+
# Use dumb-init to handle signals properly
|
|
95
|
+
ENTRYPOINT ["dumb-init", "--"]
|
|
96
|
+
|
|
97
|
+
# Start the standalone Next.js server
|
|
98
|
+
CMD ["node", "server.js"]
|
|
@@ -27,7 +27,7 @@ export default function RootLayout({
|
|
|
27
27
|
children: React.ReactNode;
|
|
28
28
|
}>): React.ReactElement {
|
|
29
29
|
return (
|
|
30
|
-
<html lang="en" suppressHydrationWarning>
|
|
30
|
+
<html lang="en" className="dark" suppressHydrationWarning>
|
|
31
31
|
<body className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}>
|
|
32
32
|
<Providers>{children}</Providers>
|
|
33
33
|
</body>
|
|
@@ -29,7 +29,7 @@ export function Providers({ children }: { children: React.ReactNode }): React.Re
|
|
|
29
29
|
return (
|
|
30
30
|
<ReduxProvider store={store}>
|
|
31
31
|
<QueryClientProvider client={queryClient}>
|
|
32
|
-
<ThemeProvider attribute="class" defaultTheme="
|
|
32
|
+
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
|
|
33
33
|
<AuthInitializer>
|
|
34
34
|
{children}
|
|
35
35
|
</AuthInitializer>
|
|
@@ -10,12 +10,16 @@ import { ROUTES } from '@/lib/constants/routes';
|
|
|
10
10
|
import { authService } from '../services/auth.service';
|
|
11
11
|
import { setUser, setInitialized } from '../store/authSlice';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const PROTECTED_PATHS: string[] = [ROUTES.DASHBOARD, ROUTES.PROFILE];
|
|
14
14
|
|
|
15
15
|
interface AuthInitializerProps {
|
|
16
16
|
children: React.ReactNode;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function isProtectedPath(pathname: string): boolean {
|
|
20
|
+
return PROTECTED_PATHS.some((path) => pathname.startsWith(path));
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
export const AuthInitializer = ({ children }: AuthInitializerProps): React.ReactElement => {
|
|
20
24
|
const dispatch = useAppDispatch();
|
|
21
25
|
const pathname = usePathname();
|
|
@@ -23,10 +27,12 @@ export const AuthInitializer = ({ children }: AuthInitializerProps): React.React
|
|
|
23
27
|
const { isAuthenticated, isLoggingOut } = useAppSelector((state) => state.auth);
|
|
24
28
|
|
|
25
29
|
useEffect(() => {
|
|
26
|
-
|
|
30
|
+
// On public pages, skip auth hydration — just mark as initialized
|
|
31
|
+
if (!isProtectedPath(pathname)) {
|
|
27
32
|
dispatch(setInitialized());
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
35
|
+
|
|
30
36
|
if (isAuthenticated || isLoggingOut) return;
|
|
31
37
|
|
|
32
38
|
let cancelled = false;
|
|
@@ -45,7 +45,7 @@ apiClient.interceptors.response.use(
|
|
|
45
45
|
|
|
46
46
|
// Don't retry auth endpoints that don't use tokens —
|
|
47
47
|
// a 401 here means wrong credentials, not an expired token.
|
|
48
|
-
const noRetryEndpoints = [
|
|
48
|
+
const noRetryEndpoints: string[] = [
|
|
49
49
|
API_ENDPOINTS.AUTH.LOGIN,
|
|
50
50
|
API_ENDPOINTS.AUTH.REGISTER,
|
|
51
51
|
API_ENDPOINTS.AUTH.REFRESH,
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# ===================================================================
|
|
2
2
|
# APPLICATION CONFIGURATION
|
|
3
3
|
# ===================================================================
|
|
4
|
+
#
|
|
5
|
+
# COOLIFY DEPLOYMENT NOTE:
|
|
6
|
+
# When adding environment variables in Coolify, do NOT check
|
|
7
|
+
# "Available at Buildtime" for any variable unless explicitly noted.
|
|
8
|
+
# The server Dockerfile handles build-time config internally.
|
|
9
|
+
# All variables below are RUNTIME-ONLY unless marked otherwise.
|
|
10
|
+
#
|
|
11
|
+
# ===================================================================
|
|
4
12
|
|
|
5
13
|
# Environment: development | production | test
|
|
14
|
+
# COOLIFY: Do NOT check "Available at Buildtime" — the Dockerfile
|
|
15
|
+
# sets NODE_ENV=development during build to ensure devDependencies install.
|
|
6
16
|
NODE_ENV=development
|
|
7
17
|
|
|
8
18
|
# Server port (default: 8000)
|
|
@@ -17,6 +27,7 @@ HOST=0.0.0.0
|
|
|
17
27
|
|
|
18
28
|
# Database connection string
|
|
19
29
|
# Format: mysql://username:password@host:port/database
|
|
30
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
|
|
20
31
|
DATABASE_URL="mysql://root:rootpassword@localhost:{{MYSQL_PORT}}/{{DATABASE_NAME}}"
|
|
21
32
|
|
|
22
33
|
# Connection pool settings (for high-traffic production)
|
|
@@ -31,6 +42,7 @@ DATABASE_POOL_MAX=10
|
|
|
31
42
|
|
|
32
43
|
# Redis connection URL
|
|
33
44
|
# Format: redis://[:password@]host:port[/database]
|
|
45
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
|
|
34
46
|
REDIS_URL="redis://localhost:{{REDIS_PORT}}"
|
|
35
47
|
|
|
36
48
|
# Max retry attempts for failed Redis operations
|
|
@@ -62,6 +74,13 @@ RATE_LIMIT_MULTIPLIER=1
|
|
|
62
74
|
# Maximum file upload size in MB (default: 10)
|
|
63
75
|
MAX_FILE_SIZE_MB=10
|
|
64
76
|
|
|
77
|
+
# COOLIFY PERSISTENT STORAGE (required for uploads to survive redeployments):
|
|
78
|
+
# Go to your service in Coolify → Storages → Add Volume Mount:
|
|
79
|
+
# Name: uploads (or <project-name>-uploads)
|
|
80
|
+
# Source Path: (leave empty — Coolify manages the Docker volume)
|
|
81
|
+
# Destination Path: /app/uploads
|
|
82
|
+
# Without this, all uploaded files are lost on every redeployment.
|
|
83
|
+
|
|
65
84
|
# ===================================================================
|
|
66
85
|
# DOCKER PORTS (auto-generated, unique per project)
|
|
67
86
|
# ===================================================================
|
|
@@ -79,6 +98,7 @@ REDIS_COMMANDER_PORT={{REDIS_COMMANDER_PORT}}
|
|
|
79
98
|
# JWT secret key (MUST be at least 32 characters)
|
|
80
99
|
# CRITICAL: Generate a strong random secret for production!
|
|
81
100
|
# Example: openssl rand -base64 48
|
|
101
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
|
|
82
102
|
JWT_SECRET="{{JWT_SECRET}}"
|
|
83
103
|
|
|
84
104
|
# Access token expiry (short-lived)
|
|
@@ -92,6 +112,7 @@ JWT_REFRESH_EXPIRY="7d"
|
|
|
92
112
|
# Cookie signing secret (separate from JWT for defense-in-depth)
|
|
93
113
|
# Optional: defaults to JWT_SECRET if not set
|
|
94
114
|
# For production: generate a separate secret: openssl rand -base64 48
|
|
115
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
|
|
95
116
|
# COOKIE_SECRET="change-this-to-a-different-secret-at-least-32-chars"
|
|
96
117
|
|
|
97
118
|
# Cookie domain for cross-origin deployments
|
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
# ===================================================================
|
|
4
4
|
# This file contains production-optimized settings for high-traffic
|
|
5
5
|
# deployments (10K-100K users/day). Copy to .env and customize.
|
|
6
|
+
#
|
|
7
|
+
# COOLIFY DEPLOYMENT NOTE:
|
|
8
|
+
# When adding environment variables in Coolify, do NOT check
|
|
9
|
+
# "Available at Buildtime" for any variable unless explicitly noted.
|
|
10
|
+
# The server Dockerfile handles build-time config internally.
|
|
11
|
+
# Secrets (DATABASE_URL, JWT_SECRET, COOKIE_SECRET, REDIS_URL) must
|
|
12
|
+
# NEVER be available at buildtime — they get baked into the image layer.
|
|
13
|
+
#
|
|
6
14
|
# ===================================================================
|
|
7
15
|
|
|
8
16
|
# ===================================================================
|
|
9
17
|
# APPLICATION
|
|
10
18
|
# ===================================================================
|
|
11
19
|
|
|
20
|
+
# COOLIFY: Do NOT check "Available at Buildtime" — the Dockerfile
|
|
21
|
+
# sets NODE_ENV=development during build to ensure devDependencies install.
|
|
12
22
|
NODE_ENV=production
|
|
13
23
|
PORT=3000
|
|
14
24
|
HOST=0.0.0.0
|
|
@@ -19,6 +29,7 @@ HOST=0.0.0.0
|
|
|
19
29
|
|
|
20
30
|
# Production database connection
|
|
21
31
|
# CRITICAL: Use secure credentials, SSL/TLS, and private network
|
|
32
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
|
|
22
33
|
DATABASE_URL="mysql://prod_user:SECURE_PASSWORD@db.internal:3306/prod_db?ssl=true"
|
|
23
34
|
|
|
24
35
|
# Connection pool for high traffic (10K-100K users/day)
|
|
@@ -33,6 +44,7 @@ DATABASE_POOL_MAX=50
|
|
|
33
44
|
|
|
34
45
|
# Production Redis instance
|
|
35
46
|
# CRITICAL: Use authentication and private network
|
|
47
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime".
|
|
36
48
|
REDIS_URL="redis://:REDIS_PASSWORD@redis.internal:6379"
|
|
37
49
|
|
|
38
50
|
# Production retry settings
|
|
@@ -60,18 +72,30 @@ RATE_LIMIT_AUTH_REGISTER_MAX=5
|
|
|
60
72
|
# Maximum file upload size in MB
|
|
61
73
|
MAX_FILE_SIZE_MB=10
|
|
62
74
|
|
|
75
|
+
# COOLIFY PERSISTENT STORAGE (required for uploads to survive redeployments):
|
|
76
|
+
# Go to your service in Coolify → Storages → Add Volume Mount:
|
|
77
|
+
# Name: uploads (or <project-name>-uploads)
|
|
78
|
+
# Source Path: (leave empty — Coolify manages the Docker volume)
|
|
79
|
+
# Destination Path: /app/uploads
|
|
80
|
+
# Without this, all uploaded files are lost on every redeployment.
|
|
81
|
+
|
|
63
82
|
# ===================================================================
|
|
64
83
|
# JWT AUTHENTICATION
|
|
65
84
|
# ===================================================================
|
|
66
85
|
|
|
67
86
|
# CRITICAL: Generate a cryptographically secure secret
|
|
68
87
|
# Example: openssl rand -base64 48
|
|
88
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
|
|
69
89
|
JWT_SECRET="YOUR_PRODUCTION_JWT_SECRET_MUST_BE_AT_LEAST_32_CHARS_LONG"
|
|
70
90
|
|
|
71
91
|
# Production token expiry (tighter security)
|
|
72
92
|
JWT_ACCESS_EXPIRY="15m"
|
|
73
93
|
JWT_REFRESH_EXPIRY="7d"
|
|
74
94
|
|
|
95
|
+
# Cookie signing secret (separate from JWT for defense-in-depth)
|
|
96
|
+
# COOLIFY: Runtime only. Do NOT check "Available at Buildtime" — this is a secret!
|
|
97
|
+
COOKIE_SECRET="YOUR_PRODUCTION_COOKIE_SECRET_MUST_BE_AT_LEAST_32_CHARS"
|
|
98
|
+
|
|
75
99
|
# ===================================================================
|
|
76
100
|
# CORS
|
|
77
101
|
# ===================================================================
|
|
@@ -118,3 +142,5 @@ SENTRY_DSN="https://YOUR_PUBLIC_KEY@o0.ingest.sentry.io/YOUR_PROJECT_ID"
|
|
|
118
142
|
# [ ] All secrets are stored in environment variables, not in code
|
|
119
143
|
# [ ] SSL/TLS certificates are configured
|
|
120
144
|
# [ ] Firewall rules restrict access to database and Redis
|
|
145
|
+
# [ ] In Coolify: NO secrets have "Available at Buildtime" checked
|
|
146
|
+
# [ ] In Coolify: NODE_ENV does NOT have "Available at Buildtime" checked
|
|
@@ -28,6 +28,11 @@ RUN if [ -f pnpm-lock.yaml ]; then \
|
|
|
28
28
|
# ===================================================================
|
|
29
29
|
FROM node:20-alpine AS builder
|
|
30
30
|
|
|
31
|
+
# Override NODE_ENV to ensure devDependencies are installed.
|
|
32
|
+
# Coolify may inject NODE_ENV=production as a build arg, which causes npm ci
|
|
33
|
+
# to skip devDependencies (typescript, @types/node, etc.) and break the build.
|
|
34
|
+
ENV NODE_ENV=development
|
|
35
|
+
|
|
31
36
|
WORKDIR /app
|
|
32
37
|
|
|
33
38
|
# Copy package files and install ALL dependencies (including dev)
|
|
@@ -77,6 +82,9 @@ COPY --from=builder --chown=nodejs:nodejs /app/package.json ./package.json
|
|
|
77
82
|
# Copy Prisma files (needed for migrations)
|
|
78
83
|
COPY --from=builder --chown=nodejs:nodejs /app/prisma ./prisma
|
|
79
84
|
|
|
85
|
+
# Create uploads directory with correct ownership (must be before USER nodejs)
|
|
86
|
+
RUN mkdir -p /app/uploads && chown nodejs:nodejs /app/uploads
|
|
87
|
+
|
|
80
88
|
# Switch to non-root user
|
|
81
89
|
USER nodejs
|
|
82
90
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"main": "dist/server.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "tsx watch src/server.ts",
|
|
9
|
-
"build": "tsc && tsc-alias",
|
|
9
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
10
10
|
"start": "node dist/server.js",
|
|
11
11
|
"test": "vitest run",
|
|
12
12
|
"test:watch": "vitest",
|
|
@@ -13,7 +13,7 @@ async function start(): Promise<void> {
|
|
|
13
13
|
if (isShuttingDown) return; // Prevent multiple shutdown attempts
|
|
14
14
|
isShuttingDown = true;
|
|
15
15
|
|
|
16
|
-
logger.info(`[SERVER] Received ${signal}
|
|
16
|
+
logger.info(`[SERVER] Received ${signal} - shutting down gracefully`);
|
|
17
17
|
try {
|
|
18
18
|
if (app) {
|
|
19
19
|
await app.close();
|