Initial commit for the blogging engine, contains SQLite authentication

This commit is contained in:
Conner Harkness 2025-06-21 16:41:41 -06:00
parent 82b42b2bda
commit 664544fea4
16 changed files with 3059 additions and 0 deletions

11
footer.php Normal file
View File

@ -0,0 +1,11 @@
<hr />
<div class="container">
<div class="row">
<div class="col-lg-4">
<div>
Copyright &copy; 2025 Your Company.
</div>
</div>
</div>
</div>

10
head.php Normal file
View File

@ -0,0 +1,10 @@
<meta name="viewport" content="initial-scale=1, width=device-width" />
<!-- -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" ></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/js/all.min.js"></script>

80
header.php Normal file
View File

@ -0,0 +1,80 @@
<?php
$varNavbarLinks = [
["Home", "/"],
["Sign in", "/user/signin"],
];
?>
<script>
// Make the page's theme dark:
$("body").first().attr("data-bs-theme", "dark");
</script>
<div class="offcanvas offcanvas-start">
<div class="offcanvas-body">
Hello world <span data-bs-dismiss="offcanvas">x</span>
</div>
</div>
<div class="navbar navbar-expand bg-secondary d-flex px-3">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex">
<span class="navbar-brand">Home</span>
<div class="dropdown d-lg-none">
<a class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">...</a>
<div class="dropdown-menu">
<?php foreach ($varNavbarLinks as $varLink): ?>
<a class="dropdown-item" href="<?= $varLink[1]; ?>"><i class="fa fa-fw fa-link pe-2"></i> <?= $varLink[0]; ?></a>
<?php endforeach; ?>
</div>
</div>
<?php foreach ($varNavbarLinks as $varLink): ?>
<a class="nav-link d-none d-lg-inline" href="<?= $varLink[1]; ?>"><?= $varLink[0]; ?></a>
<?php endforeach; ?>
</div>
<div class="navbar-nav d-inline-flex">
<div class="dropdown">
<?php
$varUser = UserAuth::getUser();
$strUserText = "User";
if ($varUser !== null)
$strUserText = $varUser["user_name"] ?? $varUser["email"] ?? "User";
?>
<a class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">User</a>
<div class="dropdown-menu dropdown-menu-end">
<?php if ($varUser !== null): ?>
<a class="dropdown-item" href="/user/info"><i class="fa fa-fw fa-user pe-2"></i> <?= $strUserText; ?></a>
<a class="dropdown-item" href="/user/signout"><i class="fa fa-fw fa-right-from-bracket pe-2"></i> Sign Out</a>
<!--
<a class="nav-link" href="/user/signin">Account</a>
-->
<?php else: ?>
<a class="dropdown-item" href="/user/signin"><i class="fa fa-fw fa-right-to-bracket pe-2"></i> Sign In</a>
<a class="dropdown-item" href="/user/register"><i class="fa fa-fw fa-user-plus pe-2"></i> Register</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>

99
init.php Normal file
View File

@ -0,0 +1,99 @@
<?php
global $c;
$c = new DatabaseConnection(
"sqlite",
"sqlite.db");
class UserAuth
{
public static function getUser()
{
global $c;
try
{
$strToken = Cookie::get("token");
if ($strToken !== null)
if (strlen($strToken) > 0)
{
$varTokenUsers = $c->query(
"SELECT *
from tokens as t
join user as u on u.email = t.email
where
t.token = ?
and (
t.expires is null
or t.expires > current_timestamp
)",
$strToken);
$varUser = null;
if (count($varTokenUsers) == 1)
$varUser = $varTokenUsers[0];
else return null;
try
{
$varUserDetails = $c->query(
"SELECT *
from user_info as ui
where
ui.email = ?",
$varUser["email"]);
if (count($varUserDetails) == 1)
$varUser = array_merge($varUser, $varUserDetails[0]);
}
catch (Exception $x) {}
return $varUser;
}
}
catch (Exception $x) {}
return null;
}
public static function hasPermission($strPermission)
{
global $c;
$varUser = UserAuth::getUser();
if ($varUser == null)
return false;
$c->query(
"CREATE table if not exists permission (
id integer primary key autoincrement,
email text not null,
name text not null)");
$varPermissions = $c->query(
"SELECT *
from permission
where
email like ?
and (
name like ?
or name like '*'
)",
$varUser["email"],
$strPermission);
if (count($varPermissions) > 0)
return true;
return false;
}
public static function requirePermission($strPermission)
{
if (!UserAuth::hasPermission($strPermission))
{
BootstrapRender::message("You do not have permission to do that, please sign into an account that does.", "warning");
Respond::redirect("/user/signin");
}
}
}
?>

