Skip to content

Commit 40fc806

Browse files
authored
Chaos 584 integrate be fe adminmemberfunc (#593)
* temporarily override styles with classname in inputpopup component for adding members, colors were weird * before shad cning table * chopped ahh table * change to use lucide icons * shadcn dialog * add dropdown menu component * backend funcs * scuffed ahh linking to frontend * link be + fe(frontend slightly chopped) * frontend styling changes * frontend styling changes * switched from using email to using user_ids * allowed superusers to view the table and add members, thus had to enforce that only org admins can edit and delete users.
1 parent 1a69e13 commit 40fc806

File tree

23 files changed

+966
-326
lines changed

23 files changed

+966
-326
lines changed

backend/server/src/handler/application.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ impl ApplicationHandler {
4141
) -> Result<impl IntoResponse, ChaosError> {
4242
let application_id = Application::create_or_get(campaign_id, user.user_id, &mut state.snowflake_generator, &mut transaction.tx).await?;
4343
transaction.tx.commit().await?;
44-
4544
Ok((StatusCode::OK, Json(json!({ "application_id": application_id.to_string() }))))
4645
}
4746

backend/server/src/handler/organisation.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
//! - Logo image handling
99
1010
use crate::models::app::AppState;
11-
use crate::models::auth::SuperUser;
11+
use crate::models::auth::{OrganisationAdminOrSuperUser, SuperUser};
1212
use crate::models::auth::{AuthUser, OrganisationAdmin};
1313
use crate::models::campaign::{Campaign, NewCampaign};
1414
use crate::models::email_template::{EmailTemplate, NewEmailTemplate};
1515
use crate::models::error::ChaosError;
16-
use crate::models::organisation::{AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, OrganisationDetails, SlugCheck};
16+
use crate::models::organisation::{
17+
AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, OrganisationDetails, SlugCheck, EmailRoleBody, IdRoleBody, IdBody
18+
};
19+
use crate::service::user::{user_exists_by_email};
20+
use crate::service::organisation::{assert_user_is_not_in_organisation};
1721
use crate::models::transaction::DBTransaction;
1822
use crate::service::auth::assert_is_super_user;
1923
use axum::extract::{Json, Path, State};
@@ -220,7 +224,7 @@ impl OrganisationHandler {
220224
pub async fn get_members(
221225
mut transaction: DBTransaction<'_>,
222226
Path(id): Path<i64>,
223-
_admin: OrganisationAdmin,
227+
_admin: OrganisationAdminOrSuperUser,
224228
) -> Result<impl IntoResponse, ChaosError> {
225229
let members = Organisation::get_members(id, &mut transaction.tx).await?;
226230

@@ -327,9 +331,11 @@ impl OrganisationHandler {
327331
mut transaction: DBTransaction<'_>,
328332
Path(id): Path<i64>,
329333
_admin: OrganisationAdmin,
330-
Json(request_body): Json<AdminToRemove>,
334+
Json(req): Json<IdBody>,
331335
) -> Result<impl IntoResponse, ChaosError> {
332-
Organisation::remove_member(id, request_body.user_id, &mut transaction.tx).await?;
336+
let tx = &mut transaction.tx;
337+
//let user_id = user_exists_by_email(req.email, tx).await?;
338+
Organisation::remove_member(id, req.user_id, tx).await?;
333339

334340
transaction.tx.commit().await?;
335341
Ok((
@@ -508,4 +514,37 @@ impl OrganisationHandler {
508514
transaction.tx.commit().await?;
509515
Ok((StatusCode::OK, Json(email_templates)))
510516
}
517+
518+
pub async fn add_member(
519+
_user: OrganisationAdminOrSuperUser,
520+
mut transaction: DBTransaction<'_>,
521+
Path(org_id): Path<i64>,
522+
Json(req): Json<EmailRoleBody>,
523+
) -> Result<impl IntoResponse, ChaosError> {
524+
let tx = &mut transaction.tx;
525+
let user_id = user_exists_by_email(req.email, tx).await?;
526+
assert_user_is_not_in_organisation(user_id, org_id, tx).await?;
527+
Organisation::add_member_or_admin_to_organisation(org_id, user_id, req.role.clone(), tx).await?;
528+
529+
transaction.tx.commit().await?;
530+
Ok((StatusCode::OK, "Member added to organisation"))
531+
}
532+
533+
pub async fn update_member_role(
534+
_user: OrganisationAdmin,
535+
mut transaction: DBTransaction<'_>,
536+
Path(org_id): Path<i64>,
537+
Json(req): Json<IdRoleBody>,
538+
) -> Result<impl IntoResponse, ChaosError> {
539+
let tx = &mut transaction.tx;
540+
Organisation::change_member_role(
541+
org_id,
542+
req.user_id,
543+
req.role,
544+
tx
545+
)
546+
.await?;
547+
transaction.tx.commit().await?;
548+
Ok(StatusCode::OK)
549+
}
511550
}

backend/server/src/models/app.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ pub async fn app() -> Result<Router, ChaosError> {
195195
)
196196
.route(
197197
"/api/v1/organisation/:organisation_id/member",
198-
delete(OrganisationHandler::remove_member),
198+
delete(OrganisationHandler::remove_member)
199+
.put(OrganisationHandler::update_member_role)
200+
.post(OrganisationHandler::add_member),
199201
)
200202
.route(
201203
"/api/v1/organisation/:organisation_id/admins",

backend/server/src/models/auth.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::service::auth::{assert_is_super_user, extract_user_id_from_request};
1212
use crate::service::campaign::user_is_campaign_admin;
1313
use crate::service::email_template::user_is_email_template_admin;
1414
use crate::service::offer::{assert_user_is_offer_admin, assert_user_is_offer_recipient};
15-
use crate::service::organisation::assert_user_is_organisation_admin;
15+
use crate::service::organisation::{assert_user_is_organisation_admin, assert_user_is_organisation_admin_or_super_user};
1616
use crate::service::question::user_is_question_admin;
1717
use crate::service::rating::{
1818
assert_user_is_application_reviewer_given_rating_id, assert_user_is_organisation_member,
@@ -172,6 +172,45 @@ where
172172
}
173173
}
174174

175+
/// Organisation admin or super user information
176+
///
177+
/// Contains the user ID of a user that may either be an organisation admin or a super user.
178+
pub struct OrganisationAdminOrSuperUser {
179+
/// ID of the superuser or org admin
180+
pub user_id: i64,
181+
}
182+
183+
/// Extractor for organization administrators or superusers, for when a certain action can be done by both.
184+
///
185+
/// This extractor is used in route handlers to ensure that the request
186+
/// comes from a user with organization administrator privileges.
187+
#[async_trait]
188+
impl<S> FromRequestParts<S> for OrganisationAdminOrSuperUser
189+
where
190+
AppState: FromRef<S>,
191+
S: Send + Sync,
192+
{
193+
type Rejection = ChaosError;
194+
195+
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
196+
let app_state = AppState::from_ref(state);
197+
let user_id = extract_user_id_from_request(parts, &app_state).await?;
198+
199+
let organisation_id = *parts
200+
.extract::<Path<HashMap<String, i64>>>()
201+
.await
202+
.map_err(|_| ChaosError::BadRequest)?
203+
.get("organisation_id")
204+
.ok_or(ChaosError::BadRequest)?;
205+
206+
let mut tx = app_state.db.begin().await?;
207+
assert_user_is_organisation_admin_or_super_user(user_id, organisation_id, &mut tx).await?;
208+
tx.commit().await?;
209+
210+
Ok(OrganisationAdminOrSuperUser { user_id })
211+
}
212+
}
213+
175214
/// Campaign administrator information.
176215
///
177216
/// Contains the user ID of a user with campaign administrator privileges.

backend/server/src/models/organisation.rs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ pub struct Member {
9999
pub name: String,
100100
/// User's role in the organisation
101101
pub role: OrganisationRole,
102+
/// User's email
103+
pub email: String,
102104
}
103105

104106
/// Collection of organisation members.
@@ -127,12 +129,52 @@ pub struct AdminUpdateList {
127129
/// from the administrator role.
128130
#[derive(Deserialize, Serialize)]
129131
pub struct AdminToRemove {
132+
#[serde(deserialize_with = "crate::models::serde_string::deserialize")]
130133
/// ID of the user to remove as administrator
131134
pub user_id: i64,
132135
}
133136

134-
/// Data structure for checking slug availability.
137+
/// Data structure for adding a user
135138
///
139+
/// This struct contains a user's email, and the role for that user.
140+
#[derive(Deserialize, Serialize)]
141+
pub struct EmailRoleBody {
142+
// email
143+
pub email: String,
144+
// role
145+
pub role: OrganisationRole
146+
}
147+
148+
/// Data structure for passing in a user's email
149+
///
150+
/// This struct contains a user's email
151+
#[derive(Deserialize, Serialize)]
152+
pub struct EmailBody {
153+
// email
154+
pub email: String
155+
}
156+
157+
/// Data structure for passing in a user's email
158+
///
159+
/// This struct contains a user's email
160+
#[derive(Deserialize, Serialize)]
161+
pub struct IdBody {
162+
// email
163+
#[serde(deserialize_with = "crate::models::serde_string::deserialize")]
164+
pub user_id: i64
165+
}
166+
167+
/// Data structure for passing in a user's email and role
168+
///
169+
/// This struct contains a user's email
170+
#[derive(Deserialize, Serialize)]
171+
pub struct IdRoleBody {
172+
// email
173+
#[serde(deserialize_with = "crate::models::serde_string::deserialize")]
174+
pub user_id: i64,
175+
pub role: OrganisationRole
176+
}
177+
136178
/// This struct contains a slug to check for availability
137179
/// when creating a new organisation.
138180
#[derive(Deserialize)]
@@ -377,7 +419,7 @@ impl Organisation {
377419
let admin_list = sqlx::query_as!(
378420
Member,
379421
"
380-
SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members
422+
SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name, users.email from organisation_members
381423
JOIN users on users.id = organisation_members.user_id
382424
WHERE organisation_members.organisation_id = $1 AND organisation_members.role = $2
383425
",
@@ -409,7 +451,7 @@ impl Organisation {
409451
let admin_list = sqlx::query_as!(
410452
Member,
411453
"
412-
SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members
454+
SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name, users.email from organisation_members
413455
JOIN users on users.id = organisation_members.user_id
414456
WHERE organisation_members.organisation_id = $1
415457
",
@@ -678,4 +720,49 @@ impl Organisation {
678720

679721
Ok(id)
680722
}
723+
724+
pub async fn add_member_or_admin_to_organisation(
725+
organisation_id: i64,
726+
member_id: i64,
727+
role: OrganisationRole, //was gonna make bool but we need to extend for other members jsut brute force w/ match arm rn
728+
transaction: &mut Transaction<'_, Postgres>,
729+
) -> Result<i64, ChaosError> {
730+
731+
let _ = sqlx::query!(
732+
"INSERT INTO organisation_members(organisation_id, user_id, role)
733+
VALUES ($1, $2, $3)",
734+
organisation_id,
735+
member_id,
736+
role as OrganisationRole
737+
)
738+
.execute(transaction.deref_mut())
739+
.await?;
740+
741+
Ok(member_id)
742+
}
743+
744+
pub async fn change_member_role(
745+
organisation_id: i64,
746+
member_id: i64,
747+
new_role: OrganisationRole,
748+
transaction: &mut Transaction<'_, Postgres>,
749+
) -> Result<(), ChaosError> {
750+
let result = sqlx::query!(
751+
"UPDATE organisation_members
752+
SET role = $1
753+
WHERE organisation_id = $2 AND user_id = $3",
754+
new_role as OrganisationRole,
755+
organisation_id,
756+
member_id
757+
)
758+
.execute(transaction.deref_mut())
759+
.await?;
760+
761+
// User didn't exist if nothing is affected
762+
if result.rows_affected() == 0 {
763+
return Err(ChaosError::BadRequest);
764+
}
765+
766+
Ok(())
767+
}
681768
}

backend/server/src/service/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ pub mod organisation;
2828
pub mod question;
2929
pub mod rating;
3030
pub mod role;
31+
pub mod user;

backend/server/src/service/organisation.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use crate::models::error::ChaosError;
77
use sqlx::{Pool, Postgres, Transaction};
8+
use crate::models::user::UserRole;
89
use std::ops::DerefMut;
910

1011
/// Verifies if a user has admin privileges for an organisation.
@@ -39,3 +40,81 @@ pub async fn assert_user_is_organisation_admin(
3940

4041
Ok(())
4142
}
43+
44+
/// Verifies if a user is in an organization
45+
///
46+
/// This function checks if the user is a member of the specified organisation.
47+
///
48+
/// # Arguments
49+
///
50+
/// * `user_id` - The ID of the user to check
51+
/// * `organisation_id` - The ID of the organisation
52+
/// * `pool` - Database connection pool
53+
///
54+
/// # Returns
55+
///
56+
/// * `Result<(), ChaosError>` - Ok if the user is a member, Unauthorized error otherwise
57+
pub async fn assert_user_is_not_in_organisation(
58+
user_id: i64,
59+
organisation_id: i64,
60+
transaction: &mut Transaction<'_, Postgres>,
61+
) -> Result<(), ChaosError> {
62+
let in_organization = sqlx::query!(
63+
"SELECT EXISTS(SELECT 1 FROM organisation_members WHERE organisation_id = $1 AND user_id = $2)",
64+
organisation_id,
65+
user_id
66+
)
67+
.fetch_one(transaction.deref_mut())
68+
.await?.exists.expect("`exists` should always exist in this query result");
69+
70+
if in_organization{
71+
return Err(ChaosError::Unauthorized);
72+
}
73+
74+
Ok(())
75+
}
76+
77+
/// Verifies if a user has admin privileges for an organisation or is a super user.
78+
///
79+
/// This function checks if the user is an admin member of the specified organisation or is a super user.
80+
///
81+
/// # Arguments
82+
///
83+
/// * `user_id` - The ID of the user to check
84+
/// * `organisation_id` - The ID of the organisation
85+
/// * `pool` - Database connection pool
86+
///
87+
/// # Returns
88+
///
89+
/// * `Result<(), ChaosError>` - Ok if the user is an admin, Unauthorized error otherwise
90+
pub async fn assert_user_is_organisation_admin_or_super_user(
91+
user_id: i64,
92+
organisation_id: i64,
93+
transaction: &mut Transaction<'_, Postgres>,
94+
) -> Result<(), ChaosError> {
95+
let is_admin = sqlx::query!(
96+
"SELECT EXISTS(SELECT 1 FROM organisation_members WHERE organisation_id = $1 AND user_id = $2 AND role = 'Admin')",
97+
organisation_id,
98+
user_id
99+
)
100+
.fetch_one(transaction.deref_mut())
101+
.await?.exists.expect("`exists` should always exist in this query result");
102+
103+
let is_super_user = sqlx::query!(
104+
"SELECT EXISTS(SELECT 1 FROM users WHERE id = $1 AND role = $2)",
105+
user_id,
106+
UserRole::SuperUser as UserRole
107+
)
108+
.fetch_one(transaction.deref_mut())
109+
.await?
110+
.exists
111+
.expect("`exists` should always exist in this query result");
112+
113+
if !is_admin && !is_super_user{
114+
return Err(ChaosError::Unauthorized);
115+
}
116+
117+
Ok(())
118+
}
119+
120+

backend/server/src/service/role.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ pub async fn user_is_role_admin(
5151
}
5252

5353
Ok(())
54-
}
54+
}

0 commit comments

Comments
 (0)