added login, logout, signup page. refined abstractions. added preliminary buttons. added preliminary error screen (will finish tomorrow.) added hashed passwords. added authentication. added users. added death and whatnot aaaaaaghhh kill meeee

This commit is contained in:
n* 2025-03-01 01:12:00 -03:00
parent 34fee31395
commit 9e29afb525
35 changed files with 741 additions and 58 deletions

149
authors.lua Normal file
View file

@ -0,0 +1,149 @@
local authors = {}
local foppy = require "./foppy"
local fs = require "fs"
local json = require "json"
local utils = require "./utils"
authors.from_id = function(id, writeable)
return foppy.open("database/authors/" .. id .. ".json", writeable)
end
authors.id_from_token = function(token, writeable)
local data = foppy.open("database/tokens/" .. token .. ".json", writeable)
if not data then
return nil, 404
end
if data.expires <= os.time() then
data.dead = true
return nil, 401
end
if data.dead then
return nil, 401
end
local exists = fs.existsSync("database/authors/" .. data.of .. ".json")
if not exists then
data.dead = true
return nil, 404
end
return data.of
end
authors.id_from_blog = function(blog, writeable)
local data = foppy.open("database/blogs/" .. blog .. ".json")
if not data then
return nil, 404
end
return data.owner
end
local is_right_password = function(id, password)
local author, code = authors.from_id(id, true)
if not author then
return nil, 404
end
-- check salted password with openssl
local hashed = utils.hash(author.salt .. password)
if author.password ~= hashed then
return nil, 401
end
return author
end
authors.auth = function(id, password)
local author, code = is_right_password(id, password)
if not author then
return nil, code
end
local hash, filename
repeat
hash = utils.uuid()
filename = "database/tokens/" .. hash .. ".json"
until not fs.existsSync(hash)
local token_data = json.encode {
of = id,
expires = os.time() + 2628000, -- 4 months, approximately.
dead = false
}
fs.writeFileSync(filename, token_data)
author.tokens[#author.tokens + 1] = hash
foppy.sync("database/authors/" .. id .. ".json")
return hash
end
authors.revoke_all_tokens = function(id, password)
local author, code = is_right_password(id, password)
if not author then
return nil, code
end
for _, v in ipairs(author.tokens) do
local f = "database/tokens/" .. v .. ".json"
local token = foppy.open(f, true)
token.dead = true
foppy.sync(f, true)
end
author.tokens = {}
foppy.sync("database/authors/" .. id .. ".json")
end
authors.change_password = function(id, password, new_password)
local author, code = is_right_password(id, password)
if not author then
return nil, code
end
author.salt = utils.uuid()
author.password = utils.data(author.salt .. new_password)
foppy.sync("database/authors/" .. id .. ".json")
end
authors.get_available_id = function()
local id
repeat
id = utils.pin()
until not fs.existsSync("database/authors/" .. id .. ".json")
return id
end
authors.create_account = function(password)
local id = authors.get_available_id()
local salt = utils.uuid()
local account_data = json.encode {
blogs = {},
password = utils.hash(salt .. password),
salt = salt,
tokens = {}
}
fs.writeFileSync(
"database/authors/" .. id .. ".json",
account_data
)
return id
end
package.loaded["./authors"] = authors
return authors

View file

@ -1,5 +1,5 @@
local config = { local config = {
base_url = "example.com" base_url = "test0.at.cyrneko.eu"
} }
package.loaded["./config"] = config package.loaded["./config"] = config

1
data.lua Normal file
View file

@ -0,0 +1 @@
-- retrieves a buncha useful data!

View file

@ -0,0 +1 @@
{"blogs":[],"tokens":[],"password":"343feb9dcdab90413da9275282ae0481b084264430ba07a1b254a7b4d7298f11","salt":"3dbd8155-477c-419a-b50e-f74c772e59f5"}

View file

@ -0,0 +1 @@
{"blogs":[],"tokens":[],"password":"01555d563d1976de96843a3d758572a858b82b892f03d45ca225e9fd5d8d81f6","salt":"51a6539a-bee9-4076-bc83-89e171a86844"}

View file

@ -0,0 +1 @@
{"tokens":[],"password":"ff9a2d7497a68b179179f542cbb75aafb9aabcffe3d5335e94a3185144a341a2","salt":"599abac8-4c36-4bd1-8d29-2034dce92fbb","blogs":[]}

View file

@ -0,0 +1 @@
{"tokens":[],"password":"4141faabc6212f8f1894efadbf5eb0ad2c3c76834822a8e380f46d9ad406165b","salt":"e03099c3-0715-4157-827b-6da5e35d4c53","blogs":[]}

View file

@ -0,0 +1 @@
{"password":"713fc8760e23ccaef815965b2e0be0946b24e2d0419eacef308ecfa7e2ed6268","tokens":[],"blogs":[],"salt":"aaa27eb1-e49c-4aa3-b2b7-a3b557cfc58d"}

View file

@ -0,0 +1 @@
{"blogs":[],"tokens":[],"password":"43af369fbf521df0634c37502bdfc6d6e7e20f0611a8c34207b95ea7b3fc1921","salt":"fc186676-5370-497b-a384-15781823bc3a"}

View file

@ -0,0 +1 @@
{"blogs":[],"tokens":[],"password":"50ad6a00e7c93eff9eaf6b05501ca99ce1b7d15988b93c6a545cf49beb22ccf1","salt":"bfb52167-d0f7-404b-88f5-a2480a11d1d5"}

View file

@ -1,6 +1,7 @@
{ {
"name": "n-asterisk", "name": "n-asterisk",
"biography": "hi, i am n*, we're a system of two, we love eachother and we love to code, hell yeah mf.", "description": "hi, i am n*, we're a system of two, we love eachother and we love to code, hell yeah mf.",
"owner": "1234",
"index": [ "index": [
{ {

View file

@ -1,4 +1,8 @@
{ {
"maker": "3819",
"real_name": "why-napkin",
"title": "why napkin?",
"versions": [ "versions": [
{ {
"contents": "<h1>Why napkin?</h1>napkin is cool, that's it, really.", "contents": "<h1>Why napkin?</h1>napkin is cool, that's it, really.",

1
database/redirects.json Normal file
View file

@ -0,0 +1 @@
[{ "from": "abcd", "to": "/@n-asterisk/why-napkin" }]

View file

@ -0,0 +1 @@
{"of":"6301","expires":1743423851,"dead":false}

View file

@ -0,0 +1 @@
{"expires":1743422868,"dead":false,"of":"5698"}

View file

@ -0,0 +1 @@
{"of":"0091","expires":1743429866,"dead":false}

View file

@ -0,0 +1 @@
{"expires":1743423490,"dead":false,"of":"5663"}

View file

@ -0,0 +1 @@
{"of":"8870","expires":1743423679,"dead":false}

View file

@ -0,0 +1 @@
{"expires":1743423622,"dead":false,"of":"3919"}

View file

@ -0,0 +1 @@
{"of":"3437","expires":1743425720,"dead":false}

0
errors.lua Normal file
View file

View file

@ -2,7 +2,8 @@ local json = require "json"
local fs = require "fs" local fs = require "fs"
local tick_precision = 2 -- tick each N seconds (decimal) local tick_precision = 2 -- tick each N seconds (decimal)
local timeout = 30 -- 30 seconds local timeout = 60 * 10 -- 10 minutes
local write_timeout = 30
local foppy = {} local foppy = {}
foppy.cache = {} foppy.cache = {}
@ -11,9 +12,7 @@ foppy.open = function(file, writeable)
local c = foppy.cache[file] local c = foppy.cache[file]
if c then if c then
c.timeout = timeout c.timeout = timeout
if writeable then c.write_timeout = writeable and write_timeout
c.written_to = true
end
return c.data return c.data
end end
@ -25,9 +24,8 @@ foppy.open = function(file, writeable)
local data = json.decode(raw) local data = json.decode(raw)
c = { c = {
timeout = timeout, timeout = timeout,
data = data, write_timeout = writeable and write_timeout,
file = file, data = data
written_to = writeable
} }
foppy.cache[file] = c foppy.cache[file] = c
@ -40,15 +38,50 @@ foppy.setup = function()
timer.setInterval(tick_precision * 1000, function() timer.setInterval(tick_precision * 1000, function()
for name, object in pairs(foppy.cache) do for name, object in pairs(foppy.cache) do
object.timeout = object.timeout - tick_precision object.timeout = object.timeout - tick_precision
object.write_timeout = object.write_timeout - tick_precision
if object.timeout <= 0 then if object.timeout <= 0 then
foppy.cache[name] = nil foppy.cache[name] = nil
object.write_timeout = 0
end
if foppy.written_to then if object.write_timeout <= 0 then
fs.writeFileSync(object.file, json.encode(object.data)) fs.writeFileSync(name, json.encode(object.data))
end
end end
end end
end) end)
-- TODO: Finish this.
--[[
local process = require("process").globalProcess()
local hecc_everything = function()
print("foppy says hecc!!!")
fs.writeFileSync("hecc", "hecc")
for name, object in pairs(foppy.cache) do
if object.write_timeout then
fs.writeFileSync(name, json.encode(object.data))
end
end
end
process:on("sigint", hecc_everything)
process:on("sigterm", hecc_everything)
process:on("sighup", hecc_everything)
process:on("sigquit", hecc_everything)
]]
end
foppy.sync = function(what, close)
local object = foppy.cache[what]
if object then
if object.written_to then
fs.writeFileSync(what, json.encode(object.data))
end
if close then
foppy.cache[what] = nil
end
end
end end
package.loaded["./foppy"] = foppy package.loaded["./foppy"] = foppy

View file

@ -3,55 +3,70 @@ local json = require("json")
local radon = require("./radon") local radon = require("./radon")
local foppy = require("./foppy") local foppy = require("./foppy")
local config = require("./config") local config = require("./config")
local authors = require("./authors")
local utils = require("./utils")
local templater = function(template) local templater = function(template)
return function(req, res) return function(req, res)
local func = dofile("template/" .. template .. ".lua") local func = dofile("template/" .. template .. ".lua")
p(req)
res.code = 200 res.code = 200
res.body = radon.compile(func, { res.body = radon.compile(func, {
request = req, request = req,
response = res, response = res
base_url = "example.com/"
}) })
res.headers["Content-Type"] = "text/html" res.headers["Content-Type"] = "text/html"
end end
end end
local api_wrapper = function()
end
weblit.app weblit.app
.bind({ host = "0.0.0.0", port = 1337 }) .bind({ host = "0.0.0.0", port = 1337 })
-- Configure weblit server -- Configure weblit server
.use(weblit.logger) .use(weblit.logger)
.use(weblit.autoHeaders) .use(weblit.autoHeaders)
.use(utils.cookies)
.route( .route(
{ path = "/static/:path:" }, { path = "/static/:path:" },
weblit.static("static") weblit.static("static")
) )
.route({ path = "/api/signup", method = "POST" }, function(req, res) .route(
local body = json.decode(req.body) { path = "/media/:path:" },
weblit.static("database/media")
)
if body == nil or type(body) ~= "table" then -- TODO: CLONE ALL OF THIS AS A REST API EVENTUALLY,
res.code = 400 .route({ path = "/signup" }, templater("signup"))
res.body = "Bad Request: Invalid request body" .route({ path = "/signup2" }, templater("signup2"))
.route({ path = "/login" }, templater("login"))
.route({ path = "/logout" }, templater("logout"))
.route({ path = "/@:blog" }, templater("blog"))
.route({ path = "/@:blog/:post" }, templater("post"))
.route({ path = "/p/:path" }, function(req, res)
local path = req.params.path
if #path == 0 then
res.code = 301
res.headers["Location"] = "/"
return return
end end
res.code = 200 local func = dofile("template/error.lua")
res.body = "Signup successful" local redirects = foppy.open("database/redirects.json")
assert(redirects)
local redirect = redirects[path]
if redirect then
res.code = 301
res.headers["Location"] = redirect
return
end
return func(404, "post", "/p/" .. path)
end) end)
.route({ path = "/p/:post" }, function() end)
.route({ path = "/@:blog" }, templater("blog"))
.route({ path = "/@:blog/:post" }, templater("post"))
.route({ path = "/" }, templater("index")) .route({ path = "/" }, templater("index"))
-- Start the server -- Start the server

View file

@ -14,7 +14,10 @@ local tag = function(name)
if t == "table" then if t == "table" then
for key, value in pairs(content) do for key, value in pairs(content) do
if type(key) == "string" then if type(key) == "string" then
data = data .. ' ' .. key .. '="' .. value .. '"' data = data .. ' ' .. key
if value ~= true then
data = data .. '="' .. tostring(value) .. '"'
end
end end
end end

View file

@ -32,27 +32,39 @@ p {
margin: 0; margin: 0;
} }
input,
button {
padding: 5px 9px;
}
input,
button, button,
.button { .button {
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 10px;
background-color: white; background-color: white;
display: flex;
gap: 5px; gap: 5px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-decoration: none; text-decoration: none;
} }
input,
button:hover, button:hover,
.button:hover { .button:hover {
border: 1px solid black; border: 1px solid black;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.action-button {
color: black;
text-decoration: none;
font-size: 1.5em;
}
/* post */ /* post */
.post { .post {
text-align: left; text-align: left;
padding: 10px;
} }
.post-list { .post-list {
@ -73,7 +85,7 @@ button:hover,
font-family: monospace; font-family: monospace;
} }
.biography { .description {
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 10px; padding: 10px;
text-align: justify; text-align: justify;
@ -89,11 +101,30 @@ section > h1 {
font-size: 2em; font-size: 2em;
} }
.tags-list { .tags-list,
.gapped-row {
gap: 15px; gap: 15px;
display: flex; display: flex;
} }
.gapped-column {
gap: 15px;
display: flex;
flex-direction: column;
}
.info-card {
background-color: rgba(208, 134, 39, 0.21);
padding: 10px 15px;
border: 1px solid rgba(208, 134, 39, 0.21);
}
.error-card {
background-color: rgba(255, 204, 204, 0.6);
padding: 10px 15px;
border: 1px solid rgba(255, 204, 204, 0.6);
}
/* /*
.tag { .tag {
border: 1px solid #ccc; border: 1px solid #ccc;
@ -108,9 +139,15 @@ section > h1 {
} }
.top-navigator { .top-navigator {
gap: 5px;
display: flex; display: flex;
flex-direction: row; justify-content: space-between;
align-items: center;
height: 1em;
}
.top-navigator-actions {
display: flex;
gap: 10px;
} }
.row { .row {
@ -126,3 +163,13 @@ br {
.centered { .centered {
text-align: center; text-align: center;
} }
.justified {
text-align: justify;
}
.ph {
vertical-align: middle;
padding: 0;
margin: 0;
}

View file

@ -52,11 +52,11 @@ return function(data)
end end
-- return our whole flippin html document -- return our whole flippin html document
return wrapper({ title = handle, url = handle }) { return wrapper(data) {
-- render header! -- render header!
section { section {
h1(blog.name), h1(blog.name),
p { class = "biography", blog.biography } p { class = "description", blog.description }
}, },
-- render the pinned post list -- render the pinned post list

View file

@ -2,11 +2,12 @@
local wrapper = require "template.wrapper" local wrapper = require "template.wrapper"
return function() return function(data)
return wrapper({ title = "index!" }) { return wrapper(data) {
h1 "welcome to napkin!",
section { section {
h1 "welcome to napkin!", "the unfunkiest place on the webs"
p "This is the index page."
}, },
a { a {

61
template/login.lua Normal file
View file

@ -0,0 +1,61 @@
---@diagnostic disable:undefined-global
local wrapper = require "template.wrapper"
local phosphor = require "template.phosphor"
return function(data)
return wrapper(data) {
h1 { phosphor("user"), " log into your account!" },
section {
class = "justified",
p [[
heya! welcome again!
]],
p [[
you can log-in either using the number we provided you
or the name of one of your blogs
]],
p [[
remember! if you forget both your number and blog names,
you're toast! so please write them down or something.
]]
},
section {
form {
action = "/login2",
method = "POST",
class = "gapped-row",
input {
name = "user",
id = "user",
minlength = 8,
maxlength = 16,
pattern = "(^@[a-zA-Z0-9_.]+$)|(^\\d{4}$)",
required = true,
placeholder = "identifier/blog goes here",
},
input {
type = "password",
name = "password",
id = "password",
minlength = 8,
maxlength = 16,
pattern = "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&]).{8,}",
required = true,
placeholder = "password goes here",
},
button "log in!",
},
a {
href = "/signup",
"don't have an account?"
}
},
}
end

56
template/login2.lua Normal file
View file

@ -0,0 +1,56 @@
---@diagnostic disable:undefined-global
local wrapper = require "template.wrapper"
local utils = require "./utils"
local authors = require "./authors"
local phosphor = require "template.phosphor"
return function(data)
if data.request.cookies.auth_token then
data.response.code = 301
data.response.headers["Location"] = "/"
return ""
end
local form = utils.parse_form(data.request.body or "")
if not (form.password and form.confirm_password) or
form.password ~= form.confirm_password or
not utils.is_valid_password(form.password) then
data.response.code = 301
data.response.headers["Location"] = "/signup"
return ""
end
local id = authors.create_account(form.password)
local token = authors.auth(id, form.password)
-- TODO: Add Secure;!
data.response.headers["Set-Cookie"] = "auth_token=" .. token
.. "; Path=/; HttpOnly; SameSite=Strict"
data.request.cookies.auth_token = token
return wrapper(data) {
h1 { phosphor("identification-card"), " your identifier is ", id, "!" },
section {
p [[
that's your identifier! please do not lose it! it's important!
it's what allows us to tell you apart from everyone else.
]],
p [[
thanks for joining us! we hope you enjoy your stay, you rock!
]],
},
p {
"you can begin by ",
a {
href = "/new-blog",
"making a blog"
}
}
}
end

6
template/logout.lua Normal file
View file

@ -0,0 +1,6 @@
return function(data)
data.response.headers["Set-Cookie"] =
"auth_token=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; HttpOnly"
data.response.headers["Location"] = "/"
data.response.code = 302
end

View file

@ -9,6 +9,7 @@ local foppy = require "./foppy"
return function(data) return function(data)
-- if the blog name is empty, then redirect to the homepage -- if the blog name is empty, then redirect to the homepage
-- todo: do this on the actual router (main.lua) -- todo: do this on the actual router (main.lua)
local post_name = data.request.params.post
local blog_name = data.request.params.blog local blog_name = data.request.params.blog
if #blog_name == 0 then if #blog_name == 0 then
data.response.code = 301 data.response.code = 301
@ -17,24 +18,24 @@ return function(data)
end end
local blog_path = "database/blogs/" .. blog_name .. ".json" local blog_path = "database/blogs/" .. blog_name .. ".json"
local handle = "@" .. blog_name
local url = handle .. "/" .. post_name
-- attempt to read the blog -- attempt to read the blog
local blog = foppy.open(blog_path) local blog = foppy.open(blog_path)
-- could find no darn blog!!! -- could find no darn blog!!!
if not blog then if not blog then
return error(404, "blog", handle) -- so, fuck it, error screen. return error(404, "blog", url) -- so, fuck it, error screen.
end end
-- if the post name is empty, then redirect to the blog page -- if the post name is empty, then redirect to the blog page
-- todo: do this on the actual router (main.lua) -- todo: do this on the actual router (main.lua)
local post_name = data.request.params.post
if #post_name == 0 then if #post_name == 0 then
data.response.code = 302 data.response.code = 302
data.response.headers["Location"] = "/@" .. blog_name data.response.headers["Location"] = "/@" .. blog_name
return "" return ""
end end
local url = "@" .. blog_name .. "/" .. post_name
local post_path = "database/blogs/" .. blog_name .. "/" .. post_name .. ".json" local post_path = "database/blogs/" .. blog_name .. "/" .. post_name .. ".json"
local post = foppy.open(post_path) local post = foppy.open(post_path)
@ -51,12 +52,13 @@ return function(data)
end end
-- return our whole flippin html document -- return our whole flippin html document
return wrapper({ title = handle, url = url }) { return wrapper(data) {
section { section {
div { div {
class = "post-content", class = "post-content",
latest_version.contents, latest_version.contents,
}, },
div(function(o) div(function(o)
o.class = "tags-list" o.class = "tags-list"
table.insert(o, phosphor("tag")) table.insert(o, phosphor("tag"))
@ -68,6 +70,12 @@ return function(data)
"#" .. tag "#" .. tag
}) })
end end
table.insert(o, a {
style = "text-align: right;",
href = "/" .. handle,
handle
})
end) end)
}, },

88
template/signup.lua Normal file
View file

@ -0,0 +1,88 @@
---@diagnostic disable:undefined-global
local wrapper = require "template.wrapper"
local phosphor = require "template.phosphor"
return function(data)
if data.request.cookies.auth_token then
data.response.code = 301
data.response.headers["Location"] = "/"
return ""
end
return wrapper(data) {
h1 { phosphor("user-plus"), " get an account!" },
section {
class = "justified",
p [[
to create your own blogs and share your thoughts with the world you
will need to create an account right here!
]],
p [[
the way that napkin accounts work is a bit different, you will need
to provide a password and then we will give you a pin code, which
will serve as your credentials, don't forget it!
]],
p {
class = "info-card",
[[
password must contain at least one uppercase letter, one lowercase
letter, one number, and one special character (@$!%*?&)
]]
},
},
section {
form {
action = "/signup2",
method = "POST",
class = "gapped-column",
--[[
textarea {
name = "message",
rows = 5, -- Adjust for height
cols = 40, -- Adjust for width
required = true,
placeholder = "Type your message here...",
wrap = "soft", -- Options: "soft" (no forced wrapping) or "hard" (includes newlines in submission)
},
]]
div {
class = "gapped-row",
input {
type = "password",
name = "password",
id = "password",
minlength = 8,
maxlength = 16,
pattern = "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&]).{8,}",
required = true,
placeholder = "password goes here",
},
input {
type = "password",
name = "confirm_password",
minlength = 8,
maxlength = 16,
required = true,
placeholder = "confirm password",
pattern = "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&]).{8,}",
title = "Passwords must match",
},
button "create account!",
}
},
a {
href = "/login",
"already have an account?"
}
},
}
end

56
template/signup2.lua Normal file
View file

@ -0,0 +1,56 @@
---@diagnostic disable:undefined-global
local wrapper = require "template.wrapper"
local utils = require "./utils"
local authors = require "./authors"
local phosphor = require "template.phosphor"
return function(data)
if data.request.cookies.auth_token then
data.response.code = 301
data.response.headers["Location"] = "/"
return ""
end
local form = utils.parse_form(data.request.body or "")
if not (form.password and form.confirm_password) or
form.password ~= form.confirm_password or
not utils.is_valid_password(form.password) then
data.response.code = 301
data.response.headers["Location"] = "/signup"
return ""
end
local id = authors.create_account(form.password)
local token = authors.auth(id, form.password)
-- TODO: Add Secure;!
data.response.headers["Set-Cookie"] = "auth_token=" .. token
.. "; Path=/; HttpOnly; SameSite=Strict"
data.request.cookies.auth_token = token
return wrapper(data) {
h1 { phosphor("identification-card"), " your identifier is ", id, "!" },
section {
p [[
that's your identifier! please do not lose it! it's important!
it's what allows us to tell you apart from everyone else.
]],
p [[
thanks for joining us! we hope you enjoy your stay, you rock!
]],
},
p {
"you can begin by ",
a {
href = "/new-blog",
"making a blog"
}
}
}
end

View file

@ -1,19 +1,24 @@
---@diagnostic disable:undefined-global ---@diagnostic disable:undefined-global
local config = require "./config" local config = require "./config"
local authors = require "./authors"
local phosphor = require "template.phosphor"
return function(settings) return function(data)
-- create top navigation element -- create top navigation element
local navigator = { local left_side = {
class = "top-navigator", class = "top-navigator-url",
a { href = "/", config.base_url .. "/" } a { href = "/", config.base_url .. "/" }
} }
if settings.url then local url = data.request.path
local last
if url then
local segments = {} local segments = {}
-- parse URL segments -- parse URL segments
for segment in settings.url:gmatch("[^/]+") do for segment in url:gmatch("[^/]+") do
table.insert(segments, segment) table.insert(segments, segment)
end end
@ -22,15 +27,59 @@ return function(settings)
for i, segment in ipairs(segments) do for i, segment in ipairs(segments) do
path = path .. "/" .. segment path = path .. "/" .. segment
last = segment .. (i < #segments and "/" or "")
-- add separator and link -- add separator and link
table.insert(navigator, " ") table.insert(left_side, " ")
table.insert(navigator, a { table.insert(left_side, a {
href = path, href = path,
segment .. (i < #segments and "/" or "") last
}) })
end end
end end
local right_side = {
class = "top-navigator-actions",
a { href = "/signup", "start blogging!" }
}
if data.request.cookies.auth_token then
local id = authors.id_from_token(data.request.cookies.auth_token)
if not id then
data.response.headers["Set-Cookie"] =
"auth_token=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; HttpOnly"
data.response.headers["Refresh"] = "0"
end
right_side = {
class = "top-navigator-actions",
a {
class = "action-button",
href = "/new-post",
phosphor("note-pencil")
},
a {
class = "action-button",
href = "/profile",
phosphor("user")
},
a {
class = "action-button",
href = "/logout",
phosphor("sign-out")
}
}
end
local error_card = ""
if error then
error_card = div {
class = "error-card",
phosphor("smiley-x-eyes"),
p "hey piece of shit"
}
end
return function(contents) return function(contents)
-- ensure contents is always a table -- ensure contents is always a table
if type(contents) == "string" then if type(contents) == "string" then
@ -42,14 +91,20 @@ return function(settings)
meta { charset = "UTF-8" }, meta { charset = "UTF-8" },
meta { ["http-equiv"] = "X-UA-Compatible", content = "IE=edge" }, meta { ["http-equiv"] = "X-UA-Compatible", content = "IE=edge" },
meta { name = "viewport", content = "width=device-width, initial-scale=1.0" }, meta { name = "viewport", content = "width=device-width, initial-scale=1.0" },
title(settings.title), title { "/", last },
link { href = "/static/baseline.css", rel = "stylesheet" }, link { href = "/static/baseline.css", rel = "stylesheet" },
script { src = "https://unpkg.com/@phosphor-icons/web@2.1.1" } script { src = "https://unpkg.com/@phosphor-icons/web@2.1.1" }
}, },
body { body {
nav(navigator), nav {
class = "top-navigator",
div(left_side),
div(right_side)
},
table.unpack(contents) table.unpack(contents)
} }
} }

View file

@ -1,4 +1,9 @@
return { local ffi = require "ffi"
local openssl = require "openssl"
local utils = {}
utils.errors = {
[400] = "bad_request", [400] = "bad_request",
[401] = "unauthorized", [401] = "unauthorized",
[403] = "forbidden", [403] = "forbidden",
@ -20,3 +25,82 @@ return {
[504] = "gateway_timeout", [504] = "gateway_timeout",
[505] = "http_version_not_supported" [505] = "http_version_not_supported"
} }
-- TODO: REWRITE
utils.random = function(min, max)
local buf = ffi.new("uint8_t[4]")
ffi.copy(buf, openssl.random(4), 4)
local num = 0
for i = 0, 3 do
num = bit.bor(bit.lshift(num, 8), buf[i])
end
return min + (num % (max - min + 1))
end
utils.pin = function()
local o = ""
for i = 1, 4 do
o = o .. tostring(utils.random(0, 9))
end
return o
end
utils.uuid = function()
local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
return string.gsub(template, '[xy]', function(c)
local v =
(c == 'x')
and utils.random(0, 0xf)
or utils.random(8, 0xb)
return string.format('%x', v)
end)
end
utils.hash = function(data)
return openssl.digest.digest("sha256", data)
end
utils.decode_url = function(str)
str = str:gsub('+', ' ') -- Convert + to space
return str:gsub("%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
end
utils.parse_form = function(data)
local result = {}
for pair in data:gmatch("[^&]+") do
local key, value = pair:match("([^=]+)=?(.*)")
if key then
result[utils.decode_url(key)] = utils.decode_url(value)
end
end
return result
end
utils.is_valid_password = function(password)
return #password >= 8
and #password <= 16
and password:match("%d")
and password:match("%l")
and password:match("%u")
and password:match("[@$!%%*?&]")
end
utils.cookies = function(req, res, go)
req.cookies = {}
for _, possible_cookie_jar in ipairs(req.headers) do
if possible_cookie_jar[1] == "Cookie" then
local form = utils.parse_form(possible_cookie_jar[2])
for key, value in pairs(form) do
req.cookies[key] = value
end
end
end
go()
end
package.preload["./utils"] = utils
return utils