# load python
library(reticulate)
use_python('C:/Users/Andrew/Anaconda3/')
use_condaenv(condaenv='my_ml', required=TRUE)
library(knitr)

Introduction

Azure Static Web Apps is a serverless hosting service that offers streamlined full-stack development from source code to global high availability. In this guide, we will build a static web app, secure it to a limited audience, and setup GitHub actions that build our posts automatically for us.

Pelican

To begin, we will use Pelican, a static site generator that requires no database or server-side logic.

To use Pelican, we will do the following:

Create a Pelican Environment

# create env
conda create -n pelican python=3.6

# activate
conda activate pelican

# install pelican
python -m pip install "pelican[markdown]"

Build a New Website

# build a new website
pelican-quickstart

# Where do you want to create your new web site? [.] 
.
# What will be the title of this web site?
Azure Static Web Apps

# Who will be the author of this web site?
Andrew Fogarty

# What will be the default language of this web site? [English]
En
# Do you want to specify a URL prefix? e.g., https://example.com   (Y/n) 
n
# Do you want to enable article pagination? (Y/n)
y
# How many articles per page do you want? [10]
5
# What is your time zone? [Europe/Paris]
America/New_York
# Do you want to generate a tasks.py/Makefile to automate generation and publishing? (Y/n)
Y
# Do you want to upload your website using FTP? (y/N)
n
# Do you want to upload your website using SSH? (y/N)
N
# Do you want to upload your website using Dropbox? (y/N)
N
# Do you want to upload your website using S3? (y/N)
N
# Do you want to upload your website using Rackspace Cloud Files? (y/N)
N
# Do you want to upload your website using GitHub Pages? (y/N)
N
# download cleanblog theme:
https://github.com/gilsondev/pelican-clean-blog/archive/refs/heads/master.zip
# install cleanblog
pelican-themes --install <path-to-cleanblog-here> --verbose

Generate a Blog Post

# generate a markdown file in website_folder/content
%%writefile /content/post1.md
Title: My super title
Date: 2010-12-03 10:20
Modified: 2010-12-05 19:30
Category: Python
Tags: pelican, publishing
Slug: my-super-post
Authors: Andrew Fogarty
Summary: Short version for index and feeds

This is the content of my super blog post.

Set the Theme

# add to pelicanconf.py
THEME='PATH/TO/THEME'
THEME_STATIC_DIR = 'theme'

View the Blog Post

# run pelican and test live edits
pelican --autoreload --listen

Azure Static Web App Security

To prevent your website from being accessed by unauthorized individuals, a staticwebapp.config.json file is required. Inside our Pelcian website, we need to place staticwebapp.config.json inside the output folder. The JSON below will rely on Azure Active Directory and check whether or not the user is allowed to be there were allowed users are assigned a specialRoles role within Azure.

{
    "routes": [
        {
            "route": "/login",
            "redirect": "/.auth/login/aad"
        },
        {
            "route": "/logout",
            "redirect": "/.auth/logout"
        },
        {
            "route": "/*",
            "allowedRoles": ["specialRoles"]
        }
    ],
    "responseOverrides": {
        "401": {
          "statusCode": 302,
          "redirect": "/login"
        }
    }
}

Generate Azure Static Web App

When building the Azure Static Web App, ensure that you select:

Push the Website to GitHub

Now we can follow the usual GitHub procedures and commit our new website to our repository. Azure Static Web App will generate a GitHub action yaml file that will automatically push our website to the Azure service.

Establish Security

Once the website is deployed, proceed to Role Management within the Static Web App and invite the select users to your website. Ensure that they are given the role specialRoles.

Automate GitHub Building

As we update our markdown files, we can automate the building of the HTML files by a clever use of GitHub action scripts that might look like:

name: Run Script

