Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/server-nestjs/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
DEV_SETUP="true"
# Mode d'exécution Node.js (development | production | test)
NODE_ENV=development
# Activation des plugins externes (true | false). Actif par défaut en développement.
USE_PLUGINS=true
# HOME=/home/node
# Secret utilisé pour signer les cookies de session (min. 32 caractères)
SESSION_SECRET=a-very-strong-secret-with-more-than-32-char
Expand Down Expand Up @@ -41,6 +43,8 @@ CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr
# --- Configuration OpenCDS ---
# URL de l'API OpenCDS (laisser vide pour désactiver)
OPENCDS_URL=
# URL interne de l'API OpenCDS
OPENCDS_INTERNAL_URL=
# Token d'authentification pour l'API OpenCDS
OPENCDS_API_TOKEN=token
# Vérification du certificat TLS de l'API OpenCDS (true | false)
Expand Down
4 changes: 4 additions & 0 deletions apps/server-nestjs/.env.docker-example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ DOCKER=true
DEV_SETUP="true"
# Mode d'exécution Node.js (development | production | test)
NODE_ENV=development
# Activation des plugins externes (true | false).
USE_PLUGINS=fasle
# Secret utilisé pour signer les cookies de session (min. 32 caractères)
SESSION_SECRET=a-very-strong-secret-with-more-than-32-char

Expand Down Expand Up @@ -43,6 +45,8 @@ CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr
# URL de l'API OpenCDS (laisser vide pour désactiver)
# Par défaut configuré sur le conteneur Mockoon (à changer sur envs non-PAX)
OPENCDS_URL=
# URL interne de l'API OpenCDS
OPENCDS_INTERNAL_URL=
# Token d'authentification pour l'API OpenCDS
OPENCDS_API_TOKEN=token
# Vérification du certificat TLS de l'API OpenCDS (true | false)
Expand Down
3 changes: 3 additions & 0 deletions apps/server-nestjs/.env.integ-example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
DEV_SETUP="false"
# Active le mode intégration (charge ce fichier en surcharge)
INTEGRATION=true
# Activation des plugins externes (true | false). Désactivé en mode intégration.
USE_PLUGINS=false

# --- Keycloak ---
# Protocole (généralement https en intégration)
Expand Down Expand Up @@ -111,5 +113,6 @@ DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7

# Configuration OpenCDS
OPENCDS_URL=
OPENCDS_INTERNAL_URL=
OPENCDS_API_TOKEN=token
OPENCDS_API_TLS_REJECT_UNAUTHORIZED=false
26 changes: 26 additions & 0 deletions apps/server-nestjs/src/config/argocd.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const argocdFeatureSchema = z.object({
USE_ARGOCD: flag(truthySchema.default('true')),
ARGO_NAMESPACE: z.string().default('argocd'),
ARGOCD_URL: nonEmpty(z.string().url()),
ARGOCD_INTERNAL_URL: nonEmpty(z.string().url()),
ARGOCD_EXTRA_REPOSITORIES: nonEmpty(z.string()),
DSO_ENV_CHART_VERSION: z.string().default('dso-env-1.6.0'),
DSO_NS_CHART_VERSION: z.string().default('dso-ns-1.1.5'),
VAULT__DEPLOY_VAULT_CONNECTION_IN_NS: flag(truthySchema.default('false')),
}).transform(raw => ({
enabled: raw.USE_ARGOCD,
namespace: raw.ARGO_NAMESPACE,
url: raw.ARGOCD_URL,
internalUrl: raw.ARGOCD_INTERNAL_URL,
extraRepositories: raw.ARGOCD_EXTRA_REPOSITORIES,
dsoEnvChartVersion: raw.DSO_ENV_CHART_VERSION,
dsoNsChartVersion: raw.DSO_NS_CHART_VERSION,
vaultDeployVaultConnectionInNs: raw.VAULT__DEPLOY_VAULT_CONNECTION_IN_NS,
internalOrPublicUrl: raw.ARGOCD_INTERNAL_URL || raw.ARGOCD_URL || undefined,
}))

