Merge pull request #1633 from cesanta/webui-login

Added web UI login example
This commit is contained in:
Sergey Lyubka 2022-07-18 21:23:41 +01:00 committed by GitHub
commit 5cddb8ecda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 245 additions and 0 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body></body>
<script type="module" src="main.js"></script>
</html>

View File

@ -0,0 +1,98 @@
'use strict';
import { h, html, render, useEffect, useState } from './preact.min.js';
const Nav = props => html`
<div style="background: #333; padding: 0.5em; color: #fff;">
<div class="container d-flex">
<div style="flex: 1 1 auto; display: flex; align-items: center;">
<b>Your Product</b>
</div>
<div style="display: flex; align-items: center; flex: 0 0 auto; ">
<span>Logged in as:</span>
<span style="padding: 0 0.5em;"><img src="user.png" height="22" /></span>
<span>${props.user}</span>
<a class="btn" onclick=${props.logout}
style="margin-left: 1em; font-size: 0.8em; background: #8aa;">logout</a>
</div>
</div>
</div>`;
const Main = props => html`
<div class="row">
<H1>${props.data.text}</H1>
</div>`;
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`
<div class="rounded border" style="max-width: 480px; margin: 0 auto; margin-top: 5em; background: #eee; ">
<div style="padding: 2em; ">
<h1 style="color: #666;">Your Product Login </h1>
<div style="margin: 0.5em 0;">
<input type='text' placeholder='Name' style="width: 100%;"
oninput=${ev => setUser(ev.target.value)} value=${user} />
</div>
<div style="margin: 0.5em 0;">
<input type="password" placeholder="Password" style="width: 100%;"
oninput=${ev => setPass(ev.target.value)} value=${pass}
onchange=${login} />
</div>
<div style="margin: 1em 0;">
<button class="btn" style="width: 100%; background: #8aa;"
disabled=${!user || !pass} onclick=${login}> Login </button>
</div>
<div style="color: #777; margin-top: 2em;">
Valid logins: admin:pass0, user1:pass1, user2:pass2
</div>
</div>
</div>`;
};
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);

File diff suppressed because one or more lines are too long

View File

@ -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%; } }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB