admin: user accounts and session login

The admin was one shared HTTP Basic login: no way to give someone
partial access, revoke one person, or tell who changed what. Add a users
table and a session login form. Passwords are scrypt hashes from Ruby's
OpenSSL, so no native gem is needed. Sessions carry a per-user version
that is bumped on password change, which logs that user out everywhere.

ADMIN_USER/ADMIN_PASSWORD now bootstrap the first superuser on an empty
table and afterwards only work while no active superuser exists, as a
recovery path that re-creates one. Until per-area permissions land,
non-superusers only reach the dashboard and their own password page.

Assisted-by: Claude:opus-5
This commit is contained in:
Sergei Poljanski 2026-10-10 08:25:46 +04:00
commit 9bf40690e5
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
13 changed files with 436 additions and 59 deletions

View file

@ -2,6 +2,7 @@ require_relative 'test_helper'
class AppTest < Minitest::Test
include Rack::Test::Methods
include AdminLogin
def app
AsxpioWeb
@ -19,10 +20,6 @@ class AppTest < Minitest::Test
flunk("no CSRF token found on #{path}")
end
def admin_env(extra = {})
{ 'HTTP_AUTHORIZATION' =>
"Basic #{Base64.strict_encode64("#{ENV['ADMIN_USER']}:#{ENV['ADMIN_PASSWORD']}")}" }.merge(extra)
end
# --- public pages ---------------------------------------------------------
@ -55,9 +52,13 @@ class AppTest < Minitest::Test
# --- admin ------------------------------------------------------------
def test_admin_requires_auth
def test_admin_requires_login
skip 'TEST_DATABASE_URL not set' unless TestDb.available?
get '/admin/invoices'
assert_equal 401, last_response.status
assert_equal 302, last_response.status
assert_match %r{/admin/login\?next=%2Fadmin%2Finvoices\z}, last_response.location
post '/admin/invoices'
assert_includes [401, 403], last_response.status
end
def test_admin_list_with_auth