Skip to content

Commit aef2df8

Browse files
committed
Upgrade invites api
1 parent 642c3f1 commit aef2df8

File tree

6 files changed

+334
-177
lines changed

6 files changed

+334
-177
lines changed

engine/src/routes/invite/accept.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use poem::{web::Data, Result};
2+
use poem_openapi::{param::Path, payload::Json, types::Example, ApiResponse, Object, OpenApi};
3+
use serde::{Deserialize, Serialize};
4+
use tracing::info;
5+
6+
use crate::{
7+
middlewares::auth::UserAuth,
8+
models::{
9+
team::{invite::UserTeamInvite, Team},
10+
user::User,
11+
},
12+
routes::{auth::BootstrapUserResponse, error::HttpError, ApiTags},
13+
state::State,
14+
utils::hash::hash_password,
15+
};
16+
17+
#[derive(Debug, Deserialize, Serialize, Object)]
18+
pub struct SiteCreateRequest {
19+
pub name: String,
20+
pub team_id: String,
21+
}
22+
23+
#[derive(Debug, Deserialize, Serialize, Object)]
24+
pub struct TeamInviteData {
25+
pub invite: UserTeamInvite,
26+
pub team: Team,
27+
}
28+
29+
#[derive(Deserialize, Debug, Object)]
30+
#[oai(example)]
31+
pub struct TeamInviteAcceptNewPayload {
32+
username: String,
33+
password: String,
34+
}
35+
36+
impl Example for TeamInviteAcceptNewPayload {
37+
fn example() -> Self {
38+
Self {
39+
username: "john".to_string(),
40+
password: "password123".to_string(),
41+
}
42+
}
43+
}
44+
45+
#[derive(Debug, ApiResponse)]
46+
#[allow(clippy::large_enum_variant)]
47+
pub enum InviteAcceptBootstrapResponse {
48+
#[oai(status = 200)]
49+
Ok(Json<BootstrapUserResponse>),
50+
#[oai(status = 404)]
51+
NotFound(Json<NotFoundResponse>),
52+
#[oai(status = 409)]
53+
AlreadyExists(Json<AlreadyExistsResponse>),
54+
}
55+
56+
#[derive(Debug, Deserialize, Serialize, Object)]
57+
#[oai(example)]
58+
pub struct NotFoundResponse {
59+
message: String,
60+
}
61+
62+
impl Example for NotFoundResponse {
63+
fn example() -> Self {
64+
Self::default()
65+
}
66+
}
67+
68+
impl Default for NotFoundResponse {
69+
fn default() -> Self {
70+
Self {
71+
message: "Invite not found".to_string(),
72+
}
73+
}
74+
}
75+
76+
#[derive(Debug, Deserialize, Serialize, Object)]
77+
#[oai(example)]
78+
pub struct AcceptedResponse {
79+
message: String,
80+
}
81+
82+
impl Example for AcceptedResponse {
83+
fn example() -> Self {
84+
Self::default()
85+
}
86+
}
87+
88+
impl Default for AcceptedResponse {
89+
fn default() -> Self {
90+
Self {
91+
message: "Invite accepted".to_string(),
92+
}
93+
}
94+
}
95+
96+
#[derive(Debug, Deserialize, Serialize, Object)]
97+
#[oai(example)]
98+
pub struct AlreadyExistsResponse {
99+
message: String,
100+
}
101+
102+
impl Example for AlreadyExistsResponse {
103+
fn example() -> Self {
104+
Self::default()
105+
}
106+
}
107+
108+
impl Default for AlreadyExistsResponse {
109+
fn default() -> Self {
110+
Self {
111+
message: "Invite already accepted".to_string(),
112+
}
113+
}
114+
}
115+
116+
#[derive(Debug, ApiResponse)]
117+
#[allow(clippy::large_enum_variant)]
118+
pub enum InviteAcceptResponse {
119+
#[oai(status = 200)]
120+
Ok(Json<AcceptedResponse>),
121+
#[oai(status = 404)]
122+
NotFound(Json<NotFoundResponse>),
123+
#[oai(status = 409)]
124+
AlreadyExists(Json<AlreadyExistsResponse>),
125+
}
126+
127+
pub struct InviteAcceptApi;
128+
129+
#[OpenApi]
130+
impl InviteAcceptApi {
131+
/// Accept an invite
132+
///
133+
/// Accepts an invite by its ID, this endpoint requires you to be authenticated
134+
/// The invite will be accepted as the user you are authenticated as
135+
#[oai(
136+
path = "/invite/:invite_id/accept",
137+
method = "post",
138+
tag = "ApiTags::Invite"
139+
)]
140+
pub async fn accept_invite(
141+
&self,
142+
user: UserAuth,
143+
state: Data<&State>,
144+
#[oai(name = "invite_id", style = "simple")] invite_id: Path<String>,
145+
) -> Result<InviteAcceptResponse> {
146+
info!("Accepting invite: {:?}", invite_id.0);
147+
148+
let user = user.required_session()?;
149+
150+
let invite = UserTeamInvite::get_by_invite_id(&state.database, &invite_id.0)
151+
.await
152+
.map_err(HttpError::from)?;
153+
154+
if invite.status != "pending" {
155+
return Ok(InviteAcceptResponse::AlreadyExists(Json(
156+
AlreadyExistsResponse::default(),
157+
)));
158+
}
159+
160+
UserTeamInvite::accept_invite(&state.database, &invite_id.0, &user.user_id)
161+
.await
162+
.map_err(HttpError::from)?;
163+
164+
Ok(InviteAcceptResponse::Ok(Json(AcceptedResponse {
165+
message: "Invite accepted".to_string(),
166+
})))
167+
}
168+
169+
/// Accept an invite and create a user
170+
///
171+
/// Accepts an invite by its ID and onboards as a new user
172+
/// This endpoint does not require authentication
173+
#[oai(
174+
path = "/invite/:invite_id/accept/new",
175+
method = "post",
176+
tag = "ApiTags::Invite"
177+
)]
178+
async fn bootstrap_user(
179+
&self,
180+
state: Data<&State>,
181+
#[oai(name = "invite_id", style = "simple")] invite_id: Path<String>,
182+
request: Json<TeamInviteAcceptNewPayload>,
183+
) -> Result<InviteAcceptBootstrapResponse> {
184+
let invite = UserTeamInvite::get_by_invite_id(&state.database, &invite_id.0)
185+
.await
186+
.map_err(HttpError::from)?;
187+
188+
if invite.status != "pending" {
189+
return Ok(InviteAcceptBootstrapResponse::AlreadyExists(Json(
190+
AlreadyExistsResponse::default(),
191+
)));
192+
}
193+
194+
let (user, team) = User::new(
195+
&state.0.database,
196+
&request.username,
197+
&hash_password(&request.password),
198+
Some(false),
199+
Some(invite.team_id),
200+
)
201+
.await
202+
.map_err(HttpError::from)?;
203+
204+
UserTeamInvite::accept_invite(&state.database, &invite_id.0, &user.user_id)
205+
.await
206+
.map_err(HttpError::from)?;
207+
208+
Ok(InviteAcceptBootstrapResponse::Ok(Json(
209+
BootstrapUserResponse { user, team },
210+
)))
211+
}
212+
}

