Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## May 7, 2025

- **Feature** Add Helm chart and templates for MET Web [🎟️ DESENG-811](https://citz-gdx.atlassian.net/browse/DESENG-811)
- Created a helm chart for the MET Web deployment.
- Migrated existing resources to Helm templates.
- Removed several unused ConfigMap values
- Removed JWT/OIDC related values in favor of requesting the data from the MET API (single source of truth).
- Created an endpoint at `/api/oidc_config` to return the OIDC configuration for the MET API.
- Modified the Docker entrypoint to generate a config script from REACT*APP* environment variables at startup.
- The config script is then served alongside the app at runtime.
- **Bugfix** Updated existing templates to set minReplicas and maxReplicas for the HPA (matching met-web) rather than trying to configure it on the Deployment, which has no effect.

## May 5, 2025

- **Feature** Add Helm chart and templates for MET API [🎟️ DESENG-811](https://citz-gdx.atlassian.net/browse/DESENG-811)
Expand Down
2 changes: 2 additions & 0 deletions met-api/src/met_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from flask import Blueprint

from .apihelper import Api
from .oidc_config import API as OIDC_CONFIG_API
from .comment import API as COMMENT_API
from .contact import API as CONTACT_API
from .document import API as DOCUMENT_API
Expand Down Expand Up @@ -77,6 +78,7 @@

# HANDLER = ExceptionHandler(API)

API.add_namespace(OIDC_CONFIG_API, path='/oidc_config')
API.add_namespace(ENGAGEMENT_API)
API.add_namespace(USER_API)
API.add_namespace(DOCUMENT_API)
Expand Down
57 changes: 57 additions & 0 deletions met-api/src/met_api/resources/oidc_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
A simple endpoint to serve the OpenID Connect configuration for the web application.
"""

from flask import jsonify
from flask_cors import cross_origin
from flask_restx import Namespace, Resource
from met_api.utils.roles import Role
from met_api.utils.util import allowedorigins, cors_preflight
from met_api.config import Config

API = Namespace(
'oidc_config',
description='Endpoints for fetching OpenID Connect configuration',
)

jwt_config = Config().JWT
keycloak_config = Config().KC

PUBLIC_CONFIG = {
# Do not overpopulate this dict with sensitive information
# as it will be intentionally exposed to the public
'KEYCLOAK_URL': keycloak_config['BASE_URL'],
'KEYCLOAK_REALM': keycloak_config['REALMNAME'],
'KEYCLOAK_CLIENT': jwt_config['AUDIENCE'],
'KEYCLOAK_ADMIN_ROLE': Role.SUPER_ADMIN.value,
}

@cors_preflight('GET,OPTIONS')
@API.route('/')
class OIDCConfigAsJson(Resource):
"""Resource for OpenID Connect configuration."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get():
"""Fetch OpenID Connect configuration."""
return jsonify(PUBLIC_CONFIG), 200

@cors_preflight('GET,OPTIONS')
@API.route('/config.js')
class OIDCConfigAsJs(Resource):
"""Resource for OpenID Connect configuration in JavaScript format."""

js_prefix = "window._env_ = window._env_ || {};\n"
js_template = "window._env_.{VAR_NAME} = '{VAR_VALUE}';\n"

@staticmethod
@cross_origin(origins=allowedorigins())
def get():
"""Fetch OpenID Connect configuration."""
js_content = OIDCConfigAsJs.js_prefix
for key, value in PUBLIC_CONFIG.items():
js_content += OIDCConfigAsJs.js_template.format(
VAR_NAME='REACT_APP_'+key, VAR_VALUE=value
)
return js_content, 200, {'Content-Type': 'application/javascript'}
14 changes: 10 additions & 4 deletions met-web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# "build-stage", based on Node.js, to build and compile the frontend
# pull official base image
FROM node:20-alpine as build-stage
FROM node:20-alpine AS build-stage

# set working directory
WORKDIR /app
Expand Down Expand Up @@ -31,7 +31,7 @@ COPY . ./
RUN npm run build

# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM nginx:1.17 as production-stage
FROM nginx:1.17 AS production-stage
RUN mkdir /app

# RUN touch /var/run/nginx.pid && \
Expand All @@ -43,14 +43,20 @@ RUN usermod -a -G root $user
COPY --from=build-stage /app/build /usr/share/nginx/html
COPY /nginx/*.conf /etc/nginx/
COPY /nginx/docker-entrypoint.sh /
COPY /nginx/config.js.template /usr/share/nginx/html/config.js.template

# Set ownership and permissions
# Set scripts as executable (make files and python files do not have to be marked)
# Make /etc/passwd writable for the root group so an entry can be created for an OpenShift assigned user account.
RUN chown -R $user:root /etc/nginx/ \
RUN chown -R "$user:root" /etc/nginx/ \
&& chmod -R ug+rw /etc/nginx/ \
&& chmod ug+x docker-entrypoint.sh \
&& chmod g+rw /etc/passwd
&& chmod g+rw /etc/passwd \
&& chown -R "$user:root" /usr/share/nginx/html/ \
&& chmod g+rw /usr/share/nginx/html/ \
&& chown -R "$user:root" /usr/share/nginx/html/config.js.template \
&& chmod g+rw /usr/share/nginx/html/config.js.template

USER $user

ENTRYPOINT ["sh", "/docker-entrypoint.sh"]
2 changes: 2 additions & 0 deletions met-web/nginx/config.js.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window._env_ = window._env_ || {};
$REACT_APP_VARS_JS
21 changes: 21 additions & 0 deletions met-web/nginx/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,25 @@ else
echo "Root configuration file not found and environment template not found, using default."
fi
fi

# Generate the config.js file from the environment variables
REACT_APP_VARS_JS=$(
# For each environment variable
env |
# that starts with REACT_APP_,
grep -E '^REACT_APP_' |
# Create a string of the form "window._env_.REACT_APP_VAR_NAME' = 'REACT_APP_VAR_VALUE';"
sed -E "s/^(REACT_APP_[^=]+)=(.*)/window._env_.\1 = '\2';/" |
# Join the lines with a newline character,
awk '{printf "%s\n", $0}' |
# then remove the last newline character
sed 's/\n$//'
)
export REACT_APP_VARS_JS

# Create the config.js file from the template
# This file will be served by Nginx and will be used by the React app to
# set the environment variables at runtime.
envsubst < /usr/share/nginx/html/config.js.template > /usr/share/nginx/html/config.js

nginx -g 'daemon off;'
8 changes: 8 additions & 0 deletions met-web/nginx/nginx.dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ http {
error_log /dev/stdout info;
access_log /dev/stdout;

location = /config/config.js {
# Serve the processed config file
alias /usr/share/nginx/html/config.js;

# Set correct content type
add_header Content-Type application/javascript;
}

location / {
root /usr/share/nginx/html;
index index.html index.htm;
Expand Down
8 changes: 8 additions & 0 deletions met-web/nginx/nginx.prod.conf
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ http {
error_log /dev/stdout info;
access_log /dev/stdout;

location = /config/config.js {
# Serve the processed config file
alias /usr/share/nginx/html/config.js;

# Set correct content type
add_header Content-Type application/javascript;
}

location / {
root /usr/share/nginx/html;
index index.html index.htm;
Expand Down
8 changes: 8 additions & 0 deletions met-web/nginx/nginx.test.conf
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ http {
error_log /dev/stdout info;
access_log /dev/stdout;

location = /config/config.js {
# Serve the processed config file
alias /usr/share/nginx/html/config.js;

# Set correct content type
add_header Content-Type application/javascript;
}

location / {
root /usr/share/nginx/html;
index index.html index.htm;
Expand Down
5 changes: 5 additions & 0 deletions met-web/public/api/oidc_config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This file is a stub for the JWT configuration.
// In production, this is generated and served by the API.
// In development, values are provided by process.env.
// It is left here so as to avoid 404 errors in development.
window['_env_'] = window['_env_'] || {};
6 changes: 5 additions & 1 deletion met-web/public/config/config.js
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
window['_env_'] = {};
// This file is a stub for the environment variables.
// In production, this is generated and served by Nginx.
// In development, values are provided by process.env.
// It is left here so as to avoid 404 errors in development.
window['_env_'] = window['_env_'] || {};
3 changes: 3 additions & 0 deletions met-web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site of MET" />
<!-- Served from nginx -->
<script src="%PUBLIC_URL%/config/config.js"></script>
<!-- Served from API -->
<script src="%PUBLIC_URL%/api/oidc_config/config.js"></script>
<script src="/snowplow.js" charset="UTF-8"></script>
<link rel="icon" href="%PUBLIC_URL%/BCFavIcon.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
Expand Down
8 changes: 0 additions & 8 deletions met-web/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@ REACT_APP_KEYCLOAK_URL= # auth-server-url
REACT_APP_KEYCLOAK_REALM= # realm
REACT_APP_KEYCLOAK_CLIENT= # resource

# Form.io settings
REACT_APP_FORMIO_PROJECT_URL=
REACT_APP_FORM_ID=
REACT_APP_FORMIO_JWT_SECRET=
REACT_APP_USER_RESOURCE_FORM_ID=
REACT_APP_FORMIO_ANONYMOUS_USER="anonymous"
REACT_APP_FORMIO_ANONYMOUS_ID=

REACT_APP_PUBLIC_URL=http://localhost:3000

# The role needed to be considered a super admin (has modify access on all tenants/endpoints)
Expand Down
9 changes: 4 additions & 5 deletions met-web/src/components/auth/AuthKeycloakContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { createContext, useState, useEffect } from 'react';
import { useAppDispatch } from 'hooks';
import UserService from '../../services/userService';
import { _kc } from 'constants/tenantConstants';
const KeycloakData = _kc;
import { KeycloakClient } from 'constants/tenantConstants';

export interface AuthKeyCloakContextProps {
isAuthenticated: boolean;
Expand All @@ -24,14 +23,14 @@ export const AuthKeyCloakContextProvider = ({ children }: { children: JSX.Elemen
useEffect(() => {
const initAuth = async () => {
try {
const authenticated = await KeycloakData.init({
const authenticated = await KeycloakClient.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
checkLoginIframe: false,
});
setIsAuthenticated(authenticated); // Update authentication state
UserService.setKeycloakInstance(KeycloakData);
UserService.setKeycloakInstance(KeycloakClient);
UserService.setAuthData(dispatch);
} catch (error) {
console.error('Authentication initialization failed:', error);
Expand All @@ -48,7 +47,7 @@ export const AuthKeyCloakContextProvider = ({ children }: { children: JSX.Elemen
value={{
isAuthenticated,
isAuthenticating,
keycloakInstance: KeycloakData,
keycloakInstance: KeycloakClient,
}}
>
{!isAuthenticating && children}
Expand Down
50 changes: 2 additions & 48 deletions met-web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,19 @@ import { hasKey } from 'utils';
declare global {
interface Window {
_env_: {
REACT_APP_API_URL: string;
REACT_APP_PUBLIC_URL: string;
REACT_APP_REDASH_PUBLIC_URL: string;
REACT_APP_REDASH_COMMENTS_PUBLIC_URL: string;

// Analytics
REACT_APP_ANALYTICS_API_URL: string;

// Formio
REACT_APP_API_PROJECT_URL: string;
REACT_APP_FORM_ID: string;
REACT_APP_FORMIO_JWT_SECRET: string;
REACT_APP_USER_RESOURCE_FORM_ID: string;
REACT_APP_FORMIO_ANONYMOUS_USER: string;
REACT_APP_ANONYMOUS_ID: string;

// Keycloak
REACT_APP_KEYCLOAK_URL: string;
REACT_APP_KEYCLOAK_CLIENT: string;
REACT_APP_KEYCLOAK_REALM: string;
REACT_APP_KEYCLOAK_ADMIN_ROLE: string;

//tenant
REACT_APP_IS_SINGLE_TENANT_ENVIRONMENT: string;
REACT_APP_DEFAULT_TENANT: string;
REACT_APP_DEFAULT_LANGUAGE_ID: string;
[key: string]: string;
};
}
}

const getEnv = (key: string, defaultValue = '') => {
if (hasKey(window._env_, key)) {
return window._env_[key];
} else return process.env[key] || defaultValue;
return window._env_[key] ?? process.env[key] ?? defaultValue;
};

const API_URL = getEnv('REACT_APP_API_URL');
const PUBLIC_URL = getEnv('REACT_APP_PUBLIC_URL');
const REACT_APP_ANALYTICS_API_URL = getEnv('REACT_APP_ANALYTICS_API_URL');

// Formio Environment Variables
const FORMIO_PROJECT_URL = getEnv('REACT_APP_FORMIO_PROJECT_URL');
const FORMIO_API_URL = getEnv('REACT_APP_FORMIO_PROJECT_URL');
const FORMIO_FORM_ID = getEnv('REACT_APP_FORM_ID');
const FORMIO_JWT_SECRET = getEnv('REACT_APP_FORMIO_JWT_SECRET');
const FORMIO_USER_RESOURCE_FORM_ID = getEnv('REACT_APP_USER_RESOURCE_FORM_ID');
const FORMIO_ANONYMOUS_USER = getEnv('REACT_APP_FORMIO_ANONYMOUS_USER');
const FORMIO_ANONYMOUS_ID = getEnv('REACT_APP_FORMIO_ANONYMOUS_ID');

// Keycloak Environment Variables
const KC_URL = getEnv('REACT_APP_KEYCLOAK_URL');
const KC_CLIENT = getEnv('REACT_APP_KEYCLOAK_CLIENT');
Expand All @@ -66,16 +30,6 @@ export const AppConfig = {
apiUrl: API_URL,
analyticsApiUrl: REACT_APP_ANALYTICS_API_URL,
publicUrl: PUBLIC_URL,
formio: {
projectUrl: FORMIO_PROJECT_URL,
apiUrl: FORMIO_API_URL,
formId: FORMIO_FORM_ID,
anonymousId: FORMIO_ANONYMOUS_ID || '',
anonymousUser: FORMIO_ANONYMOUS_USER || 'anonymous',
userResourceFormId: FORMIO_USER_RESOURCE_FORM_ID,
// TODO: potentially sensitive information, should be stored somewhere else?
jwtSecret: FORMIO_JWT_SECRET || '',
},
keycloak: {
url: KC_URL || '',
clientId: KC_CLIENT || '',
Expand Down
6 changes: 3 additions & 3 deletions met-web/src/constants/tenantConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { AppConfig } from 'config';
import Keycloak from 'keycloak-js';
import { ITenantDetail } from './types';

//TODO get from api
// Keycloak Environment Variables
export const tenantDetail: ITenantDetail = {
realm: AppConfig.keycloak.realm,
url: AppConfig.keycloak.url,
clientId: AppConfig.keycloak.clientId,
};

// eslint-disable-next-line
export const _kc: Keycloak.default = new (Keycloak as any)(tenantDetail);
// Create the Keycloak instance for the tenant
export const KeycloakClient: Keycloak = new Keycloak(tenantDetail);
Loading