159
lib/BootstrapRender.php Normal file
View File

@ -0,0 +1,159 @@
<?php
class BootstrapRender
{
public static function message()
{
if (func_num_args() > 0)
{
Cookie::set("message", func_get_arg(0));
if (func_num_args() > 1)
Cookie::set("messageClass", func_get_arg(1));
return;
}
$strMessage = Cookie::get("message");
$strMessageClass = Cookie::get("messageClass");
if (!isset($strMessageClass) || $strMessageClass == null || strlen($strMessageClass) < 1)
$strMessageClass = "info";
?>
<?php if (isset($strMessage) && $strMessage !== null && strlen($strMessage) > 0): ?>
<div class="alert alert-<?= $strMessageClass; ?> d-none" id="page-message">
<?= $strMessage; ?>
</div>
<script>
$(function() {
$("#page-message")
.hide()
.removeClass("d-none")
.fadeIn();
});
</script>
<?php endif; ?>
<?php
Cookie::set("message");
Cookie::set("messageClass");
}
public static function input($varOptions)
{
$strName = $varOptions["name"];
$strLabel = $varOptions["label"] ?? $strName;
$strPlaceholder = $varOptions["placeholder"] ?? "Enter {$strLabel}";
$strValue = $varOptions["value"] ?? "";
$intReadonly = $varOptions["readonly"] ?? 0;
$intDisabled = $varOptions["disabled"] ?? 0;
$strType = $varOptions["type"] ?? "text";
$intInline = $varOptions["inline"] ?? 0;
$strTag = $varOptions["tag"] ?? "input";
?>
<?php if ($intInline == 1): ?>
<div class="row g-3 align-items-center mb-3">
<div class="col-3">
<label class="col-form-label"><?= $strLabel; ?></label>
</div>
<div class="col-8">
<<?= $strTag; ?> type="<?= $strType; ?>"
class="form-control"
name="<?= $strName; ?>"
placeholder="Enter <?= $strLabel; ?>"
value="<?= $strValue; ?>"
<?= $intReadonly? "readonly": ""; ?>
<?= $intDisabled? "disabled": ""; ?>
<?php if ($strTag == "textarea"): ?>
><?= $strValue; ?></<?= $strTag; ?>>
<?php else: ?>
/>
<?php endif; ?>
</div>
<div class="col-auto">
<span class="form-text">test</span>
</div>
</div>
<?php else: ?>
<div class="mb-3">
<label class="form-label"><?= $strLabel; ?></label>
<div class="input-group">
<<?= $strTag; ?> type="<?= $strType; ?>"
class="form-control"
name="<?= $strName; ?>"
placeholder="Enter <?= $strLabel; ?>"
value="<?= $strValue; ?>"
<?= $intReadonly? "readonly": ""; ?>
<?= $intDisabled? "disabled": ""; ?>
<?php if ($strTag == "textarea"): ?>
><?= $strValue; ?></<?= $strTag; ?>>
<?php else: ?>
/>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php
}
public static function buttons($varOptions)
{
$strLabel = $varOptions["label"] ?? "Actions";
$intInputGroup = $varOptions["input_group"] ?? 0;
$varButtons = $varOptions["buttons"] ?? [];
$strButtonClass = $intInputGroup == 0? "me-1 mb-2": "";
?>
<?php if (count($varButtons) > 0): ?>
<div class="mb-3">
<label class="form-label"><?= $strLabel; ?></label>
<div class="<?= $intInputGroup == 1? "input-group": ""; ?>">
<?php foreach ($varButtons as $varButton): ?>
<?php
$strLabel = $varButton["label"];
$strIcon = $varButton["icon"] ?? null;
$strType = $varButton["type"] ?? null;
$strOnclick = $varButton["onclick"] ?? null;
$strHref = $varButton["href"] ?? null;
$strClass = $varButton["class"] ?? "outline-secondary";
$strTag = "button";
if ($strHref !== null)
$strTag = "a";
?>
<<?= $strTag; ?>
class="btn btn-<?= $strClass; ?> <?= $strButtonClass; ?>"
<?= $strType !== null? "type=\"{$strType}\"": ""; ?>
<?= $strOnclick !== null? "onclick=\"{$strOnclick}\"": ""; ?>
<?= $strHref !== null? "href=\"{$strHref}\"": ""; ?>>
<?php if ($strIcon !== null): ?>
<i class="fa fa-fw fa-<?= $strIcon; ?>"></i>
<?php endif; ?>
<span><?= $strLabel; ?></span>
</<?= $strTag; ?>>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php
}
}
?>

