diff --git a/.all-contributorsrc b/.all-contributorsrc index e48de31..0840b7b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,63 +1,63 @@ -{ - "files": [ - "README.md" - ], - "imageSize": 100, - "commit": false, - "contributors": [ - { - "login": "FBoucher", - "name": "Frank Boucher", - "avatar_url": "https://avatars3.githubusercontent.com/u/2404846?v=4", - "profile": "http://cloud5mins.com", - "contributions": [ - "doc", - "code", - "ideas" - ] - }, - { - "login": "jbrule", - "name": "jbrule", - "avatar_url": "https://avatars3.githubusercontent.com/u/765798?v=4", - "profile": "http://www.mayoclinic.org", - "contributions": [ - "doc" - ] - }, - { - "login": "cmatskas", - "name": "Christos Matskas", - "avatar_url": "https://avatars3.githubusercontent.com/u/4126750?v=4", - "profile": "https://cmatskas.com", - "contributions": [ - "security", - "bug" - ] - }, - { - "login": "ronhowe", - "name": "Ron Howe", - "avatar_url": "https://avatars1.githubusercontent.com/u/5210043?v=4", - "profile": "https://github.com/ronhowe", - "contributions": [ - "doc" - ] - }, - { - "login": "Mark-Phillipson", - "name": "Mark Phillipson", - "avatar_url": "https://avatars0.githubusercontent.com/u/16239024?v=4", - "profile": "https://github.com/Mark-Phillipson", - "contributions": [ - "doc" - ] - } - ], - "contributorsPerLine": 7, - "projectName": "TinyBlazorAdmin", - "projectOwner": "FBoucher", - "repoType": "github", - "repoHost": "https://github.com", - "skipCi": true -} +{ + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": false, + "contributors": [ + { + "login": "FBoucher", + "name": "Frank Boucher", + "avatar_url": "https://avatars3.githubusercontent.com/u/2404846?v=4", + "profile": "http://cloud5mins.com", + "contributions": [ + "doc", + "code", + "ideas" + ] + }, + { + "login": "jbrule", + "name": "jbrule", + "avatar_url": "https://avatars3.githubusercontent.com/u/765798?v=4", + "profile": "http://www.mayoclinic.org", + "contributions": [ + "doc" + ] + }, + { + "login": "cmatskas", + "name": "Christos Matskas", + "avatar_url": "https://avatars3.githubusercontent.com/u/4126750?v=4", + "profile": "https://cmatskas.com", + "contributions": [ + "security", + "bug" + ] + }, + { + "login": "ronhowe", + "name": "Ron Howe", + "avatar_url": "https://avatars1.githubusercontent.com/u/5210043?v=4", + "profile": "https://github.com/ronhowe", + "contributions": [ + "doc" + ] + }, + { + "login": "Mark-Phillipson", + "name": "Mark Phillipson", + "avatar_url": "https://avatars0.githubusercontent.com/u/16239024?v=4", + "profile": "https://github.com/Mark-Phillipson", + "contributions": [ + "doc" + ] + } + ], + "contributorsPerLine": 7, + "projectName": "TinyBlazorAdmin", + "projectOwner": "FBoucher", + "repoType": "github", + "repoHost": "https://github.com", + "skipCi": true +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 5f6b4c4..e8e9a78 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,12 @@ -# These are supported funding model platforms - -github: [fboucher]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +# These are supported funding model platforms + +github: [fboucher]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/azure-static-web-apps-icy-coast-0871ee210.yml b/.github/workflows/azure-static-web-apps-icy-coast-0871ee210.yml new file mode 100644 index 0000000..545a330 --- /dev/null +++ b/.github/workflows/azure-static-web-apps-icy-coast-0871ee210.yml @@ -0,0 +1,45 @@ +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - feature/security + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - feature/security + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_COAST_0871EE210 }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: "upload" + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: "src/client" # App source code path + api_location: "src/api" # Api source code path - optional + output_location: "wwwroot" # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_COAST_0871EE210 }} + action: "close" diff --git a/.gitignore b/.gitignore index 744f317..1d25b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,354 +1,357 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - - -#Igniore VSCode User Setting -**/.vscode/settings.json +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + + +#Igniore VSCode User Setting +**/.vscode/settings.json +src/TinyBlazorAdmin/wwwroot/appsettings.json +src/TinyBlazorAdmin/wwwroot/appsettings.Development.json + diff --git a/.vscode/launch.json b/.vscode/launch.json index eb7f575..e3e0a0b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,26 +1,26 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch and Debug Standalone Blazor WebAssembly App", - "type": "blazorwasm", - "request": "launch", - "cwd": "${workspaceFolder}/src/TinyBlazorAdmin" - }, - { - "name": "Attach to .NET Functions", - "type": "coreclr", - "request": "attach", - "processId": "${command:azureFunctions.pickProcess}" - } - ], - "compounds": [ - { - "name": "Client/Server", - "configurations": [ - "Attach to .NET Functions", - "Launch and Debug Standalone Blazor WebAssembly App" - ] - } - ] +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch and Debug Standalone Blazor WebAssembly App", + "type": "blazorwasm", + "request": "launch", + "cwd": "${workspaceFolder}/src/TinyBlazorAdmin" + }, + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ], + "compounds": [ + { + "name": "Client/Server", + "configurations": [ + "Attach to .NET Functions", + "Launch and Debug Standalone Blazor WebAssembly App" + ] + } + ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e3ac307..181ed8d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,81 +1,81 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "clean", - "command": "dotnet", - "args": [ - "clean", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src/AdminApi" - } - }, - { - "label": "build", - "command": "dotnet", - "args": [ - "build", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/scr/AdminApi" - } - }, - { - "label": "clean release", - "command": "dotnet", - "args": [ - "clean", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/scr/AdminApi" - } - }, - { - "label": "publish", - "command": "dotnet", - "args": [ - "publish", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean release", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/scr/AdminApi" - } - }, - { - "type": "func", - "dependsOn": "build", - "options": { - "cwd": "${workspaceFolder}/src/AdminApi/bin/Debug/net6.0" - }, - "command": "host start", - "isBackground": true, - "problemMatcher": "$func-watch" - } - ] +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src/AdminApi" + } + }, + { + "label": "build", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/scr/AdminApi" + } + }, + { + "label": "clean release", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/scr/AdminApi" + } + }, + { + "label": "publish", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/scr/AdminApi" + } + }, + { + "type": "func", + "dependsOn": "build", + "options": { + "cwd": "${workspaceFolder}/src/AdminApi/bin/Debug/net6.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-watch" + } + ] } \ No newline at end of file diff --git a/LICENSE b/LICENSE index 0db9ab4..8184489 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2020 Frank Boucher - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2020 Frank Boucher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cf9d0fd..da5ae9b 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,59 @@ -# TinyBlazorAdmin - -[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) - - - Admin tools for [Azure Url Shortener](https://github.com/FBoucher/AzUrlShortener) using [Blazor Single Page Application (webassembly)](https://azure.microsoft.com/services/app-service/static/?WT.mc_id=dotnet-0000-frbouche). - -The project is now at version 1 and ready to be used! ~~just getting started but should have a v1 ready in Summer 2020~~. It is using Azure Active Directory (AAD) as authentication for the user and to connect to the API (Azure Function). - -![Tiny Blazor Admin home page][tinyBA_home] - -Once authenticated you can manage your URLs and see some statistics. Thanks to [Syncfusion](https://www.syncfusion.com/blazor-components) for the community licences. Everyone can use Tiny Blazor Admin with that great look! - -![Tiny Blazor Admin URLs manager page][inyBA_urls] - -![Tiny Blazor Admin Statistics page][inyBA_stats] - - - -# Deployment - -Until an automatic deployment is created, you will need to deploy some part manually. [All the steps to deploy the TinyBlazorAdmin app into Azure are listed here](deployment.md). You can also run it somewhere else if you prefer, even locally. - - - -# Contributing - -If you find a bug or would like to add a feature, check out those resources: - -To see the current work in progress: [GLO boards](https://app.gitkraken.com/glo/board/XtpDU2ZLuQARV8y7) 'kanban board' - - -[TinyBlazorAdmin]: medias/TinyBlazorAdmin.png -[tinyBA_home]: medias/tinyBA_home.png -[inyBA_stats]: medias/inyBA_stats.png -[inyBA_urls]: medias/inyBA_urls.png - -## Contributors ✨ - -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - - - - - - - - - - - - -

Frank Boucher

📖 💻 🤔

jbrule

📖

Christos Matskas

🛡️ 🐛

Ron Howe

📖

Mark Phillipson

📖
- - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! +# TinyBlazorAdmin + +[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) + + + Admin tools for [Azure Url Shortener](https://github.com/FBoucher/AzUrlShortener) using [Blazor Single Page Application (webassembly)](https://azure.microsoft.com/services/app-service/static/?WT.mc_id=dotnet-0000-frbouche). + +The project is now at version 1 and ready to be used! ~~just getting started but should have a v1 ready in Summer 2020~~. It is using Azure Active Directory (AAD) as authentication for the user and to connect to the API (Azure Function). + +![Tiny Blazor Admin home page][tinyBA_home] + +Once authenticated you can manage your URLs and see some statistics. Thanks to [Syncfusion](https://www.syncfusion.com/blazor-components) for the community licences. Everyone can use Tiny Blazor Admin with that great look! + +![Tiny Blazor Admin URLs manager page][inyBA_urls] + +![Tiny Blazor Admin Statistics page][inyBA_stats] + + + +# Deployment + +Until an automatic deployment is created, you will need to deploy some part manually. [All the steps to deploy the TinyBlazorAdmin app into Azure are listed here](deployment.md). You can also run it somewhere else if you prefer, even locally. + + + +# Contributing + +If you find a bug or would like to add a feature, check out those resources: + +To see the current work in progress: [GLO boards](https://app.gitkraken.com/glo/board/XtpDU2ZLuQARV8y7) 'kanban board' + + +[TinyBlazorAdmin]: medias/TinyBlazorAdmin.png +[tinyBA_home]: medias/tinyBA_home.png +[inyBA_stats]: medias/inyBA_stats.png +[inyBA_urls]: medias/inyBA_urls.png + +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + +

Frank Boucher

📖 💻 🤔

jbrule

📖

Christos Matskas

🛡️ 🐛

Ron Howe

📖

Mark Phillipson

📖
+ + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/deployment.md b/deployment.md index 4249918..a558470 100644 --- a/deployment.md +++ b/deployment.md @@ -1,155 +1,155 @@ -# Deployment - -Until a "full automatic" deployment is created, here are all the steps to deploy the TinyBlazorAdmin app into Azure. You can run it somewhere else and even locally. - -## First thing first - -You need to **fork this repo** into your own account. You will need to update the configuration (this document will explain when and what), therefore it needs to be yours. - -To fork a GitHub repository click on the fork button on the top right of the screen. If you need more detail have a look to this GitHub doc: [Fork a repo -](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo). - -## Deploy AzUrlShortener (the Backend) - -This project is a frontend only so you will need to deploy the [Azure Url Shortener](https://github.com/FBoucher/AzUrlShortener) in "headless mode". Do to it, click the blue button below and make sure to select **none** as Frontend - -[![Deploy Backend to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/?WT.mc_id=urlshortener-github-frbouche#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FFBoucher%2FAzUrlShortener%2Fmain%2Fdeployment%2FazureDeploy.json) - -![CreateBackend][CreateBackend] - - -## Deploy TinyBlazorAdmin (the Frontend) - -There are many ways you could run Tiny Blazor Admin website. In this deployment, we will use the new [Azure Static Web App (SWA)](https://azure.microsoft.com/en-ca/services/app-service/static/?WT.mc_id=tinyblazoradmin-github-frbouche). However, because the TinyBlazorAdmin use Azure Active Directory (AAD), we need a standalone Azure Function (deployed at the previous step). - -Open Azure portal (portal.azure.com), open the **resource group** where you created the backend (ex: streamDemo is our case). Click the "**+**" and search **Static Web App**, and click the *Create* button. - -![Creating swa][swa_create1] - -> Note: You will need to **Authorize Azure Static Web Apps**, to have access to _your_ GitHub repository (the one created when you forked the project). This is required because SWA uses GitHub Action to deploy. - -![Creating swa part 2][swa_create2] - -Select your organization, repository and branch (ex: main). - -![Creating swa part 3][swa_create3] - -Select **Blazor** as your *Build Presets*. The *App location* needs to be the location of the project file; in our case `src/TinyBlazorAdmin/`. The *App artifact location* can be left to wwwroot. - ->Note: We don't need the Api location, because AzURLShortener is deployed in a full standalone Azure Function. - -Once it's all filled, click the Review, and create button. It will takes a few minutes to get deployed. During this time let's create and configure our security components. - -## Create Azure Active Directory (AAD) Components - -### Create AAD App for the Fontend - -We need a Service Principal that we will use to authenticate our user to Azure Active Directory (AAD). To achieve this we will create an application registration in Azure. - -From the Azure Portal (portal.azure.com), open the **Azure Active Directory** page. From the left option menu select **App registrations**, then create a new registration. Use a name that will help you to remember that it's for the TinyBlazorAdmin website (ex: TinyAdminApp) (1) - -![RegisterClientApp][RegisterClientApp] - -For the Redirect URL use **Web** (3) and enter the URL of the Azure Static WebApp deployed previously and add `/authentication/login-callback`. It should look lsomething like this: - -``` -https://bolly-tiger-04a15beef.azurestaticapps.net/authentication/login-callback - -``` - -**Note the ClientID and TenantID.** - -If you need to retrieve the ClientID and TenantID, they will be display at the top of the page once you select an app in the portal. - -![Create a new registration][newRegistration] - -Go back in the Authentication and in the section Implicit grant check the checkbox `Access Token` and `ID Tokens` - -![tokensaccess][tokensaccess] - -### Create App for the Azure Function - -We need a second App registration, this time to let the Azure Function validate that user information contained in the token is valid. - -![First steps to create the AD App registration][azFunction_Auth_step1] - -From the Azure Portal, go to your Azure Function. From the left panel select the *Authentication / Authorization* (1) option. Enable the *App Service Authentication* (2) and click on *Azure Active Directory*. - -![Configuring the App registration][azFunction_Auth_step2] - -1. Select Express. -2. Make sure *Create New AD App* is selected. -3. Give the AD App a name. -4. Click Ok. -5. DON'T FORGET TO CLICK THE SAVE BUTTON - -Now, we need to configure the brand-new Ad App registration. Still from the Azure portal open the *Active Directory* blade. Select the *App registration* option from the left menu. Then select the application you just created. - -![ConfigAzFuncADapp][ConfigAzFuncADapp] - -1. From the left panel click the *Expose an API* option. -2. Click the *Add a client application*. -3. Enter the ClientID of the Frontend App (the first one created). -4. Check the Impersonation checkbox. -5. Save by clicking *Add application*. - - - -## Configure Backend and Frontend to Work Together - -Now in your GitHub it's time to update the settings. The code needs to know the AD app to use and the Azure Function to call. Update those values inside `TinyBlazorAdmin\wwwroot\appsettings.json` - -> The **Endpoint** _must_ ends with a `/` - -```json -{ - "AzureAd": { - "Authority": "https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx", - "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx", - "ValidateAuthority": true - }, - "UrlShortenerSecuredService": { - "Endpoint": "https://__azFunction_URL__.azurewebsites.net/" - } -} -``` - -## Enabeling Cross-Origin Resource Sharing (CORS) - - -First we need the url of the caller (aka TinyBlazorAdmin). From the Azure Portal, open the TinyBlazorAdmin SWA blade. Note the URL display in the top right of the page this is the URL of your admin page. - -![swa_URL][swa_URL] - -Now we need to add this URL to the list in the CORS of the Azure Function that run AzUrlShortener. From the Azure portal open the blade of AzUrlShortener. From the left menu search CORS, and click it. Add the URL of the SWA and don't forget to save. - - -![azFunction_CORS][azFunction_CORS] - -## Try it! - -Voila, the deployment is now completed. You can test it by creating a new short URL using the admin SWA. - - -## Adding Custom Domain - -To add a custom domain to your AzUrlShortener & TinyBlazorAdmin, [follow these steps](https://github.com/FBoucher/AzUrlShortener/blob/main/post-deployment-configuration.md#add-a-custom-domain) from the the AzUrlShortener repo. - - -[CreateBackend]: medias/CreateBackend.png -[newRegistration]: medias/newRegistration.png -[AddPolicy]: medias/AddPolicy.png -[EditKeyVault]: medias/EditKeyVault.png -[CreateSecrets]: medias/CreateSecrets.png -[azFunction_Auth_step1]: medias/azFunction_Auth_step1.png -[azFunction_Auth_step2]: medias/azFunction_Auth_step2.png -[ConfigAzFuncADapp]: medias/ConfigAzFuncADapp.png -[tokensaccess]: medias/tokensaccess.png -[swa_create1]: medias/swa_create1.png -[swa_create2]: medias/swa_create2.png -[swa_create3]: medias/swa_create3.png -[swa_URL]: medias/swa_URL.png -[azFunction_CORS]: medias/azFunction_CORS.png -[RegisterClientApp]: medias/RegisterClientApp.png - +# Deployment + +Until a "full automatic" deployment is created, here are all the steps to deploy the TinyBlazorAdmin app into Azure. You can run it somewhere else and even locally. + +## First thing first + +You need to **fork this repo** into your own account. You will need to update the configuration (this document will explain when and what), therefore it needs to be yours. + +To fork a GitHub repository click on the fork button on the top right of the screen. If you need more detail have a look to this GitHub doc: [Fork a repo +](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo). + +## Deploy AzUrlShortener (the Backend) + +This project is a frontend only so you will need to deploy the [Azure Url Shortener](https://github.com/FBoucher/AzUrlShortener) in "headless mode". Do to it, click the blue button below and make sure to select **none** as Frontend + +[![Deploy Backend to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/?WT.mc_id=urlshortener-github-frbouche#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FFBoucher%2FAzUrlShortener%2Fmain%2Fdeployment%2FazureDeploy.json) + +![CreateBackend][CreateBackend] + + +## Deploy TinyBlazorAdmin (the Frontend) + +There are many ways you could run Tiny Blazor Admin website. In this deployment, we will use the new [Azure Static Web App (SWA)](https://azure.microsoft.com/en-ca/services/app-service/static/?WT.mc_id=tinyblazoradmin-github-frbouche). However, because the TinyBlazorAdmin use Azure Active Directory (AAD), we need a standalone Azure Function (deployed at the previous step). + +Open Azure portal (portal.azure.com), open the **resource group** where you created the backend (ex: streamDemo is our case). Click the "**+**" and search **Static Web App**, and click the *Create* button. + +![Creating swa][swa_create1] + +> Note: You will need to **Authorize Azure Static Web Apps**, to have access to _your_ GitHub repository (the one created when you forked the project). This is required because SWA uses GitHub Action to deploy. + +![Creating swa part 2][swa_create2] + +Select your organization, repository and branch (ex: main). + +![Creating swa part 3][swa_create3] + +Select **Blazor** as your *Build Presets*. The *App location* needs to be the location of the project file; in our case `src/TinyBlazorAdmin/`. The *App artifact location* can be left to wwwroot. + +>Note: We don't need the Api location, because AzURLShortener is deployed in a full standalone Azure Function. + +Once it's all filled, click the Review, and create button. It will takes a few minutes to get deployed. During this time let's create and configure our security components. + +## Create Azure Active Directory (AAD) Components + +### Create AAD App for the Fontend + +We need a Service Principal that we will use to authenticate our user to Azure Active Directory (AAD). To achieve this we will create an application registration in Azure. + +From the Azure Portal (portal.azure.com), open the **Azure Active Directory** page. From the left option menu select **App registrations**, then create a new registration. Use a name that will help you to remember that it's for the TinyBlazorAdmin website (ex: TinyAdminApp) (1) + +![RegisterClientApp][RegisterClientApp] + +For the Redirect URL use **Web** (3) and enter the URL of the Azure Static WebApp deployed previously and add `/authentication/login-callback`. It should look lsomething like this: + +``` +https://bolly-tiger-04a15beef.azurestaticapps.net/authentication/login-callback + +``` + +**Note the ClientID and TenantID.** + +If you need to retrieve the ClientID and TenantID, they will be display at the top of the page once you select an app in the portal. + +![Create a new registration][newRegistration] + +Go back in the Authentication and in the section Implicit grant check the checkbox `Access Token` and `ID Tokens` + +![tokensaccess][tokensaccess] + +### Create App for the Azure Function + +We need a second App registration, this time to let the Azure Function validate that user information contained in the token is valid. + +![First steps to create the AD App registration][azFunction_Auth_step1] + +From the Azure Portal, go to your Azure Function. From the left panel select the *Authentication / Authorization* (1) option. Enable the *App Service Authentication* (2) and click on *Azure Active Directory*. + +![Configuring the App registration][azFunction_Auth_step2] + +1. Select Express. +2. Make sure *Create New AD App* is selected. +3. Give the AD App a name. +4. Click Ok. +5. DON'T FORGET TO CLICK THE SAVE BUTTON + +Now, we need to configure the brand-new Ad App registration. Still from the Azure portal open the *Active Directory* blade. Select the *App registration* option from the left menu. Then select the application you just created. + +![ConfigAzFuncADapp][ConfigAzFuncADapp] + +1. From the left panel click the *Expose an API* option. +2. Click the *Add a client application*. +3. Enter the ClientID of the Frontend App (the first one created). +4. Check the Impersonation checkbox. +5. Save by clicking *Add application*. + + + +## Configure Backend and Frontend to Work Together + +Now in your GitHub it's time to update the settings. The code needs to know the AD app to use and the Azure Function to call. Update those values inside `TinyBlazorAdmin\wwwroot\appsettings.json` + +> The **Endpoint** _must_ ends with a `/` + +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx", + "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx", + "ValidateAuthority": true + }, + "UrlShortenerSecuredService": { + "Endpoint": "https://__azFunction_URL__.azurewebsites.net/" + } +} +``` + +## Enabeling Cross-Origin Resource Sharing (CORS) + + +First we need the url of the caller (aka TinyBlazorAdmin). From the Azure Portal, open the TinyBlazorAdmin SWA blade. Note the URL display in the top right of the page this is the URL of your admin page. + +![swa_URL][swa_URL] + +Now we need to add this URL to the list in the CORS of the Azure Function that run AzUrlShortener. From the Azure portal open the blade of AzUrlShortener. From the left menu search CORS, and click it. Add the URL of the SWA and don't forget to save. + + +![azFunction_CORS][azFunction_CORS] + +## Try it! + +Voila, the deployment is now completed. You can test it by creating a new short URL using the admin SWA. + + +## Adding Custom Domain + +To add a custom domain to your AzUrlShortener & TinyBlazorAdmin, [follow these steps](https://github.com/FBoucher/AzUrlShortener/blob/main/post-deployment-configuration.md#add-a-custom-domain) from the the AzUrlShortener repo. + + +[CreateBackend]: medias/CreateBackend.png +[newRegistration]: medias/newRegistration.png +[AddPolicy]: medias/AddPolicy.png +[EditKeyVault]: medias/EditKeyVault.png +[CreateSecrets]: medias/CreateSecrets.png +[azFunction_Auth_step1]: medias/azFunction_Auth_step1.png +[azFunction_Auth_step2]: medias/azFunction_Auth_step2.png +[ConfigAzFuncADapp]: medias/ConfigAzFuncADapp.png +[tokensaccess]: medias/tokensaccess.png +[swa_create1]: medias/swa_create1.png +[swa_create2]: medias/swa_create2.png +[swa_create3]: medias/swa_create3.png +[swa_URL]: medias/swa_URL.png +[azFunction_CORS]: medias/azFunction_CORS.png +[RegisterClientApp]: medias/RegisterClientApp.png + \ No newline at end of file diff --git a/src/TinyBlazorAdmin.sln b/src/TinyBlazorAdmin.sln index 358919b..2a2e379 100644 --- a/src/TinyBlazorAdmin.sln +++ b/src/TinyBlazorAdmin.sln @@ -1,34 +1,34 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyBlazorAdmin", "TinyBlazorAdmin\TinyBlazorAdmin.csproj", "{2190B73D-DF38-45D8-88C1-6CDF49E68D68}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdminApi", "AdminApi\AdminApi.csproj", "{3663F7F4-2064-423E-9928-3F1D25E3F2D7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "azShortenerLib", "azShortenerLib\azShortenerLib.csproj", "{3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Release|Any CPU.Build.0 = Release|Any CPU - {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Release|Any CPU.Build.0 = Release|Any CPU - {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyBlazorAdmin", "client\TinyBlazorAdmin.csproj", "{2190B73D-DF38-45D8-88C1-6CDF49E68D68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdminApi", "api\AdminApi.csproj", "{3663F7F4-2064-423E-9928-3F1D25E3F2D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "azShortenerLib", "lib\azShortenerLib.csproj", "{3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2190B73D-DF38-45D8-88C1-6CDF49E68D68}.Release|Any CPU.Build.0 = Release|Any CPU + {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3663F7F4-2064-423E-9928-3F1D25E3F2D7}.Release|Any CPU.Build.0 = Release|Any CPU + {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E0E3C70-A56F-4565-ADE2-E3E585D1CBFD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/TinyBlazorAdmin/Shared/LoginDisplay.razor b/src/TinyBlazorAdmin/Shared/LoginDisplay.razor deleted file mode 100644 index 7ebea5d..0000000 --- a/src/TinyBlazorAdmin/Shared/LoginDisplay.razor +++ /dev/null @@ -1,23 +0,0 @@ -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication - -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity?.Name! - - - - Log in - - - -@code{ - private async Task BeginLogout(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} diff --git a/src/TinyBlazorAdmin/Shared/MainLayout.razor b/src/TinyBlazorAdmin/Shared/MainLayout.razor deleted file mode 100644 index 43b4c3c..0000000 --- a/src/TinyBlazorAdmin/Shared/MainLayout.razor +++ /dev/null @@ -1,18 +0,0 @@ -@inherits LayoutComponentBase - -
- - -
-
- - About -
- -
- @Body -
-
-
diff --git a/src/TinyBlazorAdmin/Shared/RedirectToLogin.razor b/src/TinyBlazorAdmin/Shared/RedirectToLogin.razor deleted file mode 100644 index 2db61cf..0000000 --- a/src/TinyBlazorAdmin/Shared/RedirectToLogin.razor +++ /dev/null @@ -1,8 +0,0 @@ -@inject NavigationManager Navigation - -@code { - protected override void OnInitialized() - { - Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); - } -} diff --git a/src/TinyBlazorAdmin/Shared/SurveyPrompt.razor b/src/TinyBlazorAdmin/Shared/SurveyPrompt.razor deleted file mode 100644 index 962027f..0000000 --- a/src/TinyBlazorAdmin/Shared/SurveyPrompt.razor +++ /dev/null @@ -1,16 +0,0 @@ -
- - @Title - - - Please take our - brief survey - - and tell us what you think. -
- -@code { - // Demonstrates how a parent component can supply parameters - [Parameter] - public string? Title { get; set; } -} diff --git a/src/TinyBlazorAdmin/staticwebapp.config.json b/src/TinyBlazorAdmin/staticwebapp.config.json deleted file mode 100644 index 4a1bb1f..0000000 --- a/src/TinyBlazorAdmin/staticwebapp.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "navigationFallback": { - "rewrite": "/index.html" - } -} \ No newline at end of file diff --git a/src/TinyBlazorAdmin/wwwroot/appsettings.json b/src/TinyBlazorAdmin/wwwroot/appsettings.json deleted file mode 100644 index 08a67f3..0000000 --- a/src/TinyBlazorAdmin/wwwroot/appsettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - /* - The following identity settings need to be configured - before the project can be successfully executed. - For more info see https://aka.ms/dotnet-template-ms-identity-platform - */ - "AzureAd": { - "Authority": "https://login.microsoftonline.com/206bad4c-d071-4c91-9181-ef7047e6590b/", - "ClientId": "8ff845ba-70f7-479a-a2c5-73f38b650232", - "ValidateAuthority": true - }, - "API_Prefix": "http://localhost:7071" -} \ No newline at end of file diff --git a/src/TinyBlazorAdmin/wwwroot/favicon.ico b/src/TinyBlazorAdmin/wwwroot/favicon.ico deleted file mode 100644 index 63e859b..0000000 Binary files a/src/TinyBlazorAdmin/wwwroot/favicon.ico and /dev/null differ diff --git a/src/TinyBlazorAdmin/wwwroot/icon-192.png b/src/TinyBlazorAdmin/wwwroot/icon-192.png deleted file mode 100644 index 166f56d..0000000 Binary files a/src/TinyBlazorAdmin/wwwroot/icon-192.png and /dev/null differ diff --git a/src/AdminApi/.gitignore b/src/api/.gitignore similarity index 100% rename from src/AdminApi/.gitignore rename to src/api/.gitignore diff --git a/src/AdminApi/AdminApi.csproj b/src/api/AdminApi.csproj similarity index 92% rename from src/AdminApi/AdminApi.csproj rename to src/api/AdminApi.csproj index 18a597e..17efde6 100644 --- a/src/AdminApi/AdminApi.csproj +++ b/src/api/AdminApi.csproj @@ -20,6 +20,6 @@ - + diff --git a/src/AdminApi/Program.cs b/src/api/Program.cs similarity index 97% rename from src/AdminApi/Program.cs rename to src/api/Program.cs index f39d3d3..699c96d 100644 --- a/src/AdminApi/Program.cs +++ b/src/api/Program.cs @@ -1,37 +1,37 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Azure.Functions.Worker.Configuration; -using Cloud5mins.domain; -using Microsoft.Extensions.DependencyInjection; - -namespace Cloud5mins.AdminApi -{ - public class Program - { - public static void Main() - { - AdminApiSettings AdminApiSettings = null; - - var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureServices((context, services) => - { - // Add our global configuration instance - services.AddSingleton(options => - { - var configuration = context.Configuration; - AdminApiSettings = new AdminApiSettings(); - configuration.Bind(AdminApiSettings); - return configuration; - }); - - // Add our configuration class - services.AddSingleton(options => { return AdminApiSettings; }); - }) - .Build(); - - host.Run(); - } - } +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Azure.Functions.Worker.Configuration; +using Cloud5mins.domain; +using Microsoft.Extensions.DependencyInjection; + +namespace Cloud5mins.AdminApi +{ + public class Program + { + public static void Main() + { + AdminApiSettings AdminApiSettings = null; + + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices((context, services) => + { + // Add our global configuration instance + services.AddSingleton(options => + { + var configuration = context.Configuration; + AdminApiSettings = new AdminApiSettings(); + configuration.Bind(AdminApiSettings); + return configuration; + }); + + // Add our configuration class + services.AddSingleton(options => { return AdminApiSettings; }); + }) + .Build(); + + host.Run(); + } + } } \ No newline at end of file diff --git a/src/AdminApi/domain/AdminApiSettings.cs b/src/api/domain/AdminApiSettings.cs similarity index 100% rename from src/AdminApi/domain/AdminApiSettings.cs rename to src/api/domain/AdminApiSettings.cs diff --git a/src/api/domain/ClaimsUtility.cs b/src/api/domain/ClaimsUtility.cs new file mode 100644 index 0000000..6137529 --- /dev/null +++ b/src/api/domain/ClaimsUtility.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace Cloud5mins.domain +{ + public static class ClaimsUtility + { + + public static HttpStatusCode CatchUnauthorize(HttpRequestData req, ILogger log) + { + try{ + + ClaimsPrincipal principal = StaticWebAppsAuth.GetClaimsPrincipal(req,log); + + if (principal == null) + { + log.LogTrace("No principal."); + return HttpStatusCode.Unauthorized; + } + + if(!principal.IsInRole("admin")) + { + log.LogInformation("Not IsInRole admin"); + var claims = new List( principal.FindAll(ClaimTypes.Role)); + foreach(var c in claims){ + if(c.Value == "admin") + return HttpStatusCode.Continue; + } + log.LogInformation("No claim with value admin"); + return HttpStatusCode.Unauthorized; + } + + return HttpStatusCode.Continue; + } + catch (Exception ex) + { + log.LogError(ex, "An unexpected error was encountered."); + return HttpStatusCode.BadRequest; + } + } + } +} \ No newline at end of file diff --git a/src/api/domain/StaticWebAppsAuth.cs b/src/api/domain/StaticWebAppsAuth.cs new file mode 100644 index 0000000..8f12eeb --- /dev/null +++ b/src/api/domain/StaticWebAppsAuth.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace Cloud5mins.domain +{ + public static class StaticWebAppsAuth + { + private class ClientPrincipal + { + public string IdentityProvider { get; set; } + public string UserId { get; set; } + public string UserDetails { get; set; } + public IEnumerable UserRoles { get; set; } + } + + public static ClaimsPrincipal GetClaimsPrincipal(HttpRequestData req, ILogger log) + { + var principal = new ClientPrincipal(); + + if (req.Headers.TryGetValues("x-ms-client-principal", out var header)) + { + var data = header.First(); + var decoded = Convert.FromBase64String(data); + var json = Encoding.UTF8.GetString(decoded); + log.LogWarning($"===> json: {json}"); + principal = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + principal.UserRoles = principal.UserRoles?.Except(new string[] { "anonymous" }, StringComparer.CurrentCultureIgnoreCase); + + if (!principal.UserRoles?.Any() ?? true) + { + return new ClaimsPrincipal(); + } + + var identity = new ClaimsIdentity(principal.IdentityProvider); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId)); + identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails)); + identity.AddClaims(principal.UserRoles.Select(r => new Claim(ClaimTypes.Role, r))); + + return new ClaimsPrincipal(identity); + } + } +} \ No newline at end of file diff --git a/src/AdminApi/function/UrlArchive.cs b/src/api/function/UrlArchive.cs similarity index 85% rename from src/AdminApi/function/UrlArchive.cs rename to src/api/function/UrlArchive.cs index c684623..f448001 100644 --- a/src/AdminApi/function/UrlArchive.cs +++ b/src/api/function/UrlArchive.cs @@ -1,113 +1,108 @@ -/* -```c# -Input: - { - // [Required] - "PartitionKey": "d", - - // [Required] - "RowKey": "doc", - - // [Optional] all other properties - } -Output: - { - "Url": "https://docs.microsoft.com/en-ca/azure/azure-functions/functions-create-your-first-function-visual-studio", - "Title": "My Title", - "ShortUrl": null, - "Clicks": 0, - "IsArchived": true, - "PartitionKey": "a", - "RowKey": "azFunc2", - "Timestamp": "2020-07-23T06:22:33.852218-04:00", - "ETag": "W/\"datetime'2020-07-23T10%3A24%3A51.3440526Z'\"" - } - -*/ - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using System.Net; -using Microsoft.Extensions.Configuration; -using System.Security.Claims; -using Microsoft.AspNetCore.Mvc; -using System.IO; -using System.Text.Json; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using Cloud5mins.domain; -using System.Threading; -using Cloud5mins.AzShortener; - -namespace Cloud5mins.Function -{ - public class UrlArchive - { - - private readonly ILogger _logger; - private readonly AdminApiSettings _adminApiSettings; - - public UrlArchive(ILoggerFactory loggerFactory, AdminApiSettings settings) - { - _logger = loggerFactory.CreateLogger(); - _adminApiSettings = settings; - } - - [Function("UrlArchive")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req, - ExecutionContext context) - { - _logger.LogInformation($"C# HTTP trigger function processed this request: {req}"); - - string userId = string.Empty; - ShortUrlEntity input; - ShortUrlEntity result; - try - { - // var invalidRequest = Utility.CatchUnauthorize(principal, log); - // if (invalidRequest != null) - // { - // return invalidRequest; - // } - // else - // { - // userId = principal.FindFirst(ClaimTypes.GivenName).Value; - // _logger.LogInformation("Authenticated user {user}.", userId); - // } - - // Validation of the inputs - if (req == null) - { - return req.CreateResponse( HttpStatusCode.NotFound); - } - - using (var reader = new StreamReader(req.Body)) - { - var body = reader.ReadToEnd(); - input = JsonSerializer.Deserialize(body, new JsonSerializerOptions {PropertyNameCaseInsensitive = true}); - if (input == null) - { - return req.CreateResponse( HttpStatusCode.NotFound); - } - } - - StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); - - result = await stgHelper.ArchiveShortUrlEntity(input); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error was encountered."); - var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); - await badRequest.WriteAsJsonAsync(new { Message = ex.Message} ); - return badRequest; - } - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(result); - return response; - } - } -} +/* +```c# +Input: + { + // [Required] + "PartitionKey": "d", + + // [Required] + "RowKey": "doc", + + // [Optional] all other properties + } +Output: + { + "Url": "https://docs.microsoft.com/en-ca/azure/azure-functions/functions-create-your-first-function-visual-studio", + "Title": "My Title", + "ShortUrl": null, + "Clicks": 0, + "IsArchived": true, + "PartitionKey": "a", + "RowKey": "azFunc2", + "Timestamp": "2020-07-23T06:22:33.852218-04:00", + "ETag": "W/\"datetime'2020-07-23T10%3A24%3A51.3440526Z'\"" + } + +*/ + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using System.Net; +using Microsoft.Extensions.Configuration; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using System.IO; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Cloud5mins.domain; +using System.Threading; +using Cloud5mins.AzShortener; + +namespace Cloud5mins.Function +{ + public class UrlArchive + { + + private readonly ILogger _logger; + private readonly AdminApiSettings _adminApiSettings; + + public UrlArchive(ILoggerFactory loggerFactory, AdminApiSettings settings) + { + _logger = loggerFactory.CreateLogger(); + _adminApiSettings = settings; + } + + [Function("UrlArchive")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req, + ExecutionContext context) + { + _logger.LogInformation($"HTTP trigger - UrlArchive"); + + string userId = string.Empty; + ShortUrlEntity input; + ShortUrlEntity result; + try + { + var invalidCode = ClaimsUtility.CatchUnauthorize(req, _logger); + if (invalidCode != HttpStatusCode.Continue) + { + return req.CreateResponse(invalidCode); + } + + // Validation of the inputs + if (req == null) + { + return req.CreateResponse( HttpStatusCode.NotFound); + } + + using (var reader = new StreamReader(req.Body)) + { + var body = reader.ReadToEnd(); + input = JsonSerializer.Deserialize(body, new JsonSerializerOptions {PropertyNameCaseInsensitive = true}); + if (input == null) + { + return req.CreateResponse( HttpStatusCode.NotFound); + } + } + + StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); + + result = await stgHelper.ArchiveShortUrlEntity(input); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); + await badRequest.WriteAsJsonAsync(new { Message = ex.Message} ); + return badRequest; + } + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(result); + return response; + } + } +} diff --git a/src/AdminApi/function/UrlClickStatsByDay.cs b/src/api/function/UrlClickStatsByDay.cs similarity index 87% rename from src/AdminApi/function/UrlClickStatsByDay.cs rename to src/api/function/UrlClickStatsByDay.cs index 2c51862..6bcb4f1 100644 --- a/src/AdminApi/function/UrlClickStatsByDay.cs +++ b/src/api/function/UrlClickStatsByDay.cs @@ -1,118 +1,113 @@ -/* -```c# -Input: - - { - // [Required] the end of the URL that you want statistics for. - "vanity": "azFunc" - } - -Output: - { - "items": [ - { - "dateClicked": "2020-12-19", - "count": 1 - }, - { - "dateClicked": "2020-12-03", - "count": 2 - } - ], - "url": ""https://c5m.ca/29" -*/ - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using System.Net; -using Cloud5mins.domain; -using System.Security.Claims; -using System.Text.Json; -using System.IO; -using System.Linq; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using System.Threading; -using Cloud5mins.AzShortener; - -namespace Cloud5mins.Function -{ - public class UrlClickStatsByDay - { - private readonly ILogger _logger; - private readonly AdminApiSettings _adminApiSettings; - - public UrlClickStatsByDay(ILoggerFactory loggerFactory, AdminApiSettings settings) - { - _logger = loggerFactory.CreateLogger(); - _adminApiSettings = settings; - } - - [Function("UrlClickStatsByDay")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req, - ExecutionContext context) - { - _logger.LogInformation($"C# HTTP trigger function processed this request: {req}"); - - string userId = string.Empty; - UrlClickStatsRequest input; - var result = new ClickDateList(); - - // var invalidRequest = Utility.CatchUnauthorize(principal, log); - // if (invalidRequest != null) - // { - // return invalidRequest; - // } - // else - // { - // userId = principal.FindFirst(ClaimTypes.NameIdentifier).Value; - // _logger.LogInformation("Authenticated user {user}.", userId); - // } - - // Validation of the inputs - if (req == null) - { - return req.CreateResponse( HttpStatusCode.NotFound); - } - - try - { - using (var reader = new StreamReader(req.Body)) - { - var strBody = reader.ReadToEnd(); - input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions {PropertyNameCaseInsensitive = true}); - if (input == null) - { - return req.CreateResponse( HttpStatusCode.NotFound); - } - } - - StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); - - 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(_adminApiSettings.customDomain) ? req.Url.Host: _adminApiSettings.customDomain.ToString(); - result.Url = Utility.GetShortUrl(host, input.Vanity); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error was encountered."); - var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); - await badRequest.WriteAsJsonAsync(new { Message = $"{ex.Message}"} ); - return badRequest; - } - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(result); - return response; - } - } -} +/* +```c# +Input: + + { + // [Required] the end of the URL that you want statistics for. + "vanity": "azFunc" + } + +Output: + { + "items": [ + { + "dateClicked": "2020-12-19", + "count": 1 + }, + { + "dateClicked": "2020-12-03", + "count": 2 + } + ], + "url": ""https://c5m.ca/29" +*/ + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using System.Net; +using Cloud5mins.domain; +using System.Security.Claims; +using System.Text.Json; +using System.IO; +using System.Linq; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using System.Threading; +using Cloud5mins.AzShortener; + +namespace Cloud5mins.Function +{ + public class UrlClickStatsByDay + { + private readonly ILogger _logger; + private readonly AdminApiSettings _adminApiSettings; + + public UrlClickStatsByDay(ILoggerFactory loggerFactory, AdminApiSettings settings) + { + _logger = loggerFactory.CreateLogger(); + _adminApiSettings = settings; + } + + [Function("UrlClickStatsByDay")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req, + ExecutionContext context) + { + _logger.LogInformation($"HTTP trigger: UrlClickStatsByDay"); + + string userId = string.Empty; + UrlClickStatsRequest input; + var result = new ClickDateList(); + + var invalidCode = ClaimsUtility.CatchUnauthorize(req, _logger); + if (invalidCode != HttpStatusCode.Continue) + { + return req.CreateResponse(invalidCode); + } + + // Validation of the inputs + if (req == null) + { + return req.CreateResponse( HttpStatusCode.NotFound); + } + + try + { + using (var reader = new StreamReader(req.Body)) + { + var strBody = reader.ReadToEnd(); + input = JsonSerializer.Deserialize(strBody, new JsonSerializerOptions {PropertyNameCaseInsensitive = true}); + if (input == null) + { + return req.CreateResponse( HttpStatusCode.NotFound); + } + } + + StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); + + 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(_adminApiSettings.customDomain) ? req.Url.Host: _adminApiSettings.customDomain.ToString(); + result.Url = Utility.GetShortUrl(host, input.Vanity); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); + await badRequest.WriteAsJsonAsync(new { Message = $"{ex.Message}"} ); + return badRequest; + } + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(result); + return response; + } + } +} diff --git a/src/AdminApi/function/UrlList.cs b/src/api/function/UrlList.cs similarity index 79% rename from src/AdminApi/function/UrlList.cs rename to src/api/function/UrlList.cs index 5486634..cad1e44 100644 --- a/src/AdminApi/function/UrlList.cs +++ b/src/api/function/UrlList.cs @@ -1,95 +1,84 @@ -/* -```c# -Input: - - -Output: - { - "Url": "https://SOME_URL", - "Clicks": 0, - "PartitionKey": "d", - "title": "Quickstart: Create your first function in Azure using Visual Studio" - "RowKey": "doc", - "Timestamp": "0001-01-01T00:00:00+00:00", - "ETag": "W/\"datetime'2020-05-06T14%3A33%3A51.2639969Z'\"" - } -*/ - -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Linq; - -using Cloud5mins.AzShortener; -using Cloud5mins.domain; - - -//using Microsoft.AspNetCore.Http; - -namespace Cloud5mins.Function -{ - public class UrlList - { - - private readonly ILogger _logger; - private readonly AdminApiSettings _adminApiSettings; - - public UrlList(ILoggerFactory loggerFactory, AdminApiSettings settings) - { - _logger = loggerFactory.CreateLogger(); - _adminApiSettings = settings; - } - - [Function("UrlList")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, ExecutionContext context) - { - _logger.LogInformation($"C# HTTP trigger function processed this request: {req}"); - - var result = new ListResponse(); - string userId = string.Empty; - - - StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); - - try - { - // var invalidRequest = Utility.CatchUnauthorize(principal, log); - // if (invalidRequest != null) - // { - // return invalidRequest; - // } - // else - // { - // userId = principal.FindFirst(ClaimTypes.GivenName).Value; - // _logger.LogInformation("Authenticated user {user}.", userId); - // } - - result.UrlList = await stgHelper.GetAllShortUrlEntities(); - result.UrlList = result.UrlList.Where(p => !(p.IsArchived ?? false)).ToList(); - var host = string.IsNullOrEmpty(_adminApiSettings.customDomain) ? req.Url.Host: _adminApiSettings.customDomain; - foreach (ShortUrlEntity url in result.UrlList) - { - url.ShortUrl = Utility.GetShortUrl(host, url.RowKey); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error was encountered."); - var badres = req.CreateResponse(HttpStatusCode.BadRequest); - await badres.WriteAsJsonAsync(new {Message = ex.Message }); - return badres; - } - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(result); - - return response; - } - } -} +/* +```c# +Input: + + +Output: + { + "Url": "https://SOME_URL", + "Clicks": 0, + "PartitionKey": "d", + "title": "Quickstart: Create your first function in Azure using Visual Studio" + "RowKey": "doc", + "Timestamp": "0001-01-01T00:00:00+00:00", + "ETag": "W/\"datetime'2020-05-06T14%3A33%3A51.2639969Z'\"" + } +*/ + +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using System.Linq; + +using Cloud5mins.AzShortener; +using Cloud5mins.domain; + +namespace Cloud5mins.Function +{ + public class UrlList + { + + private readonly ILogger _logger; + private readonly AdminApiSettings _adminApiSettings; + + public UrlList(ILoggerFactory loggerFactory, AdminApiSettings settings) + { + _logger = loggerFactory.CreateLogger(); + _adminApiSettings = settings; + } + + [Function("UrlList")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, ExecutionContext context) + { + _logger.LogInformation($"Starting UrlList..."); + + var result = new ListResponse(); + string userId = string.Empty; + + StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); + + try + { + var invalidCode = ClaimsUtility.CatchUnauthorize(req, _logger); + if (invalidCode != HttpStatusCode.Continue) + { + return req.CreateResponse(invalidCode); + } + + result.UrlList = await stgHelper.GetAllShortUrlEntities(); + result.UrlList = result.UrlList.Where(p => !(p.IsArchived ?? false)).ToList(); + var host = string.IsNullOrEmpty(_adminApiSettings.customDomain) ? req.Url.Host: _adminApiSettings.customDomain; + foreach (ShortUrlEntity url in result.UrlList) + { + url.ShortUrl = Utility.GetShortUrl(host, url.RowKey); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + var badres = req.CreateResponse(HttpStatusCode.BadRequest); + await badres.WriteAsJsonAsync(new {Message = ex.Message }); + return badres; + } + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(result); + return response; + } + } +} diff --git a/src/AdminApi/function/UrlShortener.cs b/src/api/function/UrlShortener.cs similarity index 90% rename from src/AdminApi/function/UrlShortener.cs rename to src/api/function/UrlShortener.cs index efaace4..0edaa48 100644 --- a/src/AdminApi/function/UrlShortener.cs +++ b/src/api/function/UrlShortener.cs @@ -1,157 +1,147 @@ -/* -```c# -Input: - - { - // [Required] The url you wish to have a short version for - "url": "https://docs.microsoft.com/en-ca/azure/azure-functions/functions-create-your-first-function-visual-studio", - - // [Optional] Title of the page, or text description of your choice. - "title": "Quickstart: Create your first function in Azure using Visual Studio" - - // [Optional] the end of the URL. If nothing one will be generated for you. - "vanity": "azFunc" - } - -Output: - { - "ShortUrl": "http://c5m.ca/azFunc", - "LongUrl": "https://docs.microsoft.com/en-ca/azure/azure-functions/functions-create-your-first-function-visual-studio" - } -*/ - -using System; -using System.Threading.Tasks; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.Extensions.Logging; -using System.Net; -// using Microsoft.Extensions.Configuration; -// using System.Security.Claims; -// using Microsoft.AspNetCore.Mvc; -// using Microsoft.AspNetCore.Http; -using System.IO; -using System.Text.Json; -using System.Threading; - -using Cloud5mins.domain; -using Cloud5mins.AzShortener; - -namespace Cloud5mins.Function -{ - - public class UrlShortener - { - private readonly ILogger _logger; - private readonly AdminApiSettings _adminApiSettings; - - public UrlShortener(ILoggerFactory loggerFactory, AdminApiSettings settings) - { - _logger = loggerFactory.CreateLogger(); - _adminApiSettings = settings; - } - - [Function("UrlShortener")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, - ExecutionContext context - ) - { - _logger.LogInformation($"__trace creating shortURL: {req}"); - string userId = string.Empty; - ShortRequest input; - var result = new ShortResponse(); - - try - { - // var invalidRequest = Utility.CatchUnauthorize(principal, log); - - // if (invalidRequest != null) - // { - // return invalidRequest; - // } - // else - // { - // userId = principal.FindFirst(ClaimTypes.GivenName).Value; - // _logger.LogInformation("Authenticated user {user}.", userId); - // } - - // Validation of the inputs - if (req == null) - { - return req.CreateResponse(HttpStatusCode.NotFound); - } - - using (var reader = new StreamReader(req.Body)) - { - var strBody = reader.ReadToEnd(); - 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(_adminApiSettings.UlsDataStorage); - - 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(_adminApiSettings.customDomain) ? req.Url.Host: _adminApiSettings.customDomain.ToString(); - 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."); - - var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); - await badResponse.WriteAsJsonAsync(new {Message = ex.Message}); - return badResponse; - } - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(result); - - return response; - } - } -} +/* +```c# +Input: + + { + // [Required] The url you wish to have a short version for + "url": "https://docs.microsoft.com/en-ca/azure/azure-functions/functions-create-your-first-function-visual-studio", + + // [Optional] Title of the page, or text description of your choice. + "title": "Quickstart: Create your first function in Azure using Visual Studio" + + // [Optional] the end of the URL. If nothing one will be generated for you. + "vanity": "azFunc" + } + +Output: + { + "ShortUrl": "http://c5m.ca/azFunc", + "LongUrl": "https://docs.microsoft.com/en-ca/azure/azure-functions/functions-create-your-first-function-visual-studio" + } +*/ + +using System; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using System.Net; +using System.IO; +using System.Text.Json; +using System.Threading; + +using Cloud5mins.domain; +using Cloud5mins.AzShortener; + +namespace Cloud5mins.Function +{ + + public class UrlShortener + { + private readonly ILogger _logger; + private readonly AdminApiSettings _adminApiSettings; + + public UrlShortener(ILoggerFactory loggerFactory, AdminApiSettings settings) + { + _logger = loggerFactory.CreateLogger(); + _adminApiSettings = settings; + } + + [Function("UrlShortener")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + ExecutionContext context + ) + { + _logger.LogInformation($"__trace creating shortURL: {req}"); + string userId = string.Empty; + ShortRequest input; + var result = new ShortResponse(); + + try + { + var invalidCode = ClaimsUtility.CatchUnauthorize(req, _logger); + if (invalidCode != HttpStatusCode.Continue) + { + return req.CreateResponse(invalidCode); + } + + // Validation of the inputs + if (req == null) + { + return req.CreateResponse(HttpStatusCode.NotFound); + } + + using (var reader = new StreamReader(req.Body)) + { + var strBody = reader.ReadToEnd(); + 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(_adminApiSettings.UlsDataStorage); + + 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(_adminApiSettings.customDomain) ? req.Url.Host: _adminApiSettings.customDomain.ToString(); + 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."); + + var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); + await badResponse.WriteAsJsonAsync(new {Message = ex.Message}); + return badResponse; + } + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(result); + + return response; + } + } +} diff --git a/src/AdminApi/function/UrlUpdate.cs b/src/api/function/UrlUpdate.cs similarity index 84% rename from src/AdminApi/function/UrlUpdate.cs rename to src/api/function/UrlUpdate.cs index 27c09fc..fad4387 100644 --- a/src/AdminApi/function/UrlUpdate.cs +++ b/src/api/function/UrlUpdate.cs @@ -1,149 +1,136 @@ -/* -```c# -Input: - { - // [Required] - "PartitionKey": "d", - - // [Required] - "RowKey": "doc", - - // [Optional] New Title for this URL, or text description of your choice. - "title": "Quickstart: Create your first function in Azure using Visual Studio" - - // [Optional] New long Url where the the user will be redirect - "Url": "https://SOME_URL" - } - - -Output: - { - "Url": "https://SOME_URL", - "Clicks": 0, - "PartitionKey": "d", - "title": "Quickstart: Create your first function in Azure using Visual Studio" - "RowKey": "doc", - "Timestamp": "0001-01-01T00:00:00+00:00", - "ETag": "W/\"datetime'2020-05-06T14%3A33%3A51.2639969Z'\"" - } -*/ - -using System; -using System.Threading.Tasks; -// using Microsoft.Azure.WebJobs; -// using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.Extensions.Logging; -using System.Net; -using System.Net.Http; -using Cloud5mins.domain; -using Cloud5mins.AzShortener; -using Microsoft.Extensions.Configuration; -using System.Security.Claims; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Http; -using System.IO; -using System.Text.Json; -using System.Threading; - -namespace Cloud5mins.Function -{ - public class UrlUpdate - { - private readonly ILogger _logger; - private readonly AdminApiSettings _adminApiSettings; - - public UrlUpdate(ILoggerFactory loggerFactory, AdminApiSettings settings) - { - _logger = loggerFactory.CreateLogger(); - _adminApiSettings = settings; - } - - - [Function("UrlUpdate")] - public async Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req, - ExecutionContext context - ) - { - _logger.LogInformation($"C# HTTP trigger function processed this request: {req}"); - - string userId = string.Empty; - ShortUrlEntity input; - ShortUrlEntity result; - - try - { - // var invalidRequest = Utility.CatchUnauthorize(principal, _logger); - - // if (invalidRequest != null) - // { - // return invalidRequest; - // } - // else - // { - // userId = principal.FindFirst(ClaimTypes.GivenName).Value; - // _logger.LogInformation("Authenticated user {user}.", userId); - // } - - // Validation of the inputs - if (req == null) - { - return req.CreateResponse( HttpStatusCode.NotFound); - } - - using (var reader = new StreamReader(req.Body)) - { - var strBody = reader.ReadToEnd(); - 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; - } - - // var config = new ConfigurationBuilder() - // .SetBasePath(context.FunctionAppDirectory) - // .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) - // .AddEnvironmentVariables() - // .Build(); - - StorageTableHelper stgHelper = new StorageTableHelper(_adminApiSettings.UlsDataStorage); - - result = await stgHelper.UpdateShortUrlEntity(input); - var host = string.IsNullOrEmpty(_adminApiSettings.customDomain) ? req.Url.Host : _adminApiSettings.customDomain.ToString(); - result.ShortUrl = Utility.GetShortUrl(host, result.RowKey); - - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error was encountered."); - - var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); - await badRequest.WriteAsJsonAsync(new { Message = ex.Message} ); - return badRequest; - } - - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(result); - return response; - } - } +/* +```c# +Input: + { + // [Required] + "PartitionKey": "d", + + // [Required] + "RowKey": "doc", + + // [Optional] New Title for this URL, or text description of your choice. + "title": "Quickstart: Create your first function in Azure using Visual Studio" + + // [Optional] New long Url where the the user will be redirect + "Url": "https://SOME_URL" + } + + +Output: + { + "Url": "https://SOME_URL", + "Clicks": 0, + "PartitionKey": "d", + "title": "Quickstart: Create your first function in Azure using Visual Studio" + "RowKey": "doc", + "Timestamp": "0001-01-01T00:00:00+00:00", + "ETag": "W/\"datetime'2020-05-06T14%3A33%3A51.2639969Z'\"" + } +*/ + +using System; +using System.Threading.Tasks; +// using Microsoft.Azure.WebJobs; +// using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Http; +using Cloud5mins.domain; +using Cloud5mins.AzShortener; +using Microsoft.Extensions.Configuration; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.IO; +using System.Text.Json; +using System.Threading; + +namespace Cloud5mins.Function +{ + public class UrlUpdate + { + private readonly ILogger _logger; + private readonly AdminApiSettings _adminApiSettings; + + public UrlUpdate(ILoggerFactory loggerFactory, AdminApiSettings settings) + { + _logger = loggerFactory.CreateLogger(); + _adminApiSettings = settings; + } + + [Function("UrlUpdate")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req, + ExecutionContext context + ) + { + _logger.LogInformation($"HTTP trigger - UrlUpdate"); + + string userId = string.Empty; + ShortUrlEntity input; + ShortUrlEntity result; + + try + { + var invalidCode = ClaimsUtility.CatchUnauthorize(req, _logger); + if (invalidCode != HttpStatusCode.Continue) + { + return req.CreateResponse(invalidCode); + } + + // Validation of the inputs + if (req == null) + { + return req.CreateResponse( HttpStatusCode.NotFound); + } + + using (var reader = new StreamReader(req.Body)) + { + var strBody = reader.ReadToEnd(); + 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(_adminApiSettings.UlsDataStorage); + + result = await stgHelper.UpdateShortUrlEntity(input); + var host = string.IsNullOrEmpty(_adminApiSettings.customDomain) ? req.Url.Host : _adminApiSettings.customDomain.ToString(); + result.ShortUrl = Utility.GetShortUrl(host, result.RowKey); + + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error was encountered."); + + var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); + await badRequest.WriteAsJsonAsync(new { Message = ex.Message} ); + return badRequest; + } + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(result); + return response; + } + } } \ No newline at end of file diff --git a/src/AdminApi/host.json b/src/api/host.json similarity index 100% rename from src/AdminApi/host.json rename to src/api/host.json diff --git a/src/AdminApi/local.settings.example.json b/src/api/local.settings.example.json similarity index 95% rename from src/AdminApi/local.settings.example.json rename to src/api/local.settings.example.json index a889f86..2c6cbc3 100644 --- a/src/AdminApi/local.settings.example.json +++ b/src/api/local.settings.example.json @@ -1,13 +1,13 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "", - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "UlsDataStorage":"CONNECTIONSTRING_DATA_STORAGE_ACCOUNT" - }, - "Host": { - "LocalHttpPort": 7071, - "CORS": "*", - "CORSCredentials": false - } +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "UlsDataStorage":"CONNECTIONSTRING_DATA_STORAGE_ACCOUNT" + }, + "Host": { + "LocalHttpPort": 7071, + "CORS": "*", + "CORSCredentials": false + } } \ No newline at end of file diff --git a/src/TinyBlazorAdmin/.vscode/launch.json b/src/client/.vscode/launch.json similarity index 97% rename from src/TinyBlazorAdmin/.vscode/launch.json rename to src/client/.vscode/launch.json index e8c2dbe..e8d72de 100644 --- a/src/TinyBlazorAdmin/.vscode/launch.json +++ b/src/client/.vscode/launch.json @@ -1,14 +1,14 @@ -{ - // 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 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch and Debug Standalone Blazor WebAssembly App", - "type": "blazorwasm", - "request": "launch", - "cwd": "${workspaceFolder}" - } - ] +{ + // 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 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch and Debug Standalone Blazor WebAssembly App", + "type": "blazorwasm", + "request": "launch", + "cwd": "${workspaceFolder}" + } + ] } \ No newline at end of file diff --git a/src/TinyBlazorAdmin/.vscode/tasks.json b/src/client/.vscode/tasks.json similarity index 96% rename from src/TinyBlazorAdmin/.vscode/tasks.json rename to src/client/.vscode/tasks.json index 8abc9fd..026ecac 100644 --- a/src/TinyBlazorAdmin/.vscode/tasks.json +++ b/src/client/.vscode/tasks.json @@ -1,42 +1,42 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/TinyBlazorAdmin.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/TinyBlazorAdmin.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/TinyBlazorAdmin.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - } - ] +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/TinyBlazorAdmin.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/TinyBlazorAdmin.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/TinyBlazorAdmin.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] } \ No newline at end of file diff --git a/src/TinyBlazorAdmin/App.razor b/src/client/App.razor similarity index 52% rename from src/TinyBlazorAdmin/App.razor rename to src/client/App.razor index d07039a..51ab545 100644 --- a/src/TinyBlazorAdmin/App.razor +++ b/src/client/App.razor @@ -1,25 +1,16 @@ - - - - - - @if (context.User.Identity?.IsAuthenticated != true) - { - - } - else - { -

You are not authorized to access this resource.

- } -
-
- -
- - Not found - -

