Skip to content

Commit

Permalink
It's a mess but it works!
Browse files Browse the repository at this point in the history
  • Loading branch information
skinkade committed Jul 13, 2024
1 parent b9efdf4 commit c7187e7
Show file tree
Hide file tree
Showing 9 changed files with 723 additions and 44 deletions.
12 changes: 12 additions & 0 deletions db/migrations/20240713122829_pending_user_tenant_roles_index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- migrate:up

CREATE INDEX IX_pending_user_tenant_role_tenant
ON pending_user_tenant_roles (tenant_id);

CREATE UNIQUE INDEX UX_pending_user_tenant_role_email_tenant
ON pending_user_tenant_roles (email_address, tenant_id);

-- migrate:down

DROP INDEX IX_pending_user_tenant_role_tenant;
DROP INDEX UX_pending_user_tenant_role_email_tenant;
2 changes: 1 addition & 1 deletion src/wisp_multitenant_demo.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import wisp_multitenant_demo/web/web

pub fn main() {
wisp.configure_logger()
let secret_key_base = wisp.random_string(64)
let secret_key_base = "MpVgHx0Absdm5V9se87NiPWfTLDKwUYUWO6ksnmGDPA"

let db =
pgo.connect(
Expand Down
55 changes: 53 additions & 2 deletions src/wisp_multitenant_demo/models/user_tenant_role.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import gleam/pgo.{type Connection, type QueryError}
import gleam/result
import wisp_multitenant_demo/models/tenant.{type TenantId}
import wisp_multitenant_demo/models/user.{type UserId}
import wisp_multitenant_demo/types/email

pub type UserTenantRole {
TenantOwner
Expand All @@ -18,12 +19,12 @@ pub fn role_to_string(role: UserTenantRole) -> String {
}
}

pub fn role_from_string(str: String) -> Result(UserTenantRole, Nil) {
pub fn role_from_string(str: String) -> Result(UserTenantRole, String) {
case str {
"member" -> Ok(TenantMember)
"admin" -> Ok(TenantAdmin)
"owner" -> Ok(TenantOwner)
_ -> Error(Nil)
_ -> Error("Invalid role")
}
}

Expand Down Expand Up @@ -140,3 +141,53 @@ pub fn get_user_tenant_roles(

Ok(result.rows)
}

pub type TenantUser {
TenantUser(email_address: email.Email, role: UserTenantRole, is_pending: Bool)
}

pub fn get_tenant_users(
db: Connection,
tenant_id: TenantId,
) -> Result(List(TenantUser), QueryError) {
// Lazy hack: third boolean field is whether user is pending
let sql =
"
SELECT
u.email_address,
utr.role_desc,
false
FROM user_tenant_roles utr
JOIN users u
ON utr.user_id = u.id
WHERE tenant_id = $1
UNION ALL
SELECT
putr.email_address,
putr.role_desc,
true
FROM pending_user_tenant_roles putr
WHERE tenant_id = $1
"

let decoder =
dynamic.decode3(
TenantUser,
dynamic.element(0, email.decode_email),
dynamic.element(1, decode_role),
dynamic.element(2, dynamic.bool),
)

use result <- result.try({
pgo.execute(
sql,
db,
[tenant_id |> tenant.id_to_int() |> pgo.int()],
decoder,
)
})

Ok(result.rows)
}
78 changes: 58 additions & 20 deletions src/wisp_multitenant_demo/web/middleware.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ pub fn derive_session(
handler: fn(Option(user_session.SessionQueryRecord)) -> Response,
) -> Response {
let session = wisp.get_cookie(req, "session", wisp.Signed)
use <- bool.guard(result.is_error(session), handler(None))
use <- bool.lazy_guard(result.is_error(session), fn() { handler(None) })

let assert Ok(session) = session
let session = user_session.get_by_session_key_string(conn, session)
use <- bool.guard(result.is_error(session), wisp.internal_server_error())
use <- bool.lazy_guard(result.is_error(session), fn() {
wisp.internal_server_error()
})

let assert Ok(session) = session
use <- bool.guard(option.is_none(session), handler(None))
use <- bool.lazy_guard(option.is_none(session), fn() { handler(None) })

let assert Some(session) = session
use <- bool.guard(user_session.is_expired(session), handler(None))
use <- bool.lazy_guard(user_session.is_expired(session), fn() {
handler(None)
})

handler(Some(session))
}
Expand All @@ -39,11 +43,13 @@ pub fn derive_user(
handler: fn(Option(user.User)) -> Response,
) -> Response {
use session <- derive_session(req, conn)
use <- bool.guard(option.is_none(session), handler(None))
use <- bool.lazy_guard(option.is_none(session), fn() { handler(None) })

let assert Some(session) = session
let user = user.get_by_id(conn, session.user_id)
use <- bool.guard(result.is_error(user), wisp.internal_server_error())
use <- bool.lazy_guard(result.is_error(user), fn() {
wisp.internal_server_error()
})

let assert Ok(Some(user)) = user
// use <- bool.guard(user.disabled_or_locked(user), handler(None))
Expand All @@ -57,7 +63,7 @@ pub fn derive_user_tenant_roles(
handler: fn(Option(List(user_tenant_role.UserTenantRoleForAccess))) ->
Response,
) -> Response {
use <- bool.guard(option.is_none(user), handler(None))
use <- bool.lazy_guard(option.is_none(user), fn() { handler(None) })
let assert Some(user) = user

case user_tenant_role.get_user_tenant_roles(conn, user.id) {
Expand All @@ -73,7 +79,9 @@ pub fn require_user(
req_ctx: web.RequestContext,
handler: fn(user.User) -> Response,
) -> Response {
use <- bool.guard(option.is_none(req_ctx.user), wisp.redirect("/login"))
use <- bool.lazy_guard(option.is_none(req_ctx.user), fn() {
wisp.redirect("/login")
})
let assert Some(user) = req_ctx.user
handler(user)
}
Expand Down Expand Up @@ -145,10 +153,9 @@ pub fn tenant_auth(
req_ctx: web.RequestContext,
handler: fn(web.RequestContext) -> Response,
) -> Response {
use <- bool.guard(
option.is_none(req_ctx.user_tenant_roles),
handler(web.RequestContext(..req_ctx, selected_tenant_id: None)),
)
use <- bool.lazy_guard(option.is_none(req_ctx.user_tenant_roles), fn() {
handler(web.RequestContext(..req_ctx, selected_tenant_id: None))
})
let assert Some(roles) = req_ctx.user_tenant_roles

case req_ctx.selected_tenant_id, roles {
Expand All @@ -159,31 +166,62 @@ pub fn tenant_auth(
)
None, _ -> handler(req_ctx)
Some(selection), roles -> {
use <- bool.guard(
use <- bool.lazy_guard(
!{
roles
|> list.map(fn(utr) { utr.tenant_id })
|> list.contains(selection)
},
wisp.redirect("/demo")
|> wisp.set_cookie(req, "tenant", "", wisp.Signed, 0),
fn() {
wisp.redirect("/demo")
|> wisp.set_cookie(req, "tenant", "", wisp.Signed, 0)
},
)
handler(req_ctx)
}
}
}

pub fn require_selected_tenant(
req: wisp.Request,
req_ctx: web.RequestContext,
handler: fn(tenant.TenantId) -> Response,
) -> Response {
use <- bool.guard(
option.is_none(req_ctx.selected_tenant_id),
wisp.redirect("/"),
)
use <- bool.lazy_guard(option.is_none(req_ctx.selected_tenant_id), fn() {
wisp.redirect("/")
})

let assert Some(tenant_id) = req_ctx.selected_tenant_id

handler(tenant_id)
}

pub fn current_user_tenant_role(
req_ctx: web.RequestContext,
) -> Option(user_tenant_role.UserTenantRole) {
case req_ctx.selected_tenant_id, req_ctx.user_tenant_roles {
None, _ | _, None -> None
Some(id), Some(roles) -> {
let role = list.find(roles, fn(role) { role.tenant_id == id })
use <- bool.lazy_guard(result.is_error(role), fn() { None })
let assert Ok(role) = role
Some(role.role)
}
}
}

pub fn require_one_of_tenant_roles(
req_ctx: web.RequestContext,
roles: List(user_tenant_role.UserTenantRole),
handler: fn() -> Response,
) -> Response {
let role = current_user_tenant_role(req_ctx)
case role {
None -> wisp.response(403)
Some(role) -> {
use <- bool.lazy_guard(!list.contains(roles, role), fn() {
wisp.response(403)
})
handler()
}
}
}
5 changes: 5 additions & 0 deletions src/wisp_multitenant_demo/web/router.gleam
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import gleam/io
import wisp.{type Request, type Response}
import wisp_multitenant_demo/web/middleware
import wisp_multitenant_demo/web/routes/admin
import wisp_multitenant_demo/web/routes/demo
import wisp_multitenant_demo/web/routes/login
import wisp_multitenant_demo/web/routes/register
import wisp_multitenant_demo/web/routes/register_customer
import wisp_multitenant_demo/web/web

pub fn handle_request(req: Request, app_ctx: web.AppContext) -> Response {
Expand All @@ -22,6 +24,9 @@ pub fn handle_request(req: Request, app_ctx: web.AppContext) -> Response {
use req_ctx <- middleware.tenant_auth(req, req_ctx)

case wisp.path_segments(req) {
["customer", "register"] ->
register_customer.register_handler(req, app_ctx, req_ctx)
["admin", ..] -> admin.admin_router(req, app_ctx, req_ctx)
["demo"] -> demo.demo_handler(req, app_ctx, req_ctx)
["login"] -> login.login_handler(req, app_ctx, req_ctx)
["register"] -> register.register_handler(req, app_ctx, req_ctx)
Expand Down
Loading

0 comments on commit c7187e7

Please sign in to comment.