1994
lib/Parsedown.php Normal file

File diff suppressed because it is too large Load Diff

224
lib/TableEditor.php Normal file
View File

@ -0,0 +1,224 @@
<?php
class TableEditor
{
public static function render($strTableName, $varColumns)
{
global $c;
$varRows = $c->query("SELECT * from {$strTableName}");
$varKeys = [];
$strInput = file_get_contents("php://input");
if (strlen($strInput) > 0)
{
$a = json_decode($strInput, true);
$output = [];
foreach ($a as $r)
{
$strColumns = "";
$strQMarks = "";
$strSetLns = "";
$varValues = [];
foreach ($varColumns as $strCol)
{
$strColumns .= "{$strCol}, ";
$strQMarks .= "?, ";
$strSetLns .= "{$strCol} = ?, ";
$varValues[] = $r[$strCol];
}
$strColumns = preg_replace("/, $/", "", $strColumns);
$strQMarks = preg_replace("/, $/", "", $strQMarks);
$strSetLns = preg_replace("/, $/", "", $strSetLns);
if (strlen($r["id"]) < 1)
{
$c->query(
"INSERT into {$strTableName} ({$strColumns}) values ({$strQMarks})",
$varValues);
continue;
}
if (intval($r["delete"]) == 1)
{
$c->query("DELETE from {$strTableName} where id = ?", $r["id"]);
continue;
}
$c->query(
"UPDATE {$strTableName}
set {$strSetLns}
where id = ?",
$varValues,
$r["id"]);
$output[] = $r;
}
Respond::json(["message" => "success", "output" => $output]);
}
?>
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex">
<span class="navbar-brand">Options</span>
<a class="btn btn-outline-success" onclick="fnSave();"><i class="fa fa-fw fa-save"></i> Save</a>
</div>
<div class="navbar-nav d-inline-flex">
</div>
</div>
</div>
<style>
/* https://github.com/twbs/bootstrap/issues/37184 */
.dropdown-menu {
z-index: 1040 !important;
}
/*
th:first-child,
th:last-child {
width: 7.5%;
background: #F00 !important;
}*/
.table-responsive {
overflow-x: scroll;
}
.w-input {
width: 15em !important;
}
tr td:first-child input[type="text"]
{
width: 5em !important;
}
</style>
<div class="container">
<div class="row my-5">
<div class="col-lg-12">
<?php if (count($varRows) > 0): ?>
<div class="table-responsive">
<table class="table table-borderless">
<thead>
<tr>
<?php foreach($varRows[0] as $k => $v): ?>
<?php
$varKeys[] = $k;
?>
<th><?= $k; ?></th>
<?php endforeach; ?>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($varRows as $r): ?>
<tr>
<?php foreach ($varKeys as $k): ?>
<td>
<div class="input-group">
<input type="text" class="form-control w-input" name="<?= $k; ?>" value="<?= $r[$k]; ?>" />
</div>
</td>
<?php endforeach; ?>
<td class="align-middle text-nowrap">
<input type="hidden" name="delete" value="0" />
<a class="" onclick="fnCloneRow(this);"><i class="fa fa-fw fa-copy"></i></a>
<a class="" onclick="fnDeleteRow(this);"><i class="fa fa-fw fa-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
$(function() {
fnSerialize = function() {
var a = [];
$("table tbody tr").each(function(i, x) {
x = $(x);
var inputs = x.find("input");
var o = {};
inputs.each(function(i2, x2) {
x2 = $(x2);
var key = x2.attr("name");
var value = x2.val();
o[key] = value;
});
a.push(o);
});
console.log(a);
return a;
};
fnSave = function()
{
var data = fnSerialize();
$.ajax({
url: "",
method: "post",
data: JSON.stringify(data),
success: function(r)
{
console.log(r);
}
});
};
fnCloneRow = function(x)
{
x = $(x);
var row = x.parents("tr").first();
var rowCopy = row.clone();
rowCopy.insertAfter(row);
rowCopy.find("input").each(function(i, x2) {
x2 = $(x2);
x2.val("");
});
};
fnDeleteRow = function(x)
{
x = $(x);
var row = x.parents("tr").first();
row.hide();
row.find("[name='delete']").first().val("1");
};
});
</script>
<?php
}
}
?>

