Skip to content

Commit

Permalink
Add sign-in sample
Browse files Browse the repository at this point in the history
  • Loading branch information
chenkennt committed May 1, 2018
1 parent 2a55ee2 commit b64ca38
Show file tree
Hide file tree
Showing 15 changed files with 494 additions and 0 deletions.
Binary file added docs/images/signin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions samples/RealtimeSignIn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Realtime Sign-in Example using Azure SignalR Service

[Not working after latest SignalR update, still working on a fix]

This sample application shows how to build a realtime application using Azure SignalR Service and serverless architecture. When you open homepage of the application, you will see how many people has visited this page (and their OS and browser distribution) and the page will auto update when others open the same page.

## How Does It Work

The application is built on top of Azure SignalR Service, Functions and Storage. There is no web server needed in this sample.

Here is a diagram that illustrates the structure of this appliaction:

![architecture](../../docs/images/signin.png)

1. When user opens the homepage, a HTTP call will be made to an API exposed by Azure Function HTTP trigger, which will record your information and save it to Azure table storage.
2. This API also returns a url and token for browser to connect to Azure SignalR Service.
3. Then the API calculate statistics information (number of visits, OS and browser distribution) and use Azure SignalR Service to broadcast to all clients so browser can do a realtime update without need to do a refresh.
4. The static content (homepage, scripts) are stored in Azure blob storage and exposed to user through Azure Function proxy.

## How to Deploy to Azure

TO BE ADDED
198 changes: 198 additions & 0 deletions samples/RealtimeSignIn/content/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<!doctype html>
<html lang="en">

<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4"
crossorigin="anonymous">

<title>Azure SignalR Sign-in Sample</title>
</head>

<body>
<div id="main" class="container collapse">
<div class="row">
<div class="col-12">
<h1 class="text-center"><span id="count"></span> people have visited this page!</h1>
</div>
</div>
<div class="row">
<div class="col-12 col-lg-6">
<canvas id="chartByOS" width="400" height="400"></canvas>
</div>
<div class="col-12 col-lg-6">
<canvas id="chartByBrowser" width="400" height="400"></canvas>
</div>
</div>
<div class="row justify-content-center">
<div class="col-2"><div id="qrcode" class="d-none d-lg-block"></div></div>
</div>
</div>

<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@0.3.0"></script>
<script src="scripts/signalr.min.js"></script>
<script src="scripts/qrcode.min.js"></script>
<script>
var chartByOS, chartByBrowser;

function prepareData(data) {
var list = [];
for (var label in data) list.push([label, data[label]]);
list.sort((x, y) => x[0] > y[0] ? 1 : x[0] == y[0] ? 0 : -1);
return {
labels: list.map(i => i[0]),
values: list.map(i => i[1])
};
}

function createChart(element, data, title) {
var backgroundColors = [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
];
var borderColors = [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
];
var sorted = prepareData(data);
var ctx = element.getContext('2d');
return new Chart(ctx, {
type: 'bar',
data: {
labels: sorted.labels,
datasets: [{
label: '# of Visitors',
data: sorted.values,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1
}]
},
options: {
plugins: {
datalabels: {
color: 'black',
font: {
size: '20',
weight: 'bold'
},
formatter: Math.round
}
},
title: {
display: true,
text: title
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
}

function updateChart(chart, data) {
var sorted = prepareData(data);
chart.data.labels = sorted.labels;
chart.data.datasets[0].data = sorted.values;
chart.update();
}

function signIn(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send();
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response || xhr.responseText));
}
else {
reject(new Error(xhr.statusText));
}
};

xhr.onerror = () => {
reject(new Error(xhr.statusText));
}
});
}

function initPage(stat) {
$('#count').text(stat.totalNumber);
chartByOS = createChart(document.getElementById("chartByOS"), stat.byOS, '# of Visitors by OS');
chartByBrowser = createChart(document.getElementById("chartByBrowser"), stat.byBrowser, '# of Visitors by Browser');
$("#main").show();
}

function updatePage(stat) {
$('#count').text(stat.totalNumber);
updateChart(chartByOS, stat.byOS);
updateChart(chartByBrowser, stat.byBrowser);
}

function startConnection(url, accessToken, configureConnection) {
return function start(transport) {
console.log(`Starting connection using ${signalR.TransportType[transport]} transport`);
var connection = new signalR.HubConnection(url, { transport: transport, accessTokenFactory: () => accessToken });
if (configureConnection && typeof configureConnection === 'function') {
configureConnection(connection);
}

return connection.start()
.then(function () {
return connection;
})
.catch(function (error) {
console.log(`Cannot start the connection use ${signalR.TransportType[transport]} transport. ${error.message}`);
if (transport !== signalR.TransportType.LongPolling) {
return start(transport + 1);
}

return Promise.reject(error);
});
}(signalR.TransportType.WebSockets);
}

