commit c6a49dc6e8a4c6dd4f7fc9a28aab1fdb80604132 Author: Greg Sarjeant <1686767+gsarjeant@users.noreply.github.com> Date: Mon May 26 10:41:14 2025 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f894088 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.sqlite +*.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e68a70 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# tkr + +A bare-bones status feed for self-hosted personal websites. + +Currently very much a work in progress, but it's baically functional. + +## How it works + +Deploy the `/tkr` directory to a web server that supports php. It will work either as the root of a (sub)domain (e.g. tky.mydomain.com) or if served from a subdirectory (e.g. mydomain.com/tkr). + +If you serve it from a subdirectory, set the value of `$basePath` in `/app/config.php` to the subdirectory name, excluding the trailing slash (e.g. `/tkr`) + +It provides an rss feed at `/rss` relative to where it's being served (e.g. `/tkr/rss` if served from `/tkr/`). Each rss entry links to an individual post (which I call "ticks"). + +## Serving + +The document root should be `/PATH/TO/tkr/public`. This will ensure that only the files that need to be accessible from the internet are served by your web server. + +## Storage + +Ticks are stored in files on the filesystem under `/tkr/ticks`. This directory must be writable by the web server user and so SHOULD NOT be served by the web server. If you set your document root to `/tkr/public/`, then you'll be fine. + +The file structure is `YYYY/MM/DD.txt`. That is, each day's ticks are located in a file whose full path is `/tkr/ticks/YEAR/MONTH/DAY.txt`. This is to prevent any single file from getting too large. + +Each entry takes the form `TIMESTAMP|TICK`, where `TIMESTAMP` is the time that the entry was made and `TICK` is the text of the entry. + +For illustration, here's a sample from the file `/tkr/ticks/2025/05/25` on my test system. + +```sh +# cat /tkr/ticks/2025/05/25.txt +23:27:37|some stuff +23:27:45|some more, stuff +``` + +## Initial config + +I'll write this up when I improve it. + +## Sample images + +Logged out + + + +Logged in + + + +RSS + + + +Single tick + + + +## TODO + +* An actual setup script +* Validate CSRF token on tick submission +* Let people set the time zone for ticks +* Support basic custom styling +* Do that config improvement I implied in the previous section +* See if I can get individual ticks to resolve as urls (e.g. /2025/05/26/00/12) rather than doing the query string thing +* Clean up the nginx configs +* Add an .htaccess example +* Maybe h-feed or JSON feed? +* Microformat support? +* A little more profile info? +* Probably a bunch of other stuff I'm not thinking of \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..72122eb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + web: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./tkr-nginx-folder.conf:/etc/nginx/conf.d/default.conf + - ./tkr:/tkr + php: + image: php:fpm + volumes: + - ./tkr:/tkr \ No newline at end of file diff --git a/tkr-nginx-folder.conf b/tkr-nginx-folder.conf new file mode 100644 index 0000000..c90a0e1 --- /dev/null +++ b/tkr-nginx-folder.conf @@ -0,0 +1,33 @@ +server { + listen 80 default_server; + root /usr/share/nginx/html; + + location ^~ /tkr { + index index.php; + alias /tkr/public; + + #location ~ "^/tkr/([0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]{2}/[0-9]{2}/[0-9]{2})$" { + # rewrite "^/tkr/([0-9/]{19})$ /tkr/tick.php?path=$1" break; + #} + + location ~ ^/tkr(/.+\.php)$ { + fastcgi_pass php:9000; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /tkr/public/$1; + fastcgi_param SCRIPT_NAME $uri; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + } + } + + # Deny anything else + location / { + try_files $uri $uri/ =404; + } + + # Deny access to hidden or stray files + location ~* \.(htaccess|env|ini|log|bak)$ { + deny all; + } +} \ No newline at end of file diff --git a/tkr-nginx.conf b/tkr-nginx.conf new file mode 100644 index 0000000..42266d0 --- /dev/null +++ b/tkr-nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80 default_server; + root /app/public; + + index index.php index.html index.htm; + + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + include fastcgi_params; + } + + location / { + try_files $uri $uri/ =404; + } + + location ~* \.(htaccess|env|ini|log|bak)$ { + deny all; + } +} \ No newline at end of file diff --git a/tkr/config.php b/tkr/config.php new file mode 100644 index 0000000..ff8df93 --- /dev/null +++ b/tkr/config.php @@ -0,0 +1,12 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); +} catch (PDOException $e) { + die("Database connection failed: " . $e->getMessage()); +} diff --git a/tkr/create_db.php b/tkr/create_db.php new file mode 100644 index 0000000..d9569f6 --- /dev/null +++ b/tkr/create_db.php @@ -0,0 +1,56 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch (PDOException $e) { + die("Could not connect to DB: " . $e->getMessage() . "\n"); +} + +$pdo->exec("CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL +)"); + +$username = prompt("Enter username: "); +$password = promptSilent("Enter password: "); +$confirm = promptSilent("Confirm password: "); + +if ($password !== $confirm) { + die("Error: Passwords do not match.\n"); +} + +$passwordHash = password_hash($password, PASSWORD_DEFAULT); + +try { + $stmt = $pdo->prepare("INSERT INTO user(username, password_hash) VALUES (?, ?)"); + $stmt->execute([$username, $passwordHash]); + echo "User '$username' created successfully.\n"; +} catch (PDOException $e) { + echo "Failed to create user: " . $e->getMessage() . "\n"; +} diff --git a/tkr/db/.gitkeep b/tkr/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tkr/public/index.php b/tkr/public/index.php new file mode 100644 index 0000000..f3858fe --- /dev/null +++ b/tkr/public/index.php @@ -0,0 +1,61 @@ + + + +
+Logout = htmlspecialchars($_SESSION['username']) ?>
+ + + diff --git a/tkr/public/login.php b/tkr/public/login.php new file mode 100644 index 0000000..c3c4d25 --- /dev/null +++ b/tkr/public/login.php @@ -0,0 +1,50 @@ +prepare("SELECT id, username, password_hash FROM user WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + session_regenerate_id(true); + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + header('Location: ' . $basePath . '/'); + exit; + } else { + $error = 'Invalid username or password'; + } +} + +$csrf_token = generateCsrfToken(); +?> + + + += htmlspecialchars($error) ?>
+ + + + diff --git a/tkr/public/logout.php b/tkr/public/logout.php new file mode 100644 index 0000000..1ffad8e --- /dev/null +++ b/tkr/public/logout.php @@ -0,0 +1,11 @@ +' . "\n"; +?> +" . htmlspecialchars(explode('|', $line)[1]) . "
"; + exit; + } +} + +http_response_code(404); +echo "Tick not found."; diff --git a/tkr/session.php b/tkr/session.php new file mode 100644 index 0000000..a5b65f5 --- /dev/null +++ b/tkr/session.php @@ -0,0 +1,17 @@ + strcmp($b, $a)); // sort filenames in reverse chronological order + + $count = 0; + foreach ($tick_files as $file) { + // read all the ticks from the current file and reverse the order + // so the most recent ones are first + // + // each file is a single day, so we never hold more than + // one day's ticks in memory + $lines = array_reverse( + file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) + ); + + $pathParts = explode('/', str_replace('\\', '/', $file)); + $date = $pathParts[count($pathParts) - 3] . '-' . + $pathParts[count($pathParts) - 2] . '-' . + pathinfo($pathParts[count($pathParts) - 1], PATHINFO_FILENAME); + + foreach ($lines as $line) { + // just keep skipping ticks until we get to the starting point + if ($offset > 0) { + $offset--; + continue; + } + + // Ticks are pipe-delimited: timestamp|text + // But just in case a tick contains a pipe, only split on the first one that occurs + $tickParts = explode('|', $line, 2); + $time = $tickParts[0]; + $tick = $tickParts[1]; + + yield [ + 'timestamp' => $date . ' ' . $time, + 'tick' => $tick, + ]; + + if (++$count >= $limit) { + return; + } + } + } +} diff --git a/tkr/ticks/.gitkeep b/tkr/ticks/.gitkeep new file mode 100644 index 0000000..e69de29