Skip to content

Commit c577fc0

Browse files
committed
feat: add invitations for pms
1 parent 8d300ff commit c577fc0

File tree

12 files changed

+273
-30
lines changed

12 files changed

+273
-30
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ALTER TABLE users DROP CONSTRAINT valid_role;
2+
ALTER TABLE users ADD CONSTRAINT valid_role
3+
CHECK (role IN ('user', 'admin'));
4+
5+
UPDATE users SET role = 'user' WHERE role = 'developer';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ALTER TABLE users DROP CONSTRAINT valid_role;
2+
ALTER TABLE users ADD CONSTRAINT valid_role
3+
CHECK (role IN ('admin', 'project_manager', 'developer'));
4+
5+
UPDATE users SET role = 'developer' WHERE role = 'user';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE dev_pm_relationships;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
CREATE TABLE dev_pm_relationships (
2+
id SERIAL PRIMARY KEY,
3+
developer_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
4+
project_manager_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5+
status VARCHAR NOT NULL CHECK (status IN ('pending', 'accepted', 'rejected')),
6+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
8+
);
9+
10+
CREATE INDEX idx_dev_pm_developer ON dev_pm_relationships(developer_id);
11+
CREATE INDEX idx_dev_pm_manager ON dev_pm_relationships(project_manager_id);
12+
CREATE INDEX idx_dev_pm_status ON dev_pm_relationships(status);
13+
14+
CREATE UNIQUE INDEX idx_unique_dev_pm_relationship
15+
ON dev_pm_relationships(developer_id, project_manager_id)
16+
WHERE status != 'rejected';
17+
18+
SELECT diesel_manage_updated_at('dev_pm_relationships');

src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ mod db;
99
mod schema;
1010

1111
use crate::routes::auth::{login, register};
12-
use crate::routes::snack::{create_snack, delete_snack, list_snacks, update_snack};
12+
use crate::routes::relationship::{invite_project_manager, list_developers, list_snacks, respond_to_invite};
13+
use crate::routes::snack::{create_snack, delete_snack, update_snack};
1314
use dotenv::dotenv;
1415
use rocket::*;
1516

@@ -29,7 +30,7 @@ fn index() -> &'static str {
2930
fn rocket() -> _ {
3031
dotenv().ok();
3132

32-
rocket::build().mount("/", routes![index, create_snack, list_snacks, update_snack, delete_snack, register, login]).register("/", catchers![catchers::unauthorized, catchers::not_found,
33+
rocket::build().mount("/", routes![index, invite_project_manager,list_developers, respond_to_invite, create_snack, list_snacks, update_snack, delete_snack, register, login]).register("/", catchers![catchers::unauthorized, catchers::not_found,
3334
catchers::internal_server_error])
3435
}
3536

src/models/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod snack;
2-
pub(crate) mod user;
2+
pub(crate) mod user;
3+
pub(crate) mod relationship;

src/models/relationship.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// src/models/relationship.rs
2+
use crate::models::user::User;
3+
use chrono::NaiveDateTime;
4+
use diesel::prelude::*;
5+
use serde::{Deserialize, Serialize};
6+
use diesel::pg::Pg;
7+
use crate::schema::dev_pm_relationships;
8+
use crate::schema::dev_pm_relationships::dsl;
9+
10+
#[derive(Queryable, Selectable, Serialize, Identifiable, Associations, Debug)]
11+
#[diesel(belongs_to(User, foreign_key = developer_id))]
12+
#[diesel(table_name = crate::schema::dev_pm_relationships)]
13+
pub struct DevPmRelationship {
14+
pub id: i32,
15+
pub developer_id: i32,
16+
pub project_manager_id: i32,
17+
pub status: String,
18+
pub created_at: NaiveDateTime,
19+
pub updated_at: NaiveDateTime,
20+
}
21+
22+
#[derive(Insertable)]
23+
#[diesel(table_name = crate::schema::dev_pm_relationships)]
24+
pub struct NewDevPmRelationship {
25+
pub developer_id: i32,
26+
pub project_manager_id: i32,
27+
pub status: String,
28+
}
29+
30+
#[derive(Deserialize)]
31+
pub struct InvitePmRequest {
32+
pub project_manager_id: i32,
33+
}
34+
35+
#[derive(Deserialize)]
36+
pub struct RespondToInviteRequest {
37+
pub status: String,
38+
}
39+
40+
// Query builders helper methods
41+
impl DevPmRelationship {
42+
pub fn for_developer(user_id: i32) -> dev_pm_relationships::BoxedQuery<'static, Pg> {
43+
use crate::schema::dev_pm_relationships::dsl::*;
44+
45+
dev_pm_relationships
46+
.filter(developer_id.eq(user_id))
47+
.into_boxed()
48+
}
49+
50+
pub fn for_project_manager(user_id: i32) -> dev_pm_relationships::BoxedQuery<'static, Pg> {
51+
use crate::schema::dev_pm_relationships::dsl::*;
52+
53+
dev_pm_relationships
54+
.filter(project_manager_id.eq(user_id))
55+
.into_boxed()
56+
}
57+
58+
pub fn pending() -> dev_pm_relationships::BoxedQuery<'static, Pg> {
59+
use crate::schema::dev_pm_relationships::dsl::*;
60+
61+
dev_pm_relationships
62+
.filter(status.eq("pending"))
63+
.into_boxed()
64+
}
65+
66+
pub fn accepted() -> dev_pm_relationships::BoxedQuery<'static, Pg> {
67+
use crate::schema::dev_pm_relationships::dsl::*;
68+
69+
dev_pm_relationships
70+
.filter(status.eq("accepted"))
71+
.into_boxed()
72+
}
73+
}