Sorry, there's nothing at this address.

-
-
-
-
+ + + + + + + + + + + + Not found + + + + diff --git a/src/TinyBlazorAdmin/Pages/Authentication.razor b/src/client/Pages/Authentication.razor similarity index 96% rename from src/TinyBlazorAdmin/Pages/Authentication.razor rename to src/client/Pages/Authentication.razor index 6c74356..b53dee2 100644 --- a/src/TinyBlazorAdmin/Pages/Authentication.razor +++ b/src/client/Pages/Authentication.razor @@ -1,7 +1,7 @@ -@page "/authentication/{action}" -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication - - -@code{ - [Parameter] public string? Action { get; set; } -} +@page "/authentication/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + +@code{ + [Parameter] public string? Action { get; set; } +} diff --git a/src/client/Pages/Help.razor b/src/client/Pages/Help.razor new file mode 100644 index 0000000..af0f049 --- /dev/null +++ b/src/client/Pages/Help.razor @@ -0,0 +1,11 @@ +@page "/help" + +Tiny Blazor Admin - Help + +

Help

+
+
    +
  • You need to be authenticated to have access. By default Active Directory is the one used (any outlook.com). Others (GitHub, Twitter, etc.) are also available to used.
  • +
  • You need to be part or the role admin (all lowercase) to have access. This is manage from the Azure Portal, in the Role management blade of your Azure Static Web App. Refer to the documentation for more details.
  • +
