Skip to content

Commit

Permalink
Merge applications into users
Browse files Browse the repository at this point in the history
  • Loading branch information
lpil committed Jun 14, 2024
1 parent c5e9c8f commit 6ede85c
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 211 deletions.
2 changes: 2 additions & 0 deletions src/puck.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ fn unknown() {
}

fn server(config: Config) {
wisp.configure_logger()

case config.environment {
"development" -> Nil
_ -> install_log_handler(send_error_email(_, config))
Expand Down
12 changes: 2 additions & 10 deletions src/puck/database.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,15 @@ create table if not exists users (
constraint valid_login_token_created_at check (
login_token_created_at is null
or datetime(login_token_created_at) not null
)
) strict;
create table if not exists applications (
id integer primary key autoincrement not null,
user_id integer not null unique,
),
payment_reference text not null unique collate nocase
constraint valid_payment_reference check (
length(payment_reference) = 14 and payment_reference like 'm-%'
),
answers text not null default '{}'
constraint valid_answers_json check (json(answers) not null),
foreign key (user_id) references users (id)
constraint valid_answers_json check (json(answers) not null)
) strict;
create table if not exists payments (
Expand Down
4 changes: 2 additions & 2 deletions src/puck/payment.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ pub fn total(conn: database.Connection) -> Result(Int, Error) {
coalesce(sum(amount), 0) as total
from
payments
inner join applications on
payments.reference = applications.payment_reference
inner join users on
payments.reference = users.payment_reference
"

database.one(sql, conn, [], dynamic.element(0, dynamic.int))
Expand Down
29 changes: 16 additions & 13 deletions src/puck/routes.gleam
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import gleam/bool
import gleam/dict
import gleam/http/response
import gleam/io
import gleam/list
import gleam/option.{None, Some}
import gleam/option.{Some}
import gleam/result
import gleam/string_builder.{type StringBuilder}
import nakai/html
import nakai/html/attrs.{Attr}
import puck/config.{type Config}
import puck/payment.{type Payment}
import puck/user.{type Application, type User}
import puck/user.{type User}
import puck/web.{type Context, p}
import puck/web/admin
import puck/web/auth
Expand All @@ -32,7 +34,10 @@ pub fn handle_request(req: Request, ctx: Context) -> Response {
["information"] -> event.information(req, ctx)
["sign-up", key] if key == attend -> auth.sign_up(req, ctx)
["login"] -> auth.login(req, ctx)
["login", user_id, token] -> auth.login_via_token(user_id, token, ctx)
["login", user_id, token] -> {
io.debug("login via token")
auth.login_via_token(user_id, token, ctx)
}
["api", "payment", key] if key == pay -> money.payment_webhook(req, ctx)
_ -> wisp.not_found()
}
Expand Down Expand Up @@ -68,19 +73,18 @@ fn middleware(

fn home(ctx: Context) -> Response {
use user <- web.require_user(ctx)
let assert Ok(application) = user.get_application(ctx.db, user.id)

case application {
Some(application) -> dashboard(user, application, ctx)
None -> event.application_form(ctx)
case user.answers == dict.new() {
True -> event.application_form(ctx)
False -> dashboard(user, ctx)
}
}

fn dashboard(user: User, application: Application, ctx: Context) -> Response {
fn dashboard(user: User, ctx: Context) -> Response {
let assert Ok(payments) =
payment.for_reference(ctx.db, application.payment_reference)
payment.for_reference(ctx.db, user.payment_reference)
let assert Ok(total) = payment.total(ctx.db)
dashboard_html(user, application, payments, total, ctx.config)
dashboard_html(user, payments, total, ctx.config)
|> wisp.html_response(200)
}

Expand All @@ -90,7 +94,6 @@ fn table_row(label, value) -> html.Node(a) {

fn dashboard_html(
user: User,
application: Application,
payments: List(Payment),
_total_contributions: Int,
config: Config,
Expand Down Expand Up @@ -133,7 +136,7 @@ fn dashboard_html(
table_row("Account holder", config.account_name),
table_row("Account number", config.account_number),
table_row("Sort code", config.sort_code),
table_row("Unique reference", application.payment_reference),
table_row("Unique reference", user.payment_reference),
]),
])

Expand All @@ -142,7 +145,7 @@ fn dashboard_html(
web.dt_dl("What's your name?", user.name),
web.dt_dl("What's your email?", user.email),
web.dt_dl("How much have you contributed?", user_contributed_text),
html.Fragment(event.application_answers_list_html(application)),
html.Fragment(event.application_answers_list_html(user)),
]

let expandable = fn(title, body) {
Expand Down
91 changes: 29 additions & 62 deletions src/puck/user.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import puck/error.{type Error}
import sqlight

pub type User {
User(id: Int, name: String, email: String, interactions: Int, is_admin: Bool)
}

pub type Application {
Application(
User(
id: Int,
name: String,
email: String,
interactions: Int,
is_admin: Bool,
payment_reference: String,
user_id: Int,
answers: Dict(String, String),
)
}
Expand All @@ -31,13 +30,17 @@ pub fn insert(
let sql =
"
insert into users
(name, email)
(name, email, payment_reference)
values
(?1, ?2)
(?1, ?2, ?3)
returning
id, name, email, interactions, is_admin
id, name, email, interactions, is_admin, payment_reference, answers
"
let arguments = [sqlight.text(name), sqlight.text(string.lowercase(email))]
let arguments = [
sqlight.text(name),
sqlight.text(string.lowercase(email)),
sqlight.text(generate_reference()),
]

case database.one(sql, conn, arguments, decoder) {
Ok(user) -> Ok(user)
Expand All @@ -54,7 +57,7 @@ pub fn list_all(conn: database.Connection) -> Result(List(User), Error) {
let sql =
"
select
id, name, email, interactions, is_admin
id, name, email, interactions, is_admin, payment_reference, answers
from users
limit 1000
"
Expand All @@ -69,7 +72,7 @@ pub fn get_by_email(
let sql =
"
select
id, name, email, interactions, is_admin
id, name, email, interactions, is_admin, payment_reference, answers
from
users
where
Expand All @@ -85,52 +88,27 @@ pub fn get_by_email(

/// Insert the application for a user. If the user already has an application then
/// the answers are merged into the existing record.
pub fn insert_application(
pub fn record_answers(
conn: database.Connection,
user_id user_id: Int,
answers answers: Dict(String, String),
) -> Result(Application, Error) {
) -> Result(Nil, Error) {
let sql =
"
insert into applications
(user_id, payment_reference, answers)
values
(?1, ?2, ?3)
on conflict (user_id) do
update set answers = json_patch(answers, excluded.answers)
returning
id, payment_reference, user_id, answers
update users set
answers = json_patch(answers, ?2)
where
id = ?1
"
let json =
json.to_string(json.object(
answers
|> dict.map_values(fn(_, v) { json.string(v) })
|> dict.to_list,
))
let arguments = [
sqlight.int(user_id),
sqlight.text(generate_reference()),
sqlight.text(json),
]
database.one(sql, conn, arguments, application_decoder)
}

/// Get the application for a user.
pub fn get_application(
conn: database.Connection,
user_id: Int,
) -> Result(Option(Application), Error) {
let sql =
"
select
id, payment_reference, user_id, answers
from
applications
where
user_id = ?1
"
let arguments = [sqlight.int(user_id)]
database.maybe_one(sql, conn, arguments, application_decoder)
let arguments = [sqlight.int(user_id), sqlight.text(json)]
use _ <- result.try(database.query(sql, conn, arguments, Ok))
Ok(Nil)
}

pub fn get_user_by_payment_reference(
Expand All @@ -140,11 +118,9 @@ pub fn get_user_by_payment_reference(
let sql =
"
select
users.id, name, email, interactions, is_admin
users.id, name, email, interactions, is_admin, payment_reference, answers
from
users
join
applications on users.id = applications.user_id
where
payment_reference = ?1
"
Expand All @@ -163,7 +139,7 @@ pub fn get_and_increment_interaction(
where
id = ?1
returning
id, name, email, interactions, is_admin
id, name, email, interactions, is_admin, payment_reference, answers
"
let arguments = [sqlight.int(user_id)]
database.maybe_one(sql, conn, arguments, decoder)
Expand Down Expand Up @@ -222,24 +198,15 @@ pub fn get_login_token(

fn decoder(data: Dynamic) {
data
|> dy.decode5(
|> dy.decode7(
User,
dy.element(0, dy.int),
dy.element(1, dy.string),
dy.element(2, dy.string),
dy.element(3, dy.int),
dy.element(4, sqlight.decode_bool),
)
}

fn application_decoder(data: Dynamic) {
data
|> dy.decode4(
Application,
dy.element(0, dy.int),
dy.element(1, dy.string),
dy.element(2, dy.int),
dy.element(3, json_object(dy.string)),
dy.element(5, dy.string),
dy.element(6, json_object(dy.string)),
)
}

Expand Down
34 changes: 13 additions & 21 deletions src/puck/web/admin.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import gleam/dict
import gleam/http
import gleam/int
import gleam/list
import gleam/option.{Some}
import gleam/result
import nakai/html
import puck/payment.{type Payment}
Expand Down Expand Up @@ -65,26 +64,19 @@ fn day_income(payment: #(String, Int, Int)) -> html.Node(a) {
}

fn user_row(user: User, ctx: Context) -> html.Node(a) {
let application_data = case user.get_application(ctx.db, user.id) {
Ok(Some(application)) -> {
let assert Ok(total) =
payment.total_for_reference(ctx.db, application.payment_reference)
let get = fn(key) {
result.unwrap(dict.get(application.answers, key), "")
}
[
money.pence_to_pounds(total),
application.payment_reference,
get(event.field_attended),
get(event.field_support_network),
get(event.field_support_network_attended),
get(event.field_dietary_requirements),
get(event.field_accessibility_requirements),
]
}
let assert Ok(total) =
payment.total_for_reference(ctx.db, user.payment_reference)
let get = fn(key) { result.unwrap(dict.get(user.answers, key), "") }

_ -> ["", "", "", "", "", "", ""]
}
let user_data = [
money.pence_to_pounds(total),
user.payment_reference,
get(event.field_attended),
get(event.field_support_network),
get(event.field_support_network_attended),
get(event.field_dietary_requirements),
get(event.field_accessibility_requirements),
]

html.tr(
[],
Expand All @@ -94,7 +86,7 @@ fn user_row(user: User, ctx: Context) -> html.Node(a) {
user.name,
user.email,
int.to_string(user.interactions),
..application_data
..user_data
],
td,
),
Expand Down
9 changes: 8 additions & 1 deletion src/puck/web/auth.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import gleam/http/cookie
import gleam/http/request
import gleam/http/response
import gleam/int
import gleam/io
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
Expand Down Expand Up @@ -179,16 +180,22 @@ pub fn login_via_token(user_id: String, token: String, ctx: Context) {
// This application isn't very sensitive, so we're just comparing tokens
// rather than doing the much more secure thing of storing and comparing
// a hash in a constant time way.
io.debug(0)
use user_id <- web.try_(int.parse(user_id), bad_token_page)
io.debug(1)
use db_token <- web.try_(
user.get_login_token(ctx.db, user_id),
bad_token_page,
)
io.debug(2)
use db_token <- web.some(db_token, bad_token_page)
case token == db_token {
io.debug(3)
case io.debug(token == db_token) {
True ->
wisp.redirect("/")
|> response.set_header("x-louis", "hello!!!!")
|> set_signed_user_id_cookie(user_id, ctx.config.signing_secret)
|> io.debug
False -> bad_token_page()
}
}
Expand Down
Loading

0 comments on commit 6ede85c

Please sign in to comment.