Skip to content

Commit

Permalink
Finish integration with keycloak for accepting new members (#161)
Browse files Browse the repository at this point in the history
Finishes implementation of keycloak integration.

* improves error handling
* small fixes/improvements in api (tracking date)
* add error handling to front-end
  • Loading branch information
turboMaCk authored and ICTGuerrilla committed Jan 13, 2024
1 parent ea0944c commit 44320e9
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 32 deletions.
36 changes: 27 additions & 9 deletions melon-head/src/App/MemberDetail.res
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,34 @@ module Actions = {

module Accept = {
@react.component
let make = (~modal, ~api, ~id) => {
let make = (~modal, ~api, ~id, ~setDetail) => {
let (error, setError) = React.useState(() => None)

let doAccept = (_: JsxEvent.Mouse.t) => {
let req =
api->Api.patchJson(
~path="/members/" ++ Uuid.toString(id) ++ "/accept",
~decoder=Json.Decode.string,
~decoder=MemberData.Decode.detail,
~body=Js.Json.null,
)

req->Future.get(res => {
switch res {
| Ok(data) => {
setDetail(_ => RemoteData.Success(data))
Modal.Interface.closeModal(modal)
}
| Error(e) => setError(_ => Some(e))
}
})
}

<Modal.Content>
<p> {React.string("Approve user and give allow them to access internal resources")} </p>
<p> {React.string("Accept member and allow them to access internal resources.")} </p>
{switch error {
| None => React.null
| Some(err) => <Message.Error> {React.string(err->Api.showError)} </Message.Error>
}}
<Button.Panel>
<Button onClick={_ => modal->Modal.Interface.closeModal}>
{React.string("Cancel")}
Expand All @@ -118,17 +134,19 @@ module Actions = {
}
}

let acceptModal = (~modal, ~api, ~id): Modal.modalContent => {
let acceptModal = (~modal, ~api, ~id, ~setDetail): Modal.modalContent => {
title: "Accept User",
content: <Accept modal api id />,
content: <Accept modal api id setDetail />,
}

@react.component
let make = (~status, ~modal, ~api, ~id) => {
let make = (~status, ~modal, ~api, ~id, ~setDetail) => {
switch status {
| NewMember =>
<Button.Panel>
<Button onClick={_ => modal->Modal.Interface.openModal(acceptModal(~modal, ~api, ~id))}>
<Button
onClick={_ =>
modal->Modal.Interface.openModal(acceptModal(~modal, ~api, ~id, ~setDetail))}>
{React.string("Accept member")}
</Button>
</Button.Panel>
Expand All @@ -153,7 +171,7 @@ let viewOccupation = (occupation: MemberData.occupation) => {

@react.component
let make = (~api, ~id, ~modal) => {
let (detail: Api.webData<MemberData.detail>, _setDetail, _) =
let (detail: Api.webData<MemberData.detail>, setDetail, _) =
api->Hook.getData(~path="/members/" ++ Uuid.toString(id), ~decoder=MemberData.Decode.detail)

let status = RemoteData.map(detail, MemberData.getStatus)
Expand Down Expand Up @@ -264,7 +282,7 @@ let make = (~api, ~id, ~modal) => {
/>
</Tabbed.Content>
{switch status {
| Success(s) => <Actions status=s modal api id />
| Success(s) => <Actions status=s modal api id setDetail />
| _ => React.null
}}
</Page>
Expand Down
1 change: 1 addition & 0 deletions orca/src/api/members/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ pub fn assing_member_oid_sub<'a>(id: Id<Member>, uuid: Uuid) -> QueryAs<'a, Deta
"
UPDATE members
SET sub = $2
, onboarding_finished_at = NOW()
WHERE id = $1
RETURNING id
, member_number
Expand Down
3 changes: 3 additions & 0 deletions orca/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ impl<'r> Responder<'r, 'static> for oid::Error {
BadToken(_) => Err(Status::Unauthorized),
Http(_) => Err(Status::BadGateway),
Parsing(_) => Err(Status::InternalServerError),
Proxy(status_code) => Err(rocket::http::Status {
code: status_code.as_u16(),
}),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion orca/src/api/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async fn pair_by_email<'r>(
oid_provider: &State<Provider>,
token: JwtToken<'r>,
) -> Response<Json<SessionInfo>> {
// TODO: shouw we require some role for this action?
// TODO: should we require some role for this action?
// in a way we're already trusting token so maybe we can also just
// let any member assing themselves.
let token_data = oid_provider.inner().decode_jwt(&token)?;
Expand Down
5 changes: 3 additions & 2 deletions orca/src/server/oid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ use self::keycloak::KeycloakProvider;

#[derive(Debug, sqlx::FromRow)]
pub struct User {
first_name: String,
last_name: String,
email: String,
first_name: Option<String>,
last_name: Option<String>,
}

#[derive(Debug, Clone, FromForm)]
Expand Down Expand Up @@ -182,6 +182,7 @@ pub enum Error {
Disabled,
Http(reqwest::Error),
Parsing(String),
Proxy(reqwest::StatusCode),
}

impl From<jwk::Error> for Error {
Expand Down
45 changes: 25 additions & 20 deletions orca/src/server/oid/keycloak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,29 +132,34 @@ impl OidProvider for KeycloakProvider {
.send()
.await?;

debug!("Keycloak response status: {}", response.status());
let status = response.status();
debug!("Keycloak response status: {}", status);
debug!("Keycloak response: {:?}", response);

// Keycloak responds with empty body but it includes `Location` with full
// api path to new user resource.
// Last segment of this path is uuid identifying new user so we can parse it out of this header.
match response.headers().get("Location") {
Some(header) => {
let string = header
.to_str()
.map_err(|_| Error::Parsing(format!("Bad header {:?}", header)))?;
let url = Url::parse(string)
.map_err(|_| Error::Parsing(format!("Expected URL got {}", string)))?;
let uuid = url
.path_segments()
.ok_or(Error::Parsing(format!("Bad url {url}")))?
.last()
.ok_or(Error::Parsing(format!("Bad url {url}")))?;

Uuid::parse_str(uuid)
.map_err(|_| Error::Parsing(format!("Canot parse UUID from {}", uuid)))
if status.is_success() {
// Keycloak responds with empty body but it includes `Location` with full
// api path to new user resource.
// Last segment of this path is uuid identifying new user so we can parse it out of this header.
match response.headers().get("Location") {
Some(header) => {
let string = header
.to_str()
.map_err(|_| Error::Parsing(format!("Bad header {:?}", header)))?;
let url = Url::parse(string)
.map_err(|_| Error::Parsing(format!("Expected URL got {}", string)))?;
let uuid = url
.path_segments()
.ok_or(Error::Parsing(format!("Bad url {url}")))?
.last()
.ok_or(Error::Parsing(format!("Bad url {url}")))?;

Uuid::parse_str(uuid)
.map_err(|_| Error::Parsing(format!("Canot parse UUID from {}", uuid)))
}
None => Err(Error::Parsing("Missing Location header".to_string())),
}
None => Err(Error::Parsing("Missing Location header".to_string())),
} else {
Err(Error::Proxy(status))
}
}
}

0 comments on commit 44320e9

Please sign in to comment.