src/routes/auth.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ use jsonwebtoken::{encode, EncodingKey, Header};
88
use rocket::http::Status;
99
use rocket::serde::json::Json;
1010
use serde::{Deserialize, Serialize};
11+
1112
#[derive(Deserialize)]
1213
pub struct RegisterInfo {
1314
username: String,
1415
password: String,
16+
role: Option<String>,
1517
}
1618

1719
#[derive(Deserialize)]
1820
pub struct LoginInfo {
1921
username: String,
2022
password: String,
2123
}
24+
2225
#[derive(Serialize)]
2326
pub struct TokenResponse {
2427
token: String,
@@ -30,10 +33,16 @@ pub fn register(info: Json<RegisterInfo>) -> Result<Json<User>, Status> {
3033
let hashed_password = hash(&info.password, DEFAULT_COST)
3134
.map_err(|_| Status::InternalServerError)?;
3235

36+
let user_role = match info.role.as_deref() {
37+
Some("developer") => "developer",
38+
Some("project_manager") => "project_manager",
39+
_ => "developer"
40+
}.to_string();
41+
3342
let new_user = NewUser {
3443
username: info.username.clone(),
3544
password_hash: hashed_password,
36-
role: "user".to_string(),
45+
role: user_role,
3746
};
3847

3948
diesel::insert_into(users)
@@ -66,4 +75,4 @@ pub fn login(info: Json<LoginInfo>) -> Result<Json<TokenResponse>, Status> {
6675
} else {
6776
Err(Status::Unauthorized)
6877
}
69-
}
78+
}

src/routes/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod snack;
2-
pub mod auth;
2+
pub mod auth;
3+
pub mod relationship;

