diff --git a/backend/migrations/20240406023149_create_users.sql b/backend/migrations/20240406023149_create_users.sql index f862c0da..8dc86bf3 100644 --- a/backend/migrations/20240406023149_create_users.sql +++ b/backend/migrations/20240406023149_create_users.sql @@ -5,6 +5,8 @@ CREATE TABLE users ( email TEXT NOT NULL UNIQUE, zid TEXT, name TEXT NOT NULL, + pronouns TEXT NOT NULL, + gender TEXT NOT NULL, degree_name TEXT, degree_starting_year INTEGER, role user_role NOT NULL, diff --git a/backend/server/src/handler/mod.rs b/backend/server/src/handler/mod.rs index 3273c2a7..e0a5a8f6 100644 --- a/backend/server/src/handler/mod.rs +++ b/backend/server/src/handler/mod.rs @@ -1,5 +1,6 @@ pub mod auth; +pub mod user; pub mod campaign; pub mod organisation; pub mod role; -pub mod application; \ No newline at end of file +pub mod application; diff --git a/backend/server/src/handler/user.rs b/backend/server/src/handler/user.rs new file mode 100644 index 00000000..4909ba91 --- /dev/null +++ b/backend/server/src/handler/user.rs @@ -0,0 +1,77 @@ +use crate::models::app::AppState; +use crate::models::auth::AuthUser; +use crate::models::error::ChaosError; +use axum::extract::{Json, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use crate::models::user::{User, UserDegree, UserGender, UserName, UserPronouns, UserZid}; + +pub struct UserHandler; + +impl UserHandler { + pub async fn get( + State(state): State, + user: AuthUser, + ) -> Result { + let user = User::get(user.user_id, &state.db).await?; + Ok((StatusCode::OK, Json(user))) + } + + pub async fn update_name( + State(state): State, + user: AuthUser, + Json(request_body): Json, + ) -> Result { + User::update_name(user.user_id, request_body.name, &state.db).await?; + + Ok((StatusCode::OK, "Updated username")) + } + + pub async fn update_pronouns( + State(state): State, + user: AuthUser, + Json(request_body): Json, + ) -> Result { + User::update_pronouns(user.user_id, request_body.pronouns, &state.db).await?; + + Ok((StatusCode::OK, "Updated pronouns")) + } + + pub async fn update_gender( + State(state): State, + user: AuthUser, + Json(request_body): Json, + ) -> Result { + User::update_gender(user.user_id, request_body.gender, &state.db).await?; + + Ok((StatusCode::OK, "Updated gender")) + } + + pub async fn update_zid( + State(state): State, + user: AuthUser, + Json(request_body): Json, + ) -> Result { + User::update_zid(user.user_id, request_body.zid, &state.db).await?; + + Ok((StatusCode::OK, "Updated zid")) + } + + pub async fn update_degree( + State(state): State, + user: AuthUser, + Json(request_body): Json, + ) -> Result { + User::update_degree( + user.user_id, + request_body.degree_name, + request_body.degree_starting_year, + &state.db + ) + .await?; + + Ok((StatusCode::OK, "Updated user degree")) + } +} + + diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index d2a0618c..a0a7eaac 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,4 +1,5 @@ use crate::handler::auth::google_callback; +use handler::user::UserHandler; use crate::handler::campaign::CampaignHandler; use crate::handler::organisation::OrganisationHandler; use crate::handler::application::ApplicationHandler; @@ -67,6 +68,12 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(|| async { "Hello, World!" })) .route("/api/auth/callback/google", get(google_callback)) + .route("/api/v1/user", get(UserHandler::get)) + .route("/api/v1/user/name", patch(UserHandler::update_name)) + .route("/api/v1/user/pronouns", patch(UserHandler::update_pronouns)) + .route("/api/v1/user/gender", patch(UserHandler::update_gender)) + .route("/api/v1/user/zid", patch(UserHandler::update_zid)) + .route("/api/v1/user/degree", patch(UserHandler::update_degree)) .route( "/api/v1/user/applications", get(ApplicationHandler::get_from_curr_user), diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index da002b22..65e55d21 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{FromRow, Pool, Postgres, Transaction}; use std::ops::DerefMut; -use crate::models::user::User; +use crate::models::user::UserDetails; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Application { @@ -38,7 +38,7 @@ pub struct NewApplication { pub struct ApplicationDetails { pub id: i64, pub campaign_id: i64, - pub user: User, + pub user: UserDetails, pub status: ApplicationStatus, pub private_status: ApplicationStatus, pub applied_roles: Vec @@ -52,6 +52,8 @@ pub struct ApplicationData { pub user_email: String, pub user_zid: Option, pub user_name: String, + pub user_pronouns: String, + pub user_gender: String, pub user_degree_name: Option, pub user_degree_starting_year: Option, pub status: ApplicationStatus, @@ -122,7 +124,8 @@ impl Application { " SELECT a.id AS id, campaign_id, user_id, status AS \"status: ApplicationStatus\", private_status AS \"private_status: ApplicationStatus\", u.email AS user_email, - u.zid AS user_zid, u.name AS user_name, u.degree_name AS user_degree_name, + u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, + u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id WHERE a.id = $1 @@ -153,11 +156,13 @@ impl Application { status: application_data.status, private_status: application_data.private_status, applied_roles, - user: User { + user: UserDetails { id: application_data.user_id, email: application_data.user_email, zid: application_data.user_zid, name: application_data.user_name, + pronouns: application_data.user_pronouns, + gender: application_data.user_gender, degree_name: application_data.user_degree_name, degree_starting_year: application_data.user_degree_starting_year, }, @@ -176,7 +181,8 @@ impl Application { " SELECT a.id AS id, campaign_id, user_id, status AS \"status: ApplicationStatus\", private_status AS \"private_status: ApplicationStatus\", u.email AS user_email, - u.zid AS user_zid, u.name AS user_name, u.degree_name AS user_degree_name, + u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, + u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id LEFT JOIN application_roles ar on ar.application_id = a.id WHERE ar.id = $1 @@ -208,11 +214,13 @@ impl Application { status: application_data.status, private_status: application_data.private_status, applied_roles, - user: User { + user: UserDetails { id: application_data.user_id, email: application_data.user_email, zid: application_data.user_zid, name: application_data.user_name, + pronouns: application_data.user_pronouns, + gender: application_data.user_gender, degree_name: application_data.user_degree_name, degree_starting_year: application_data.user_degree_starting_year, }, @@ -234,7 +242,8 @@ impl Application { " SELECT a.id AS id, campaign_id, user_id, status AS \"status: ApplicationStatus\", private_status AS \"private_status: ApplicationStatus\", u.email AS user_email, - u.zid AS user_zid, u.name AS user_name, u.degree_name AS user_degree_name, + u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, + u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id WHERE a.campaign_id = $1 @@ -266,11 +275,13 @@ impl Application { status: application_data.status, private_status: application_data.private_status, applied_roles, - user: User { + user: UserDetails { id: application_data.user_id, email: application_data.user_email, zid: application_data.user_zid, name: application_data.user_name, + pronouns: application_data.user_pronouns, + gender: application_data.user_gender, degree_name: application_data.user_degree_name, degree_starting_year: application_data.user_degree_starting_year, }, @@ -292,7 +303,8 @@ impl Application { " SELECT a.id AS id, campaign_id, user_id, status AS \"status: ApplicationStatus\", private_status AS \"private_status: ApplicationStatus\", u.email AS user_email, - u.zid AS user_zid, u.name AS user_name, u.degree_name AS user_degree_name, + u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, + u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year FROM applications a LEFT JOIN users u ON u.id = a.user_id WHERE a.user_id = $1 @@ -324,11 +336,13 @@ impl Application { status: application_data.status, private_status: application_data.private_status, applied_roles, - user: User { + user: UserDetails { id: application_data.user_id, email: application_data.user_email, zid: application_data.user_zid, name: application_data.user_name, + pronouns: application_data.user_pronouns, + gender: application_data.user_gender, degree_name: application_data.user_degree_name, degree_starting_year: application_data.user_degree_starting_year, }, diff --git a/backend/server/src/models/auth.rs b/backend/server/src/models/auth.rs index c120284a..9e1fc040 100644 --- a/backend/server/src/models/auth.rs +++ b/backend/server/src/models/auth.rs @@ -14,6 +14,7 @@ use axum::{async_trait, RequestPartsExt}; use axum_extra::{headers::Cookie, TypedHeader}; use serde::{Deserialize, Serialize}; +// tells the web framework how to take the url query params they will have #[derive(Deserialize, Serialize)] pub struct AuthRequest { pub code: String, @@ -39,6 +40,8 @@ pub struct AuthUser { pub user_id: i64, } +// extractor - takes a request, and we define what we do to it, +// returns the struct of the type defined #[async_trait] impl FromRequestParts for AuthUser where diff --git a/backend/server/src/models/storage.rs b/backend/server/src/models/storage.rs index a94c6dea..d44b35aa 100644 --- a/backend/server/src/models/storage.rs +++ b/backend/server/src/models/storage.rs @@ -1,6 +1,6 @@ use crate::models::error::ChaosError; use s3::creds::Credentials; -use s3::{Bucket, BucketConfiguration, Region}; +use s3::{Bucket, Region}; use std::env; pub struct Storage; diff --git a/backend/server/src/models/user.rs b/backend/server/src/models/user.rs index f91e9ce7..19009a1f 100644 --- a/backend/server/src/models/user.rs +++ b/backend/server/src/models/user.rs @@ -1,4 +1,6 @@ use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, Pool, Postgres}; +use crate::models::error::ChaosError; #[derive(Deserialize, Serialize, sqlx::Type, Clone)] #[sqlx(type_name = "user_role", rename_all = "PascalCase")] @@ -7,13 +9,147 @@ pub enum UserRole { SuperUser, } -// Placeholder until User CRUD is merged -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, FromRow)] +pub struct UserDetails { + pub id: i64, + pub email: String, + pub zid: Option, + pub name: String, + pub pronouns: String, + pub gender: String, + pub degree_name: Option, + pub degree_starting_year: Option, +} + +#[derive(Deserialize, Serialize, FromRow)] pub struct User { pub id: i64, pub email: String, pub zid: Option, pub name: String, + pub pronouns: String, + pub gender: String, pub degree_name: Option, pub degree_starting_year: Option, -} \ No newline at end of file + pub role: UserRole, +} + +#[derive(Deserialize, Serialize)] +pub struct UserName { + pub name: String, +} + +#[derive(Deserialize, Serialize)] +pub struct UserPronouns { + pub pronouns: String, +} + +#[derive(Deserialize, Serialize)] +pub struct UserGender { + pub gender: String, +} + +#[derive(Deserialize, Serialize)] +pub struct UserZid { + pub zid: String, +} + +#[derive(Deserialize, Serialize)] +pub struct UserDegree { + pub degree_name: String, + pub degree_starting_year: i32, +} + +impl User { + pub async fn get(id: i64, pool: &Pool) -> Result { + let user = sqlx::query_as!( + User, + r#" + SELECT id, email, zid, name, pronouns, gender, degree_name, + degree_starting_year, role AS "role!: UserRole" + FROM users WHERE id = $1 + "#, + id + ) + .fetch_one(pool) + .await?; + + Ok(user) + } + + pub async fn update_name(id: i64, name: String, pool: &Pool) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE users SET name = $1 WHERE id = $2 RETURNING id + ", + name, + id + ) + .fetch_one(pool) + .await?; + + Ok(()) + } + + pub async fn update_pronouns(id: i64, pronouns: String, pool: &Pool) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE users SET pronouns = $1 WHERE id = $2 RETURNING id + ", + pronouns, + id + ) + .fetch_one(pool) + .await?; + + Ok(()) + } + + pub async fn update_gender(id: i64, gender: String, pool: &Pool) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE users SET gender = $1 WHERE id = $2 RETURNING id + ", + gender, + id + ) + .fetch_one(pool) + .await?; + + Ok(()) + } + + pub async fn update_zid(id: i64, zid: String, pool: &Pool) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE users SET zid = $1 WHERE id = $2 RETURNING id + ", + zid, + id + ) + .fetch_one(pool) + .await?; + + Ok(()) + } + + pub async fn update_degree( + id: i64, + degree_name: String, + degree_starting_year: i32, + pool: &Pool, + ) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE users SET degree_name = $1, degree_starting_year = $2 WHERE id = $3 RETURNING id + ", + degree_name, + degree_starting_year, + id + ) + .fetch_one(pool) + .await?; + + Ok(()) + } +}