+
\ No newline at end of file diff --git a/src/TinyBlazorAdmin/Pages/Index.razor b/src/client/Pages/Index.razor similarity index 98% rename from src/TinyBlazorAdmin/Pages/Index.razor rename to src/client/Pages/Index.razor index a17c522..1e695af 100644 --- a/src/TinyBlazorAdmin/Pages/Index.razor +++ b/src/client/Pages/Index.razor @@ -1,20 +1,20 @@ -@page "/" - -Tiny Blazor Admin - -

Tiny Blazor Admin

-
-

- Welcome to the Tiny Blazor Admin -

- -
-

This is the frontend of a solution using AzUrlShortener both projects are available on GitHub.

- -

This site is build using .Net Blazor Web Assembly and some Blazor components from Syncfusion (thank you for the Community licence)

- -

If you want to learn more about Tiny Blazor Admin please refer to the documentation.

-
- -
+@page "/" + +Tiny Blazor Admin + +

Tiny Blazor Admin

+
+

+ Welcome to the Tiny Blazor Admin +

+ +
+

This is the frontend of a solution using AzUrlShortener both projects are available on GitHub.

+ +

This site is build using .Net Blazor Web Assembly and some Blazor components from Syncfusion (thank you for the Community licence)

+ +

If you want to learn more about Tiny Blazor Admin please refer to the documentation.