function bindConnectionMessage(connection) {
connection.on('updateSignInStats', updatePage);
}

new QRCode(document.getElementById("qrcode"), window.location.href);
signIn("https://kenchensigninfun2.azurewebsites.net/signin")
// signIn("/signin")
.then(function (result) {
initPage(result.Stats);
return startConnection(result.AuthInfo.ServiceUrl, result.AuthInfo.AccessToken, bindConnectionMessage);
});
/*.then(onConnected)
.catch(onConnectionError);*/
</script>
</body>

</html>
1 change: 1 addition & 0 deletions samples/RealtimeSignIn/content/scripts/qrcode.min.js

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions samples/RealtimeSignIn/content/scripts/signalr.min.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions samples/RealtimeSignIn/function/NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="azure-signalr-dev" value="https://www.myget.org/F/azure-signalr-dev/api/v3/index.json" />
<add key="aspnetcore-dev" value="https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" />
</packageSources>
</configuration>
31 changes: 31 additions & 0 deletions samples/RealtimeSignIn/function/RealtimeSignIn.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs" Version="2.2.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.0.0-preview-10001" />
<PackageReference Include="UAParser" Version="3.0.0" />
<PackageReference Include="WindowsAzure.Storage" Version="7.2.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="proxies.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="signin\function.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
25 changes: 25 additions & 0 deletions samples/RealtimeSignIn/function/RealtimeSignIn.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2005
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RealtimeSignIn", "RealtimeSignIn.csproj", "{D0547646-AA6D-4759-89C8-1FB966C1A1F7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3C3EC256-4B06-41F4-88B3-90EE0C1B7086}
EndGlobalSection
EndGlobal
95 changes: 95 additions & 0 deletions samples/RealtimeSignIn/function/SignInFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using UAParser;

namespace RealtimeSignIn
{
public class SignInInfo : TableEntity
{
public string OS { get; set; }
public string Browser { get; set; }

public SignInInfo(string os, string browser)
{
PartitionKey = "SignIn";
RowKey = Guid.NewGuid().ToString();
OS = os;
Browser = browser;
}

public SignInInfo()
{
}
}

class SignInStats
{
public int totalNumber;
public Dictionary<string, int> byOS = new Dictionary<string, int>();
public Dictionary<string, int> byBrowser = new Dictionary<string, int>();
}

class SignInResult
{
public AuthInfo AuthInfo;
public SignInStats Stats;
}

public static class SignInFunction
{
[FunctionName("signin")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req, TraceWriter log)
{
var ua = Parser.GetDefault().Parse(req.Headers.UserAgent.ToString());
var os = ua.OS.Family;
var browser = ua.UserAgent.Family;

/*
var os = req.GetQueryNameValuePairs().FirstOrDefault(q => q.Key == "os").Value;
var browser = req.GetQueryNameValuePairs().FirstOrDefault(q => q.Key == "browser").Value;
dynamic data = await req.Content.ReadAsAsync<object>();
os = os ?? data?.os;
browser = browser ?? data?.browser;
if (os == null) return req.CreateErrorResponse(HttpStatusCode.BadRequest, "Missing os");
if (browser == null) return req.CreateErrorResponse(HttpStatusCode.BadRequest, "Missing browser");*/

var account = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("TableConnectionString"));
var client = account.CreateCloudTableClient();
var table = client.GetTableReference("SignInInfo");
var newInfo = new SignInInfo(os, browser);
var insert = TableOperation.Insert(newInfo);
table.Execute(insert);

var query = new TableQuery<SignInInfo>();
var stats = new SignInStats();
foreach (var info in table.ExecuteQuery(query))
{
stats.totalNumber++;
if (!stats.byBrowser.ContainsKey(info.Browser)) stats.byBrowser[info.Browser] = 0;
if (!stats.byOS.ContainsKey(info.OS)) stats.byOS[info.OS] = 0;
stats.byBrowser[info.Browser]++;
stats.byOS[info.OS]++;
}

var result = new SignInResult()
{
AuthInfo = SignInHub.GetAuthInfo(),
Stats = stats
};

await SignInHub.UpdateSignInStats(stats);

return req.CreateResponse(HttpStatusCode.OK, result, "application/json");
}
}
}
Loading

0 comments on commit b64ca38

Please sign in to comment.