engine/src/routes/invite/get.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use poem::{web::Data, Result};
2+
use poem_openapi::{param::Path, payload::Json, ApiResponse, OpenApi};
3+
use tracing::info;
4+
5+
use crate::{
6+
models::team::{invite::UserTeamInvite, Team},
7+
routes::{error::HttpError, invite::accept::TeamInviteData, ApiTags},
8+
state::State,
9+
};
10+
11+
pub struct InviteGetApi;
12+
13+
#[derive(Debug, ApiResponse)]
14+
#[allow(clippy::large_enum_variant)]
15+
pub enum InviteGetResponse {
16+
#[oai(status = 200)]
17+
Ok(Json<TeamInviteData>),
18+
#[oai(status = 404)]
19+
NotFound,
20+
}
21+
22+
#[OpenApi]
23+
impl InviteGetApi {
24+
/// Get an invite
25+
///
26+
/// Gets an invite by its ID
27+
#[oai(path = "/invite/:invite_id", method = "get", tag = "ApiTags::Invite")]
28+
pub async fn get_invite(
29+
&self,
30+
// user: UserAuth,
31+
state: Data<&State>,
32+
#[oai(name = "invite_id", style = "simple")] invite_id: Path<String>,
33+
) -> Result<InviteGetResponse> {
34+
info!("Getting invite: {:?}", invite_id.0);
35+
36+
let invite = UserTeamInvite::get_by_invite_id(&state.database, &invite_id.0)
37+
.await
38+
.map_err(HttpError::from)?;
39+
40+
let team = Team::get_by_id(&state.database, &invite.team_id)
41+
.await
42+
.map_err(HttpError::from)?;
43+
44+
Ok(InviteGetResponse::Ok(Json(TeamInviteData { invite, team })))
45+
}
46+
}

0 commit comments

Comments
 (0)