on:
  push:
  pull_request:
  schedule:  # run this every day
    - cron: '0 7 * * *'  # 0700 UTC daily; roughly 0300 EDT

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: '3.6' # Version range or exact version of a Python version to use, using SemVer's version range syntax
          architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified

      - name: Run a pull
        run: |
          git pull
          
      - name: Ensure a commit
        run: |
          touch " " >> ghost.txt
          
      - name: Install python dependencies
        run: |
          # install pip
          python -m pip install --upgrade pip
          # install requirements.txt
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
          
      - name: Install pelican
        run: |
          # download pelican theme
          curl -LO https://github.com/gilsondev/pelican-clean-blog/archive/refs/heads/master.zip
          # install unzip
          sudo apt-get install unzip
          # unzip theme -- choose All after to overwrite
          unzip master.zip<<<A
          # install theme
          pelican-themes --install pelican-clean-blog-master --verbose
          
      - name: Alter Markdown
        run: |
          # update the date every day
          # get the date to replace
          str3=$(sed -n 2p content/post1.md)  # copy the second line
          # get the current date to inject
          str4=$(date "+%B %_d, %Y")
          # search and replace date
          sed -i -e "s|$str3|Date: $str4|g" content/post1.md
          
      - name: Build pelican website
        run: |
          # generate new html
          pelican content -s pelicanconf.py
          
      - name: Commit files to git
        run: |
          git config user.email user.email.here
          git config user.name user.name.here
          git add .
          git commit -m 'update markdown pipeline'
          git push -u origin main
          
  build_and_deploy_job:
    # paste Azure Static Web App generated yaml file here

Switch Security to Azure Active Directory - Microsoft Tenant

In the event we want a more general security whereby all Microsoft employees can access our website, we should use the following staticwebapp.config.json settings below:

{
    "routes": [
        {
            "route": "/login",
            "redirect": "/.auth/login/aad"
        },
        {
            "route": "/logout",
            "redirect": "/.auth/logout"
        },
        {
            "route": "/*",
            "allowedRoles": ["specialRoles", "authenticated"]
        }
    ],
    "responseOverrides": {
        "401": {
          "statusCode": 302,
          "redirect": "/login"
        }
    },
    "auth": {
        "identityProviders": {
            "azureActiveDirectory": {
                "registration": {
                    "openIdIssuer": "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0",
                    "clientIdSettingName": "web_id",
                    "clientSecretSettingName": "web_key"
                }
            }
        }
    }
  }

Note above that we specify two things, in addition to our tenant: (1) a clientIdSettingName, and (2) a clientSecretSettingName.

While still within our Azure Active Directory App Registration, select Authentication on the left menu. Select Add a platform, select Web and set this value for the Redirect URI: https://website.azurestaticapps.net/.auth/login/aad/callback. Note the suffix we added to the website. Next, set the Front-channel logout URL as: https://website.azurestaticapps.net/logout. Lastly, check the ID tokens check-box below.

With one more step to go, head over to the Azure static web app service and select Configuration on the left menu. Here, add two items: (1) web_id and (2) web_key (to match the staticwebapp.config.json setting). The values for each should be the App Registration client ID and the App Registration client secret respectively.

Establish Application Insights

We also might be interested in collecting metrics related to our website use. We can get Application Insights telemetry by pasting the following code snippet into each HTML page we want to monitor. Note the code below which asks that we paste in our InstrumentationKey. We get this value from creating an Application Insights service – it is available on the front overview page.

