Skip to content

Commit

Permalink
Workplaces (#178)
Browse files Browse the repository at this point in the history
added DB migrations for workplaces, new roles to Keycloak and EPs to
get, create and assign workplace.

We still need to add EPs to list all members assigned to workplace. I
will hopefully do it tomorrow, but I would appreciate review on the
current code, in case there will be some obvious mistake and I would
need to rewrite it :)

---------

[anonymized]
  • Loading branch information
Kubik161 authored and ICTGuerrilla committed Aug 26, 2024
1 parent 9eb01db commit 0f2eaca
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 2 deletions.
3 changes: 2 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ services:
POSTGRES_HOST_AUTH_METHOD: "trust"
ports:
- "5432:5432"

keycloak:
image: quay.io/keycloak/keycloak:23.0.3
environment:
Expand All @@ -34,3 +33,5 @@ services:
- "./docker/keycloak/realm.json:/opt/keycloak/data/import/realm.json"
ports:
- "8180:8180"
depends_on:
- postgres
22 changes: 21 additions & 1 deletion docker/keycloak/realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,24 @@
"clientRole": true,
"containerId": "3577db7d-91cf-4d27-898e-c1c8a73b2a6b",
"attributes": {}
},
{
"id": "6e9e0b13-452f-415a-986f-b41f708a9813",
"name": "list-workplaces",
"description": "Listing of workplaces",
"composite": false,
"clientRole": true,
"containerId": "3577db7d-91cf-4d27-898e-c1c8a73b2a6b",
"attributes": {}
},
{
"id": "fdff6901-df97-4224-a1bd-db02648f464b",
"name": "manage-workplaces",
"description": "Manage (Create) workplaces",
"composite": false,
"clientRole": true,
"containerId": "3577db7d-91cf-4d27-898e-c1c8a73b2a6b",
"attributes": {}
}
],
"security-admin-console": [],
Expand Down Expand Up @@ -483,7 +501,9 @@
"resolve-applications",
"manage-members",
"list-applications",
"view-member"
"view-member",
"list-workplaces",
"manage-workplaces"
]
}
}
Expand Down
5 changes: 5 additions & 0 deletions gray-whale/migrations/V18__remove_chairperson.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE workplaces
DROP COLUMN chairperson_id,
ADD COLUMN email TEXT NOT NULL;

COMMENT ON COLUMN workplaces.email IS 'Email that was created for the workplace in our mail server';
7 changes: 7 additions & 0 deletions gray-whale/migrations/V19__member_workplace_junction.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE members_workplaces
( member_id UUID NOT NULL REFERENCES members(id)
, workplace_id UUID NOT NULL REFERENCES workplaces(id)
, PRIMARY KEY (member_id, workplace_id)
);

COMMENT ON TABLE members_workplaces IS 'Members Workplaces is junction table for Many-to-Many relation between members and workplaces';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Grant permissions for new table
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE members_workplaces TO orca;
8 changes: 8 additions & 0 deletions melon-head/src/App/Settings.res
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ let make = (~api: Api.t, ~session: Api.webData<Session.t>) => {
"Manage realm users",
session => {<ViewBool value={Session.hasRealmRole(session, ~role=Session.ManageUsers)} />},
),
(
"List all workplaces",
session => {<ViewBool value={Session.hasRole(session, ~role=Session.ListWorkplaces)} />},
),
(
"Manage workplaces",
session => {<ViewBool value={Session.hasRole(session, ~role=Session.ManageWorkplaces)} />},
),
(
"Super-Powers (be careful!)",
session => {<ViewBool value={Session.hasRole(session, ~role=Session.ManageMembers)} />},
Expand Down
6 changes: 6 additions & 0 deletions melon-head/src/Data/Session.res
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type orcaRole =
| ListMembers
| ViewMember
| ManageMembers
| ListWorkplaces
| ManageWorkplaces
| SuperPowers

let showOrcaRole = (r: orcaRole): string =>
Expand All @@ -19,6 +21,8 @@ let showOrcaRole = (r: orcaRole): string =>
| ListMembers => "list-members"
| ViewMember => "view-member"
| ManageMembers => "manage-members"
| ListWorkplaces => "list-workplaces"
| ManageWorkplaces => "manage-workplaces"
| SuperPowers => "super-powers"
}

