diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2a168531 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## v0.1.1 + +- Ajout d'une image Docker de production "ext" (Next.js `next start`) lisant les URLs d'API via variables d'environnement, évitant toute référence à `localhost`. +- Remplacement d'URLs codées en dur dans l'écran de connexion email par l'utilisation de `FrontendVariables` et des `NEXT_PUBLIC_*`. +- Documentation `docs/ext.md` ajoutée (build, run, push, variables supportées). + +## v0.1.2 + +- LoginCallback (`index.tsx`) ajusté: suppression de la redirection spéciale `local.4nkweb.com` au profit d'un flux standard basé sur variables d'environnement. diff --git a/Dockerfile b/Dockerfile index 87f6f126..9af6dd44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM node:19-alpine AS deps WORKDIR /leCoffre-front COPY package.json ./ +COPY package-lock.json ./ RUN apk update && apk add --no-cache openssh-client git @@ -10,7 +11,7 @@ RUN apk update && apk add --no-cache openssh-client git RUN --mount=type=ssh \ mkdir -p /root/.ssh && \ ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts && \ - npm install --frozen-lockfile + npm install --no-audit --no-fund # Configuration pour le développement FROM node:19-alpine AS development @@ -26,4 +27,71 @@ RUN adduser -D lecoffreuser --uid 10000 && chown -R lecoffreuser . USER lecoffreuser CMD ["npm", "run", "dev"] -EXPOSE 3000 \ No newline at end of file +EXPOSE 3000 + +# --- Build de production +FROM node:19-alpine AS builder +WORKDIR /leCoffre-front + +COPY --from=deps /leCoffre-front/node_modules ./node_modules +COPY --from=deps /leCoffre-front/package.json ./package.json +COPY . . + +# Arguments/variables d'environnement publics pour le build Next +ARG NEXT_PUBLIC_BACK_API_PROTOCOL +ARG NEXT_PUBLIC_BACK_API_HOST +ARG NEXT_PUBLIC_BACK_API_PORT +ARG NEXT_PUBLIC_BACK_API_ROOT_URL +ARG NEXT_PUBLIC_BACK_API_VERSION +ARG NEXT_PUBLIC_FRONT_APP_HOST +ARG NEXT_PUBLIC_FRONT_APP_PORT +ARG NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT +ARG NEXT_PUBLIC_IDNOT_CLIENT_ID +ARG NEXT_PUBLIC_IDNOT_BASE_URL +ARG NEXT_PUBLIC_DOCAPOSTE_API_URL +ARG NEXT_PUBLIC_HOTJAR_SITE_ID +ARG NEXT_PUBLIC_HOTJAR_VERSION +ARG NEXT_PUBLIC_4NK_URL +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_DEFAULT_VALIDATOR_ID +ARG NEXT_PUBLIC_DEFAULT_STORAGE_URLS + +ENV NEXT_PUBLIC_BACK_API_PROTOCOL=${NEXT_PUBLIC_BACK_API_PROTOCOL} \ + NEXT_PUBLIC_BACK_API_HOST=${NEXT_PUBLIC_BACK_API_HOST} \ + NEXT_PUBLIC_BACK_API_PORT=${NEXT_PUBLIC_BACK_API_PORT} \ + NEXT_PUBLIC_BACK_API_ROOT_URL=${NEXT_PUBLIC_BACK_API_ROOT_URL} \ + NEXT_PUBLIC_BACK_API_VERSION=${NEXT_PUBLIC_BACK_API_VERSION} \ + NEXT_PUBLIC_FRONT_APP_HOST=${NEXT_PUBLIC_FRONT_APP_HOST} \ + NEXT_PUBLIC_FRONT_APP_PORT=${NEXT_PUBLIC_FRONT_APP_PORT} \ + NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT=${NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT} \ + NEXT_PUBLIC_IDNOT_CLIENT_ID=${NEXT_PUBLIC_IDNOT_CLIENT_ID} \ + NEXT_PUBLIC_IDNOT_BASE_URL=${NEXT_PUBLIC_IDNOT_BASE_URL} \ + NEXT_PUBLIC_DOCAPOSTE_API_URL=${NEXT_PUBLIC_DOCAPOSTE_API_URL} \ + NEXT_PUBLIC_HOTJAR_SITE_ID=${NEXT_PUBLIC_HOTJAR_SITE_ID} \ + NEXT_PUBLIC_HOTJAR_VERSION=${NEXT_PUBLIC_HOTJAR_VERSION} \ + NEXT_PUBLIC_4NK_URL=${NEXT_PUBLIC_4NK_URL} \ + NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} \ + NEXT_PUBLIC_DEFAULT_VALIDATOR_ID=${NEXT_PUBLIC_DEFAULT_VALIDATOR_ID} \ + NEXT_PUBLIC_DEFAULT_STORAGE_URLS=${NEXT_PUBLIC_DEFAULT_STORAGE_URLS} + +RUN npm run build + +# --- Image d'exécution "ext" +FROM node:19-alpine AS ext +WORKDIR /leCoffre-front + +ENV NODE_ENV=production \ + PORT=3000 + +COPY --from=builder /leCoffre-front/.next ./.next +COPY --from=builder /leCoffre-front/public ./public +COPY --from=deps /leCoffre-front/node_modules ./node_modules +COPY --from=builder /leCoffre-front/package.json ./package.json +COPY --from=builder /leCoffre-front/next.config.js ./next.config.js + +# Création de l'utilisateur non-root +RUN adduser -D lecoffreuser --uid 10000 && chown -R lecoffreuser . +USER lecoffreuser + +EXPOSE 3000 +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/docs/ci.md b/docs/ci.md index 08705a3d..2830a3ba 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -5,7 +5,7 @@ Cette documentation décrit le pipeline CI/CD tel qu’il peut être déduit des ### Portée - **Build applicatif**: Next.js (Node 19-alpine) avec dépendance privée `le-coffre-resources` via SSH. -- **Image Docker**: construction multi-étapes, publication attendue vers un registre Scaleway Container Registry. +- **Image Docker**: construction multi-étapes, publication vers le registre Docker hébergé sur `git.4nkweb.com` (accès via clés SSH). - **Déploiement Kubernetes**: namespace `lecoffre`, intégration Vault Agent pour l’injection d’ENV, `ExternalSecret` pour le secret de pull Docker, `Ingress` TLS via cert-manager, ressources de `Deployment`/`Service`. ### Chaîne de build @@ -25,9 +25,8 @@ Cette documentation décrit le pipeline CI/CD tel qu’il peut être déduit des ### Image, registre et version -- **Image utilisée en cluster**: `rg.fr-par.scw.cloud/lecoffre/front:v0.1.9` (cf. `temp.yaml`). -- **Registre**: Scaleway Container Registry (région `fr-par`). -- **Tagging**: la version d’exemple observée est `v0.1.9`. L’origine du tag (automatique via CI, ou manuel) n’est pas dans le dépôt. +- **Registre**: Docker registry interne sur `git.4nkweb.com`. +- **Tagging**: contrôlé par la CI via le message de commit (préfixe `ci: docker_tag=`), sinon fallback `dev-test`. La branche peut être utilisée comme tag par défaut selon la CI. ### Déploiement Kubernetes (extrait de `temp.yaml`) @@ -57,11 +56,11 @@ Cette documentation décrit le pipeline CI/CD tel qu’il peut être déduit des - `npm run build` (désactive la télémétrie et le lint bloquant). 4. **Construction de l’image** - - `docker build` avec BuildKit et forward d’agent SSH. - - Taggage semver (ex. `v0.1.9`) et éventuellement `latest`/environnement (non constaté ici). + - Réalisée par la CI (BuildKit + forward d’agent SSH) après un `git push` sur `git.4nkweb.com`. + - Taggage déterminé par le message de commit et/ou la branche. 5. **Push au registre** - - `docker push rg.fr-par.scw.cloud/lecoffre/front:`. + - Réalisé par la CI vers le registre `git.4nkweb.com`. 6. **Déploiement Kubernetes** - Application des manifestes (ou rendu Helm) dans le namespace `lecoffre`. diff --git a/docs/ext.md b/docs/ext.md new file mode 100644 index 00000000..8e4f5d6b --- /dev/null +++ b/docs/ext.md @@ -0,0 +1,58 @@ +### Image Docker "ext" (Next.js) – variables d'environnement et publication + +Cette image exécute l'app Next.js en mode production via `next start` et lit la configuration via les variables d'environnement exposées (préfixe `NEXT_PUBLIC_`). L'objectif est d'éviter toute dépendance à `localhost` dans les appels API : les URLs sont construites dynamiquement côté front à partir de ces variables. + +#### Variables d'environnement supportées + +- **NEXT_PUBLIC_BACK_API_PROTOCOL**: protocole de l'API (ex: `https://`) +- **NEXT_PUBLIC_BACK_API_HOST**: hôte de l'API (ex: `api.example.com`) +- **NEXT_PUBLIC_BACK_API_PORT**: port de l'API (ex: `443`) +- **NEXT_PUBLIC_BACK_API_ROOT_URL**: racine (ex: `/` ou `/api`) +- **NEXT_PUBLIC_BACK_API_VERSION**: version (ex: `v1`) +- **NEXT_PUBLIC_FRONT_APP_HOST**: base publique du front (ex: `https://app.example.com`) +- **NEXT_PUBLIC_FRONT_APP_PORT**: port front si nécessaire (ex: `443`) +- **NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT** +- **NEXT_PUBLIC_IDNOT_CLIENT_ID** +- **NEXT_PUBLIC_IDNOT_BASE_URL** +- **NEXT_PUBLIC_DOCAPOSTE_API_URL** +- **NEXT_PUBLIC_HOTJAR_SITE_ID** +- **NEXT_PUBLIC_HOTJAR_VERSION** +- **NEXT_PUBLIC_4NK_URL** +- **NEXT_PUBLIC_API_URL** +- **NEXT_PUBLIC_DEFAULT_VALIDATOR_ID** +- **NEXT_PUBLIC_DEFAULT_STORAGE_URLS** (liste séparée par des virgules) + +Notes: + +- Le front initialise ses variables via `next.config.js` et `_app.tsx`, ce qui alimente `FrontendVariables`. Les appels API utilisent ces valeurs et n'emploient pas `localhost`. +- Les valeurs doivent être passées au conteneur au runtime (`docker run -e ...` ou manifest K8s via `env:`/`secretRef`). + +#### Construction de l'image (cible "ext") + +Prérequis: Docker BuildKit activé et agent SSH opérationnel pour cloner `le-coffre-resources` depuis `git.4nkweb.com`. + +1. `cd /home/debian/lecoffre-front` +2. `export DOCKER_BUILDKIT=1` +3. `docker build --target ext --ssh default -t lecoffre/front:ext-0.1.2 -f /home/debian/lecoffre-front/Dockerfile /home/debian/lecoffre-front` + +#### Exécution locale (validation) + +Exemple minimal (adapter les valeurs): + +```bash +docker run --rm -p 3000:3000 \ + -e NEXT_PUBLIC_BACK_API_PROTOCOL=https:// \ + -e NEXT_PUBLIC_BACK_API_HOST=api.example.com \ + -e NEXT_PUBLIC_BACK_API_PORT=443 \ + -e NEXT_PUBLIC_BACK_API_ROOT_URL=/ \ + -e NEXT_PUBLIC_BACK_API_VERSION=v1 \ + -e NEXT_PUBLIC_FRONT_APP_HOST=https://app.example.com \ + -e NEXT_PUBLIC_4NK_URL=https://app.example.com \ + lecoffre/front:ext-0.1.2 +``` + +#### Publication via CI (git.4nkweb.com) + +- Le push d'image est effectué par la CI de `git.4nkweb.com` suite à un `git push`. +- Définir le tag Docker dans le message de commit: `ci: docker_tag=ext-0.1.2` (fallback CI: `dev-test`). +- La branche peut être utilisée par la CI comme tag en l’absence d’override. diff --git a/package.json b/package.json index 77160f8d..a0c0c918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lecoffre-front", - "version": "0.1.0", + "version": "0.1.2", "private": true, "scripts": { "dev": "next dev", diff --git a/src/front/Api/Auth/IdNot/index.ts b/src/front/Api/Auth/IdNot/index.ts index 94191b27..a23dfd42 100644 --- a/src/front/Api/Auth/IdNot/index.ts +++ b/src/front/Api/Auth/IdNot/index.ts @@ -42,9 +42,9 @@ export default class Auth extends BaseApiService { public async idNotAuth(autorizationCode: string | string[]): Promise<{ idNotUser: any; authToken: string }> { const variables = FrontendVariables.getInstance(); - + const baseBackUrl = `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}:${variables.BACK_API_PORT}${variables.BACK_API_ROOT_URL}${variables.BACK_API_VERSION}`; - + const url = new URL(`${baseBackUrl}/api/v1/idnot/auth/${autorizationCode}`); try { return await this.postRequest<{ idNotUser: any; authToken: string }>(url); @@ -57,7 +57,7 @@ export default class Auth extends BaseApiService { public async getIdNotUser(): Promise<{ success: boolean; data: any }> { const variables = FrontendVariables.getInstance(); const baseBackUrl = `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}:${variables.BACK_API_PORT}${variables.BACK_API_ROOT_URL}${variables.BACK_API_VERSION}`; - + const url = new URL(`${baseBackUrl}/api/v1/idnot/user`); try { return await this.getRequest(url); @@ -71,7 +71,7 @@ export default class Auth extends BaseApiService { public async getIdNotOfficeForUser(userId: string): Promise { const variables = FrontendVariables.getInstance(); const baseBackUrl = `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}:${variables.BACK_API_PORT}${variables.BACK_API_ROOT_URL}${variables.BACK_API_VERSION}`; - + const url = new URL(`${baseBackUrl}/api/v1/idnot/user/rattachements`); url.searchParams.set('idNot', userId); try { @@ -86,7 +86,7 @@ export default class Auth extends BaseApiService { public async getIdNotUserForOffice(officeId: string): Promise { const variables = FrontendVariables.getInstance(); const baseBackUrl = `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}:${variables.BACK_API_PORT}${variables.BACK_API_ROOT_URL}${variables.BACK_API_VERSION}`; - + const url = new URL(`${baseBackUrl}/api/v1/idnot/office/rattachements`); url.searchParams.set('idNot', officeId); try { diff --git a/src/front/Components/Layouts/Login/StepEmail/index.tsx b/src/front/Components/Layouts/Login/StepEmail/index.tsx index f9b904ee..3c0fea87 100644 --- a/src/front/Components/Layouts/Login/StepEmail/index.tsx +++ b/src/front/Components/Layouts/Login/StepEmail/index.tsx @@ -42,17 +42,11 @@ export default function StepEmail(props: IProps) { const router = useRouter(); const error = router.query["error"]; const redirectUserOnConnection = useCallback(() => { - /* TODO: review const variables = FrontendVariables.getInstance(); + const redirectUri = `${variables.FRONT_APP_HOST}/authorized-client`; + const authorizeBase = `${variables.IDNOT_BASE_URL}${variables.IDNOT_AUTHORIZE_ENDPOINT}`; router.push( - `${variables.IDNOT_BASE_URL + variables.IDNOT_AUTHORIZE_ENDPOINT}?client_id=${variables.IDNOT_CLIENT_ID}&redirect_uri=${ - variables.FRONT_APP_HOST - }/authorized-client&scope=openid,profile&response_type=code`, - ); - */ - const redirectUri = 'http://local.4nkweb.com:3000/authorized-client'; - router.push( - `https://qual-connexion.idnot.fr/user/IdPOAuth2/authorize/idnot_idp_v1?client_id=B3CE56353EDB15A9&redirect_uri=${redirectUri}&scope=openid,profile&response_type=code`, + `${authorizeBase}?client_id=${variables.IDNOT_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=openid,profile&response_type=code`, ); }, [router]); diff --git a/src/front/Components/Layouts/LoginCallback/index.tsx b/src/front/Components/Layouts/LoginCallback/index.tsx index 2a22401f..ef63cb9e 100644 --- a/src/front/Components/Layouts/LoginCallback/index.tsx +++ b/src/front/Components/Layouts/LoginCallback/index.tsx @@ -44,7 +44,7 @@ export default function LoginCallBack() { resolve(UserStore.instance.getUser()); return; } - + // Poll for userInfo every 100ms const checkInterval = setInterval(() => { if (UserStore.instance.getUser()) { @@ -52,7 +52,7 @@ export default function LoginCallBack() { resolve(UserStore.instance.getUser()); } }, 100); - + // Timeout after 60 seconds setTimeout(() => { clearInterval(checkInterval); @@ -66,12 +66,11 @@ export default function LoginCallBack() { UserStore.instance.disconnect(); // TODO: review - // HACK: If start with http://local.lecoffreio.4nkweb:3000/authorized-client + // HACK: If start with http://local.lecoffreio.4nkweb:3000/authorized-client // Replace with http://localhost:3000/authorized-client - if (window.location.href.startsWith('http://local.4nkweb.com')) { - window.location.href = window.location.href.replace('http://local.4nkweb.com:3000/authorized-client', 'http://localhost:3000/authorized-client'); - return; - } + // if (window.location.href.startsWith('http://local.4nkweb.com')) { + // return; + // } const code = router.query["code"]; if (code) { @@ -83,15 +82,15 @@ export default function LoginCallBack() { } const user: any = await Auth.getInstance().idNotAuth(code as string); - + // Extract both user data and auth token from the response const { idNotUser, authToken } = user; - + if (!authToken) { console.error('[LoginCallback] No authToken received from backend'); return router.push(Module.getInstance().get().modules.pages.Login.props.path + "?error=1"); } - + // Store the auth token for API authentication // TODO The authToken is just a uuid for now, it's very broken CookieService.getInstance().setCookie("leCoffreAccessToken", authToken); @@ -109,7 +108,7 @@ export default function LoginCallBack() { CookieService.getInstance().setCookie("leCoffreUserInfo", JSON.stringify(userInfoResponse.data)); setIsAuthModalOpen(true); console.log('[LoginCallback] authToken stored successfully'); - + return; } catch (e: any) { if (e.http_status === 401 && e.message === "Email not found") { @@ -217,7 +216,7 @@ export default function LoginCallBack() { // Office may not have a process too let collaboratorProcess: { processId: string, processData: { [key: string]: any } } | null = null; let officeProcess: { processId: string, processData: { [key: string]: any } } | null = null; - + // Initialize collaborator process try { // Wait for pairing ID to be available before proceeding @@ -225,7 +224,7 @@ export default function LoginCallBack() { console.log('[LoginCallback] Pairing ID obtained:', pairingId); // Check if we are part of the right collaborator process const myCollaboratorProcessesData = await MessageBus.getInstance().getProcessesDecoded((processId: string, values: { [key: string]: any }) => { - return values['utype'] === 'collaborator' + return values['utype'] === 'collaborator' && values['idNot'] === userInfo.idNot && values['isDeleted'] === 'false'; }); @@ -261,7 +260,7 @@ export default function LoginCallBack() { console.log('[LoginCallback] Pairing ID obtained:', pairingId); // Now we need to check for office process const myOfficeProcessesData = await MessageBus.getInstance().getProcessesDecoded((processId: string, values: { [key: string]: any }) => { - return values['utype'] === 'office' + return values['utype'] === 'office' && values['idNot'] === userInfo.office.idNot && values['isDeleted'] === 'false'; }); diff --git a/tests/ext.md b/tests/ext.md new file mode 100644 index 00000000..30839f2d --- /dev/null +++ b/tests/ext.md @@ -0,0 +1,16 @@ +### Tests image "ext" + +Objectif: vérifier que l'image démarre et que les URLs d'API proviennent des variables d'environnement. + +Plan de test manuel: + +1. Construire l'image `--target ext` avec BuildKit et SSH forward. +2. Démarrer un conteneur en exposant le port 3000 et en définissant des `NEXT_PUBLIC_*` non-localhost. +3. Vérifier que le service répond en HTTP 200 sur `/`. +4. Arrêter et supprimer le conteneur. + +Critères de réussite: + +- Le build aboutit sans erreur et produit une image étiquetée `ext-`. +- Le conteneur écoute sur 3000 et renvoie un code HTTP 200. +- Aucune référence à `localhost` n'est nécessaire pour les appels API (validation via configuration d'ENV uniquement).