forked from chookspace/chookchat
start work on persistent login
This commit is contained in:
1
build.sh
1
build.sh
@@ -6,6 +6,7 @@ bin2cpp --file=client/header_top.html --output=server/src/generated
|
|||||||
bin2cpp --file=client/header_bottom.html --output=server/src/generated
|
bin2cpp --file=client/header_bottom.html --output=server/src/generated
|
||||||
|
|
||||||
bin2cpp --file=client/footer.html --output=server/src/generated
|
bin2cpp --file=client/footer.html --output=server/src/generated
|
||||||
|
bin2cpp --file=client/login.html --output=server/src/generated
|
||||||
bin2cpp --file=client/e404.html --output=server/src/generated
|
bin2cpp --file=client/e404.html --output=server/src/generated
|
||||||
bin2cpp --file=client/style.css --output=server/src/generated
|
bin2cpp --file=client/style.css --output=server/src/generated
|
||||||
bin2cpp --file=client/script.js --output=server/src/generated
|
bin2cpp --file=client/script.js --output=server/src/generated
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">home</a></li>
|
<li><a href="/">home</a></li>
|
||||||
<li><a href="/login">login</a></li>
|
<li><a id="loginLink" href="/login">login</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
33
client/login.html
Normal file
33
client/login.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>login</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="/script.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<div id="brand">
|
||||||
|
<h2><a href="/">chookchat</a></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">home</a></li>
|
||||||
|
<li><a id="loginLink" href="/login">login</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="login" id="posts">
|
||||||
|
<h1>login</h1>
|
||||||
|
<p>if you haven't registered, enter a username that doesn't exist and hit 'register'</p>
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input name="username" id="username"></input>
|
||||||
|
<br>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" name="password" id="password"></input>
|
||||||
|
<br>
|
||||||
|
<button onclick="login()">Log In</button>
|
||||||
|
<button onclick="register()">Register</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
async function like(id) {
|
async function like(id) {
|
||||||
const usernameBox = document.getElementById('username');
|
|
||||||
const passwordBox = document.getElementById('password');
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('username', usernameBox.value);
|
|
||||||
formData.append('password', passwordBox.value);
|
|
||||||
formData.append('post', id);
|
formData.append('post', id);
|
||||||
|
|
||||||
const result = await fetch('/like', {
|
const result = await fetch('/like', {
|
||||||
@@ -12,7 +7,60 @@ async function like(id) {
|
|||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result.status === 401) {
|
||||||
|
alert("you need to login before you like a post");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.status < 200 || result.status >= 400) {
|
if (result.status < 200 || result.status >= 400) {
|
||||||
alert("your post didnt get the like because the server thought your opinion was invalid");
|
alert("your post didnt get the like because the server thought your opinion was invalid");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
const usernameBox = document.getElementById('username');
|
||||||
|
const passwordBox = document.getElementById('password');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('username', usernameBox.value);
|
||||||
|
formData.append('password', passwordBox.value);
|
||||||
|
|
||||||
|
const result = await fetch('/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status < 200 || result.status >= 400) {
|
||||||
|
alert("fard");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = "/";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const usernameBox = document.getElementById('username');
|
||||||
|
const passwordBox = document.getElementById('password');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('username', usernameBox.value);
|
||||||
|
formData.append('password', passwordBox.value);
|
||||||
|
|
||||||
|
console.log(formData);
|
||||||
|
|
||||||
|
const result = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status < 200 || result.status >= 400) {
|
||||||
|
alert("for some reason the server hates you and didn't let you log into your account");
|
||||||
|
console.log(result.status);
|
||||||
|
console.log(result.content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ sources = [
|
|||||||
'src/generated/header_top.cpp',
|
'src/generated/header_top.cpp',
|
||||||
'src/generated/header_bottom.cpp',
|
'src/generated/header_bottom.cpp',
|
||||||
'src/generated/footer.cpp',
|
'src/generated/footer.cpp',
|
||||||
|
'src/generated/login.cpp',
|
||||||
'src/generated/style.cpp',
|
'src/generated/style.cpp',
|
||||||
'src/generated/script.cpp',
|
'src/generated/script.cpp',
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
#include "db.h"
|
#include "db.h"
|
||||||
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
std::string generateSecureToken(size_t numBytes) {
|
||||||
|
std::ifstream urandom("/dev/urandom", std::ios::binary);
|
||||||
|
if (!urandom) {
|
||||||
|
throw std::runtime_error("couldn't open /dev/urandom");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> buf(numBytes);
|
||||||
|
urandom.read(reinterpret_cast<char*>(buf.data()), numBytes);
|
||||||
|
if (!urandom) {
|
||||||
|
throw std::runtime_error("short read from /dev/urandom");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
for (unsigned char b : buf) {
|
||||||
|
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
Database::Database(const std::string& path) {
|
Database::Database(const std::string& path) {
|
||||||
// init db
|
// init db
|
||||||
@@ -25,6 +47,12 @@ Database::Database(const std::string& path) {
|
|||||||
timestamp INTEGER NOT NULL,
|
timestamp INTEGER NOT NULL,
|
||||||
likes INTEGER NOT NULL DEFAULT 0
|
likes INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token TEXT PRIMARY KEY,
|
||||||
|
userid INTEGER NOT NULL,
|
||||||
|
expiry INTEGER NOT NULL
|
||||||
|
);
|
||||||
)";
|
)";
|
||||||
|
|
||||||
char* errmsg = nullptr;
|
char* errmsg = nullptr;
|
||||||
@@ -227,6 +255,61 @@ std::optional<User> Database::getUserByName(const std::string& name) {
|
|||||||
return User(id, name, password);
|
return User(id, name, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> Database::createNewToken(uint64_t id) {
|
||||||
|
std::string token = generateSecureToken(32);
|
||||||
|
int64_t expiry = static_cast<int64_t>(std::time(nullptr)) + 60 * 60 * 24 * 30; // 30 days
|
||||||
|
|
||||||
|
const char* sql = R"(
|
||||||
|
INSERT INTO sessions (token, userid, expiry)
|
||||||
|
VALUES (?, ?, ?);
|
||||||
|
)";
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_int64(stmt, 2, id);
|
||||||
|
sqlite3_bind_int64(stmt, 3, expiry);
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<User> Database::getUserByToken(const std::string& token) {
|
||||||
|
const char* sql = "SELECT userid, expiry FROM sessions WHERE token = ?;";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t userid = sqlite3_column_int64(stmt, 0);
|
||||||
|
int64_t expiry = sqlite3_column_int64(stmt, 1);
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (expiry < static_cast<int64_t>(std::time(nullptr))) {
|
||||||
|
// expired; not bothering to delete it here, an expiry sweep
|
||||||
|
// job would be a reasonable thing to add later
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUser(userid);
|
||||||
|
}
|
||||||
|
|
||||||
void Database::addUser(User& user) {
|
void Database::addUser(User& user) {
|
||||||
const char* sql = R"(
|
const char* sql = R"(
|
||||||
INSERT INTO users (name, password)
|
INSERT INTO users (name, password)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
void sanitize(std::string& str);
|
void sanitize(std::string& str);
|
||||||
|
|
||||||
|
std::string generateSecureToken(size_t numBytes);
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
uint64_t id = 0;
|
uint64_t id = 0;
|
||||||
std::string name = "";
|
std::string name = "";
|
||||||
@@ -41,6 +43,8 @@ class Database {
|
|||||||
|
|
||||||
std::optional<User> getUser(uint64_t id);
|
std::optional<User> getUser(uint64_t id);
|
||||||
std::optional<User> getUserByName(const std::string& name);
|
std::optional<User> getUserByName(const std::string& name);
|
||||||
|
std::optional<User> getUserByToken(const std::string& token);
|
||||||
|
std::optional<std::string> createNewToken(uint64_t id);
|
||||||
|
|
||||||
// this will modify the user to have their user ID
|
// this will modify the user to have their user ID
|
||||||
void addUser(User& user);
|
void addUser(User& user);
|
||||||
|
|||||||
@@ -15,13 +15,35 @@
|
|||||||
#include "generated/header_top.h"
|
#include "generated/header_top.h"
|
||||||
#include "generated/header_bottom.h"
|
#include "generated/header_bottom.h"
|
||||||
#include "generated/footer.h"
|
#include "generated/footer.h"
|
||||||
|
#include "generated/login.h"
|
||||||
#include "generated/style.h"
|
#include "generated/style.h"
|
||||||
#include "generated/script.h"
|
#include "generated/script.h"
|
||||||
|
|
||||||
|
std::optional<User> getLoggedInUser(const httplib::Request& request, Database& database) {
|
||||||
|
if (!request.has_header("Cookie")) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cookieHeader = request.get_header_value("Cookie");
|
||||||
|
const std::string key = "session=";
|
||||||
|
|
||||||
|
size_t pos = cookieHeader.find(key);
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
pos += key.length();
|
||||||
|
|
||||||
|
size_t end = cookieHeader.find(';', pos);
|
||||||
|
std::string token = cookieHeader.substr(pos, end == std::string::npos ? std::string::npos : end - pos);
|
||||||
|
|
||||||
|
return database.getUserByToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
const bin2cpp::File& headerTopFile = bin2cpp::getHeader_topHtmlFile();
|
const bin2cpp::File& headerTopFile = bin2cpp::getHeader_topHtmlFile();
|
||||||
const bin2cpp::File& headerBottomFile = bin2cpp::getHeader_bottomHtmlFile();
|
const bin2cpp::File& headerBottomFile = bin2cpp::getHeader_bottomHtmlFile();
|
||||||
const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile();
|
const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile();
|
||||||
|
const bin2cpp::File& loginfile = bin2cpp::getLoginHtmlFile();
|
||||||
const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile();
|
const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile();
|
||||||
const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile();
|
const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile();
|
||||||
const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile();
|
const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile();
|
||||||
@@ -29,6 +51,7 @@ int main() {
|
|||||||
std::string headerTop{headerTopFile.getBuffer(), headerTopFile.getSize()};
|
std::string headerTop{headerTopFile.getBuffer(), headerTopFile.getSize()};
|
||||||
std::string headerBottom{headerBottomFile.getBuffer(), headerBottomFile.getSize()};
|
std::string headerBottom{headerBottomFile.getBuffer(), headerBottomFile.getSize()};
|
||||||
std::string footer{footerfile.getBuffer(), footerfile.getSize()};
|
std::string footer{footerfile.getBuffer(), footerfile.getSize()};
|
||||||
|
std::string login{loginfile.getBuffer(), loginfile.getSize()};
|
||||||
std::string e404{e404file.getBuffer(), e404file.getSize()};
|
std::string e404{e404file.getBuffer(), e404file.getSize()};
|
||||||
std::string style{stylefile.getBuffer(), stylefile.getSize()};
|
std::string style{stylefile.getBuffer(), stylefile.getSize()};
|
||||||
std::string script{scriptfile.getBuffer(), scriptfile.getSize()};
|
std::string script{scriptfile.getBuffer(), scriptfile.getSize()};
|
||||||
@@ -47,6 +70,62 @@ int main() {
|
|||||||
response.set_content(script, "text/css");
|
response.set_content(script, "text/css");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
svr.Get("/login", [&login](const httplib::Request& request, httplib::Response& response) {
|
||||||
|
response.set_content(login, "text/html");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/login.html", [&login](const httplib::Request& request, httplib::Response& response) {
|
||||||
|
response.set_content(login, "text/html");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Post("/login", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
|
||||||
|
std::string username = request.form.get_field("username");
|
||||||
|
std::string password = request.form.get_field("password");
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(data_mutex);
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::optional<User> user = database.getUserByName(username);
|
||||||
|
|
||||||
|
if (user.has_value()) {
|
||||||
|
if (!bcrypt::validatePassword(password, user->passwordHash)) {
|
||||||
|
response.status = 401;
|
||||||
|
response.set_content("<p>wrong password lmao</p>", "text/html");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (username.empty()) {
|
||||||
|
response.status = 400;
|
||||||
|
response.set_content("<p>hey you can't have an empty username!!!!1!!!1! >:(</p>", "text/html");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// register on the fly, same as make_post currently does
|
||||||
|
User newUser{0, username, bcrypt::generateHash(password)};
|
||||||
|
database.addUser(newUser);
|
||||||
|
user = newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> token = database.createNewToken(user->id);
|
||||||
|
if (!token.has_value()) {
|
||||||
|
response.status = 500;
|
||||||
|
response.set_content("<p>couldn't create a session, sorry</p>", "text/html");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HttpOnly so script.js can't read/leak it, SameSite=Lax so it
|
||||||
|
// isn't sent on cross-site POSTs (basic CSRF mitigation),
|
||||||
|
// Max-Age matches the 30 day expiry stored in the DB
|
||||||
|
response.set_header(
|
||||||
|
"Set-Cookie",
|
||||||
|
"session=" + *token + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000"
|
||||||
|
);
|
||||||
|
response.set_redirect("/");
|
||||||
|
} catch (const std::runtime_error& e) {
|
||||||
|
response.status = 500;
|
||||||
|
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
svr.Get("/", [&headerTop, &headerBottom, &footer, &database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
|
svr.Get("/", [&headerTop, &headerBottom, &footer, &database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
|
|||||||
Reference in New Issue
Block a user