Expand Down Expand Up @@ -76,6 +80,8 @@ module Decode = {
| "list-members" => ListMembers
| "view-member" => ViewMember
| "manage-members" => ManageMembers
| "list-workplaces" => ListWorkplaces
| "manage-workplaces" => ManageWorkplaces
| "super-powers" => SuperPowers
| _ => UnknownOrcaRole(str)
}
Expand Down
2 changes: 2 additions & 0 deletions orca/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ which needs to be configured for the `keycloak_client_id` set in the `Rocket.tom
| list-members | List of all past and current members |
| view-member | Accept detail of individual member (by uuid) |
| manage-members | Manage (Create, Remove) members |
| list-workplaces | List of all workplaces |
| manage-workplaces | Manage (Create, Edit) workplaces |
| super-powers | Dangerous actions like hard delete of data |

## Developing
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 @@ -11,6 +11,7 @@ mod members;
mod registration;
mod session;
mod stats;
mod workplaces;

use crate::db::{self, DbPool};
use crate::processing::SenderError;
Expand Down Expand Up @@ -234,6 +235,8 @@ pub fn build() -> Rocket<Build> {
.register("/stats", errors::catchers())
.mount("/members", members::routes())
.register("/members", errors::catchers())
.mount("/workplaces", workplaces::routes())
.register("/workplaces", errors::catchers())
// Files use default catchers
.mount("/files", files::routes())
}
Expand Down
145 changes: 145 additions & 0 deletions orca/src/api/workplaces/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use chrono::{DateTime, Utc};
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_validation::{Validate, Validated};
use serde::{Deserialize, Serialize};

use crate::api::members::Summary;
use crate::api::Response;
use crate::data::{Id, Workplace};
use crate::db::DbPool;
use crate::server::oid::{JwtToken, Provider, Role};

use super::SuccessResponse;
use uuid::Uuid;

pub mod query;

#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct WorkplaceSummary {
id: Id<Workplace>,
name: String,
email: String,
created_at: DateTime<Utc>,
}

#[get("/")]
async fn list_all<'r>(
db_pool: &State<DbPool>,
oid_provider: &State<Provider>,
token: JwtToken<'r>,
) -> Response<Json<Vec<WorkplaceSummary>>> {
oid_provider.require_role(&token, Role::ListWorkplaces)?;

let summaries = query::list_summaries().fetch_all(db_pool.inner()).await?;

Ok(Json(summaries))
}

#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct NewWorkplace {
#[validate(required)]
name: Option<String>,
#[validate(required)]
email: Option<String>,
}

#[post("/", format = "json", data = "<new_workplace>")]
async fn create_workplace<'r>(
db_pool: &State<DbPool>,
oid_provider: &State<Provider>,
token: JwtToken<'r>,
new_workplace: Validated<Json<NewWorkplace>>,
) -> Response<Json<WorkplaceSummary>> {
oid_provider.require_role(&token, Role::ManageWorkplaces)?;

// Create new workplace
let workplace = query::create_workplace(&new_workplace.into_inner().into_inner())
.fetch_one(db_pool.inner())
.await?;

Ok(Json(workplace))
}

#[derive(Debug, Serialize, Deserialize)]
pub struct NewWorkplaceMember {
member_id: Uuid,
}

#[post("/<workplace_id>", format = "json", data = "<new_workplace_member>")]
async fn assign_member_to_workplace<'r>(
db_pool: &State<DbPool>,
oid_provider: &State<Provider>,
token: JwtToken<'r>,
workplace_id: Id<Workplace>,
new_workplace_member: Json<NewWorkplaceMember>,
) -> Response<SuccessResponse> {
oid_provider.require_role(&token, Role::ManageWorkplaces)?;

// Create new connection
query::create_connection_between_member_and_workplace(
workplace_id,
new_workplace_member.into_inner().member_id,
)
.execute(db_pool.inner())
.await?;

Ok(SuccessResponse::Accepted)
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RemovedWorkplaceMember {
member_id: Uuid,
}

#[delete(
"/<workplace_id>",
format = "json",
data = "<removed_workplace_member>"
)]
async fn remove_member_from_workplace<'r>(
db_pool: &State<DbPool>,
oid_provider: &State<Provider>,
token: JwtToken<'r>,
workplace_id: Id<Workplace>,
removed_workplace_member: Json<RemovedWorkplaceMember>,
) -> Response<SuccessResponse> {
oid_provider.require_role(&token, Role::ManageWorkplaces)?;

// Remove existing connection
query::remove_connection_between_member_and_workplace(
workplace_id,
removed_workplace_member.into_inner().member_id,
)
.execute(db_pool.inner())
.await?;

Ok(SuccessResponse::Accepted)
}

#[get("/<workplace_id>")]
async fn get_all_workplace_members<'r>(
db_pool: &State<DbPool>,
oid_provider: &State<Provider>,
token: JwtToken<'r>,
workplace_id: Id<Workplace>,
) -> Response<Json<Vec<Summary>>> {
oid_provider.require_role(&token, Role::ListWorkplaces)?;
oid_provider.require_role(&token, Role::ListMembers)?;

let summaries = query::get_all_workplace_members(workplace_id)
.fetch_all(db_pool.inner())
.await?;

Ok(Json(summaries))
}

pub fn routes() -> Vec<Route> {
routes![
list_all,
create_workplace,
assign_member_to_workplace,
remove_member_from_workplace,
get_all_workplace_members,
]
}
105 changes: 105 additions & 0 deletions orca/src/api/workplaces/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use uuid::Uuid;

use super::{NewWorkplace, WorkplaceSummary};
use crate::api::members::Summary;
use crate::data::{Id, Workplace};
use crate::db::{Query, QueryAs};

pub fn list_summaries() -> QueryAs<'static, WorkplaceSummary> {
sqlx::query_as(
"
SELECT id
, name
, email
, created_at
FROM workplaces
ORDER BY created_at DESC
",
)
}

pub fn create_workplace(new_workplace: &NewWorkplace) -> QueryAs<'_, WorkplaceSummary> {
sqlx::query_as(
"
INSERT INTO workplaces
( name
, email
)
VALUES
( $1, $2 )
RETURNING id
, name
, email
, created_at
",
)
.bind(&new_workplace.name)
.bind(&new_workplace.email)
}

pub fn create_connection_between_member_and_workplace<'a>(
workplace_id: Id<Workplace>,
member_id: Uuid,
) -> Query<'a> {
sqlx::query(
"
INSERT INTO members_workplaces
( workplace_id
, member_id
)
VALUES
( $1, $2 )
",
)
.bind(workplace_id)
.bind(member_id)
}

pub fn remove_connection_between_member_and_workplace<'a>(
workplace_id: Id<Workplace>,
member_id: Uuid,
) -> Query<'a> {
sqlx::query(
"
DELETE FROM members_workplaces
WHERE
workplace_id=$1 AND member_id=$2
",
)
.bind(workplace_id)
.bind(member_id)
}

pub fn get_all_workplace_members<'a>(workplace_id: Id<Workplace>) -> QueryAs<'a, Summary> {
sqlx::query_as(
"
SELECT m.id
, m.member_number
, m.first_name
, m.last_name
, m.email
, m.note
, m.phone_number
, m.city
, m.left_at
, array_agg(o.company_name ORDER BY o.created_at DESC) AS company_names
, m.created_at
FROM members_current AS m
LEFT JOIN occupations o ON o.member_id = m.id
LEFT JOIN members_workplaces mw ON mw.member_id = m.id
WHERE mw.workplace_id = $1
GROUP BY m.id
, m.member_number
, m.first_name
, m.last_name
, m.email
, m.phone_number
, m.note
, m.city
, m.left_at
, m.created_at
ORDER BY m.member_number DESC
",
)
.bind(workplace_id)
}
3 changes: 3 additions & 0 deletions orca/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ pub struct RegistrationRequest;
#[derive(Debug, Clone, Copy)]
pub struct Member;

#[derive(Debug, Clone, Copy)]
pub struct Workplace;

#[derive(Debug, Clone, Copy)]
pub struct MemberNumber(i32);

Expand Down
Loading

0 comments on commit 0f2eaca

Please sign in to comment.