export const argocdConfigFactory = registerAs('argocd', () => argocdFeatureSchema.parse(process.env))
48 changes: 48 additions & 0 deletions apps/server-nestjs/src/config/base.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { BaseConfig } from '../modules/infrastructure/config/base.config'
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const baseFeatureSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
INTEGRATION: flag(truthySchema.default('false')),
CI: flag(truthySchema.default('false')),
DEV_SETUP: flag(truthySchema.default('false')),
DOCKER: flag(truthySchema.default('false')),
SERVER_HOST: z.string().default('localhost'),
SERVER_PORT: z.string().transform(Number).default('0'),
APP_VERSION: z.string().optional().default('unknown'),
DB_URL: z.string().url().optional(),
SESSION_SECRET: z.string().min(32).optional(),
CONTACT_EMAIL: z.string().email().default('cloudpinative-relations@interieur.gouv.fr'),
MOCK_PLUGINS: flag(truthySchema.default('false')),
PROJECTS_ROOT_DIR: nonEmpty(z.string()),
PLUGINS_DIR: z.string().default('/plugins'),
HTTP_PROXY: nonEmpty(z.string().url()),
HTTPS_PROXY: nonEmpty(z.string().url()),
}).transform((raw): BaseConfig => ({
nodeEnv: raw.NODE_ENV === 'test' ? 'test' : raw.NODE_ENV === 'development' ? 'development' : 'production',

Check warning on line 24 in apps/server-nestjs/src/config/base.config.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/config/base.config.ts#L24

Extract this nested ternary operation into an independent statement.
isTest: raw.NODE_ENV === 'test',
isDev: raw.NODE_ENV === 'development',
isCI: raw.CI,
isProd: raw.NODE_ENV === 'production',
integration: raw.INTEGRATION,
ci: raw.CI,
devSetup: raw.DEV_SETUP,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: redundant field — both ci: raw.CI (here) and isCI: raw.CI (line 27) are mapped; the BaseConfig interface declares both. Harmless, but pick one name.

docker: raw.DOCKER,
serverHost: raw.SERVER_HOST,
serverPort: raw.SERVER_PORT,
appVersion: raw.NODE_ENV === 'production' ? raw.APP_VERSION : 'dev',
dbUrl: raw.DB_URL,
sessionSecret: raw.SESSION_SECRET,
contactEmail: raw.CONTACT_EMAIL,
mockPlugins: raw.MOCK_PLUGINS,
projectsRootDir: raw.PROJECTS_ROOT_DIR,
pluginsDir: raw.PLUGINS_DIR,
httpProxy: raw.HTTP_PROXY,
httpsProxy: raw.HTTPS_PROXY,
}))

export const baseConfigFactory = registerAs('base', () => baseFeatureSchema.parse(process.env))

export type { BaseConfig } from '../modules/infrastructure/config/base.config'
52 changes: 52 additions & 0 deletions apps/server-nestjs/src/config/config.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import z from 'zod'
import { csv, flag, nonEmpty, truthySchema } from './config.utils'

describe('config.utils', () => {
describe('flag', () => {
it('coerces true/1 (case-insensitive) to true', () => {
const schema = flag(truthySchema.default('false'))
expect(schema.parse('true')).toBe(true)
expect(schema.parse('1')).toBe(true)
expect(schema.parse('TRUE')).toBe(true)
})

it('coerces false/0 to false', () => {
const schema = flag(truthySchema.default('true'))
expect(schema.parse('false')).toBe(false)
expect(schema.parse('0')).toBe(false)
})

it('falls back to the schema default when missing', () => {
expect(flag(truthySchema.default('false')).parse(undefined)).toBe(false)
expect(flag(truthySchema.default('true')).parse(undefined)).toBe(true)
})
})

describe('nonEmpty', () => {
const schema = nonEmpty(z.string())

it('trims and keeps a non-empty value', () => {
expect(schema.parse(' value ')).toBe('value')
})

it('maps empty/whitespace/missing to undefined', () => {
expect(schema.parse('')).toBeUndefined()
expect(schema.parse(' ')).toBeUndefined()
expect(schema.parse(undefined)).toBeUndefined()
})
})

describe('csv', () => {
const schema = csv(z.string())

it('splits, trims and drops empty parts', () => {
expect(schema.parse('a, b ,,c')).toEqual(['a', 'b', 'c'])
})

it('maps missing/empty to an empty array', () => {
expect(schema.parse(undefined)).toEqual([])
expect(schema.parse('')).toEqual([])
})
})
})
36 changes: 36 additions & 0 deletions apps/server-nestjs/src/config/config.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import z from 'zod'

