diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..89e6114 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,35 @@ +# Find the Dockerfile for mcr.microsoft.com/azure-functions/dotnet:3.0-dotnet3-core-tools at this URL +# https://github.com/Azure/azure-functions-docker/blob/main/host/3.0/buster/amd64/dotnet/dotnet-core-tools.Dockerfile +FROM mcr.microsoft.com/azure-functions/dotnet:4-dotnet6-core-tools + +# Uncomment following lines If you want to enable Development Container Script +# For more details https://github.com/microsoft/vscode-dev-containers/tree/main/script-library + +# Avoid warnings by switching to noninteractive +# ENV DEBIAN_FRONTEND=noninteractive + +# # Comment out these lines if you want to use zsh. + +ARG INSTALL_ZSH=false +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN apt-get update && curl -ssL https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/script-library/common-debian.sh -o /tmp/common-script.sh \ + && /bin/bash /tmp/common-script.sh "$INSTALL_ZSH" "$USERNAME" "$USER_UID" "$USER_GID" \ + && rm /tmp/common-script.sh + +# Setup .NET 5 & 6 SDKs +RUN wget https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +RUN dpkg -i packages-microsoft-prod.deb +RUN rm packages-microsoft-prod.deb +RUN apt-get update \ + && apt-get install -y apt-transport-https \ + && apt-get update \ + && apt-get install -y dotnet-sdk-5.0 \ + && apt-get install -y dotnet-sdk-6.0 + +# Install Chrome for Desktop Testing through VNC +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && curl -sSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o /tmp/chrome.deb \ + && apt-get -y install /tmp/chrome.deb \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f34aad8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,42 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/azure-functions-dotnetcore-3.1 +{ + "name": "TwoWeeksReady", + "dockerFile": "Dockerfile", + "forwardPorts": [ 7071, 6080, 5901 ], + + // Set *default* container specific settings.json values on container create. + "settings": {}, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp", + "azurite.azurite", + //"ms-azuretools.vscode-cosmosdb" + // unable to get preview extension working through devcontainer emulator due to certificate error, but works with deprecated feature in Azure Storage Explorer + ], + + "features": { + "docker-in-docker": { + "version": "latest", + "moby": true + }, + "node": { + "version": "lts", + "nodeGypDependencies": true + }, + "desktop-lite": { + "password": "vscode", + "webPort": "6080", + "vncPort": "5901" + }, + "github-cli": "latest" + }, + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "bash -i tools/first-build.sh", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/library-scripts/docker-in-docker.sh b/.devcontainer/library-scripts/docker-in-docker.sh new file mode 100644 index 0000000..904fe0d --- /dev/null +++ b/.devcontainer/library-scripts/docker-in-docker.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby] [Engine/CLI Version] + +ENABLE_NONROOT_DOCKER=${1:-"true"} +USERNAME=${2:-"automatic"} +USE_MOBY=${3:-"true"} +DOCKER_VERSION=${4:-"latest"} # The Docker/Moby Engine + CLI should match in version +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +DOCKER_DASH_COMPOSE_VERSION="1" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + 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." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates lxc pigz iptables gnupg2 dirmngr +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install git +fi + +# Swap to legacy iptables for compatibility +if type iptables-legacy > /dev/null 2>&1; then + update-alternatives --set iptables /usr/sbin/iptables-legacy + update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +fi + +# Source /etc/os-release to get OS info +. /etc/os-release +# Fetch host/container arch. +architecture="$(dpkg --print-architecture)" + +# Set up the necessary apt repos (either Microsoft's or Docker's) +if [ "${USE_MOBY}" = "true" ]; then + + # Name of open source engine/cli + engine_package_name="moby-engine" + cli_package_name="moby-cli" + + # Import key safely and import Microsoft apt repo + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + 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 +else + # Name of licensed engine/cli + engine_package_name="docker-ce" + cli_package_name="docker-ce-cli" + + # Import key safely and import Docker apt repo + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg + 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 +fi + +# Refresh apt lists +apt-get update + +# Soft version matching +if [ "${DOCKER_VERSION}" = "latest" ] || [ "${DOCKER_VERSION}" = "lts" ] || [ "${DOCKER_VERSION}" = "stable" ]; then + # Empty, meaning grab whatever "latest" is in apt repo + engine_version_suffix="" + cli_version_suffix="" +else + # Fetch a valid version from the apt-cache (eg: the Microsoft repo appends +azure, breakfix, etc...) + docker_version_dot_escaped="${DOCKER_VERSION//./\\.}" + docker_version_dot_plus_escaped="${docker_version_dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + docker_version_regex="^(.+:)?${docker_version_dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - will handle gracefully + cli_version_suffix="=$(apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + engine_version_suffix="=$(apt-cache madison ${engine_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + set -e + if [ -z "${engine_version_suffix}" ] || [ "${engine_version_suffix}" = "=" ] || [ -z "${cli_version_suffix}" ] || [ "${cli_version_suffix}" = "=" ] ; then + echo "(!) No full or partial Docker / Moby version match found for \"${DOCKER_VERSION}\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:" + apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 + fi + echo "engine_version_suffix ${engine_version_suffix}" + echo "cli_version_suffix ${cli_version_suffix}" +fi + +# Install Docker / Moby CLI if not already installed +if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then + echo "Docker / Moby CLI and Engine already installed." +else + if [ "${USE_MOBY}" = "true" ]; then + apt-get -y install --no-install-recommends moby-cli${cli_version_suffix} moby-buildx moby-engine${engine_version_suffix} + 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." + else + apt-get -y install --no-install-recommends docker-ce-cli${cli_version_suffix} docker-ce${engine_version_suffix} + fi +fi + +echo "Finished installing docker / moby!" + +# Install Docker Compose if not already installed and is on a supported architecture +if type docker-compose > /dev/null 2>&1; then + echo "Docker Compose already installed." +else + target_compose_arch="${architecture}" + if [ "${target_compose_arch}" = "amd64" ]; then + target_compose_arch="x86_64" + fi + if [ "${target_compose_arch}" != "x86_64" ]; then + # Use pip to get a version that runns on this architecture + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PYTHONUSERBASE=/tmp/pip-tmp + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx_bin=pipx + if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx + pipx_bin=/tmp/pip-tmp/bin/pipx + fi + ${pipx_bin} install --pip-args '--no-cache-dir --force-reinstall' docker-compose + rm -rf /tmp/pip-tmp + else + # Only supports docker-compose v1 + find_version_from_git_tags DOCKER_DASH_COMPOSE_VERSION "https://github.com/docker/compose" "tags/" + echo "(*) Installing docker-compose ${DOCKER_DASH_COMPOSE_VERSION}..." + curl -fsSL "https://github.com/docker/compose/releases/download/${DOCKER_DASH_COMPOSE_VERSION}/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + fi +fi + +# If init file already exists, exit +if [ -f "/usr/local/share/docker-init.sh" ]; then + echo "/usr/local/share/docker-init.sh already exists, so exiting." + exit 0 +fi +echo "docker-init doesnt exist, adding..." + +# Add user to the docker group +if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then + if ! getent group docker > /dev/null 2>&1; then + groupadd docker + fi + + usermod -aG docker ${USERNAME} +fi + +tee /usr/local/share/docker-init.sh > /dev/null \ +<< 'EOF' +#!/bin/sh +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +set -e + +dockerd_start="$(cat << 'INNEREOF' + # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly + # ie: docker kill + find /run /var/run -iname 'docker*.pid' -delete || : + find /run /var/run -iname 'container*.pid' -delete || : + + ## Dind wrapper script from docker team, adapted to a function + # Maintained: https://github.com/moby/moby/blob/master/hack/dind + + export container=docker + + if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then + mount -t securityfs none /sys/kernel/security || { + echo >&2 'Could not mount /sys/kernel/security.' + echo >&2 'AppArmor detection and --privileged mode might break.' + } + fi + + # Mount /tmp (conditionally) + if ! mountpoint -q /tmp; then + mount -t tmpfs none /tmp + fi + + # cgroup v2: enable nesting + if [ -f /sys/fs/cgroup/cgroup.controllers ]; then + # move the processes from the root group to the /init group, + # otherwise writing subtree_control fails with EBUSY. + # An error during moving non-existent process (i.e., "cat") is ignored. + mkdir -p /sys/fs/cgroup/init + xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || : + # enable controllers + sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ + > /sys/fs/cgroup/cgroup.subtree_control + fi + ## Dind wrapper over. + + # Handle DNS + set +e + cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' + if [ $? -eq 0 ] + then + echo "Setting dockerd Azure DNS." + CUSTOMDNS="--dns 168.63.129.16" + else + echo "Not setting dockerd DNS manually." + CUSTOMDNS="" + fi + set -e + + # Start docker/moby engine + ( dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) & +INNEREOF +)" + +# Start using sudo if not invoked as root +if [ "$(id -u)" -ne 0 ]; then + sudo /bin/sh -c "${dockerd_start}" +else + eval "${dockerd_start}" +fi + +set +e + +# Execute whatever commands were passed in (if any). This allows us +# to set this script to ENTRYPOINT while still executing the default CMD. +exec "$@" +EOF + +chmod +x /usr/local/share/docker-init.sh +chown ${USERNAME}:root /usr/local/share/docker-init.sh \ No newline at end of file diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh new file mode 100644 index 0000000..5394a8f --- /dev/null +++ b/.devcontainer/library-scripts/node-debian.sh @@ -0,0 +1,169 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] + +export NVM_DIR=${1:-"/usr/local/share/nvm"} +export NODE_VERSION=${2:-"lts"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" +export NVM_VERSION="0.38.0" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr + +# Install yarn +if type yarn > /dev/null 2>&1; then + echo "Yarn already installed." +else + # Import key safely (new method rather than deprecated apt-key approach) and install + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list + apt-get update + apt-get -y install --no-install-recommends yarn +fi + +# Adjust node version if required +if [ "${NODE_VERSION}" = "none" ]; then + export NODE_VERSION= +elif [ "${NODE_VERSION}" = "lts" ]; then + export NODE_VERSION="lts/*" +fi + +# Create a symlink to the installed version for use in Dockerfile PATH statements +export NVM_SYMLINK_CURRENT=true + +# Install the specified node version if NVM directory already exists, then exit +if [ -d "${NVM_DIR}" ]; then + echo "NVM already installed." + if [ "${NODE_VERSION}" != "" ]; then + su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" + fi + exit 0 +fi + +# Create nvm group, nvm dir, and set sticky bit +if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then + groupadd -r nvm +fi +umask 0002 +usermod -a -G nvm ${USERNAME} +mkdir -p ${NVM_DIR} +chown :nvm ${NVM_DIR} +chmod g+s ${NVM_DIR} +su ${USERNAME} -c "$(cat << EOF + set -e + umask 0002 + # Do not update profile - we'll do this manually + export PROFILE=/dev/null + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash + source ${NVM_DIR}/nvm.sh + if [ "${NODE_VERSION}" != "" ]; then + nvm alias default ${NODE_VERSION} + fi + nvm clear-cache +EOF +)" 2>&1 +# Update rc files +if [ "${UPDATE_RC}" = "true" ]; then +updaterc "$(cat < /dev/null 2>&1; then + to_install="${to_install} make" + fi + if ! type gcc > /dev/null 2>&1; then + to_install="${to_install} gcc" + fi + if ! type g++ > /dev/null 2>&1; then + to_install="${to_install} g++" + fi + if ! type python3 > /dev/null 2>&1; then + to_install="${to_install} python3-minimal" + fi + if [ ! -z "${to_install}" ]; then + apt_get_update_if_needed + apt-get -y install ${to_install} + fi +fi + +echo "Done!" \ No newline at end of file diff --git a/.github/workflows/2wr-app.yaml b/.github/workflows/2wr-app.yaml index 8e2702c..985bfc9 100644 --- a/.github/workflows/2wr-app.yaml +++ b/.github/workflows/2wr-app.yaml @@ -5,12 +5,12 @@ on: paths: - '2wr-app/**' - '.github/workflows/2wr-app.yaml' - branches: [ master ] + branches: [ main ] pull_request: paths: - '2wr-app/**' - '.github/workflows/2wr-app.yaml' - branches: [ master ] + branches: [ main ] workflow_dispatch: jobs: @@ -20,7 +20,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [16.x] steps: - uses: actions/checkout@v2 @@ -30,6 +30,20 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci working-directory: ./2wr-app + - run: npx playwright install chromium --with-deps + working-directory: ./2wr-app + - name: Write sample env file for e2e tests + run: 'cp ./2wr-app/.env-sample ./2wr-app/.env' + - run: npm run e2etest + working-directory: ./2wr-app + - name: Upload e2e Test Results + uses: actions/upload-artifact@v2 + if: failure() + with: + name: e2e-test-results + path: ./2wr-app/test-results + - name: Clear sample env file from e2e tests + run: 'rm ./2wr-app/.env' - name: Write env file run: 'echo "$DEV_ENV" > ./2wr-app/.env' shell: bash diff --git a/.github/workflows/admin.yaml b/.github/workflows/admin.yaml index 28c9fae..eb1c561 100644 --- a/.github/workflows/admin.yaml +++ b/.github/workflows/admin.yaml @@ -7,13 +7,13 @@ on: - '2wr-app/public/images/**' - 'TwoWeeksReady.Common/**' - '.github/workflows/admin.yaml' - branches: [ master ] + branches: [ main ] pull_request: paths: - 'admin/**' - '2wr-app/public/images/**' - '.github/workflows/admin.yaml' - branches: [ master ] + branches: [ main ] workflow_dispatch: jobs: build: @@ -23,11 +23,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x - - name: Setup .NET 5 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Build run: dotnet build -c Release working-directory: ./admin @@ -36,12 +32,12 @@ jobs: working-directory: ./admin - name: Zip Functions Package run: zip -r ../deploy.zip ./ - working-directory: ./admin/TwoWeeksReady.Admin/bin/Release/net5.0/publish/ + working-directory: ./admin/TwoWeeksReady.Admin/bin/Release/net6.0/publish/ - name: Upload Deployment Zip uses: actions/upload-artifact@v2 with: name: deployment_zip - path: ./admin/TwoWeeksReady.Admin/bin/Release/net5.0/deploy.zip + path: ./admin/TwoWeeksReady.Admin/bin/Release/net6.0/deploy.zip - name: Upload Deployment Script uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index 2d69bd8..b5593e7 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -6,12 +6,12 @@ on: - 'api/**' - 'TwoWeeksReady.Common/**' - '.github/workflows/api.yaml' - branches: [ master ] + branches: [ main ] pull_request: paths: - 'api/**' - '.github/workflows/api.yaml' - branches: [ master ] + branches: [ main ] workflow_dispatch: jobs: build: @@ -21,7 +21,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: 6.0.x - name: Build run: dotnet build -c Release working-directory: ./api @@ -30,12 +30,12 @@ jobs: working-directory: ./api - name: Zip Functions Package run: zip -r ../deploy.zip ./ - working-directory: ./api/TwoWeeksReady/bin/Release/netcoreapp3.1/publish/ + working-directory: ./api/TwoWeeksReady/bin/Release/net6.0/publish/ - name: Upload Deployment Zip uses: actions/upload-artifact@v2 with: name: deployment_zip - path: ./api/TwoWeeksReady/bin/Release/netcoreapp3.1/deploy.zip + path: ./api/TwoWeeksReady/bin/Release/net6.0/deploy.zip - name: Upload Deployment Script uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index cf346a0..02b7d96 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ *.userosscache *.sln.docstates +# Azurite files +__azurite_*.json +__blobstorage__ + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cf8e0c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "azureFunctions.projectSubpath": "api/TwoWeeksReady", + "azureFunctions.deploySubpath": "api/TwoWeeksReady/bin/Release/net6.0/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~4", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "publish (functions)" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9660683..fbefe0e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,7 @@ { "version": "2.0.0", "tasks": [ + // TwoWeeksReady.Admin tasks { "label": "build", "command": "dotnet", @@ -37,6 +38,72 @@ "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" + }, + // CosmoDB Emulator tasks + { + "label": "Start CosmoDB Emulator (Docker)", + "command": ". ./tools/CosmosEmulator/start-emulator.sh", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Stop CosmoDB Emulator (Docker)", + "command": ". ./tools/CosmosEmulator/stop-emulator.sh", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": true, + "problemMatcher": [] + }, + // Azure Functions (API) Tasks + { + "label": "API (Clean)", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/api/TwoWeeksReady" + } + }, + { + "label": "API (Build)", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "API (Clean)", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/api/TwoWeeksReady" + } + }, + { + "label": "API (Run)", + "type": "func", + "dependsOn": "API (Build)", + "options": { + "cwd": "${workspaceFolder}/api/TwoWeeksReady/bin/Debug/net6.0", + "languageWorkers__node__arguments": "--inspect=5858" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" } ] -} \ No newline at end of file +} diff --git a/2wr-app/.gitignore b/2wr-app/.gitignore index 0035d70..de9d1d3 100644 --- a/2wr-app/.gitignore +++ b/2wr-app/.gitignore @@ -8,6 +8,9 @@ node_modules .env.local .env.*.local +#test results +/test-results + # Log files npm-debug.log* yarn-debug.log* diff --git a/2wr-app/README.md b/2wr-app/README.md index 2d5ff32..3895b4a 100644 --- a/2wr-app/README.md +++ b/2wr-app/README.md @@ -4,7 +4,7 @@ ## Application wireframes -See [Application wireframes](https://xd.adobe.com/view/158c8bc4-5ef2-47dd-90c0-ba3c508e4d62-40df) +See [Application wireframes](https://xd.adobe.com/view/158c8bc4-5ef2-47dd-90c0-ba3c508e4d62-40df/grid) ## Material design icons @@ -18,7 +18,7 @@ In VSCode, you can get a walk-through of the code via the [CodeTour](https://mar ## Project Prerequisites -1. Node.js 14.17.6 LTS +1. Node.js 16.x.x LTS ## Project setup diff --git a/2wr-app/package-lock.json b/2wr-app/package-lock.json index 272891f..fa37555 100644 --- a/2wr-app/package-lock.json +++ b/2wr-app/package-lock.json @@ -3463,9 +3463,9 @@ } }, "node_modules/async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "dependencies": { "lodash": "^4.17.14" @@ -13853,9 +13853,9 @@ } }, "node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "dev": true }, "node_modules/shelljs": { @@ -20167,9 +20167,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -28490,9 +28490,9 @@ "dev": true }, "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "dev": true }, "shelljs": { diff --git a/2wr-app/playwright.config.js b/2wr-app/playwright.config.js index 24181b1..a04ed48 100644 --- a/2wr-app/playwright.config.js +++ b/2wr-app/playwright.config.js @@ -8,5 +8,8 @@ const config = { port: 8080, timeout: 120 * 1000, }, + use: { + screenshot: 'only-on-failure', + } }; module.exports = config; \ No newline at end of file diff --git a/2wr-app/public/images/kits/build-a-kit.png b/2wr-app/public/images/kits/build-a-kit.png new file mode 100644 index 0000000..6ea7366 Binary files /dev/null and b/2wr-app/public/images/kits/build-a-kit.png differ diff --git a/2wr-app/public/images/kits/go-kit.png b/2wr-app/public/images/kits/go-kit.png new file mode 100644 index 0000000..c1ea374 Binary files /dev/null and b/2wr-app/public/images/kits/go-kit.png differ diff --git a/2wr-app/public/images/kits/work-kit.png b/2wr-app/public/images/kits/work-kit.png index dc736a5..7cebc59 100644 Binary files a/2wr-app/public/images/kits/work-kit.png and b/2wr-app/public/images/kits/work-kit.png differ diff --git a/2wr-app/src/api/emergency-kit-api.js b/2wr-app/src/api/emergency-kit-api.js index 80834b3..7b21740 100644 --- a/2wr-app/src/api/emergency-kit-api.js +++ b/2wr-app/src/api/emergency-kit-api.js @@ -1,8 +1,8 @@ import baseApiInstance from './base-api-instance'; const emergencyKitApi = { - async getAll() { - return (await baseApiInstance.getInstance()).get('emergencykits'); + async getAllByBaseKitId(baseKitId) { + return (await baseApiInstance.getInstance()).get(`emergencykits/${baseKitId}`); }, async getById(id) { return (await baseApiInstance.getInstance()).get(`emergencykit-by-id/${id}`); diff --git a/2wr-app/src/api/hazard-hunt-api.js b/2wr-app/src/api/hazard-hunt-api.js index 7c661dc..a30e16e 100644 --- a/2wr-app/src/api/hazard-hunt-api.js +++ b/2wr-app/src/api/hazard-hunt-api.js @@ -4,4 +4,7 @@ export default { async getDocuments() { return (await baseApiInstance.getInstance()).get('hazardhunt-list'); }, + async get(id) { + return (await baseApiInstance.getInstance()).get(`hazardhunt-by-id/${id}`); + } }; \ No newline at end of file diff --git a/2wr-app/src/api/hazard-info-api.js b/2wr-app/src/api/hazard-info-api.js index 7317d9d..0f0fe6c 100644 --- a/2wr-app/src/api/hazard-info-api.js +++ b/2wr-app/src/api/hazard-info-api.js @@ -7,5 +7,5 @@ export default { async get(id) { return (await baseApiInstance.getInstance()).get(`hazardinfo-by-id/${id}`); - }, + } }; \ No newline at end of file diff --git a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-build.vue b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-build.vue new file mode 100644 index 0000000..1fa96bc --- /dev/null +++ b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-build.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-create.vue b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-create.vue deleted file mode 100644 index 2451dee..0000000 --- a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-create.vue +++ /dev/null @@ -1,366 +0,0 @@ - - - \ No newline at end of file diff --git a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-details.vue b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-details.vue new file mode 100644 index 0000000..a05ff24 --- /dev/null +++ b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-details.vue @@ -0,0 +1,317 @@ + + + \ No newline at end of file diff --git a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-edit.vue b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-edit.vue deleted file mode 100644 index 8f45dce..0000000 --- a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-edit.vue +++ /dev/null @@ -1,332 +0,0 @@ - - - \ No newline at end of file diff --git a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-list.vue b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-list.vue index 7b34f7e..555c6ac 100644 --- a/2wr-app/src/components/prepare/emergency-kits/emergency-kit-list.vue +++ b/2wr-app/src/components/prepare/emergency-kits/emergency-kit-list.vue @@ -2,62 +2,69 @@ mdi-arrow-left - mdi-medical-bag - Emergency Kit List - - - mdi-plus - - + Build a Kit + - - - - - + + mdi-plus + + Add Kit + + + + + + + + + + + + + No Kits Defined + + + + + + + \ No newline at end of file diff --git a/2wr-app/src/components/prepare/make-a-plan/make-a-plan.vue b/2wr-app/src/components/prepare/make-a-plan/make-a-plan.vue new file mode 100644 index 0000000..4954ba5 --- /dev/null +++ b/2wr-app/src/components/prepare/make-a-plan/make-a-plan.vue @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/2wr-app/src/components/prepare/prepare-landing.vue b/2wr-app/src/components/prepare/prepare-landing.vue index f402b5f..f3a5aca 100644 --- a/2wr-app/src/components/prepare/prepare-landing.vue +++ b/2wr-app/src/components/prepare/prepare-landing.vue @@ -26,7 +26,7 @@ - + diff --git a/2wr-app/src/router/index.js b/2wr-app/src/router/index.js index bec426e..a24107a 100644 --- a/2wr-app/src/router/index.js +++ b/2wr-app/src/router/index.js @@ -3,12 +3,14 @@ import VueRouter from 'vue-router'; import Prepare from '../views/prepare/prepare.vue'; import Welcome from '../views/welcome/welcome.vue'; +import MakeAPlan from '../views/prepare/make-a-plan/make-a-plan.vue'; +import EmergencyKitBuildPage from '../views/prepare/emergency-kits/emergency-kit-build.vue'; import EmergencyKitListing from '../views/prepare/emergency-kits/emergency-kit-listing.vue'; -import EmergencyKitCreatePage from '../views/prepare/emergency-kits/emergency-kit-create.vue'; -import EmergencyKitEditPage from '../views/prepare/emergency-kits/emergency-kit-edit.vue'; +import EmergencyKitDetailsPage from '../views/prepare/emergency-kits/emergency-kit-details.vue'; import HazardHuntListing from '../views/prepare/hazards/hazard-hunt-list-view.vue'; import HazardInfoListing from '../views/prepare/hazards/hazard-info-list-view.vue'; import HazardInfo from '../views/prepare/hazards/hazard-info-view.vue'; +import HazardHunt from '../views/prepare/hazards/hazard-hunt-view.vue'; import Recent from '../views/recent/recent.vue'; import Settings from '../views/settings/settings.vue'; @@ -35,25 +37,41 @@ const routes = [{ component: Prepare }, { - path: '/prepare/emergencykits', - name: 'emergencykits', - component: EmergencyKitListing, + path: '/prepare/makeaplan', + name: 'makeaplan', + component: MakeAPlan, meta: { requiresAuth: true } }, { - path: '/prepare/emergencykits/create', + path: '/prepare/emergencykits/build', + name: 'emergencykitsbuild', + component: EmergencyKitBuildPage, + meta: { + requiresAuth: true + } + }, + { + path: '/prepare/emergencykits/:baseKitId/create', name: 'emergencykitcreate', - component: EmergencyKitCreatePage, + component: EmergencyKitDetailsPage, + meta: { + requiresAuth: true + } + }, + { + path: '/prepare/emergencykits/:baseKitId', + name: 'emergencykits', + component: EmergencyKitListing, meta: { requiresAuth: true } }, { - path: '/prepare/emergencykits/edit/:id', + path: '/prepare/emergencykits/:baseKitId/edit/:id', name: 'emergencykitedit', - component: EmergencyKitEditPage, + component: EmergencyKitDetailsPage, meta: { requiresAuth: true } @@ -65,7 +83,16 @@ const routes = [{ meta: { requiresAuth: true } - }, { + }, + { + path: '/prepare/hazardhunt/:id', + name: 'hazardhuntdetails', + component: HazardHunt, + meta: { + requiresAuth: true + } + }, + { path: '/prepare/hazardinfo', name: 'hazardinfolist', component: HazardInfoListing, diff --git a/2wr-app/src/store/modules/prepare/emergency-kits/emergency-kit-store.js b/2wr-app/src/store/modules/prepare/emergency-kits/emergency-kit-store.js index 5714f97..0ffe922 100644 --- a/2wr-app/src/store/modules/prepare/emergency-kits/emergency-kit-store.js +++ b/2wr-app/src/store/modules/prepare/emergency-kits/emergency-kit-store.js @@ -55,13 +55,13 @@ const emergencyKitStore = { }, }, actions: { - async getEmergencyKitListAsync({ commit, rootState }) { + async getEmergencyKitListAsync({ commit, rootState }, baseKitId) { commit("setBusy", null, { root: true }); commit("setError", "", { root: true }); try { if (rootState.globalStore.online) { - let response = await emergencyKitApi.getAll(); + let response = await emergencyKitApi.getAllByBaseKitId(baseKitId); commit("SET_LIST", response.data); await localForage.setItem("getEmergencyKitListAsync", response.data); } else { diff --git a/2wr-app/src/store/modules/prepare/hazards/hazard-hunt-store.js b/2wr-app/src/store/modules/prepare/hazards/hazard-hunt-store.js index 20fdfd3..456cad6 100644 --- a/2wr-app/src/store/modules/prepare/hazards/hazard-hunt-store.js +++ b/2wr-app/src/store/modules/prepare/hazards/hazard-hunt-store.js @@ -3,16 +3,21 @@ import localForage from 'localforage'; const CACHE_KEY = 'HazardHunts'; const SET_LIST = 'SET_LIST'; +const SET_ITEM = 'SET_ITEM'; export default { namespaced: true, state: { - list: [] + list: [], + item: null }, mutations: { [SET_LIST](state, payload) { state.list = payload; }, + [SET_ITEM](state, payload) { + state.item = payload; + } }, actions: { async getHazardHuntsAsync({ commit, rootState }) { @@ -30,6 +35,31 @@ export default { } } + }, + async getHazardHuntAsync({ commit, rootState} , id) { + + try { + commit("setBusy", null, { root: true }); + commit("setError", "", { root: true }); + const itemCacheKey = `${CACHE_KEY}/${id}` + if (rootState.globalStore.online) { + let response = await api.get(id); + commit(SET_ITEM, response.data); + await localForage.setItem(itemCacheKey, response.data); + } else { + var data = await localForage.getItem(itemCacheKey) + if (data) { + console.log("Serving from cache"); + commit(SET_ITEM, data); + } else { + console.log("Offline without data"); + } + } + } catch { + commit("setError", "Could not load hazard hunt.", { root: true }); + } finally { + commit("clearBusy", null, { root: true }); + } } } }; \ No newline at end of file diff --git a/2wr-app/src/views/prepare/emergency-kits/emergency-kit-build.vue b/2wr-app/src/views/prepare/emergency-kits/emergency-kit-build.vue new file mode 100644 index 0000000..b1229e2 --- /dev/null +++ b/2wr-app/src/views/prepare/emergency-kits/emergency-kit-build.vue @@ -0,0 +1,14 @@ + + + diff --git a/2wr-app/src/views/prepare/emergency-kits/emergency-kit-create.vue b/2wr-app/src/views/prepare/emergency-kits/emergency-kit-create.vue deleted file mode 100644 index 91129a8..0000000 --- a/2wr-app/src/views/prepare/emergency-kits/emergency-kit-create.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/2wr-app/src/views/prepare/emergency-kits/emergency-kit-details.vue b/2wr-app/src/views/prepare/emergency-kits/emergency-kit-details.vue new file mode 100644 index 0000000..05b278f --- /dev/null +++ b/2wr-app/src/views/prepare/emergency-kits/emergency-kit-details.vue @@ -0,0 +1,14 @@ + + + diff --git a/2wr-app/src/views/prepare/emergency-kits/emergency-kit-edit.vue b/2wr-app/src/views/prepare/emergency-kits/emergency-kit-edit.vue deleted file mode 100644 index 349b213..0000000 --- a/2wr-app/src/views/prepare/emergency-kits/emergency-kit-edit.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/2wr-app/src/views/prepare/hazards/hazard-hunt-view.vue b/2wr-app/src/views/prepare/hazards/hazard-hunt-view.vue new file mode 100644 index 0000000..a8a9dfb --- /dev/null +++ b/2wr-app/src/views/prepare/hazards/hazard-hunt-view.vue @@ -0,0 +1,14 @@ + + + diff --git a/2wr-app/src/views/prepare/make-a-plan/make-a-plan.vue b/2wr-app/src/views/prepare/make-a-plan/make-a-plan.vue new file mode 100644 index 0000000..6241248 --- /dev/null +++ b/2wr-app/src/views/prepare/make-a-plan/make-a-plan.vue @@ -0,0 +1,14 @@ + + + diff --git a/2wr-app/tests/welcome.spec.js b/2wr-app/tests/welcome.spec.js index 5426f31..2c56029 100644 --- a/2wr-app/tests/welcome.spec.js +++ b/2wr-app/tests/welcome.spec.js @@ -9,7 +9,7 @@ test.describe('Welcome page accessibility test', () => { await injectAxe(page); }); - test('Check Accessability', async () => { + test('Check Accessibility', async () => { await checkA11y(page, null, { axeOptions: { diff --git a/README.md b/README.md index d0d5370..92d8f2c 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,27 @@ For many years, we’ve been talking about the importance of being prepared for **Use this application as a means to prepare to become two weeks ready.** -For additional details, questions, etc. You can reachout out to Pascal @schuback or Maximilian @MaximilianDixon on twitter and/or join our hackathon dev channel on slack (https://join.slack.com/share/zt-x4s24iua-M0eTipxEhx1LtQCwlvHErQ). (LINK posted 10/12 and expires 14 days after +For additional details, questions, etc. You can reach out out to Pascal @schuback or Richard @richcampbell on twitter and/or join our hackathon dev channel on slack (https://join.slack.com/share/enQtMzE4MjQ4MTUzMzM2Ny0yNWE1OGJlMGIwNDJmMTI5MTEyYTVmMDViNzBmMThjYzkyOTk3ZDQwNTgyZGU1YjI3ZTA3ZmEyZjU5MDU1MGRk). (LINK posted 3/5 and expires 14 days after) + +## Getting Started +This project is configured to work with DevContainers/Codespaces to get developers up and running as quickly as possible. For DevContainers, all you need is [Docker][2] installed and you should be good to go! For Codespaces, you will need a GitHub account wiht Codespaces enabled. + +To see a full demo on how to setup your DevContainer/Codespaces environment, check out [this video][1] from @davidwesst where he walks through setting up HTBox/TwoWeeksReady, executing the various development tasks, and provides an overview about DevContainers and Codespaces. + +To setup your own development environment from scratch or to install the dependendies locally, refer to the files [`.devcontainer/devcontainer.json`](https://github.com/HTBox/TwoWeeksReady/blob/main/.devcontainer/devcontainer.json) and [`.devcontainer/Dockerfile`](https://github.com/HTBox/TwoWeeksReady/blob/main/.devcontainer/devcontainer.json) to understand what tools are required. + +## Solution Architecture + +![Architecture of Two Weeks Ready](https://user-images.githubusercontent.com/2531875/168096742-0b29eee3-b3e1-4485-9d77-c095cb6a9f2e.png) + + +### Resources +- [Tutorial Video from @davidwesst][1] +- [Docker Desktop][2] +- [Developing with VSCode and DevContainers][3] +- [Developing with VSCode and GitHub Codespaces][4] + +[1]: https://www.youtube.com/watch?v=rYfsNBODfZc +[2]: https://www.docker.com/products/personal/ +[3]: https://code.visualstudio.com/docs/remote/containers#_quick-start-try-a-development-container +[4]: https://code.visualstudio.com/docs/remote/codespaces diff --git a/TwoWeeksReady.Common/EmergencyKits/BaseKit.cs b/TwoWeeksReady.Common/EmergencyKits/BaseKit.cs index 3e735b9..075a712 100644 --- a/TwoWeeksReady.Common/EmergencyKits/BaseKit.cs +++ b/TwoWeeksReady.Common/EmergencyKits/BaseKit.cs @@ -11,8 +11,11 @@ public class BaseKit [JsonProperty(PropertyName = "name")] public string Name { get; set; } - [JsonProperty(PropertyName = "icon")] - public string Icon { get; set; } // material icon + [JsonProperty(PropertyName = "description")] + public string Description { get; set; } + + [JsonProperty(PropertyName = "iconUrl")] + public string IconUrl { get; set; } [JsonProperty(PropertyName = "items")] public List Items { get; set; } = new List(); diff --git a/TwoWeeksReady.Common/EmergencyKits/BaseKitItem.cs b/TwoWeeksReady.Common/EmergencyKits/BaseKitItem.cs index 9b5e208..005f7aa 100644 --- a/TwoWeeksReady.Common/EmergencyKits/BaseKitItem.cs +++ b/TwoWeeksReady.Common/EmergencyKits/BaseKitItem.cs @@ -13,11 +13,14 @@ public class BaseKitItem [JsonProperty(PropertyName = "description")] public string Description { get; set; } - [JsonProperty(PropertyName = "photo")] - public string Photo { get; set; } // URL or byte[] ? + [JsonProperty(PropertyName = "quantityPerAdult")] + public int QuantityPerAdult { get; set; } //per adult + + [JsonProperty(PropertyName = "quantityPerChild")] + public int QuantityPerChild { get; set; } //per child - [JsonProperty(PropertyName = "quantityPerCount")] - public int QuantityPerCount { get; set; } //per person or pet, 14 days + [JsonProperty(PropertyName = "quantityPerPet")] + public int QuantityPerPet { get; set; } //per pet [JsonProperty(PropertyName = "quantityUnit")] public string QuantityUnit { get; set; } //measurement unit, 14 days diff --git a/TwoWeeksReady.Common/EmergencyKits/Kit.cs b/TwoWeeksReady.Common/EmergencyKits/Kit.cs index 65a0bfb..7560bd1 100644 --- a/TwoWeeksReady.Common/EmergencyKits/Kit.cs +++ b/TwoWeeksReady.Common/EmergencyKits/Kit.cs @@ -8,18 +8,15 @@ public class Kit [JsonProperty(PropertyName = "id")] public string Id { get; set; } + [JsonProperty(PropertyName = "baseKitId")] + public string BaseKitId { get; set; } + [JsonProperty(PropertyName = "userId")] public string UserId { get; set; } [JsonProperty(PropertyName = "name")] public string Name { get; set; } - [JsonProperty(PropertyName = "color")] - public string Color { get; set; } //hex color - - [JsonProperty(PropertyName = "icon")] - public string Icon { get; set; } //material design icon - [JsonProperty(PropertyName = "kitItems")] public List Items { get; set; } = new List(); } diff --git a/TwoWeeksReady.Common/EmergencyKits/KitItem.cs b/TwoWeeksReady.Common/EmergencyKits/KitItem.cs index 9407783..dc44885 100644 --- a/TwoWeeksReady.Common/EmergencyKits/KitItem.cs +++ b/TwoWeeksReady.Common/EmergencyKits/KitItem.cs @@ -7,6 +7,9 @@ public class KitItem [JsonProperty(PropertyName = "id")] public string Id { get; set; } + [JsonProperty(PropertyName = "baseKitItemId")] + public string BaseKitItemId { get; set; } + [JsonProperty(PropertyName = "userId")] public string UserId { get; set; } diff --git a/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor b/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor index cbcf971..669b6c7 100644 --- a/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor +++ b/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor @@ -1,62 +1,55 @@ @inject IRepository repository
- @Item.Name - - @if (IsEditMode) - { -
- Name:
- Description:
- Quantity Per Person:
- Quantity Unit (lbs, oz):
-
- - } - else - { -
-
@Item.Name
- @Item.Description -
- - } - +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
@code { - - public bool IsEditMode { get; set; } = false; - [Parameter] public BaseKitItem Item { get; set; } - - [Parameter] - public EventCallback OnEdit { get; set; } - - [Parameter] - public EventCallback OnSave { get; set; } - - public async Task Edit() - { - - IsEditMode = true; - await OnEdit.InvokeAsync(Item); - } - - - public async Task Save() - { - IsEditMode = false; - await repository.SaveBaseKitItem(Item); - await OnSave.InvokeAsync(Item); - - } } diff --git a/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs b/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs index 3507f5c..b35c0f9 100644 --- a/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs @@ -20,14 +20,15 @@ public FunctionsRepository(IHttpClientFactory httpClientFactory, TokenProvider t _tokenProvider = tokenProvider; this._httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenProvider.AccessToken); } - public Task> GetAllBaseKits() + + public async Task> GetAllBaseKits() { - throw new NotImplementedException(); + return await _httpClient.GetFromJsonAsync>("basekits"); } - public Task> GetAllHazardHunts() + public async Task> GetAllHazardHunts() { - throw new NotImplementedException(); + return await _httpClient.GetFromJsonAsync>("hazardhunt-list"); } public async Task> GetAllHazardInfos() @@ -35,14 +36,14 @@ public async Task> GetAllHazardInfos() return await _httpClient.GetFromJsonAsync>("hazardinfo-list"); } - public Task GetBaseKitById(string id) + public async Task GetBaseKitById(string id) { - throw new NotImplementedException(); + return await _httpClient.GetFromJsonAsync($"basekit-by-id/{id}"); } - public Task GetHazardHuntById(string id) + public async Task GetHazardHuntById(string id) { - throw new NotImplementedException(); + return await _httpClient.GetFromJsonAsync($"hazardhunt-by-id/{id}"); } public async Task GetHazardInfoById(string id) @@ -50,14 +51,30 @@ public async Task GetHazardInfoById(string id) return await _httpClient.GetFromJsonAsync($"hazardinfo-by-id/{id}"); } - public Task SaveBaseKitItem(BaseKitItem kit) + public async Task SaveBaseKit(BaseKit kit) { - throw new NotImplementedException(); + var response = await _httpClient.PutAsJsonAsync("basekit-update", kit); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + throw new Exception("Error saving base kit"); + } } - public Task SaveHazardHunt(HazardHunt hazardHunt) + public async Task SaveHazardHunt(HazardHunt hazardHunt) { - throw new NotImplementedException(); + var response = await _httpClient.PutAsJsonAsync("hazardhunt-update", hazardHunt); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + throw new Exception("Error saving hazard hunt"); + } } public async Task SaveHazardInfo(HazardInfo hazardInfo) @@ -66,13 +83,26 @@ public async Task SaveHazardInfo(HazardInfo hazardInfo) if (response.IsSuccessStatusCode) { return await response.Content.ReadFromJsonAsync(); - } + } else { throw new Exception("Error saving hazard info"); } } + public async Task CreateBaseKit(BaseKit kit) + { + var response = await _httpClient.PostAsJsonAsync("basekit-create", kit); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + throw new Exception("Error saving base kit"); + } + } + public async Task CreateHazardInfo(HazardInfo hazardInfo) { var response = await _httpClient.PostAsJsonAsync("hazardinfo-create", hazardInfo); @@ -85,5 +115,57 @@ public async Task CreateHazardInfo(HazardInfo hazardInfo) throw new Exception("Error saving hazard info"); } } + + public async Task CreateHazardHunt(HazardHunt hazardHunt) + { + var response = await _httpClient.PostAsJsonAsync("hazardhunt-create", hazardHunt); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + throw new Exception("Error saving hazard hunt"); + } + } + + public async Task DeleteBaseKit(string id) + { + var response = await _httpClient.DeleteAsync($"basekit-delete/{id}"); + if (response.IsSuccessStatusCode) + { + return true; + } + else + { + throw new Exception("Error deleting base kit"); + } + } + + public async Task DeleteHazardHunt(string id) + { + var response = await _httpClient.DeleteAsync($"hazardhunt-delete/{id}"); + if (response.IsSuccessStatusCode) + { + return true; + } + else + { + throw new Exception("Error deleting hazard hunt"); + } + } + + public async Task DeleteHazardInfo(string id) + { + var response = await _httpClient.DeleteAsync($"hazardinfo-delete/{id}"); + if (response.IsSuccessStatusCode) + { + return true; + } + else + { + throw new Exception("Error deleting hazard info"); + } + } } } diff --git a/admin/TwoWeeksReady.Admin/Data/IRepository.cs b/admin/TwoWeeksReady.Admin/Data/IRepository.cs index 210f445..f676162 100644 --- a/admin/TwoWeeksReady.Admin/Data/IRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/IRepository.cs @@ -5,27 +5,24 @@ namespace TwoWeeksReady.Admin.Data { public interface IRepository - { - - Task GetBaseKitById(string id); - - Task> GetAllBaseKits(); - - Task SaveBaseKitItem(BaseKitItem kit); - - Task> GetAllHazardHunts(); - - Task GetHazardHuntById(string id); - - Task SaveHazardHunt(HazardHunt hazardHunt); - - Task> GetAllHazardInfos(); - - Task GetHazardInfoById(string id); - - Task SaveHazardInfo(HazardInfo hazardInfo); - - Task CreateHazardInfo(HazardInfo hazardInfo); - } + { + Task> GetAllBaseKits(); + Task GetBaseKitById(string id); + Task CreateBaseKit(BaseKit kit); + Task SaveBaseKit(BaseKit kit); + Task DeleteBaseKit(string id); + + Task> GetAllHazardHunts(); + Task GetHazardHuntById(string id); + Task CreateHazardHunt(HazardHunt hazardHunt); + Task SaveHazardHunt(HazardHunt hazardHunt); + Task DeleteHazardHunt(string id); + + Task> GetAllHazardInfos(); + Task GetHazardInfoById(string id); + Task CreateHazardInfo(HazardInfo hazardInfo); + Task SaveHazardInfo(HazardInfo hazardInfo); + Task DeleteHazardInfo(string id); + } } diff --git a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs index ecd131e..e600d6d 100644 --- a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs @@ -5,111 +5,129 @@ namespace TwoWeeksReady.Admin.Data { - public class StubRepository : IRepository - { - - private List _BaseKits = new List - { - - new BaseKit { - Id="1234", - Name="Emergency Kit", - Icon="mdi-medical-bag", - Items = new List { - new BaseKitItem { - Id="1", - Name="Flashlight", - Description="You need a flashlight if the power is out", - Photo="", - QuantityUnit="Flashlights", - QuantityPerCount=1 - }, - new BaseKitItem { - Id="2", - Name="Cans Of Soup", - Description="You need to eat - nonperishable soup is easy to prepare", - Photo="", - QuantityUnit="Cans", - QuantityPerCount=14 - }, - } - } - - }; - - private List _HazardHunt = new List() - { - new HazardHunt - { - Id = "ABCDEFG", - Name = "Bookshelf Safety", - Description = "Strap the bookshelf to the wall" - } - }; - - private List _HazardInfo = new List() - { - new HazardInfo - { - Id = "BEGIN", - Name = "Earthquakes", - Description = "All about earthquakes" - } - }; - - public Task> GetAllBaseKits() - { - return Task.FromResult(_BaseKits.AsEnumerable()); - } - - public Task GetBaseKitById(string id) - { - return Task.FromResult(_BaseKits.FirstOrDefault(b => b.Id == id)); - } - - public Task SaveBaseKitItem(BaseKitItem kitItem) - { - return Task.FromResult(kitItem); + public class StubRepository : IRepository + { + + private List _BaseKits = new List + { + + new BaseKit { + Id="1234", + Name="Emergency Kit", + IconUrl="", + Items = new List { + new BaseKitItem { + Id="1", + Name="Flashlight", + Description="You need a flashlight if the power is out", + QuantityUnit="Flashlights", + QuantityPerAdult=1 + }, + new BaseKitItem { + Id="2", + Name="Cans Of Soup", + Description="You need to eat - nonperishable soup is easy to prepare", + QuantityUnit="Cans", + QuantityPerAdult=14 + }, + } + } + + }; + + private List _HazardHunt = new List() + { + new HazardHunt + { + Id = "ABCDEFG", + Name = "Bookshelf Safety", + Description = "Strap the bookshelf to the wall" + } + }; + + private List _HazardInfo = new List() + { + new HazardInfo + { + Id = "BEGIN", + Name = "Earthquakes", + Description = "All about earthquakes" + } + }; + + public Task> GetAllBaseKits() + { + return Task.FromResult(_BaseKits.AsEnumerable()); } + public Task GetBaseKitById(string id) + { + return Task.FromResult(_BaseKits.FirstOrDefault(b => b.Id == id)); + } - public Task> GetAllHazardHunts() + public Task SaveBaseKit(BaseKit kit) { - return Task.FromResult(_HazardHunt.AsEnumerable()); + return Task.FromResult(kit); } - public Task GetHazardHuntById(string id) - { - return Task.FromResult(_HazardHunt.FirstOrDefault(h => h.Id == id)); - } + public Task CreateBaseKit(BaseKit kit) + { + return Task.FromResult(kit); + } + + public Task DeleteBaseKit(string id) + { + return Task.FromResult(true); + } + public Task> GetAllHazardHunts() + { + return Task.FromResult(_HazardHunt.AsEnumerable()); + } + + public Task GetHazardHuntById(string id) + { + return Task.FromResult(_HazardHunt.FirstOrDefault(h => h.Id == id)); + } - public Task SaveHazardHunt(HazardHunt hazardHunt) - { - return Task.FromResult(hazardHunt); - } + public Task SaveHazardHunt(HazardHunt hazardHunt) + { + return Task.FromResult(hazardHunt); + } - public Task> GetAllHazardInfos() - { - return Task.FromResult(_HazardInfo.AsEnumerable()); - } + public Task CreateHazardHunt(HazardHunt hazardHunt) + { + return Task.FromResult(hazardHunt); + } - public Task GetHazardInfoById(string id) - { - return Task.FromResult(_HazardInfo.FirstOrDefault(h => h.Id == id)); - } + public Task DeleteHazardHunt(string id) + { + return Task.FromResult(true); + } - public Task SaveHazardInfo(HazardInfo hazardInfo) - { - return Task.FromResult(hazardInfo); - } + public Task> GetAllHazardInfos() + { + return Task.FromResult(_HazardInfo.AsEnumerable()); + } - public Task CreateHazardInfo(HazardInfo hazardInfo) - { - return Task.FromResult(hazardInfo); - } + public Task GetHazardInfoById(string id) + { + return Task.FromResult(_HazardInfo.FirstOrDefault(h => h.Id == id)); + } + public Task SaveHazardInfo(HazardInfo hazardInfo) + { + return Task.FromResult(hazardInfo); + } - } + public Task CreateHazardInfo(HazardInfo hazardInfo) + { + return Task.FromResult(hazardInfo); + } + public Task DeleteHazardInfo(string id) + { + return Task.FromResult(true); + } + } } diff --git a/admin/TwoWeeksReady.Admin/Pages/HazardHunts/Details.razor b/admin/TwoWeeksReady.Admin/Pages/HazardHunts/Details.razor index fed19f0..2fbaaeb 100644 --- a/admin/TwoWeeksReady.Admin/Pages/HazardHunts/Details.razor +++ b/admin/TwoWeeksReady.Admin/Pages/HazardHunts/Details.razor @@ -1,18 +1,90 @@ @page "/HazardHunts/{id}" +@page "/HazardHunts/new" @attribute [Authorize(Roles = "admin")] +@using TinyMCE.Blazor +@using System.IO; +@using System.Linq; + @inject IRepository repository @inject IJSRuntime JS +@inject Microsoft.Extensions.Configuration.IConfiguration configuration +@inject ClientImageService clientImages + +@{ + var tinyMCEApiKey = configuration["TinyMCEApiKey"]; +} + +@if(Hazard == null) +{ +

Loading...

+} +else +{ +

Details

+ +
+ + +
+
+ + +
+
+ + + + @foreach(var image in clientImages.Images) + { + + } + + + +
+
+ + + @foreach(var image in clientImages.Images) + { + + } + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
-

Details

-Hazard Name: - -
-Short Description: - -Save + +
+ + +
+ + +
+} @code { + public Dictionary EditorConfig = new Dictionary + { + { "plugins", "image" }, + { "toolbar", "image" }, + }; [Parameter] public string Id { get; set; } @@ -22,19 +94,47 @@ Short Description: private HazardHunt Hazard { get; set; } + private string ExternalLinks + { + get + { + return string.Join(Environment.NewLine, Hazard?.ExternalLinks ?? new string[0]); + } + set + { + var links = value.Split(new string[] { "\n", Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)); + Hazard.ExternalLinks = links.ToArray(); + } + } + protected override async Task OnInitializedAsync() { - Hazard = await repository.GetHazardHuntById(Id); + if (string.IsNullOrEmpty(Id)) + { + Hazard = new HazardHunt(); + } + else + { + Hazard = await repository.GetHazardHuntById(Id); + } + EditorConfig["image_list"] = clientImages.Images.Select(i => new { title = i.RelativePath, value = i.AbsolutePath }).ToArray(); } - - + public async Task Save() { - await repository.SaveHazardHunt(Hazard); - await OnSave.InvokeAsync(Hazard); + if (string.IsNullOrEmpty(Hazard.Id)) + { + Hazard = await repository.CreateHazardHunt(Hazard); + } + else + { + Hazard = await repository.SaveHazardHunt(Hazard); + } + + //await OnSave.InvokeAsync(Hazard); - await JS.InvokeVoidAsync("alert", new object[] { "Hazard Hunt Saved" }); + await JS.InvokeVoidAsync("alert", new object[] { "Hazard Info Saved" }); } } diff --git a/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor b/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor index bcfea27..25ed074 100644 --- a/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor +++ b/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor @@ -4,8 +4,6 @@ @inject IRepository Repository

Administer Hazard Hunt for Two Weeks Ready

-

Current Hazards Defined:

- @if (_HazardHunts != null && _HazardHunts.Any()) { @@ -35,6 +33,7 @@ else {

No Hazards defined.

+ Add New Hazard } @code { @@ -43,7 +42,12 @@ else protected override async Task OnInitializedAsync() { - _HazardHunts = await Repository.GetAllHazardHunts(); + try { + _HazardHunts = await Repository.GetAllHazardHunts(); + } catch (NotImplementedException) { + // Application is still growing, let's not throw an error yet + _HazardHunts = Enumerable.Empty(); + } } diff --git a/admin/TwoWeeksReady.Admin/Pages/HazardInfos/Details.razor b/admin/TwoWeeksReady.Admin/Pages/HazardInfos/Details.razor index f049b92..f01ed0d 100644 --- a/admin/TwoWeeksReady.Admin/Pages/HazardInfos/Details.razor +++ b/admin/TwoWeeksReady.Admin/Pages/HazardInfos/Details.razor @@ -61,12 +61,12 @@ else
- +
- +
diff --git a/admin/TwoWeeksReady.Admin/Pages/HazardInfos/List.razor b/admin/TwoWeeksReady.Admin/Pages/HazardInfos/List.razor index 938e39e..2d82d48 100644 --- a/admin/TwoWeeksReady.Admin/Pages/HazardInfos/List.razor +++ b/admin/TwoWeeksReady.Admin/Pages/HazardInfos/List.razor @@ -37,6 +37,7 @@ else if (_HazardInfos == null) else {

No hazard infos defined

+ Add New Hazard } @code { diff --git a/admin/TwoWeeksReady.Admin/Pages/Kits/Details.razor b/admin/TwoWeeksReady.Admin/Pages/Kits/Details.razor index c1c7975..910713a 100644 --- a/admin/TwoWeeksReady.Admin/Pages/Kits/Details.razor +++ b/admin/TwoWeeksReady.Admin/Pages/Kits/Details.razor @@ -1,41 +1,100 @@ @page "/Kits/{id}" +@page "/Kits/new" @attribute [Authorize(Roles = "admin")] @inject IRepository repository +@inject IJSRuntime JS +@inject ClientImageService clientImages @if (Kit != null) { -

Kit Details :: @Kit.Name

+

Kit Details :: @Kit.Name

-
- - @foreach (var item in Kit.Items) { - - } +
+ +
+ + +
+
+ + +
+
+ -
+ + @foreach (var image in clientImages.Images) + { + + } + + +
+ +
+ +
+ + @foreach (var item in Kit.Items) + { + + } + +
+ + } else { -

Loading...

+

Loading...

} @code { - [Parameter] - public string Id { get; set; } - - public BaseKit Kit { get; set; } + [Parameter] + public string Id { get; set; } - protected override async Task OnInitializedAsync() - { + public BaseKit Kit { get; set; } - Kit = await repository.GetBaseKitById(Id); + public void AddKitItem() + { + if (Kit != null && Kit.Items != null) + { + Kit.Items.Add(new BaseKitItem { Id = Guid.NewGuid().ToString() }); + } + } - await base.OnInitializedAsync(); + public async Task SaveBaseKit() + { + if (Kit != null) + { + if (string.IsNullOrEmpty(Kit.Id)) + { + Kit = await repository.CreateBaseKit(Kit); + } + else + { + Kit = await repository.SaveBaseKit(Kit); + } - } + await JS.InvokeVoidAsync("alert", new object[] { "Base Kit Saved" }); + } + } + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrEmpty(Id)) + { + Kit = new BaseKit(); + } + else + { + Kit = await repository.GetBaseKitById(Id); + } + await base.OnInitializedAsync(); + } } diff --git a/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor b/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor index 1a1fd59..e407342 100644 --- a/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor +++ b/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor @@ -1,15 +1,14 @@ @page "/Kits/" @attribute [Authorize (Roles = "admin")] @inject IRepository Repository +@inject ClientImageService clientImages -

Administer the Base Kits for Two Weeks Ready

- -

Current Basekits Defined:

+

Administer Base Kits

@if (BaseKits != null && BaseKits.Any()) { - +
@@ -19,12 +18,10 @@ @foreach (var kit in BaseKits) { - @@ -33,11 +30,16 @@
Name
- @kit.Name + @kit.Name @kit.Items.Count()
- + Add New Kit +} +else if (BaseKits == null) +{ +

Loading....

} else {

No base kits defined.

+ Add New Kit } @code { @@ -47,7 +49,13 @@ else protected override async Task OnInitializedAsync() { - BaseKits = await Repository.GetAllBaseKits(); + try { + BaseKits = await Repository.GetAllBaseKits(); + } catch (NotImplementedException) + { + // do nothing for now... site is still growing + BaseKits = Enumerable.Empty(); + } } diff --git a/admin/TwoWeeksReady.Admin/Program.cs b/admin/TwoWeeksReady.Admin/Program.cs index 0e145a5..3391fa0 100644 --- a/admin/TwoWeeksReady.Admin/Program.cs +++ b/admin/TwoWeeksReady.Admin/Program.cs @@ -1,26 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace TwoWeeksReady.Admin +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using TwoWeeksReady.Admin; + +var builder = WebApplication.CreateBuilder(args); + +// Configure the container. +builder.Services.ConfigureServices(builder.Configuration); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) { - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); } + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseCookiePolicy(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); diff --git a/admin/TwoWeeksReady.Admin/Startup.cs b/admin/TwoWeeksReady.Admin/Startup.cs index 4e96986..3cf407f 100644 --- a/admin/TwoWeeksReady.Admin/Startup.cs +++ b/admin/TwoWeeksReady.Admin/Startup.cs @@ -15,137 +15,100 @@ using System; using System.Net.Http; -namespace TwoWeeksReady.Admin +namespace TwoWeeksReady.Admin; + +public static class StartupExtensions { - public class Startup + public static IServiceCollection ConfigureServices(this IServiceCollection services, IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } + services.AddRazorPages(); + services.AddServerSideBlazor(); - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) + services.Configure(options => { - services.AddRazorPages(); - services.AddServerSideBlazor(); + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); - services.Configure(options => - { - options.CheckConsentNeeded = context => true; - options.MinimumSameSitePolicy = SameSiteMode.None; - }); + // Add authentication services + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect("Auth0", options => + { + options.Authority = $"https://{configuration["Auth0:Domain"]}"; - // Add authentication services - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }) - .AddCookie() - .AddOpenIdConnect("Auth0", options => - { - options.Authority = $"https://{Configuration["Auth0:Domain"]}"; + options.ClientId = configuration["Auth0:ClientId"]; + options.ClientSecret = configuration["Auth0:ClientSecret"]; - options.ClientId = Configuration["Auth0:ClientId"]; - options.ClientSecret = Configuration["Auth0:ClientSecret"]; + options.ResponseType = OpenIdConnectResponseType.Code; + options.SaveTokens = true; - options.ResponseType = OpenIdConnectResponseType.Code; - options.SaveTokens = true; + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); - options.Scope.Clear(); - options.Scope.Add("openid"); - options.Scope.Add("profile"); + options.CallbackPath = new PathString("/callback"); + options.ClaimsIssuer = "Auth0"; - options.CallbackPath = new PathString("/callback"); - options.ClaimsIssuer = "Auth0"; + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "name", + RoleClaimType = "https://schemas.2wradmin.com/roles" + }; - options.TokenValidationParameters = new TokenValidationParameters + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = context => { - NameClaimType = "name", - RoleClaimType = "https://schemas.2wradmin.com/roles" - }; - - options.Events = new OpenIdConnectEvents + // The context's ProtocolMessage can be used to pass along additional query parameters + // to Auth0's /authorize endpoint. + // + // Set the audience query parameter to the API identifier to ensure the returned Access Tokens can be used + // to call protected endpoints on the corresponding API. + context.ProtocolMessage.SetParameter("audience", configuration["Auth0:Audience"]); + + return Task.FromResult(0); + }, + OnRedirectToIdentityProviderForSignOut = (context) => { - OnRedirectToIdentityProvider = context => - { - // The context's ProtocolMessage can be used to pass along additional query parameters - // to Auth0's /authorize endpoint. - // - // Set the audience query parameter to the API identifier to ensure the returned Access Tokens can be used - // to call protected endpoints on the corresponding API. - context.ProtocolMessage.SetParameter("audience", Configuration["Auth0:Audience"]); - - return Task.FromResult(0); - }, - OnRedirectToIdentityProviderForSignOut = (context) => - { - var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}"; + var logoutUri = $"https://{configuration["Auth0:Domain"]}/v2/logout?client_id={configuration ["Auth0:ClientId"]}"; - var postLogoutUri = context.Properties.RedirectUri; + var postLogoutUri = context.Properties.RedirectUri; - if (!string.IsNullOrEmpty(postLogoutUri)) + if (!string.IsNullOrEmpty(postLogoutUri)) + { + if (postLogoutUri.StartsWith("/")) { - if (postLogoutUri.StartsWith("/")) - { - var request = context.Request; - postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri; - } + var request = context.Request; + postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri; } + } - context.Response.Redirect(logoutUri); - context.HandleResponse(); - - return Task.CompletedTask; - } - }; - }); + context.Response.Redirect(logoutUri); + context.HandleResponse(); - services.AddHttpContextAccessor(); - services.AddHttpClient("ApiClient", (HttpClient client) => - { - client.BaseAddress = new Uri(Configuration["ApiUrl"]); - }); - - services.AddScoped(); - //services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - } + return Task.CompletedTask; + } + }; + }); - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + services.AddHttpContextAccessor(); + services.AddHttpClient("ApiClient", (HttpClient client) => { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); + client.BaseAddress = new Uri(configuration["ApiUrl"]); + }); - app.UseCookiePolicy(); - app.UseAuthentication(); - app.UseAuthorization(); + services.AddScoped(); + //services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); - app.UseEndpoints(endpoints => - { - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); - }); - } + return services; } + } diff --git a/admin/TwoWeeksReady.Admin/TwoWeeksReady.Admin.csproj b/admin/TwoWeeksReady.Admin/TwoWeeksReady.Admin.csproj index f25e619..b83dcb5 100644 --- a/admin/TwoWeeksReady.Admin/TwoWeeksReady.Admin.csproj +++ b/admin/TwoWeeksReady.Admin/TwoWeeksReady.Admin.csproj @@ -1,8 +1,9 @@  - net5.0 + net6.0 4c82d094-3afe-4274-922b-9c0d8bdda7c5 + enable @@ -16,7 +17,7 @@ - + diff --git a/api/README.md b/api/README.md index d52e557..5f4d106 100644 --- a/api/README.md +++ b/api/README.md @@ -4,11 +4,11 @@ ## Prerequisites -[.NET Core SDK 3.1](https://dotnet.microsoft.com/download) +[.NET SDK 6.0](https://dotnet.microsoft.com/download) -[Azure Functions Core Tools 3](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#install-the-azure-functions-core-tools) +[Azure Functions Core Tools 4](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#install-the-azure-functions-core-tools) - `npm i -g azure-functions-core-tools@3 --unsafe-perm true` + `npm i -g azure-functions-core-tools@4 --unsafe-perm true` ### Using VS Code @@ -24,11 +24,11 @@ Alternatively, there is an Azurite VS Code Extension: [Install Azure Extension](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite#install-and-run-the-azurite-visual-studio-code-extension) -### Using Visual Studio 2019 +### Using Visual Studio 2022 -Open the `api.sln` in Visual Studio 2019. +Open the `api.sln` in Visual Studio 2022. -You will also need the Azure Storage Emulator if using Visual Studio 2019 (if you're using VS Code, look above). +You will also need the Azure Storage Emulator if using Visual Studio 2022 (if you're using VS Code, look above). [Install Azure Storage Emulator](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-emulator?toc=/azure/storage/blobs/toc.json#get-the-storage-emulator) @@ -64,6 +64,7 @@ Create a `local.settings.json` file in the TwoWeeksReady project folder. You wil { "IsEncrypted": false, "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "OidcApiAuthSettings:Audience": "https://2wrdev.azurewebsites.net", "OidcApiAuthSettings:IssuerUrl": "https://login.2wr.org/" diff --git a/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs b/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs index 455ced2..cca6c09 100644 --- a/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs +++ b/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using AzureFunctions.OidcAuthentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Documents; @@ -13,190 +14,230 @@ using Microsoft.Azure.WebJobs.Host; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using AzureFunctions.OidcAuthentication; -using TwoWeeksReady.Common.EmergencyKits; using TwoWeeksReady.Authorization; +using TwoWeeksReady.Common.EmergencyKits; namespace TwoWeeksReady.EmergencyKits { - public class BaseKitsApi - { - private readonly IApiAuthentication _apiAuthentication; - - public BaseKitsApi(IApiAuthentication apiAuthentication) + public class BaseKitsApi { - _apiAuthentication = apiAuthentication; - } + private readonly IApiAuthentication _apiAuthentication; - [FunctionName("basekits")] - public async Task GetKits( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] + public BaseKitsApi(IApiAuthentication apiAuthentication) + { + _apiAuthentication = apiAuthentication; + } + + [FunctionName("basekits")] + public async Task GetKits( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, - [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] + [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] DocumentClient client, - ILogger log) - { + ILogger log) + { - log.LogInformation($"Getting list of base kits"); - var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "basekits"); - var query = client.CreateDocumentQuery(collectionUri, new FeedOptions { EnableCrossPartitionQuery = true }) - .AsDocumentQuery(); - - var baseKits = new List(); - while (query.HasMoreResults) - { - var result = await query.ExecuteNextAsync(); - baseKits.AddRange(result); - } - - return new OkObjectResult(baseKits); - } + log.LogInformation($"Getting list of base kits"); + var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); + if (authorizationResult.Failed) + { + log.LogWarning(authorizationResult.FailureReason); + return new UnauthorizedResult(); + } + Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "basekits"); + var query = client.CreateDocumentQuery(collectionUri, new FeedOptions { EnableCrossPartitionQuery = true }) + .AsDocumentQuery(); + + var baseKits = new List(); + while (query.HasMoreResults) + { + var result = await query.ExecuteNextAsync(); + baseKits.AddRange(result); + } + + return new OkObjectResult(baseKits); + } - [FunctionName("basekit-create")] - public async Task CreateBaseKit( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] - HttpRequest req, - [CosmosDB(databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] - DocumentClient client, - ILogger log) - { - - log.LogInformation($"Creating a base kit"); - var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); - if (authorizationResult.Failed) + [FunctionName("basekit-by-id")] + public async Task GetKit( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "basekit-by-id/{id}")] + HttpRequest req, + string id, + [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] + DocumentClient client, + ILogger log) { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); + var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); + if (authorizationResult.Failed) + { + log.LogWarning(authorizationResult.FailureReason); + return new UnauthorizedResult(); + } + + if (String.IsNullOrWhiteSpace(id)) + { + return new BadRequestObjectResult("Kit id was not specified."); + } + + log.LogInformation($"Getting single base kit"); + Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "basekits"); + + var feedOptions = new FeedOptions { EnableCrossPartitionQuery = false }; + var baseKit = client.CreateDocumentQuery(collectionUri, feedOptions) + .Where(d => d.Id == id) + .AsEnumerable() + .FirstOrDefault(); + + if (baseKit == null) + { + log.LogWarning($"Kit: {id} not found."); + return new BadRequestObjectResult("Kit not found."); + } + + return new OkObjectResult(baseKit); } - if (!authorizationResult.IsInRole(Roles.Admin)) + [FunctionName("basekit-create")] + public async Task CreateBaseKit( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] + HttpRequest req, + [CosmosDB(databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] + DocumentClient client, + ILogger log) { - log.LogWarning($"User is not in the {Roles.Admin} role"); - return new UnauthorizedResult(); - } + + log.LogInformation($"Creating a base kit"); + var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); + if (authorizationResult.Failed) + { + log.LogWarning(authorizationResult.FailureReason); + return new UnauthorizedResult(); + } + + if (!authorizationResult.IsInRole(Roles.Admin)) + { + log.LogWarning($"User is not in the {Roles.Admin} role"); + return new UnauthorizedResult(); + } var content = await new StreamReader(req.Body).ReadToEndAsync(); - var newBaseKit = JsonConvert.DeserializeObject(content); - newBaseKit.Id = Guid.NewGuid().ToString(); - if(newBaseKit.Items.Count > 0) - { - newBaseKit.Items.ForEach(ki => {ki.Id=Guid.NewGuid().ToString();}); - } + var newBaseKit = JsonConvert.DeserializeObject(content); + newBaseKit.Id = Guid.NewGuid().ToString(); + if (newBaseKit.Items.Count > 0) + { + newBaseKit.Items.ForEach(ki => { ki.Id = Guid.NewGuid().ToString(); }); + } - Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "basekits"); - Document document = await client.CreateDocumentAsync(collectionUri, newBaseKit); - - return new OkObjectResult(newBaseKit); - } + Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "basekits"); + Document document = await client.CreateDocumentAsync(collectionUri, newBaseKit); + + return new OkObjectResult(newBaseKit); + } - [FunctionName("basekit-update")] - public async Task UpdateBaseKit( - [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = null)] + [FunctionName("basekit-update")] + public async Task UpdateBaseKit( + [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = null)] HttpRequest req, - [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] + [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] DocumentClient client, - ILogger log) - { - - log.LogInformation($"Updating a base kit"); - var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - - if (!authorizationResult.IsInRole(Roles.Admin)) - { - log.LogWarning($"User is not in the {Roles.Admin} role"); - return new UnauthorizedResult(); - } - - var content = await new StreamReader(req.Body).ReadToEndAsync(); - var kit = JsonConvert.DeserializeObject(content); - - //verify existing document (not upserting as this is an update only function) - var baseKitUri = UriFactory.CreateDocumentUri("2wr", "basekits", kit.Id); - BaseKit existingKit = null; - try - { - existingKit = (await client.ReadDocumentAsync(baseKitUri, new RequestOptions{PartitionKey = new PartitionKey(kit.Id)})).Document; - } - catch(DocumentClientException ex) - { - if(ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - log.LogWarning($"Base Kit: {kit.Id} not found."); - return new BadRequestObjectResult("Base Kit not found."); - } - } - - if(kit.Items.Count > 0) - { - kit.Items.ForEach(ki => { - if(String.IsNullOrWhiteSpace(ki.Id)){ - ki.Id=Guid.NewGuid().ToString(); - } - }); - } - Document document = await client.ReplaceDocumentAsync(baseKitUri, kit, new RequestOptions{PartitionKey = new PartitionKey(existingKit.Id)}); - - return new OkObjectResult(kit); - } - - [FunctionName("basekit-delete")] - public async Task DeleteBaseKit( - [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "basekit-delete/{id}")] + ILogger log) + { + + log.LogInformation($"Updating a base kit"); + var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); + if (authorizationResult.Failed) + { + log.LogWarning(authorizationResult.FailureReason); + return new UnauthorizedResult(); + } + + if (!authorizationResult.IsInRole(Roles.Admin)) + { + log.LogWarning($"User is not in the {Roles.Admin} role"); + return new UnauthorizedResult(); + } + + var content = await new StreamReader(req.Body).ReadToEndAsync(); + var kit = JsonConvert.DeserializeObject(content); + + //verify existing document (not upserting as this is an update only function) + var baseKitUri = UriFactory.CreateDocumentUri("2wr", "basekits", kit.Id); + BaseKit existingKit = null; + try + { + existingKit = (await client.ReadDocumentAsync(baseKitUri, new RequestOptions { PartitionKey = new PartitionKey(kit.Id) })).Document; + } + catch (DocumentClientException ex) + { + if (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + log.LogWarning($"Base Kit: {kit.Id} not found."); + return new BadRequestObjectResult("Base Kit not found."); + } + } + + if (kit.Items.Count > 0) + { + kit.Items.ForEach(ki => + { + if (String.IsNullOrWhiteSpace(ki.Id)) + { + ki.Id = Guid.NewGuid().ToString(); + } + }); + } + Document document = await client.ReplaceDocumentAsync(baseKitUri, kit, new RequestOptions { PartitionKey = new PartitionKey(existingKit.Id) }); + + return new OkObjectResult(kit); + } + + [FunctionName("basekit-delete")] + public async Task DeleteBaseKit( + [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "basekit-delete/{id}")] HttpRequest req, - string id, - [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] + string id, + [CosmosDB( databaseName: "2wr", collectionName: "basekits", ConnectionStringSetting = "CosmosDBConnection")] DocumentClient client, - ILogger log) - { - - log.LogInformation($"Deleting a base kit"); - var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); - if (authorizationResult.Failed) - { - log.LogWarning(authorizationResult.FailureReason); - return new UnauthorizedResult(); - } - if (!authorizationResult.IsInRole(Roles.Admin)) - { - log.LogWarning($"User is not in the {Roles.Admin} role"); - return new UnauthorizedResult(); - } + ILogger log) + { + + log.LogInformation($"Deleting a base kit"); + var authorizationResult = await _apiAuthentication.AuthenticateAsync(req.Headers); + if (authorizationResult.Failed) + { + log.LogWarning(authorizationResult.FailureReason); + return new UnauthorizedResult(); + } + if (!authorizationResult.IsInRole(Roles.Admin)) + { + log.LogWarning($"User is not in the {Roles.Admin} role"); + return new UnauthorizedResult(); + } if (String.IsNullOrWhiteSpace(id)) - { - return new BadRequestObjectResult("Base Kit id was not specified."); - } - - //verify existing document - var baseKitUri = UriFactory.CreateDocumentUri("2wr", "basekits", id); - BaseKit existingKit = null; - try - { - existingKit = (await client.ReadDocumentAsync(baseKitUri, new RequestOptions{PartitionKey = new PartitionKey(id)})).Document; - } - catch(DocumentClientException ex) - { - if(ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - log.LogWarning($"Base Kit: {id} not found."); - return new BadRequestObjectResult("Base Kit not found."); - } - } - - Document document = await client.DeleteDocumentAsync(baseKitUri, new RequestOptions{PartitionKey = new PartitionKey(existingKit.Id)}); - return new OkObjectResult(true); - } - - } + { + return new BadRequestObjectResult("Base Kit id was not specified."); + } + + //verify existing document + var baseKitUri = UriFactory.CreateDocumentUri("2wr", "basekits", id); + BaseKit existingKit = null; + try + { + existingKit = (await client.ReadDocumentAsync(baseKitUri, new RequestOptions { PartitionKey = new PartitionKey(id) })).Document; + } + catch (DocumentClientException ex) + { + if (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + log.LogWarning($"Base Kit: {id} not found."); + return new BadRequestObjectResult("Base Kit not found."); + } + } + + Document document = await client.DeleteDocumentAsync(baseKitUri, new RequestOptions { PartitionKey = new PartitionKey(existingKit.Id) }); + return new OkObjectResult(true); + } + + } } diff --git a/api/TwoWeeksReady/EmergencyKits/EmergencyKitsApi.cs b/api/TwoWeeksReady/EmergencyKits/EmergencyKitsApi.cs index c6232d6..d797298 100644 --- a/api/TwoWeeksReady/EmergencyKits/EmergencyKitsApi.cs +++ b/api/TwoWeeksReady/EmergencyKits/EmergencyKitsApi.cs @@ -28,8 +28,9 @@ public EmergencyKitsApi(IApiAuthentication apiAuthentication) [FunctionName("emergencykits")] public async Task GetKits( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "emergencykits/{baseKitId?}")] HttpRequest req, + string baseKitId, [CosmosDB( databaseName: "2wr", collectionName: "emergencykits", ConnectionStringSetting = "CosmosDBConnection")] DocumentClient client, ILogger log) @@ -44,14 +45,21 @@ public async Task GetKits( log.LogInformation($"Getting list of emergency kits"); Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "emergencykits"); - var query = client.CreateDocumentQuery(collectionUri, new FeedOptions{EnableCrossPartitionQuery = false}) - .Where(e => e.UserId == authorizationResult.User.Identity.Name) - .AsDocumentQuery(); + + var kitQuery = client.CreateDocumentQuery(collectionUri, new FeedOptions { EnableCrossPartitionQuery = false }) + .Where(e => e.UserId == authorizationResult.User.Identity.Name); + + if (!string.IsNullOrEmpty(baseKitId)) + { + kitQuery = kitQuery.Where(e => e.BaseKitId == baseKitId); + } + + var documentQuery = kitQuery.AsDocumentQuery(); var emergencyKits = new List(); - while (query.HasMoreResults) + while (documentQuery.HasMoreResults) { - var result = await query.ExecuteNextAsync(); + var result = await documentQuery.ExecuteNextAsync(); emergencyKits.AddRange(result); } @@ -175,8 +183,7 @@ public async Task CreateFromBaseKit( UserId = authorizationResult.User.Identity.Name, Name = bki.Name, Description = bki.Description, - Photo = bki.Photo, - Quantity = bki.QuantityPerCount * request.Count, + Quantity = bki.QuantityPerAdult * request.Count, QuantityUnit = bki.QuantityUnit, IsAvailableInKit = false })); diff --git a/api/TwoWeeksReady/TwoWeeksReady.csproj b/api/TwoWeeksReady/TwoWeeksReady.csproj index e910129..f6d7c40 100644 --- a/api/TwoWeeksReady/TwoWeeksReady.csproj +++ b/api/TwoWeeksReady/TwoWeeksReady.csproj @@ -1,14 +1,14 @@ - netcoreapp3.1 - v3 + net6.0 + v4 <_FunctionsSkipCleanOutput>true - - - - + + + + diff --git a/solution architecture.xml b/solution architecture.xml new file mode 100644 index 0000000..a8c52d4 --- /dev/null +++ b/solution architecture.xml @@ -0,0 +1 @@ +7VrbcuI4EP0aqnYf4vId/Agh7FwyVdSmajKzLykFK7YmsuWV5QD5+m3ZEvhGYBKcnYdJKmC1hC6nu093i4ycy2TzF0dZ/IWFmI5sM9yMnPnIti3XcuFNSraVZDxRgoiTUA3aC27IM1ZCU0kLEuK8MVAwRgXJmsIVS1O8Eg0Z4pytm8MeGG2umqEIdwQ3K0S70lsSilhJLdPcd3zAJIrV0hNPdSRID1aCPEYhW9dEztXIueSMieop2VxiKsHTuFSfWxzo3W2M41Sc8oFv11/IPzeZa339YX7/bH16yK7GF+NqlidEC3VgtVmx1QhEnBWZGoa5wJs+3NG9Hm5292XtTgtmglmCBd/CEDXRREOpLMTTCK73eNtaFtegth0lRErH0W7uPQzwoJD4CVTc46isYyLwTYZWsr0G2x85s1gksMzcgkeUZ5U1PpANhqVmBwGsA3VYRV306ujYfejYQ6FjHUenc/waNBkjqSj35M1G3lyCRUmUgmAFQGAOApKUXjl7YKlQlGDZe/mcJBHsnJJ7eEXPBcfyRCECK0Q5kIW9mErh3SXLE5bfzWdG/hQNoQM1i980YKtrv0GPgoKh9OP36MensOqsoA01+f8WknxKmC/yEucpDLCCbLPvhKdIvZdzUPL6OT4g/ox4mOu57vnb91Mawjk3Cb6LofMzEflgayxQQqi0oCVF6XDLXCWYRzhdbU85DwilfWhhy6PBZaQ8F5w94ktGGfjpPGVp6aaE0pao5u99TCl9kECQnSrPT0gYypVmDHoeaBkmY5DhtMUCwRBurFOTE+OO5Q/GrOYvEI6dJpvteKoGizV+V1iCDirT5cc3R+TzmZVl9iNaR8x/V8Qmv2aIXrEkKwSw62JRpCtBWHo3zbL8zeH5kALUNJ7VsOjAcC1r7Ltj05tMbMdzOvbtu11leb6x+4QdwAz+UCRgdXSFQ6hJVJNxEbOIpYhe7aUzIIU0lGqcm5JFd2OuGcuUUn9gIbZKVagQrKnytjccZOhcIC6msrKS5kBRnpOVFi8I1fNVZ5AbP6hKfU5W8BU+no3DAhF+yQqCfiPgmCJBnpr7OL/W7OMupzG9RveYLllOpAdA1z0TgiU9oAvWIrFdDSm1HKI83qm8Csy6RFVjM7lwsolkaW6gNOSMhEYWQ4i2O7F8ZDsrU/6+lhpfDiteM6r0BRWrr4pxB4u1Xkdh9u3fIJgul5AFoURCn97nWS1FkvlqlR99LWSO+OkGXv5Y3k7/PJQv1XV3YurUodqDrtgX8ZpEMIAiW+mB4/dosrdaH0yRffVOm0DT8ABldcUvkCJAxLffFLZl47tsGLan2/NNvXe+Va0zsqEmmtfSYU1PfWrSspNZU62wlClDzd0nTTNxzZb6q3OqT9VvrVoT+W1781oTVTh0JipNaXfsN1hX94ZsGiYklUSRZdL9ZxQ9M95lAFYISlJweH0xKQ0BqDgkoPM2DbC9CIjYc70Z5CAncAYMnowDK/Db8eCVsaZDPi94Q9lW5zX7MuxOBFrnrgGbL+BYqMz+Qpw/VguXKSfmV0+4yjz1JdNbqMpu2WDPxaIVjI1J1wusyWBsdUJqfm62ujAN02oylufaRyirbC0xJ3BuaQln57Hxbx57Tx7rFtEfQ1AaERLMLyiFsi3Bau26MWon1gXfUWKhsmOGVo9RmYvUmOqh/DnMPW3+KJecaqlZqy5jIeT3P1MJk70oMspQaKzJIxhrSJDBuCw+ZTuT7aryTJi8aVuIuEhkUerJP/lwDZUSENHdtBCxWZai9gKqPTPbXHS6jCw9uVJ9mZvGnm04k2M5se2ODd/uWvdg9KQp8x3o6exc8n9xhG3tYkg70vwsS4xbZVLnhmhglrBP+G7nXNFpQ0SZShvBxFNtlU07rmrvQ5NsbGuNIQPTqQm29WtFpvb3Sa+NTG675B7a5k64OTm7zTUs7mVr01lUI4PaNYYv+ILf5ngec4Tm/r8tquH7/1lxrv4D \ No newline at end of file diff --git a/tools/CosmosEmulator/CosmosEmulator.csproj b/tools/CosmosEmulator/CosmosEmulator.csproj index eeae762..da3707d 100644 --- a/tools/CosmosEmulator/CosmosEmulator.csproj +++ b/tools/CosmosEmulator/CosmosEmulator.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/tools/CosmosEmulator/start-emulator.sh b/tools/CosmosEmulator/start-emulator.sh new file mode 100644 index 0000000..887a97b --- /dev/null +++ b/tools/CosmosEmulator/start-emulator.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Setup CosmosDB Docker Emulator +ipaddr="`ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1`" +docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator + +# start emulator +docker run --detach --rm -p 8081:8081 -p 10251:10251 -p 10252:10252 -p 10253:10253 -p 10254:10254 -m 3g --cpus=2.0 --name=test-linux-emulator -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=true -e AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE=$ipaddr -it mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator & + +# wait for process to complete and server to start +wait +sleep 120s # necessary wait time for the cosmosdb server to start up and start responding to web requests + +# install the certificate +curl --insecure --url https://localhost:8081/_explorer/emulator.pem -o ~/emulatorcert.crt +sudo cp ~/emulatorcert.crt /usr/local/share/ca-certificates/emulatorcert.crt +sudo update-ca-certificates + +# setup database tables +pushd ./tools/CosmosEmulator +dotnet run /setup +popd diff --git a/tools/CosmosEmulator/stop-emulator.sh b/tools/CosmosEmulator/stop-emulator.sh new file mode 100644 index 0000000..95d777d --- /dev/null +++ b/tools/CosmosEmulator/stop-emulator.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# stop docker process +docker stop test-linux-emulator + +# remove SSL certificate +sudo rm /usr/local/share/ca-certificates/emulatorcert.crt +sudo update-ca-certificates \ No newline at end of file diff --git a/tools/first-build.sh b/tools/first-build.sh new file mode 100644 index 0000000..9926a2c --- /dev/null +++ b/tools/first-build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# restore and build dotnet projects (api & admin) +dotnet restore ./TwoWeeksReady.Common +dotnet restore ./api/api.sln +dotnet restore ./admin/admin.sln +dotnet build ./TwoWeeksReady.Common +dotnet build ./api/api.sln +dotnet build ./admin/admin.sln + +# restore Vue app (2wr-app) +pushd ./2wr-app +npm install +npm install -g azurite +npx playwright install +npm run build +popd + +# setup default configuration files +bash ./tools/setup-config.sh > /dev/null \ No newline at end of file diff --git a/tools/samples/local.settings.sample.json b/tools/samples/local.settings.sample.json new file mode 100644 index 0000000..56d290e --- /dev/null +++ b/tools/samples/local.settings.sample.json @@ -0,0 +1,16 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "OidcApiAuthSettings:Audience": "https://2wrdev.azurewebsites.net", + "OidcApiAuthSettings:IssuerUrl": "https://login.2wr.org/" + }, + "ConnectionStrings": { + "CosmosDBConnection": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", + "StorageConnection": "UseDevelopmentStorage=true" + }, + "Host":{ + "CORS": "*" + } +} \ No newline at end of file diff --git a/tools/setup-config.sh b/tools/setup-config.sh new file mode 100644 index 0000000..847150b --- /dev/null +++ b/tools/setup-config.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# navigate to script directory +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +pushd $SCRIPT_DIR + +# create api settings file from sample +API_SETTINGS_FILE=../api/TwoWeeksReady/local.settings.json +if [[ ! -f $API_SETTINGS_FILE ]] +then +cp ./samples/local.settings.sample.json $API_SETTINGS_FILE +fi + +# create 2wr-app env from sample +APP_SETTINGS_FILE=../2wr-app/.env +if [[ ! -f $APP_SETTINGS_FILE ]] +then +cp ../2wr-app/.env-sample $APP_SETTINGS_FILE +fi +popd \ No newline at end of file