src/routes/relationship.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use crate::auth::user::AuthenticatedUser;
2+
use crate::db;
3+
use crate::models::relationship::{DevPmRelationship, InvitePmRequest, NewDevPmRelationship, RespondToInviteRequest};
4+
use crate::models::snack::Snack;
5+
use crate::models::user::User;
6+
use crate::schema::dev_pm_relationships::{self, developer_id, project_manager_id, status};
7+
use crate::schema::snacks::{self, user_id};
8+
use crate::schema::users;
9+
use diesel::prelude::*;
10+
use rocket::http::Status;
11+
use rocket::serde::json::Json;
12+
13+
#[post("/invite-pm", data = "<invite_data>")]
14+
pub fn invite_project_manager(
15+
invite_data: Json<InvitePmRequest>,
16+
user: AuthenticatedUser,
17+
) -> Result<Json<DevPmRelationship>, Status> {
18+
let mut conn = db::establish_connection();
19+
20+
if user.0.role != "developer" {
21+
return Err(Status::Forbidden);
22+
}
23+
24+
let pm = users::table
25+
.find(invite_data.project_manager_id)
26+
.first::<User>(&mut conn)
27+
.map_err(|_| Status::NotFound)?;
28+
29+
if pm.role != "project_manager" {
30+
return Err(Status::BadRequest);
31+
}
32+
33+
let existing = DevPmRelationship::for_developer(user.0.id)
34+
.filter(project_manager_id.eq(invite_data.project_manager_id))
35+
.filter(status.ne("rejected"))
36+
.first::<DevPmRelationship>(&mut conn);
37+
38+
if existing.is_ok() {
39+
return Err(Status::BadRequest);
40+
}
41+
42+
let new_relationship = NewDevPmRelationship {
43+
developer_id: user.0.id,
44+
project_manager_id: invite_data.project_manager_id,
45+
status: "pending".to_string(),
46+
};
47+
48+
diesel::insert_into(dev_pm_relationships::table)
49+
.values(&new_relationship)
50+
.get_result(&mut conn)
51+
.map(Json)
52+
.map_err(|_| Status::InternalServerError)
53+
}
54+
55+
#[get("/my-developers")]
56+
pub fn list_developers(user: AuthenticatedUser) -> Result<Json<Vec<User>>, Status> {
57+
let mut conn = db::establish_connection();
58+
59+
if user.0.role != "project_manager" {
60+
return Err(Status::Forbidden);
61+
}
62+
63+
users::table
64+
.inner_join(
65+
dev_pm_relationships::table
66+
.on(developer_id.eq(users::id)
67+
.and(project_manager_id.eq(user.0.id))
68+
.and(status.eq("accepted")))
69+
)
70+
.select(users::all_columns)
71+
.load::<User>(&mut conn)
72+
.map(Json)
73+
.map_err(|_| Status::InternalServerError)
74+
}
75+
76+
#[get("/snacks")]
77+
pub fn list_snacks(user: AuthenticatedUser) -> Result<Json<Vec<Snack>>, Status> {
78+
let mut conn = db::establish_connection();
79+
80+
match user.0.role.as_str() {
81+
"admin" => {
82+
snacks::table
83+
.limit(100)
84+
.select(Snack::as_select())
85+
.load(&mut conn)
86+
}
87+
"project_manager" => {
88+
snacks::table
89+
.inner_join(
90+
dev_pm_relationships::table
91+
.on(user_id.eq(developer_id)
92+
.and(project_manager_id.eq(user.0.id))
93+
.and(status.eq("accepted")))
94+
)
95+
.select(Snack::as_select())
96+
.distinct()
97+
.limit(100)
98+
.load(&mut conn)
99+
}
100+
_ => {
101+
snacks::table
102+
.filter(user_id.eq(user.0.id))
103+
.limit(100)
104+
.select(Snack::as_select())
105+
.load(&mut conn)
106+
}
107+
}
108+
.map(Json)
109+
.map_err(|err| {
110+
println!("Database error: {:?}", err);
111+
Status::InternalServerError
112+
})
113+
}
114+
115+
#[patch("/respond-to-invite/<relationship_id>", data = "<response_data>")]
116+
pub fn respond_to_invite(
117+
relationship_id: i32,
118+
response_data: Json<RespondToInviteRequest>,
119+
user: AuthenticatedUser,
120+
) -> Result<Json<DevPmRelationship>, Status> {
121+
let mut conn = db::establish_connection();
122+
123+
if user.0.role != "project_manager" {
124+
return Err(Status::Forbidden);
125+
}
126+
127+
let relationship = DevPmRelationship::for_project_manager(user.0.id)
128+
.filter(dev_pm_relationships::id.eq(relationship_id))
129+
.first::<DevPmRelationship>(&mut conn)
130+
.map_err(|_| Status::NotFound)?;
131+
132+
if relationship.status != "pending" {
133+
return Err(Status::BadRequest);
134+
}
135+
136+
diesel::update(dev_pm_relationships::table.find(relationship_id))
137+
.set(status.eq(&response_data.status))
138+
.get_result(&mut conn)
139+
.map(Json)
140+
.map_err(|_| Status::InternalServerError)
141+
}

0 commit comments

Comments
 (0)