diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..ce3cdac7 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# Find the Dockerfile at this URL +# https://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/python/python39/python39-core-tools.Dockerfile +FROM mcr.microsoft.com/azure-functions/python:4-python3.9-core-tools + +# Copy library scripts to execute +COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ + +# Install Node.js, Azure Static Web Apps CLI and Azure Functions Core Tools +ARG NODE_VERSION="16" +ARG CORE_TOOLS_VERSION="4" +ENV NVM_DIR="/usr/local/share/nvm" \ + NVM_SYMLINK_CURRENT=true \ + PATH="${NVM_DIR}/current/bin:${PATH}" +RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \ + && su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" \ + && su vscode -c "umask 0002 && npm install --cache /tmp/empty-cache -g @azure/static-web-apps-cli" \ + && if [ $CORE_TOOLS_VERSION != "4" ]; then apt-get remove -y azure-functions-core-tools-4 && apt-get update && apt-get install -y "azure-functions-core-tools-${CORE_TOOLS_VERSION}"; fi \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..2dbe549e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,37 @@ +{ + "name": "Azure Static Web Apps", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Please look at runtime version support to make sure you're using compatible versions + // https://docs.microsoft.com/en-us/azure/azure-functions/supported-languages#languages-by-runtime-version + "NODE_VERSION": "16", + "CORE_TOOLS_VERSION": "4" + } + }, + "forwardPorts": [ 7071, 4280 ], + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-azurestaticwebapps", + "ms-dotnettools.csharp", + "ms-python.python", + "dbaeumer.vscode-eslint" + ] + } + }, + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "node --version", + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {} + } +} diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh new file mode 100644 index 00000000..f7829618 --- /dev/null +++ b/.devcontainer/library-scripts/node-debian.sh @@ -0,0 +1,170 @@ +#!/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 + ls -lah /home/${USERNAME}/.nvs || : + 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!" diff --git a/src/.vscode/extensions.json b/src/.vscode/extensions.json deleted file mode 100644 index de991f40..00000000 --- a/src/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "ms-azuretools.vscode-azurefunctions", - "ms-dotnettools.csharp" - ] -} diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json deleted file mode 100644 index 33df042a..00000000 --- a/src/.vscode/launch.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to .NET Functions", - "type": "coreclr", - "request": "attach", - "processId": "${command:azureFunctions.pickProcess}" - }, - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/bin/Debug/net6.0/shortenerTools.dll", - "args": [], - "cwd": "${workspaceFolder}/src", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json deleted file mode 100644 index ebf5724b..00000000 --- a/src/.vscode/tasks.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "clean (functions)", - "command": "dotnet", - "args": [ - "clean", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src" - } - }, - { - "label": "build (functions)", - "command": "dotnet", - "args": [ - "build", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean (functions)", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src" - } - }, - { - "label": "clean release (functions)", - "command": "dotnet", - "args": [ - "clean", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src" - } - }, - { - "label": "publish (functions)", - "command": "dotnet", - "args": [ - "publish", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean release (functions)", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src" - } - }, - { - "type": "func", - "dependsOn": "build (functions)", - "options": { - "cwd": "${workspaceFolder}/src/bin/Debug/net6.0" - }, - "command": "host start", - "isBackground": true, - "problemMatcher": "$func-dotnet-watch" - }, - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/src/shortenerTools.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/src/shortenerTools.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/src/shortenerTools.csproj" - ], - "problemMatcher": "$msCompile" - } - ] -} diff --git a/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.csproj b/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.csproj index 1c4f3ea3..752efae6 100644 --- a/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.csproj +++ b/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.csproj @@ -1,13 +1,14 @@ - net6.0 + net8.0 enable enable + diff --git a/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.sln b/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.sln new file mode 100644 index 00000000..6f9c8309 --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.Core/Cloud5mins.ShortenerTools.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud5mins.ShortenerTools.Core", "Cloud5mins.ShortenerTools.Core.csproj", "{139FABFE-571E-4B26-822C-781B1A3BA456}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {139FABFE-571E-4B26-822C-781B1A3BA456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {139FABFE-571E-4B26-822C-781B1A3BA456}.Debug|Any CPU.Build.0 = Debug|Any CPU + {139FABFE-571E-4B26-822C-781B1A3BA456}.Release|Any CPU.ActiveCfg = Release|Any CPU + {139FABFE-571E-4B26-822C-781B1A3BA456}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4444C53C-16B1-4EE5-BB65-90A1CB088D3C} + EndGlobalSection +EndGlobal diff --git a/src/Cloud5mins.ShortenerTools.Core/Service/ShortenerToolException.cs b/src/Cloud5mins.ShortenerTools.Core/Service/ShortenerToolException.cs new file mode 100644 index 00000000..9d27c4d8 --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.Core/Service/ShortenerToolException.cs @@ -0,0 +1,16 @@ +using System.Net; + +namespace Cloud5mins.ShortenerTools.Core.Services; + +public class ShortenerToolException: Exception +{ + public HttpStatusCode StatusCode { get; set; } + public ShortenerToolException(string message) : base(message) + { + } + + public ShortenerToolException(HttpStatusCode statusCode, string message) : base(message) + { + StatusCode = statusCode; + } +} \ No newline at end of file diff --git a/src/Cloud5mins.ShortenerTools.Core/Service/UrlServices.cs b/src/Cloud5mins.ShortenerTools.Core/Service/UrlServices.cs new file mode 100644 index 00000000..89f695e6 --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.Core/Service/UrlServices.cs @@ -0,0 +1,196 @@ +using Cloud5mins.ShortenerTools; +using Cloud5mins.ShortenerTools.Core.Domain; +using Cloud5mins.ShortenerTools.Core.Messages; +using Microsoft.Extensions.Logging; +using System.Net; + +namespace Cloud5mins.ShortenerTools.Core.Services; + +public class UrlServices +{ + private readonly ShortenerSettings _settings; + private readonly ILogger _logger; + private readonly StorageTableHelper _stgHelper; + + public UrlServices(ShortenerSettings settings, ILogger logger) + { + _settings = settings; + _logger = logger; + } + + private StorageTableHelper StgHelper => _stgHelper ?? new StorageTableHelper(_settings.DataStorage); + + public async Task Archive(ShortUrlEntity input) + { + ShortUrlEntity result = await _stgHelper.ArchiveShortUrlEntity(input); + return result; + } + public async Task Redirect(string shortUrl) + { + string redirectUrl = "https://azure.com"; + try + { + if (!string.IsNullOrWhiteSpace(shortUrl)) + { + redirectUrl = _settings.DefaultRedirectUrl ?? redirectUrl; + + var tempUrl = new ShortUrlEntity(string.Empty, shortUrl); + var newUrl = await StgHelper.GetShortUrlEntity(tempUrl); + + if (newUrl != null) + { + _logger.LogInformation($"Found it: {newUrl.Url}"); + newUrl.Clicks++; + await StgHelper.SaveClickStatsEntity(new ClickStatsEntity(newUrl.RowKey)); + await StgHelper.SaveShortUrlEntity(newUrl); + redirectUrl = WebUtility.UrlDecode(newUrl.ActiveUrl); + } + } + else + { + _logger.LogInformation("Bad Link, resorting to fallback."); + } + } + catch (Exception ex) + { + _logger.LogInformation($"Problem accessing storage: {ex.Message}"); + } + return redirectUrl; + } + + public async Task List(string host) + { + _logger.LogInformation($"Starting UrlList..."); + + var result = new ListResponse(); + string userId = string.Empty; + + try + { + result.UrlList = await StgHelper.GetAllShortUrlEntities(); + result.UrlList = result.UrlList.Where(p => !(p.IsArchived ?? false)).ToList(); + foreach (ShortUrlEntity url in result.UrlList) + { + url.ShortUrl = Utility.GetShortUrl(host, url.RowKey); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + throw; + } + + return result; + } + + public async Task Create(ShortRequest input, string host) + { + ShortResponse result; + + try + { + + // If the Url parameter only contains whitespaces or is empty return with BadRequest. + if (string.IsNullOrWhiteSpace(input.Url)) + { + throw new ShortenerToolException(HttpStatusCode.BadRequest, "The url parameter can not be empty."); + } + + // Validates if input.url is a valid aboslute url, aka is a complete refrence to the resource, ex: http(s)://google.com + if (!Uri.IsWellFormedUriString(input.Url, UriKind.Absolute)) + { + throw new ShortenerToolException(HttpStatusCode.BadRequest, $"{input.Url} is not a valid absolute Url. The Url parameter must start with 'http://' or 'http://'."); + } + + string longUrl = input.Url.Trim(); + string vanity = string.IsNullOrWhiteSpace(input.Vanity) ? "" : input.Vanity.Trim(); + string title = string.IsNullOrWhiteSpace(input.Title) ? "" : input.Title.Trim(); + + ShortUrlEntity newRow; + + if (!string.IsNullOrEmpty(vanity)) + { + newRow = new ShortUrlEntity(longUrl, vanity, title, input.Schedules); + + if (await StgHelper.IfShortUrlEntityExist(newRow)) + { + throw new ShortenerToolException(HttpStatusCode.Conflict, "This Short URL already exist."); + } + } + else + { + newRow = new ShortUrlEntity(longUrl, await Utility.GetValidEndUrl(vanity, StgHelper), title, input.Schedules); + } + + await StgHelper.SaveShortUrlEntity(newRow); + + result = new ShortResponse(host, newRow.Url, newRow.RowKey, newRow.Title); + + _logger.LogInformation("Short Url created."); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + throw; + } + + return result; + } + + public async Task Update(ShortUrlEntity input, string host) + { + ShortUrlEntity result; + + try + { + // If the Url parameter only contains whitespaces or is empty return with BadRequest. + if (string.IsNullOrWhiteSpace(input.Url)) + { + throw new ShortenerToolException(HttpStatusCode.BadRequest, "The url parameter can not be empty."); + } + + // Validates if input.url is a valid aboslute url, aka is a complete refrence to the resource, ex: http(s)://google.com + if (!Uri.IsWellFormedUriString(input.Url, UriKind.Absolute)) + { + throw new ShortenerToolException(HttpStatusCode.BadRequest, $"{input.Url} is not a valid absolute Url. The Url parameter must start with 'http://' or 'http://'."); + } + + result = await StgHelper.UpdateShortUrlEntity(input); + result.ShortUrl = Utility.GetShortUrl(host, result.RowKey); + + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + throw; + } + + return result; + } + + + public async Task ClickStatsByDay(UrlClickStatsRequest input, string host) + { + var result = new ClickDateList(); + try + { + var rawStats = await StgHelper.GetAllStatsByVanity(input.Vanity); + + result.Items = rawStats.GroupBy(s => DateTime.Parse(s.Datetime).Date) + .Select(stat => new ClickDate + { + DateClicked = stat.Key.ToString("yyyy-MM-dd"), + Count = stat.Count() + }).OrderBy(s => DateTime.Parse(s.DateClicked).Date).ToList(); + + result.Url = Utility.GetShortUrl(host, input.Vanity); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + throw; + } + return result; + } +} + diff --git a/src/Cloud5mins.ShortenerTools.Core/Service/Utility.cs b/src/Cloud5mins.ShortenerTools.Core/Service/Utility.cs new file mode 100644 index 00000000..3bbd7e1e --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.Core/Service/Utility.cs @@ -0,0 +1,70 @@ +using Cloud5mins.ShortenerTools.Core.Domain; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Threading.Tasks; + + + +namespace Cloud5mins.ShortenerTools +{ + public static class Utility + { + //reshuffled for randomisation, same unique characters just jumbled up, you can replace with your own version + private const string ConversionCode = "FjTG0s5dgWkbLf_8etOZqMzNhmp7u6lUJoXIDiQB9-wRxCKyrPcv4En3Y21aASHV"; + private static readonly int Base = ConversionCode.Length; + //sets the length of the unique code to add to vanity + private const int MinVanityCodeLength = 5; + + public static async Task GetValidEndUrl(string vanity, StorageTableHelper stgHelper) + { + if (string.IsNullOrEmpty(vanity)) + { + var newKey = await stgHelper.GetNextTableId(); + string getCode() => Encode(newKey); + if (await stgHelper.IfShortUrlEntityExistByVanity(getCode())) + return await GetValidEndUrl(vanity, stgHelper); + + return string.Join(string.Empty, getCode()); + } + else + { + return string.Join(string.Empty, vanity); + } + } + + public static string Encode(int i) + { + if (i == 0) + return ConversionCode[0].ToString(); + + return GenerateUniqueRandomToken(i); + } + + public static string GetShortUrl(string host, string vanity) + { + return host + "/" + vanity; + } + + // generates a unique, random, and alphanumeric token for the use as a url + //(not entirely secure but not sequential so generally not guessable) + public static string GenerateUniqueRandomToken(int uniqueId) + { + using (var generator = RandomNumberGenerator.Create()) + { + //minimum size I would suggest is 5, longer the better but we want short URLs! + var bytes = new byte[MinVanityCodeLength]; + generator.GetBytes(bytes); + var chars = bytes + .Select(b => ConversionCode[b % ConversionCode.Length]); + var token = new string(chars.ToArray()); + var reversedToken = string.Join(string.Empty, token.Reverse()); + return uniqueId + reversedToken; + } + } + } +} \ No newline at end of file diff --git a/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.csproj b/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.csproj index 982a1b53..b3f928ff 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.csproj +++ b/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.csproj @@ -1,15 +1,21 @@ - net6.0 + net8.0 v4 Exe + + + + + + - - - + + + diff --git a/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.sln b/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.sln new file mode 100644 index 00000000..2525eefd --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.Functions/Cloud5mins.ShortenerTools.Functions.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud5mins.ShortenerTools.Functions", "Cloud5mins.ShortenerTools.Functions.csproj", "{24117867-C5C2-4BE3-A31D-5A9CB08DB917}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {24117867-C5C2-4BE3-A31D-5A9CB08DB917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24117867-C5C2-4BE3-A31D-5A9CB08DB917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24117867-C5C2-4BE3-A31D-5A9CB08DB917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24117867-C5C2-4BE3-A31D-5A9CB08DB917}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {77520AE4-6589-4ED2-BCF5-B15FD42E4855} + EndGlobalSection +EndGlobal diff --git a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlArchive.cs b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlArchive.cs index 0e35338a..f5e74ba8 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlArchive.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlArchive.cs @@ -26,6 +26,7 @@ */ using Cloud5mins.ShortenerTools.Core.Domain; +using Cloud5mins.ShortenerTools.Core.Services; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; @@ -52,35 +53,23 @@ public UrlArchive(ILoggerFactory loggerFactory, ShortenerSettings settings) [Function("UrlArchive")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "api/UrlArchive")] HttpRequestData req, + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "UrlArchive")] HttpRequestData req, ExecutionContext context) { _logger.LogInformation($"HTTP trigger - UrlArchive"); - string userId = string.Empty; - ShortUrlEntity input; ShortUrlEntity result; try { // Validation of the inputs - if (req == null) + ShortUrlEntity input = await InputValidator.ValidateShortUrlEntity(req); + if(input == null) { return req.CreateResponse(HttpStatusCode.NotFound); } - using (var reader = new StreamReader(req.Body)) - { - var body = await reader.ReadToEndAsync(); - input = JsonSerializer.Deserialize(body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (input == null) - { - return req.CreateResponse(HttpStatusCode.NotFound); - } - } - - StorageTableHelper stgHelper = new StorageTableHelper(_settings.DataStorage); - - result = await stgHelper.ArchiveShortUrlEntity(input); + var urlServices = new UrlServices(_settings, _logger); + result = await urlServices.Archive(input); } catch (Exception ex) { diff --git a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlClickStatsByDay.cs b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlClickStatsByDay.cs index e64299e5..d456737a 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlClickStatsByDay.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlClickStatsByDay.cs @@ -24,6 +24,7 @@ using Cloud5mins.ShortenerTools.Core.Domain; using Cloud5mins.ShortenerTools.Core.Messages; +using Cloud5mins.ShortenerTools.Core.Services; using Google.Protobuf.WellKnownTypes; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; @@ -51,46 +52,24 @@ public UrlClickStatsByDay(ILoggerFactory loggerFactory, ShortenerSettings settin [Function("UrlClickStatsByDay")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "api/UrlClickStatsByDay")] HttpRequestData req, + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "UrlClickStatsByDay")] HttpRequestData req, ExecutionContext context) { _logger.LogInformation($"HTTP trigger: UrlClickStatsByDay"); - string userId = string.Empty; - UrlClickStatsRequest input; var result = new ClickDateList(); - // Validation of the inputs - if (req == null) + UrlClickStatsRequest input = await InputValidator.ValidateUrlClickStatsRequest(req); + if(input == null) { return req.CreateResponse(HttpStatusCode.NotFound); } - + try { - using (var reader = new StreamReader(req.Body)) - { - var strBody = await reader.ReadToEndAsync(); - input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (input == null) - { - return req.CreateResponse(HttpStatusCode.NotFound); - } - } - - StorageTableHelper stgHelper = new StorageTableHelper(_settings.DataStorage); - - var rawStats = await stgHelper.GetAllStatsByVanity(input.Vanity); - - result.Items = rawStats.GroupBy(s => DateTime.Parse(s.Datetime).Date) - .Select(stat => new ClickDate - { - DateClicked = stat.Key.ToString("yyyy-MM-dd"), - Count = stat.Count() - }).OrderBy(s => DateTime.Parse(s.DateClicked).Date).ToList(); - var host = string.IsNullOrEmpty(_settings.CustomDomain) ? req.Url.Host : _settings.CustomDomain.ToString(); - result.Url = Utility.GetShortUrl(host, input.Vanity); + var urlServices = new UrlServices(_settings, _logger); + result = await urlServices.ClickStatsByDay(input, host); } catch (Exception ex) { diff --git a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlCreate.cs b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlCreate.cs index 80373f9d..bfe72fe9 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlCreate.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlCreate.cs @@ -22,6 +22,7 @@ using Cloud5mins.ShortenerTools.Core.Domain; using Cloud5mins.ShortenerTools.Core.Messages; +using Cloud5mins.ShortenerTools.Core.Services; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; @@ -48,79 +49,30 @@ public UrlCreate(ILoggerFactory loggerFactory, ShortenerSettings settings) [Function("UrlCreate")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "api/UrlCreate")] HttpRequestData req, + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "UrlCreate")] HttpRequestData req, ExecutionContext context ) { _logger.LogInformation($"__trace creating shortURL: {req}"); - string userId = string.Empty; - ShortRequest input; - var result = new ShortResponse(); - + ShortResponse result; try { // Validation of the inputs - if (req == null) + ShortRequest input = await InputValidator.ValidateShortRequest(req); + if(input == null) { return req.CreateResponse(HttpStatusCode.NotFound); } - using (var reader = new StreamReader(req.Body)) - { - var strBody = await reader.ReadToEndAsync(); - input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (input == null) - { - return req.CreateResponse(HttpStatusCode.NotFound); - } - } - - // If the Url parameter only contains whitespaces or is empty return with BadRequest. - if (string.IsNullOrWhiteSpace(input.Url)) - { - var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); - await badResponse.WriteAsJsonAsync(new { Message = "The url parameter can not be empty." }); - return badResponse; - } - - // Validates if input.url is a valid aboslute url, aka is a complete refrence to the resource, ex: http(s)://google.com - if (!Uri.IsWellFormedUriString(input.Url, UriKind.Absolute)) - { - var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); - await badResponse.WriteAsJsonAsync(new { Message = $"{input.Url} is not a valid absolute Url. The Url parameter must start with 'http://' or 'http://'." }); - return badResponse; - } - - StorageTableHelper stgHelper = new StorageTableHelper(_settings.DataStorage); - - string longUrl = input.Url.Trim(); - string vanity = string.IsNullOrWhiteSpace(input.Vanity) ? "" : input.Vanity.Trim(); - string title = string.IsNullOrWhiteSpace(input.Title) ? "" : input.Title.Trim(); - - - ShortUrlEntity newRow; - - if (!string.IsNullOrEmpty(vanity)) - { - newRow = new ShortUrlEntity(longUrl, vanity, title, input.Schedules); - if (await stgHelper.IfShortUrlEntityExist(newRow)) - { - var badResponse = req.CreateResponse(HttpStatusCode.Conflict); - await badResponse.WriteAsJsonAsync(new { Message = "This Short URL already exist." }); - return badResponse; - } - } - else - { - newRow = new ShortUrlEntity(longUrl, await Utility.GetValidEndUrl(vanity, stgHelper), title, input.Schedules); - } - - await stgHelper.SaveShortUrlEntity(newRow); - - var host = string.IsNullOrEmpty(_settings.CustomDomain) ? req.Url.Host : _settings.CustomDomain.ToString(); - result = new ShortResponse(host, newRow.Url, newRow.RowKey, newRow.Title); - - _logger.LogInformation("Short Url created."); + var host = string.IsNullOrEmpty(_settings.CustomDomain) ? req.Url.Host : _settings.CustomDomain; + var urlServices = new UrlServices(_settings, _logger); + result = await urlServices.Create(input, host); + } + catch (ShortenerToolException shortEx) + { + var badResponse = req.CreateResponse(shortEx.StatusCode); + await badResponse.WriteAsJsonAsync(new { shortEx.Message }); + return badResponse; } catch (Exception ex) { diff --git a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlList.cs b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlList.cs index 97c78e81..2cd3817c 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlList.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlList.cs @@ -16,6 +16,7 @@ */ using Cloud5mins.ShortenerTools.Core.Domain; +using Cloud5mins.ShortenerTools.Core.Services; using Cloud5mins.ShortenerTools.Core.Messages; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; @@ -42,24 +43,14 @@ public UrlList(ILoggerFactory loggerFactory, ShortenerSettings settings) [Function("UrlList")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "api/UrlList")] HttpRequestData req, ExecutionContext context) + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "UrlList")] HttpRequestData req, ExecutionContext context) { - _logger.LogInformation($"Starting UrlList..."); - var result = new ListResponse(); - string userId = string.Empty; - - StorageTableHelper stgHelper = new StorageTableHelper(_settings.DataStorage); - try { - result.UrlList = await stgHelper.GetAllShortUrlEntities(); - result.UrlList = result.UrlList.Where(p => !(p.IsArchived ?? false)).ToList(); var host = string.IsNullOrEmpty(_settings.CustomDomain) ? req.Url.Host : _settings.CustomDomain; - foreach (ShortUrlEntity url in result.UrlList) - { - url.ShortUrl = Utility.GetShortUrl(host, url.RowKey); - } + var urlServices = new UrlServices(_settings, _logger); + result = await urlServices.List(host); } catch (Exception ex) { diff --git a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlRedirect.cs b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlRedirect.cs.bak similarity index 51% rename from src/Cloud5mins.ShortenerTools.Functions/Functions/UrlRedirect.cs rename to src/Cloud5mins.ShortenerTools.Functions/Functions/UrlRedirect.cs.bak index f7378881..18e0b2e6 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlRedirect.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlRedirect.cs.bak @@ -1,4 +1,5 @@ using Cloud5mins.ShortenerTools.Core.Domain; +using Cloud5mins.ShortenerTools.Core.Services; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; @@ -26,31 +27,9 @@ public async Task Run( string shortUrl, ExecutionContext context) { - string redirectUrl = "https://azure.com"; - - - if (!string.IsNullOrWhiteSpace(shortUrl)) - { - redirectUrl = _settings.DefaultRedirectUrl ?? redirectUrl; - - StorageTableHelper stgHelper = new StorageTableHelper(_settings.DataStorage); - - var tempUrl = new ShortUrlEntity(string.Empty, shortUrl); - var newUrl = await stgHelper.GetShortUrlEntity(tempUrl); - - if (newUrl != null) - { - _logger.LogInformation($"Found it: {newUrl.Url}"); - newUrl.Clicks++; - await stgHelper.SaveClickStatsEntity(new ClickStatsEntity(newUrl.RowKey)); - await stgHelper.SaveShortUrlEntity(newUrl); - redirectUrl = WebUtility.UrlDecode(newUrl.ActiveUrl); - } - } - else - { - _logger.LogInformation("Bad Link, resorting to fallback."); - } + + UrlServices urlServices = new UrlServices(_settings, _logger); + string redirectUrl = await urlServices.Redirect(shortUrl); var res = req.CreateResponse(HttpStatusCode.Redirect); res.Headers.Add("Location", redirectUrl); diff --git a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlUpdate.cs b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlUpdate.cs index 7d839e2c..a6ac98b8 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlUpdate.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Functions/UrlUpdate.cs @@ -29,6 +29,7 @@ */ using Cloud5mins.ShortenerTools.Core.Domain; +using Cloud5mins.ShortenerTools.Core.Services; // using Microsoft.Azure.WebJobs; // using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.Functions.Worker; @@ -56,56 +57,31 @@ public UrlUpdate(ILoggerFactory loggerFactory, ShortenerSettings settings) [Function("UrlUpdate")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "api/UrlUpdate")] HttpRequestData req, + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "UrlUpdate")] HttpRequestData req, ExecutionContext context ) { _logger.LogInformation($"HTTP trigger - UrlUpdate"); - - string userId = string.Empty; - ShortUrlEntity input; ShortUrlEntity result; try { // Validation of the inputs - if (req == null) + ShortUrlEntity input = await InputValidator.ValidateShortUrlEntity(req); + if(input == null) { return req.CreateResponse(HttpStatusCode.NotFound); } - using (var reader = new StreamReader(req.Body)) - { - var strBody = await reader.ReadToEndAsync(); - input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (input == null) - { - return req.CreateResponse(HttpStatusCode.NotFound); - } - } - - // If the Url parameter only contains whitespaces or is empty return with BadRequest. - if (string.IsNullOrWhiteSpace(input.Url)) - { - var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); - await badRequest.WriteAsJsonAsync(new { Message = "The url parameter can not be empty." }); - return badRequest; - } - - // Validates if input.url is a valid aboslute url, aka is a complete refrence to the resource, ex: http(s)://google.com - if (!Uri.IsWellFormedUriString(input.Url, UriKind.Absolute)) - { - var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); - await badRequest.WriteAsJsonAsync(new { Message = $"{input.Url} is not a valid absolute Url. The Url parameter must start with 'http://' or 'http://'." }); - return badRequest; - } - - StorageTableHelper stgHelper = new StorageTableHelper(_settings.DataStorage); - - result = await stgHelper.UpdateShortUrlEntity(input); var host = string.IsNullOrEmpty(_settings.CustomDomain) ? req.Url.Host : _settings.CustomDomain.ToString(); - result.ShortUrl = Utility.GetShortUrl(host, result.RowKey); - + var urlServices = new UrlServices(_settings, _logger); + result = await urlServices.Update(input, host); + } + catch (ShortenerToolException shortEx) + { + var badResponse = req.CreateResponse(shortEx.StatusCode); + await badResponse.WriteAsJsonAsync(new { shortEx.Message }); + return badResponse; } catch (Exception ex) { diff --git a/src/Cloud5mins.ShortenerTools.Functions/Utility.cs b/src/Cloud5mins.ShortenerTools.Functions/Utility.cs index dc1a26b2..a68566dc 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/Utility.cs +++ b/src/Cloud5mins.ShortenerTools.Functions/Utility.cs @@ -11,61 +11,72 @@ +using Microsoft.Azure.Functions.Worker.Http; +using Cloud5mins.ShortenerTools.Core.Messages; +using System.IO; +using System.Text.Json; + namespace Cloud5mins.ShortenerTools { - public static class Utility + public static class InputValidator { - //reshuffled for randomisation, same unique characters just jumbled up, you can replace with your own version - private const string ConversionCode = "FjTG0s5dgWkbLf_8etOZqMzNhmp7u6lUJoXIDiQB9-wRxCKyrPcv4En3Y21aASHV"; - private static readonly int Base = ConversionCode.Length; - //sets the length of the unique code to add to vanity - private const int MinVanityCodeLength = 5; - public static async Task GetValidEndUrl(string vanity, StorageTableHelper stgHelper) + public static async Task ValidateShortRequest(HttpRequestData req) { - if (string.IsNullOrEmpty(vanity)) + ShortRequest input; + if (req == null) { - var newKey = await stgHelper.GetNextTableId(); - string getCode() => Encode(newKey); - if (await stgHelper.IfShortUrlEntityExistByVanity(getCode())) - return await GetValidEndUrl(vanity, stgHelper); - - return string.Join(string.Empty, getCode()); + return null; } - else + + using var reader = new StreamReader(req.Body); + var strBody = await reader.ReadToEndAsync(); + input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (input == null) { - return string.Join(string.Empty, vanity); + return null; } + return input; } - public static string Encode(int i) + + public static async Task ValidateShortUrlEntity(HttpRequestData req) { - if (i == 0) - return ConversionCode[0].ToString(); + ShortUrlEntity input; + if (req == null) + { + return null; + } - return GenerateUniqueRandomToken(i); + using var reader = new StreamReader(req.Body); + var strBody = await reader.ReadToEndAsync(); + input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (input == null) + { + return null; + } + return input; } - public static string GetShortUrl(string host, string vanity) + public static async Task ValidateUrlClickStatsRequest(HttpRequestData req) { - return host + "/" + vanity; - } + UrlClickStatsRequest input; + if (req == null) + { + return null; + } - // generates a unique, random, and alphanumeric token for the use as a url - //(not entirely secure but not sequential so generally not guessable) - public static string GenerateUniqueRandomToken(int uniqueId) - { - using (var generator = RandomNumberGenerator.Create()) + using var reader = new StreamReader(req.Body); + var strBody = await reader.ReadToEndAsync(); + input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (input == null) { - //minimum size I would suggest is 5, longer the better but we want short URLs! - var bytes = new byte[MinVanityCodeLength]; - generator.GetBytes(bytes); - var chars = bytes - .Select(b => ConversionCode[b % ConversionCode.Length]); - var token = new string(chars.ToArray()); - var reversedToken = string.Join(string.Empty, token.Reverse()); - return uniqueId + reversedToken; + return null; } + return input; } + + + } } \ No newline at end of file diff --git a/src/Cloud5mins.ShortenerTools.Functions/host.json b/src/Cloud5mins.ShortenerTools.Functions/host.json index 5ae5a72c..170dcb3a 100644 --- a/src/Cloud5mins.ShortenerTools.Functions/host.json +++ b/src/Cloud5mins.ShortenerTools.Functions/host.json @@ -10,7 +10,7 @@ }, "extensions": { "http": { - "routePrefix": "" + "routePrefix": "api" } } } \ No newline at end of file diff --git a/src/Cloud5mins.ShortenerTools.FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj b/src/Cloud5mins.ShortenerTools.FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj new file mode 100644 index 00000000..a21ef43a --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj @@ -0,0 +1,29 @@ + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/src/Cloud5mins.ShortenerTools.FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.sln b/src/Cloud5mins.ShortenerTools.FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.sln new file mode 100644 index 00000000..91a95831 --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud5mins.ShortenerTools.FunctionsLight", "Cloud5mins.ShortenerTools.FunctionsLight.csproj", "{8A8D339F-43CC-438C-8595-12DB42A7C16D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8A8D339F-43CC-438C-8595-12DB42A7C16D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A8D339F-43CC-438C-8595-12DB42A7C16D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A8D339F-43CC-438C-8595-12DB42A7C16D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A8D339F-43CC-438C-8595-12DB42A7C16D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8FCA429F-78B7-45ED-85B5-4BAA83D5BD49} + EndGlobalSection +EndGlobal diff --git a/src/Cloud5mins.ShortenerTools.FunctionsLight/Functions/UrlRedirect.cs b/src/Cloud5mins.ShortenerTools.FunctionsLight/Functions/UrlRedirect.cs new file mode 100644 index 00000000..f4759da2 --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.FunctionsLight/Functions/UrlRedirect.cs @@ -0,0 +1,44 @@ +using Cloud5mins.ShortenerTools.Core.Services; +using Cloud5mins.ShortenerTools.Core.Domain; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Cloud5mins.ShortenerTools.Functions +{ + public class UrlRedirect + { + private readonly ILogger _logger; + private readonly ShortenerSettings _settings; + + public UrlRedirect(ILoggerFactory loggerFactory, ShortenerSettings settings) + { + _logger = loggerFactory.CreateLogger(); + _logger.LogInformation("UrlRedirect in constructor"); + _settings = settings; + } + + [Function("UrlRedirect")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "{shortUrl}")] + HttpRequestData req, + string shortUrl, + ExecutionContext context) + { + _logger.LogInformation("Function reached"); + UrlServices UrlServices = new UrlServices(_settings, _logger); + _logger.LogInformation("Services created"); + _logger.LogInformation($"Redirecting {shortUrl}"); + string redirectUrl = await UrlServices.Redirect(shortUrl); + _logger.LogInformation("got the redirect url"); + var res = req.CreateResponse(HttpStatusCode.Redirect); + res.Headers.Add("Location", redirectUrl); + _logger.LogInformation("response created"); + return res; + + } + } +} diff --git a/src/Cloud5mins.ShortenerTools.FunctionsLight/Program.cs b/src/Cloud5mins.ShortenerTools.FunctionsLight/Program.cs new file mode 100644 index 00000000..46cf9970 --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.FunctionsLight/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Hosting; +using Cloud5mins.ShortenerTools.Core.Domain; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; + +ShortenerSettings shortenerSettings = null; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices((context, services) => + { + // Add our global configuration instance + services.AddSingleton(options => + { + var configuration = context.Configuration; + shortenerSettings = new ShortenerSettings(); + configuration.Bind(shortenerSettings); + return configuration; + }); + + // Add our configuration class + services.AddSingleton(options => { return shortenerSettings; }); + }) + .Build(); + +host.Run(); diff --git a/src/Cloud5mins.ShortenerTools.FunctionsLight/host.json b/src/Cloud5mins.ShortenerTools.FunctionsLight/host.json new file mode 100644 index 00000000..c13ccb7b --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.FunctionsLight/host.json @@ -0,0 +1,16 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "http": { + "routePrefix": "" + } + } +} \ No newline at end of file diff --git a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj index 0cca6eef..5a6e8401 100644 --- a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj +++ b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 disable enable Cloud5mins.ShortenerTools.TinyBlazorAdmin @@ -9,9 +9,9 @@ - - - + + + diff --git a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.sln b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.sln new file mode 100644 index 00000000..827fd1ae --- /dev/null +++ b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud5mins.ShortenerTools.TinyBlazorAdmin", "Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj", "{7BF063FC-0EB2-4A4A-87DE-5D81372AC390}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7BF063FC-0EB2-4A4A-87DE-5D81372AC390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BF063FC-0EB2-4A4A-87DE-5D81372AC390}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BF063FC-0EB2-4A4A-87DE-5D81372AC390}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BF063FC-0EB2-4A4A-87DE-5D81372AC390}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {45A68468-783E-44EB-A122-6DD5169AF8BA} + EndGlobalSection +EndGlobal diff --git a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/Statistics.razor b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/Statistics.razor index f08c3151..e617a1aa 100644 --- a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/Statistics.razor +++ b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/Statistics.razor @@ -83,7 +83,7 @@ else{ subTitle = (!String.IsNullOrEmpty(vanity))? $"Clicks for: {vanity}": "All clicks"; try{ CancellationToken cancellationToken = new CancellationToken(); - using var response = await Http.PostAsJsonAsync("/api/UrlClickStatsByDay", new UrlClickStatsRequest(vanity), cancellationToken); + using var response = await Http.PostAsJsonAsync("/api/api/UrlClickStatsByDay", new UrlClickStatsRequest(vanity), cancellationToken); if(response.IsSuccessStatusCode){ var jsonResult = await response.Content.ReadAsStringAsync(); clickStatsList = JsonSerializer.Deserialize(jsonResult); diff --git a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/UrlManager.razor b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/UrlManager.razor index 293cf0f8..acf47c68 100644 --- a/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/UrlManager.razor +++ b/src/Cloud5mins.ShortenerTools.TinyBlazorAdmin/Pages/UrlManager.razor @@ -285,7 +285,7 @@ else public async Task ArchiveShortUrl(ShortUrlEntity urlEntity) { - await Http.PostAsJsonAsync("/api/UrlArchive", urlEntity); + await Http.PostAsJsonAsync("/api/UrlList", urlEntity); await UpdateUIList(); } diff --git a/src/deployment/azureDeploy.json b/src/deployment/azureDeploy.json index d14321e7..56b4f43a 100644 --- a/src/deployment/azureDeploy.json +++ b/src/deployment/azureDeploy.json @@ -9,11 +9,12 @@ "description": "Name used as base-template to name the resources to be deployed in Azure." } }, - "existingSWAName": { + "deployingWhat":{ "type": "string", - "defaultValue": "SKIP-THIS-RESOURCE", + "allowedValues": ["Full UrlShortener", "Light UrlShortener"], + "defaultValue": "Light UrlShortener", "metadata": { - "description": "Optional (SKIP-THIS-RESOURCE): If provided, this is name of the the already created Static Web App in this resource group. If not provided: API will be standalone" + "description" : "Full UrlShortener deploys the complete but unsecure Azure function. Light UrlShortener deploys only the redirect Azure function and required the an admin tool (ex: TinyBlazorAdmin)" } }, "defaultRedirectUrl": { @@ -55,12 +56,10 @@ "variables": { "suffix": "[substring(toLower(uniqueString(resourceGroup().id, resourceGroup().location)),0,5)]", "funcAppName": "[toLower(concat(parameters('baseName'), '-', variables('suffix'), '-fa'))]", - "swaName": "[parameters('existingSWAName')]", - "deployTinyBlazorAdmin": "[if(equals(parameters('existingSWAName'), 'SKIP-THIS-RESOURCE'), 'false', 'true')]", "storageAccountName": "[tolower(concat(substring(parameters('baseName'), 0, min(length(parameters('baseName')),16)), variables('suffix'), 'sa'))]", "funcHhostingPlanName": "[concat(substring(parameters('baseName'), 0, min(length(parameters('baseName')),13)), '-', variables('suffix'), '-asp')]", "insightsAppName": "[concat(substring(parameters('baseName'), 0, min(length(parameters('baseName')),13)), '-', variables('suffix'), '-ai')]", - "functionProjectFolder": "src" + "functionProjectFolder": "[if(equals(parameters('deployingWhat'), 'Light UrlShortener'), 'src\\Cloud5mins.ShortenerTools.FunctionsLight', 'src\\Cloud5mins.ShortenerTools.Functions')]" }, "resources": [ { @@ -117,12 +116,20 @@ { "name": "defaultRedirectUrl", "value": "[parameters('defaultRedirectUrl')]" + }, + { + "name": "PROJECT", + "value": "[variables('functionProjectFolder')]" + }, + { + "name": "AzureFunctionsJobHost__extensions__http__routePrefix", + "value": "" } ] }, "serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('funcHhostingPlanName'))]", "use32BitWorkerProcess": true, - "netFrameworkVersion": "v6.0", + "netFrameworkVersion": "v8.0", "clientAffinityEnabled": true }, "resources": [ @@ -151,10 +158,7 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", - "[resourceId('Microsoft.Web/staticSites', variables('swaName'))]", - "[format('{0}/resourceGroups/{1}/providers/Microsoft.Web/staticSites/{2}/userProvidedFunctionApps/backend1', subscription().id, resourceGroup().name, variables('swaName'))]", - "[format('{0}/resourceGroups/{1}/providers/Microsoft.Web/staticSites/{2}/linkedBackends/backend1', subscription().id, resourceGroup().name, variables('swaName'))]" + "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]" ] }, { @@ -209,73 +213,6 @@ "properties": { "supportsHttpsTrafficOnly": true } - }, - { - "condition": "[equals(toLower(variables('deployTinyBlazorAdmin')), 'true')]", - "type": "Microsoft.Web/staticSites", - "name": "[variables('swaName')]", - "location": "[resourceGroup().location]", - "apiVersion": "2022-03-01", - "sku": { - "name": "Standard", - "tier": "Standard" - }, - "properties": { - "repositoryUrl": "[parameters('GitHubURL')]", - "branch": "[parameters('GitHubBranch')]", - "buildProperties": { - "appLocation": "./src/Cloud5mins.ShortenerTools.TinyBlazorAdmin", - "outputLocation": "wwwroot" - }, - "stagingEnvironmentPolicy": "Enabled", - "allowConfigFileUpdates": true, - "provider": "GitHub", - "enterpriseGradeCdnStatus": "Disabled" - }, - "resources": - [ - { - "condition": "[equals(toLower(variables('deployTinyBlazorAdmin')), 'true')]", - "type": "linkedBackends", - "apiVersion": "2022-03-01", - "name": "backend1", - "dependsOn": [ - "[resourceId('Microsoft.Web/staticSites', variables('swaName'))]", - "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]" - ], - "properties": { - "backendResourceId": "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", - "region": "[resourceGroup().location]" - } - }, - { - "condition": "[equals(toLower(variables('deployTinyBlazorAdmin')), 'true')]", - "type": "userProvidedFunctionApps", - "apiVersion": "2022-03-01", - "name": "backend1", - "dependsOn": [ - "[resourceId('Microsoft.Web/staticSites', variables('swaName'))]", - "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]" - ], - "properties": { - "functionAppResourceId": "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", - "functionAppRegion": "[resourceGroup().location]" - } - } - ] - }, - { - "condition": "[equals(toLower(variables('deployTinyBlazorAdmin')), 'true')]", - "type": "Microsoft.Web/staticSites/config", - "apiVersion": "2022-03-01", - "name": "[format('{0}/{1}', variables('swaName'), 'appsettings')]", - "properties": { - "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(concat('microsoft.insights/components/', variables('insightsAppName')), '2015-05-01').InstrumentationKey]", - "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(concat('microsoft.insights/components/', variables('insightsAppName')), '2015-05-01').InstrumentationKey]" - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/staticSites', variables('swaName'))]" - ] } ], "outputs": {} diff --git a/src/deployment/azureDeploy.params.dev.json b/src/deployment/azureDeploy.params.dev.json index 10e89400..9295d7f5 100644 --- a/src/deployment/azureDeploy.params.dev.json +++ b/src/deployment/azureDeploy.params.dev.json @@ -3,19 +3,22 @@ "contentVersion": "1.0.0.0", "parameters": { "baseName": { - "value": "shortenertool" + "value": "url2del" + }, + "deployingWhat":{ + "value": "Light UrlShortener" }, "existingSWAName": { - "value": "shortenertool-swa" + "value": "admin2del" }, "GitHubURL": { - "value": "https://github.com/ch-rob/AzUrlShortener" + "value": "https://github.com/fboucheros/AzUrlShortener" }, "GitHubBranch": { - "value": "UpdateDeployment2" + "value": "core-extra" }, "OwnerName": { - "value": "Chad V" + "value": "frank" }, "defaultRedirectUrl": { "value": "https://azure.com" diff --git a/src/deployment/azureDeploy.params.json b/src/deployment/azureDeploy.params.json index 3807c918..9465b0e3 100644 --- a/src/deployment/azureDeploy.params.json +++ b/src/deployment/azureDeploy.params.json @@ -3,16 +3,28 @@ "contentVersion": "1.0.0.0", "parameters": { "baseName": { - "value": "AzUrlShortTools" + "value": "shorter" }, "OwnerName": { - "value": "Me" + "value": "me" }, "GitHubBranch": { - "value": "main" + "value": "core-extra" }, "defaultRedirectUrl": { - "value": "https://azure.com" + "value": "https://github.com" + }, + "existingSWAName": { + "value": "short-test3" + }, + "deployingWhat": { + "value": "Light UrlShortener" + }, + "GitHubURL": { + "value": "https://github.com/fboucheros/AzUrlShortener.git" + }, + "repositoryToken": { + "value": "" } } } \ No newline at end of file diff --git a/src/deployment/debug.azcli b/src/deployment/debug.azcli index dd894cab..6cf1e658 100644 --- a/src/deployment/debug.azcli +++ b/src/deployment/debug.azcli @@ -1,19 +1,19 @@ -az group create -n crv-urlshortener-rg2 -l eastus +az group create -n short-test3 -l eastus2 ## validate with Parameter file -az deployment group validate -g crv-urlshortener-rg2 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose +az deployment group validate -g short-test3 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.json" --verbose ## deploy it with parameter file -# az deployment group create -g crv-urlshortener-rg2 -n urlshortenerv1 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose +# az deployment group create -g short-test2 -n urlshortenerv1 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose ## what-if -az deployment group create -g crv-urlshortener-rg2 -n urlshortenerv1 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose --what-what-if-result-format +az deployment group create -g short-test2 -n urlshortenerv1 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose --what-what-if-result-format ## deploy it -az deployment group create -g crv-urlshortener-rg2 -n urlshortenerv1 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose +az deployment group create -g short-test2 -n urlshortenerv1 --template-file "deployment/azureDeploy.json" --parameters "deployment/azureDeploy.params.dev.json" --verbose ## update setting -az functionapp config appsettings set --name c5murltest --resource-group crv-urlshortener-rg2 --settings "FUNCTIONS_WORKER_RUNTIME=dotnet-isolated" +az functionapp config appsettings set --name c5murltest --resource-group short-test2 --settings "FUNCTIONS_WORKER_RUNTIME=dotnet-isolated" diff --git a/src/package.json b/src/package.json new file mode 100644 index 00000000..7a038845 --- /dev/null +++ b/src/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@azure/static-web-apps-cli": "^1.1.6" + } +} diff --git a/src/shortenerTools.sln b/src/shortenerTools.sln index 24bd5b78..86bd5d3a 100644 --- a/src/shortenerTools.sln +++ b/src/shortenerTools.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud5mins.ShortenerTools.C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloud5mins.ShortenerTools.TinyBlazorAdmin", "Cloud5mins.ShortenerTools.TinyBlazorAdmin\Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj", "{B5FB0C0A-05BA-491B-9379-CE4BDF025B3B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.FunctionsLight", "Cloud5mins.ShortenerTools.FunctionsLight\Cloud5mins.ShortenerTools.FunctionsLight.csproj", "{51F31B6D-0D80-4CB3-8FC3-774B65AF66D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {B5FB0C0A-05BA-491B-9379-CE4BDF025B3B}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5FB0C0A-05BA-491B-9379-CE4BDF025B3B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5FB0C0A-05BA-491B-9379-CE4BDF025B3B}.Release|Any CPU.Build.0 = Release|Any CPU + {51F31B6D-0D80-4CB3-8FC3-774B65AF66D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51F31B6D-0D80-4CB3-8FC3-774B65AF66D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51F31B6D-0D80-4CB3-8FC3-774B65AF66D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51F31B6D-0D80-4CB3-8FC3-774B65AF66D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/swa-cli.config.json b/src/swa-cli.config.json index 76645e64..0ffc1acc 100644 --- a/src/swa-cli.config.json +++ b/src/swa-cli.config.json @@ -1,13 +1,17 @@ { "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", "configurations": { - "AzUrlShortener": { + "src": { "appLocation": "Cloud5mins.ShortenerTools.TinyBlazorAdmin", "apiLocation": "Cloud5mins.ShortenerTools.Functions", - "outputLocation": "bin/Debug/net6.0/wwwroot", + "outputLocation": "bin\\wwwroot", + "apiLanguage": "dotnetisolated", + "apiVersion": "6.0", "appBuildCommand": "dotnet publish -c Release -o bin", "apiBuildCommand": "dotnet publish -c Release", - "run": "dotnet watch run" + "run": "dotnet watch run", + "appDevserverUrl": "http://localhost:5000", + "apiPort": 7071 } } } \ No newline at end of file