// Shared truthy enum for flag(): 'true'/'false'/'1'/'0'.
export const truthySchema = z.enum(['true', 'false', '1', '0'])

// Boolean flag. Strictly takes a z string schema (e.g. truthySchema.default('true')).
// Lowercases, then coerces 'true'/'1' -> true, 'false'/'0' -> false.
// Missing value falls back to the schema's default.
export function flag(schema: z.ZodType<string, z.ZodTypeDef, unknown>) {
return z
.preprocess(
val => (typeof val === 'string' ? val.toLowerCase() : val),
schema,
)
.transform(val => val === 'true' || val === '1')
}

// Trimmed string; empty/whitespace-only -> undefined; missing/empty string -> undefined.
// Pass a refined string schema (e.g. z.string().url()) to validate the non-empty value.
export function nonEmpty<T extends z.ZodString>(schema: T) {
return schema
.transform(value => value.trim() || undefined)
.optional()
.or(z.literal('').transform(() => undefined))
}

// Comma-separated string -> array of schema-validated, trimmed non-empty parts.
// Empty/missing -> []. Strictly takes a z string schema (applied per element).
export function csv<T extends z.ZodType<string, z.ZodTypeDef, unknown>>(schema: T) {
return z

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: csv() is never imported anywhere (only flag/nonEmpty/truthySchema are used). New dead code — remove (YAGNI).

.preprocess(
val => (typeof val === 'string' ? val : val ?? ''),
z.string().transform(value => value.split(',').map(part => part.trim()).filter(Boolean)),
)
.pipe(z.array(schema))
}
28 changes: 28 additions & 0 deletions apps/server-nestjs/src/config/gitlab.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const gitlabFeatureSchema = z.object({
USE_GITLAB: flag(truthySchema.default('true')),
GITLAB_TOKEN: z.string().optional(),
GITLAB_URL: nonEmpty(z.string().url()),
GITLAB_INTERNAL_URL: nonEmpty(z.string().url()),
GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS: z.coerce.number().int().positive().default(180),
GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS: z.coerce.number().int().positive().default(90),
PROJECTS_ROOT_DIR: nonEmpty(z.string()),
}).transform((raw) => {
const urlBase = raw.GITLAB_INTERNAL_URL || raw.GITLAB_URL || undefined
return {
enabled: raw.USE_GITLAB,
token: raw.GITLAB_TOKEN,
url: raw.GITLAB_URL,
internalUrl: raw.GITLAB_INTERNAL_URL,
mirrorTokenExpirationDays: raw.GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS,
mirrorTokenRotationThresholdDays: raw.GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS,
projectRootDir: raw.PROJECTS_ROOT_DIR,
internalOrPublicUrl: urlBase,
probeUrl: urlBase ? new URL('/-/health', urlBase).toString() : undefined,
}
})

