Work on ripping out spp

This commit is contained in:
Robin Malley 2023-05-17 21:18:24 +00:00
parent 60213086dc
commit e430d9b512
42 changed files with 347 additions and 647 deletions

View File

@ -16,12 +16,14 @@ kmgr_chroot = $(smr_var)/kore_kmgr
parent_chroot = $(smr_var)/kore_parent
conf_path = /etc/smr
smr_bin_path = /usr/local/lib
app_root=$(worker_chroot)/var/smr
ifeq ($(DEV),true)
worker_chroot = ./kore_chroot
kmgr_chroot = ./kore_chroot
parent_chroot = ./kore_chroot
conf_path = ./kore_chroot/conf
smr_bin_path = ./kore_chroot
app_root=./kore_chroot/app
endif
mirror=http://dl-cdn.alpinelinux.org/alpine/
@ -35,11 +37,11 @@ domain=test.monster:$(port)
server_cert=/root/cert/server.pem
server_key=/root/cert/key.pem
SPPFLAGS=-D port=$(port) -D kore_chroot=$(build_dir) -D chuser=$(user) -D domain=$(domain) -D bin_path="$(bin_path)" -D server_cert="$(server_cert)" -D server_key="$(server_key)" -D worker_chroot="$(worker_chroot)" -D kmgr_chroot="$(kmgr_chroot)"
SPPFLAGS=-D port=$(port) -D kore_chroot=$(worker_chroot) -D chuser=$(user) -D domain=$(domain) -D bin_path="$(bin_path)" -D server_cert="$(server_cert)" -D server_key="$(server_key)" -D worker_chroot="$(worker_chroot)" -D kmgr_chroot="$(kmgr_chroot)"
# squelch prints, flip to print verbose information
#Q=@
Q=
LUAROCKS_FLAGS=--tree $(build_dir)/usr/lib/luarocks --lua-version 5.1
LUAROCKS_FLAGS=--tree $(worker_chroot)/usr/lib/luarocks --lua-version 5.1
chroot_packages=\
-p luarocks5.1 \
-p "build-base" \
@ -62,21 +64,18 @@ lua_packages = \
zlib
# Probably don't change stuff past here if you're just using smr
lua_in_files=$(shell find src/lua/*.in -type f)
lua_files=$(shell find src/lua/*.lua -type f) $(shell find src/lua/endpoints -type f) $(lua_in_files:%.in=%)
lua_files=$(shell find src/lua/*.lua -type f) $(shell find src/lua/endpoints -type f)
src_files=$(shell find src -type f) $(shell find conf -type f)
sql_files=$(shell find src/sql -type f)
test_files=$(shell find spec -type f)
built_tests=$(test_files:%=$(build_dir)%)
built_files=$(lua_files:src/lua/%.lua=$(build_dir)%.lua)
in_page_files=$(shell find src/pages/*.in -type f)
page_files=$(in_page_files:%.in=%)
test_files=$(shell find spec -type f) $(shell find spec/parser_tests -type f)
page_files=$(shell find src/pages -type f)
built_tests=$(test_files:%=$(app_root)/%)
built_files=$(lua_files:src/lua/%.lua=$(app_root)/%.lua)
part_files=$(shell find src/pages/parts/*.etlua -type f)
built_pages=$(page_files:src/pages/%.etlua=$(build_dir)pages/%.etlua)
built_sql=$(sql_files:src/sql/%.sql=$(build_dir)sql/%.sql)
built_parts=$(part_files:src/%=$(app_root)/%)
built_pages=$(page_files:src/pages/%.etlua=$(app_root)/pages/%.etlua)
built_sql=$(sql_files:src/sql/%.sql=$(app_root)/sql/%.sql)
built=$(built_files) $(built_sql) $(built_pages) $(built_tests)
asset_in_files=$(wildcard assets/*.in -type f)
asset_files=$(asset_in_files:%.in=%)
initscript=/lib/systemd/system/smr.service
config=$(conf_path)/smr.conf
built_bin=$(smr_bin_path)/smr.so
@ -88,7 +87,7 @@ apk_hash := $(APK_$(arch)_HASH)
help: ## Print this help
$(Q)$(GREP) -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | $(SORT) | $(AWK) 'BEGIN {FS = ":.*?## "}; {printf "%-10s %s\n", $$1, $$2}'
all: $(build_dir) smr.so $(built_files) $(built_pages) $(built_sql) ## Build and run smr in a chroot
all: $(app_root) smr.so $(built_files) $(built_pages) $(built_sql) ## Build and run smr in a chroot
apk-tools-static-$(version).apk:
wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk
@ -100,7 +99,7 @@ clean: ## clean up all the files generated by this makefile
$(Q)$(RM) $(asset_files)
$(Q)$(RM) smr.so
install: $(worker_chroot) $(kmgr_chroot) $(parent_chroot) $(initscript) $(config) smr.so $(built_files) $(built_pages) $(built_sql) ## Install smr into a new host system
install: $(app_root) $(kmgr_chroot) $(parent_chroot) $(initscript) $(config) smr.so $(built_files) $(built_pages) $(built_sql) ## Install smr into a new host system
$(Q)$(COPY) smr.so $(built_bin)
$(config) : conf/smr.conf
@ -111,15 +110,19 @@ $(initscript) : packaging/systemd/smr.service
$(Q)$(COPY) $< $@
cloc: ## calculate source lines of code in smr
cloc --force-lang="html",etlua.in --force-lang="html",etlua --force-lang="lua",lua.in src assets Makefile
cloc --force-lang="html",etlua --force-lang="lua",luasrc assets Makefile
$(build_dir):
$(Q)$(MKDIR) $(build_dir)
$(Q)$(MKDIR) $(build_dir)/pages
$(Q)$(MKDIR) $(build_dir)/sql
$(Q)$(MKDIR) $(build_dir)/data
$(Q)$(MKDIR) $(build_dir)/data/archive
$(Q)$(MKDIR) $(build_dir)/endpoints
$(app_root):
$(Q)$(MKDIR) $(app_root)
$(app_root): $(worker_chroot)
$(Q)$(MKDIR) $(app_root)
$(Q)$(MKDIR) $(app_root)/pages
$(Q)$(MKDIR) $(app_root)/pages/parts
$(Q)$(MKDIR) $(app_root)/sql
$(Q)$(MKDIR) $(app_root)/data
$(Q)$(MKDIR) $(app_root)/data/archive
$(Q)$(MKDIR) $(app_root)/endpoints
alpine-chroot-install:
$(Q)wget https://raw.githubusercontent.com/alpinelinux/alpine-chroot-install/v0.14.0/alpine-chroot-install \
@ -134,43 +137,39 @@ $(worker_chroot) $(kmgr_chroot) $(parent_chroot): alpine-chroot-install
code : $(built_files)
$(built_files): $(build_dir)%.lua : src/lua/%.lua $(build_dir)
$(built_files): $(app_root)/%.lua : src/lua/%.lua $(app_root)
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $< $@
$(built_pages): $(build_dir)pages/%.etlua : src/pages/%.etlua $(build_dir)
$(built_pages): $(app_root)/pages/%.etlua : src/pages/%.etlua $(app_root)
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $< $@
src/lua/config.lua : src/lua/config.lua.in Makefile
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(built_parts): $(app_root)/% : src/%
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $< $@
$(page_files) : % : %.in $(part_files)
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(built_sql): $(build_dir)sql/%.sql : src/sql/%.sql
$(built_sql): $(app_root)/sql/%.sql : src/sql/%.sql
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $^ $@
$(built_tests) : $(build_dir)% : src/spec/%
$(built_tests) : $(app_root)/spec/% : spec/% $(app_root)/spec
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $^ $@
$(Q)$(COPY) $< $@
$(asset_files) : % : %.in
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(app_root)/spec: $(app_root)
$(Q)$(MKDIR) $@
$(Q)$(MKDIR) $@/parser_tests
smr.so : $(src_files) conf/build.conf $(asset_files)
$(Q)$(ECHO) "[build] $@"
$(Q)$(KODEV) build
test : $(built) ## run the unit tests
$(Q)$(CD) kore_chroot && busted -v --no-keep-going #--exclude-tags slow
$(Q)$(CD) $(app_root) && busted -v --no-keep-going --exclude-tags "slow,todo,working"
cov : $(built) ## code coverage (based on unit tests)
$(Q)$(RM) $(kore_chroot)/luacov.stats.out
$(Q)$(CD) $(kore_chroot) && busted -v -c --no-keep-going #--exclude-tags slow
$(Q)$(CD) $(kore_chroot) && busted -v -c --no-keep-going --exclude-tags slow
$(Q)$(CD) $(kore_chroot) && luacov endpoints/
$(Q)$(ECHO) "open kore_chroot/luacov.report.out to view coverage results."

View File

@ -2,7 +2,7 @@ _G.spy = spy
local mock_env = require("spec.env_mock")
local rng = require("spec.fuzzgen")
describe("smr biography",function()
describe("smr biography #todo",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
it("should allow users to set their biography",function()

View File

@ -1,10 +1,10 @@
_G.spy = spy
local mock_env = require("spec.env_mock")
local env_mock = require("spec.env_mock")
describe("smr cacheing",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
setup(env_mock.setup)
teardown(env_mock.teardown)
it("caches a page if the page is requested twice #working",function()
local read_get = require("endpoints.read_get")
local cache = require("cache")
@ -29,9 +29,15 @@ describe("smr cacheing",function()
))
end
end)
it("should expose the database connection", function()
local cache = require("cache")
configure()
assert(cache.cache, "Not exposed under .cache")
end)
it("does not cache the page if the user is logged in", function()
local read_get = require("endpoints.read_get")
local cache = require("cache")
env_mock.mockdb()
renderspy = spy.on(cache,"render")
configure()
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
@ -40,7 +46,7 @@ describe("smr cacheing",function()
"request have been made."
))
end
local req = mock_env.session()
local req = env_mock.session()
req.method = "GET"
req.path = "/a"
req.args = {}

View File

@ -12,6 +12,7 @@ local ntostring
local login_post
local fuzzy
local claim_post
local session
print_table= function(...)
print("Print called")
local args = {...}
@ -69,7 +70,10 @@ local smr_mock_env = {
http_request_get_path = spy.new(function(req) return req.path or "/" end),
http_request_populate_qs = spy.new(function(req) req.qs_populated = true end),
http_request_populate_post = spy.new(function(req) req.post_populated = true end),
http_populate_multipart_form = spy.new(function(req) req.post_populated = true end),
http_populate_multipart_form = spy.new(function(req)
req.post_populated = true
req.multipart_form_populated = true
end),
http_argument_get_string = spy.new(function(req,str)
assert(req.args,"requests should have a .args table")
assert(
@ -83,7 +87,7 @@ local smr_mock_env = {
return req.args[str]
end),
http_file_get = spy.new(function(req,filename)
assert(req.multipart_forum_populated,[[
assert(req.multipart_form_populated,[[
http_file_get() can only be called after the approriate
populate method has been called. (http_populate_multipart_form())
]])
@ -156,9 +160,9 @@ local string_fmt_override = {
setmetatable(string_fmt_override,{__index = string})
local smr_override_env = {
--Detour assert so we don't actually perform any checks
assert = spy.new(function(bool,msg,level) return bool end),
--assert = spy.new(function(bool,msg,level) return bool end),
--Allow string.format to accept nil as arguments
string = string_fmt_override
--string = string_fmt_override
}
mock.olds = {}
@ -173,10 +177,12 @@ end
function mock.mockdb()
local config = require("config")
config.db = "data/unittest.db"
--config.db = "data/unittest.db"
config.db = ":memory:"
assert(os.execute("rm " .. config.db))
package.loaded.db = nil
local db = require("db")
configure()
end
function mock.teardown()
@ -207,7 +213,7 @@ local session_m = {__index = {
}}
function mock.session(tbl)
if post_login == nil then
if login_post == nil then
login_post = require("endpoints.login_post")
fuzzy = require("spec.fuzzgen")
claim_post = require("endpoints.claim_post")

View File

@ -1,13 +1,13 @@
_G.spy = spy
local mock_env = require("spec.env_mock")
local env_mock = require("spec.env_mock")
local rng = require("spec.fuzzgen")
describe("smr login",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
setup(env_mock.setup)
teardown(env_mock.teardown)
it("should allow someone to claim an account",function()
mock_env.mockdb()
env_mock.mockdb()
local claim_post = require("endpoints.claim_post")
configure()
claim_req = {
@ -186,6 +186,7 @@ describe("smr login",function()
text = "post text",
markup = "plain",
tags = "",
pasteas = username
}
}
paste_post(paste_req_post)

View File

@ -10,8 +10,18 @@ borrowed sha3 implementation from https://keccak.team
#include "libcrypto.h"
#include "keccak.h"
/*
sha3(data::string)::string
/* rst
@name lua/sha3
Provides a sha3 implementation.
::signature
sha3(data :: {{lua/string}}) :: {{lua/string}}
::params
{{lua/string}} data - The data to hash
::returns
{{lua/string}} - The hash of the input string
*/
int
lsha3(lua_State *L){

View File

@ -1,7 +1,9 @@
--[[
--[[ md
@name lua/addon
Addon loader - Addons are either:
* A folder with at least two files:
- meta.lua - contains load order information
- meta.lua - contains addon information
- init.lua - entrypoint that gets run to load the addon
* A zip file with the same
* A sqlite3 database with a table "files" that has at least the columns
@ -15,9 +17,15 @@ each worker), and should return a table with at least the following information
{
name :: string - A name for the addon (all addons must have unique names)
desc :: string - A description for the addon.
entry :: table[number -> string] - Describes the load order for this
addon. Each addon's meta.lua is run, and sorted to get a load
order for entrypoints (the strings are the names files)
order :: number - When should we run init.lua relative to other addons?
Each addon's meta.lua is run (in any order), addons are sorted
according to their order, and finally each addon's init.lua is
called according to this order.
}
meta.lua may include additional information that can be read and used by other
addons. meta.lua is run in a restricted environment with almost no functions
available.
]]
local oldconfigure = configure

View File

@ -17,12 +17,11 @@ local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
local oldconfigure = configure
function configure(...)
local cache = db.sqlassert(sql.open_memory())
ret.cache = cache -- Expose db for testing
ret.cache = db.sqlassert(sql.open_memory())-- Expose db for testing
--A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO
assert(cache:exec([[
assert(ret.cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
data BLOB,
@ -30,7 +29,7 @@ function configure(...)
dirty INTEGER
);
]]))
stmnt_cache = assert(cache:prepare([[
stmnt_cache = assert(ret.cache:prepare([[
SELECT data
FROM cache
WHERE
@ -38,14 +37,14 @@ function configure(...)
((dirty = 0) OR (strftime('%s','now') - updated) > 20)
;
]]))
stmnt_insert_cache = assert(cache:prepare([[
stmnt_insert_cache = assert(ret.cache:prepare([[
INSERT OR REPLACE INTO cache (
path, data, updated, dirty
) VALUES (
:path, :data, strftime('%s','now'), 0
);
]]))
stmnt_dirty_cache = assert(cache:prepare([[
stmnt_dirty_cache = assert(ret.cache:prepare([[
UPDATE OR IGNORE cache
SET dirty = 1
WHERE path = :path;

View File

@ -1,13 +0,0 @@
--[[
Holds configuration.
A one-stop-shop for runtime configuration
]]
local config = {
domain = "<{get domain}>",
production = false,
legacy_url_cutoff = 144,
approot = "<{get approot}>"
}
config.db = config.approot .. "data/posts.db"
return config

View File

@ -1,4 +1,5 @@
--[[
--[[ md
@name lua/db
Does most of the database interaction.
Notably, holds a connection to the open sqlite3 database in .conn
]]
@ -9,9 +10,34 @@ local config = require("config")
local db = {}
--[[
--[[ md
@name lua/db/sqlassert
Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success.
Parameters:
0. r - {{lsqlite/stmnt}} | {{lua/nil}} - The userdata returned from
{{lsqlite/db/prepare}}
0. errcode - {{lua/nil}} | {{lua/number}} - If the first argument back from
{{lsqlite/db/prepare}} is nil, this second argument is a numeric errorcode,
see {{lsqlite/errcodes}}
0. err - {{lua/nil}} | {{lua/string}} - The string error returned from
{{lsqlite/db/prepare}}. Only non-nil if the first return value was nil. A
string message describing what went wrong in the statment. If this argument is
also {{lua/nil}}, this function retrives the error mssage from
{{lsqlite/db/errmsg}}.
Returns:
0. r - {{lua/userdata}} | {{lua/nil}} - The first argument passed in. Used so
that error checking and assignment can all be done on a single line.
Example:
> db = require("db")
> query = db.sqlassert(db.conn:parepare("SELECT 'Hello, world!'"))
]]
function db.sqlassert(r, errcode, err)
if not r then
@ -24,8 +50,27 @@ function db.sqlassert(r, errcode, err)
return r
end
--[[
Continuously tries to perform an sql statement until it goes through
--[[ md
@name lua/db/do_sql
Continuously tries to perform an sql statement until it goes through. This function may call {{lua/coroutine/yield}}
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The userdata returned form {{lsqlite/db/prepare}}
Returns:
0. err - {{lua/number}} - The error code returned from running the statement. Will be `lsqlite.OK` on success, see {{lsqlite/errcodes}}
Example:
> sql = require("lsqlite3")
> configure = function(...) end -- Mock smr environment
> db = require("db")
> configure()
> query = db.conn:prepare("SELECT 'Hello, world!';")
> assert(db.do_sql(query))
]]
function db.do_sql(stmnt)
if not stmnt then error("No statement",2) end
@ -42,11 +87,33 @@ function db.do_sql(stmnt)
return err
end
--[[
Provides an iterator that loops over results in an sql statement
or throws an error, then resets the statement after the loop is done.
--[[ md
@name lua/db/sql_rows
Provides an iterator that loops over results in an sql statement or throws an
error, then resets the statement after the loop is done.
Returned iterator returns varargs, so the values can be unpacked in-line in the
for loop.
for loop. This statement is approximately the same as {{sqlite/stmt/rows}}, but
may yield when the db connection is busy, and continue execution when the
connection is free again.
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The userdata returned from {{sqlite/db/prepare}}
Returns:
0. iterator - {{lua/iterator}} - The iterator function that returns varargs of the returns from the sql statement.
Example:
> db = require("db")
> query = db.conn:prepare("SELECT 'Hello, world!';")
> for row in db.sql_rows(query) do
> print(row)
> end
Hello, world!
]]
function db.sql_rows(stmnt)
if not stmnt then error("No statement",2) end
@ -75,7 +142,9 @@ function db.sql_rows(stmnt)
end
--[[
Binds an argument to as statement with nice error reporting on failure
@name lua/db/sqlbind
Binds an argument to as statement with nice error reporting on failure. Approximatly the same as {{lsqlite/stmt/bind_names}}, but with better error reporting.
stmnt :: sql.stmnt - the prepared sql statemnet
call :: string - a string "bind" or "bind_blob"
position :: number - the argument position to bind to
@ -85,7 +154,15 @@ function db.sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
local f = stmnt[call](stmnt,position,data)
if f ~= sql.OK then
error(string.format("Failed call %s(%d,%q): %s", call, position, data, db.conn:errmsg()),2)
local errs = string.format(
"Failed call %s(%d,%q): %s",
call,
position,
data,
db.conn:errmsg()
)
log(LOG_ERR,errs)
error(errs,2)
end
end
@ -94,39 +171,25 @@ end
local oldconfigure = configure
db.conn = db.sqlassert(sql.open(config.db))
function configure(...)
--Create sql tables
assert(db.conn:exec(queries.create_table_authors))
--Create a fake "anonymous" user, so we don't run into trouble
--so that no one runs into trouble being able to paste under this account.
assert(db.conn:exec(queries.insert_anon_author))
--If/when an author deletes their account, all posts
--and comments by that author are also deleted (on
--delete cascade) this is intentional. This also
--means that all comments by other users on a post
--an author makes will also be deleted.
--
--Post text uses zlib compression
assert(db.conn:exec(queries.create_table_posts))
--Store the raw text so people can download it later, maybe
--we can use it for "download as image" or "download as pdf"
--in the future too. Stil stored zlib compressed
assert(db.conn:exec(queries.create_table_raw_text))
--Maybe we want to store images one day?
assert(db.conn:exec(queries.create_table_images))
--Comments on a post
assert(db.conn:exec(queries.create_table_comments))
--Tags for a post
assert(db.conn:exec(queries.create_table_tags))
--Index for tags
assert(db.conn:exec(queries.create_index_tags))
--Store a cookie for logged in users. Logged in users can edit
--their own posts, and edit their biographies.
assert(db.conn:exec(queries.create_table_session))
local statements = {
"create_table_authors",
"insert_anon_author",
"create_table_posts",
"create_table_raw_text",
"create_table_images",
"create_table_comments",
"create_table_tags",
"create_index_tags",
"create_table_session"
}
-- ipairs() needed, "create table authors" must be executed before
-- "insert anon author"
for _, statement in ipairs(statements) do
db.sqlassert(db.conn:exec(queries[statement]))
end
return oldconfigure(...)
end
configure()
function db.close()
db.conn:close()

View File

@ -15,7 +15,6 @@ local stmnt_author_create
--a while, but whatever.
local oldconfigure = configure
function configure(...)
stmnt_author_create = db.sqlassert(db.conn:prepare(queries.insert_author))
return oldconfigure(...)
end

View File

@ -35,7 +35,7 @@ local function get_site_home(req, loggedin)
isanon = tonumber(iar) == 1,
posted = os.date("%B %d %Y",tonumber(dater)),
author = author,
tags = libtags.get(idr),
taglist = libtags.get(idr),
hits = hits,
ncomments = cmts
}

View File

@ -5,6 +5,7 @@ local util = require("util")
local session = require("session")
local config = require("config")
local pages = require("pages")
local api = require("hooks")
local stmnt_author_acct
@ -18,47 +19,43 @@ function configure(...)
return oldconfigure(...)
end
local old_authenticate = api.authenticate
function api.authenticate(data)
stmnt_author_acct:bind_names{name=data.user}
local err = db.do_sql(stmnt_author_acct)
if err ~= sql.ROW then
stmnt_author_acct:reset()
log(LOG_NOTICE,string.format("User %q failed to log in",data.user))
end
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
stmnt_author_acct:reset()
local hash = sha3(salt .. data.pass)
if hash == passhash then
return id
end
end
local function login_post(req)
--Try to log in
http_populate_multipart_form(req)
local name = assert(http_argument_get_string(req,"user"))
local pass = assert(http_file_get(req,"pass"))
stmnt_author_acct:bind_names{
name = name
}
local text
local err = db.do_sql(stmnt_author_acct)
if err == sql.ROW then
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
stmnt_author_acct:reset()
local todigest = salt .. pass
local hash = sha3(todigest)
if hash == passhash then
local mysession = session.start(id)
local domain_no_port = config.domain:match("(.*):.*") or config.domain
http_response_header(req,"set-cookie",string.format(
[[session=%s; SameSite=Lax; Path=/; Domain=%s; HttpOnly; Secure]],mysession,domain_no_port
))
local loc = string.format("https://%s.%s",name,config.domain)
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
else
text = pages.login{
err = "Incorrect username or password"
}
end
elseif err == sql.DONE then --Allows user enumeration, do we want this?
--Probably not a problem since all passwords are forced to be "good"
stmnt_author_acct:reset()
text = pages.login{
err = "Failed to find user:" .. name
}
else
stmnt_author_acct:reset()
error("Other sql error during login")
local uid, err = api.authenticate({user=name,pass=pass})
if not uid then
http_response(req,200,pages.login{err=err})
return
end
http_response(req,200,text)
local user_session = session.start(uid)
local domain_no_port = config.domain:match("(.*):.*") or config.domain
local cookie_string = string.format(
[[session=%s; SameSite=Lax; Path=/; Domain=%s; HttpOnly; Secure]],
user_session,
domain_no_port
)
http_response_header(req,"set-cookie",cookie_string)
local loc = string.format("https://%s.%s",name,config.domain)
http_response_header(req,"Location",loc)
http_response(req,303,"")
end
return login_post

View File

@ -102,7 +102,7 @@ local function author_paste(req,ps)
text = ps.text
}
end
local asanon = assert(http_argument_get_string(req,"pasteas"))
local asanon = assert(http_argument_get_string(req,"pasteas") or "anonymous")
local textsha3 = sha3(ps.text .. get_random_bytes(32))
--No need to check if the author is posting to the
--"right" sudomain, just post it to the one they have

View File

@ -14,9 +14,9 @@ local stmnt_read, stmnt_update_views, stmnt_comments
local oldconfigure = configure
function configure(...)
stmnt_read = assert(db.conn:prepare(queries.select_post))
stmnt_update_views = assert(db.conn:prepare(queries.update_views))
stmnt_comments = assert(db.conn:prepare(queries.select_comments))
stmnt_read = db.sqlassert(db.conn:prepare(queries.select_post))
stmnt_update_views = db.sqlassert(db.conn:prepare(queries.update_views))
stmnt_comments = db.sqlassert(db.conn:prepare(queries.select_comments))
return oldconfigure(...)
end

View File

@ -28,6 +28,18 @@ local pagenames = {
"parts/story_breif",
"parts/taglist"
}
--Functions available to all templates
local global_env = {
include = function(filename)
local fp = assert(io.open(filename,"r"))
local data = assert(fp:read("*a"))
fp:close()
return data
end,
}
local global_env_m = {
__index=global_env
}
local pages = {}
for k,v in pairs(pagenames) do
local path = string.format(config.approot .. "pages/%s.etlua",v)
@ -46,6 +58,12 @@ for k,v in pairs(pagenames) do
assert(func, "Failed to load " .. path)
pages[v] = function(env)
assert(type(env) == "table","env must be a table")
-- Add our global metatable functions at the bottom metatable's __index
local cursor = env
while cursor ~= nil and getmetatable(cursor) and getmetatable(cursor).__index do
cursor = getmetatable(cursor)
end
setmetatable(cursor,global_env_m)
local buff, err = parser:run(func,env)
if not buff then
errorf("Failed to render %s : %s", path, err)

View File

@ -4,12 +4,13 @@ local db = require("db")
local util = require("util")
local queries = require("queries")
local oldconfigure = configure
local stmnt_get_session, stmnt_insert_session, stmnt_delete_session
local oldconfigure = configure
function configure(...)
stmnt_get_session = assert(db.conn:prepare(queries.select_valid_sessions))
stmnt_insert_session = assert(db.conn:prepare(queries.insert_session))
stmnt_delete_session = assert(db.conn:prepare(queries.delete_session))
stmnt_get_session = db.sqlassert(db.conn:prepare(queries.select_valid_sessions))
stmnt_insert_session = db.sqlassert(db.conn:prepare(queries.insert_session))
stmnt_delete_session = db.sqlassert(db.conn:prepare(queries.delete_session))
return oldconfigure(...)
end
@ -59,7 +60,7 @@ function session.start(who)
}
local err = db.do_sql(stmnt_insert_session)
stmnt_insert_session:reset()
assert(err == sql.DONE)
assert(err == sql.DONE, "Error should have been 'DONE', was: " .. tostring(err))
return session
end

View File

@ -1,3 +1,14 @@
--[[ md
@name lua/tags
Helper methods for cleaning story tags.
Tags are the main way to search smr, a simple `+<tag>` or `-<tag>` will show all
stories that include (+) or do not include (-) a particular tag.
Tags are stored in the {{table_tags}} and are deleted if the story they are
attached to is deleted. If an author is deleted, all their stories are deleted,
and this will cascade to deleting tags on their stories too.
]]
local sql = require("lsqlite3")
local db = require("db")

View File

@ -1,3 +1,9 @@
--[[ md
@name lua/util
Various utilities that aren't big enough for their own module, but are still
used in more than one place.
]]
local sql = require("lsqlite3")
local config = require("config")
@ -15,8 +21,26 @@ function configure(...)
return oldconfigure(...)
end
--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
--no underscore because we use that for our operative pages
--[[ md
@name doc/url_spec
URLs generated from smr use letters and numbers to encode a monotonically
increasing post id into a url that can easily be shared (and ends up
considerably shorter). The characters used in url generation are:
[a-z][A-Z][0-9], and numbers are encoded to use the second available 1-character
permuation, then the first available 2-character permutation, and so on.
For example, the first post is encoded as 'b', the second as 'c', the thrid
as 'd', and so on. The off-by-one nature is to simplify implementation of
2-character and 3-character combinations with Lua's 1-indexed arrays.
see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
no underscore because we use that for our operative pages
A set of legacy characters that are no longer in use (because they were invalid
to use in URL's) is also defined, but unused as long as
{{config/legacy_url_cutoff}} is set to 0.
]]
local url_characters =
[[abcdefghijklmnopqrstuvwxyz]]..
[[ABCDEFGHIJKLMNOPQRSTUVWXYZ]]..
@ -34,8 +58,11 @@ local url_characters_rev_legacy = {}
for i = 1,string.len(url_characters_legacy) do
url_characters_rev_legacy[string.sub(url_characters_legacy,i,i)] = i
end
--[[
Encode a number to a shorter HTML-safe url path
--[[ md
@name lua/util/encode_id
Encode a number to a shorter HTML-safe url path. Url paths are generated
according to the {{doc/url_spec}
]]
function util.encode_id(number)
local result = {}

View File

@ -1,27 +0,0 @@
<% assert(author,"No author specified") %>
<% assert(bio,"No bio included") %>
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="content">
<form action="https://<%= author %>.<%= domain %>/" method="post" class="container">
<textarea name="" cols=80 rows=24 class="column">
<%= bio %>
</textarea><br/>
<input type="submit">
</form>
</div>
<div class="content">
<% if #stories == 0 then %>
This author has not made any pastes yet.
<% else %>
<table>
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,39 +0,0 @@
<% assert(author,"No author specified") %>
<% assert(bio,"No bio included") %>
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<% if not loggedin then %>
<a href="/_login" class="button column column-0">Log in</a>
<% else %>
<a href="/_logout" class="button column column-0">Log out</a>
<a href="/_bio" class="button column column-0">Edit bio</a>
<% end %>
<span class="column column-0"></span>
<{system cat src/pages/parts/search.etlua}>
</div>
</div>
<% if bio ~= "" then %>
<div class="container">
<blockquote class="biography">
<%- bio %>
</blockquote>
</div>
<% end %>
<div class="content">
<% if #stories == 0 then %>
This author has not made any pastes yet.
<% else %>
<table>
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,34 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_paste" method="post" class="container">
<fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-70"></input>
<select id="pasteas" name="pasteas" class="column column-10">
<option value="<%= user %>"><%= user %></option>
<option value="anonymous">Anonymous</option>
</select>
<select id="markup" name="markup" class="column column-10">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-10">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>
</div>
<div class="row">
<input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input>
</div>
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,11 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
&#128577;
</h1>
<div class="container">
<p>
You don't have permission to edit: <%= path %>
</p>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,20 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Register
</h1>
Once you press submit, you will be prompted to download a file.<br/>
slash.monster uses this file in place of a password, keep it safe.<br/>
Consider keeping a copy on a USB in case your hard drive fails.<br/>
The admin cannot recover your passfile, and will not reset accounts.<br/>
<b>Names may be up to 30 characters, alphanumeric, no symbols, all lower case.</b><br/>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="/_claim" method="post">
<fieldset>
<label for="user">Name:</label>
<input type="text" name="user" id="user" placeholder="name">
<input type="submit">
</fieldset>
</form>
Once you have your file, you can <a href="/_login">log in</a>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,48 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_edit" method="post" class="container">
<fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-60" value="<%= title %>"></input>
<input type="hidden" name="story" value="<%= story %>">
<select id="pasteas" name="pasteas" class="column column-10">
<% if isanon then %>
<option value="<%= user %>"><%= user %></option>
<option value="anonymous" selected>Anonymous</option>
<% else %>
<option value="<%= user %>" selected><%= user %></option>
<option value="anonymous">Anonymous</option>
<% end %>
</select>
<select id="markup" name="markup" class="column column-10">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-20">
<label for="unlisted" class="label-inline">Unlisted</label>
<input
type="checkbox"
name="unlisted"
id="unlisted"
<% if unlisted then %>
checked
<% end %>
>
</input>
</div>
</div>
<div class="row">
<input type="text" name="tags" value="<%= tags %>" placeholder="Tags (semicolon;seperated)" class="column"></input>
</div>
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,18 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Edit Biography for <%= user %>
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_bio" method="post" class="container">
<fieldset>
<input type="hidden" name="author" value="<%= user %>">
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<div class="row">
<input type="submit">
</div>
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,14 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<% if errcode then -%><%= errcode -%><% end -%>:
<% if errcodemsg then -%><%= errcodemsg -%><% end %>
</h1>
<% if explanation then -%><%= explanation -%><% end -%>
<% if should_traceback then %>
<code><pre>
<%- debug.traceback() %>
</pre></code>
<% end %>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,36 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title column">
<a href="https://<%= domain %>">
<%= domain %>
</a>
</h1>
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<% if not loggedin then %>
<a href="/_login" class="button column column-0">Log in</a>
<a href="/_claim" class="button column column-0">Register</a>
<% else %>
<a href="/_logout" class="button column column-0">Log out</a>
<span class="column column-0"></span>
<% end %>
<{system cat src/pages/parts/search.etlua}>
</div>
<p>
<{ system cat src/pages/parts/motd.etlua }>
</p>
</div>
<div class="content">
<% if #stories == 0 then %>
No stories available.
<% else %>
<table id="story_list">
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,15 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Login
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="/_login" method="post" enctype="multipart/form-data">
<fieldset>
<label for="user">Name:</label>
<input type="text" name="user" id="user" placeholder="name" autocorrect="off" autocapitalize="none">
<label for="pass">Passfile:</label>
<input type="file" name="pass" id="pass">
<input type="submit" value="Log In"/>
</fieldset>
</form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,11 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
&#128577;
</h1>
<div class="container">
<p>
No author found: <%= author %>
</p>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,10 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
&#128577;
</h1>
<div class="container">
<p>
No story found: <%= path %>
</p>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,27 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= domain %>/_paste" method="post" class="container"><fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-60"></input>
<select id="markup" name="markup" class="column column-20">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-20">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>
</div>
<div class="row">
<input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input>
</div>
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset></form>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,84 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<% local api = require("hooks") %>
<nav>
<a href="https://<%= domain %>"><%= domain %></a>/<a href="https://<%= domain %>/<%= short %>"><%= short %></a>
</nav>
<% if owner then -%>
<div class="row">
<% for _, spec in ipairs(api.get.page_owner(getfenv(1))) do %>
<form action="<%= spec.endpoint %>" method="<%= spec.method %>"><fieldset>
<% for key, value in pairs(spec.fields) do %>
<input type="hidden" name="<%= key %>" value="<%= value %>"/>
<% end %>
<input type="submit" value="<%= spec.text %>" class="button column column-0"/>
</fieldset></form>
<% end %>
</div>
<% end -%>
<article>
<h2 class="title"> <%- title %> </h2>
<h3>
<% if isanon or author == nil then -%>
By Anonymous
<% else -%>
By <a href="https://<%= author %>.<%= domain %>"><%= author %></a>
<% end -%>
</h3>
<ul class="tag-list">
<% for _,tag in pairs(tags) do -%>
<{system cat src/pages/parts/taglist.etlua}>
<% end -%>
</ul>
<%- text %>
</article>
<hr/>
<p><%= views %> Hits, <%= #comments %> Comments</p>
<div class="row">
<% for _, spec in ipairs(api.get.page_reader(getfenv(1))) do %>
<form action="<%= spec.endpoint %>" method="<%= spec.method %>"><fieldset>
<% for key, value in pairs(spec.fields) do %>
<input type="hidden" name="<%= key %>" value="<%= value %>"/>
<% end %>
<input type="submit" value="<%= spec.text %>" class="button">
</fieldset></form>
<% end %>
</div>
<form action="https://<%= domain %>/<%= short %>" method="POST">
<% if unlisted then %>
<input type="hidden" name="pwd" value="<%= hashstr %>"/>
<% end %>
<textarea name="text" cols=60 rows=10 class="column"></textarea>
</div><% if iam then %>
<select id="postas" name="postas">
<option value="Anonymous">Anonymous</option>
<option value="<%= iam %>"><%= iam %></option>
</select>
<input type="submit" value="post" class="button">
<% else %>
<input type="hidden" name="postas" value="Anonymous">
<input type="submit" value="post" class="button">
<% end %>
</form>
<% if comments and #comments == 0 then %>
<p><i>No comments yet</i></p>
<% else %>
<section>
<% for _,comment in pairs(comments) do %>
<article>
<% if comment.isanon then %>
<p><b>Anonymous</b></p>
<% else %>
<p><b><%= comment.author %></b></p>
<% end %>
<p><%= comment.text %></p>
</article>
<% end %>
</section>
<% end %>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,19 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
<a href="https://<%= domain %>"><%= domain %></a>/
</h1>
<div class="row">
<{system cat src/pages/parts/search.etlua}>
</div>
<div class="content">
<% if #results == 0 then %>
No stories matched your search.
<% else %>
<table>
<% for k,story in pairs(results) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>
</table>
<% end %>
</div>
<{system cat src/pages/parts/footer.etlua}>

View File

@ -1,56 +0,0 @@
SELECT
posts.id,
posts.post_title,
posts.isanon,
authors.name,
posts.post_time,
posts.views,
COUNT(comments.id)
FROM
posts,authors
LEFT JOIN comments ON comments.postid = posts.id
WHERE
authors.id = posts.authorid
AND posts.unlisted = 0
<% for field, values in pairs(result) do -%>
<% for _,value in pairs(values) do -%>
<% local pn,expr,value = unpack(value) -%>
<% local n = (pn == "+" and "" or "NOT") -%>
<% if field == "title" then -%>
AND <%= n %> posts.post_title LIKE ?
<% elseif field == "author" then -%>
AND <%= n %> authors.name LIKE ?
<% elseif field == "date" then -%>
AND <%= n %> posts.post_time <%- expr %> ?
<% elseif field == "hits" then -%>
AND posts.views <%- expr -%> ?
<% end -%>
<% end -%>
<% end -%>
GROUP BY
posts.id
<% for _,tag in pairs(result.tags) do -%>
INTERSECT
SELECT
posts.id,
posts.post_title,
posts.isanon,
authors.name,
posts.post_time,
posts.views,
COUNT(comments.id)
FROM
posts,authors,tags
LEFT JOIN comments ON comments.postid = posts.id
WHERE
posts.authorid = authors.id
AND tags.postid = posts.id
<% local n,v,t = unpack(tag) -%>
<% n = (n == "-" and "NOT" or "") -%>
AND <%= n %> tags.tag = ?
GROUP BY
posts.id
<% end -%>
ORDER BY
posts.post_time DESC
;

View File

@ -33,7 +33,9 @@ int delete(struct http_request *);
int do_lua(struct http_request *req, const char *name);
int errhandeler(lua_State *);
lua_State *L;
/* These should be defined in in kore somewhere and included here */
/*
These should be defined in in kore somewhere and included here
*/
void kore_worker_configure(void);
void kore_worker_teardown(void);
/*
@ -111,7 +113,8 @@ do_lua(struct http_request *req, const char *name){
return do_lua(req,#lua_method);\
}
/***
/* md
@name http/_paste
Called at the endpoint <domain>/_paste.
This method doesn't need any parameters for GET requests.
This method expects the following for POST requests:
@ -121,10 +124,14 @@ This method expects the following for POST requests:
In addition to the normal assets, this page includes
suggest_tags.js, which suggests tags that have been
submitted to the site before.
@function _G.paste
@custom http_method GET POST
*/
/* md
@name lua/paste
This function is called automatically with the request submitted at
<endpoint>/_paste
@param http_request req The request to service
***/
*/
route(post_story,"paste");
/***
@ -180,10 +187,14 @@ route(read_story, "read");
/***
Called at the endpoint <domain>/_login
This method does not requirei any parameters for GET requests.
This method does not require any parameters for GET requests.
This method requiries the following for POST requests:
* user :: [a-z0-9]{1,30} - The username to log in as
* pass :: any - The passfile for this user
To overload login functionality in an addon, see @{api.authenticate}
@function _G.login
@custom http_method GET POST
@param http_request req The request to service.
***/
int
login(struct http_request *req){

View File

@ -1 +1,4 @@
/*
The tags table is indexed on tag, so that search is fast
*/
CREATE INDEX tag_index ON tags(tag);

View File

@ -1,3 +1,11 @@
/* md
@name sql/table/authors
If/when an author deletes their account, all posts
and comments by that author are also deleted (on
delete cascade) this is intentional. This also
means that all comments by other users on a post
an author makes will also be deleted.
*/
CREATE TABLE IF NOT EXISTS authors (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT UNIQUE ON CONFLICT FAIL,

View File

@ -1,3 +1,9 @@
/*
Comments on a post.
When an author deletes their account or the posts this comment
is posted on is deleted, this comment will also be deleted.
*/
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
postid REFERENCES posts(id) ON DELETE CASCADE,

View File

@ -1,3 +1,6 @@
/*
We may want to store images one day. This is unused for now
*/
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT,

View File

@ -1,8 +1,10 @@
/*
Store a cookie for logged in users. Logged in users can edit
their own posts.
their own posts, edit their biographies, and post stories and comment under their own name.
TODO: WE can hash the "key" so that even if the database gets
dumped, a hacker can't cookie-steal with only read access
to the db.
*/
CREATE TABLE IF NOT EXISTS sessions (
key TEXT PRIMARY KEY,
author REFERENCES authors(id) ON DELETE CASCADE,

View File

@ -1,3 +1,7 @@
/*
Tags on a post
A post's tags are deleted if the post is deleted.
*/
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
postid REFERENCES posts(id) ON DELETE CASCADE,