+
+ +
\ No newline at end of file diff --git a/src/TinyBlazorAdmin/Pages/Statistics.razor b/src/client/Pages/Statistics.razor similarity index 70% rename from src/TinyBlazorAdmin/Pages/Statistics.razor rename to src/client/Pages/Statistics.razor index 819d1fd..679e82f 100644 --- a/src/TinyBlazorAdmin/Pages/Statistics.razor +++ b/src/client/Pages/Statistics.razor @@ -1,99 +1,111 @@ -@page "/statistics" -@page "/statistics/{vanity}" - -@using Syncfusion.Blazor.Charts -@using Syncfusion.Blazor.Spinner -@using Cloud5mins.AzShortener -@using Microsoft.AspNetCore.Authorization -@using System.Collections.ObjectModel -@using System.Text.Json -@inject IJSRuntime JSRuntime; -@inject HttpClient Http - - -

Click Statistics

-

@subTitle

- << Back - -
-@if(clicksHistory != null){ - - - - - - - - - - - - - - - - - - - - -} -else{ - -} -
-
-

@((MarkupString)dayCount)

-
- -@code { - [Parameter] - public string? vanity { get; set; } - - private bool isLoading { get; set; } = true; - private string subTitle = ""; - private ClickDateList clickStatsList; - private ObservableCollection clicksHistory; - private readonly Random _random = new Random(); - private string dayCount = string.Empty; - - public class ClickData - { - public string? XValue; - public int YValue; - } - - private async Task> UpdateUIList() - { - subTitle = (!String.IsNullOrEmpty(vanity))? $"Clicks for: {vanity}": "All clicks"; - try{ - CancellationToken cancellationToken = new CancellationToken(); - var response = await Http.PostAsJsonAsync("/api/UrlClickStatsByDay", new UrlClickStatsRequest(vanity), cancellationToken); - var jsonResult = await response.Content.ReadAsStringAsync(); - clickStatsList = JsonSerializer.Deserialize(jsonResult); - - return new ObservableCollection(clickStatsList.Items); - } - catch (System.Exception ex) - { - Console.WriteLine(ex.ToString()); - return null; - } - - } - - protected override async void OnInitialized() - { - clicksHistory = await UpdateUIList(); - this.isLoading = false; - StateHasChanged(); - dayCount = "Day(s): " + clicksHistory.Count.ToString(); - - } +@page "/statistics" +@page "/statistics/{vanity}" + +@using Syncfusion.Blazor.Charts +@using Syncfusion.Blazor.Spinner +@using Cloud5mins.AzShortener +@using Microsoft.AspNetCore.Authorization +@using System.Collections.ObjectModel +@using System.Text.Json +@using System.Net +@inject IJSRuntime JSRuntime +@inject HttpClient Http +@inject NavigationManager NavigationManager + +@attribute [Authorize(Roles = "admin")] + +

Click Statistics

+

@subTitle

+ << Back + +
+@if(clicksHistory != null){ + + + + + + + + + + + + + + + + + + + + +} +else{ + +} +
+
+

@((MarkupString)dayCount)

+
+ +@code { + [Parameter] + public string? vanity { get; set; } + + private bool isLoading { get; set; } = true; + private string subTitle = ""; + private ClickDateList clickStatsList; + private ObservableCollection clicksHistory; + private readonly Random _random = new Random(); + private string dayCount = string.Empty; + + public class ClickData + { + public string? XValue; + public int YValue; + } + + private async Task> UpdateUIList() + { + 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); + if(response.IsSuccessStatusCode){ + var jsonResult = await response.Content.ReadAsStringAsync(); + clickStatsList = JsonSerializer.Deserialize(jsonResult); + + return new ObservableCollection(clickStatsList.Items); + } + switch (response.StatusCode) + { + case HttpStatusCode.Unauthorized: NavigationManager.NavigateTo("/unauthorized"); + break; + default: NavigationManager.NavigateTo("/404"); + break; + } + } + catch (System.Exception ex) + { + Console.WriteLine(ex.ToString()); + } + return null; + } + + protected override async void OnInitialized() + { + clicksHistory = await UpdateUIList(); + this.isLoading = false; + StateHasChanged(); + if(clicksHistory != null) + dayCount = "Day(s): " + clicksHistory.Count.ToString(); + + } } \ No newline at end of file diff --git a/src/TinyBlazorAdmin/Pages/UrlManager.razor b/src/client/Pages/UrlManager.razor similarity index 90% rename from src/TinyBlazorAdmin/Pages/UrlManager.razor rename to src/client/Pages/UrlManager.razor index 76b6497..7861c11 100644 --- a/src/TinyBlazorAdmin/Pages/UrlManager.razor +++ b/src/client/Pages/UrlManager.razor @@ -1,276 +1,291 @@ -@page "/urlmanager" - -@using Microsoft.AspNetCore.Authorization -@using Syncfusion.Blazor.Grids -@using Syncfusion.Blazor.Inputs -@using Cloud5mins.AzShortener -@* @using System.Text.Json *@ -@inject HttpClient Http -@inject IJSRuntime JSRuntime; -@inject NavigationManager NavigationManager; - -Url Manager - -