export const gitlabConfigFactory = registerAs('gitlab', () => gitlabFeatureSchema.parse(process.env))
37 changes: 37 additions & 0 deletions apps/server-nestjs/src/config/harbor.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { HarborConfig } from '../modules/registry/harbor.module-definition'
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const harborFeatureSchema = z.object({
USE_HARBOR: flag(truthySchema.default('true')),
HARBOR_URL: nonEmpty(z.string().url()),
HARBOR_INTERNAL_URL: nonEmpty(z.string().url()),
HARBOR_ADMIN: z.string().min(1, 'HARBOR_ADMIN is required'),
HARBOR_ADMIN_PASSWORD: z.string().min(1, 'HARBOR_ADMIN_PASSWORD is required'),
HARBOR_RULE_TEMPLATE: z.string().min(1).optional(),
HARBOR_RULE_COUNT: z.string().optional(),
HARBOR_RETENTION_CRON: z.string().default('0 22 2 * * *'),
HARBOR_ROBOT_ROTATION_THRESHOLD_DAYS: z.coerce.number().int().positive().default(90),
HARBOR_PROJECT_SLUG_CACHE_TTL_MS: z.coerce.number().int().positive().default(300_000),
}).transform((raw) => {
const urlBase = raw.HARBOR_INTERNAL_URL || raw.HARBOR_URL || undefined
return {
enabled: raw.USE_HARBOR,
url: raw.HARBOR_URL,
internalUrl: raw.HARBOR_INTERNAL_URL,
admin: raw.HARBOR_ADMIN,
adminPassword: raw.HARBOR_ADMIN_PASSWORD,
ruleTemplate: raw.HARBOR_RULE_TEMPLATE,
ruleCount: raw.HARBOR_RULE_COUNT,
retentionCron: raw.HARBOR_RETENTION_CRON,
robotRotationThresholdDays: raw.HARBOR_ROBOT_ROTATION_THRESHOLD_DAYS,
projectSlugCacheTtlMs: raw.HARBOR_PROJECT_SLUG_CACHE_TTL_MS,
internalOrPublicUrl: urlBase,
probeUrl: urlBase ? new URL('/api/v2.0/health', urlBase).toString() : undefined,
}
})

export type HarborAppConfig = HarborConfig

export const harborConfigFactory = registerAs('harbor', () => harborFeatureSchema.parse(process.env))
55 changes: 55 additions & 0 deletions apps/server-nestjs/src/config/keycloak.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const keycloakRawSchema = z.object({
USE_KEYCLOAK: flag(truthySchema.default('true')),
KEYCLOAK_PROTOCOL: z.string().default('https'),
KEYCLOAK_DOMAIN: nonEmpty(z.string()),
KEYCLOAK_PUBLIC_PROTOCOL: z.string().default('https'),
KEYCLOAK_PUBLIC_DOMAIN: nonEmpty(z.string()),
KEYCLOAK_REALM: nonEmpty(z.string()),
KEYCLOAK_CLIENT_ID: nonEmpty(z.string()),
KEYCLOAK_CLIENT_SECRET: nonEmpty(z.string()),
KEYCLOAK_ADMIN: nonEmpty(z.string()),
KEYCLOAK_ADMIN_PASSWORD: nonEmpty(z.string()),
KEYCLOAK_ADMIN_CLIENT_ID: z.string().default('admin-cli'),
KEYCLOAK_REDIRECT_URI: nonEmpty(z.string().url()),
KEYCLOAK_JWKS_CACHE_TTL_MS: z.coerce.number().int().positive().default(300_000),
KEYCLOAK_JWKS_TIMEOUT_MS: z.coerce.number().int().positive().default(5_000),
KEYCLOAK_OPENID_CONFIGURATION_CACHE_TTL_MS: z.coerce.number().int().positive().default(300_000),
ADMIN_KC_USER_ID: z.preprocess(
value => (typeof value === 'string' ? value.split(',').map(part => part.trim()).filter(Boolean) : value),
z.array(z.string()).default([]),
),
})

