diff --git a/examples/webui-login/Makefile b/examples/webui-login/Makefile
new file mode 100644
index 00000000..9addcb99
--- /dev/null
+++ b/examples/webui-login/Makefile
@@ -0,0 +1,20 @@
+PROG ?= example
+SSL = ?
+
+ifeq "$(SSL)" "MBEDTLS"
+CFLAGS += -DMG_ENABLE_MBEDTLS=1 -lmbedtls -lmbedcrypto -lmbedx509
+endif
+
+ifeq "$(SSL)" "OPENSSL"
+CFLAGS += -DMG_ENABLE_OPENSSL=1 -lssl -lcrypto
+endif
+
+all: $(PROG)
+ $(DEBUGGER) ./$(PROG) $(ARGS)
+
+
+$(PROG): main.c
+ $(CC) ../../mongoose.c -I../.. -W -Wall $(CFLAGS) $(EXTRA_CFLAGS) -o $(PROG) main.c
+
+clean:
+ rm -rf $(PROG) *.o *.dSYM *.gcov *.gcno *.gcda *.obj *.exe *.ilk *.pdb
diff --git a/examples/webui-login/main.c b/examples/webui-login/main.c
new file mode 100644
index 00000000..63ac9207
--- /dev/null
+++ b/examples/webui-login/main.c
@@ -0,0 +1,75 @@
+// Copyright (c) 2022 Cesanta Software Limited
+// All rights reserved
+
+#include "mongoose.h"
+
+const char *s_listening_url = "http://0.0.0.0:8000";
+
+// Authenticated user.
+// A user can be authenticated by:
+// - a name:pass pair
+// - a token
+// When a user is shown a login screen, they enter a user:pass. If successful,
+// the server returns the user info, which includes the token. From that point
+// on, the client can use the token for authentication. Tokens could be
+// refreshed/changed at server side, forcing clients to re-login.
+struct user {
+ const char *name, *pass, *token;
+};
+
+// Parse HTTP requests, return authenticated user or NULL
+static struct user *getuser(struct mg_http_message *hm) {
+ // In production, make passwords strong and tokens randomly generated
+ // In this example, user list is kept in RAM. In production, it can
+ // be backed by file, database, or some other method.
+ static struct user users[] = {
+ {"admin", "pass0", "admin_token"},
+ {"user1", "pass1", "user1_token"},
+ {"user2", "pass2", "user2_token"},
+ {NULL, NULL, NULL},
+ };
+ char user[256], pass[256];
+ struct user *u;
+ mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
+ if (user[0] != '\0' && pass[0] != '\0') {
+ // Both user and password are set, search by user/password
+ for (u = users; u->name != NULL; u++)
+ if (strcmp(user, u->name) == 0 && strcmp(pass, u->pass) == 0) return u;
+ } else if (user[0] == '\0') {
+ // Only password is set, search by token
+ for (u = users; u->name != NULL; u++)
+ if (strcmp(pass, u->token) == 0) return u;
+ }
+ return NULL;
+}
+
+void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
+ if (ev == MG_EV_HTTP_MSG) {
+ struct mg_http_message *hm = (struct mg_http_message *) ev_data;
+ struct user *u = getuser(hm);
+ if (u == NULL && mg_http_match_uri(hm, "/api/#")) {
+ // All URIs starting with /api/ must be authenticated
+ mg_printf(c, "%s", "HTTP/1.1 403 Denied\r\nContent-Length: 0\r\n\r\n");
+ } else if (mg_http_match_uri(hm, "/api/data")) {
+ mg_http_reply(c, 200, "Content-Type: application/json\r\n",
+ "{%Q:%Q,%Q:%Q}\n", "text", "Hello!", "data", "somedata");
+ } else if (mg_http_match_uri(hm, "/api/login")) {
+ mg_http_reply(c, 200, "Content-Type: application/json\r\n",
+ "{%Q:%Q,%Q:%Q}\n", "user", u->name, "token", u->token);
+ } else {
+ struct mg_http_serve_opts opts = {.root_dir = "web_root"};
+ mg_http_serve_dir(c, ev_data, &opts);
+ }
+ }
+ (void) fn_data;
+}
+
+int main(void) {
+ struct mg_mgr mgr;
+ mg_log_set("2"); // Set to 3 for debug, to 4 for very verbose level
+ mg_mgr_init(&mgr);
+ mg_http_listen(&mgr, s_listening_url, fn, &mgr);
+ while (mgr.conns != NULL) mg_mgr_poll(&mgr, 500);
+ mg_mgr_free(&mgr);
+ return 0;
+}
diff --git a/examples/webui-login/web_root/index.html b/examples/webui-login/web_root/index.html
new file mode 100644
index 00000000..1ac23a2b
--- /dev/null
+++ b/examples/webui-login/web_root/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/examples/webui-login/web_root/main.js b/examples/webui-login/web_root/main.js
new file mode 100644
index 00000000..b16376c7
--- /dev/null
+++ b/examples/webui-login/web_root/main.js
@@ -0,0 +1,98 @@
+'use strict';
+import { h, html, render, useEffect, useState } from './preact.min.js';
+
+const Nav = props => html`
+
+
+
+ Your Product
+
+
+
Logged in as:
+

+
${props.user}
+
logout
+
+
+
`;
+
+const Main = props => html`
+
+
${props.data.text}
+`;
+
+const Login = function (props) {
+ const [user, setUser] = useState('');
+ const [pass, setPass] = useState('');
+ const login = () =>
+ fetch(
+ '/api/login',
+ { headers: { Authorization: 'Basic ' + btoa(user + ':' + pass) } })
+ .then(r => r.json())
+ .then(r => r && props.login(r))
+ .catch(err => err);
+ return html`
+
+
+
Your Product Login
+
+ setUser(ev.target.value)} value=${user} />
+
+
+ setPass(ev.target.value)} value=${pass}
+ onchange=${login} />
+
+
+
+
+
+ Valid logins: admin:pass0, user1:pass1, user2:pass2
+
+
+
`;
+};
+
+
+
+const App = function () {
+ const [user, setUser] = useState('');
+ const [data, setData] = useState({});
+
+ const getin = () =>
+ fetch('/api/data', { headers: { Authorization: '' } })
+ .then(r => r.json())
+ .then(r => setData(r))
+ .catch(err => console.log(err));
+
+ const login = function (u) {
+ document.cookie = `access_token=${u.token};path=/;max-age=3600`;
+ setUser(u.user);
+ return getin();
+ };
+
+ const logout = () => {
+ document.cookie = `access_token=;path=/;max-age=0`;
+ setUser('');
+ };
+
+ useEffect(() => {
+ // Called once at init time
+ fetch('/api/login', { headers: { Authorization: '' } })
+ .then(r => r.json())
+ .then(r => login(r))
+ .catch(() => setUser(''));
+ }, []);
+
+ if (!user) return html`<${Login} login=${login} />`;
+
+ return html`
+<${Nav} user=${user} logout=${logout} />
+<${Main} data=${data} />
+`;
+};
+
+window.onload = () => render(h(App), document.body);
diff --git a/examples/webui-login/web_root/preact.min.js b/examples/webui-login/web_root/preact.min.js
new file mode 100644
index 00000000..e24f87b9
--- /dev/null
+++ b/examples/webui-login/web_root/preact.min.js
@@ -0,0 +1 @@
+var e,n,_,t,o,r,u,l={},i=[],c=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function s(e,n){for(var _ in n)e[_]=n[_];return e}function f(e){var n=e.parentNode;n&&n.removeChild(e)}function a(n,_,t){var o,r,u,l={};for(u in _)"key"==u?o=_[u]:"ref"==u?r=_[u]:l[u]=_[u];if(arguments.length>2&&(l.children=arguments.length>3?e.call(arguments,2):t),"function"==typeof n&&null!=n.defaultProps)for(u in n.defaultProps)void 0===l[u]&&(l[u]=n.defaultProps[u]);return p(n,l,o,r,null)}function p(e,t,o,r,u){var l={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==u?++_:u};return null!=n.vnode&&n.vnode(l),l}function h(e){return e.children}function d(e,n){this.props=e,this.context=n}function v(e,n){if(null==n)return e.__?v(e.__,e.__.__k.indexOf(e)+1):null;for(var _;n0?p(m.type,m.props,m.key,null,m.__v):m)){if(m.__=_,m.__b=_.__b+1,null===(y=H[a])||y&&m.key==y.key&&m.type===y.type)H[a]=void 0;else for(d=0;d=t.__.length&&t.__.push({}),t.__[e]}function G(e){return R=1,z(ie,e)}function z(e,n,_){var t=j(L++,2);return t.t=e,t.__c||(t.__=[_?_(n):ie(void 0,n),function(e){var n=t.t(t.__[0],e);t.__[0]!==n&&(t.__=[n,t.__[1]],t.__c.setState({}))}],t.__c=N),t.__}function J(e,_){var t=j(L++,3);!n.__s&&le(t.__H,_)&&(t.__=e,t.__H=_,N.__H.__h.push(t))}function K(e,_){var t=j(L++,4);!n.__s&&le(t.__H,_)&&(t.__=e,t.__H=_,N.__h.push(t))}function Q(e){return R=5,Y(function(){return{current:e}},[])}function X(e,n,_){R=6,K(function(){"function"==typeof e?e(n()):e&&(e.current=n())},null==_?_:_.concat(e))}function Y(e,n){var _=j(L++,7);return le(_.__H,n)&&(_.__=e(),_.__H=n,_.__h=e),_.__}function Z(e,n){return R=8,Y(function(){return e},n)}function ee(e){var n=N.context[e.__c],_=j(L++,9);return _.c=e,n?(null==_.__&&(_.__=!0,n.sub(N)),n.props.value):e.__}function ne(e,_){n.useDebugValue&&n.useDebugValue(_?_(e):e)}function _e(e){var n=j(L++,10),_=G();return n.__=e,N.componentDidCatch||(N.componentDidCatch=function(e){n.__&&n.__(e),_[1](e)}),[_[0],function(){_[1](void 0)}]}function te(){I.forEach(function(e){if(e.__P)try{e.__H.__h.forEach(re),e.__H.__h.forEach(ue),e.__H.__h=[]}catch(_){e.__H.__h=[],n.__e(_,e.__v)}}),I=[]}n.__b=function(e){N=null,O&&O(e)},n.__r=function(e){V&&V(e),L=0;var n=(N=e.__c).__H;n&&(n.__h.forEach(re),n.__h.forEach(ue),n.__h=[])},n.diffed=function(e){q&&q(e);var _=e.__c;_&&_.__H&&_.__H.__h.length&&(1!==I.push(_)&&W===n.requestAnimationFrame||((W=n.requestAnimationFrame)||function(e){var n,_=function(){clearTimeout(t),oe&&cancelAnimationFrame(n),setTimeout(e)},t=setTimeout(_,100);oe&&(n=requestAnimationFrame(_))})(te)),N=void 0},n.__c=function(e,_){_.some(function(e){try{e.__h.forEach(re),e.__h=e.__h.filter(function(e){return!e.__||ue(e)})}catch(t){_.some(function(e){e.__h&&(e.__h=[])}),_=[],n.__e(t,e.__v)}}),B&&B(e,_)},n.unmount=function(e){$&&$(e);var _=e.__c;if(_&&_.__H)try{_.__H.__.forEach(re)}catch(e){n.__e(e,_.__v)}};var oe="function"==typeof requestAnimationFrame;function re(e){var n=N;"function"==typeof e.__c&&e.__c(),N=n}function ue(e){var n=N;e.__c=e.__(),N=n}function le(e,n){return!e||e.length!==n.length||n.some(function(n,_){return n!==e[_]})}function ie(e,n){return"function"==typeof n?n(e):n}var ce=function(e,n,_,t){var o;n[0]=0;for(var r=1;r=5&&((o||!e&&5===t)&&(u.push(t,0,o,_),t=6),e&&(u.push(t,e,0,_),t=6)),o=""},i=0;i"===n?(t=1,o=""):o=n+o[0]:r?n===r?r="":o+=n:'"'===n||"'"===n?r=n:">"===n?(l(),t=1):t&&("="===n?(t=5,_=o,o=""):"/"===n&&(t<5||">"===e[i][c+1])?(l(),3===t&&(u=u[0]),t=u,(u=u[0]).push(2,0,t),t=0):" "===n||"\t"===n||"\n"===n||"\r"===n?(l(),t=2):o+=n),3===t&&"!--"===o&&(t=4,u=u[0])}return l(),u}(e)),n),arguments,[])).length>1?n:n[0]}.bind(a);export{a as h,fe as html,M as render,d as Component,F as createContext,G as useState,z as useReducer,J as useEffect,K as useLayoutEffect,Q as useRef,X as useImperativeHandle,Y as useMemo,Z as useCallback,ee as useContext,ne as useDebugValue,_e as useErrorBoundary};
diff --git a/examples/webui-login/web_root/style.css b/examples/webui-login/web_root/style.css
new file mode 100644
index 00000000..b483f801
--- /dev/null
+++ b/examples/webui-login/web_root/style.css
@@ -0,0 +1,43 @@
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; height: 100%; font: 16px sans-serif; }
+select, input, label::before, textarea { outline: none; box-shadow:none !important; border: 1px solid #ccc !important; }
+code, pre { color: #373; font-family: monospace; font-weight: bolder; font-size: smaller; background: #ddd; padding: 0.1em 0.3em; border-radius: 0.2em; }
+textarea, input, .addon { font-size: 15px; border: 1px solid #ccc; padding: 0.5em; }
+a, a:visited, a:active { color: #55f; }
+.addon { background: #eee; min-width: 9em;}
+.btn {
+ background: #ccc; border-radius: 0.3em; border: 0; color: #fff; cursor: pointer;
+ display: inline-block; padding: 0.6em 2em; font-weight: bolder;
+}
+.btn[disabled] { opacity: 0.5; cursor: auto;}
+.smooth { transition: all .2s; }
+.container { margin: 0 20px; width: auto; }
+.d-flex { display: flex; }
+.d-none { display: none; }
+.border { border: 1px solid #ddd; }
+.rounded { border-radius: 0.5em; }
+.nowrap { white-space: nowrap; }
+.msg { background: #def; border-left: 5px solid #59d; padding: 0.5em; font-size: 90%; margin: 1em 0; }
+.section { margin: 0 1em; }
+.topic, .data, .qos { padding: 0.2em 0.5em; border-radius: 0.4em; margin-right: 0.5em; }
+.qos { background: #efa; }
+.topic { background: #fea; }
+.data { background: #aef; }
+
+/* Grid */
+.row { display: flex; flex-wrap: wrap; }
+.col { margin: 0; padding: 0; overflow: auto; }
+.col-12 { width: 100%; }
+.col-11 { width: 91.66%; }
+.col-10 { width: 83.33%; }
+.col-9 { width: 75%; }
+.col-8 { width: 66.66%; }
+.col-7 { width: 58.33%; }
+.col-6 { width: 50%; }
+.col-5 { width: 41.66%; }
+.col-4 { width: 33.33%; }
+.col-3 { width: 25%; }
+.col-2 { width: 16.66%; }
+.col-1 { width: 8.33%; }
+@media (min-width: 1310px) { .container { margin: auto; width: 1270px; } }
+@media (max-width: 920px) { .row .col { width: 100%; } }
diff --git a/examples/webui-login/web_root/user.png b/examples/webui-login/web_root/user.png
new file mode 100644
index 00000000..0727b82c
Binary files /dev/null and b/examples/webui-login/web_root/user.png differ