Urls Manager

- -@if (urls.UrlList == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - - - - - - - - - - - - - - -} - - -@if (ShowCreatePopup) -{ - - -} - - -@if (ShowEditPopup) -{ - - -} - -@code { - bool ShowCreatePopup = false; - bool ShowEditPopup = false; - private ListResponse urls = new ListResponse(); - ShortUrlRequest shortUrlRequest = new ShortUrlRequest(); - ShortUrlEntity editedUrl; - SfGrid grdUrls; - public List gridData { get; set; } - - - protected override async Task OnInitializedAsync() - { - await RefreshGrid(); - } - - private async Task RefreshGrid() - { - try - { - urls = await Http.GetFromJsonAsync("/api/UrlList") ?? new ListResponse(); - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - } - } - - private async Task UpdateUIList() - { - await RefreshGrid(); - StateHasChanged(); - } - private async Task SaveShortUrl() - { - ShowCreatePopup = false; - - try - { - await Http.PostAsJsonAsync("/api/UrlShortener", shortUrlRequest); - await UpdateUIList(); - @* await grdUrls.ClearFilteringAsync(); - await grdUrls.FilterByColumnAsync("RowKey", "equal", shortUrlRequest.Vanity); *@ - } - catch (System.Exception ex) - { - Console.WriteLine(ex.ToString()); - } - } - - void ClosePopup() - { - ShowCreatePopup = false; - ShowEditPopup = false; - } - - [Authorize(Roles = "admin")] - void CreateShortUrl() - { - shortUrlRequest = new ShortUrlRequest(); - ShowCreatePopup = true; - } - - [Inject] public IJSRuntime JsRuntime { get; set; } - public async Task CopyToClipboardAsync(string url) - { - await JSRuntime.InvokeVoidAsync("clipboardCopy.copyText", url); - } - - - void EditShortUrl(ShortUrlEntity urlEntity) - { - editedUrl = urlEntity; - ShowEditPopup = true; - } - - private async Task SaveUpdatedShortUrl() - { - ShowEditPopup = false; - await Http.PostAsJsonAsync("/api/UrlUpdate", editedUrl); - await UpdateUIList(); - } - - private void NavigateToStats(string vanity){ - NavigationManager.NavigateTo("/Statistics/" + vanity); - } - - public async Task ArchiveShortUrl(ShortUrlEntity urlEntity) - { - await Http.PostAsJsonAsync("/api/UrlArchive", urlEntity); - await UpdateUIList(); - } - -} +@page "/urlmanager" + +@using Microsoft.AspNetCore.Authorization +@using Syncfusion.Blazor.Grids +@using Syncfusion.Blazor.Inputs +@using Cloud5mins.AzShortener +@using System.Net +@inject HttpClient Http +@inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager + +@attribute [Authorize(Roles = "admin")] + +Url Manager + +

Urls Manager

+

Create, Edit, Achives your URLs

