Add a Sinatra server that keeps boards in SQLite

Nullboard's storage layer bottoms out in getItem/setItem/delItem over
string keys, with boards, revisions and undo history all modelled on top
of them client-side. So the server needs to be nothing more than a
key/value store, and it doesn't parse a board anywhere.

One password, bcrypt-hashed, in the same database as the boards. The
session secret lives there too, so restarting the service doesn't sign
you out and the secret never has to exist in the unit file or in git.

Until a password is set every route returns 503 pointing at
bin/validboard-passwd. A first-run setup page would be friendlier, but it
would also hand the board to whoever found the URL first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UU1vyTHj3uE9PJYSxRxwkU
This commit is contained in:
Tom Flux
2026-08-14 22:40:51 +01:00
co-authored by Claude Opus 5
parent db65363530
commit 2ae11e8d1e
8 changed files with 706 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# Sets (or resets) the single ValidBoard password.
#
# bin/validboard-passwd # prompts, twice, without echo
# VALIDBOARD_DB=/var/lib/validboard/validboard.db bin/validboard-passwd
#
# Run it as the same user the service runs as, or the new database file ends up
# owned by the wrong account.
require 'io/console'
require_relative '../store'
MIN_LENGTH = 8
store = ValidBoard::Store.new(ValidBoard.db_path)
puts "ValidBoard database: #{store.path}"
puts store.password_set? ? 'A password is already set; this will replace it.' : 'No password set yet.'
puts
def prompt(label)
$stdout.print(label)
$stdout.flush
value = $stdin.noecho(&:gets)
puts
value&.chomp
end
# Piped in — `pass show validboard | bin/validboard-passwd` — so take the one
# line and skip the confirmation there is no one there to type.
password =
if $stdin.tty?
typed = prompt('New password: ')
abort 'Aborted.' if typed.nil?
abort 'Passwords did not match.' unless prompt('Repeat password: ') == typed
typed
else
$stdin.gets&.chomp
end
abort 'No password given.' if password.nil? || password.empty?
if password.length < MIN_LENGTH
abort "Password must be at least #{MIN_LENGTH} characters."
end
store.password = password
puts 'Password updated.'
puts 'Existing sessions stay valid — restart the service if you want to force a re-login.'