94
pages/edit/index.php Normal file
View File

@ -0,0 +1,94 @@
<?php
global $c;
$strId = Request::getArg(0);
$strPath = "";
$strContent = "";
if (strlen($strId) > 0)
{
$varRows = $c->query("SELECT * from post where id = ?", $strId);
if (count($varRows) !== 1)
{
BootstrapRender::message("Zero or more than one row returned", "danger");
}
$varRow = $varRows[0];
$strPath = $varRow["path"];
$strContent = $varRow["content"];
}
if (Request::posts("path", "content"))
{
$strPath = Request::getPosted("path");
$strContent = Request::getPosted("content");
if ($strId == null || strlen($strId) < 1)
{
$c->query(
"INSERT into post (author, path, content)
values (?, ?, ?)",
"caharkness@gmail.com",
$strPath,
$strContent);
$strId = $c->query("SELECT * from post where rowid = last_insert_rowid()")[0]["id"];
}
$c->query(
"UPDATE post
set
path = ?,
content = ?,
updated = current_timestamp",
$strPath,
$strContent);
Respond::redirect("/edit/{$strId}");
}
if (strlen($strId) > 0)
{
$varRows = $c->query("SELECT * from post where id = ?", $strId);
}
?>
<div class="container my-5">
<div class="row">
<div class="col-lg-6">
<div class="mb-3">
<?php BootstrapRender::message(); ?>
</div>
<form method="post">
<?php BootstrapRender::input([
"name" => "path",
"label" => "Path",
"value" => $strPath
]); ?>
<?php BootstrapRender::input([
"name" => "content",
"label" => "Content",
"tag" => "textarea",
"value" => $strContent
]); ?>
<?php BootstrapRender::buttons([
"buttons" => [[
"label" => "Submit",
"icon" => "save"
]]
]); ?>
</div>
</div>
</div>

39
pages/index.php Normal file
View File

@ -0,0 +1,39 @@
<?php
$strPath = "/";
$strPath .= implode("/", Request::getPathParts());
global $c;
$c->query(
"CREATE table if not exists post (
id integer primary key autoincrement,
author text not null,
path text not null,
content text not null,
created timestamp not null default current_timestamp,
updated timestamp not null default current_timestamp)");
$varPosts = $c->query(
"SELECT *
from post
where
path like ?
or path like '*'",
$strPath);
$varParsedown = new Parsedown();
?>
<?php foreach ($varPosts as $p): ?>
<div class="container my-5">
<div class="row">
<div class="col-lg-12">
<?php
$strContent = $varParsedown->text($p["content"]);
echo $strContent;
?>
</div>
</div>
</div>
<?php endforeach; ?>