function mapKeycloakConfig(raw: z.infer<typeof keycloakRawSchema>) {
const keycloakUrl = `${raw.KEYCLOAK_PROTOCOL}://${raw.KEYCLOAK_DOMAIN}`
const keycloakRealmUrl = `${keycloakUrl}/realms/${raw.KEYCLOAK_REALM}`
return {
enabled: raw.USE_KEYCLOAK,
protocol: raw.KEYCLOAK_PROTOCOL,
domain: raw.KEYCLOAK_DOMAIN,
publicProtocol: raw.KEYCLOAK_PUBLIC_PROTOCOL,
publicDomain: raw.KEYCLOAK_PUBLIC_DOMAIN,
realm: raw.KEYCLOAK_REALM,
clientId: raw.KEYCLOAK_CLIENT_ID,
clientSecret: raw.KEYCLOAK_CLIENT_SECRET,
admin: raw.KEYCLOAK_ADMIN,
adminPassword: raw.KEYCLOAK_ADMIN_PASSWORD,
adminClientId: raw.KEYCLOAK_ADMIN_CLIENT_ID,
redirectUri: raw.KEYCLOAK_REDIRECT_URI,
jwksCacheTtlMs: raw.KEYCLOAK_JWKS_CACHE_TTL_MS,
jwksTimeoutMs: raw.KEYCLOAK_JWKS_TIMEOUT_MS,
openidConfigurationCacheTtlMs: raw.KEYCLOAK_OPENID_CONFIGURATION_CACHE_TTL_MS,
adminKcUserId: raw.ADMIN_KC_USER_ID,
url: keycloakUrl,
realmUrl: keycloakRealmUrl,
openidConfigurationUrl: `${keycloakRealmUrl}/.well-known/openid-configuration`,
}
}

const keycloakConfigSchema = keycloakRawSchema.transform(mapKeycloakConfig)

export const keycloakConfigFactory = registerAs('keycloak', () => keycloakConfigSchema.parse(process.env))
26 changes: 26 additions & 0 deletions apps/server-nestjs/src/config/nexus.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const nexusFeatureSchema = z.object({
USE_NEXUS: flag(truthySchema.default('true')),
NEXUS_URL: nonEmpty(z.string().url()),
NEXUS_INTERNAL_URL: nonEmpty(z.string().url()),
NEXUS_ADMIN: z.string().min(1, 'NEXUS_ADMIN is required'),
NEXUS_ADMIN_PASSWORD: z.string().min(1, 'NEXUS_ADMIN_PASSWORD is required'),
NEXUS__SECRET_EXPOSE_INTERNAL_URL: flag(truthySchema.default('false')),
}).transform((raw) => {
const urlBase = raw.NEXUS_INTERNAL_URL || raw.NEXUS_URL || undefined
return {
enabled: raw.USE_NEXUS,
url: raw.NEXUS_URL,
internalUrl: raw.NEXUS_INTERNAL_URL,
admin: raw.NEXUS_ADMIN,
adminPassword: raw.NEXUS_ADMIN_PASSWORD,
secretExposeInternalUrl: raw.NEXUS__SECRET_EXPOSE_INTERNAL_URL,
internalOrPublicUrl: urlBase,
probeUrl: urlBase ? new URL('/service/rest/v1/status', urlBase).toString() : undefined,
}
})

export const nexusConfigFactory = registerAs('nexus', () => nexusFeatureSchema.parse(process.env))
23 changes: 23 additions & 0 deletions apps/server-nestjs/src/config/opencds.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { registerAs } from '@nestjs/config'
import z from 'zod'
import { flag, nonEmpty, truthySchema } from './config.utils'

const opencdsFeatureSchema = z.object({
USE_OPENCDS: flag(truthySchema.default('true')),
OPENCDS_URL: nonEmpty(z.string().url()),
OPENCDS_INTERNAL_URL: nonEmpty(z.string().url()),
OPENCDS_API_TOKEN: nonEmpty(z.string()),
OPENCDS_API_TLS_REJECT_UNAUTHORIZED: flag(truthySchema.default('false')),
}).transform((raw) => {
const probeBase = raw.OPENCDS_INTERNAL_URL || raw.OPENCDS_URL
return {
enabled: raw.USE_OPENCDS,
url: raw.OPENCDS_URL,
internalUrl: raw.OPENCDS_INTERNAL_URL,
probeUrl: probeBase ? new URL('/api/v1/health', probeBase).toString() : undefined,
apiToken: raw.OPENCDS_API_TOKEN,
apiTlsRejectUnauthorized: raw.OPENCDS_API_TLS_REJECT_UNAUTHORIZED,
}
})

export const openCdsConfigFactory = registerAs('opencds', () => opencdsFeatureSchema.parse(process.env))
Loading
Loading