+ +@if (urls.UrlList == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + + + + + + + + + + + + + + + +} + + +@if (ShowCreatePopup) +{ + + +} + + +@if (ShowEditPopup) +{ + + +} + +@code { + bool ShowCreatePopup = false; + bool ShowEditPopup = false; + private ListResponse urls = new ListResponse(); + ShortUrlRequest shortUrlRequest = new ShortUrlRequest(); + ShortUrlEntity editedUrl; + SfGrid grdUrls; + public List gridData { get; set; } + + + protected override async Task OnInitializedAsync() + { + await RefreshGrid(); + } + + private async Task RefreshGrid() + { + try + { + using var response = await Http.GetAsync("/api/UrlList"); + if(response.IsSuccessStatusCode){ + urls = await response.Content.ReadFromJsonAsync(); + } + else{ + switch (response.StatusCode) + { + case HttpStatusCode.Unauthorized: NavigationManager.NavigateTo("/unauthorized"); + break; + default: NavigationManager.NavigateTo("/404"); + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + private async Task UpdateUIList() + { + await RefreshGrid(); + StateHasChanged(); + } + private async Task SaveShortUrl() + { + ShowCreatePopup = false; + + try + { + await Http.PostAsJsonAsync("/api/UrlShortener", shortUrlRequest); + await UpdateUIList(); + @* await grdUrls.ClearFilteringAsync(); + await grdUrls.FilterByColumnAsync("RowKey", "equal", shortUrlRequest.Vanity); *@ + } + catch (System.Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + void ClosePopup() + { + ShowCreatePopup = false; + ShowEditPopup = false; + } + + [Authorize(Roles = "admin")] + void CreateShortUrl() + { + shortUrlRequest = new ShortUrlRequest(); + ShowCreatePopup = true; + } + + [Inject] public IJSRuntime JsRuntime { get; set; } + public async Task CopyToClipboardAsync(string url) + { + await JSRuntime.InvokeVoidAsync("clipboardCopy.copyText", url); + } + + + void EditShortUrl(ShortUrlEntity urlEntity) + { + editedUrl = urlEntity; + ShowEditPopup = true; + } + + private async Task SaveUpdatedShortUrl() + { + ShowEditPopup = false; + await Http.PostAsJsonAsync("/api/UrlUpdate", editedUrl); + await UpdateUIList(); + } + + private void NavigateToStats(string vanity){ + NavigationManager.NavigateTo("/Statistics/" + vanity); + } + + public async Task ArchiveShortUrl(ShortUrlEntity urlEntity) + { + await Http.PostAsJsonAsync("/api/UrlArchive", urlEntity); + await UpdateUIList(); + } + +} diff --git a/src/TinyBlazorAdmin/Program.cs b/src/client/Program.cs similarity index 65% rename from src/TinyBlazorAdmin/Program.cs rename to src/client/Program.cs index 6cb5923..e90241f 100644 --- a/src/TinyBlazorAdmin/Program.cs +++ b/src/client/Program.cs @@ -1,21 +1,25 @@ -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Syncfusion.Blazor; -using Cloud5mins.TinyBlazorAdmin; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); - -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["API_Prefix"] ?? builder.HostEnvironment.BaseAddress) }); - -builder.Services.AddMsalAuthentication(options => -{ - builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); -}); -// regiser fusion blazor service -// Community Licence for your personal use ONLY. Thank you Syncfusion for this generous offer. -Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("NzYyMzI1QDMyMzAyZTMxMmUzMFY0cEZ3MVozdkwvekVhek8xTWdPMkg2NlhvdVFNR1lvZHdhQWJWUlNjZW89"); -builder.Services.AddSyncfusionBlazor(options => { options.IgnoreScriptIsolation = true; }); - -await builder.Build().RunAsync(); +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Syncfusion.Blazor; +using Cloud5mins.TinyBlazorAdmin; +using AzureStaticWebApps.Blazor.Authentication; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services + .AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }) + .AddStaticWebAppsAuthentication(); + +// builder.Services.AddMsalAuthentication(options => +// { +// builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); +// }); + +// regiser fusion blazor service +// Community Licence for your personal use ONLY. Thank you Syncfusion for this generous offer. +Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("NzYyMzI1QDMyMzAyZTMxMmUzMFY0cEZ3MVozdkwvekVhek8xTWdPMkg2NlhvdVFNR1lvZHdhQWJWUlNjZW89"); +builder.Services.AddSyncfusionBlazor(options => { options.IgnoreScriptIsolation = true; }); + +await builder.Build().RunAsync(); diff --git a/src/TinyBlazorAdmin/Properties/launchSettings.json b/src/client/Properties/launchSettings.json similarity index 100% rename from src/TinyBlazorAdmin/Properties/launchSettings.json rename to src/client/Properties/launchSettings.json diff --git a/src/client/Shared/401.razor b/src/client/Shared/401.razor new file mode 100644 index 0000000..86e134e --- /dev/null +++ b/src/client/Shared/401.razor @@ -0,0 +1,3 @@ +@page "/unauthorized" +Tiny Blazor Admin - 401 + \ No newline at end of file diff --git a/src/client/Shared/404.razor b/src/client/Shared/404.razor new file mode 100644 index 0000000..aff36a0 --- /dev/null +++ b/src/client/Shared/404.razor @@ -0,0 +1,3 @@ +Tiny Blazor Admin - 404 +@page "/404" + \ No newline at end of file diff --git a/src/client/Shared/Custom401.razor b/src/client/Shared/Custom401.razor new file mode 100644 index 0000000..67ee647 --- /dev/null +++ b/src/client/Shared/Custom401.razor @@ -0,0 +1,29 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + +
+
+
+
+
+

Oops!

+

401 Unauthorized

+ + +
+ Sorry, you are not authorized to access this resource. +
+
+ +
+ Sorry, please get authenticated. +
+
+
+ Return Home +
+
+
+
+ +
diff --git a/src/client/Shared/Custom404.razor b/src/client/Shared/Custom404.razor new file mode 100644 index 0000000..da5c486 --- /dev/null +++ b/src/client/Shared/Custom404.razor @@ -0,0 +1,18 @@ + +
+
+
+
+
+

Oops!

+

404 Not Found

+
+ Sorry, an error has occured, Requested page not found! +
+ Return Home> +
+
+
+
+ +
\ No newline at end of file diff --git a/src/client/Shared/MainLayout.razor b/src/client/Shared/MainLayout.razor new file mode 100644 index 0000000..65dbb5a --- /dev/null +++ b/src/client/Shared/MainLayout.razor @@ -0,0 +1,26 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ + + Hello, @context.User.Identity?.Name! + Log out + + + Log in + + + About +
+ +
+ @Body +
+
+
diff --git a/src/TinyBlazorAdmin/Shared/MainLayout.razor.css b/src/client/Shared/MainLayout.razor.css similarity index 100% rename from src/TinyBlazorAdmin/Shared/MainLayout.razor.css rename to src/client/Shared/MainLayout.razor.css diff --git a/src/TinyBlazorAdmin/Shared/NavMenu.razor b/src/client/Shared/NavMenu.razor similarity index 54% rename from src/TinyBlazorAdmin/Shared/NavMenu.razor rename to src/client/Shared/NavMenu.razor index 70d57a8..5ba484f 100644 --- a/src/TinyBlazorAdmin/Shared/NavMenu.razor +++ b/src/client/Shared/NavMenu.razor @@ -11,17 +11,26 @@ diff --git a/src/TinyBlazorAdmin/Shared/NavMenu.razor.css b/src/client/Shared/NavMenu.razor.css similarity index 100% rename from src/TinyBlazorAdmin/Shared/NavMenu.razor.css rename to src/client/Shared/NavMenu.razor.css diff --git a/src/client/Shared/RedirectToLogin.razor b/src/client/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..d2eb183 --- /dev/null +++ b/src/client/Shared/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager Navigation + +@code { + protected override void OnInitialized() + { + Navigation.NavigateTo($"/.auth/login/aad?post_login_redirect_uri={Uri.EscapeDataString(Navigation.Uri)}"); + } +} diff --git a/src/TinyBlazorAdmin/Shared/ScheduleComponent.razor b/src/client/Shared/ScheduleComponent.razor similarity index 97% rename from src/TinyBlazorAdmin/Shared/ScheduleComponent.razor rename to src/client/Shared/ScheduleComponent.razor index c0e8379..4f7254f 100644 --- a/src/TinyBlazorAdmin/Shared/ScheduleComponent.razor +++ b/src/client/Shared/ScheduleComponent.razor @@ -1,39 +1,39 @@ -@inherits LayoutComponentBase -@using Cloud5mins.AzShortener -@using Syncfusion.Blazor.Calendars -@using Syncfusion.Blazor.Inputs - -
-
-
- -
-
- -
-
-
-
- -
-
-
-
- - Tool: https://crontab.guru/ -
-
-
-
- -
-
-
- - -@code { - - [Parameter] - public Schedule schedule { get; set; } - -} +@inherits LayoutComponentBase +@using Cloud5mins.AzShortener +@using Syncfusion.Blazor.Calendars +@using Syncfusion.Blazor.Inputs + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + Tool: https://crontab.guru/ +
+
+
+
+ +
+
+
+ + +@code { + + [Parameter] + public Schedule schedule { get; set; } + +} diff --git a/src/TinyBlazorAdmin/Shared/SchedulesComponent.razor b/src/client/Shared/SchedulesComponent.razor similarity index 96% rename from src/TinyBlazorAdmin/Shared/SchedulesComponent.razor rename to src/client/Shared/SchedulesComponent.razor index cc1d3fd..8919a7b 100644 --- a/src/TinyBlazorAdmin/Shared/SchedulesComponent.razor +++ b/src/client/Shared/SchedulesComponent.razor @@ -1,61 +1,61 @@ -@inherits LayoutComponentBase -@using Cloud5mins.AzShortener -@using Syncfusion.Blazor.Buttons -@using Syncfusion.Blazor.Navigations - -
-
- -
- - - - @foreach(var s in schedules) - { - - - - @s.Start.ToString("yyyy-MM-dd") < - @s.GetDisplayableUrl(25) > - @s.End.ToString("yyyy-MM-dd") - - - - - - } - - - -
- - - - - -@code { - - [Parameter] - public List schedules { get; set; } - - - private void AddScheduleClick(){ - schedules.Add(new Schedule()); - StateHasChanged(); - } - - private void DeleteSchedule(Schedule schedule){ - schedules.Remove(schedule); - StateHasChanged(); - } - +@inherits LayoutComponentBase +@using Cloud5mins.AzShortener +@using Syncfusion.Blazor.Buttons +@using Syncfusion.Blazor.Navigations + +
+
+ +
+ + + + @foreach(var s in schedules) + { + + + + @s.Start.ToString("yyyy-MM-dd") < + @s.GetDisplayableUrl(25) > + @s.End.ToString("yyyy-MM-dd") + + + + + + } + + + +
+ + + + + +@code { + + [Parameter] + public List schedules { get; set; } + + + private void AddScheduleClick(){ + schedules.Add(new Schedule()); + StateHasChanged(); + } + + private void DeleteSchedule(Schedule schedule){ + schedules.Remove(schedule); + StateHasChanged(); + } + } \ No newline at end of file diff --git a/src/TinyBlazorAdmin/TinyBlazorAdmin.csproj b/src/client/TinyBlazorAdmin.csproj similarity index 83% rename from src/TinyBlazorAdmin/TinyBlazorAdmin.csproj rename to src/client/TinyBlazorAdmin.csproj index bbb6eb0..410b954 100644 --- a/src/TinyBlazorAdmin/TinyBlazorAdmin.csproj +++ b/src/client/TinyBlazorAdmin.csproj @@ -1,24 +1,24 @@ - - - - net6.0 - disable - enable - Cloud5mins.TinyBlazorAdmin - - - - - - - - - - - - - - - - - + + + + net6.0 + disable + enable + Cloud5mins.TinyBlazorAdmin + + + + + + + + + + + + + + + + + diff --git a/src/TinyBlazorAdmin/_Imports.razor b/src/client/_Imports.razor similarity index 84% rename from src/TinyBlazorAdmin/_Imports.razor rename to src/client/_Imports.razor index 72da8ff..f6f049b 100644 --- a/src/TinyBlazorAdmin/_Imports.razor +++ b/src/client/_Imports.razor @@ -1,13 +1,14 @@ -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.AspNetCore.Components.WebAssembly.Http -@using Microsoft.JSInterop -@using Cloud5mins.TinyBlazorAdmin -@using Cloud5mins.TinyBlazorAdmin.Shared -@using Syncfusion.Blazor -@using Syncfusion.Blazor.Calendars \ No newline at end of file +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Cloud5mins.TinyBlazorAdmin +@using Cloud5mins.TinyBlazorAdmin.Shared +@using Syncfusion.Blazor +@using Syncfusion.Blazor.Calendars diff --git a/src/client/staticwebapp.config.json b/src/client/staticwebapp.config.json new file mode 100644 index 0000000..33093bb --- /dev/null +++ b/src/client/staticwebapp.config.json @@ -0,0 +1,50 @@ +{ + "navigationFallback": { + "rewrite": "/index.html" + }, + "routes": [ + { + "route": "/urlmanager*", + "allowedRoles": ["admin"] + }, + { + "route": "/statistics*", + "allowedRoles": ["admin"] + }, + { + "route": "/api/*", + "methods": ["GET", "POST"], + "allowedRoles": ["admin"] + }, + { + "route": "/login", + "rewrite": "/.auth/login/aad" + }, + { + "route": "/.auth/login/twitter", + "statusCode": 404 + }, + { + "route": "/logout*", + "redirect": "/.auth/logout" + } + , + { + "route": "/unauthorized*", + "redirect": "/unauthorized" + } + ], + "responseOverrides": { + "401": { + "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer", + "statusCode": 302 + }, + "403": { + "rewrite": "/unauthorized" + }, + "404": { + "rewrite": "/404" + } + } + +} \ No newline at end of file diff --git a/src/TinyBlazorAdmin/wwwroot/appsettings.example.json b/src/client/wwwroot/appsettings.example.json similarity index 97% rename from src/TinyBlazorAdmin/wwwroot/appsettings.example.json rename to src/client/wwwroot/appsettings.example.json index f848fb8..acd9704 100644 --- a/src/TinyBlazorAdmin/wwwroot/appsettings.example.json +++ b/src/client/wwwroot/appsettings.example.json @@ -1,13 +1,13 @@ -{ - /* - The following identity settings need to be configured - before the project can be successfully executed. - For more info see https://aka.ms/dotnet-template-ms-identity-platform - */ - "AzureAd": { - "Authority": "https://login.microsoftonline.com/206bad4c-d071-4c91-9181-ef7047e6590b", - "ClientId": "8ff845ba-70f7-479a-a2c5-73f38b650232", - "ValidateAuthority": true - }, - "API_Prefix": "http://localhost:7071" +{ + /* + The following identity settings need to be configured + before the project can be successfully executed. + For more info see https://aka.ms/dotnet-template-ms-identity-platform + */ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/206bad4c-d071-4c91-9181-ef7047e6590b", + "ClientId": "8ff845ba-70f7-479a-a2c5-73f38b650232", + "ValidateAuthority": true + }, + "API_Prefix": "http://localhost:7071" } \ No newline at end of file diff --git a/src/client/wwwroot/appsettings.json b/src/client/wwwroot/appsettings.json new file mode 100644 index 0000000..be1fc3d --- /dev/null +++ b/src/client/wwwroot/appsettings.json @@ -0,0 +1,3 @@ +{ + "API_Prefix": "http://localhost:7071" +} \ No newline at end of file diff --git a/src/TinyBlazorAdmin/wwwroot/css/app.css b/src/client/wwwroot/css/app.css similarity index 97% rename from src/TinyBlazorAdmin/wwwroot/css/app.css rename to src/client/wwwroot/css/app.css index 9cd148f..7157ee8 100644 --- a/src/TinyBlazorAdmin/wwwroot/css/app.css +++ b/src/client/wwwroot/css/app.css @@ -1,64 +1,64 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); - -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -h1:focus { - outline: none; -} - -a, .btn-link { - color: #0071c1; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.content { - padding-top: 1.1rem; -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid red; -} - -.validation-message { - color: red; -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } diff --git a/src/TinyBlazorAdmin/wwwroot/css/bootstrap/bootstrap.min.css b/src/client/wwwroot/css/bootstrap/bootstrap.min.css similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/bootstrap/bootstrap.min.css rename to src/client/wwwroot/css/bootstrap/bootstrap.min.css diff --git a/src/TinyBlazorAdmin/wwwroot/css/bootstrap/bootstrap.min.css.map b/src/client/wwwroot/css/bootstrap/bootstrap.min.css.map similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/bootstrap/bootstrap.min.css.map rename to src/client/wwwroot/css/bootstrap/bootstrap.min.css.map diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/FONT-LICENSE b/src/client/wwwroot/css/open-iconic/FONT-LICENSE similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/FONT-LICENSE rename to src/client/wwwroot/css/open-iconic/FONT-LICENSE diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/ICON-LICENSE b/src/client/wwwroot/css/open-iconic/ICON-LICENSE similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/ICON-LICENSE rename to src/client/wwwroot/css/open-iconic/ICON-LICENSE diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/README.md b/src/client/wwwroot/css/open-iconic/README.md similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/README.md rename to src/client/wwwroot/css/open-iconic/README.md diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/src/client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css rename to src/client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.eot rename to src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.otf rename to src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.svg similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.svg rename to src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.svg diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf rename to src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf diff --git a/src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/css/open-iconic/font/fonts/open-iconic.woff rename to src/client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff diff --git a/src/client/wwwroot/favicon-16x16.png b/src/client/wwwroot/favicon-16x16.png new file mode 100644 index 0000000..25a56b4 Binary files /dev/null and b/src/client/wwwroot/favicon-16x16.png differ diff --git a/src/client/wwwroot/favicon-32x32.png b/src/client/wwwroot/favicon-32x32.png new file mode 100644 index 0000000..9dd0a08 Binary files /dev/null and b/src/client/wwwroot/favicon-32x32.png differ diff --git a/src/client/wwwroot/favicon.ico b/src/client/wwwroot/favicon.ico new file mode 100644 index 0000000..f4ac6c2 Binary files /dev/null and b/src/client/wwwroot/favicon.ico differ diff --git a/src/TinyBlazorAdmin/wwwroot/images/TinyBlazorAdmin.png b/src/client/wwwroot/images/TinyBlazorAdmin.png similarity index 100% rename from src/TinyBlazorAdmin/wwwroot/images/TinyBlazorAdmin.png rename to src/client/wwwroot/images/TinyBlazorAdmin.png diff --git a/src/TinyBlazorAdmin/wwwroot/index.html b/src/client/wwwroot/index.html similarity index 84% rename from src/TinyBlazorAdmin/wwwroot/index.html rename to src/client/wwwroot/index.html index 9c7a04b..17295d3 100644 --- a/src/TinyBlazorAdmin/wwwroot/index.html +++ b/src/client/wwwroot/index.html @@ -1,39 +1,42 @@ - - - - - - - Tiny Blazor Admin - - - - - - - - - -
Loading all the tiny URLs...
- -
- An unhandled error has occurred. - Reload - 🗙 -
- - - - - - - + + + + + + + Tiny Blazor Admin + + + + + + + + + + + + +
Loading all the tiny URLs...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + diff --git a/src/azShortenerLib/ClickDate.cs b/src/lib/ClickDate.cs similarity index 95% rename from src/azShortenerLib/ClickDate.cs rename to src/lib/ClickDate.cs index 2a78384..f625bb8 100644 --- a/src/azShortenerLib/ClickDate.cs +++ b/src/lib/ClickDate.cs @@ -1,8 +1,8 @@ -namespace Cloud5mins.AzShortener -{ - public class ClickDate - { - public string DateClicked { get; set; } - public int Count { get; set; } - } +namespace Cloud5mins.AzShortener +{ + public class ClickDate + { + public string DateClicked { get; set; } + public int Count { get; set; } + } } \ No newline at end of file diff --git a/src/azShortenerLib/ClickDateList.cs b/src/lib/ClickDateList.cs similarity index 95% rename from src/azShortenerLib/ClickDateList.cs rename to src/lib/ClickDateList.cs index 850ba07..f43ee11 100644 --- a/src/azShortenerLib/ClickDateList.cs +++ b/src/lib/ClickDateList.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; - -namespace Cloud5mins.AzShortener -{ - public class ClickDateList - { - public List? Items { get; set; } - public string Url { get; set; } - public ClickDateList(){ - Url = string.Empty; - } - public ClickDateList (List list) - { - Items = list; - Url = string.Empty; - } - } +using System; +using System.Collections.Generic; + +namespace Cloud5mins.AzShortener +{ + public class ClickDateList + { + public List? Items { get; set; } + public string Url { get; set; } + public ClickDateList(){ + Url = string.Empty; + } + public ClickDateList (List list) + { + Items = list; + Url = string.Empty; + } + } } \ No newline at end of file diff --git a/src/azShortenerLib/ClickStatsEntity.cs b/src/lib/ClickStatsEntity.cs similarity index 100% rename from src/azShortenerLib/ClickStatsEntity.cs rename to src/lib/ClickStatsEntity.cs diff --git a/src/azShortenerLib/ClickStatsEntityList.cs b/src/lib/ClickStatsEntityList.cs similarity index 96% rename from src/azShortenerLib/ClickStatsEntityList.cs rename to src/lib/ClickStatsEntityList.cs index 70516bd..5ae8e8e 100644 --- a/src/azShortenerLib/ClickStatsEntityList.cs +++ b/src/lib/ClickStatsEntityList.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; - -namespace Cloud5mins.AzShortener -{ - public class ClickStatsEntityList - { - public List ClickStatsList { get; set; } - - public ClickStatsEntityList(){} - public ClickStatsEntityList (List list) - { - ClickStatsList = list; - } - } +using System; +using System.Collections.Generic; + +namespace Cloud5mins.AzShortener +{ + public class ClickStatsEntityList + { + public List ClickStatsList { get; set; } + + public ClickStatsEntityList(){} + public ClickStatsEntityList (List list) + { + ClickStatsList = list; + } + } } \ No newline at end of file diff --git a/src/azShortenerLib/ListResponse.cs b/src/lib/ListResponse.cs similarity index 100% rename from src/azShortenerLib/ListResponse.cs rename to src/lib/ListResponse.cs diff --git a/src/azShortenerLib/NextId.cs b/src/lib/NextId.cs similarity index 100% rename from src/azShortenerLib/NextId.cs rename to src/lib/NextId.cs diff --git a/src/azShortenerLib/Schedule.cs b/src/lib/Schedule.cs similarity index 96% rename from src/azShortenerLib/Schedule.cs rename to src/lib/Schedule.cs index c9c5df2..836a252 100644 --- a/src/azShortenerLib/Schedule.cs +++ b/src/lib/Schedule.cs @@ -1,45 +1,45 @@ -using System; -using Cronos; - -namespace Cloud5mins.AzShortener -{ - public class Schedule - { - public DateTime Start { get; set; } = DateTime.Now.AddMonths(-6); - public DateTime End { get; set; } = DateTime.Now.AddMonths(6); - - public string AlternativeUrl { get; set; } = ""; - public string Cron { get; set; } = "* * * * *"; - - public int DurationMinutes { get; set; } = 0; - - public string GetDisplayableUrl(int max) - { - var length = AlternativeUrl.ToString().Length; - if (length >= max) - { - return string.Concat(AlternativeUrl.Substring(0, max-1), "..."); - } - return AlternativeUrl; - } - - public bool IsActive(DateTime pointInTime) - { - var bufferStart = pointInTime.AddMinutes(-DurationMinutes); - var expires = pointInTime.AddMinutes(DurationMinutes); - - CronExpression expression = CronExpression.Parse(Cron); - var occurences = expression.GetOccurrences(bufferStart, expires); - - foreach (DateTime d in occurences) - { - if (d < pointInTime && d < expires) - { - return true; - } - } - - return false; - } - } -} +using System; +using Cronos; + +namespace Cloud5mins.AzShortener +{ + public class Schedule + { + public DateTime Start { get; set; } = DateTime.Now.AddMonths(-6); + public DateTime End { get; set; } = DateTime.Now.AddMonths(6); + + public string AlternativeUrl { get; set; } = ""; + public string Cron { get; set; } = "* * * * *"; + + public int DurationMinutes { get; set; } = 0; + + public string GetDisplayableUrl(int max) + { + var length = AlternativeUrl.ToString().Length; + if (length >= max) + { + return string.Concat(AlternativeUrl.Substring(0, max-1), "..."); + } + return AlternativeUrl; + } + + public bool IsActive(DateTime pointInTime) + { + var bufferStart = pointInTime.AddMinutes(-DurationMinutes); + var expires = pointInTime.AddMinutes(DurationMinutes); + + CronExpression expression = CronExpression.Parse(Cron); + var occurences = expression.GetOccurrences(bufferStart, expires); + + foreach (DateTime d in occurences) + { + if (d < pointInTime && d < expires) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/azShortenerLib/ShortRequest.cs b/src/lib/ShortRequest.cs similarity index 95% rename from src/azShortenerLib/ShortRequest.cs rename to src/lib/ShortRequest.cs index 81b264d..8962635 100644 --- a/src/azShortenerLib/ShortRequest.cs +++ b/src/lib/ShortRequest.cs @@ -1,13 +1,13 @@ -namespace Cloud5mins.AzShortener -{ - public class ShortRequest - { - public string Vanity { get; set; } - - public string Url { get; set; } - - public string Title { get; set; } - - public Schedule[] Schedules { get; set; } - } +namespace Cloud5mins.AzShortener +{ + public class ShortRequest + { + public string Vanity { get; set; } + + public string Url { get; set; } + + public string Title { get; set; } + + public Schedule[] Schedules { get; set; } + } } \ No newline at end of file diff --git a/src/azShortenerLib/ShortResponse.cs b/src/lib/ShortResponse.cs similarity index 96% rename from src/azShortenerLib/ShortResponse.cs rename to src/lib/ShortResponse.cs index ba84ad4..c07aecf 100644 --- a/src/azShortenerLib/ShortResponse.cs +++ b/src/lib/ShortResponse.cs @@ -1,20 +1,20 @@ -using System; - -namespace Cloud5mins.AzShortener -{ - public class ShortResponse - { - public string ShortUrl { get; set; } - public string LongUrl { get; set; } - public string Title { get; set; } - - public ShortResponse(){} - public ShortResponse (string host, string longUrl, string endUrl, string title) - { - LongUrl = longUrl; - ShortUrl = string.Concat(host, "/", endUrl); - Title = title; - - } - } +using System; + +namespace Cloud5mins.AzShortener +{ + public class ShortResponse + { + public string ShortUrl { get; set; } + public string LongUrl { get; set; } + public string Title { get; set; } + + public ShortResponse(){} + public ShortResponse (string host, string longUrl, string endUrl, string title) + { + LongUrl = longUrl; + ShortUrl = string.Concat(host, "/", endUrl); + Title = title; + + } + } } \ No newline at end of file diff --git a/src/azShortenerLib/ShortUrlEntity.cs b/src/lib/ShortUrlEntity.cs similarity index 96% rename from src/azShortenerLib/ShortUrlEntity.cs rename to src/lib/ShortUrlEntity.cs index 0009238..bf6af84 100644 --- a/src/azShortenerLib/ShortUrlEntity.cs +++ b/src/lib/ShortUrlEntity.cs @@ -1,130 +1,130 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Azure.Cosmos.Table; - -namespace Cloud5mins.AzShortener -{ - public class ShortUrlEntity : TableEntity - { - public string Url { get; set; } - private string _activeUrl { get; set; } - - public string ActiveUrl - { - get - { - if (String.IsNullOrEmpty(_activeUrl)) - _activeUrl = GetActiveUrl(); - return _activeUrl; - } - } - - - public string Title { get; set; } - - public string ShortUrl { get; set; } - - public int Clicks { get; set; } - - public bool? IsArchived { get; set; } - public string SchedulesPropertyRaw { get; set; } - - private List _schedules { get; set; } - - [IgnoreProperty] - public List Schedules - { - get - { - if (_schedules == null) - { - if (String.IsNullOrEmpty(SchedulesPropertyRaw)) - { - _schedules = new List(); - } - else - { - _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw).ToList(); - } - } - return _schedules; - } - set - { - _schedules = value; - } - } - - public ShortUrlEntity() { } - - public ShortUrlEntity(string longUrl, string endUrl) - { - Initialize(longUrl, endUrl, string.Empty, null); - } - - public ShortUrlEntity(string longUrl, string endUrl, Schedule[] schedules) - { - Initialize(longUrl, endUrl, string.Empty, schedules); - } - - public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[] schedules) - { - Initialize(longUrl, endUrl, title, schedules); - } - - private void Initialize(string longUrl, string endUrl, string title, Schedule[] schedules) - { - PartitionKey = endUrl.First().ToString(); - RowKey = endUrl; - Url = longUrl; - Title = title; - Clicks = 0; - IsArchived = false; - Schedules = schedules.ToList(); - - if(schedules.Length>0) - SchedulesPropertyRaw = JsonSerializer.Serialize>(Schedules); - } - - public static ShortUrlEntity GetEntity(string longUrl, string endUrl, string title, Schedule[] schedules) - { - return new ShortUrlEntity - { - PartitionKey = endUrl.First().ToString(), - RowKey = endUrl, - Url = longUrl, - Title = title, - Schedules = schedules.ToList() - }; - } - - private string GetActiveUrl() - { - if (Schedules != null) - return GetActiveUrl(DateTime.UtcNow); - return Url; - } - private string GetActiveUrl(DateTime pointInTime) - { - var link = Url; - var active = Schedules.Where(s => - s.End > pointInTime && //hasn't ended - s.Start < pointInTime //already started - ).OrderBy(s => s.Start); //order by start to process first link - - foreach (var sched in active.ToArray()) - { - if (sched.IsActive(pointInTime)) - { - link = sched.AlternativeUrl; - break; - } - } - return link; - } - - } - +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Azure.Cosmos.Table; + +namespace Cloud5mins.AzShortener +{ + public class ShortUrlEntity : TableEntity + { + public string Url { get; set; } + private string _activeUrl { get; set; } + + public string ActiveUrl + { + get + { + if (String.IsNullOrEmpty(_activeUrl)) + _activeUrl = GetActiveUrl(); + return _activeUrl; + } + } + + + public string Title { get; set; } + + public string ShortUrl { get; set; } + + public int Clicks { get; set; } + + public bool? IsArchived { get; set; } + public string SchedulesPropertyRaw { get; set; } + + private List _schedules { get; set; } + + [IgnoreProperty] + public List Schedules + { + get + { + if (_schedules == null) + { + if (String.IsNullOrEmpty(SchedulesPropertyRaw)) + { + _schedules = new List(); + } + else + { + _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw).ToList(); + } + } + return _schedules; + } + set + { + _schedules = value; + } + } + + public ShortUrlEntity() { } + + public ShortUrlEntity(string longUrl, string endUrl) + { + Initialize(longUrl, endUrl, string.Empty, null); + } + + public ShortUrlEntity(string longUrl, string endUrl, Schedule[] schedules) + { + Initialize(longUrl, endUrl, string.Empty, schedules); + } + + public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[] schedules) + { + Initialize(longUrl, endUrl, title, schedules); + } + + private void Initialize(string longUrl, string endUrl, string title, Schedule[] schedules) + { + PartitionKey = endUrl.First().ToString(); + RowKey = endUrl; + Url = longUrl; + Title = title; + Clicks = 0; + IsArchived = false; + Schedules = schedules.ToList(); + + if(schedules.Length>0) + SchedulesPropertyRaw = JsonSerializer.Serialize>(Schedules); + } + + public static ShortUrlEntity GetEntity(string longUrl, string endUrl, string title, Schedule[] schedules) + { + return new ShortUrlEntity + { + PartitionKey = endUrl.First().ToString(), + RowKey = endUrl, + Url = longUrl, + Title = title, + Schedules = schedules.ToList() + }; + } + + private string GetActiveUrl() + { + if (Schedules != null) + return GetActiveUrl(DateTime.UtcNow); + return Url; + } + private string GetActiveUrl(DateTime pointInTime) + { + var link = Url; + var active = Schedules.Where(s => + s.End > pointInTime && //hasn't ended + s.Start < pointInTime //already started + ).OrderBy(s => s.Start); //order by start to process first link + + foreach (var sched in active.ToArray()) + { + if (sched.IsActive(pointInTime)) + { + link = sched.AlternativeUrl; + break; + } + } + return link; + } + + } + } \ No newline at end of file diff --git a/src/azShortenerLib/ShortUrlRequest.cs b/src/lib/ShortUrlRequest.cs similarity index 95% rename from src/azShortenerLib/ShortUrlRequest.cs rename to src/lib/ShortUrlRequest.cs index 2a22779..78eb4cb 100644 --- a/src/azShortenerLib/ShortUrlRequest.cs +++ b/src/lib/ShortUrlRequest.cs @@ -1,41 +1,41 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Cloud5mins.AzShortener -{ - public class ShortUrlRequest - { - private string _vanity; - - public string? Title { get; set; } - - public string Vanity - { - get - { - return (_vanity != null) ? _vanity : string.Empty; - } - set - { - _vanity = value; - } - } - - [Required] - public string Url { get; set; } - - private List _schedules; - - public List Schedules { - get{ - if(_schedules == null){ - _schedules = new List(); - } - return _schedules; - } - set{ - _schedules = value; - } - } - } +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Cloud5mins.AzShortener +{ + public class ShortUrlRequest + { + private string _vanity; + + public string? Title { get; set; } + + public string Vanity + { + get + { + return (_vanity != null) ? _vanity : string.Empty; + } + set + { + _vanity = value; + } + } + + [Required] + public string Url { get; set; } + + private List _schedules; + + public List Schedules { + get{ + if(_schedules == null){ + _schedules = new List(); + } + return _schedules; + } + set{ + _schedules = value; + } + } + } } \ No newline at end of file diff --git a/src/azShortenerLib/StorageTableHelper.cs b/src/lib/StorageTableHelper.cs similarity index 97% rename from src/azShortenerLib/StorageTableHelper.cs rename to src/lib/StorageTableHelper.cs index 5947512..1e8065b 100644 --- a/src/azShortenerLib/StorageTableHelper.cs +++ b/src/lib/StorageTableHelper.cs @@ -1,196 +1,196 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Azure.Cosmos.Table; - -namespace Cloud5mins.AzShortener -{ - public class StorageTableHelper - { - private string StorageConnectionString { get; set; } - - public StorageTableHelper() { } - - public StorageTableHelper(string storageConnectionString) - { - StorageConnectionString = storageConnectionString; - } - public CloudStorageAccount CreateStorageAccountFromConnectionString() - { - CloudStorageAccount storageAccount = CloudStorageAccount.Parse(this.StorageConnectionString); - return storageAccount; - } - - private CloudTable GetTable(string tableName) - { - CloudStorageAccount storageAccount = this.CreateStorageAccountFromConnectionString(); - CloudTableClient tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration()); - CloudTable table = tableClient.GetTableReference(tableName); - table.CreateIfNotExists(); - - return table; - } - private CloudTable GetUrlsTable() - { - CloudTable table = GetTable("UrlsDetails"); - return table; - } - - private CloudTable GetStatsTable() - { - CloudTable table = GetTable("ClickStats"); - return table; - } - - public async Task GetShortUrlEntity(ShortUrlEntity row) - { - TableOperation selOperation = TableOperation.Retrieve(row.PartitionKey, row.RowKey); - TableResult result = await GetUrlsTable().ExecuteAsync(selOperation); - ShortUrlEntity eShortUrl = result.Result as ShortUrlEntity; - return eShortUrl; - } - - public async Task> GetAllShortUrlEntities() - { - var tblUrls = GetUrlsTable(); - TableContinuationToken token = null; - var lstShortUrl = new List(); - do - { - // Retreiving all entities that are NOT the NextId entity - // (it's the only one in the partion "KEY") - TableQuery rangeQuery = new TableQuery().Where( - filter: TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.NotEqual, "KEY")); - - var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(rangeQuery, token); - lstShortUrl.AddRange(queryResult.Results as List); - token = queryResult.ContinuationToken; - } while (token != null); - return lstShortUrl; - } - - /// - /// Returns the ShortUrlEntity of the - /// - /// - /// ShortUrlEntity - public async Task GetShortUrlEntityByVanity(string vanity) - { - var tblUrls = GetUrlsTable(); - TableContinuationToken token = null; - ShortUrlEntity shortUrlEntity = null; - do - { - TableQuery query = new TableQuery().Where( - filter: TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, vanity)); - var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(query, token); - shortUrlEntity = queryResult.Results.FirstOrDefault(); - } while (token != null); - - return shortUrlEntity; - } - - public async Task SaveClickStatsEntity(ClickStatsEntity newStats) - { - TableOperation insOperation = TableOperation.InsertOrMerge(newStats); - TableResult result = await GetStatsTable().ExecuteAsync(insOperation); - } - - public async Task SaveShortUrlEntity(ShortUrlEntity newShortUrl) - { - - // serializing the collection easier on json shares - //newShortUrl.SchedulesPropertyRaw = JsonSerializer.Serialize>(newShortUrl.Schedules); - - TableOperation insOperation = TableOperation.InsertOrMerge(newShortUrl); - TableResult result = await GetUrlsTable().ExecuteAsync(insOperation); - ShortUrlEntity eShortUrl = result.Result as ShortUrlEntity; - return eShortUrl; - } - - public async Task IfShortUrlEntityExistByVanity(string vanity) - { - ShortUrlEntity shortUrlEntity = await GetShortUrlEntityByVanity(vanity); - return (shortUrlEntity != null); - } - - public async Task IfShortUrlEntityExist(ShortUrlEntity row) - { - ShortUrlEntity eShortUrl = await GetShortUrlEntity(row); - return (eShortUrl != null); - } - public async Task GetNextTableId() - { - //Get current ID - TableOperation selOperation = TableOperation.Retrieve("1", "KEY"); - TableResult result = await GetUrlsTable().ExecuteAsync(selOperation); - NextId entity = result.Result as NextId; - - if (entity == null) - { - entity = new NextId - { - PartitionKey = "1", - RowKey = "KEY", - Id = 1024 - }; - } - entity.Id++; - - //Update - TableOperation updOperation = TableOperation.InsertOrMerge(entity); - - // Execute the operation. - await GetUrlsTable().ExecuteAsync(updOperation); - - return entity.Id; - } - - - public async Task UpdateShortUrlEntity(ShortUrlEntity urlEntity) - { - ShortUrlEntity originalUrl = await GetShortUrlEntity(urlEntity); - originalUrl.Url = urlEntity.Url; - originalUrl.Title = urlEntity.Title; - originalUrl.SchedulesPropertyRaw = JsonSerializer.Serialize>(urlEntity.Schedules); - - return await SaveShortUrlEntity(originalUrl); - } - - - public async Task> GetAllStatsByVanity(string vanity) - { - var tblUrls = GetStatsTable(); - TableContinuationToken token = null; - var lstShortUrl = new List(); - do - { - TableQuery rangeQuery; - - if(string.IsNullOrEmpty(vanity)){ - rangeQuery = new TableQuery(); - } - else{ - rangeQuery = new TableQuery().Where( - filter: TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, vanity)); - } - - var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(rangeQuery, token); - lstShortUrl.AddRange(queryResult.Results as List); - token = queryResult.ContinuationToken; - } while (token != null); - return lstShortUrl; - } - - - public async Task ArchiveShortUrlEntity(ShortUrlEntity urlEntity) - { - ShortUrlEntity originalUrl = await GetShortUrlEntity(urlEntity); - originalUrl.IsArchived = true; - - return await SaveShortUrlEntity(originalUrl); - } - } +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos.Table; + +namespace Cloud5mins.AzShortener +{ + public class StorageTableHelper + { + private string StorageConnectionString { get; set; } + + public StorageTableHelper() { } + + public StorageTableHelper(string storageConnectionString) + { + StorageConnectionString = storageConnectionString; + } + public CloudStorageAccount CreateStorageAccountFromConnectionString() + { + CloudStorageAccount storageAccount = CloudStorageAccount.Parse(this.StorageConnectionString); + return storageAccount; + } + + private CloudTable GetTable(string tableName) + { + CloudStorageAccount storageAccount = this.CreateStorageAccountFromConnectionString(); + CloudTableClient tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration()); + CloudTable table = tableClient.GetTableReference(tableName); + table.CreateIfNotExists(); + + return table; + } + private CloudTable GetUrlsTable() + { + CloudTable table = GetTable("UrlsDetails"); + return table; + } + + private CloudTable GetStatsTable() + { + CloudTable table = GetTable("ClickStats"); + return table; + } + + public async Task GetShortUrlEntity(ShortUrlEntity row) + { + TableOperation selOperation = TableOperation.Retrieve(row.PartitionKey, row.RowKey); + TableResult result = await GetUrlsTable().ExecuteAsync(selOperation); + ShortUrlEntity eShortUrl = result.Result as ShortUrlEntity; + return eShortUrl; + } + + public async Task> GetAllShortUrlEntities() + { + var tblUrls = GetUrlsTable(); + TableContinuationToken token = null; + var lstShortUrl = new List(); + do + { + // Retreiving all entities that are NOT the NextId entity + // (it's the only one in the partion "KEY") + TableQuery rangeQuery = new TableQuery().Where( + filter: TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.NotEqual, "KEY")); + + var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(rangeQuery, token); + lstShortUrl.AddRange(queryResult.Results as List); + token = queryResult.ContinuationToken; + } while (token != null); + return lstShortUrl; + } + + /// + /// Returns the ShortUrlEntity of the + /// + /// + /// ShortUrlEntity + public async Task GetShortUrlEntityByVanity(string vanity) + { + var tblUrls = GetUrlsTable(); + TableContinuationToken token = null; + ShortUrlEntity shortUrlEntity = null; + do + { + TableQuery query = new TableQuery().Where( + filter: TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, vanity)); + var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(query, token); + shortUrlEntity = queryResult.Results.FirstOrDefault(); + } while (token != null); + + return shortUrlEntity; + } + + public async Task SaveClickStatsEntity(ClickStatsEntity newStats) + { + TableOperation insOperation = TableOperation.InsertOrMerge(newStats); + TableResult result = await GetStatsTable().ExecuteAsync(insOperation); + } + + public async Task SaveShortUrlEntity(ShortUrlEntity newShortUrl) + { + + // serializing the collection easier on json shares + //newShortUrl.SchedulesPropertyRaw = JsonSerializer.Serialize>(newShortUrl.Schedules); + + TableOperation insOperation = TableOperation.InsertOrMerge(newShortUrl); + TableResult result = await GetUrlsTable().ExecuteAsync(insOperation); + ShortUrlEntity eShortUrl = result.Result as ShortUrlEntity; + return eShortUrl; + } + + public async Task IfShortUrlEntityExistByVanity(string vanity) + { + ShortUrlEntity shortUrlEntity = await GetShortUrlEntityByVanity(vanity); + return (shortUrlEntity != null); + } + + public async Task IfShortUrlEntityExist(ShortUrlEntity row) + { + ShortUrlEntity eShortUrl = await GetShortUrlEntity(row); + return (eShortUrl != null); + } + public async Task GetNextTableId() + { + //Get current ID + TableOperation selOperation = TableOperation.Retrieve("1", "KEY"); + TableResult result = await GetUrlsTable().ExecuteAsync(selOperation); + NextId entity = result.Result as NextId; + + if (entity == null) + { + entity = new NextId + { + PartitionKey = "1", + RowKey = "KEY", + Id = 1024 + }; + } + entity.Id++; + + //Update + TableOperation updOperation = TableOperation.InsertOrMerge(entity); + + // Execute the operation. + await GetUrlsTable().ExecuteAsync(updOperation); + + return entity.Id; + } + + + public async Task UpdateShortUrlEntity(ShortUrlEntity urlEntity) + { + ShortUrlEntity originalUrl = await GetShortUrlEntity(urlEntity); + originalUrl.Url = urlEntity.Url; + originalUrl.Title = urlEntity.Title; + originalUrl.SchedulesPropertyRaw = JsonSerializer.Serialize>(urlEntity.Schedules); + + return await SaveShortUrlEntity(originalUrl); + } + + + public async Task> GetAllStatsByVanity(string vanity) + { + var tblUrls = GetStatsTable(); + TableContinuationToken token = null; + var lstShortUrl = new List(); + do + { + TableQuery rangeQuery; + + if(string.IsNullOrEmpty(vanity)){ + rangeQuery = new TableQuery(); + } + else{ + rangeQuery = new TableQuery().Where( + filter: TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, vanity)); + } + + var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(rangeQuery, token); + lstShortUrl.AddRange(queryResult.Results as List); + token = queryResult.ContinuationToken; + } while (token != null); + return lstShortUrl; + } + + + public async Task ArchiveShortUrlEntity(ShortUrlEntity urlEntity) + { + ShortUrlEntity originalUrl = await GetShortUrlEntity(urlEntity); + originalUrl.IsArchived = true; + + return await SaveShortUrlEntity(originalUrl); + } + } } \ No newline at end of file diff --git a/src/azShortenerLib/UrlClickStatsRequest.cs b/src/lib/UrlClickStatsRequest.cs similarity index 95% rename from src/azShortenerLib/UrlClickStatsRequest.cs rename to src/lib/UrlClickStatsRequest.cs index be16ac2..795f7a4 100644 --- a/src/azShortenerLib/UrlClickStatsRequest.cs +++ b/src/lib/UrlClickStatsRequest.cs @@ -1,12 +1,12 @@ -namespace Cloud5mins.AzShortener -{ - public class UrlClickStatsRequest - { - public string Vanity { get; set; } - - public UrlClickStatsRequest(string vanity) - { - Vanity = vanity; - } - } +namespace Cloud5mins.AzShortener +{ + public class UrlClickStatsRequest + { + public string Vanity { get; set; } + + public UrlClickStatsRequest(string vanity) + { + Vanity = vanity; + } + } } \ No newline at end of file diff --git a/src/azShortenerLib/Utility.cs b/src/lib/Utility.cs similarity index 68% rename from src/azShortenerLib/Utility.cs rename to src/lib/Utility.cs index c019a2f..6429ee5 100644 --- a/src/azShortenerLib/Utility.cs +++ b/src/lib/Utility.cs @@ -1,98 +1,67 @@ -using System.Linq; -using System.Security.Cryptography; -using System.Threading.Tasks; -using System.Security.Claims; -using Microsoft.Extensions.Logging; -//using Microsoft.AspNetCore.Mvc; - - -namespace Cloud5mins.AzShortener -{ - 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 = new RNGCryptoServiceProvider()) - { - //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; - } - } - - // public static IActionResult CatchUnauthorize(ClaimsPrincipal principal, ILogger log) - // { - // if (principal == null) - // { - // _logger.LogWarning("No principal."); - // return new UnauthorizedResult(); - // } - - // if (principal.Identity == null) - // { - // _logger.LogWarning("No identity."); - // return new UnauthorizedResult(); - // } - - // if (!principal.Identity.IsAuthenticated) - // { - // _logger.LogWarning("Request was not authenticated."); - // return new UnauthorizedResult(); - // } - - // if (principal.FindFirst(ClaimTypes.GivenName) is null) - // { - // _logger.LogError("Claim not Found"); - // return new BadRequestObjectResult(new - // { - // message = "Claim not Found", - // StatusCode = System.Net.HttpStatusCode.BadRequest - // }); - // } - // return null; - // } - } +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System.Security.Claims; +using Microsoft.Extensions.Logging; +//using Microsoft.AspNetCore.Mvc; + + +namespace Cloud5mins.AzShortener +{ + 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 = new RNGCryptoServiceProvider()) + { + //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/azShortenerLib/azShortenerLib.csproj b/src/lib/azShortenerLib.csproj similarity index 97% rename from src/azShortenerLib/azShortenerLib.csproj rename to src/lib/azShortenerLib.csproj index f2ca2a3..7f755a2 100644 --- a/src/azShortenerLib/azShortenerLib.csproj +++ b/src/lib/azShortenerLib.csproj @@ -1,18 +1,18 @@ - - - - net6.0 - enable - enable - Cloud5mins.AzUrlShortener - - - - - - - - - - - + + + + net6.0 + enable + enable + Cloud5mins.AzUrlShortener + + + + + + + + + + + diff --git a/src/swa-cli.config.json b/src/swa-cli.config.json index ced70a1..26c4a47 100644 --- a/src/swa-cli.config.json +++ b/src/swa-cli.config.json @@ -1,14 +1,14 @@ -{ - "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", - "configurations": { - "alpha-tiny-blazor-admin": { - "appLocation": "src/TinyBlazorAdmin", - "apiLocation": "src/AdminApi", - "outputLocation": "bin/wwwroot", - "appBuildCommand": "dotnet publish -c Release -o bin", - "apiBuildCommand": "dotnet publish -c Release", - "run": "dotnet watch run", - "appDevserverUrl": "http://localhost:8000" - } - } +{ + "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", + "configurations": { + "alpha-tiny-blazor-admin": { + "appLocation": "src/TinyBlazorAdmin", + "apiLocation": "src/AdminApi", + "outputLocation": "bin/wwwroot", + "appBuildCommand": "dotnet publish -c Release -o bin", + "apiBuildCommand": "dotnet publish -c Release", + "run": "dotnet watch run", + "appDevserverUrl": "http://localhost:8000" + } + } } \ No newline at end of file