<script type="text/javascript">
!function(T,l,y){var S=T.location,k="script",D="instrumentationKey",C="ingestionendpoint",I="disableExceptionTracking",E="ai.device.",b="toLowerCase",w="crossOrigin",N="POST",e="appInsightsSDK",t=y.name||"appInsights";(y.name||T[e])&&(T[e]=t);var n=T[t]||function(d){var g=!1,f=!1,m={initialize:!0,queue:[],sv:"5",version:2,config:d};function v(e,t){var n={},a="Browser";return n[E+"id"]=a[b](),n[E+"type"]=a,n["ai.operation.name"]=S&&S.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(m.sv||m.version),{time:function(){var e=new Date;function t(e){var t=""+e;return 1===t.length&&(t="0"+t),t}return e.getUTCFullYear()+"-"+t(1+e.getUTCMonth())+"-"+t(e.getUTCDate())+"T"+t(e.getUTCHours())+":"+t(e.getUTCMinutes())+":"+t(e.getUTCSeconds())+"."+((e.getUTCMilliseconds()/1e3).toFixed(3)+"").slice(2,5)+"Z"}(),iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}}}}var h=d.url||y.src;if(h){function a(e){var t,n,a,i,r,o,s,c,u,p,l;g=!0,m.queue=[],f||(f=!0,t=h,s=function(){var e={},t=d.connectionString;if(t)for(var n=t.split(";"),a=0;a<n.length;a++){var i=n[a].split("=");2===i.length&&(e[i[0][b]()]=i[1])}if(!e[C]){var r=e.endpointsuffix,o=r?e.location:null;e[C]="https://"+(o?o+".":"")+"dc."+(r||"services.visualstudio.com")}return e}(),c=s[D]||d[D]||"",u=s[C],p=u?u+"/v2/track":d.endpointUrl,(l=[]).push((n="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",a=t,i=p,(o=(r=v(c,"Exception")).data).baseType="ExceptionData",o.baseData.exceptions=[{typeName:"SDKLoadFailed",message:n.replace(/\./g,"-"),hasFullStack:!1,stack:n+"\nSnippet failed to load ["+a+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(S&&S.pathname||"_unknown_")+"\nEndpoint: "+i,parsedStack:[]}],r)),l.push(function(e,t,n,a){var i=v(c,"Message"),r=i.data;r.baseType="MessageData";var o=r.baseData;return o.message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+n+")").replace(/\"/g,"")+'"',o.properties={endpoint:a},i}(0,0,t,p)),function(e,t){if(JSON){var n=T.fetch;if(n&&!y.useXhr)n(t,{method:N,body:JSON.stringify(e),mode:"cors"});else if(XMLHttpRequest){var a=new XMLHttpRequest;a.open(N,t),a.setRequestHeader("Content-type","application/json"),a.send(JSON.stringify(e))}}}(l,p))}function i(e,t){f||setTimeout(function(){!t&&m.core||a()},500)}var e=function(){var n=l.createElement(k);n.src=h;var e=y[w];return!e&&""!==e||"undefined"==n[w]||(n[w]=e),n.onload=i,n.onerror=a,n.onreadystatechange=function(e,t){"loaded"!==n.readyState&&"complete"!==n.readyState||i(0,t)},n}();y.ld<0?l.getElementsByTagName("head")[0].appendChild(e):setTimeout(function(){l.getElementsByTagName(k)[0].parentNode.appendChild(e)},y.ld||0)}try{m.cookie=l.cookie}catch(p){}function t(e){for(;e.length;)!function(t){m[t]=function(){var e=arguments;g||m.queue.push(function(){m[t].apply(m,e)})}}(e.pop())}var n="track",r="TrackPage",o="TrackEvent";t([n+"Event",n+"PageView",n+"Exception",n+"Trace",n+"DependencyData",n+"Metric",n+"PageViewPerformance","start"+r,"stop"+r,"start"+o,"stop"+o,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),m.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4};var s=(d.extensionConfig||{}).ApplicationInsightsAnalytics||{};if(!0!==d[I]&&!0!==s[I]){var c="onerror";t(["_"+c]);var u=T[c];T[c]=function(e,t,n,a,i){var r=u&&u(e,t,n,a,i);return!0!==r&&m["_"+c]({message:e,url:t,lineNumber:n,columnNumber:a,error:i}),r},d.autoExceptionInstrumented=!0}return m}(y.cfg);function a(){y.onInit&&y.onInit(n)}(T[t]=n).queue&&0===n.queue.length?(n.queue.push(a),n.trackPageView({})):a()}(window,document,{
src: "https://js.monitor.azure.com/scripts/b/ai.2.min.js", // The SDK URL Source
// name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
// ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout,
// useXhr: 1, // Use XHR instead of fetch to report failures (if available),
crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
// onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called)
cfg: { // Application Insights Configuration
    instrumentationKey: "INSTRUMENTATION_KEY"
}});
</script>

We can now use Application Insights to monitor our static web app.

Tracking Authenticated User Access

We can go a step further and log authenticated user access with our App Insights by adding the following JavaScript to our snippet above. In the code below, we use JavaScript to parse the JSON of our security information found in the URL extension .auth/me and save it into a few variables. We then send those variables into this line of code, setAuthenticatedUserContext(authenticatedUserId: string, accountId?: string, storeInCookie = false) as follows: appInsights.setAuthenticatedUserContext(jsondata, jsondata2, true)

... //truncated code snippet 

    instrumentationKey: "INSTRUMENTATION_KEY"
}});

  async function jsonparse1() {
              let response = await fetch('https://static_web_App_url/.auth/me')
              let users = await response.json()
              var jsondata = users.clientPrincipal.userDetails
              var jsondata2 = users.clientPrincipal.userId
              console.log(users.clientPrincipal.userDetails)
              //document.querySelector('#debug').innerText = jsondata
              appInsights.setAuthenticatedUserContext(jsondata, jsondata2, true);
      }
  jsonparse1()

//appInsights.setAuthenticatedUserContext(authUserId, accountId, true);