Skip to content

Commit 1f8ea7d

Browse files
authored
server: Allow creating an application in app-portal-access (#2096)
Add support for creating apps in app-portal-access part of: svix/monorepo-private#11626
2 parents 93d5d88 + 0220e44 commit 1f8ea7d

File tree

5 files changed

+161
-9
lines changed

5 files changed

+161
-9
lines changed

server/openapi.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
"schemas": {
44
"AppPortalAccessIn": {
55
"properties": {
6+
"application": {
7+
"$ref": "#/components/schemas/ApplicationIn",
8+
"description": "Optionally creates a new application alongside the message.\n\nIf the application id or uid that is used in the path already exists, this argument is ignored.",
9+
"nullable": true
10+
},
611
"featureFlags": {
712
"description": "The set of feature flags the created token will have access to.",
813
"example": [],

server/svix-server/src/v1/endpoints/auth.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ use validator::Validate;
1313

1414
use crate::{
1515
core::{permissions, security::generate_app_token, types::FeatureFlagSet},
16-
error::Result,
17-
v1::utils::{api_not_implemented, openapi_tag, ApplicationPath, ValidatedJson},
16+
db::models::application,
17+
error::{HttpError, Result},
18+
v1::{
19+
endpoints::{
20+
application::{create_app_from_app_in, ApplicationIn},
21+
message::validate_create_app_uid,
22+
},
23+
utils::{api_not_implemented, openapi_tag, ApplicationPath, ValidatedJson},
24+
},
1825
AppState,
1926
};
2027

@@ -45,6 +52,13 @@ pub struct AppPortalAccessIn {
4552
#[serde(default, skip_serializing_if = "FeatureFlagSet::is_empty")]
4653
#[schemars(example = "feature_flag_set_example")]
4754
pub feature_flags: FeatureFlagSet,
55+
/// Optionally creates a new application alongside the message.
56+
///
57+
/// If the application id or uid that is used in the path already exists,
58+
/// this argument is ignored.
59+
#[validate]
60+
#[serde(skip_serializing_if = "Option::is_none")]
61+
pub application: Option<ApplicationIn>,
4862
}
4963

5064
#[derive(Serialize, JsonSchema)]
@@ -62,11 +76,33 @@ impl From<DashboardAccessOut> for AppPortalAccessOut {
6276
/// Use this function to get magic links (and authentication codes) for connecting your users to the Consumer Application Portal.
6377
#[aide_annotate(op_id = "v1.authentication.app-portal-access")]
6478
async fn app_portal_access(
65-
State(AppState { cfg, .. }): State<AppState>,
66-
_: Path<ApplicationPath>,
67-
permissions::OrganizationWithApplication { app }: permissions::OrganizationWithApplication,
79+
State(AppState { ref db, cfg, .. }): State<AppState>,
80+
Path(ApplicationPath { app_id }): Path<ApplicationPath>,
81+
permissions::Organization { org_id }: permissions::Organization,
6882
ValidatedJson(data): ValidatedJson<AppPortalAccessIn>,
6983
) -> Result<Json<AppPortalAccessOut>> {
84+
let app_from_path_app_id =
85+
application::Entity::secure_find_by_id_or_uid(org_id.clone(), app_id.to_owned())
86+
.one(db)
87+
.await?;
88+
89+
let app = match (&data.application, app_from_path_app_id) {
90+
(None, None) => {
91+
return Err(
92+
HttpError::not_found(None, Some("Application not found".to_string())).into(),
93+
);
94+
}
95+
96+
(_, Some(app_from_path_param)) => app_from_path_param,
97+
(Some(app_from_body), None) => {
98+
validate_create_app_uid(&app_id, app_from_body)?;
99+
let (app, _metadata) =
100+
create_app_from_app_in(db, app_from_body.to_owned(), org_id).await?;
101+
102+
app
103+
}
104+
};
105+
70106
let token = generate_app_token(
71107
&cfg.jwt_signing_config,
72108
app.org_id,
@@ -87,14 +123,15 @@ async fn app_portal_access(
87123
async fn dashboard_access(
88124
state: State<AppState>,
89125
path: Path<ApplicationPath>,
90-
permissions: permissions::OrganizationWithApplication,
126+
permissions: permissions::Organization,
91127
) -> Result<Json<DashboardAccessOut>> {
92128
app_portal_access(
93129
state,
94130
path,
95131
permissions,
96132
ValidatedJson(AppPortalAccessIn {
97133
feature_flags: FeatureFlagSet::default(),
134+
application: None,
98135
}),
99136
)
100137
.await

server/svix-server/src/v1/endpoints/message.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,10 @@ pub(crate) async fn create_message_inner(
460460
Ok(msg_out)
461461
}
462462

463-
fn validate_create_app_uid(app_id_or_uid: &ApplicationIdOrUid, data: &ApplicationIn) -> Result<()> {
463+
pub fn validate_create_app_uid(
464+
app_id_or_uid: &ApplicationIdOrUid,
465+
data: &ApplicationIn,
466+
) -> Result<()> {
464467
// If implicit app creation is requested then the UID must be set
465468
// in the request body, and it must match the UID given in the path
466469
if let Some(uid) = &data.uid {

server/svix-server/tests/it/e2e_auth.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
//! that the tokens returned by the endpoint have restricted functionality and that the response
33
//! from the endpoint is valid in the process.
44
5+
use rand::distributions::DistString;
56
use reqwest::StatusCode;
67
use serde::de::IgnoredAny;
7-
use serde_json::Value;
8+
use serde_json::{json, Value};
89
use svix_server::{
910
core::{
1011
security::{INVALID_TOKEN_ERR, JWT_SECRET_ERR},
@@ -154,3 +155,106 @@ async fn test_invalid_auth_error_detail() {
154155
}
155156
}
156157
}
158+
159+
#[tokio::test]
160+
async fn test_app_portal_access_with_application() {
161+
let (client, _jh) = start_svix_server().await;
162+
163+
let app_uid = format!(
164+
"app-created-in-portal-{}",
165+
rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 15)
166+
);
167+
168+
// app-portal-access without the application field fails
169+
let _: IgnoredAny = client
170+
.post(
171+
&format!("api/v1/auth/app-portal-access/{app_uid}/"),
172+
json!({
173+
"featureFlags": []
174+
}),
175+
StatusCode::NOT_FOUND,
176+
)
177+
.await
178+
.unwrap();
179+
180+
// app-portal-access with application
181+
let _: IgnoredAny = client
182+
.post(
183+
&format!("api/v1/auth/app-portal-access/{app_uid}/"),
184+
json!({
185+
"featureFlags": [],
186+
"application": {
187+
"name": "Test App Created With Portal Access",
188+
"uid": app_uid,
189+
}
190+
}),
191+
StatusCode::OK,
192+
)
193+
.await
194+
.unwrap();
195+
196+
// app was created
197+
let app: serde_json::Value = client
198+
.get(&format!("api/v1/app/{app_uid}/"), StatusCode::OK)
199+
.await
200+
.unwrap();
201+
202+
assert_eq!(app["uid"], app_uid);
203+
assert_eq!(app["name"], "Test App Created With Portal Access");
204+
205+
// Access portal again with application field - should be ignored since app exists
206+
let _: IgnoredAny = client
207+
.post(
208+
&format!("api/v1/auth/app-portal-access/{app_uid}/"),
209+
json!({
210+
"featureFlags": [],
211+
"application": {
212+
"name": "Updated name will be ignored",
213+
"uid": app_uid,
214+
}
215+
}),
216+
StatusCode::OK,
217+
)
218+
.await
219+
.unwrap();
220+
221+
// Verify the app name didn't change
222+
let app_after: serde_json::Value = client
223+
.get(&format!("api/v1/app/{app_uid}/"), StatusCode::OK)
224+
.await
225+
.unwrap();
226+
227+
assert_eq!(app_after["name"], "Test App Created With Portal Access");
228+
229+
// UID in path must match UID in body
230+
let _: IgnoredAny = client
231+
.post(
232+
"api/v1/auth/app-portal-access/different-uid/",
233+
json!({
234+
"featureFlags": [],
235+
"application": {
236+
"name": "Test App",
237+
"uid": app_uid, // This doesn't match the path
238+
}
239+
}),
240+
StatusCode::UNPROCESSABLE_ENTITY,
241+
)
242+
.await
243+
.unwrap();
244+
245+
// UID must be set in body when creating
246+
let _: IgnoredAny = client
247+
.post(
248+
"api/v1/auth/app-portal-access/new-app-uid/",
249+
json!({
250+
"featureFlags": [],
251+
"application": {
252+
"name": "Test App Without UID",
253+
// Missing uid field
254+
}
255+
}),
256+
StatusCode::UNPROCESSABLE_ENTITY,
257+
)
258+
.await
259+
.unwrap();
260+
}

server/svix-server/tests/it/utils/common_calls.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,10 @@ pub async fn app_portal_access(
416416
let resp: DashboardAccessOut = org_client
417417
.post(
418418
&format!("api/v1/auth/app-portal-access/{application_id}/"),
419-
AppPortalAccessIn { feature_flags },
419+
AppPortalAccessIn {
420+
feature_flags,
421+
application: None,
422+
},
420423
StatusCode::OK,
421424
)
422425
.await

0 commit comments

Comments
 (0)