diff --git a/client/src/client.gleam b/client/src/client.gleam index ecfa116..83df039 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -195,8 +195,7 @@ fn layout( element_view: element.Element(a), f: fn(a) -> Msg, ) -> element.Element(Msg) { - let attributes = [class("text-white")] - oat_sidebar.sidebar_always(html.main, attributes, [ + oat_sidebar.sidebar(html.main, [class("text-white")], [ header.view(model.session) |> element.map(HeaderMsg), sidebar.view(model.session) |> element.map(SidebarMsg), element_view |> element.map(f), diff --git a/client/src/client/page/dashboard.gleam b/client/src/client/page/dashboard.gleam index c95a2cd..c5d1fab 100644 --- a/client/src/client/page/dashboard.gleam +++ b/client/src/client/page/dashboard.gleam @@ -1,17 +1,16 @@ +import client/ui import client/ui/header import glaze/oat/button import glaze/oat/card -import gleam/bool -import gleam/list import icon import lustre/attribute.{class} as attr import lustre/element import lustre/element/html import shared/role import shared/route -import shared/session +import shared/session.{type Session} -pub const empty = Model +pub const empty: Model = Model pub type Model { Model @@ -21,7 +20,7 @@ pub type Msg { Msg } -pub fn view(session: session.Session, _model: Model) -> element.Element(Msg) { +pub fn view(session: Session, _model: Model) -> element.Element(Msg) { let attributes = [ class(header.offset), class("p-4 mx-auto w-full max-w-xl"), @@ -30,62 +29,68 @@ pub fn view(session: session.Session, _model: Model) -> element.Element(Msg) { html.section(attributes, [view_quick_access(session)]) } -fn view_quick_access(session: session.Session) -> element.Element(Msg) { +fn view_quick_access(session: Session) -> element.Element(Msg) { card.card([], [ card.header([], [html.h4([], [html.text("Quick Access")])]), card.content([class("flex flex-col gap-4")], [ // User Management - html.div([], [ - html.h5([class("flex gap-2")], [ - icon.user([class("inline")]), - html.text("User Management"), - ]), - - view_quick_access_item( - session:, - require: [role.Admin], - text: "Register an user", - href: route.Signup, - ), - ]), + view_quick_access_section( + session:, + required: [role.Admin], + icon: icon.user([class("inline")]), + heading: "User Management", + elements: [ + view_quick_access_item( + session:, + required: [role.Admin], + text: "Register an user", + href: route.Signup, + ), + ], + ), // Crew Management - html.div([], [ - html.h5([class("flex gap-2")], [ - icon.users([class("inline")]), - html.text("Crew Management"), - ]), - - view_quick_access_item( - session:, - require: [role.Admin], - text: "Register a crew", - href: route.RegisterCrew, - ), - ]), + view_quick_access_section( + session:, + required: [role.Admin], + icon: icon.users([class("inline")]), + heading: "Crew Management", + elements: [ + view_quick_access_item( + session:, + required: [role.Admin], + text: "Register a crew", + href: route.RegisterCrew, + ), + ], + ), ]), ]) } +fn view_quick_access_section( + session session: Session, + required required: List(role.Role), + icon icon: element.Element(Msg), + heading heading: String, + elements elements: List(element.Element(Msg)), +) -> element.Element(Msg) { + use <- ui.require_role(session, required) + + html.div([], [ + html.h6([class("flex gap-2")], [icon, html.text(heading)]), + ..elements + ]) +} + fn view_quick_access_item( - session session: session.Session, - require require: List(role.Role), + session session: Session, + required required: List(role.Role), text text: String, href href: route.Route, ) -> element.Element(Msg) { - // Render nothing if user is not authorized - use <- bool.guard( - when: !is_authorized(session, require), - return: element.none(), - ) + use <- ui.require_role(session, required) let href = attr.href(route.to_path(href)) button.link([href, class("w-full")], [html.text(text)]) } - -fn is_authorized(session: session.Session, required: List(role.Role)) { - case session { - session.Authenticated(user:) -> list.contains(required, user.role) - _ -> False - } -} diff --git a/client/src/client/page/signup.gleam b/client/src/client/page/signup.gleam index 5ac825f..7e2f49d 100644 --- a/client/src/client/page/signup.gleam +++ b/client/src/client/page/signup.gleam @@ -63,7 +63,6 @@ pub const empty = Model( is_valid: True, ), user_role: role.None, - user_is_active: False, loading: False, ) @@ -75,7 +74,6 @@ pub type Model { user_password: input.Input, user_confirm_password: input.Input, user_role: role.Role, - user_is_active: Bool, loading: Bool, ) } @@ -87,7 +85,6 @@ fn build_request(model: Model) -> contract.RequestBody { email: model.user_email.value, phone: model.user_phone.value, password: model.user_password.value, - is_active: model.user_is_active, ) } diff --git a/client/src/client/ui.gleam b/client/src/client/ui.gleam new file mode 100644 index 0000000..ee84650 --- /dev/null +++ b/client/src/client/ui.gleam @@ -0,0 +1,21 @@ +import gleam/list +import lustre/element +import shared/role +import shared/session.{type Session} + +/// Render nothing if user is not authenticated or lack permissions +pub fn require_role( + session: Session, + required: List(role.Role), + next: fn() -> element.Element(a), +) -> element.Element(a) { + case session { + session.Authenticated(user:) -> + case list.contains(required, user.role) { + True -> next() + False -> element.none() + } + + _ -> element.none() + } +} diff --git a/server/src/server/router/signup.gleam b/server/src/server/router/signup.gleam index 4d091fb..ea9861d 100644 --- a/server/src/server/router/signup.gleam +++ b/server/src/server/router/signup.gleam @@ -31,7 +31,7 @@ fn handle_body(body: contract.RequestBody, ctx: Context) -> Response { user_password: body.password, user_email: body.email, user_phone: body.phone, - is_active: body.is_active, + is_active: True, )) shared_user.to_json(user) diff --git a/server/test/user_test.gleam b/server/test/user_test.gleam index d9d215f..81e7ecb 100644 --- a/server/test/user_test.gleam +++ b/server/test/user_test.gleam @@ -25,7 +25,6 @@ pub fn signup_test() -> Nil { email: test_email, phone: wisp.random_string(12), password: wisp.random_string(12), - is_active: False, ) |> signup_contract.request_to_json() @@ -61,7 +60,6 @@ pub fn signup_invalid_email_test() -> Nil { email: "invalid_email", phone: wisp.random_string(12), password: wisp.random_string(12), - is_active: False, ) |> signup_contract.request_to_json() @@ -86,7 +84,6 @@ pub fn signup_email_conflict_test() -> Nil { email: seed.email, phone: wisp.random_string(12), password: wisp.random_string(12), - is_active: False, ) |> signup_contract.request_to_json() @@ -111,7 +108,6 @@ pub fn signup_phone_conflict_test() -> Nil { email: test_email, phone: seed.phone, password: wisp.random_string(12), - is_active: False, ) |> signup_contract.request_to_json() diff --git a/shared/src/shared/contract/signup.gleam b/shared/src/shared/contract/signup.gleam index ebd852c..8f916ab 100644 --- a/shared/src/shared/contract/signup.gleam +++ b/shared/src/shared/contract/signup.gleam @@ -14,7 +14,6 @@ pub type RequestBody { email: String, phone: String, password: String, - is_active: Bool, ) } @@ -25,7 +24,6 @@ pub fn request_to_json(body: RequestBody) -> json.Json { #("email", json.string(body.email)), #("phone", json.string(body.phone)), #("password", json.string(body.password)), - #("is_active", json.bool(body.is_active)), ]) } @@ -34,9 +32,8 @@ pub fn request_decoder() -> decode.Decoder(RequestBody) { use role <- decode.field("role", role.decoder()) use phone <- decode.field("phone", decode.string) use password <- decode.field("password", decode.string) - use is_active <- decode.field("is_active", decode.bool) use email <- decode.field("email", email.decoder()) - RequestBody(name:, role:, email:, phone:, password:, is_active:) + RequestBody(name:, role:, email:, phone:, password:) |> decode.success }