From 5302a25415a49ad81fd3d98b288f841c2b02d7cf Mon Sep 17 00:00:00 2001 From: David Wesst <867084+davidwesst@users.noreply.github.com> Date: Mon, 24 Jan 2022 15:58:48 -0600 Subject: [PATCH 01/20] Include DevContainer for Development Environment (#126) * added baseline C# functions devcontainer config. #124 * added some extensions and tool installs. #124 * added node, docker, and dotnet-install sdk scripts. #124 * added missing comma * installing .net 5 and 6 sdks in dockerfile. #124 * added vnc desktop testing and first-build script to confirm environment is working immediately. #124 * added azurite to first-build script for setup * finished adding scripts for cosmosdb emulator and database setup. #124 * minor fixes and removed github feature for now. #124 * adding config files on first build. #124 * fixed typo * refreshed start-stop script and VSCode tasks for managing CosmosDB emulator * added gh cli * added task for vscode for cosmosdb * added tasks and settings to run azure function vscode extension * minor update to gitignore and linux compatible run command * reverted npm script command back to windows style * Added labels to tasks.json * increased sleep time to give cosmosdb more time to startup --- .devcontainer/Dockerfile | 35 ++ .devcontainer/devcontainer.json | 42 +++ .../library-scripts/docker-in-docker.sh | 324 ++++++++++++++++++ .devcontainer/library-scripts/node-debian.sh | 169 +++++++++ .gitignore | 3 + .vscode/settings.json | 8 + .vscode/tasks.json | 69 +++- tools/CosmosEmulator/start-emulator.sh | 22 ++ tools/CosmosEmulator/stop-emulator.sh | 8 + tools/first-build.sh | 20 ++ tools/samples/local.settings.sample.json | 15 + tools/setup-config.sh | 20 ++ 12 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/library-scripts/docker-in-docker.sh create mode 100644 .devcontainer/library-scripts/node-debian.sh create mode 100644 .vscode/settings.json create mode 100644 tools/CosmosEmulator/start-emulator.sh create mode 100644 tools/CosmosEmulator/stop-emulator.sh create mode 100644 tools/first-build.sh create mode 100644 tools/samples/local.settings.sample.json create mode 100644 tools/setup-config.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..0d62f54 --- /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:3.0-dotnet3-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/.gitignore b/.gitignore index cf346a0..0683a89 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ *.userosscache *.sln.docstates +# Azurite files +__azurite_*.json + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eb91ab2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "azureFunctions.projectSubpath": "api/TwoWeeksReady", + "azureFunctions.deploySubpath": "api/TwoWeeksReady/bin/Release/netcoreapp3.1/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~3", + "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..7b83460 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/netcoreapp3.1", + "languageWorkers__node__arguments": "--inspect=5858" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" } ] -} \ No newline at end of file +} 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..322b1e8 --- /dev/null +++ b/tools/samples/local.settings.sample.json @@ -0,0 +1,15 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "OidcApiAuthSettings:Audience": "https://2wrdev.azurewebsites.net", + "OidcApiAuthSettings:IssuerUrl": "https://login.2wr.org/" + }, + "ConnectionStrings": { + "CosmosDBConnection": "AccountEndpoint=https://localhost:8081/;AccountKey=YOURLOCALACCOUNTKEY", + "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 From dcb56f0ba03390f9cffe36bdf0a1c8f71d78ccfc Mon Sep 17 00:00:00 2001 From: codingbandit Date: Thu, 3 Mar 2022 11:56:53 -0500 Subject: [PATCH 02/20] Updated wireframes url Updated wireframes url to the grid view of all screens --- 2wr-app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2wr-app/README.md b/2wr-app/README.md index 2d5ff32..89b6dab 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 From 9764f106c5c00473a2ca0fa913c6e29ae2a1ad81 Mon Sep 17 00:00:00 2001 From: codingbandit Date: Sat, 5 Mar 2022 07:58:45 -0500 Subject: [PATCH 03/20] Added slack invitation link Added slack invitation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0d5370..c268aca 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ 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 Maximilian @MaximilianDixon 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) From bc63e228ab40e538dfc9b5205e383506eed8c060 Mon Sep 17 00:00:00 2001 From: codingbandit Date: Sat, 5 Mar 2022 08:01:19 -0500 Subject: [PATCH 04/20] Updated to Richard Campbell for slack contact Updated to Richard Campbell for slack contact --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c268aca..6ed5b56 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ 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 reach out out to Pascal @schuback or Maximilian @MaximilianDixon 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) +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) From 10f76fa365e378c81ed5b74d6a3d55954bc45193 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Sat, 5 Mar 2022 10:26:41 -0500 Subject: [PATCH 05/20] Updated to .NET6 and consolidated Program.cs (#138) --- .../Pages/HazardHunts/List.razor | 7 +- .../Pages/HazardInfos/List.razor | 1 + .../TwoWeeksReady.Admin/Pages/Kits/List.razor | 8 +- admin/TwoWeeksReady.Admin/Program.cs | 54 +++--- admin/TwoWeeksReady.Admin/Startup.cs | 181 +++++++----------- .../TwoWeeksReady.Admin.csproj | 5 +- 6 files changed, 119 insertions(+), 137 deletions(-) diff --git a/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor b/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor index bcfea27..753ece8 100644 --- a/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor +++ b/admin/TwoWeeksReady.Admin/Pages/HazardHunts/List.razor @@ -43,7 +43,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/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/List.razor b/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor index 1a1fd59..7e42741 100644 --- a/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor +++ b/admin/TwoWeeksReady.Admin/Pages/Kits/List.razor @@ -47,7 +47,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 @@ - + From c2663e3dfbb9bc966492059f5bf16b51fa101dcd Mon Sep 17 00:00:00 2001 From: David Paquette Date: Sat, 5 Mar 2022 08:53:50 -0700 Subject: [PATCH 06/20] Update function app to .NET 6 (#139) * Update function app to .NET 6 * Update github workflows for branch name and .net version Co-authored-by: David Paquette --- .github/workflows/admin.yaml | 14 +++++--------- .github/workflows/api.yaml | 10 +++++----- api/README.md | 13 +++++++------ api/TwoWeeksReady/TwoWeeksReady.csproj | 12 ++++++------ 4 files changed, 23 insertions(+), 26 deletions(-) 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/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/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 - - - - + + + + From 992833c730d32ebbd51074b7c296abd1c59cc97a Mon Sep 17 00:00:00 2001 From: David Paquette Date: Sat, 5 Mar 2022 10:01:29 -0700 Subject: [PATCH 07/20] Run e2e tests in PRs (#140) * Run e2e tests PRs * Main, not master * Install playwright browsers in workflow * Add github reporter * Screenshots on failure for e2e tests * Upload test results if failure * Fix e2e tests that were failing * Update to Node 16 * Update readme to reference Node 16.x Co-authored-by: David Paquette --- .github/workflows/2wr-app.yaml | 20 +++++++++++++++++--- 2wr-app/.gitignore | 3 +++ 2wr-app/README.md | 2 +- 2wr-app/playwright.config.js | 3 +++ 2wr-app/tests/welcome.spec.js | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/2wr-app.yaml b/.github/workflows/2wr-app.yaml index 8e2702c..d0cead4 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 + 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/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 89b6dab..3895b4a 100644 --- a/2wr-app/README.md +++ b/2wr-app/README.md @@ -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/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/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: { From 1d6e38f0130b6f1f53942e53f073544da30698e1 Mon Sep 17 00:00:00 2001 From: codingbandit Date: Sat, 5 Mar 2022 12:53:46 -0500 Subject: [PATCH 08/20] Implemented Hazard Hunt data entry, added calls to the API, and fixed some labels (#143) --- .../Data/FunctionsRepository.cs | 34 ++++- admin/TwoWeeksReady.Admin/Data/IRepository.cs | 2 + .../Data/StubRepository.cs | 4 + .../Pages/HazardHunts/Details.razor | 126 ++++++++++++++++-- .../Pages/HazardHunts/List.razor | 3 +- .../Pages/HazardInfos/Details.razor | 4 +- 6 files changed, 150 insertions(+), 23 deletions(-) diff --git a/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs b/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs index 3507f5c..8fe6732 100644 --- a/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs @@ -25,9 +25,9 @@ public Task> GetAllBaseKits() throw new NotImplementedException(); } - public Task> GetAllHazardHunts() + public async Task> GetAllHazardHunts() { - throw new NotImplementedException(); + return await _httpClient.GetFromJsonAsync>("hazardhunt-list"); } public async Task> GetAllHazardInfos() @@ -40,9 +40,9 @@ public Task GetBaseKitById(string id) throw new NotImplementedException(); } - 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) @@ -55,9 +55,17 @@ public Task SaveBaseKitItem(BaseKitItem kit) throw new NotImplementedException(); } - 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) @@ -73,6 +81,7 @@ public async Task SaveHazardInfo(HazardInfo hazardInfo) } } + public async Task CreateHazardInfo(HazardInfo hazardInfo) { var response = await _httpClient.PostAsJsonAsync("hazardinfo-create", hazardInfo); @@ -85,5 +94,18 @@ 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"); + } + } } } diff --git a/admin/TwoWeeksReady.Admin/Data/IRepository.cs b/admin/TwoWeeksReady.Admin/Data/IRepository.cs index 210f445..63cce13 100644 --- a/admin/TwoWeeksReady.Admin/Data/IRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/IRepository.cs @@ -26,6 +26,8 @@ public interface IRepository Task SaveHazardInfo(HazardInfo hazardInfo); Task CreateHazardInfo(HazardInfo hazardInfo); + + Task CreateHazardHunt(HazardHunt hazardHunt); } } diff --git a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs index ecd131e..683c6ba 100644 --- a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs @@ -109,6 +109,10 @@ public Task CreateHazardInfo(HazardInfo hazardInfo) return Task.FromResult(hazardInfo); } + public Task CreateHazardHunt(HazardHunt hazardHunt) + { + return Task.FromResult(hazardHunt); + } } 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 753ece8..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 { 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
- +
- +
From 56e194d3db278fbc5b0da6a8494435aa4b0d409f Mon Sep 17 00:00:00 2001 From: codingbandit Date: Sat, 5 Mar 2022 13:43:00 -0500 Subject: [PATCH 09/20] Adding Hazard Hunt information into the UI --- 2wr-app/src/api/hazard-hunt-api.js | 3 + 2wr-app/src/api/hazard-info-api.js | 2 +- .../prepare/hazards/hazard-hunt-list.vue | 49 +++--- .../prepare/hazards/hazard-hunt.vue | 164 ++++++++++++++++++ .../components/prepare/prepare-landing.vue | 6 +- 2wr-app/src/router/index.js | 12 +- .../prepare/hazards/hazard-hunt-store.js | 32 +++- .../prepare/hazards/hazard-hunt-view.vue | 14 ++ 8 files changed, 252 insertions(+), 30 deletions(-) create mode 100644 2wr-app/src/components/prepare/hazards/hazard-hunt.vue create mode 100644 2wr-app/src/views/prepare/hazards/hazard-hunt-view.vue 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/hazards/hazard-hunt-list.vue b/2wr-app/src/components/prepare/hazards/hazard-hunt-list.vue index 081e250..68060ed 100644 --- a/2wr-app/src/components/prepare/hazards/hazard-hunt-list.vue +++ b/2wr-app/src/components/prepare/hazards/hazard-hunt-list.vue @@ -2,7 +2,7 @@ mdi-arrow-left - mdi-shield-search + mdi-shield-alert-outline Hazard Hunt - - - - + + + + + + + + + @@ -45,7 +43,7 @@ export default { }; }, computed: mapState({ - hunts: state => state.hazardHuntStore.list + items: state => state.hazardHuntStore.list }), async created() { await this.$store.dispatch(`hazardHuntStore/getHazardHuntsAsync`); @@ -53,7 +51,10 @@ export default { }, methods: { goBack() { - window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/'); + window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/'); + }, + viewItem(id){ + this.$router.push(`/prepare/hazardhunt/${id}`); } } } diff --git a/2wr-app/src/components/prepare/hazards/hazard-hunt.vue b/2wr-app/src/components/prepare/hazards/hazard-hunt.vue new file mode 100644 index 0000000..b6ba373 --- /dev/null +++ b/2wr-app/src/components/prepare/hazards/hazard-hunt.vue @@ -0,0 +1,164 @@ + + + + + \ 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..844adff 100644 --- a/2wr-app/src/components/prepare/prepare-landing.vue +++ b/2wr-app/src/components/prepare/prepare-landing.vue @@ -57,8 +57,8 @@ - + diff --git a/2wr-app/src/router/index.js b/2wr-app/src/router/index.js index bec426e..0eb5fc0 100644 --- a/2wr-app/src/router/index.js +++ b/2wr-app/src/router/index.js @@ -9,6 +9,7 @@ import EmergencyKitEditPage from '../views/prepare/emergency-kits/emergency-kit- 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'; @@ -65,7 +66,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/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/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 @@ + + + From 1c53c1589babfd9b433e11bcf6c1277cf8a9d807 Mon Sep 17 00:00:00 2001 From: codingbandit Date: Sat, 5 Mar 2022 13:58:58 -0500 Subject: [PATCH 10/20] Adding Hazard Hunt information into the UI (#144) --- 2wr-app/src/api/hazard-hunt-api.js | 3 + 2wr-app/src/api/hazard-info-api.js | 2 +- .../prepare/hazards/hazard-hunt-list.vue | 49 +++--- .../prepare/hazards/hazard-hunt.vue | 164 ++++++++++++++++++ .../components/prepare/prepare-landing.vue | 6 +- 2wr-app/src/router/index.js | 12 +- .../prepare/hazards/hazard-hunt-store.js | 32 +++- .../prepare/hazards/hazard-hunt-view.vue | 14 ++ 8 files changed, 252 insertions(+), 30 deletions(-) create mode 100644 2wr-app/src/components/prepare/hazards/hazard-hunt.vue create mode 100644 2wr-app/src/views/prepare/hazards/hazard-hunt-view.vue 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/hazards/hazard-hunt-list.vue b/2wr-app/src/components/prepare/hazards/hazard-hunt-list.vue index 081e250..68060ed 100644 --- a/2wr-app/src/components/prepare/hazards/hazard-hunt-list.vue +++ b/2wr-app/src/components/prepare/hazards/hazard-hunt-list.vue @@ -2,7 +2,7 @@ mdi-arrow-left - mdi-shield-search + mdi-shield-alert-outline Hazard Hunt - - - - + + + + + + + + + @@ -45,7 +43,7 @@ export default { }; }, computed: mapState({ - hunts: state => state.hazardHuntStore.list + items: state => state.hazardHuntStore.list }), async created() { await this.$store.dispatch(`hazardHuntStore/getHazardHuntsAsync`); @@ -53,7 +51,10 @@ export default { }, methods: { goBack() { - window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/'); + window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/'); + }, + viewItem(id){ + this.$router.push(`/prepare/hazardhunt/${id}`); } } } diff --git a/2wr-app/src/components/prepare/hazards/hazard-hunt.vue b/2wr-app/src/components/prepare/hazards/hazard-hunt.vue new file mode 100644 index 0000000..b6ba373 --- /dev/null +++ b/2wr-app/src/components/prepare/hazards/hazard-hunt.vue @@ -0,0 +1,164 @@ + + + + + \ 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..844adff 100644 --- a/2wr-app/src/components/prepare/prepare-landing.vue +++ b/2wr-app/src/components/prepare/prepare-landing.vue @@ -57,8 +57,8 @@ - + diff --git a/2wr-app/src/router/index.js b/2wr-app/src/router/index.js index bec426e..0eb5fc0 100644 --- a/2wr-app/src/router/index.js +++ b/2wr-app/src/router/index.js @@ -9,6 +9,7 @@ import EmergencyKitEditPage from '../views/prepare/emergency-kits/emergency-kit- 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'; @@ -65,7 +66,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/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/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 @@ + + + From 027a979c050e77613e034b5e7ee1a7d900d15866 Mon Sep 17 00:00:00 2001 From: codingbandit Date: Sat, 5 Mar 2022 15:07:50 -0500 Subject: [PATCH 11/20] Added a placeholder for the "Make A Plan" screen. This screen has a button to move to the family plan --- .../prepare/make-a-plan/make-a-plan.vue | 29 +++++++++++++++++++ .../components/prepare/prepare-landing.vue | 2 +- 2wr-app/src/router/index.js | 9 ++++++ .../views/prepare/make-a-plan/make-a-plan.vue | 14 +++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 2wr-app/src/components/prepare/make-a-plan/make-a-plan.vue create mode 100644 2wr-app/src/views/prepare/make-a-plan/make-a-plan.vue 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 844adff..f3c9f96 100644 --- a/2wr-app/src/components/prepare/prepare-landing.vue +++ b/2wr-app/src/components/prepare/prepare-landing.vue @@ -26,7 +26,7 @@ + + + + From 35f163d31a9579f90fcc9c60506beebd4111c5a8 Mon Sep 17 00:00:00 2001 From: David Wesst Date: Sat, 5 Mar 2022 20:32:35 +0000 Subject: [PATCH 12/20] updated devcontainer to use .NET 6 and Func v4 --- .devcontainer/Dockerfile | 2 +- .gitignore | 1 + .vscode/settings.json | 4 ++-- .vscode/tasks.json | 2 +- tools/CosmosEmulator/CosmosEmulator.csproj | 2 +- tools/samples/local.settings.sample.json | 27 +++++++++++----------- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0d62f54..89e6114 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # 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:3.0-dotnet3-core-tools +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 diff --git a/.gitignore b/.gitignore index 0683a89..02b7d96 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # Azurite files __azurite_*.json +__blobstorage__ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/.vscode/settings.json b/.vscode/settings.json index eb91ab2..cf8e0c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { "azureFunctions.projectSubpath": "api/TwoWeeksReady", - "azureFunctions.deploySubpath": "api/TwoWeeksReady/bin/Release/netcoreapp3.1/publish", + "azureFunctions.deploySubpath": "api/TwoWeeksReady/bin/Release/net6.0/publish", "azureFunctions.projectLanguage": "C#", - "azureFunctions.projectRuntime": "~3", + "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 7b83460..fbefe0e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -98,7 +98,7 @@ "type": "func", "dependsOn": "API (Build)", "options": { - "cwd": "${workspaceFolder}/api/TwoWeeksReady/bin/Debug/netcoreapp3.1", + "cwd": "${workspaceFolder}/api/TwoWeeksReady/bin/Debug/net6.0", "languageWorkers__node__arguments": "--inspect=5858" }, "command": "host start", 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/samples/local.settings.sample.json b/tools/samples/local.settings.sample.json index 322b1e8..56d290e 100644 --- a/tools/samples/local.settings.sample.json +++ b/tools/samples/local.settings.sample.json @@ -1,15 +1,16 @@ { - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "OidcApiAuthSettings:Audience": "https://2wrdev.azurewebsites.net", - "OidcApiAuthSettings:IssuerUrl": "https://login.2wr.org/" - }, - "ConnectionStrings": { - "CosmosDBConnection": "AccountEndpoint=https://localhost:8081/;AccountKey=YOURLOCALACCOUNTKEY", - "StorageConnection": "UseDevelopmentStorage=true" - }, - "Host":{ - "CORS": "*" - } + "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 From c2da0e7d6693d2e9a2540f6506931c36329a50a7 Mon Sep 17 00:00:00 2001 From: Rollerss <46410630+Rollerss@users.noreply.github.com> Date: Sat, 5 Mar 2022 15:04:34 -0800 Subject: [PATCH 13/20] Added get basekit-by-id (#149) --- .../EmergencyKits/BaseKitsApi.cs | 361 ++++++++++-------- 1 file changed, 199 insertions(+), 162 deletions(-) diff --git a/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs b/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs index 455ced2..a5a1156 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,226 @@ 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 baseKit = client.CreateDocumentQuery(collectionUri, new FeedOptions { EnableCrossPartitionQuery = true }) + .Where(b => b.Id == id).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); + } + + } } From acdc29c20be0e143eb46a502beee692fe7072ee7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Apr 2022 15:00:54 +0000 Subject: [PATCH 14/20] Bump async from 2.6.3 to 2.6.4 in /2wr-app Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] --- 2wr-app/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/2wr-app/package-lock.json b/2wr-app/package-lock.json index 272891f..dfce2b9 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" @@ -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" From 3473aabc1198e99e1a38fed46612ab364a046a48 Mon Sep 17 00:00:00 2001 From: David Wesst <867084+davidwesst@users.noreply.github.com> Date: Sun, 1 May 2022 20:44:01 -0500 Subject: [PATCH 15/20] DevContainer/Codespaces Documentation (#155) * added getting started section to README.md. Resolves #127 * Update README.md Co-authored-by: David Paquette --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 6ed5b56..46a79db 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,21 @@ 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 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. + +### 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 \ No newline at end of file From c90ae6ead76f19cc8b0879375298fe181543d9ca Mon Sep 17 00:00:00 2001 From: Rollerss <46410630+Rollerss@users.noreply.github.com> Date: Wed, 11 May 2022 14:26:02 -0700 Subject: [PATCH 16/20] Update repository methods (#152) --- .../Components/KitItemDisplay.razor | 2 +- .../Data/FunctionsRepository.cs | 76 ++++++- admin/TwoWeeksReady.Admin/Data/IRepository.cs | 43 ++-- .../Data/StubRepository.cs | 208 ++++++++++-------- 4 files changed, 200 insertions(+), 129 deletions(-) diff --git a/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor b/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor index cbcf971..c4a9bd1 100644 --- a/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor +++ b/admin/TwoWeeksReady.Admin/Components/KitItemDisplay.razor @@ -55,7 +55,7 @@ public async Task Save() { IsEditMode = false; - await repository.SaveBaseKitItem(Item); + @* 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 8fe6732..e62d423 100644 --- a/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs @@ -20,9 +20,10 @@ 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 async Task> GetAllHazardHunts() @@ -35,9 +36,9 @@ 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 async Task GetHazardHuntById(string id) @@ -50,9 +51,17 @@ 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("basekits-update", kit); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + throw new Exception("Error saving base kit"); + } } public async Task SaveHazardHunt(HazardHunt hazardHunt) @@ -74,14 +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); @@ -107,5 +128,44 @@ public async Task CreateHazardHunt(HazardHunt hazardHunt) 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 63cce13..f676162 100644 --- a/admin/TwoWeeksReady.Admin/Data/IRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/IRepository.cs @@ -5,29 +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 CreateHazardHunt(HazardHunt hazardHunt); - } + { + 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 683c6ba..631af41 100644 --- a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs @@ -5,115 +5,131 @@ 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", + 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> 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 CreateHazardHunt(HazardHunt hazardHunt) - { - return Task.FromResult(hazardHunt); - } + 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); + } + } } From 13bd29354676ae7fade1d49df7d3e782fa8e06c8 Mon Sep 17 00:00:00 2001 From: David Paquette Date: Thu, 12 May 2022 08:19:54 -0600 Subject: [PATCH 17/20] Added architecture diagram --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46a79db..92d8f2c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,11 @@ To see a full demo on how to setup your DevContainer/Codespaces environment, che 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] @@ -46,4 +51,4 @@ To setup your own development environment from scratch or to install the depende [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 \ No newline at end of file +[4]: https://code.visualstudio.com/docs/remote/codespaces From d07f774cb4f2b0e995036cf33ce9bef82d65173e Mon Sep 17 00:00:00 2001 From: David Paquette Date: Thu, 12 May 2022 08:22:38 -0600 Subject: [PATCH 18/20] Support running github actions locally --- .github/workflows/2wr-app.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/2wr-app.yaml b/.github/workflows/2wr-app.yaml index d0cead4..985bfc9 100644 --- a/.github/workflows/2wr-app.yaml +++ b/.github/workflows/2wr-app.yaml @@ -30,10 +30,10 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci working-directory: ./2wr-app - - run: npx playwright install + - 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: 'cp ./2wr-app/.env-sample ./2wr-app/.env' - run: npm run e2etest working-directory: ./2wr-app - name: Upload e2e Test Results From 6206c6200808783858bcf04da99ed580a00fc112 Mon Sep 17 00:00:00 2001 From: David Paquette Date: Sat, 14 May 2022 07:44:51 -0600 Subject: [PATCH 19/20] Build Emergency Kits Feature (#157) * Ability to add and edit base kits from admin app * Solution architecture diagram from diagrams.net * Admin App: Add new base kits and refactored Base Kit properties * Client App: Build A Kit page * Added ability to fetch emergency kits by baseKitId * Add description to BaseKit * Client App: Build a kit for each base kit type * Client App: Create New Kit * Client App: Edit Emergency Kits * Rename EmergencyKitCreate to EmergencyKitDetails * Fix undefined error Co-authored-by: David Paquette --- 2wr-app/public/images/kits/build-a-kit.png | Bin 0 -> 47084 bytes 2wr-app/public/images/kits/go-kit.png | Bin 0 -> 6075 bytes 2wr-app/public/images/kits/work-kit.png | Bin 1874 -> 7432 bytes 2wr-app/src/api/emergency-kit-api.js | 4 +- .../emergency-kits/emergency-kit-build.vue | 71 ++++ .../emergency-kits/emergency-kit-create.vue | 366 ------------------ .../emergency-kits/emergency-kit-details.vue | 317 +++++++++++++++ .../emergency-kits/emergency-kit-edit.vue | 332 ---------------- .../emergency-kits/emergency-kit-list.vue | 135 ++++--- .../components/prepare/prepare-landing.vue | 2 +- 2wr-app/src/router/index.js | 26 +- .../emergency-kits/emergency-kit-store.js | 4 +- .../emergency-kits/emergency-kit-build.vue | 14 + .../emergency-kits/emergency-kit-create.vue | 14 - .../emergency-kits/emergency-kit-details.vue | 14 + .../emergency-kits/emergency-kit-edit.vue | 14 - TwoWeeksReady.Common/EmergencyKits/BaseKit.cs | 7 +- .../EmergencyKits/BaseKitItem.cs | 11 +- TwoWeeksReady.Common/EmergencyKits/Kit.cs | 9 +- TwoWeeksReady.Common/EmergencyKits/KitItem.cs | 3 + .../Components/KitItemDisplay.razor | 99 +++-- .../Data/FunctionsRepository.cs | 2 +- .../Data/StubRepository.cs | 8 +- .../Pages/Kits/Details.razor | 93 ++++- .../TwoWeeksReady.Admin/Pages/Kits/List.razor | 18 +- .../EmergencyKits/BaseKitsApi.cs | 8 +- .../EmergencyKits/EmergencyKitsApi.cs | 23 +- solution architecture.xml | 1 + 28 files changed, 688 insertions(+), 907 deletions(-) create mode 100644 2wr-app/public/images/kits/build-a-kit.png create mode 100644 2wr-app/public/images/kits/go-kit.png create mode 100644 2wr-app/src/components/prepare/emergency-kits/emergency-kit-build.vue delete mode 100644 2wr-app/src/components/prepare/emergency-kits/emergency-kit-create.vue create mode 100644 2wr-app/src/components/prepare/emergency-kits/emergency-kit-details.vue delete mode 100644 2wr-app/src/components/prepare/emergency-kits/emergency-kit-edit.vue create mode 100644 2wr-app/src/views/prepare/emergency-kits/emergency-kit-build.vue delete mode 100644 2wr-app/src/views/prepare/emergency-kits/emergency-kit-create.vue create mode 100644 2wr-app/src/views/prepare/emergency-kits/emergency-kit-details.vue delete mode 100644 2wr-app/src/views/prepare/emergency-kits/emergency-kit-edit.vue create mode 100644 solution architecture.xml 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 0000000000000000000000000000000000000000..6ea7366c7c36eaea69e9e94062184a9dfb6f8e1a GIT binary patch literal 47084 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1ea374cb8b1b5a018b53be0f7bc8ee8c1e082a4 GIT binary patch literal 6075 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 literal 1874 zcmV-Y2d(&tP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc0%TB3R7L;)|2iro z{`uLi+TA)TBd*%r-(V*G`Psf%EAH~z`PHd9DkGzmg3#vM@w0!f+TGt^CW3Ng(p@Rd z=itDa!4 zcgxy$%-wsZ#(T}&e$3i`&Dw%UErib7g|NzoRx*dr+lSEFh|k%L(cF#E-ICGUmQF31 z!_t}4+?-x9pw`@=*V?1c*reLzsMy@9+S#nz+O1S9uG-zP-{Z2}-Lv54wA|dZ;N!I6 z=e6PIw&Lfv;^(;D-MQY}y58No-`Tu-LA~JJzTn-z;n~39+Q8-K!MvuybTUc-~f#^~zDqOP$>-(E z=i$refzJq+tlaV)a&Kd>E71p z+t%yg*6ZQd>D$)PL7Cg1Mf-|pYw?AhS#+u-iq z;_%_)?%L(=+U9U5=kVF*@Z9L|+UW7z>3lHie=_Rv+UoP)>wPKfektpMGwbr&>+;;} zhdk`^+U_1g87EcN!;_4eEL_S*ND zEcec#_?#{Hrce0y+xe+e`S;rTr9}FyRr;-2`mbC5`PoJ03~!%PE!C75Fju> zKtN1POkiYiaDae_h>MVvl$e{KsIb7$(A?nY@bvhy!PPMU00c%!L_t(&-tC!*cN0Y% z$5W3K@c>c8tAfXvRG>mbYP?bvv8V_-(0aBCTG4PsnzAvq+IkgYTeTW09%VhK4b%%# zO#lKJl2-={m9nf<qi(~82Rcwae|e@tz;WkHAoa+z0r{a? zApAa0n1qQMBN&q&+ymDT2Lwr(3}lgp*noUkhk$Xl5LqR1N+9SEhzb{FL{weNZYR`x z>zeAkK@_a_(1Cb-SW$;m0vE0fw+_QVR6z77DI=n)h9LrIISzV}H;C(yCxC;fwjKp! z5|iNya9ANqn52eCjNaB^J))|nX3@;PR3KE%Y+g`8@u_v^N>k|&8lgDifpw4D`!mZ(QJ#t5nv@)9mzbzKMXPTg+T0;v>H6XO1im?GCL?S#8WxM zScW5x3xvdL*h!>=Tg+^PBXGJ9@v9NRYosEGWES!D_n>da$H$Kf#39fp?}1p2!G1_R zwLi=zVipGCARI)65U;-vdZw|l@qU4L9JKj%5UVlRN5MWp?FU)JBR7Fo(IyZ#g6^Sj ztcLdP&e=84TTShovx(S!2k1W91mZ!^bM%eX(B88-`!w`6QTwG6MRSK3?R|Sd$(=U= zHl1hp^$sNUh_Syj)4$~hVz)Mj57M^35HWVr zu*oFktDUG(z*#F>UD%+zMkW6(a{(aiK@^Fs}{HGuAd_9_2t zc&kJpt`;dM(oxI6HF>RMrrJW7Z!$NDou$}uYWqsM!F-s1I-AVXf|wZT?T$nuo$WXj z3iS#IrHtKjSBYg7ON3$ePx}q=Up%5gM308RjsO4v M07*qoM6N<$f}F+zT>t<8 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/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 + + + + + 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/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 c4a9bd1..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 e62d423..b35c0f9 100644 --- a/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/FunctionsRepository.cs @@ -53,7 +53,7 @@ public async Task GetHazardInfoById(string id) public async Task SaveBaseKit(BaseKit kit) { - var response = await _httpClient.PutAsJsonAsync("basekits-update", kit); + var response = await _httpClient.PutAsJsonAsync("basekit-update", kit); if (response.IsSuccessStatusCode) { return await response.Content.ReadFromJsonAsync(); diff --git a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs index 631af41..e600d6d 100644 --- a/admin/TwoWeeksReady.Admin/Data/StubRepository.cs +++ b/admin/TwoWeeksReady.Admin/Data/StubRepository.cs @@ -14,23 +14,21 @@ public class StubRepository : IRepository new BaseKit { Id="1234", Name="Emergency Kit", - Icon="mdi-medical-bag", + IconUrl="", Items = new List { new BaseKitItem { Id="1", Name="Flashlight", Description="You need a flashlight if the power is out", - Photo="", QuantityUnit="Flashlights", - QuantityPerCount=1 + QuantityPerAdult=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 + QuantityPerAdult=14 }, } } 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 7e42741..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 { diff --git a/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs b/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs index a5a1156..cca6c09 100644 --- a/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs +++ b/api/TwoWeeksReady/EmergencyKits/BaseKitsApi.cs @@ -81,8 +81,12 @@ public async Task GetKit( log.LogInformation($"Getting single base kit"); Uri collectionUri = UriFactory.CreateDocumentCollectionUri("2wr", "basekits"); - var baseKit = client.CreateDocumentQuery(collectionUri, new FeedOptions { EnableCrossPartitionQuery = true }) - .Where(b => b.Id == id).FirstOrDefault(); + + var feedOptions = new FeedOptions { EnableCrossPartitionQuery = false }; + var baseKit = client.CreateDocumentQuery(collectionUri, feedOptions) + .Where(d => d.Id == id) + .AsEnumerable() + .FirstOrDefault(); if (baseKit == null) { 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/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 From 67912ad72b7980cd9dcb840609993bb6d0664b9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 22:53:56 +0000 Subject: [PATCH 20/20] Bump eventsource from 1.1.0 to 1.1.1 in /2wr-app Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: eventsource dependency-type: indirect ... Signed-off-by: dependabot[bot] --- 2wr-app/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/2wr-app/package-lock.json b/2wr-app/package-lock.json index dfce2b9..0060fe3 100644 --- a/2wr-app/package-lock.json +++ b/2wr-app/package-lock.json @@ -6889,9 +6889,9 @@ } }, "node_modules/eventsource": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", - "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", + "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", "dev": true, "dependencies": { "original": "^1.0.0" @@ -22925,9 +22925,9 @@ "dev": true }, "eventsource": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", - "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", + "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", "dev": true, "requires": { "original": "^1.0.0"