├── .devcontainer ├── Dockerfile ├── devcontainer.json └── library-scripts │ ├── common-debian.sh │ └── docker-in-docker-debian.sh ├── .github └── workflows │ ├── deploy.yaml │ ├── destroy.yml │ └── pull-request.yaml ├── .gitignore ├── CODE-OF-CONDUCT.md ├── COMPLIANCE.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── IAM_policies │ ├── App_Deployment_IAM_Policy.json │ └── Registry_Deployment_IAM_Policy.json └── images │ ├── browserstack-logo-white-small.png │ ├── ports.png │ ├── workflows.drawio │ └── workflows.png └── src ├── api ├── .dockerignore ├── Dockerfile ├── app.js ├── bin │ └── www ├── package-lock.json ├── package.json └── routes │ └── greeting.js ├── terraform ├── app │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf └── ecr │ ├── main.tf │ ├── variables.tf │ └── versions.tf └── web ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo.svg ├── logo192.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── components ├── GreetingContext.js ├── GreetingList.js ├── GreetingProvider.js ├── GreetingSelector.js ├── Header.css └── Header.js ├── config.js ├── index.css ├── index.js └── reportWebVitals.js /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Note: You can use any Debian/Ubuntu based image you want. 2 | # Ref: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/debian 3 | ARG VARIANT=bullseye 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} 5 | 6 | # [Option] Install zsh 7 | ARG INSTALL_ZSH="true" 8 | # [Option] Upgrade OS packages to their latest versions 9 | ARG UPGRADE_PACKAGES="false" 10 | # [Option] Enable non-root Docker access in container 11 | ARG ENABLE_NONROOT_DOCKER="true" 12 | # [Option] Use the OSS Moby Engine instead of the licensed Docker Engine 13 | ARG USE_MOBY="true" 14 | # [Option] Engine/CLI Version 15 | ARG DOCKER_VERSION="latest" 16 | 17 | # Enable new "BUILDKIT" mode for Docker CLI 18 | ENV DOCKER_BUILDKIT=1 19 | 20 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your 21 | # own dependencies. A user of "automatic" attempts to reuse an user ID if one already exists. 22 | ARG USERNAME=automatic 23 | ARG USER_UID=1000 24 | ARG USER_GID=$USER_UID 25 | COPY library-scripts/*.sh /tmp/library-scripts/ 26 | RUN apt-get update \ 27 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 28 | # Use Docker script from script library to set things up 29 | && /bin/bash /tmp/library-scripts/docker-in-docker-debian.sh "${ENABLE_NONROOT_DOCKER}" "${USERNAME}" "${USE_MOBY}" "${DOCKER_VERSION}" \ 30 | # Clean up 31 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts/ 32 | 33 | # Install build-essential 34 | RUN apt-get update && apt-get install -y build-essential 35 | 36 | # Install AWS CLI - TODO: hash verification needed 37 | RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" \ 38 | && unzip awscliv2.zip && ./aws/install 39 | 40 | VOLUME [ "/var/lib/docker" ] 41 | 42 | # Setting the ENTRYPOINT to docker-init.sh will start up the Docker Engine 43 | # inside the container "overrideCommand": false is set in devcontainer.json. 44 | # The script will also execute CMD if you need to alter startup behaviors. 45 | ENTRYPOINT [ "/usr/local/share/docker-init.sh" ] 46 | CMD [ "sleep", "infinity" ] 47 | 48 | # [Optional] Uncomment this section to install additional OS packages. 49 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 50 | # && apt-get -y install --no-install-recommends 51 | 52 | # Define environment variables for local development 53 | ENV REACT_APP_API_URL="" 54 | ENV PROJECT=ssp 55 | ENV PROFILE=ssp-dev 56 | ENV DB_SERVER=mongodb 57 | ENV DB_PORT=27017 58 | ENV DB_USER=development 59 | ENV DB_PASSWORD=development 60 | ENV DB_NAME=development -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/docker-in-docker 3 | { 4 | "name": "Docker in Docker", 5 | "dockerFile": "Dockerfile", 6 | "runArgs": ["--init", "--privileged"], 7 | "mounts": ["source=dind-var-lib-docker,target=/var/lib/docker,type=volume"], 8 | "overrideCommand": false, 9 | 10 | // Use this environment variable if you need to bind mount your local source code into a new container. 11 | "remoteEnv": { 12 | "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" 13 | }, 14 | 15 | // Environment variables 16 | 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": {}, 19 | // Add the IDs of extensions you want installed when the container is created. 20 | "extensions": [ 21 | "dbaeumer.vscode-eslint", 22 | "ms-azuretools.vscode-docker" 23 | ], 24 | 25 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 26 | "forwardPorts": [4000], 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "GIT_LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)", 30 | 31 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 32 | "remoteUser": "vscode", 33 | "build": { 34 | "args": { 35 | "ENABLE_NONROOT_DOCKER": "true", 36 | "USE_MOBY": "false" 37 | } 38 | }, 39 | "features": { 40 | "terraform": "1.0" 41 | } 42 | } -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | set -e 13 | 14 | INSTALL_ZSH=${1:-"true"} 15 | USERNAME=${2:-"automatic"} 16 | USER_UID=${3:-"automatic"} 17 | USER_GID=${4:-"automatic"} 18 | UPGRADE_PACKAGES=${5:-"true"} 19 | INSTALL_OH_MYS=${6:-"true"} 20 | ADD_NON_FREE_PACKAGES=${7:-"false"} 21 | SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" 22 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 23 | 24 | if [ "$(id -u)" -ne 0 ]; then 25 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 26 | exit 1 27 | fi 28 | 29 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 30 | rm -f /etc/profile.d/00-restore-env.sh 31 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 32 | chmod +x /etc/profile.d/00-restore-env.sh 33 | 34 | # If in automatic mode, determine if a user already exists, if not use vscode 35 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 36 | USERNAME="" 37 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 38 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 39 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 40 | USERNAME=${CURRENT_USER} 41 | break 42 | fi 43 | done 44 | if [ "${USERNAME}" = "" ]; then 45 | USERNAME=vscode 46 | fi 47 | elif [ "${USERNAME}" = "none" ]; then 48 | USERNAME=root 49 | USER_UID=0 50 | USER_GID=0 51 | fi 52 | 53 | # Load markers to see which steps have already run 54 | if [ -f "${MARKER_FILE}" ]; then 55 | echo "Marker file found:" 56 | cat "${MARKER_FILE}" 57 | source "${MARKER_FILE}" 58 | fi 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Function to call apt-get if needed 64 | apt_get_update_if_needed() 65 | { 66 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 67 | echo "Running apt-get update..." 68 | apt-get update 69 | else 70 | echo "Skipping apt-get update." 71 | fi 72 | } 73 | 74 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 75 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 76 | 77 | package_list="apt-utils \ 78 | openssh-client \ 79 | gnupg2 \ 80 | dirmngr \ 81 | iproute2 \ 82 | procps \ 83 | lsof \ 84 | htop \ 85 | net-tools \ 86 | psmisc \ 87 | curl \ 88 | wget \ 89 | rsync \ 90 | ca-certificates \ 91 | unzip \ 92 | zip \ 93 | nano \ 94 | vim-tiny \ 95 | less \ 96 | jq \ 97 | lsb-release \ 98 | apt-transport-https \ 99 | dialog \ 100 | libc6 \ 101 | libgcc1 \ 102 | libkrb5-3 \ 103 | libgssapi-krb5-2 \ 104 | libicu[0-9][0-9] \ 105 | liblttng-ust0 \ 106 | libstdc++6 \ 107 | zlib1g \ 108 | locales \ 109 | sudo \ 110 | ncdu \ 111 | man-db \ 112 | strace \ 113 | manpages \ 114 | manpages-dev \ 115 | init-system-helpers" 116 | 117 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 118 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 119 | # Bring in variables from /etc/os-release like VERSION_CODENAME 120 | . /etc/os-release 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 123 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 127 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 128 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 129 | # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html 130 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 131 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 132 | echo "Running apt-get update..." 133 | apt-get update 134 | package_list="${package_list} manpages-posix manpages-posix-dev" 135 | else 136 | apt_get_update_if_needed 137 | fi 138 | 139 | # Install libssl1.1 if available 140 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 141 | package_list="${package_list} libssl1.1" 142 | fi 143 | 144 | # Install appropriate version of libssl1.0.x if available 145 | libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 146 | if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 147 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 148 | # Debian 9 149 | package_list="${package_list} libssl1.0.2" 150 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 151 | # Ubuntu 18.04, 16.04, earlier 152 | package_list="${package_list} libssl1.0.0" 153 | fi 154 | fi 155 | 156 | echo "Packages to verify are installed: ${package_list}" 157 | apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 158 | 159 | # Install git if not already installed (may be more recent than distro version) 160 | if ! type git > /dev/null 2>&1; then 161 | apt-get -y install --no-install-recommends git 162 | fi 163 | 164 | PACKAGES_ALREADY_INSTALLED="true" 165 | fi 166 | 167 | # Get to latest versions of all packages 168 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 169 | apt_get_update_if_needed 170 | apt-get -y upgrade --no-install-recommends 171 | apt-get autoremove -y 172 | fi 173 | 174 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 175 | # Common need for both applications and things like the agnoster ZSH theme. 176 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 177 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 178 | locale-gen 179 | LOCALE_ALREADY_SET="true" 180 | fi 181 | 182 | # Create or update a non-root user to match UID/GID. 183 | group_name="${USERNAME}" 184 | if id -u ${USERNAME} > /dev/null 2>&1; then 185 | # User exists, update if needed 186 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then 187 | group_name="$(id -gn $USERNAME)" 188 | groupmod --gid $USER_GID ${group_name} 189 | usermod --gid $USER_GID $USERNAME 190 | fi 191 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 192 | usermod --uid $USER_UID $USERNAME 193 | fi 194 | else 195 | # Create user 196 | if [ "${USER_GID}" = "automatic" ]; then 197 | groupadd $USERNAME 198 | else 199 | groupadd --gid $USER_GID $USERNAME 200 | fi 201 | if [ "${USER_UID}" = "automatic" ]; then 202 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 203 | else 204 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 205 | fi 206 | fi 207 | 208 | # Add add sudo support for non-root user 209 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 210 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 211 | chmod 0440 /etc/sudoers.d/$USERNAME 212 | EXISTING_NON_ROOT_USER="${USERNAME}" 213 | fi 214 | 215 | # ** Shell customization section ** 216 | if [ "${USERNAME}" = "root" ]; then 217 | user_rc_path="/root" 218 | else 219 | user_rc_path="/home/${USERNAME}" 220 | fi 221 | 222 | # Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty 223 | if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then 224 | cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" 225 | fi 226 | 227 | # Restore user .profile defaults from skeleton file if it doesn't exist or is empty 228 | if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then 229 | cp /etc/skel/.profile "${user_rc_path}/.profile" 230 | fi 231 | 232 | # .bashrc/.zshrc snippet 233 | rc_snippet="$(cat << 'EOF' 234 | 235 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 236 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 237 | 238 | # Display optional first run image specific notice if configured and terminal is interactive 239 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 240 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 241 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 242 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 243 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 244 | fi 245 | mkdir -p "$HOME/.config/vscode-dev-containers" 246 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 247 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 248 | fi 249 | 250 | # Set the default git editor if not already set 251 | if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then 252 | if [ "${TERM_PROGRAM}" = "vscode" ]; then 253 | if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then 254 | export GIT_EDITOR="code-insiders --wait" 255 | else 256 | export GIT_EDITOR="code --wait" 257 | fi 258 | fi 259 | fi 260 | 261 | EOF 262 | )" 263 | 264 | # code shim, it fallbacks to code-insiders if code is not available 265 | cat << 'EOF' > /usr/local/bin/code 266 | #!/bin/sh 267 | 268 | get_in_path_except_current() { 269 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 270 | } 271 | 272 | code="$(get_in_path_except_current code)" 273 | 274 | if [ -n "$code" ]; then 275 | exec "$code" "$@" 276 | elif [ "$(command -v code-insiders)" ]; then 277 | exec code-insiders "$@" 278 | else 279 | echo "code or code-insiders is not installed" >&2 280 | exit 127 281 | fi 282 | EOF 283 | chmod +x /usr/local/bin/code 284 | 285 | # systemctl shim - tells people to use 'service' if systemd is not running 286 | cat << 'EOF' > /usr/local/bin/systemctl 287 | #!/bin/sh 288 | set -e 289 | if [ -d "/run/systemd/system" ]; then 290 | exec /bin/systemctl/systemctl "$@" 291 | else 292 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all' 293 | fi 294 | EOF 295 | chmod +x /usr/local/bin/systemctl 296 | 297 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 298 | codespaces_bash="$(cat \ 299 | <<'EOF' 300 | 301 | # Codespaces bash prompt theme 302 | __bash_prompt() { 303 | local userpart='`export XIT=$? \ 304 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 305 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 306 | local gitbranch='`\ 307 | if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ 308 | export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ 309 | if [ "${BRANCH}" != "" ]; then \ 310 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 311 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 312 | echo -n " \[\033[1;33m\]✗"; \ 313 | fi \ 314 | && echo -n "\[\033[0;36m\]) "; \ 315 | fi; \ 316 | fi`' 317 | local lightblue='\[\033[1;34m\]' 318 | local removecolor='\[\033[0m\]' 319 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 320 | unset -f __bash_prompt 321 | } 322 | __bash_prompt 323 | 324 | EOF 325 | )" 326 | 327 | codespaces_zsh="$(cat \ 328 | <<'EOF' 329 | # Codespaces zsh prompt theme 330 | __zsh_prompt() { 331 | local prompt_username 332 | if [ ! -z "${GITHUB_USER}" ]; then 333 | prompt_username="@${GITHUB_USER}" 334 | else 335 | prompt_username="%n" 336 | fi 337 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 338 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 339 | PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status 340 | PROMPT+='%{$fg[white]%}$ %{$reset_color%}' 341 | unset -f __zsh_prompt 342 | } 343 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 344 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 345 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 346 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 347 | __zsh_prompt 348 | 349 | EOF 350 | )" 351 | 352 | # Add RC snippet and custom bash prompt 353 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 354 | echo "${rc_snippet}" >> /etc/bash.bashrc 355 | echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" 356 | echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" 357 | if [ "${USERNAME}" != "root" ]; then 358 | echo "${codespaces_bash}" >> "/root/.bashrc" 359 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 360 | fi 361 | chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" 362 | RC_SNIPPET_ALREADY_ADDED="true" 363 | fi 364 | 365 | # Optionally install and configure zsh and Oh My Zsh! 366 | if [ "${INSTALL_ZSH}" = "true" ]; then 367 | if ! type zsh > /dev/null 2>&1; then 368 | apt_get_update_if_needed 369 | apt-get install -y zsh 370 | fi 371 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 372 | echo "${rc_snippet}" >> /etc/zsh/zshrc 373 | ZSH_ALREADY_INSTALLED="true" 374 | fi 375 | 376 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 377 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 378 | oh_my_install_dir="${user_rc_path}/.oh-my-zsh" 379 | if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 380 | template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" 381 | user_rc_file="${user_rc_path}/.zshrc" 382 | umask g-w,o-w 383 | mkdir -p ${oh_my_install_dir} 384 | git clone --depth=1 \ 385 | -c core.eol=lf \ 386 | -c core.autocrlf=false \ 387 | -c fsck.zeroPaddedFilemode=ignore \ 388 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 389 | -c receive.fsck.zeroPaddedFilemode=ignore \ 390 | "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 391 | echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} 392 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} 393 | 394 | mkdir -p ${oh_my_install_dir}/custom/themes 395 | echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" 396 | # Shrink git while still enabling updates 397 | cd "${oh_my_install_dir}" 398 | git repack -a -d -f --depth=1 --window=1 399 | # Copy to non-root user if one is specified 400 | if [ "${USERNAME}" != "root" ]; then 401 | cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root 402 | chown -R ${USERNAME}:${group_name} "${user_rc_path}" 403 | fi 404 | fi 405 | fi 406 | 407 | # Persist image metadata info, script if meta.env found in same directory 408 | meta_info_script="$(cat << 'EOF' 409 | #!/bin/sh 410 | . /usr/local/etc/vscode-dev-containers/meta.env 411 | 412 | # Minimal output 413 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 414 | echo "${VERSION}" 415 | exit 0 416 | elif [ "$1" = "release" ]; then 417 | echo "${GIT_REPOSITORY_RELEASE}" 418 | exit 0 419 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 420 | echo "${CONTENTS_URL}" 421 | exit 0 422 | fi 423 | 424 | #Full output 425 | echo 426 | echo "Development container image information" 427 | echo 428 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 429 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 430 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 431 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 432 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 433 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 434 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 435 | echo 436 | EOF 437 | )" 438 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 439 | mkdir -p /usr/local/etc/vscode-dev-containers/ 440 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 441 | echo "${meta_info_script}" > /usr/local/bin/devcontainer-info 442 | chmod +x /usr/local/bin/devcontainer-info 443 | fi 444 | 445 | # Write marker file 446 | mkdir -p "$(dirname "${MARKER_FILE}")" 447 | echo -e "\ 448 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 449 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 450 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 451 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 452 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 453 | 454 | echo "Done!" 455 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/docker-in-docker-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby] [Engine/CLI Version] 11 | 12 | ENABLE_NONROOT_DOCKER=${1:-"true"} 13 | USERNAME=${2:-"automatic"} 14 | USE_MOBY=${3:-"true"} 15 | DOCKER_VERSION=${4:-"latest"} # The Docker/Moby Engine + CLI should match in version 16 | MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" 17 | DOCKER_DASH_COMPOSE_VERSION="1" 18 | 19 | set -e 20 | 21 | if [ "$(id -u)" -ne 0 ]; then 22 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 23 | exit 1 24 | fi 25 | 26 | # Determine the appropriate non-root user 27 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 28 | USERNAME="" 29 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 30 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 31 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 32 | USERNAME=${CURRENT_USER} 33 | break 34 | fi 35 | done 36 | if [ "${USERNAME}" = "" ]; then 37 | USERNAME=root 38 | fi 39 | elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then 40 | USERNAME=root 41 | fi 42 | 43 | # Get central common setting 44 | get_common_setting() { 45 | if [ "${common_settings_file_loaded}" != "true" ]; then 46 | curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." 47 | common_settings_file_loaded=true 48 | fi 49 | if [ -f "/tmp/vsdc-settings.env" ]; then 50 | local multi_line="" 51 | if [ "$2" = "true" ]; then multi_line="-z"; fi 52 | local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" 53 | if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi 54 | fi 55 | echo "$1=${!1}" 56 | } 57 | 58 | # Function to run apt-get if needed 59 | apt_get_update_if_needed() 60 | { 61 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 62 | echo "Running apt-get update..." 63 | apt-get update 64 | else 65 | echo "Skipping apt-get update." 66 | fi 67 | } 68 | 69 | # Checks if packages are installed and installs them if not 70 | check_packages() { 71 | if ! dpkg -s "$@" > /dev/null 2>&1; then 72 | apt_get_update_if_needed 73 | apt-get -y install --no-install-recommends "$@" 74 | fi 75 | } 76 | 77 | # Figure out correct version of a three part version number is not passed 78 | find_version_from_git_tags() { 79 | local variable_name=$1 80 | local requested_version=${!variable_name} 81 | if [ "${requested_version}" = "none" ]; then return; fi 82 | local repository=$2 83 | local prefix=${3:-"tags/v"} 84 | local separator=${4:-"."} 85 | local last_part_optional=${5:-"false"} 86 | if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then 87 | local escaped_separator=${separator//./\\.} 88 | local last_part 89 | if [ "${last_part_optional}" = "true" ]; then 90 | last_part="(${escaped_separator}[0-9]+)?" 91 | else 92 | last_part="${escaped_separator}[0-9]+" 93 | fi 94 | local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" 95 | local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" 96 | if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then 97 | declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" 98 | else 99 | set +e 100 | declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" 101 | set -e 102 | fi 103 | fi 104 | if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then 105 | echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 106 | exit 1 107 | fi 108 | echo "${variable_name}=${!variable_name}" 109 | } 110 | 111 | # Ensure apt is in non-interactive to avoid prompts 112 | export DEBIAN_FRONTEND=noninteractive 113 | 114 | # Install dependencies 115 | check_packages apt-transport-https curl ca-certificates lxc pigz iptables gnupg2 dirmngr 116 | if ! type git > /dev/null 2>&1; then 117 | apt_get_update_if_needed 118 | apt-get -y install git 119 | fi 120 | 121 | # Swap to legacy iptables for compatibility 122 | if type iptables-legacy > /dev/null 2>&1; then 123 | update-alternatives --set iptables /usr/sbin/iptables-legacy 124 | update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy 125 | fi 126 | 127 | # Source /etc/os-release to get OS info 128 | . /etc/os-release 129 | # Fetch host/container arch. 130 | architecture="$(dpkg --print-architecture)" 131 | 132 | # Set up the necessary apt repos (either Microsoft's or Docker's) 133 | if [ "${USE_MOBY}" = "true" ]; then 134 | 135 | # Name of open source engine/cli 136 | engine_package_name="moby-engine" 137 | cli_package_name="moby-cli" 138 | 139 | # Import key safely and import Microsoft apt repo 140 | get_common_setting MICROSOFT_GPG_KEYS_URI 141 | curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg 142 | echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list 143 | else 144 | # Name of licensed engine/cli 145 | engine_package_name="docker-ce" 146 | cli_package_name="docker-ce-cli" 147 | 148 | # Import key safely and import Docker apt repo 149 | curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg 150 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list 151 | fi 152 | 153 | # Refresh apt lists 154 | apt-get update 155 | 156 | # Soft version matching 157 | if [ "${DOCKER_VERSION}" = "latest" ] || [ "${DOCKER_VERSION}" = "lts" ] || [ "${DOCKER_VERSION}" = "stable" ]; then 158 | # Empty, meaning grab whatever "latest" is in apt repo 159 | engine_version_suffix="" 160 | cli_version_suffix="" 161 | else 162 | # Fetch a valid version from the apt-cache (eg: the Microsoft repo appends +azure, breakfix, etc...) 163 | docker_version_dot_escaped="${DOCKER_VERSION//./\\.}" 164 | docker_version_dot_plus_escaped="${docker_version_dot_escaped//+/\\+}" 165 | # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ 166 | docker_version_regex="^(.+:)?${docker_version_dot_plus_escaped}([\\.\\+ ~:-]|$)" 167 | set +e # Don't exit if finding version fails - will handle gracefully 168 | cli_version_suffix="=$(apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" 169 | engine_version_suffix="=$(apt-cache madison ${engine_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" 170 | set -e 171 | if [ -z "${engine_version_suffix}" ] || [ "${engine_version_suffix}" = "=" ] || [ -z "${cli_version_suffix}" ] || [ "${cli_version_suffix}" = "=" ] ; then 172 | echo "(!) No full or partial Docker / Moby version match found for \"${DOCKER_VERSION}\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:" 173 | apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' 174 | exit 1 175 | fi 176 | echo "engine_version_suffix ${engine_version_suffix}" 177 | echo "cli_version_suffix ${cli_version_suffix}" 178 | fi 179 | 180 | # Install Docker / Moby CLI if not already installed 181 | if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then 182 | echo "Docker / Moby CLI and Engine already installed." 183 | else 184 | if [ "${USE_MOBY}" = "true" ]; then 185 | apt-get -y install --no-install-recommends moby-cli${cli_version_suffix} moby-buildx moby-engine${engine_version_suffix} 186 | apt-get -y install --no-install-recommends moby-compose || echo "(*) Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." 187 | else 188 | apt-get -y install --no-install-recommends docker-ce-cli${cli_version_suffix} docker-ce${engine_version_suffix} 189 | fi 190 | fi 191 | 192 | echo "Finished installing docker / moby!" 193 | 194 | # Install Docker Compose if not already installed and is on a supported architecture 195 | if type docker-compose > /dev/null 2>&1; then 196 | echo "Docker Compose already installed." 197 | else 198 | target_compose_arch="${architecture}" 199 | if [ "${target_compose_arch}" = "amd64" ]; then 200 | target_compose_arch="x86_64" 201 | fi 202 | if [ "${target_compose_arch}" != "x86_64" ]; then 203 | # Use pip to get a version that runns on this architecture 204 | if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then 205 | apt_get_update_if_needed 206 | apt-get -y install python3-minimal python3-pip libffi-dev python3-venv 207 | fi 208 | export PIPX_HOME=/usr/local/pipx 209 | mkdir -p ${PIPX_HOME} 210 | export PIPX_BIN_DIR=/usr/local/bin 211 | export PYTHONUSERBASE=/tmp/pip-tmp 212 | export PIP_CACHE_DIR=/tmp/pip-tmp/cache 213 | pipx_bin=pipx 214 | if ! type pipx > /dev/null 2>&1; then 215 | pip3 install --disable-pip-version-check --no-cache-dir --user pipx 216 | pipx_bin=/tmp/pip-tmp/bin/pipx 217 | fi 218 | ${pipx_bin} install --pip-args '--no-cache-dir --force-reinstall' docker-compose 219 | rm -rf /tmp/pip-tmp 220 | else 221 | # Only supports docker-compose v1 222 | find_version_from_git_tags DOCKER_DASH_COMPOSE_VERSION "https://github.com/docker/compose" "tags/" 223 | echo "(*) Installing docker-compose ${DOCKER_DASH_COMPOSE_VERSION}..." 224 | curl -fsSL "https://github.com/docker/compose/releases/download/${DOCKER_DASH_COMPOSE_VERSION}/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose 225 | chmod +x /usr/local/bin/docker-compose 226 | fi 227 | fi 228 | 229 | # If init file already exists, exit 230 | if [ -f "/usr/local/share/docker-init.sh" ]; then 231 | echo "/usr/local/share/docker-init.sh already exists, so exiting." 232 | exit 0 233 | fi 234 | echo "docker-init doesnt exist, adding..." 235 | 236 | # Add user to the docker group 237 | if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then 238 | if ! getent group docker > /dev/null 2>&1; then 239 | groupadd docker 240 | fi 241 | 242 | usermod -aG docker ${USERNAME} 243 | fi 244 | 245 | tee /usr/local/share/docker-init.sh > /dev/null \ 246 | << 'EOF' 247 | #!/bin/sh 248 | #------------------------------------------------------------------------------------------------------------- 249 | # Copyright (c) Microsoft Corporation. All rights reserved. 250 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 251 | #------------------------------------------------------------------------------------------------------------- 252 | 253 | set -e 254 | 255 | dockerd_start="$(cat << 'INNEREOF' 256 | # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly 257 | # ie: docker kill 258 | find /run /var/run -iname 'docker*.pid' -delete || : 259 | find /run /var/run -iname 'container*.pid' -delete || : 260 | 261 | ## Dind wrapper script from docker team, adapted to a function 262 | # Maintained: https://github.com/moby/moby/blob/master/hack/dind 263 | 264 | export container=docker 265 | 266 | if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then 267 | mount -t securityfs none /sys/kernel/security || { 268 | echo >&2 'Could not mount /sys/kernel/security.' 269 | echo >&2 'AppArmor detection and --privileged mode might break.' 270 | } 271 | fi 272 | 273 | # Mount /tmp (conditionally) 274 | if ! mountpoint -q /tmp; then 275 | mount -t tmpfs none /tmp 276 | fi 277 | 278 | # cgroup v2: enable nesting 279 | if [ -f /sys/fs/cgroup/cgroup.controllers ]; then 280 | # move the processes from the root group to the /init group, 281 | # otherwise writing subtree_control fails with EBUSY. 282 | # An error during moving non-existent process (i.e., "cat") is ignored. 283 | mkdir -p /sys/fs/cgroup/init 284 | xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || : 285 | # enable controllers 286 | sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ 287 | > /sys/fs/cgroup/cgroup.subtree_control 288 | fi 289 | ## Dind wrapper over. 290 | 291 | # Handle DNS 292 | set +e 293 | cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' 294 | if [ $? -eq 0 ] 295 | then 296 | echo "Setting dockerd Azure DNS." 297 | CUSTOMDNS="--dns 168.63.129.16" 298 | else 299 | echo "Not setting dockerd DNS manually." 300 | CUSTOMDNS="" 301 | fi 302 | set -e 303 | 304 | # Start docker/moby engine 305 | ( dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) & 306 | INNEREOF 307 | )" 308 | 309 | # Start using sudo if not invoked as root 310 | if [ "$(id -u)" -ne 0 ]; then 311 | sudo /bin/sh -c "${dockerd_start}" 312 | else 313 | eval "${dockerd_start}" 314 | fi 315 | 316 | set +e 317 | 318 | # Execute whatever commands were passed in (if any). This allows us 319 | # to set this script to ENTRYPOINT while still executing the default CMD. 320 | exec "$@" 321 | EOF 322 | 323 | chmod +x /usr/local/share/docker-init.sh 324 | chown ${USERNAME}:root /usr/local/share/docker-init.sh 325 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | env: 7 | description: select the environment 8 | type: environment 9 | required: true 10 | 11 | env: 12 | IMAGE_ID: ${{ vars.AWS_ECR_URI }} 13 | TF_VERSION: 1.5.7 14 | 15 | permissions: 16 | id-token: write # This is required for GitHub OIDC auth with AWS 17 | contents: read # This is required for actions/checkout 18 | 19 | jobs: 20 | terraform_apply_ecr: 21 | name: terraform apply ecr 22 | environment: tools 23 | runs-on: ubuntu-22.04 24 | 25 | steps: 26 | - name: Check out the repo 27 | uses: actions/checkout@v3 28 | with: 29 | ref: ${{ github.event.workflow_run.head_branch }} 30 | 31 | - name: Configure AWS Credentials 32 | uses: aws-actions/configure-aws-credentials@v2 33 | with: 34 | role-to-assume: ${{ vars.TERRAFORM_DEPLOY_ROLE_ARN }} 35 | aws-region: ${{ vars.AWS_REGION }} 36 | 37 | - name: terraform plan ecr 38 | env: 39 | TF_VAR_read_principals: ${{ vars.AWS_ACCOUNTS_ECR_READ_ACCESS }} 40 | working-directory: src/terraform/ecr 41 | run: | 42 | cat < backend.hcl 43 | bucket = "${{ vars.S3_BACKEND_NAME }}" 44 | key = "startup-sample-app-aws-containers-ecr.tfstate" 45 | dynamodb_table = "${{ vars.DYNAMO_DB_TABLE_NAME }}" 46 | EOF 47 | 48 | terraform init -backend-config=backend.hcl 49 | terraform apply -auto-approve 50 | 51 | docker_push: 52 | name: docker build and push 53 | environment: tools 54 | runs-on: ubuntu-22.04 55 | needs: terraform_apply_ecr 56 | 57 | steps: 58 | - name: Check out the repo 59 | uses: actions/checkout@v3 60 | 61 | - name: Configure AWS Credentials 62 | uses: aws-actions/configure-aws-credentials@v2 63 | with: 64 | role-to-assume: ${{ vars.TERRAFORM_DEPLOY_ROLE_ARN }} 65 | aws-region: ${{ vars.AWS_REGION }} 66 | 67 | - name: Login to Amazon ECR 68 | id: login-ecr 69 | uses: aws-actions/amazon-ecr-login@v1 70 | 71 | - name: Build and push 72 | id: docker_build 73 | uses: docker/build-push-action@v4 74 | with: 75 | builder: ${{ steps.buildx.outputs.name }} 76 | context: ./src/api 77 | file: ./src/api/Dockerfile 78 | push: true 79 | tags: ${{ env.IMAGE_ID }}:${{ github.sha }} 80 | 81 | terraform_apply_app: 82 | name: terraform apply app 83 | environment: ${{ inputs.env }} 84 | runs-on: ubuntu-22.04 85 | needs: docker_push 86 | 87 | steps: 88 | - name: Check out the repo 89 | uses: actions/checkout@v3 90 | 91 | - name: Configure AWS Credentials 92 | uses: aws-actions/configure-aws-credentials@v2 93 | with: 94 | role-to-assume: ${{ vars.TERRAFORM_DEPLOY_ROLE_ARN }} 95 | aws-region: ${{ vars.AWS_REGION }} 96 | 97 | - name: terraform apply app 98 | env: 99 | TF_VAR_target_env: ${{ inputs.env }} 100 | TF_VAR_app_image: ${{ env.IMAGE_ID }}:${{ github.sha }} 101 | working-directory: src/terraform/app 102 | run: | 103 | cat < backend.hcl 104 | bucket = "${{ vars.S3_BACKEND_NAME }}" 105 | key = "startup-sample-app-aws-containers.tfstate" 106 | dynamodb_table = "${{ vars.DYNAMO_DB_TABLE_NAME }}" 107 | EOF 108 | 109 | terraform init -backend-config=backend.hcl 110 | terraform apply -auto-approve 111 | 112 | - name: Extract outputs from Terraform 113 | id: tf-outputs 114 | working-directory: src/terraform/app 115 | run: | 116 | terraform output -json > outputs.json 117 | echo "S3_BUCKET_ARN=$(jq -r .s3_bucket_arn.value outputs.json)" >> $GITHUB_ENV 118 | echo "CF_DOMAIN=$(jq -r .cloudfront.value.domain_name outputs.json)" >> $GITHUB_ENV 119 | echo "CF_DISTRIBUTION_ID=$(jq -r .cloudfront.value.distribution_id outputs.json)" >> $GITHUB_ENV 120 | echo "API_GW_URL=$(jq -r .apigw_url.value outputs.json)" >> $GITHUB_ENV 121 | 122 | - name: Build and deploy the front-end 123 | run: | 124 | cd src/web 125 | echo "REACT_APP_API_BASE_URL=$API_GW_URL" > .env 126 | npm install 127 | npm run build 128 | aws s3 sync --delete ./build s3://$(echo "$S3_BUCKET_ARN" | cut -d: -f6) 129 | aws cloudfront create-invalidation --distribution-id $CF_DISTRIBUTION_ID --paths "/*" 130 | -------------------------------------------------------------------------------- /.github/workflows/destroy.yml: -------------------------------------------------------------------------------- 1 | name: Destroy 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | env: 7 | description: select the environment 8 | type: environment 9 | required: true 10 | env: 11 | TF_VERSION: 1.5.7 12 | environment: ${{ inputs.env }} 13 | 14 | permissions: 15 | id-token: write # This is required for requesting the JWT and AWS credentials using AssumeRoleWithWebIdentity 16 | contents: read # This is required for actions/checkout 17 | 18 | jobs: 19 | destroy: 20 | name: Destroy the app 21 | runs-on: ubuntu-latest 22 | environment: 23 | name: ${{ inputs.env }} 24 | 25 | steps: 26 | - name: checkout the repo and building the Backend 27 | uses: actions/checkout@v3 28 | 29 | - name: Configure AWS Credentials 30 | uses: aws-actions/configure-aws-credentials@v2 31 | with: 32 | role-to-assume: ${{ vars.TERRAFORM_DEPLOY_ROLE_ARN }} 33 | aws-region: ca-central-1 34 | 35 | - name: terraform destroy 36 | run: | 37 | cd src/terraform 38 | cat < backend.hcl 39 | bucket = "${{ vars.S3_BACKEND_NAME }}" 40 | key = "${{ vars.LICENSEPLATE }}/${{ env.environment }}/startup-sample-app-aws-containers.tfstate" 41 | dynamodb_table = "${{ vars.DYNAMO_DB_TABLE_NAME }}" 42 | EOF 43 | 44 | terraform init -backend-config=backend.hcl 45 | terraform destroy -auto-approve 46 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: [main, dev, test] 6 | 7 | env: 8 | TF_VERSION: 1.5.7 9 | IMAGE_ID: ${{ vars.AWS_ECR_URI }} 10 | 11 | permissions: 12 | id-token: write # This is required for requesting the JWT and AWS credentials using AssumeRoleWithWebIdentity 13 | contents: read # This is required for actions/checkout 14 | 15 | jobs: 16 | env_selector: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: env selector 20 | id: identify 21 | run: | 22 | if [ ${GITHUB_BASE_REF} = "main" ];then export github_env='prod'; else export github_env=${GITHUB_BASE_REF}; fi 23 | echo "name=${github_env}" >> "$GITHUB_ENV" 24 | 25 | outputs: 26 | github_env: ${{ env.name }} 27 | 28 | terraform_plan_ecr: 29 | name: terraform plan ecr 30 | environment: tools 31 | runs-on: ubuntu-22.04 32 | 33 | steps: 34 | - name: Check out the repo 35 | uses: actions/checkout@v3 36 | with: 37 | ref: ${{ github.event.workflow_run.head_branch }} 38 | 39 | - name: Configure AWS Credentials 40 | uses: aws-actions/configure-aws-credentials@v2 41 | with: 42 | role-to-assume: ${{ vars.TERRAFORM_DEPLOY_ROLE_ARN }} 43 | aws-region: ${{ vars.AWS_REGION }} 44 | 45 | - name: terraform plan ecr 46 | env: 47 | TF_VAR_read_principals: ${{ vars.AWS_ACCOUNTS_ECR_READ_ACCESS }} 48 | working-directory: src/terraform/ecr 49 | run: | 50 | cat < backend.hcl 51 | bucket = "${{ vars.S3_BACKEND_NAME }}" 52 | key = "startup-sample-app-aws-containers-ecr.tfstate" 53 | dynamodb_table = "${{ vars.DYNAMO_DB_TABLE_NAME }}" 54 | EOF 55 | 56 | terraform init -backend-config=backend.hcl 57 | terraform plan 58 | 59 | docker_build: 60 | name: docker build 61 | runs-on: ubuntu-22.04 62 | 63 | steps: 64 | - name: Check out the repo 65 | uses: actions/checkout@v3 66 | 67 | - name: Set up Docker Buildx 68 | uses: docker/setup-buildx-action@v3 69 | 70 | - name: Build 71 | id: docker_build 72 | uses: docker/build-push-action@v5 73 | with: 74 | builder: ${{ steps.buildx.outputs.name }} 75 | context: src/api 76 | file: src/api/Dockerfile 77 | push: false 78 | 79 | terraform_plan_app: 80 | name: terraform plan app 81 | runs-on: ubuntu-latest 82 | needs: env_selector 83 | environment: ${{ needs.env_selector.outputs.github_env }} 84 | 85 | steps: 86 | - name: checkout the repo and building the Backend 87 | uses: actions/checkout@v3 88 | 89 | - uses: actions/setup-node@v3 90 | with: 91 | node-version: 16 92 | 93 | - name: Configure AWS Credentials 94 | uses: aws-actions/configure-aws-credentials@v2 95 | with: 96 | role-to-assume: ${{ vars.TERRAFORM_DEPLOY_ROLE_ARN }} 97 | aws-region: ca-central-1 98 | 99 | - uses: hashicorp/setup-terraform@v2 100 | with: 101 | terraform_version: ${{ env.TF_VERSION }} 102 | 103 | - name: terraform plan app 104 | env: 105 | TF_VAR_target_env: ${{ needs.env_selector.outputs.github_env }} 106 | TF_VAR_app_image: ${{ env.IMAGE_ID }}:${{ github.sha }} 107 | working-directory: src/terraform/app 108 | run: | 109 | cat < backend.hcl 110 | bucket = "${{ vars.S3_BACKEND_NAME }}" 111 | key = "startup-sample-app-aws-containers.tfstate" 112 | dynamodb_table = "${{ vars.DYNAMO_DB_TABLE_NAME }}" 113 | EOF 114 | 115 | terraform init -backend-config=backend.hcl 116 | terraform plan 117 | 118 | - name: Build the front-end 119 | working-directory: src/web 120 | run: | 121 | npm install 122 | npm run build 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Misc. 5 | audit-report.* 6 | .DS_Store 7 | .env 8 | test-results 9 | bcfs-web-* 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | .idea 14 | .vscode 15 | .eslintcache 16 | 17 | # Local .terraform directories 18 | **/.terraform/* 19 | 20 | # .tfstate files 21 | *.tfstate 22 | *.tfstate.* 23 | 24 | # Crash log files 25 | crash.log 26 | 27 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 28 | # .tfvars files are managed as part of configuration and so should be included in 29 | # version control. 30 | # 31 | *.auto.tfvars 32 | 33 | # Ignore override files as they are usually used to override resources locally and so 34 | # are not checked in 35 | override.tf 36 | override.tf.json 37 | *_override.tf 38 | *_override.tf.json 39 | 40 | # Include override files you do wish to add to version control using negated pattern 41 | # 42 | # !example_override.tf 43 | 44 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 45 | # example: *tfplan* 46 | 47 | # Ignore CLI configuration files 48 | .terraformrc 49 | terraform.rc 50 | .terragrunt-cache 51 | 52 | # Ignore lock files (this should normally be committed) 53 | .terraform.lock.hcl 54 | 55 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 -------------------------------------------------------------------------------- /COMPLIANCE.yaml: -------------------------------------------------------------------------------- 1 | name: compliance 2 | description: | 3 | This document is used to track a projects PIA and STRA 4 | compliance. 5 | spec: 6 | - name: PIA 7 | status: not-required 8 | last-updated: '2020-05-25T13:23:52.010Z' 9 | - name: STRA 10 | status: not-required 11 | last-updated: '2020-05-25T13:23:52.010Z' -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Government employees, public and members of the private sector are encouraged to contribute to the repository by forking and submitting a pull request. 4 | 5 | (If you are new to GitHub, you might start with a [basic tutorial](https://help.github.com/en/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) 6 | 7 | Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the master. 8 | 9 | All contributors retain the original copyright to their work, but by contributing to this project, you grant a world-wide, royalty-free, perpetual, irrevocable, non-exclusive, transferable license to all users under the terms of the license under which this project is distributed. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Startup Project 2 | 3 | [![img](https://img.shields.io/badge/Lifecycle-Experimental-339999)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md). 4 | 5 | --- 6 | 7 | ## Introduction 8 | 9 | Welcome to your new project. This is a basic BC Gov AWS "Hello World" starter project to get you started in your cloud journey. It is a NodeJS app connected to a database for you to modify and expand to fit your needs. 10 | 11 | The code can be run: 12 | 13 | - Locally on your machine 14 | - On the AWS Cloud using GitHub Actions workflows and Terraform scripts 15 | 16 | ## Running Locally 17 | 18 | To run the application locally, you will need: 19 | 20 | - [Node.js](https://nodejs.org/) installed 21 | - [Localstack](https://github.com/localstack/localstack) installed and running 22 | 23 | ### Run the API 24 | 25 | Create a `.env` file in the `src/api` folder with the following contents: 26 | 27 | ```bash 28 | LOCALSTACK_ENDPOINT= 29 | DYNAMODB_TABLE_NAME= 30 | ``` 31 | 32 | ```bash 33 | cd src/api 34 | npm install 35 | ``` 36 | 37 | The API will run on . 38 | 39 | ### Run the Frontend 40 | 41 | ```bash 42 | cd src/web 43 | npm install 44 | npm start 45 | ``` 46 | 47 | The frontend will run on 48 | 49 | ## Deploy Application on the AWS Cloud 50 | 51 | ### Prerequisites for building in the AWS Cloud 52 | 53 | This code assumes that you have access to AWS Cloud and that you have created IAM roles linked to your repository and the Github OIDC provider in AWS. These roles will be used by the Terraform scripts to create the infrastructure in AWS. Those roles has to be created by the Ministry teams and then added in your repository. (You can see below how to create them) 54 | 55 | Once the project set is created, it will have one or more service accounts associated each of them with different IAM Roles ARN. 56 | 57 | These roles ARN, necessary to access AWS Cloud, are set for every Github environment. The values themselves are stored as GitHub environment _Variables_ 58 | 59 | The required environment Variables are: 60 | 61 | - `TERRAFORM_DEPLOY_ROLE_ARN` This is the ARN of IAM Role used to deploy resources through the Github action authenticate with the GitHub OpenID Connect. 62 | - Create the IAM Policy for `tools` with this [policy](https://github.com/bcgov/startup-sample-project-aws-containers/blob/main/docs/IAM_policies/Registry_Deployment_IAM_Policy.json) 63 | - Create the IAM Policy for `dev` `test` and `prod` with this [policy](https://github.com/bcgov/startup-sample-project-aws-containers/blob/main/docs/IAM_policies/App_Deployment_IAM_Policy.json) 64 | - To access the `TERRAFORM_DEPLOY_ROLE_ARN` you need to create the role beforehand and link them to the previously created policies in each account. Then you have to add the right arn for each Github environment. 65 | To create the role trust relationship you can use this example: 66 | 67 | ```json 68 | { 69 | "Version": "2012-10-17", 70 | "Statement": [ 71 | { 72 | "Effect": "Allow", 73 | "Principal": { 74 | "Federated": "arn:aws:iam:::oidc-provider/token.actions.githubusercontent.com" 75 | }, 76 | "Action": "sts:AssumeRoleWithWebIdentity", 77 | "Condition": { 78 | "StringLike": { 79 | "token.actions.githubusercontent.com:sub": "repo:/:ref:refs/heads/" 80 | }, 81 | "ForAllValues:StringEquals": { 82 | "token.actions.githubusercontent.com:iss": "https://token.actions.githubusercontent.com", 83 | "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" 84 | } 85 | } 86 | } 87 | ] 88 | } 89 | 90 | ``` 91 | 92 | For the following secret they are global for every account so you can put them as Github repository _Variables_ 93 | 94 | - `AWS_ACCOUNTS_ECR_READ_ACCESS` - is used to authorize the read access to the ECS from the other AWS accounts (dev, test, prod). It is an array where the individual elements take the format follows the format `arn:aws:iam::############:root` where `############` is your AWS account number. For example: 95 | 96 | AWS_ACCOUNTS_ECR_READ_ACCESS='["arn:aws:iam::DEV_ACCOUNT_NUMBER:root", "arn:aws:iam::TEST_ACCOUNT_NUMBER:root", "arn:aws:iam::PROD_ACCOUNT_NUMBER:root"]' 97 | 98 | - `AWS_ECR_URI` - ECR repository URI. Follows the format `############.dkr.ecr.ca-central-1.amazonaws.com/startup-sample-app` where `############` is your AWS account number. 99 | - `AWS_REGION` - should be `ca-central-1` 100 | 101 | ### GitHub Actions 102 | 103 | The deployment of the sample containers app to the AWS Cloud uses several steps. 104 | 105 | - Configure the _Variables_ in your GitHub repository 106 | - Execute a _Pull Request_ to the GitHub repository that includes the changes described in the previous section 107 | 108 | #### Deploy 109 | 110 | [.github/workflows/deploy.yml](.github/workflows/deploy.yml) 111 | 112 | The deploy workflow is triggered by manual dispatch. It will deploy the selected branch to the selected environment. 113 | 114 | >NOTE: For this sample application we chose a manual deploy workflow to keep the cost down. In a real world scenario you may want to use an automated workflow. 115 | 116 | #### Destroy 117 | 118 | [.github/workflows/destroy.yml](.github/workflows/destroy.yml) 119 | 120 | The destroy workflow is triggered by manual dispatch. It will destroy the selected branch from the selected environment. 121 | 122 | #### Pull Request 123 | 124 | [.github/workflows/pull_request.yml](.github/workflows/pull_request.yml) 125 | 126 | The pull request workflow is triggered by pull request to any of the `dev`, `test`, or `main` branches. It will run a `terraform plan` and build the container. 127 | 128 | ### Connecting to the client 129 | 130 | You will be able to access the client using the address for the cloudfront distribution. You can find it in the output of the terraform script. It will be something like this: `https://d1q2w3e4r5t6y7.cloudfront.net/` 131 | 132 | ## Contributing 133 | 134 | Be aware that pull request from fork will fail due to the lack of access of the secrets variables. 135 | You are still welcome to participle but for the plan to work before merge it has to be done from within the repository. 136 | 137 | :warning: The terraform plan stage will fail in cross account pr workflow :warning: 138 | 139 | ## License 140 | 141 | ```text 142 | Copyright 2021 Province of British Columbia 143 | 144 | Licensed under the Apache License, Version 2.0 (the "License"); 145 | you may not use this file except in compliance with the License. 146 | You may obtain a copy of the License at 147 | 148 | 149 | 150 | Unless required by applicable law or agreed to in writing, software 151 | distributed under the License is distributed on an "AS IS" BASIS, 152 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 153 | See the License for the specific language governing permissions and 154 | limitations under the License. 155 | ``` 156 | -------------------------------------------------------------------------------- /docs/IAM_policies/App_Deployment_IAM_Policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "IAM", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "iam:*" 9 | ], 10 | "Resource": [ 11 | "*" 12 | ] 13 | }, 14 | { 15 | "Sid": "S3", 16 | "Effect": "Allow", 17 | "Action": [ 18 | "s3:*" 19 | ], 20 | "Resource": [ 21 | "*" 22 | ] 23 | }, 24 | { 25 | "Sid": "Cloudfront", 26 | "Effect": "Allow", 27 | "Action": [ 28 | "cloudfront:*" 29 | ], 30 | "Resource": [ 31 | "*" 32 | ] 33 | }, 34 | { 35 | "Sid": "ecs", 36 | "Effect": "Allow", 37 | "Action": [ 38 | "ecs:*" 39 | ], 40 | "Resource": "*" 41 | }, 42 | { 43 | "Sid": "ecr", 44 | "Effect": "Allow", 45 | "Action": [ 46 | "ecr:*" 47 | ], 48 | "Resource": "*" 49 | }, 50 | { 51 | "Sid": "Dynamodb", 52 | "Effect": "Allow", 53 | "Action": [ 54 | "dynamodb:*" 55 | ], 56 | "Resource": [ 57 | "*" 58 | ] 59 | }, 60 | { 61 | "Sid": "APIgateway", 62 | "Effect": "Allow", 63 | "Action": [ 64 | "apigateway:*" 65 | ], 66 | "Resource": [ 67 | "*" 68 | ] 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /docs/IAM_policies/Registry_Deployment_IAM_Policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "IAM", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "iam:*" 9 | ], 10 | "Resource": [ 11 | "*" 12 | ] 13 | }, 14 | { 15 | "Sid": "S3", 16 | "Effect": "Allow", 17 | "Action": [ 18 | "s3:*" 19 | ], 20 | "Resource": [ 21 | "*" 22 | ] 23 | }, 24 | { 25 | "Sid": "ecr", 26 | "Effect": "Allow", 27 | "Action": [ 28 | "ecr:*" 29 | ], 30 | "Resource": "*" 31 | }, 32 | { 33 | "Sid": "Dynamodb", 34 | "Effect": "Allow", 35 | "Action": [ 36 | "dynamodb:*" 37 | ], 38 | "Resource": [ 39 | "*" 40 | ] 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /docs/images/browserstack-logo-white-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/startup-sample-project-aws-containers/6687a834b4377f17a1753b772b438c777c6c178f/docs/images/browserstack-logo-white-small.png -------------------------------------------------------------------------------- /docs/images/ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/startup-sample-project-aws-containers/6687a834b4377f17a1753b772b438c777c6c178f/docs/images/ports.png -------------------------------------------------------------------------------- /docs/images/workflows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/startup-sample-project-aws-containers/6687a834b4377f17a1753b772b438c777c6c178f/docs/images/workflows.png -------------------------------------------------------------------------------- /src/api/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY ./ ./ 6 | RUN npm set progress=false && npm ci --no-cache 7 | 8 | EXPOSE 5000 9 | CMD [ "node", "./bin/www" ] -------------------------------------------------------------------------------- /src/api/app.js: -------------------------------------------------------------------------------- 1 | var createError = require("http-errors"); 2 | var express = require("express"); 3 | var path = require("path"); 4 | var cookieParser = require("cookie-parser"); 5 | var logger = require("morgan"); 6 | var cors = require("cors"); 7 | 8 | var greetingRouter = require("./routes/greeting"); 9 | 10 | var app = express(); 11 | 12 | app.use(cors()); 13 | app.use(logger("dev")); 14 | app.use(express.json()); 15 | app.use(express.urlencoded({ extended: false })); 16 | app.use(cookieParser()); 17 | app.use(express.static(path.join(__dirname, "public"))); 18 | 19 | app.use("/api/v1/greeting", greetingRouter); 20 | 21 | // catch 404 and forward to error handler 22 | app.use(function (req, res, next) { 23 | next(createError(404)); 24 | }); 25 | 26 | // error handler 27 | app.use(function (err, req, res, next) { 28 | // set locals, only providing error in development 29 | res.locals.message = err.message; 30 | res.locals.error = req.app.get("env") === "development" ? err : {}; 31 | 32 | // render the error page 33 | res.status(err.status || 500); 34 | res.render("error"); 35 | }); 36 | 37 | module.exports = app; 38 | -------------------------------------------------------------------------------- /src/api/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require("../app"); 8 | var debug = require("debug")("server:server"); 9 | var http = require("http"); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || "5000"); 16 | app.set("port", port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on("error", onError); 30 | server.on("listening", onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== "listen") { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; 62 | 63 | // handle specific listen errors with friendly messages 64 | switch (error.code) { 65 | case "EACCES": 66 | console.error(bind + " requires elevated privileges"); 67 | process.exit(1); 68 | break; 69 | case "EADDRINUSE": 70 | console.error(bind + " is already in use"); 71 | process.exit(1); 72 | break; 73 | default: 74 | throw error; 75 | } 76 | } 77 | 78 | /** 79 | * Event listener for HTTP server "listening" event. 80 | */ 81 | 82 | function onListening() { 83 | var addr = server.address(); 84 | var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; 85 | debug("Listening on " + bind); 86 | } 87 | -------------------------------------------------------------------------------- /src/api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "aws-sdk": "^2.1457.0", 12 | "cookie-parser": "~1.4.4", 13 | "cors": "^2.8.5", 14 | "debug": "~2.6.9", 15 | "express": "~4.18.2", 16 | "http-errors": "~1.6.3", 17 | "morgan": "~1.9.1" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "^3.0.1" 21 | } 22 | }, 23 | "node_modules/abbrev": { 24 | "version": "1.1.1", 25 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 26 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 27 | "dev": true 28 | }, 29 | "node_modules/accepts": { 30 | "version": "1.3.8", 31 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 32 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 33 | "dependencies": { 34 | "mime-types": "~2.1.34", 35 | "negotiator": "0.6.3" 36 | }, 37 | "engines": { 38 | "node": ">= 0.6" 39 | } 40 | }, 41 | "node_modules/anymatch": { 42 | "version": "3.1.3", 43 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 44 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 45 | "dev": true, 46 | "dependencies": { 47 | "normalize-path": "^3.0.0", 48 | "picomatch": "^2.0.4" 49 | }, 50 | "engines": { 51 | "node": ">= 8" 52 | } 53 | }, 54 | "node_modules/array-flatten": { 55 | "version": "1.1.1", 56 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 57 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 58 | }, 59 | "node_modules/available-typed-arrays": { 60 | "version": "1.0.5", 61 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", 62 | "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", 63 | "engines": { 64 | "node": ">= 0.4" 65 | }, 66 | "funding": { 67 | "url": "https://github.com/sponsors/ljharb" 68 | } 69 | }, 70 | "node_modules/aws-sdk": { 71 | "version": "2.1457.0", 72 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1457.0.tgz", 73 | "integrity": "sha512-Gj6R/YISU9L8XzFWJgOW6dKDAZciiCvHs0CGb9GCHsxHO2vgQf2rVrvxV8kg/5ISypOCRtqT9J1xY7HK35fYOA==", 74 | "dependencies": { 75 | "buffer": "4.9.2", 76 | "events": "1.1.1", 77 | "ieee754": "1.1.13", 78 | "jmespath": "0.16.0", 79 | "querystring": "0.2.0", 80 | "sax": "1.2.1", 81 | "url": "0.10.3", 82 | "util": "^0.12.4", 83 | "uuid": "8.0.0", 84 | "xml2js": "0.5.0" 85 | }, 86 | "engines": { 87 | "node": ">= 10.0.0" 88 | } 89 | }, 90 | "node_modules/balanced-match": { 91 | "version": "1.0.2", 92 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 93 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 94 | "dev": true 95 | }, 96 | "node_modules/base64-js": { 97 | "version": "1.5.1", 98 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 99 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 100 | "funding": [ 101 | { 102 | "type": "github", 103 | "url": "https://github.com/sponsors/feross" 104 | }, 105 | { 106 | "type": "patreon", 107 | "url": "https://www.patreon.com/feross" 108 | }, 109 | { 110 | "type": "consulting", 111 | "url": "https://feross.org/support" 112 | } 113 | ] 114 | }, 115 | "node_modules/basic-auth": { 116 | "version": "2.0.1", 117 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", 118 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", 119 | "dependencies": { 120 | "safe-buffer": "5.1.2" 121 | }, 122 | "engines": { 123 | "node": ">= 0.8" 124 | } 125 | }, 126 | "node_modules/binary-extensions": { 127 | "version": "2.2.0", 128 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 129 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 130 | "dev": true, 131 | "engines": { 132 | "node": ">=8" 133 | } 134 | }, 135 | "node_modules/body-parser": { 136 | "version": "1.20.1", 137 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 138 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 139 | "dependencies": { 140 | "bytes": "3.1.2", 141 | "content-type": "~1.0.4", 142 | "debug": "2.6.9", 143 | "depd": "2.0.0", 144 | "destroy": "1.2.0", 145 | "http-errors": "2.0.0", 146 | "iconv-lite": "0.4.24", 147 | "on-finished": "2.4.1", 148 | "qs": "6.11.0", 149 | "raw-body": "2.5.1", 150 | "type-is": "~1.6.18", 151 | "unpipe": "1.0.0" 152 | }, 153 | "engines": { 154 | "node": ">= 0.8", 155 | "npm": "1.2.8000 || >= 1.4.16" 156 | } 157 | }, 158 | "node_modules/body-parser/node_modules/depd": { 159 | "version": "2.0.0", 160 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 161 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 162 | "engines": { 163 | "node": ">= 0.8" 164 | } 165 | }, 166 | "node_modules/body-parser/node_modules/http-errors": { 167 | "version": "2.0.0", 168 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 169 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 170 | "dependencies": { 171 | "depd": "2.0.0", 172 | "inherits": "2.0.4", 173 | "setprototypeof": "1.2.0", 174 | "statuses": "2.0.1", 175 | "toidentifier": "1.0.1" 176 | }, 177 | "engines": { 178 | "node": ">= 0.8" 179 | } 180 | }, 181 | "node_modules/body-parser/node_modules/inherits": { 182 | "version": "2.0.4", 183 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 184 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 185 | }, 186 | "node_modules/body-parser/node_modules/on-finished": { 187 | "version": "2.4.1", 188 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 189 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 190 | "dependencies": { 191 | "ee-first": "1.1.1" 192 | }, 193 | "engines": { 194 | "node": ">= 0.8" 195 | } 196 | }, 197 | "node_modules/body-parser/node_modules/setprototypeof": { 198 | "version": "1.2.0", 199 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 200 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 201 | }, 202 | "node_modules/body-parser/node_modules/statuses": { 203 | "version": "2.0.1", 204 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 205 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 206 | "engines": { 207 | "node": ">= 0.8" 208 | } 209 | }, 210 | "node_modules/brace-expansion": { 211 | "version": "1.1.11", 212 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 213 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 214 | "dev": true, 215 | "dependencies": { 216 | "balanced-match": "^1.0.0", 217 | "concat-map": "0.0.1" 218 | } 219 | }, 220 | "node_modules/braces": { 221 | "version": "3.0.2", 222 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 223 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 224 | "dev": true, 225 | "dependencies": { 226 | "fill-range": "^7.0.1" 227 | }, 228 | "engines": { 229 | "node": ">=8" 230 | } 231 | }, 232 | "node_modules/buffer": { 233 | "version": "4.9.2", 234 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 235 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 236 | "dependencies": { 237 | "base64-js": "^1.0.2", 238 | "ieee754": "^1.1.4", 239 | "isarray": "^1.0.0" 240 | } 241 | }, 242 | "node_modules/bytes": { 243 | "version": "3.1.2", 244 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 245 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 246 | "engines": { 247 | "node": ">= 0.8" 248 | } 249 | }, 250 | "node_modules/call-bind": { 251 | "version": "1.0.2", 252 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 253 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 254 | "dependencies": { 255 | "function-bind": "^1.1.1", 256 | "get-intrinsic": "^1.0.2" 257 | }, 258 | "funding": { 259 | "url": "https://github.com/sponsors/ljharb" 260 | } 261 | }, 262 | "node_modules/chokidar": { 263 | "version": "3.5.3", 264 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 265 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 266 | "dev": true, 267 | "funding": [ 268 | { 269 | "type": "individual", 270 | "url": "https://paulmillr.com/funding/" 271 | } 272 | ], 273 | "dependencies": { 274 | "anymatch": "~3.1.2", 275 | "braces": "~3.0.2", 276 | "glob-parent": "~5.1.2", 277 | "is-binary-path": "~2.1.0", 278 | "is-glob": "~4.0.1", 279 | "normalize-path": "~3.0.0", 280 | "readdirp": "~3.6.0" 281 | }, 282 | "engines": { 283 | "node": ">= 8.10.0" 284 | }, 285 | "optionalDependencies": { 286 | "fsevents": "~2.3.2" 287 | } 288 | }, 289 | "node_modules/concat-map": { 290 | "version": "0.0.1", 291 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 292 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 293 | "dev": true 294 | }, 295 | "node_modules/content-disposition": { 296 | "version": "0.5.4", 297 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 298 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 299 | "dependencies": { 300 | "safe-buffer": "5.2.1" 301 | }, 302 | "engines": { 303 | "node": ">= 0.6" 304 | } 305 | }, 306 | "node_modules/content-disposition/node_modules/safe-buffer": { 307 | "version": "5.2.1", 308 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 309 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 310 | "funding": [ 311 | { 312 | "type": "github", 313 | "url": "https://github.com/sponsors/feross" 314 | }, 315 | { 316 | "type": "patreon", 317 | "url": "https://www.patreon.com/feross" 318 | }, 319 | { 320 | "type": "consulting", 321 | "url": "https://feross.org/support" 322 | } 323 | ] 324 | }, 325 | "node_modules/content-type": { 326 | "version": "1.0.5", 327 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 328 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 329 | "engines": { 330 | "node": ">= 0.6" 331 | } 332 | }, 333 | "node_modules/cookie": { 334 | "version": "0.4.1", 335 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", 336 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", 337 | "engines": { 338 | "node": ">= 0.6" 339 | } 340 | }, 341 | "node_modules/cookie-parser": { 342 | "version": "1.4.6", 343 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", 344 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", 345 | "dependencies": { 346 | "cookie": "0.4.1", 347 | "cookie-signature": "1.0.6" 348 | }, 349 | "engines": { 350 | "node": ">= 0.8.0" 351 | } 352 | }, 353 | "node_modules/cookie-signature": { 354 | "version": "1.0.6", 355 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 356 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 357 | }, 358 | "node_modules/cors": { 359 | "version": "2.8.5", 360 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 361 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 362 | "dependencies": { 363 | "object-assign": "^4", 364 | "vary": "^1" 365 | }, 366 | "engines": { 367 | "node": ">= 0.10" 368 | } 369 | }, 370 | "node_modules/debug": { 371 | "version": "2.6.9", 372 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 373 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 374 | "dependencies": { 375 | "ms": "2.0.0" 376 | } 377 | }, 378 | "node_modules/depd": { 379 | "version": "1.1.2", 380 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 381 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", 382 | "engines": { 383 | "node": ">= 0.6" 384 | } 385 | }, 386 | "node_modules/destroy": { 387 | "version": "1.2.0", 388 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 389 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 390 | "engines": { 391 | "node": ">= 0.8", 392 | "npm": "1.2.8000 || >= 1.4.16" 393 | } 394 | }, 395 | "node_modules/ee-first": { 396 | "version": "1.1.1", 397 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 398 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 399 | }, 400 | "node_modules/encodeurl": { 401 | "version": "1.0.2", 402 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 403 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 404 | "engines": { 405 | "node": ">= 0.8" 406 | } 407 | }, 408 | "node_modules/escape-html": { 409 | "version": "1.0.3", 410 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 411 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 412 | }, 413 | "node_modules/etag": { 414 | "version": "1.8.1", 415 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 416 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 417 | "engines": { 418 | "node": ">= 0.6" 419 | } 420 | }, 421 | "node_modules/events": { 422 | "version": "1.1.1", 423 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 424 | "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", 425 | "engines": { 426 | "node": ">=0.4.x" 427 | } 428 | }, 429 | "node_modules/express": { 430 | "version": "4.18.2", 431 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 432 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 433 | "dependencies": { 434 | "accepts": "~1.3.8", 435 | "array-flatten": "1.1.1", 436 | "body-parser": "1.20.1", 437 | "content-disposition": "0.5.4", 438 | "content-type": "~1.0.4", 439 | "cookie": "0.5.0", 440 | "cookie-signature": "1.0.6", 441 | "debug": "2.6.9", 442 | "depd": "2.0.0", 443 | "encodeurl": "~1.0.2", 444 | "escape-html": "~1.0.3", 445 | "etag": "~1.8.1", 446 | "finalhandler": "1.2.0", 447 | "fresh": "0.5.2", 448 | "http-errors": "2.0.0", 449 | "merge-descriptors": "1.0.1", 450 | "methods": "~1.1.2", 451 | "on-finished": "2.4.1", 452 | "parseurl": "~1.3.3", 453 | "path-to-regexp": "0.1.7", 454 | "proxy-addr": "~2.0.7", 455 | "qs": "6.11.0", 456 | "range-parser": "~1.2.1", 457 | "safe-buffer": "5.2.1", 458 | "send": "0.18.0", 459 | "serve-static": "1.15.0", 460 | "setprototypeof": "1.2.0", 461 | "statuses": "2.0.1", 462 | "type-is": "~1.6.18", 463 | "utils-merge": "1.0.1", 464 | "vary": "~1.1.2" 465 | }, 466 | "engines": { 467 | "node": ">= 0.10.0" 468 | } 469 | }, 470 | "node_modules/express/node_modules/cookie": { 471 | "version": "0.5.0", 472 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 473 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 474 | "engines": { 475 | "node": ">= 0.6" 476 | } 477 | }, 478 | "node_modules/express/node_modules/depd": { 479 | "version": "2.0.0", 480 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 481 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 482 | "engines": { 483 | "node": ">= 0.8" 484 | } 485 | }, 486 | "node_modules/express/node_modules/http-errors": { 487 | "version": "2.0.0", 488 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 489 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 490 | "dependencies": { 491 | "depd": "2.0.0", 492 | "inherits": "2.0.4", 493 | "setprototypeof": "1.2.0", 494 | "statuses": "2.0.1", 495 | "toidentifier": "1.0.1" 496 | }, 497 | "engines": { 498 | "node": ">= 0.8" 499 | } 500 | }, 501 | "node_modules/express/node_modules/inherits": { 502 | "version": "2.0.4", 503 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 504 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 505 | }, 506 | "node_modules/express/node_modules/on-finished": { 507 | "version": "2.4.1", 508 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 509 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 510 | "dependencies": { 511 | "ee-first": "1.1.1" 512 | }, 513 | "engines": { 514 | "node": ">= 0.8" 515 | } 516 | }, 517 | "node_modules/express/node_modules/safe-buffer": { 518 | "version": "5.2.1", 519 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 520 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 521 | "funding": [ 522 | { 523 | "type": "github", 524 | "url": "https://github.com/sponsors/feross" 525 | }, 526 | { 527 | "type": "patreon", 528 | "url": "https://www.patreon.com/feross" 529 | }, 530 | { 531 | "type": "consulting", 532 | "url": "https://feross.org/support" 533 | } 534 | ] 535 | }, 536 | "node_modules/express/node_modules/setprototypeof": { 537 | "version": "1.2.0", 538 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 539 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 540 | }, 541 | "node_modules/express/node_modules/statuses": { 542 | "version": "2.0.1", 543 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 544 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 545 | "engines": { 546 | "node": ">= 0.8" 547 | } 548 | }, 549 | "node_modules/fill-range": { 550 | "version": "7.0.1", 551 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 552 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 553 | "dev": true, 554 | "dependencies": { 555 | "to-regex-range": "^5.0.1" 556 | }, 557 | "engines": { 558 | "node": ">=8" 559 | } 560 | }, 561 | "node_modules/finalhandler": { 562 | "version": "1.2.0", 563 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 564 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 565 | "dependencies": { 566 | "debug": "2.6.9", 567 | "encodeurl": "~1.0.2", 568 | "escape-html": "~1.0.3", 569 | "on-finished": "2.4.1", 570 | "parseurl": "~1.3.3", 571 | "statuses": "2.0.1", 572 | "unpipe": "~1.0.0" 573 | }, 574 | "engines": { 575 | "node": ">= 0.8" 576 | } 577 | }, 578 | "node_modules/finalhandler/node_modules/on-finished": { 579 | "version": "2.4.1", 580 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 581 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 582 | "dependencies": { 583 | "ee-first": "1.1.1" 584 | }, 585 | "engines": { 586 | "node": ">= 0.8" 587 | } 588 | }, 589 | "node_modules/finalhandler/node_modules/statuses": { 590 | "version": "2.0.1", 591 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 592 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 593 | "engines": { 594 | "node": ">= 0.8" 595 | } 596 | }, 597 | "node_modules/for-each": { 598 | "version": "0.3.3", 599 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", 600 | "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", 601 | "dependencies": { 602 | "is-callable": "^1.1.3" 603 | } 604 | }, 605 | "node_modules/forwarded": { 606 | "version": "0.2.0", 607 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 608 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 609 | "engines": { 610 | "node": ">= 0.6" 611 | } 612 | }, 613 | "node_modules/fresh": { 614 | "version": "0.5.2", 615 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 616 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 617 | "engines": { 618 | "node": ">= 0.6" 619 | } 620 | }, 621 | "node_modules/fsevents": { 622 | "version": "2.3.3", 623 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 624 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 625 | "dev": true, 626 | "hasInstallScript": true, 627 | "optional": true, 628 | "os": [ 629 | "darwin" 630 | ], 631 | "engines": { 632 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 633 | } 634 | }, 635 | "node_modules/function-bind": { 636 | "version": "1.1.1", 637 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 638 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 639 | }, 640 | "node_modules/get-intrinsic": { 641 | "version": "1.2.1", 642 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 643 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 644 | "dependencies": { 645 | "function-bind": "^1.1.1", 646 | "has": "^1.0.3", 647 | "has-proto": "^1.0.1", 648 | "has-symbols": "^1.0.3" 649 | }, 650 | "funding": { 651 | "url": "https://github.com/sponsors/ljharb" 652 | } 653 | }, 654 | "node_modules/glob-parent": { 655 | "version": "5.1.2", 656 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 657 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 658 | "dev": true, 659 | "dependencies": { 660 | "is-glob": "^4.0.1" 661 | }, 662 | "engines": { 663 | "node": ">= 6" 664 | } 665 | }, 666 | "node_modules/gopd": { 667 | "version": "1.0.1", 668 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 669 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 670 | "dependencies": { 671 | "get-intrinsic": "^1.1.3" 672 | }, 673 | "funding": { 674 | "url": "https://github.com/sponsors/ljharb" 675 | } 676 | }, 677 | "node_modules/has": { 678 | "version": "1.0.3", 679 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 680 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 681 | "dependencies": { 682 | "function-bind": "^1.1.1" 683 | }, 684 | "engines": { 685 | "node": ">= 0.4.0" 686 | } 687 | }, 688 | "node_modules/has-flag": { 689 | "version": "3.0.0", 690 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 691 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 692 | "dev": true, 693 | "engines": { 694 | "node": ">=4" 695 | } 696 | }, 697 | "node_modules/has-proto": { 698 | "version": "1.0.1", 699 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 700 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 701 | "engines": { 702 | "node": ">= 0.4" 703 | }, 704 | "funding": { 705 | "url": "https://github.com/sponsors/ljharb" 706 | } 707 | }, 708 | "node_modules/has-symbols": { 709 | "version": "1.0.3", 710 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 711 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 712 | "engines": { 713 | "node": ">= 0.4" 714 | }, 715 | "funding": { 716 | "url": "https://github.com/sponsors/ljharb" 717 | } 718 | }, 719 | "node_modules/has-tostringtag": { 720 | "version": "1.0.0", 721 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", 722 | "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", 723 | "dependencies": { 724 | "has-symbols": "^1.0.2" 725 | }, 726 | "engines": { 727 | "node": ">= 0.4" 728 | }, 729 | "funding": { 730 | "url": "https://github.com/sponsors/ljharb" 731 | } 732 | }, 733 | "node_modules/http-errors": { 734 | "version": "1.6.3", 735 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 736 | "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", 737 | "dependencies": { 738 | "depd": "~1.1.2", 739 | "inherits": "2.0.3", 740 | "setprototypeof": "1.1.0", 741 | "statuses": ">= 1.4.0 < 2" 742 | }, 743 | "engines": { 744 | "node": ">= 0.6" 745 | } 746 | }, 747 | "node_modules/iconv-lite": { 748 | "version": "0.4.24", 749 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 750 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 751 | "dependencies": { 752 | "safer-buffer": ">= 2.1.2 < 3" 753 | }, 754 | "engines": { 755 | "node": ">=0.10.0" 756 | } 757 | }, 758 | "node_modules/ieee754": { 759 | "version": "1.1.13", 760 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 761 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 762 | }, 763 | "node_modules/ignore-by-default": { 764 | "version": "1.0.1", 765 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 766 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 767 | "dev": true 768 | }, 769 | "node_modules/inherits": { 770 | "version": "2.0.3", 771 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 772 | "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" 773 | }, 774 | "node_modules/ipaddr.js": { 775 | "version": "1.9.1", 776 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 777 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 778 | "engines": { 779 | "node": ">= 0.10" 780 | } 781 | }, 782 | "node_modules/is-arguments": { 783 | "version": "1.1.1", 784 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", 785 | "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", 786 | "dependencies": { 787 | "call-bind": "^1.0.2", 788 | "has-tostringtag": "^1.0.0" 789 | }, 790 | "engines": { 791 | "node": ">= 0.4" 792 | }, 793 | "funding": { 794 | "url": "https://github.com/sponsors/ljharb" 795 | } 796 | }, 797 | "node_modules/is-binary-path": { 798 | "version": "2.1.0", 799 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 800 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 801 | "dev": true, 802 | "dependencies": { 803 | "binary-extensions": "^2.0.0" 804 | }, 805 | "engines": { 806 | "node": ">=8" 807 | } 808 | }, 809 | "node_modules/is-callable": { 810 | "version": "1.2.7", 811 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 812 | "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", 813 | "engines": { 814 | "node": ">= 0.4" 815 | }, 816 | "funding": { 817 | "url": "https://github.com/sponsors/ljharb" 818 | } 819 | }, 820 | "node_modules/is-extglob": { 821 | "version": "2.1.1", 822 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 823 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 824 | "dev": true, 825 | "engines": { 826 | "node": ">=0.10.0" 827 | } 828 | }, 829 | "node_modules/is-generator-function": { 830 | "version": "1.0.10", 831 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", 832 | "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", 833 | "dependencies": { 834 | "has-tostringtag": "^1.0.0" 835 | }, 836 | "engines": { 837 | "node": ">= 0.4" 838 | }, 839 | "funding": { 840 | "url": "https://github.com/sponsors/ljharb" 841 | } 842 | }, 843 | "node_modules/is-glob": { 844 | "version": "4.0.3", 845 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 846 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 847 | "dev": true, 848 | "dependencies": { 849 | "is-extglob": "^2.1.1" 850 | }, 851 | "engines": { 852 | "node": ">=0.10.0" 853 | } 854 | }, 855 | "node_modules/is-number": { 856 | "version": "7.0.0", 857 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 858 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 859 | "dev": true, 860 | "engines": { 861 | "node": ">=0.12.0" 862 | } 863 | }, 864 | "node_modules/is-typed-array": { 865 | "version": "1.1.12", 866 | "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", 867 | "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", 868 | "dependencies": { 869 | "which-typed-array": "^1.1.11" 870 | }, 871 | "engines": { 872 | "node": ">= 0.4" 873 | }, 874 | "funding": { 875 | "url": "https://github.com/sponsors/ljharb" 876 | } 877 | }, 878 | "node_modules/isarray": { 879 | "version": "1.0.0", 880 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 881 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 882 | }, 883 | "node_modules/jmespath": { 884 | "version": "0.16.0", 885 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", 886 | "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", 887 | "engines": { 888 | "node": ">= 0.6.0" 889 | } 890 | }, 891 | "node_modules/lru-cache": { 892 | "version": "6.0.0", 893 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 894 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 895 | "dev": true, 896 | "dependencies": { 897 | "yallist": "^4.0.0" 898 | }, 899 | "engines": { 900 | "node": ">=10" 901 | } 902 | }, 903 | "node_modules/media-typer": { 904 | "version": "0.3.0", 905 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 906 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 907 | "engines": { 908 | "node": ">= 0.6" 909 | } 910 | }, 911 | "node_modules/merge-descriptors": { 912 | "version": "1.0.1", 913 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 914 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 915 | }, 916 | "node_modules/methods": { 917 | "version": "1.1.2", 918 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 919 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 920 | "engines": { 921 | "node": ">= 0.6" 922 | } 923 | }, 924 | "node_modules/mime": { 925 | "version": "1.6.0", 926 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 927 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 928 | "bin": { 929 | "mime": "cli.js" 930 | }, 931 | "engines": { 932 | "node": ">=4" 933 | } 934 | }, 935 | "node_modules/mime-db": { 936 | "version": "1.52.0", 937 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 938 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 939 | "engines": { 940 | "node": ">= 0.6" 941 | } 942 | }, 943 | "node_modules/mime-types": { 944 | "version": "2.1.35", 945 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 946 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 947 | "dependencies": { 948 | "mime-db": "1.52.0" 949 | }, 950 | "engines": { 951 | "node": ">= 0.6" 952 | } 953 | }, 954 | "node_modules/minimatch": { 955 | "version": "3.1.2", 956 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 957 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 958 | "dev": true, 959 | "dependencies": { 960 | "brace-expansion": "^1.1.7" 961 | }, 962 | "engines": { 963 | "node": "*" 964 | } 965 | }, 966 | "node_modules/morgan": { 967 | "version": "1.9.1", 968 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", 969 | "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", 970 | "dependencies": { 971 | "basic-auth": "~2.0.0", 972 | "debug": "2.6.9", 973 | "depd": "~1.1.2", 974 | "on-finished": "~2.3.0", 975 | "on-headers": "~1.0.1" 976 | }, 977 | "engines": { 978 | "node": ">= 0.8.0" 979 | } 980 | }, 981 | "node_modules/ms": { 982 | "version": "2.0.0", 983 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 984 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 985 | }, 986 | "node_modules/negotiator": { 987 | "version": "0.6.3", 988 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 989 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 990 | "engines": { 991 | "node": ">= 0.6" 992 | } 993 | }, 994 | "node_modules/nodemon": { 995 | "version": "3.0.1", 996 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", 997 | "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", 998 | "dev": true, 999 | "dependencies": { 1000 | "chokidar": "^3.5.2", 1001 | "debug": "^3.2.7", 1002 | "ignore-by-default": "^1.0.1", 1003 | "minimatch": "^3.1.2", 1004 | "pstree.remy": "^1.1.8", 1005 | "semver": "^7.5.3", 1006 | "simple-update-notifier": "^2.0.0", 1007 | "supports-color": "^5.5.0", 1008 | "touch": "^3.1.0", 1009 | "undefsafe": "^2.0.5" 1010 | }, 1011 | "bin": { 1012 | "nodemon": "bin/nodemon.js" 1013 | }, 1014 | "engines": { 1015 | "node": ">=10" 1016 | }, 1017 | "funding": { 1018 | "type": "opencollective", 1019 | "url": "https://opencollective.com/nodemon" 1020 | } 1021 | }, 1022 | "node_modules/nodemon/node_modules/debug": { 1023 | "version": "3.2.7", 1024 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 1025 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 1026 | "dev": true, 1027 | "dependencies": { 1028 | "ms": "^2.1.1" 1029 | } 1030 | }, 1031 | "node_modules/nodemon/node_modules/ms": { 1032 | "version": "2.1.3", 1033 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1034 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1035 | "dev": true 1036 | }, 1037 | "node_modules/nopt": { 1038 | "version": "1.0.10", 1039 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 1040 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", 1041 | "dev": true, 1042 | "dependencies": { 1043 | "abbrev": "1" 1044 | }, 1045 | "bin": { 1046 | "nopt": "bin/nopt.js" 1047 | }, 1048 | "engines": { 1049 | "node": "*" 1050 | } 1051 | }, 1052 | "node_modules/normalize-path": { 1053 | "version": "3.0.0", 1054 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1055 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1056 | "dev": true, 1057 | "engines": { 1058 | "node": ">=0.10.0" 1059 | } 1060 | }, 1061 | "node_modules/object-assign": { 1062 | "version": "4.1.1", 1063 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1064 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1065 | "engines": { 1066 | "node": ">=0.10.0" 1067 | } 1068 | }, 1069 | "node_modules/object-inspect": { 1070 | "version": "1.12.3", 1071 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 1072 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 1073 | "funding": { 1074 | "url": "https://github.com/sponsors/ljharb" 1075 | } 1076 | }, 1077 | "node_modules/on-finished": { 1078 | "version": "2.3.0", 1079 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1080 | "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", 1081 | "dependencies": { 1082 | "ee-first": "1.1.1" 1083 | }, 1084 | "engines": { 1085 | "node": ">= 0.8" 1086 | } 1087 | }, 1088 | "node_modules/on-headers": { 1089 | "version": "1.0.2", 1090 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 1091 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", 1092 | "engines": { 1093 | "node": ">= 0.8" 1094 | } 1095 | }, 1096 | "node_modules/parseurl": { 1097 | "version": "1.3.3", 1098 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1099 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1100 | "engines": { 1101 | "node": ">= 0.8" 1102 | } 1103 | }, 1104 | "node_modules/path-to-regexp": { 1105 | "version": "0.1.7", 1106 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1107 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 1108 | }, 1109 | "node_modules/picomatch": { 1110 | "version": "2.3.1", 1111 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1112 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1113 | "dev": true, 1114 | "engines": { 1115 | "node": ">=8.6" 1116 | }, 1117 | "funding": { 1118 | "url": "https://github.com/sponsors/jonschlinkert" 1119 | } 1120 | }, 1121 | "node_modules/proxy-addr": { 1122 | "version": "2.0.7", 1123 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1124 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1125 | "dependencies": { 1126 | "forwarded": "0.2.0", 1127 | "ipaddr.js": "1.9.1" 1128 | }, 1129 | "engines": { 1130 | "node": ">= 0.10" 1131 | } 1132 | }, 1133 | "node_modules/pstree.remy": { 1134 | "version": "1.1.8", 1135 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1136 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1137 | "dev": true 1138 | }, 1139 | "node_modules/punycode": { 1140 | "version": "1.3.2", 1141 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 1142 | "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" 1143 | }, 1144 | "node_modules/qs": { 1145 | "version": "6.11.0", 1146 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 1147 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 1148 | "dependencies": { 1149 | "side-channel": "^1.0.4" 1150 | }, 1151 | "engines": { 1152 | "node": ">=0.6" 1153 | }, 1154 | "funding": { 1155 | "url": "https://github.com/sponsors/ljharb" 1156 | } 1157 | }, 1158 | "node_modules/querystring": { 1159 | "version": "0.2.0", 1160 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 1161 | "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", 1162 | "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", 1163 | "engines": { 1164 | "node": ">=0.4.x" 1165 | } 1166 | }, 1167 | "node_modules/range-parser": { 1168 | "version": "1.2.1", 1169 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1170 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1171 | "engines": { 1172 | "node": ">= 0.6" 1173 | } 1174 | }, 1175 | "node_modules/raw-body": { 1176 | "version": "2.5.1", 1177 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 1178 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 1179 | "dependencies": { 1180 | "bytes": "3.1.2", 1181 | "http-errors": "2.0.0", 1182 | "iconv-lite": "0.4.24", 1183 | "unpipe": "1.0.0" 1184 | }, 1185 | "engines": { 1186 | "node": ">= 0.8" 1187 | } 1188 | }, 1189 | "node_modules/raw-body/node_modules/depd": { 1190 | "version": "2.0.0", 1191 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 1192 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 1193 | "engines": { 1194 | "node": ">= 0.8" 1195 | } 1196 | }, 1197 | "node_modules/raw-body/node_modules/http-errors": { 1198 | "version": "2.0.0", 1199 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1200 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1201 | "dependencies": { 1202 | "depd": "2.0.0", 1203 | "inherits": "2.0.4", 1204 | "setprototypeof": "1.2.0", 1205 | "statuses": "2.0.1", 1206 | "toidentifier": "1.0.1" 1207 | }, 1208 | "engines": { 1209 | "node": ">= 0.8" 1210 | } 1211 | }, 1212 | "node_modules/raw-body/node_modules/inherits": { 1213 | "version": "2.0.4", 1214 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1215 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1216 | }, 1217 | "node_modules/raw-body/node_modules/setprototypeof": { 1218 | "version": "1.2.0", 1219 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1220 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1221 | }, 1222 | "node_modules/raw-body/node_modules/statuses": { 1223 | "version": "2.0.1", 1224 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1225 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1226 | "engines": { 1227 | "node": ">= 0.8" 1228 | } 1229 | }, 1230 | "node_modules/readdirp": { 1231 | "version": "3.6.0", 1232 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1233 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1234 | "dev": true, 1235 | "dependencies": { 1236 | "picomatch": "^2.2.1" 1237 | }, 1238 | "engines": { 1239 | "node": ">=8.10.0" 1240 | } 1241 | }, 1242 | "node_modules/safe-buffer": { 1243 | "version": "5.1.2", 1244 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1245 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1246 | }, 1247 | "node_modules/safer-buffer": { 1248 | "version": "2.1.2", 1249 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1250 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1251 | }, 1252 | "node_modules/sax": { 1253 | "version": "1.2.1", 1254 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 1255 | "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" 1256 | }, 1257 | "node_modules/semver": { 1258 | "version": "7.5.4", 1259 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 1260 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 1261 | "dev": true, 1262 | "dependencies": { 1263 | "lru-cache": "^6.0.0" 1264 | }, 1265 | "bin": { 1266 | "semver": "bin/semver.js" 1267 | }, 1268 | "engines": { 1269 | "node": ">=10" 1270 | } 1271 | }, 1272 | "node_modules/send": { 1273 | "version": "0.18.0", 1274 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 1275 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 1276 | "dependencies": { 1277 | "debug": "2.6.9", 1278 | "depd": "2.0.0", 1279 | "destroy": "1.2.0", 1280 | "encodeurl": "~1.0.2", 1281 | "escape-html": "~1.0.3", 1282 | "etag": "~1.8.1", 1283 | "fresh": "0.5.2", 1284 | "http-errors": "2.0.0", 1285 | "mime": "1.6.0", 1286 | "ms": "2.1.3", 1287 | "on-finished": "2.4.1", 1288 | "range-parser": "~1.2.1", 1289 | "statuses": "2.0.1" 1290 | }, 1291 | "engines": { 1292 | "node": ">= 0.8.0" 1293 | } 1294 | }, 1295 | "node_modules/send/node_modules/depd": { 1296 | "version": "2.0.0", 1297 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 1298 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 1299 | "engines": { 1300 | "node": ">= 0.8" 1301 | } 1302 | }, 1303 | "node_modules/send/node_modules/http-errors": { 1304 | "version": "2.0.0", 1305 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1306 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1307 | "dependencies": { 1308 | "depd": "2.0.0", 1309 | "inherits": "2.0.4", 1310 | "setprototypeof": "1.2.0", 1311 | "statuses": "2.0.1", 1312 | "toidentifier": "1.0.1" 1313 | }, 1314 | "engines": { 1315 | "node": ">= 0.8" 1316 | } 1317 | }, 1318 | "node_modules/send/node_modules/inherits": { 1319 | "version": "2.0.4", 1320 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1321 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1322 | }, 1323 | "node_modules/send/node_modules/ms": { 1324 | "version": "2.1.3", 1325 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1326 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1327 | }, 1328 | "node_modules/send/node_modules/on-finished": { 1329 | "version": "2.4.1", 1330 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1331 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1332 | "dependencies": { 1333 | "ee-first": "1.1.1" 1334 | }, 1335 | "engines": { 1336 | "node": ">= 0.8" 1337 | } 1338 | }, 1339 | "node_modules/send/node_modules/setprototypeof": { 1340 | "version": "1.2.0", 1341 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1342 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1343 | }, 1344 | "node_modules/send/node_modules/statuses": { 1345 | "version": "2.0.1", 1346 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1347 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1348 | "engines": { 1349 | "node": ">= 0.8" 1350 | } 1351 | }, 1352 | "node_modules/serve-static": { 1353 | "version": "1.15.0", 1354 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1355 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1356 | "dependencies": { 1357 | "encodeurl": "~1.0.2", 1358 | "escape-html": "~1.0.3", 1359 | "parseurl": "~1.3.3", 1360 | "send": "0.18.0" 1361 | }, 1362 | "engines": { 1363 | "node": ">= 0.8.0" 1364 | } 1365 | }, 1366 | "node_modules/setprototypeof": { 1367 | "version": "1.1.0", 1368 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 1369 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 1370 | }, 1371 | "node_modules/side-channel": { 1372 | "version": "1.0.4", 1373 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1374 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1375 | "dependencies": { 1376 | "call-bind": "^1.0.0", 1377 | "get-intrinsic": "^1.0.2", 1378 | "object-inspect": "^1.9.0" 1379 | }, 1380 | "funding": { 1381 | "url": "https://github.com/sponsors/ljharb" 1382 | } 1383 | }, 1384 | "node_modules/simple-update-notifier": { 1385 | "version": "2.0.0", 1386 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 1387 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 1388 | "dev": true, 1389 | "dependencies": { 1390 | "semver": "^7.5.3" 1391 | }, 1392 | "engines": { 1393 | "node": ">=10" 1394 | } 1395 | }, 1396 | "node_modules/statuses": { 1397 | "version": "1.4.0", 1398 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 1399 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", 1400 | "engines": { 1401 | "node": ">= 0.6" 1402 | } 1403 | }, 1404 | "node_modules/supports-color": { 1405 | "version": "5.5.0", 1406 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1407 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1408 | "dev": true, 1409 | "dependencies": { 1410 | "has-flag": "^3.0.0" 1411 | }, 1412 | "engines": { 1413 | "node": ">=4" 1414 | } 1415 | }, 1416 | "node_modules/to-regex-range": { 1417 | "version": "5.0.1", 1418 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1419 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1420 | "dev": true, 1421 | "dependencies": { 1422 | "is-number": "^7.0.0" 1423 | }, 1424 | "engines": { 1425 | "node": ">=8.0" 1426 | } 1427 | }, 1428 | "node_modules/toidentifier": { 1429 | "version": "1.0.1", 1430 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1431 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1432 | "engines": { 1433 | "node": ">=0.6" 1434 | } 1435 | }, 1436 | "node_modules/touch": { 1437 | "version": "3.1.0", 1438 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 1439 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 1440 | "dev": true, 1441 | "dependencies": { 1442 | "nopt": "~1.0.10" 1443 | }, 1444 | "bin": { 1445 | "nodetouch": "bin/nodetouch.js" 1446 | } 1447 | }, 1448 | "node_modules/type-is": { 1449 | "version": "1.6.18", 1450 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1451 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1452 | "dependencies": { 1453 | "media-typer": "0.3.0", 1454 | "mime-types": "~2.1.24" 1455 | }, 1456 | "engines": { 1457 | "node": ">= 0.6" 1458 | } 1459 | }, 1460 | "node_modules/undefsafe": { 1461 | "version": "2.0.5", 1462 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1463 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 1464 | "dev": true 1465 | }, 1466 | "node_modules/unpipe": { 1467 | "version": "1.0.0", 1468 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1469 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1470 | "engines": { 1471 | "node": ">= 0.8" 1472 | } 1473 | }, 1474 | "node_modules/url": { 1475 | "version": "0.10.3", 1476 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 1477 | "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", 1478 | "dependencies": { 1479 | "punycode": "1.3.2", 1480 | "querystring": "0.2.0" 1481 | } 1482 | }, 1483 | "node_modules/util": { 1484 | "version": "0.12.5", 1485 | "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", 1486 | "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", 1487 | "dependencies": { 1488 | "inherits": "^2.0.3", 1489 | "is-arguments": "^1.0.4", 1490 | "is-generator-function": "^1.0.7", 1491 | "is-typed-array": "^1.1.3", 1492 | "which-typed-array": "^1.1.2" 1493 | } 1494 | }, 1495 | "node_modules/utils-merge": { 1496 | "version": "1.0.1", 1497 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1498 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1499 | "engines": { 1500 | "node": ">= 0.4.0" 1501 | } 1502 | }, 1503 | "node_modules/uuid": { 1504 | "version": "8.0.0", 1505 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", 1506 | "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", 1507 | "bin": { 1508 | "uuid": "dist/bin/uuid" 1509 | } 1510 | }, 1511 | "node_modules/vary": { 1512 | "version": "1.1.2", 1513 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1514 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1515 | "engines": { 1516 | "node": ">= 0.8" 1517 | } 1518 | }, 1519 | "node_modules/which-typed-array": { 1520 | "version": "1.1.11", 1521 | "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", 1522 | "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", 1523 | "dependencies": { 1524 | "available-typed-arrays": "^1.0.5", 1525 | "call-bind": "^1.0.2", 1526 | "for-each": "^0.3.3", 1527 | "gopd": "^1.0.1", 1528 | "has-tostringtag": "^1.0.0" 1529 | }, 1530 | "engines": { 1531 | "node": ">= 0.4" 1532 | }, 1533 | "funding": { 1534 | "url": "https://github.com/sponsors/ljharb" 1535 | } 1536 | }, 1537 | "node_modules/xml2js": { 1538 | "version": "0.5.0", 1539 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 1540 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 1541 | "dependencies": { 1542 | "sax": ">=0.6.0", 1543 | "xmlbuilder": "~11.0.0" 1544 | }, 1545 | "engines": { 1546 | "node": ">=4.0.0" 1547 | } 1548 | }, 1549 | "node_modules/xmlbuilder": { 1550 | "version": "11.0.1", 1551 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 1552 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 1553 | "engines": { 1554 | "node": ">=4.0" 1555 | } 1556 | }, 1557 | "node_modules/yallist": { 1558 | "version": "4.0.0", 1559 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1560 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 1561 | "dev": true 1562 | } 1563 | } 1564 | } 1565 | -------------------------------------------------------------------------------- /src/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon ./bin/www" 7 | }, 8 | "dependencies": { 9 | "aws-sdk": "^2.1457.0", 10 | "cookie-parser": "~1.4.4", 11 | "cors": "^2.8.5", 12 | "debug": "~2.6.9", 13 | "express": "~4.18.2", 14 | "http-errors": "~1.6.3", 15 | "morgan": "~1.9.1" 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^3.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/routes/greeting.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const crypto = require("crypto"); 4 | 5 | const AWS = require("aws-sdk"); 6 | 7 | var aws_config = {}; 8 | 9 | if (process.env.LOCALSTACK_ENDPOINT) { 10 | aws_config = { 11 | region: 12 | process.env.AWS_REGION || 13 | process.env.AWS_DEFAULT_REGION || 14 | "ca-central-1", 15 | endpoint: process.env.LOCALSTACK_ENDPOINT, 16 | }; 17 | } else { 18 | aws_config = { 19 | region: 20 | process.env.AWS_REGION || 21 | process.env.AWS_DEFAULT_REGION || 22 | "ca-central-1", 23 | }; 24 | } 25 | 26 | console.log("aws_config", aws_config); 27 | 28 | AWS.config.update(aws_config); 29 | 30 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 31 | 32 | const bodyParser = require("body-parser"); 33 | router.use(bodyParser.json()); 34 | 35 | // POST /api/v1/greeting 36 | router.post("/", async (req, res) => { 37 | try { 38 | const { greeting } = req.body; 39 | 40 | if (!greeting) { 41 | return res.status(400).json({ message: "Greeting is required" }); 42 | } 43 | 44 | const item = { 45 | id: crypto.randomBytes(16).toString("hex"), 46 | greeting, 47 | createdAt: new Date().toISOString(), 48 | }; 49 | 50 | await dynamodb 51 | .put({ 52 | TableName: process.env.DYNAMODB_TABLE_NAME, 53 | Item: item, 54 | }) 55 | .promise(); 56 | 57 | res.json(item); 58 | } catch (error) { 59 | console.error(error); 60 | res.status(500).json({ message: "Internal server error" }); 61 | } 62 | }); 63 | 64 | // GET /api/v1/greetings/latest 65 | router.get("/latest", async (req, res) => { 66 | try { 67 | const result = await dynamodb 68 | .scan({ 69 | TableName: process.env.DYNAMODB_TABLE_NAME, 70 | }) 71 | .promise(); 72 | 73 | const greetings = result.Items.map((item) => ({ 74 | id: item.id, 75 | greeting: item.greeting, 76 | createdAt: item.createdAt, 77 | updatedAt: item.updatedAt, 78 | })); 79 | 80 | res.json({ greetingItems: greetings }); 81 | } catch (error) { 82 | console.error(error); 83 | res.status(500).json({ message: "Error retrieving greetings" }); 84 | } 85 | }); 86 | 87 | module.exports = router; 88 | -------------------------------------------------------------------------------- /src/terraform/app/main.tf: -------------------------------------------------------------------------------- 1 | 2 | locals { 3 | common_tags = var.common_tags 4 | create_ecs_service = var.app_image == "" ? 0 : 1 5 | } 6 | 7 | data "aws_caller_identity" "current" {} 8 | 9 | # Gather VPC information from the network module 10 | 11 | module "network" { 12 | source = "git::https://github.com/BCDevOps/terraform-octk-aws-sea-network-info.git//?ref=master" 13 | environment = var.target_env 14 | } 15 | 16 | # S3 bucket for static assets 17 | 18 | resource "aws_s3_bucket" "site" { 19 | bucket = "${var.app_name}-site-${data.aws_caller_identity.current.account_id}-${var.aws_region}" 20 | } 21 | 22 | # CloudFront distribution for the S3 bucket 23 | resource "aws_cloudfront_origin_access_identity" "oai" { 24 | comment = "OAI for ${var.app_name} site." 25 | } 26 | 27 | resource "aws_s3_bucket_policy" "site_policy" { 28 | bucket = aws_s3_bucket.site.bucket 29 | 30 | policy = jsonencode({ 31 | Version = "2012-10-17", 32 | Statement = [ 33 | { 34 | Effect = "Allow", 35 | Principal = { 36 | "AWS" : "${aws_cloudfront_origin_access_identity.oai.iam_arn}" 37 | }, 38 | Action = "s3:GetObject", 39 | Resource = "arn:aws:s3:::${aws_s3_bucket.site.bucket}/*" 40 | } 41 | ] 42 | }) 43 | } 44 | 45 | resource "aws_cloudfront_distribution" "s3_distribution" { 46 | enabled = true 47 | is_ipv6_enabled = true 48 | comment = "Distribution for ${var.app_name} site." 49 | default_root_object = "index.html" 50 | 51 | origin { 52 | domain_name = aws_s3_bucket.site.bucket_regional_domain_name 53 | origin_id = aws_s3_bucket.site.bucket 54 | 55 | s3_origin_config { 56 | origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path 57 | } 58 | } 59 | 60 | default_cache_behavior { 61 | allowed_methods = ["GET", "HEAD"] 62 | cached_methods = ["GET", "HEAD"] 63 | target_origin_id = aws_s3_bucket.site.bucket 64 | 65 | forwarded_values { 66 | query_string = false 67 | 68 | cookies { 69 | forward = "none" 70 | } 71 | } 72 | 73 | viewer_protocol_policy = "redirect-to-https" 74 | min_ttl = 0 75 | default_ttl = 3600 76 | max_ttl = 86400 77 | } 78 | 79 | restrictions { 80 | geo_restriction { 81 | restriction_type = "none" 82 | } 83 | } 84 | 85 | viewer_certificate { 86 | cloudfront_default_certificate = true 87 | } 88 | 89 | tags = { 90 | Name = "${var.app_name}-distribution" 91 | } 92 | } 93 | 94 | # API Gateway 95 | 96 | resource "aws_apigatewayv2_vpc_link" "app" { 97 | name = var.app_name 98 | subnet_ids = module.network.aws_subnet_ids.web.ids 99 | security_group_ids = [module.network.aws_security_groups.web.id] 100 | } 101 | 102 | resource "aws_apigatewayv2_api" "app" { 103 | name = var.app_name 104 | protocol_type = "HTTP" 105 | } 106 | 107 | resource "aws_apigatewayv2_integration" "app" { 108 | api_id = aws_apigatewayv2_api.app.id 109 | integration_type = "HTTP_PROXY" 110 | connection_id = aws_apigatewayv2_vpc_link.app.id 111 | connection_type = "VPC_LINK" 112 | integration_method = "ANY" 113 | integration_uri = aws_alb_listener.internal.arn 114 | } 115 | 116 | resource "aws_apigatewayv2_route" "app" { 117 | api_id = aws_apigatewayv2_api.app.id 118 | route_key = "ANY /{proxy+}" 119 | target = "integrations/${aws_apigatewayv2_integration.app.id}" 120 | } 121 | 122 | resource "aws_apigatewayv2_stage" "app" { 123 | api_id = aws_apigatewayv2_api.app.id 124 | name = "$default" 125 | auto_deploy = true 126 | } 127 | 128 | # Internal ALB 129 | 130 | resource "aws_alb" "app-alb" { 131 | 132 | name = var.app_name 133 | internal = true 134 | subnets = module.network.aws_subnet_ids.web.ids 135 | security_groups = [module.network.aws_security_groups.web.id] 136 | enable_cross_zone_load_balancing = true 137 | tags = local.common_tags 138 | 139 | lifecycle { 140 | ignore_changes = [access_logs] 141 | } 142 | } 143 | resource "aws_alb_listener" "internal" { 144 | load_balancer_arn = aws_alb.app-alb.arn 145 | port = "80" 146 | protocol = "HTTP" 147 | 148 | default_action { 149 | type = "forward" 150 | target_group_arn = aws_alb_target_group.app.arn 151 | } 152 | 153 | } 154 | resource "aws_alb_target_group" "app" { 155 | name = "${var.app_name}-tg" 156 | port = var.app_port 157 | protocol = "HTTP" 158 | vpc_id = module.network.aws_vpc.id 159 | target_type = "ip" 160 | deregistration_delay = 30 161 | 162 | health_check { 163 | healthy_threshold = "2" 164 | interval = "5" 165 | protocol = "HTTP" 166 | matcher = "200" 167 | timeout = "3" 168 | path = var.health_check_path 169 | unhealthy_threshold = "2" 170 | } 171 | 172 | tags = local.common_tags 173 | } 174 | 175 | # ECS 176 | 177 | resource "aws_ecs_cluster" "main" { 178 | name = "${var.app_name}-cluster" 179 | tags = local.common_tags 180 | } 181 | 182 | resource "aws_ecs_cluster_capacity_providers" "sample" { 183 | cluster_name = aws_ecs_cluster.main.name 184 | 185 | capacity_providers = ["FARGATE_SPOT"] 186 | 187 | default_capacity_provider_strategy { 188 | weight = 100 189 | capacity_provider = "FARGATE_SPOT" 190 | } 191 | } 192 | 193 | resource "aws_ecs_task_definition" "app" { 194 | count = local.create_ecs_service 195 | family = var.app_name 196 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 197 | task_role_arn = aws_iam_role.sample_app_container_role.arn 198 | network_mode = "awsvpc" 199 | requires_compatibilities = ["FARGATE"] 200 | cpu = var.fargate_cpu 201 | memory = var.fargate_memory 202 | tags = local.common_tags 203 | container_definitions = jsonencode([ 204 | { 205 | essential = true 206 | name = var.container_name 207 | image = var.app_image 208 | cpu = var.fargate_cpu 209 | memory = var.fargate_memory 210 | networkMode = "awsvpc" 211 | portMappings = [ 212 | { 213 | protocol = "tcp" 214 | containerPort = var.app_port 215 | hostPort = var.app_port 216 | } 217 | ] 218 | environment = [ 219 | { 220 | name = "DYNAMODB_TABLE_NAME" 221 | value = var.db_name 222 | }, 223 | { 224 | name = "AWS_REGION", 225 | value = var.aws_region 226 | } 227 | ] 228 | logConfiguration = { 229 | logDriver = "awslogs" 230 | options = { 231 | awslogs-create-group = "true" 232 | awslogs-group = "/ecs/${var.app_name}" 233 | awslogs-region = var.aws_region 234 | awslogs-stream-prefix = "ecs" 235 | } 236 | } 237 | mountPoints = [] 238 | volumesFrom = [] 239 | } 240 | ]) 241 | } 242 | 243 | resource "aws_ecs_service" "main" { 244 | count = local.create_ecs_service 245 | name = var.app_name 246 | cluster = aws_ecs_cluster.main.id 247 | task_definition = aws_ecs_task_definition.app[count.index].arn 248 | desired_count = var.app_count 249 | enable_ecs_managed_tags = true 250 | propagate_tags = "TASK_DEFINITION" 251 | health_check_grace_period_seconds = 60 252 | wait_for_steady_state = false 253 | 254 | 255 | capacity_provider_strategy { 256 | capacity_provider = "FARGATE_SPOT" 257 | weight = 100 258 | } 259 | 260 | 261 | network_configuration { 262 | security_groups = [module.network.aws_security_groups.app.id] 263 | subnets = module.network.aws_subnet_ids.app.ids 264 | assign_public_ip = false 265 | } 266 | 267 | load_balancer { 268 | target_group_arn = aws_alb_target_group.app.id 269 | container_name = var.container_name 270 | container_port = var.app_port 271 | } 272 | 273 | depends_on = [aws_iam_role_policy_attachment.ecs_task_execution_role] 274 | 275 | tags = local.common_tags 276 | } 277 | 278 | # ECS task execution role data 279 | data "aws_iam_policy_document" "ecs_task_execution_role" { 280 | version = "2012-10-17" 281 | statement { 282 | sid = "" 283 | effect = "Allow" 284 | actions = ["sts:AssumeRole"] 285 | 286 | principals { 287 | type = "Service" 288 | identifiers = ["ecs-tasks.amazonaws.com"] 289 | } 290 | } 291 | } 292 | # ECS task execution role 293 | resource "aws_iam_role" "ecs_task_execution_role" { 294 | name = "${var.app_name}_ecs_task_execution_role" 295 | assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json 296 | 297 | tags = local.common_tags 298 | } 299 | 300 | # ECS task execution role policy attachment 301 | resource "aws_iam_role_policy_attachment" "ecs_task_execution_role" { 302 | role = aws_iam_role.ecs_task_execution_role.name 303 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 304 | } 305 | 306 | resource "aws_iam_role_policy" "ecs_task_execution_cwlogs" { 307 | name = "${var.app_name}-ecs_task_execution_cwlogs" 308 | role = aws_iam_role.ecs_task_execution_role.id 309 | 310 | policy = <<-EOF 311 | { 312 | "Version": "2012-10-17", 313 | "Statement": [ 314 | { 315 | "Effect": "Allow", 316 | "Action": [ 317 | "logs:CreateLogGroup" 318 | ], 319 | "Resource": [ 320 | "arn:aws:logs:*:*:*" 321 | ] 322 | } 323 | ] 324 | } 325 | EOF 326 | } 327 | 328 | resource "aws_iam_role" "sample_app_container_role" { 329 | name = "${var.app_name}_container_role" 330 | 331 | assume_role_policy = <0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/startup-sample-project-aws-containers/6687a834b4377f17a1753b772b438c777c6c178f/src/web/public/favicon.ico -------------------------------------------------------------------------------- /src/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Public Cloud Sample Application 25 | 26 | 27 |
28 | 38 | 39 | -------------------------------------------------------------------------------- /src/web/public/logo.svg: -------------------------------------------------------------------------------- 1 | BCID_H_rgb_rev 2 | -------------------------------------------------------------------------------- /src/web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/startup-sample-project-aws-containers/6687a834b4377f17a1753b772b438c777c6c178f/src/web/public/logo192.png -------------------------------------------------------------------------------- /src/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Sample App", 3 | "name": "Public Cloud Sample Application", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } -------------------------------------------------------------------------------- /src/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/web/src/App.css: -------------------------------------------------------------------------------- 1 | /* App.css */ 2 | 3 | :root { 4 | --font-family: "BCSans"; 5 | --primary-color: #014166; 6 | background-color: #f2f2f2; 7 | } 8 | 9 | .App { 10 | margin-top: 8rem; 11 | } 12 | 13 | .container { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | gap: var(--padding); 18 | padding: 1rem; 19 | width: 50%; 20 | margin: 2% auto; 21 | background-color: white; 22 | } 23 | 24 | .pagination { 25 | display: flex; 26 | } 27 | 28 | body { 29 | font-family: "BCSans", sans-serif; 30 | gap: var(--padding); 31 | padding: var(--padding); 32 | } 33 | 34 | h1, 35 | h2 { 36 | color: var(--primary-color); 37 | } 38 | 39 | h3 { 40 | border-bottom: 2px solid #fcba19; 41 | } 42 | 43 | a { 44 | color: var(--primary-color); 45 | } 46 | 47 | p { 48 | max-width: 60ch; 49 | } 50 | 51 | li { 52 | list-style-type: none; 53 | background-color: #f2f2f2; 54 | padding: 0.5rem 1rem; 55 | border-radius: 0.25rem; /* round corners */ 56 | margin: 2% auto; 57 | max-width: 60ch; 58 | } 59 | 60 | button { 61 | margin-right: 10px; 62 | } 63 | 64 | button:last-child { 65 | margin-right: 0; 66 | } 67 | 68 | .bc-gov-form { 69 | display: flex; 70 | flex-direction: column; 71 | align-items: flex-start; 72 | } 73 | 74 | .bc-gov-dropdown-label { 75 | margin-bottom: 10px; 76 | } 77 | 78 | .bc-gov-dropdown { 79 | font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; 80 | font-size: 1rem; 81 | color: #313132; 82 | background: white; 83 | box-shadow: none; 84 | border: 2px solid #606060; 85 | min-width: 200px; 86 | padding: 8px 45px 8px 15px; 87 | -webkit-appearance: none; 88 | -moz-appearance: none; 89 | appearance: none; 90 | } 91 | 92 | .fa-chevron-down { 93 | pointer-events: none; 94 | position: absolute; 95 | top: calc(1em - 4px); 96 | right: 1em; 97 | color: #003366; 98 | } 99 | 100 | .bc-gov-dropdown-wrapper { 101 | position: relative; 102 | display: inline; 103 | } 104 | 105 | :focus { 106 | outline: 4px solid #3b99fc; 107 | outline-offset: 1px; 108 | } 109 | 110 | .BC-Gov-PrimaryButton { 111 | background-color: #003366; 112 | border: none; 113 | border-radius: 4px; 114 | color: white; 115 | padding: 12px 32px; 116 | text-align: center; 117 | text-decoration: none; 118 | display: block; 119 | font-size: 18px; 120 | font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; 121 | font-weight: 700; 122 | letter-spacing: 1px; 123 | cursor: pointer; 124 | } 125 | 126 | .BC-Gov-PrimaryButton:hover { 127 | text-decoration: underline; 128 | opacity: 0.8; 129 | } 130 | 131 | :focus { 132 | outline: 4px solid #3b99fc; 133 | outline-offset: 1px; 134 | } 135 | 136 | .BC-Gov-PrimaryButton:active { 137 | opacity: 1; 138 | } 139 | 140 | .BC-Gov-PrimaryButton-disabled { 141 | background-color: #003366; 142 | opacity: 0.3; 143 | border: none; 144 | border-radius: 4px; 145 | color: white; 146 | padding: 12px 32px; 147 | text-align: center; 148 | text-decoration: none; 149 | display: block; 150 | font-size: 18px; 151 | font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; 152 | font-weight: 700; 153 | letter-spacing: 1px; 154 | cursor: not-allowed; 155 | } 156 | 157 | .BC-Gov-SecondaryButton { 158 | background: none; 159 | border-radius: 4px; 160 | border: 2px solid #003366; 161 | padding: 10px 30px; 162 | text-align: center; 163 | text-decoration: none; 164 | display: block; 165 | font-size: 18px; 166 | font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; 167 | font-weight: 700; 168 | letter-spacing: 1px; 169 | cursor: pointer; 170 | color: #003366; 171 | } 172 | 173 | .BC-Gov-SecondaryButton:hover { 174 | opacity: 0.8; 175 | text-decoration: underline; 176 | background-color: #003366; 177 | color: #ffffff; 178 | } 179 | 180 | :focus { 181 | outline-offset: 1px; 182 | outline: 4px solid #3b99fc; 183 | } 184 | 185 | .BC-Gov-SecondaryButton:active { 186 | opacity: 1; 187 | } 188 | 189 | .BC-Gov-SecondaryButton-disabled { 190 | background-color: white; 191 | opacity: 0.3; 192 | border: 2px solid #003366; 193 | border-radius: 4px; 194 | color: #003366; 195 | padding: 10px 30px; 196 | text-align: center; 197 | text-decoration: none; 198 | display: block; 199 | font-size: 18px; 200 | font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; 201 | font-weight: 700; 202 | letter-spacing: 1px; 203 | cursor: not-allowed; 204 | } 205 | -------------------------------------------------------------------------------- /src/web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "@bcgov/bc-sans/css/BCSans.css"; 3 | import "./App.css"; 4 | import GreetingProvider from "./components/GreetingProvider"; 5 | import GreetingList from "./components/GreetingList"; 6 | import GreetingSelector from "./components/GreetingSelector"; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/web/src/components/GreetingContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const GreetingContext = createContext({ 4 | greetingItems: [], 5 | setGreetingItems: () => {}, 6 | page: 1, 7 | setPage: () => {}, 8 | }); 9 | 10 | export default GreetingContext; 11 | -------------------------------------------------------------------------------- /src/web/src/components/GreetingList.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import GreetingContext from "./GreetingContext"; 3 | 4 | const PAGE_SIZE = 10; 5 | 6 | const GreetingList = () => { 7 | const { greetingItems, page, setPage } = useContext(GreetingContext); 8 | 9 | if (!greetingItems || !Array.isArray(greetingItems)) { 10 | return
No greetings available.
; 11 | } 12 | 13 | const sortedGreetings = [...greetingItems].sort( 14 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt) 15 | ); 16 | 17 | const startIndex = (page - 1) * PAGE_SIZE; 18 | const endIndex = startIndex + PAGE_SIZE; 19 | const pageGreetings = sortedGreetings.slice(startIndex, endIndex); 20 | 21 | const totalPages = Math.ceil(sortedGreetings.length / PAGE_SIZE); 22 | 23 | const handlePrevPage = () => { 24 | if (page > 1) { 25 | setPage(page - 1); 26 | } 27 | }; 28 | 29 | const handleNextPage = () => { 30 | if (page < totalPages) { 31 | setPage(page + 1); 32 | } 33 | }; 34 | 35 | return ( 36 |
37 |

Previous greeting selections

38 |
    39 | {pageGreetings.map((item) => ( 40 |
  • 41 | {new Date(item.createdAt).toLocaleString()} - 42 | {item.greeting} 43 |
  • 44 | ))} 45 |
46 |
47 | 50 | 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default GreetingList; 59 | -------------------------------------------------------------------------------- /src/web/src/components/GreetingProvider.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import GreetingContext from "./GreetingContext"; 3 | import axios from "axios"; 4 | import { API_BASE_URL } from "../config"; 5 | 6 | const GreetingProvider = ({ children }) => { 7 | const [greetingItems, setGreetingItems] = useState([]); 8 | const [page, setPage] = useState(1); 9 | 10 | useEffect(() => { 11 | // Fetch greetings when component mounts 12 | axios 13 | .get(`${API_BASE_URL}/api/v1/greeting/latest`) 14 | .then((response) => { 15 | const items = response.data.greetingItems; 16 | if (Array.isArray(items)) { 17 | setGreetingItems(items); 18 | } else { 19 | console.error("Unexpected data format from API:", response.data); 20 | } 21 | }) 22 | .catch((error) => { 23 | console.error("Error fetching greetings:", error); 24 | }); 25 | }, []); 26 | 27 | return ( 28 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export default GreetingProvider; 37 | -------------------------------------------------------------------------------- /src/web/src/components/GreetingSelector.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import GreetingContext from "./GreetingContext"; 3 | import axios from "axios"; 4 | import { API_BASE_URL } from "../config"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; 7 | 8 | const GreetingSelector = () => { 9 | const [selectedGreeting, setSelectedGreeting] = useState(""); 10 | const [isLoading, setIsLoading] = useState(false); 11 | const { greetingItems, setGreetingItems, setPage } = 12 | useContext(GreetingContext); 13 | 14 | const handleGreetingChange = (event) => { 15 | setSelectedGreeting(event.target.value); 16 | }; 17 | 18 | const handleSendGreeting = () => { 19 | setIsLoading(true); 20 | setPage(1); 21 | axios 22 | .post(`${API_BASE_URL}/api/v1/greeting`, { greeting: selectedGreeting }) 23 | .then((response) => { 24 | const newGreetingItem = response.data; 25 | setGreetingItems([newGreetingItem, ...greetingItems]); 26 | setSelectedGreeting(""); 27 | }) 28 | .catch((error) => { 29 | console.error("Error sending greeting:", error); 30 | }) 31 | .finally(() => { 32 | setIsLoading(false); 33 | }); 34 | }; 35 | 36 | return ( 37 |
38 |

Select your favorite greeting

39 |
40 | 41 | 58 |
59 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default GreetingSelector; 77 | -------------------------------------------------------------------------------- /src/web/src/components/Header.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | color: #fff; 3 | background-color: #036; 4 | border-bottom: 2px solid #fcba19; 5 | top: 0px; 6 | position: fixed; 7 | z-index: 100; 8 | width: 100%; 9 | display: flex; 10 | justify-content: flex-start; 11 | align-items: center; 12 | margin: 0 10px 0 0; 13 | /* border-style: dotted; 14 | border-width: 1px; 15 | border-color: lightgrey; */ 16 | } 17 | 18 | .banner img { 19 | width: 100%; 20 | height: auto; 21 | } 22 | 23 | .other { 24 | display: flex; 25 | flex-grow: 1; 26 | /* border-style: dotted; 27 | border-width: 1px; 28 | border-color: lightgrey; */ 29 | } 30 | 31 | h1 { 32 | font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; 33 | font-weight: normal; /* 400 */ 34 | margin: 5px 5px 0 18px; 35 | visibility: hidden; 36 | color: #fff; 37 | } 38 | 39 | /* 40 | These are sample media queries only. Media queries are quite subjective 41 | but, in general, should be made for the three different classes of screen 42 | size: phone, tablet, full. 43 | */ 44 | 45 | @media screen and (min-width: 600px) and (max-width: 899px) { 46 | h1 { 47 | font-size: calc(7px + 2.2vw); 48 | visibility: visible; 49 | } 50 | } 51 | 52 | @media screen and (min-width: 900px) { 53 | h1 { 54 | font-size: 2em; 55 | visibility: visible; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/web/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "@bcgov/bc-sans/css/BCSans.css"; 3 | import "./Header.css"; 4 | 5 | function App() { 6 | return ( 7 |
8 | 17 |
 
18 |
19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/web/src/config.js: -------------------------------------------------------------------------------- 1 | const API_BASE_URL = 2 | process.env.REACT_APP_API_BASE_URL || "http://localhost:3001"; 3 | 4 | export { API_BASE_URL }; 5 | -------------------------------------------------------------------------------- /src/web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import Header from "./components/Header"; 6 | import reportWebVitals from "./reportWebVitals"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 |
12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /src/web/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | --------------------------------------------------------------------------------