init
This commit is contained in:
commit
96856ea7c9
10 changed files with 438 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
*.beam
|
||||
*.ez
|
||||
/build
|
||||
erl_crash.dump
|
||||
.envrc
|
25
gleam.toml
Normal file
25
gleam.toml
Normal file
|
@ -0,0 +1,25 @@
|
|||
name = "bowie"
|
||||
version = "1.0.0"
|
||||
|
||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||
# your project to the Hex package manager.
|
||||
#
|
||||
# description = ""
|
||||
# licences = ["Apache-2.0"]
|
||||
# repository = { type = "github", user = "username", repo = "project" }
|
||||
# links = [{ title = "Website", href = "https://gleam.run" }]
|
||||
#
|
||||
# For a full reference of all the available options, you can have a look at
|
||||
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = "~> 0.34 or ~> 1.0"
|
||||
nakai = "~> 0.9"
|
||||
gleam_erlang = "~> 0.25"
|
||||
wisp = "~> 0.14"
|
||||
mist = "~> 1.0"
|
||||
envoy = "~> 1.0"
|
||||
sqlight = "~> 0.9"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = "~> 1.0"
|
38
manifest.toml
Normal file
38
manifest.toml
Normal file
|
@ -0,0 +1,38 @@
|
|||
# This file was generated by Gleam
|
||||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" },
|
||||
{ name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" },
|
||||
{ name = "esqlite", version = "0.8.7", build_tools = ["rebar3"], requirements = [], otp_app = "esqlite", source = "hex", outer_checksum = "AF89C66027704B681657FDCCFFCEAA238D0DD702D5F687CDA3037F1D599A7551" },
|
||||
{ name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
|
||||
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
|
||||
{ name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
|
||||
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
|
||||
{ name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
|
||||
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
|
||||
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
|
||||
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
||||
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
|
||||
{ name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
|
||||
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
|
||||
{ name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" },
|
||||
{ name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" },
|
||||
{ name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" },
|
||||
{ name = "nakai", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "nakai", source = "hex", outer_checksum = "F6FFED9EF4B0E14C7A09B2FB87B42D3A93EFE024FD0299C11F041E92321163A6" },
|
||||
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
|
||||
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
|
||||
{ name = "sqlight", version = "0.9.0", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "2D9C9BA420A5E7DCE7DB2DAAE4CAB0BE6218BEB48FD1531C583550B3D1316E94" },
|
||||
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
|
||||
{ name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
envoy = { version = "~> 1.0" }
|
||||
gleam_erlang = { version = "~> 0.25" }
|
||||
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
|
||||
gleeunit = { version = "~> 1.0" }
|
||||
mist = { version = "~> 1.0" }
|
||||
nakai = { version = "~> 0.9" }
|
||||
sqlight = { version = "~> 0.9"}
|
||||
wisp = { version = "~> 0.14" }
|
41
src/bowie.gleam
Normal file
41
src/bowie.gleam
Normal file
|
@ -0,0 +1,41 @@
|
|||
import gleam/erlang/process
|
||||
import mist
|
||||
import wisp
|
||||
import bowie/router
|
||||
import bowie/web.{Context, Env}
|
||||
import envoy
|
||||
import sqlight
|
||||
|
||||
pub fn main() {
|
||||
wisp.configure_logger()
|
||||
let secret_key_base = wisp.random_string(64)
|
||||
|
||||
let assert Ok(admin_token) = envoy.get("ADMIN_TOKEN")
|
||||
let assert Ok(annual_link) = envoy.get("ANNUAL_LINK")
|
||||
let assert Ok(monthly_link) = envoy.get("MONTHLY_LINK")
|
||||
let assert Ok(forgejo_url) = envoy.get("FORGEJO_URL")
|
||||
let assert Ok(stripe_token) = envoy.get("STRIPE_TOKEN")
|
||||
let assert Ok(database_file) = envoy.get("DATABASE_FILE")
|
||||
|
||||
use db <- sqlight.with_connection(database_file)
|
||||
|
||||
let context =
|
||||
Context(
|
||||
db: db,
|
||||
env: Env(
|
||||
annual_link: annual_link,
|
||||
monthly_link: monthly_link,
|
||||
forgejo_url: forgejo_url,
|
||||
stripe_token: stripe_token,
|
||||
admin_token: admin_token,
|
||||
),
|
||||
)
|
||||
|
||||
let assert Ok(_) =
|
||||
wisp.mist_handler(router.handle_request(_, context), secret_key_base)
|
||||
|> mist.new
|
||||
|> mist.port(8000)
|
||||
|> mist.start_http
|
||||
|
||||
process.sleep_forever()
|
||||
}
|
46
src/bowie/router.gleam
Normal file
46
src/bowie/router.gleam
Normal file
|
@ -0,0 +1,46 @@
|
|||
import wisp.{type Request, type Response}
|
||||
import gleam/http.{Get}
|
||||
import bowie/web.{type Context}
|
||||
import sqlight
|
||||
import bowie/templates/signup
|
||||
import gleam/option.{None, Some}
|
||||
|
||||
pub fn handle_request(req: Request, context: Context) -> Response {
|
||||
use req <- web.middleware(req)
|
||||
|
||||
case wisp.path_segments(req) {
|
||||
["signup"] ->
|
||||
case req.method {
|
||||
http.Get -> get_signup(req, context)
|
||||
http.Post -> post_signup(req)
|
||||
_ -> wisp.method_not_allowed(allowed: [http.Get, http.Post])
|
||||
}
|
||||
|
||||
["payment_success", receipt] -> payment_success(req, receipt)
|
||||
|
||||
_ -> wisp.not_found()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_signup(req, context: Context) {
|
||||
use <- wisp.require_method(req, Get)
|
||||
|
||||
wisp.ok()
|
||||
|> wisp.html_body(signup.render(
|
||||
monthly_link: Some(context.env.monthly_link),
|
||||
annual_link: Some(context.env.annual_link),
|
||||
invitation: None,
|
||||
))
|
||||
}
|
||||
|
||||
fn post_signup(req) {
|
||||
use <- wisp.require_method(req, http.Post)
|
||||
|
||||
wisp.ok()
|
||||
}
|
||||
|
||||
fn payment_success(req, receipt) {
|
||||
use <- wisp.require_method(req, http.Post)
|
||||
|
||||
wisp.ok()
|
||||
}
|
23
src/bowie/templates/base.gleam
Normal file
23
src/bowie/templates/base.gleam
Normal file
|
@ -0,0 +1,23 @@
|
|||
import nakai
|
||||
import nakai/html
|
||||
import nakai/html/attrs
|
||||
|
||||
pub fn render(title title, content content) {
|
||||
html.div([attrs.id("content")], [
|
||||
html.Html([attrs.lang("en-US")], []),
|
||||
html.Head([
|
||||
html.title(title),
|
||||
html.meta([
|
||||
attrs.name("viewport"),
|
||||
attrs.content("width=device-width, initial-scale=1"),
|
||||
]),
|
||||
html.link([attrs.rel("stylesheet"), attrs.href("/assets/css/index.css")]),
|
||||
html.link([
|
||||
attrs.rel("stylesheet"),
|
||||
attrs.href("/assets/css/theme-forgejo-auto.css"),
|
||||
]),
|
||||
]),
|
||||
..content
|
||||
])
|
||||
|> nakai.to_string_builder()
|
||||
}
|
40
src/bowie/templates/signup.gleam
Normal file
40
src/bowie/templates/signup.gleam
Normal file
|
@ -0,0 +1,40 @@
|
|||
import nakai/html
|
||||
import nakai/html/attrs
|
||||
import bowie/templates/base
|
||||
import gleam/option.{type Option, None, Some}
|
||||
|
||||
pub fn render(
|
||||
monthly_link monthly_link: Option(String),
|
||||
annual_link annual_link: Option(String),
|
||||
invitation invitation: Option(String),
|
||||
) {
|
||||
let title = "Welcome to the Kitten Collective!"
|
||||
|
||||
base.render(
|
||||
title: title,
|
||||
content: content(monthly_link, annual_link, invitation),
|
||||
)
|
||||
}
|
||||
|
||||
fn content(
|
||||
monthly_link: Option(String),
|
||||
annual_link: Option(String),
|
||||
invitation: Option(String),
|
||||
) {
|
||||
let monthly = format_link(monthly_link, "Just $3/month!")
|
||||
let annual = format_link(annual_link, "Just $30/year!")
|
||||
let invite =
|
||||
format_link(
|
||||
invitation,
|
||||
"Free, limited account for collaborating with other kittens",
|
||||
)
|
||||
[monthly, annual, invite]
|
||||
}
|
||||
|
||||
fn format_link(link, text) {
|
||||
case link {
|
||||
None -> html.Nothing
|
||||
Some(link) ->
|
||||
html.div([], [html.p([], [html.a_text([attrs.href(link)], text)])])
|
||||
}
|
||||
}
|
150
src/bowie/templates/user_form.gleam
Normal file
150
src/bowie/templates/user_form.gleam
Normal file
|
@ -0,0 +1,150 @@
|
|||
import nakai/html
|
||||
import nakai/html/attrs
|
||||
import bowie/templates/base
|
||||
|
||||
pub fn render(receipt) {
|
||||
let title = "Welcome, friend, to git.kittencollective.com"
|
||||
base.render(title: title, content: content(receipt))
|
||||
}
|
||||
|
||||
fn content(receipt) {
|
||||
[
|
||||
html.div(
|
||||
[
|
||||
attrs.Attr(name: "role", value: "main"),
|
||||
attrs.Attr(name: "aria-label", value: "Sign In"),
|
||||
attrs.class("page-content user signin"),
|
||||
],
|
||||
[
|
||||
html.div([attrs.class("ui middle very relaxed page grid")], [
|
||||
html.div([attrs.class("ui container column fluid")], [
|
||||
html.h4_text(
|
||||
[attrs.class("ui top attached header center")],
|
||||
"Sign Up",
|
||||
),
|
||||
html.div([attrs.class("ui attached segment")], [
|
||||
html.form(
|
||||
[
|
||||
attrs.class("ui form"),
|
||||
attrs.action("/signup"),
|
||||
attrs.Attr(
|
||||
name: "enctype",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
),
|
||||
attrs.method("post"),
|
||||
],
|
||||
[
|
||||
html.input([
|
||||
attrs.type_("hidden"),
|
||||
attrs.value(receipt),
|
||||
attrs.name("receipt"),
|
||||
]),
|
||||
required_field(
|
||||
for: "username",
|
||||
text: "Username",
|
||||
input_type: "text",
|
||||
id: "username",
|
||||
extra_attributes: [
|
||||
attrs.Attr(name: "minlength", value: "1"),
|
||||
attrs.Attr(name: "maxlength", value: "20"),
|
||||
],
|
||||
),
|
||||
optional_field(
|
||||
for: "displayname",
|
||||
text: "Displayname (Optional)",
|
||||
input_type: "text",
|
||||
id: "displayname",
|
||||
extra_attributes: [],
|
||||
),
|
||||
required_field(
|
||||
for: "email",
|
||||
text: "Email",
|
||||
input_type: "text",
|
||||
id: "email",
|
||||
extra_attributes: [],
|
||||
),
|
||||
required_field(
|
||||
for: "password",
|
||||
text: "Password",
|
||||
input_type: "password",
|
||||
id: "password",
|
||||
extra_attributes: [],
|
||||
),
|
||||
required_field(
|
||||
for: "confirm_password",
|
||||
text: "Confirm Password",
|
||||
input_type: "password",
|
||||
id: "pw_verify",
|
||||
extra_attributes: [],
|
||||
),
|
||||
html.div([attrs.class("inline field")], [
|
||||
html.label([], []),
|
||||
html.button_text(
|
||||
[attrs.class("ui primary button")],
|
||||
"Sign Up",
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn optional_field(
|
||||
for label_for,
|
||||
text text,
|
||||
input_type input_type,
|
||||
id id,
|
||||
extra_attributes extra_attributes,
|
||||
) {
|
||||
field(
|
||||
for: label_for,
|
||||
text: text,
|
||||
input_type: input_type,
|
||||
id: id,
|
||||
extra_attributes: extra_attributes,
|
||||
)
|
||||
}
|
||||
|
||||
fn required_field(
|
||||
for label_for,
|
||||
text text,
|
||||
input_type input_type,
|
||||
id id,
|
||||
extra_attributes extra_attributes,
|
||||
) {
|
||||
field(
|
||||
for: label_for,
|
||||
text: text,
|
||||
input_type: input_type,
|
||||
id: id,
|
||||
extra_attributes: [
|
||||
attrs.Attr(name: "required", value: ""),
|
||||
..extra_attributes
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn field(
|
||||
for label_for,
|
||||
text text,
|
||||
input_type input_type,
|
||||
id id,
|
||||
extra_attributes extra_attributes,
|
||||
) {
|
||||
html.div([attrs.class("required inline field ")], [
|
||||
html.label_text([attrs.for(label_for)], text),
|
||||
html.input([
|
||||
attrs.type_(input_type),
|
||||
attrs.name(id),
|
||||
attrs.id(id),
|
||||
attrs.Attr(name: "required", value: ""),
|
||||
..extra_attributes
|
||||
]),
|
||||
html.br([]),
|
||||
])
|
||||
}
|
28
src/bowie/web.gleam
Normal file
28
src/bowie/web.gleam
Normal file
|
@ -0,0 +1,28 @@
|
|||
import wisp
|
||||
import sqlight.{type Connection}
|
||||
|
||||
pub type Context {
|
||||
Context(env: Env, db: Connection)
|
||||
}
|
||||
|
||||
pub type Env {
|
||||
Env(
|
||||
annual_link: String,
|
||||
forgejo_url: String,
|
||||
stripe_token: String,
|
||||
monthly_link: String,
|
||||
admin_token: String,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn middleware(
|
||||
req: wisp.Request,
|
||||
handle_request: fn(wisp.Request) -> wisp.Response,
|
||||
) -> wisp.Response {
|
||||
let req = wisp.method_override(req)
|
||||
use <- wisp.log_request(req)
|
||||
use <- wisp.rescue_crashes
|
||||
use req <- wisp.handle_head(req)
|
||||
|
||||
handle_request(req)
|
||||
}
|
42
user_invitations.sql
Normal file
42
user_invitations.sql
Normal file
|
@ -0,0 +1,42 @@
|
|||
create table if not exists customers (
|
||||
id integer primary key,
|
||||
username text not null unique,
|
||||
receipt text not null unique,
|
||||
billing_email text,
|
||||
invitation id,
|
||||
created_at int not null default (unixepoch()),
|
||||
updated_at int not null default (unixepoch()),
|
||||
foreign key (invitation) references invitations (id)
|
||||
);
|
||||
create index if not exists customers_username_dex on customers (lower(username));
|
||||
create index if not exists customers_email_dex on customers (lower(billing_email));
|
||||
create index if not exists customers_receipt_dex on customers (receipt);
|
||||
create index if not exists customers_invitation_dex on customers (invitation); -- does this need to be created? it's already a foreign key
|
||||
|
||||
create trigger if not exists update_customers_updated_at
|
||||
after update on customers
|
||||
when OLD.updated_at = NEW.updated_at or OLD.updated_at is null
|
||||
BEGIN
|
||||
update customers set updated_at = (select unixepoch()) where id=NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
create table if not exists invitations (
|
||||
id integer primary key,
|
||||
owner integer not null,
|
||||
remaining integer not null default 1,
|
||||
expires_at integer,
|
||||
created_at integer not null default (unixepoch()),
|
||||
updated_at integer not null default (unixepoch()),
|
||||
foreign key (owner) references customers (id)
|
||||
);
|
||||
create index if not exists invitations_owner_dex on invitations (owner);
|
||||
|
||||
create trigger if not exists update_invitations_updated_at
|
||||
after update on invitations
|
||||
when OLD.updated_at = NEW.updated_at or OLD.updated_at is null
|
||||
BEGIN
|
||||
update invitations set updated_at = (select unixepoch()) where id=NEW.id;
|
||||
END;
|
||||
|
||||
|
Loading…
Reference in a new issue