29
pages/pd/index.php Normal file
View File

@ -0,0 +1,29 @@
<?php
$varParsedown = new Parsedown();
$strText = $varParsedown->text("
# Hello world
1. Testing 123
2. Testing 456
```
if then else
```
");
if (Request::getArg(0) == "plain")
{
ob_clean();
echo $strText;
ob_end_flush();
exit;
}
?>
<div class="container my-5">
<div class="row">
<div class="col-lg-6">
<?= $strText; ?>
</div>
</div>
</div>

92
pages/user/info.php Normal file
View File

@ -0,0 +1,92 @@
<?php
global $c;
$strError = null;
$c->query(
"CREATE table if not exists user_info (
id integer primary key autoincrement,
email text not null unique,
user_name text null,
display_name text null)");
$varUser = UserAuth::getUser();
$strUsername = $varUser["user_name"] ?? "";
$strDisplayName = $varUser["display_name"] ?? "";
if ($varUser == null)
Respond::redirect("/");
try
{
if (Request::posts("user_name", "display_name"))
{
$strUsername = Request::getPosted("user_name");
$strDisplayName = Request::getPosted("display_name");
if (!preg_match("/^[A-Za-z0-9]{1,}$/", $strUsername))
throw new Exception("Username must be alphanumeric characters only");
$c->query(
"INSERT or replace into user_info (email, user_name, display_name)
select
?,
?,
?",
$varUser["email"],
$strUsername,
$strDisplayName);
BootstrapRender::message("Profile updated", "success");
}
}
catch (Exception $x)
{
BootstrapRender::message($x->getMessage(), "danger");
}
?>
<div class="container">
<div class="row my-5">
<div class="col-md-4">
<div class="mb-3">Edit your account details here.</div>
</div>
<div class="col-md-4">
<?php BootstrapRender::message(); ?>
<form method="post">
<?php BootstrapRender::input([
"name" => "email",
"label" => "E-Mail Address",
"value" => $varUser["email"],
"disabled" => 1,
]); ?>
<?php BootstrapRender::input([
"name" => "user_name",
"label" => "Username",
"value" => $strUsername,
]); ?>
<?php BootstrapRender::input([
"name" => "display_name",
"label" => "Display Name",
"value" => $strDisplayName,
]); ?>
<?php BootstrapRender::buttons([
"input_group" => 0,
"buttons" => [[
"icon" => "save",
"label" => "Save",
"type" => "submit",
"class" => "outline-success"
]]]); ?>
</form>
</div>
</div>
</div>

6
pages/user/list.php Normal file
View File

@ -0,0 +1,6 @@
<?php
global $c;
UserAuth::requirePermission("hello_world");
TableEditor::render("user", ["email", "hash"]);
?>

View File

@ -0,0 +1,5 @@
<?php
global $c;
UserAuth::requirePermission("hello_world");
TableEditor::render("permission", ["email", "name"]);
?>

100
pages/user/register.php Normal file
View File

@ -0,0 +1,100 @@
<?php
global $c;
try
{
if (Request::posts("email", "password", "repeat"))
{
$strEmail = Request::getPosted("email");
$strPassword = Request::getPosted("password");
$strRepeat = Request::getPosted("repeat");
if (!preg_match("/^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$/", $strEmail))
throw new Exception("Not a valid e-mail address");
if (Request::getPosted("password") !== Request::getPosted("repeat"))
throw new Exception("Passwords do not match");
if (strlen($strPassword) < 6)
throw new Exception("Password must be at least 6 characters");
$c->query(
"CREATE table if not exists user (
id integer primary key autoincrement,
email text not null unique,
hash text not null)");
$varUsers = $c->query("SELECT * from user where email like ?", $strEmail);
if (count($varUsers) > 0)
throw new Exception("E-mail address in use");
$strHash = sha1($strPassword);
$c->query(
"INSERT into user (email, hash) values (?, ?)",
$strEmail,
$strHash);
BootstrapRender::message("Registration was a success, please sign in to continue.");
Respond::redirect("/user/signin");
}
}
catch (Exception $x)
{
BootstrapRender::message($x->getMessage(), "danger");
}
?>
<script>
$(".app-header").hide();
</script>
<div class="container">
<div class="row my-5">
<div class="col-md-4 offset-md-4">
<?php BootstrapRender::message(); ?>
<form method="post">
<?php BootstrapRender::input([
"name" => "email",
"label" => "E-Mail Address",
"value" => Request::getPosted("email")
]); ?>
<?php BootstrapRender::input([
"name" => "password",
"label" => "Password",
"value" => Request::getPosted("password"),
"type" => "password",
]); ?>
<?php BootstrapRender::input([
"name" => "repeat",
"label" => "Repeat Password",
"value" => Request::getPosted("repeat"),
"type" => "password",
]); ?>
<div class="mb-3">
<label class="form-label">Actions</label>
<div class="input-group">
<button class="btn btn-outline-primary" type="submit">
<i class="fa fa-fw fa-right-to-bracket"></i>
<span>Continue</span>
</button>
</div>
</div>
<div class="mb-3">
<a class="text-decoration-none" href="/user/signin">Already have an account?</a>
</div>
</form>
</div>
</div>
</div>

88
pages/user/signin.php Normal file
View File

@ -0,0 +1,88 @@
<?php
global $c;
try
{
if (Request::posts("email", "password"))
{
$strEmail = Request::getPosted("email");
$strPassword = Request::getPosted("password");
$strHash = sha1($strPassword);
$varUsers = $c->query(
"SELECT *
from user
where
email like ?
and hash = ?",
$strEmail,
$strHash);
if (count($varUsers) !== 1)
throw new Exception("Zero or more than one user returned for credentials provided");
$strToken = sha1(microtime());
$c->query("CREATE table if not exists tokens (
id integer primary key autoincrement,
email text not null,
token text not null,
expires timestamp null)");
$c->query(
"INSERT into tokens (email, token) values (?, ?)",
$strEmail,
$strToken);
Cookie::set("token", $strToken);
BootstrapRender::message(
"Successfully signed in",
"info");
Respond::redirect("/user/info");
}
}
catch (Exception $x)
{
BootstrapRender::message($x->getMessage(), "danger");
}
?>
<script>
$(".app-header").hide();
</script>
<div class="container">
<div class="row my-5">
<div class="col-md-4 offset-md-4">
<?php BootstrapRender::message(); ?>
<form method="post">
<?php BootstrapRender::input([
"name" => "email",
"label" => "E-Mail Address",
"value" => Request::getPosted("email")
]); ?>
<?php BootstrapRender::input([
"name" => "password",
"label" => "Password",
"value" => Request::getPosted("password"),
"type" => "password",
]); ?>
<?php BootstrapRender::buttons([
"input_group" => 0,
"buttons" => [
["icon" => "right-to-bracket", "label" => "Continue", "type" => "submit", "class" => "outline-primary"],
["icon" => "home", "label" => "Home", "href" => "/"]
]]); ?>
<div class="mb-3">
<a class="text-decoration-none" href="/user/register">Don't have an account?</a>
</div>
</form>
</div>
</div>
</div>

29
pages/user/signout.php Normal file
View File

@ -0,0 +1,29 @@
<?php
global $c;
$varUser = UserAuth::getUser();
if ($varUser !== null)
{
if (Request::getArg(0) == "all")
{
$c->query(
"UPDATE tokens
set
expires = current_timestamp
where email = ?",
$varUser["email"]);
}
else
{
$c->query(
"UPDATE tokens
set
expires = current_timestamp
where token = ?",
$varUser["token"]);
}
}
BootstrapRender::message("You have successfully signed out");
Respond::redirect("/user/signin");
?>