Skip to content

Commit be12cef

Browse files
authored
Update tutorial-build-chat.md
1 parent 69b29ec commit be12cef

File tree

1 file changed

+224
-1
lines changed

1 file changed

+224
-1
lines changed

articles/azure-web-pubsub/tutorial-build-chat.md

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ In this section, we use Azure CLI to set the event handlers and use [awps-tunnel
823823
824824
We set the URL template to use `tunnel` scheme so that Web PubSub routes messages through the `awps-tunnel`'s tunnel connection. Event handlers can be set from either the portal or the CLI as [described in this article](howto-develop-eventhandler.md#configure-event-handler), here we set it through CLI. Since we listen events in path `/eventhandler` as the previous step sets, we set the url template to `tunnel:///eventhandler`.
825825
826-
Use the Azure CLI [az webpubsub hub create](/cli/azure/webpubsub/hub#az-webpubsub-hub-update) command to create the event handler settings for the chat hub.
826+
Use the Azure CLI [az webpubsub hub create](/cli/azure/webpubsub/hub#az-webpubsub-hub-create) command to create the event handler settings for the `Sample_ChatApp` hub.
827827
828828
> [!Important]
829829
> Replace <your-unique-resource-name> with the name of your Web PubSub resource created from the previous steps.
@@ -884,6 +884,229 @@ Open `http://localhost:8080/index.html`. You can input your user name and start
884884
885885
<!-- Adding Lazy Auth part with `connect` handling -->
886886
887+
## Lazy Auth with `connect` event handler
888+
889+
In previous sections, we demonstrate how to use [negotiate](#add-negotiate-endpoint) endpoint to return the Web PubSub service URL and the JWT access token for the clients to connect to Web PubSub service. In some cases, for example, edge devices that have limited resources, clients might prefer direct connect to Web PubSub resources. In such cases, you can configure `connect` event handler to lazy auth the clients, assign user ID to the clients, specify the groups the clients join once they connect, configure the permissions the clients have and WebSocket subprotocol as the WebSocket response to the client, etc. Details please refer to [connect event handler spec](./reference-client-events.md#connect). Now let's use `connect` event handler to acheive the similar as what the [negotiate](#negotiate) section does.
890+
891+
### Update hub settings
892+
893+
First let's update hub settings to also include `connect` event handler, we need to also allow anonymous connect so that clients without JWT access token can connect to the service.
894+
895+
Use the Azure CLI [az webpubsub hub update](/cli/azure/webpubsub/hub#az-webpubsub-hub-update) command to create the event handler settings for the `Sample_ChatApp` hub.
896+
897+
> [!Important]
898+
> Replace &lt;your-unique-resource-name&gt; with the name of your Web PubSub resource created from the previous steps.
899+
900+
```azurecli-interactive
901+
az webpubsub hub update -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --allow-anonymous true --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected" system-event="connect"
902+
```
903+
904+
### Update upstream logic to handle connect event
905+
906+
Now let's update upstream logic to handle connect event. We could also remove the negotiate endpoint now. As similar to what we do in negotiate endpoint as demo purpose, we also read id from the query parameters. In connect event, the original client query is preserved in connect event requet body.
907+
908+
# [C#](#tab/csharp)
909+
910+
Inside the class `Sample_ChatApp`, override `OnConnectAsync()` to handle `connect` event:
911+
912+
```csharp
913+
sealed class Sample_ChatApp : WebPubSubHub
914+
{
915+
private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;
916+
917+
public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
918+
{
919+
_serviceClient = serviceClient;
920+
}
921+
922+
public override ValueTask<ConnectEventResponse> OnConnectAsync(ConnectEventRequest request, CancellationToken cancellationToken)
923+
{
924+
if (request.Query.TryGetValue("id", out var id))
925+
{
926+
return new ValueTask<ConnectEventResponse>(request.CreateResponse(userId: id.FirstOrDefault(), null, null, null));
927+
}
928+
929+
// The SDK catches this exception and returns 401 to the caller
930+
throw new UnauthorizedAccessException("Request missing id");
931+
}
932+
933+
public override async Task OnConnectedAsync(ConnectedEventRequest request)
934+
{
935+
Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
936+
}
937+
938+
public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
939+
{
940+
await _serviceClient.SendToAllAsync(RequestContent.Create(
941+
new
942+
{
943+
from = request.ConnectionContext.UserId,
944+
message = request.Data.ToString()
945+
}),
946+
ContentType.ApplicationJson);
947+
948+
return new UserEventResponse();
949+
}
950+
}
951+
```
952+
953+
# [JavaScript](#tab/javascript)
954+
955+
Update server.js to handle the client connect event:
956+
957+
```javascript
958+
const express = require("express");
959+
const { WebPubSubServiceClient } = require("@azure/web-pubsub");
960+
const { WebPubSubEventHandler } = require("@azure/web-pubsub-express");
961+
962+
const app = express();
963+
const hubName = "Sample_ChatApp";
964+
965+
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
966+
967+
let handler = new WebPubSubEventHandler(hubName, {
968+
path: "/eventhandler",
969+
handleConnect: async (req, res) => {
970+
if (req.context.query.id){
971+
res.success({ userId: req.context.query.id });
972+
} else {
973+
res.fail(401, "missing user id");
974+
}
975+
},
976+
onConnected: async (req) => {
977+
console.log(`${req.context.userId} connected`);
978+
},
979+
handleUserEvent: async (req, res) => {
980+
if (req.context.eventName === "message")
981+
await serviceClient.sendToAll({
982+
from: req.context.userId,
983+
message: req.data,
984+
});
985+
res.success();
986+
},
987+
});
988+
app.use(express.static("public"));
989+
app.use(handler.getMiddleware());
990+
991+
app.listen(8080, () => console.log("server started"));
992+
```
993+
994+
# [Java](#tab/java)
995+
Now let's add the logic to handle the connect event `azure.webpubsub.sys.connect`:
996+
```java
997+
// validation: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#validation
998+
app.options("/eventhandler", ctx -> {
999+
ctx.header("WebHook-Allowed-Origin", "*");
1000+
});
1001+
1002+
// handle events: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#events
1003+
app.post("/eventhandler", ctx -> {
1004+
String event = ctx.header("ce-type");
1005+
if ("azure.webpubsub.sys.connect".equals(event)) {
1006+
System.out.println("Connecting.");
1007+
if (){
1008+
ctx.status(200);
1009+
1010+
} else {
1011+
ctx.status(401).text("missing user id");
1012+
1013+
}
1014+
String id = ctx.header("ce-userId");
1015+
} else if ("azure.webpubsub.sys.connected".equals(event)) {
1016+
String id = ctx.header("ce-userId");
1017+
System.out.println(id + " connected.");
1018+
ctx.status(200);
1019+
} else if ("azure.webpubsub.user.message".equals(event)) {
1020+
String id = ctx.header("ce-userId");
1021+
String message = ctx.body();
1022+
service.sendToAll(String.format("{\"from\":\"%s\",\"message\":\"%s\"}", id, message), WebPubSubContentType.APPLICATION_JSON);
1023+
ctx.status(200);
1024+
}
1025+
});
1026+
1027+
```
1028+
1029+
# [Python](#tab/python)
1030+
Now let's handle the system `connect` event, which should contain the header `ce-type` as `azure.webpubsub.sys.connect`. We add the logic after abuse protection:
1031+
1032+
```python
1033+
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
1034+
def handle_event():
1035+
if request.method == 'OPTIONS':
1036+
if request.headers.get('WebHook-Request-Origin'):
1037+
res = Response()
1038+
res.headers['WebHook-Allowed-Origin'] = '*'
1039+
res.status_code = 200
1040+
return res
1041+
elif request.method == 'POST':
1042+
user_id = request.headers.get('ce-userid')
1043+
type = request.headers.get('ce-type')
1044+
if type == 'azure.webpubsub.sys.connect':
1045+
print(f"{user_id} connected")
1046+
return '', 204
1047+
elif type == 'azure.webpubsub.sys.connected':
1048+
print(f"{user_id} connected")
1049+
return '', 204
1050+
elif type == 'azure.webpubsub.user.message':
1051+
# default uses JSON
1052+
service.send_to_all(message={
1053+
'from': user_id,
1054+
'message': request.data.decode('UTF-8')
1055+
})
1056+
# returned message is also received by the client
1057+
return {
1058+
'from': "system",
1059+
'message': "message handled by server"
1060+
}, 200
1061+
else:
1062+
return 'Bad Request', 400
1063+
```
1064+
1065+
---
1066+
1067+
### Update index.html to direct connect
1068+
1069+
Now let's update the web page to direct connect to Web PubSub service. One thing to mention is that now for demo purpose the Web PubSub service endpoint is hard-coded into the client code, please update the service hostname `<the host name of your service>` in the below html with the value from your own service. It might be still useful to fetch the Web PubSub service endpoint value from your server, it gives you more flexibility and controllability to where the client connects to.
1070+
1071+
```html
1072+
<html>
1073+
<body>
1074+
<h1>Azure Web PubSub Chat</h1>
1075+
<input id="message" placeholder="Type to chat...">
1076+
<div id="messages"></div>
1077+
<script>
1078+
(async function () {
1079+
let hostname = "<the host name of your service>";
1080+
let id = prompt('Please input your user name');
1081+
let ws = new WebSocket(`wss://${hostname}/client/hubs/Sample_ChatApp?id=${id}`);
1082+
ws.onopen = () => console.log('connected');
1083+
1084+
let messages = document.querySelector('#messages');
1085+
1086+
ws.onmessage = event => {
1087+
let m = document.createElement('p');
1088+
let data = JSON.parse(event.data);
1089+
m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
1090+
messages.appendChild(m);
1091+
};
1092+
1093+
let message = document.querySelector('#message');
1094+
message.addEventListener('keypress', e => {
1095+
if (e.charCode !== 13) return;
1096+
ws.send(message.value);
1097+
message.value = '';
1098+
});
1099+
})();
1100+
</script>
1101+
</body>
1102+
1103+
</html>
1104+
```
1105+
1106+
### Rerun the server
1107+
1108+
Now [rerun the server](#run-the-web-server) and visit the web page following the instructions before. If you've stopped `awps-tunnel`, please also rerun the tunnel tool.
1109+
8871110
## Next steps
8881111
8891112
This tutorial provides you with a basic idea of how the event system works in Azure Web PubSub service.

0 commit comments

Comments
 (0)