Skip to content

Commit 7fe70b6

Browse files
authored
add edit gateway page (#2108)
* copy edge edit page for gateways * add tests for gateway API * fix api call * adjust edit page layout * copy modifications to Edge edit page * formatting
1 parent 27f1415 commit 7fe70b6

14 files changed

Lines changed: 433 additions & 30 deletions

File tree

crates/defguard_common/src/db/models/gateway.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::fmt;
22

3-
use chrono::{NaiveDateTime, Utc};
3+
use chrono::{NaiveDateTime, Timelike, Utc};
44
use model_derive::Model;
55
use serde::{Deserialize, Serialize};
66
use sqlx::{PgExecutor, query, query_as};
@@ -44,6 +44,14 @@ impl Gateway {
4444
port: i32,
4545
modified_by: Id,
4646
) -> Self {
47+
// FIXME: this is a workaround for reducing timestamp precision.
48+
// `chrono` has nanosecond precision by default, while Postgres only does microseconds.
49+
// It avoids issues when comparing to objects fetched from DB.
50+
let modified_at = Utc::now().naive_utc();
51+
let modified_at = modified_at
52+
.with_nanosecond((modified_at.nanosecond() / 1_000) * 1_000)
53+
.expect("failed to truncate timestamp precision");
54+
4755
Self {
4856
id: NoId,
4957
location_id: network_id,
@@ -56,7 +64,7 @@ impl Gateway {
5664
certificate_expiry: None,
5765
version: None,
5866
modified_by,
59-
modified_at: Utc::now().naive_utc(),
67+
modified_at,
6068
}
6169
}
6270
}

crates/defguard_core/src/handlers/gateway.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use axum::{
22
Json,
3+
extract::rejection::JsonRejection,
34
extract::{Path, State},
45
};
56
use chrono::NaiveDateTime;
@@ -13,6 +14,7 @@ use utoipa::ToSchema;
1314
use crate::{
1415
appstate::AppState,
1516
auth::{AdminRole, SessionInfo},
17+
error::WebError,
1618
events::{ApiEvent, ApiEventType, ApiRequestContext},
1719
handlers::{ApiResponse, ApiResult},
1820
};
@@ -83,6 +85,7 @@ impl GatewayInfo {
8385
}
8486

8587
#[derive(Serialize, Deserialize, ToSchema)]
88+
#[serde(deny_unknown_fields)]
8689
pub struct GatewayUpdateData {
8790
pub name: String,
8891
}
@@ -173,17 +176,26 @@ pub(crate) async fn update_gateway(
173176
State(appstate): State<AppState>,
174177
session: SessionInfo,
175178
context: ApiRequestContext,
176-
Json(data): Json<GatewayUpdateData>,
179+
payload: Result<Json<GatewayUpdateData>, JsonRejection>,
177180
) -> ApiResult {
181+
let Json(data) = match payload {
182+
Ok(payload) => payload,
183+
Err(err) => {
184+
let msg = format!("Failed to parse request data: {err}");
185+
warn!(msg);
186+
return Err(WebError::BadRequest(msg));
187+
}
188+
};
178189
debug!(
179190
"User {} updating gateway {gateway_id}",
180191
session.user.username
181192
);
182193
let gateway = Gateway::find_by_id(&appstate.pool, gateway_id).await?;
183194

184195
let Some(mut gateway) = gateway else {
185-
warn!("Gateway {gateway_id} not found");
186-
return Ok(ApiResponse::json(Value::Null, StatusCode::NOT_FOUND));
196+
let msg = format!("Gateway {gateway_id} not found");
197+
warn!(msg);
198+
return Err(WebError::ObjectNotFound(msg));
187199
};
188200
let before = gateway.clone();
189201

@@ -235,8 +247,9 @@ pub(crate) async fn delete_gateway(
235247
let gateway = Gateway::find_by_id(&appstate.pool, gateway_id).await?;
236248

237249
let Some(gateway) = gateway else {
238-
warn!("Gateway {gateway_id} not found");
239-
return Ok(ApiResponse::json(Value::Null, StatusCode::NOT_FOUND));
250+
let msg = format!("Gateway {gateway_id} not found");
251+
warn!(msg);
252+
return Err(WebError::ObjectNotFound(msg));
240253
};
241254

242255
gateway.clone().delete(&appstate.pool).await?;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use defguard_common::db::{
2+
Id,
3+
models::{WireguardNetwork, gateway::Gateway},
4+
};
5+
use defguard_core::handlers::Auth;
6+
use reqwest::StatusCode;
7+
use serde_json::json;
8+
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
9+
10+
use super::common::{make_network, make_test_client, setup_pool};
11+
12+
#[sqlx::test]
13+
async fn test_gateway_crud(_: PgPoolOptions, options: PgConnectOptions) {
14+
let pool = setup_pool(options).await;
15+
16+
let (mut client, client_state) = make_test_client(pool).await;
17+
18+
client.login_user("admin", "pass123").await;
19+
20+
let response = make_network(&client, "network").await;
21+
let network: WireguardNetwork<Id> = response.json().await;
22+
client.drain_all_events();
23+
client.drain_all_events();
24+
25+
let gateway_1 = Gateway::new(network.id, "gateway1", "127.0.0.1", 50051, 1)
26+
.save(&client_state.pool)
27+
.await
28+
.unwrap();
29+
let gateway_2 = Gateway::new(network.id, "gateway2", "1.2.3.1", 55555, 1)
30+
.save(&client_state.pool)
31+
.await
32+
.unwrap();
33+
34+
let response = client.get("/api/v1/gateway").send().await;
35+
assert_eq!(response.status(), StatusCode::OK);
36+
let gateways: Vec<Gateway<Id>> = response.json().await;
37+
assert_eq!(gateways.len(), 2);
38+
let gateway_from_list = &gateways[0];
39+
assert_eq!(gateway_from_list, &gateway_1);
40+
let gateway_from_list = &gateways[1];
41+
assert_eq!(gateway_from_list, &gateway_2);
42+
43+
let response = client
44+
.get(format!("/api/v1/gateway/{}", gateway_1.id))
45+
.send()
46+
.await;
47+
assert_eq!(response.status(), StatusCode::OK);
48+
let gateway_details: Gateway<Id> = response.json().await;
49+
assert_eq!(gateway_details, gateway_1);
50+
51+
let response = client
52+
.put(format!("/api/v1/gateway/{}", gateway_1.id))
53+
.json(&json!({
54+
"name": "gateway-updated",
55+
}))
56+
.send()
57+
.await;
58+
assert_eq!(response.status(), StatusCode::OK);
59+
let updated_gateway: Gateway<Id> = response.json().await;
60+
assert_eq!(updated_gateway.name, "gateway-updated");
61+
assert_eq!(updated_gateway.address, gateway_1.address);
62+
assert_eq!(updated_gateway.port, gateway_1.port);
63+
64+
let response = client
65+
.delete(format!("/api/v1/gateway/{}", gateway_1.id))
66+
.send()
67+
.await;
68+
assert_eq!(response.status(), StatusCode::OK);
69+
70+
let response = client
71+
.get(format!("/api/v1/gateway/{}", gateway_1.id))
72+
.send()
73+
.await;
74+
assert_eq!(response.status(), StatusCode::NOT_FOUND);
75+
76+
let response = client.get("/api/v1/gateway").send().await;
77+
assert_eq!(response.status(), StatusCode::OK);
78+
let gateways: Vec<Gateway<Id>> = response.json().await;
79+
assert_eq!(gateways.len(), 1);
80+
}
81+
82+
#[sqlx::test]
83+
async fn test_gateway_endpoints_require_admin(_: PgPoolOptions, options: PgConnectOptions) {
84+
let pool = setup_pool(options).await;
85+
86+
let (mut client, client_state) = make_test_client(pool).await;
87+
88+
client.login_user("admin", "pass123").await;
89+
90+
let response = make_network(&client, "network").await;
91+
let network: WireguardNetwork<Id> = response.json().await;
92+
93+
let gateway = Gateway::new(network.id, "gateway", "127.0.0.1", 50051, 1)
94+
.save(&client_state.pool)
95+
.await
96+
.unwrap();
97+
98+
let auth = Auth::new("hpotter", "pass123");
99+
let response = client.post("/api/v1/auth").json(&auth).send().await;
100+
assert_eq!(response.status(), StatusCode::OK);
101+
102+
let response = client.get("/api/v1/gateway").send().await;
103+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
104+
105+
let response = client
106+
.get(format!("/api/v1/gateway/{}", gateway.id))
107+
.send()
108+
.await;
109+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
110+
111+
let response = client
112+
.put(format!("/api/v1/gateway/{}", gateway.id))
113+
.json(&json!({
114+
"name": "gateway-updated",
115+
}))
116+
.send()
117+
.await;
118+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
119+
120+
let response = client
121+
.delete(format!("/api/v1/gateway/{}", gateway.id))
122+
.send()
123+
.await;
124+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
125+
}
126+
127+
#[sqlx::test]
128+
async fn test_gateway_update_rejects_unknown_fields(_: PgPoolOptions, options: PgConnectOptions) {
129+
let pool = setup_pool(options).await;
130+
131+
let (mut client, client_state) = make_test_client(pool).await;
132+
133+
client.login_user("admin", "pass123").await;
134+
135+
let response = make_network(&client, "network").await;
136+
let network: WireguardNetwork<Id> = response.json().await;
137+
138+
let gateway = Gateway::new(network.id, "gateway", "127.0.0.1", 50051, 1)
139+
.save(&client_state.pool)
140+
.await
141+
.unwrap();
142+
143+
let response = client
144+
.put(format!("/api/v1/gateway/{}", gateway.id))
145+
.json(&json!({
146+
"name": "gateway-updated",
147+
"address": "127.0.0.2",
148+
"port": 50052,
149+
"location_id": 999,
150+
}))
151+
.send()
152+
.await;
153+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
154+
}

crates/defguard_core/tests/integration/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod common;
55
mod enrollment;
66
mod enterprise_settings;
77
mod forward_auth;
8+
mod gateway;
89
mod group;
910
mod location_stats;
1011
mod oauth;

web/messages/en/gateway.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
{
22
"$schema": "https://inlang.com/schema/inlang-message-format",
3+
"gateway_title": "Gateways",
4+
"gateway_edit_title": "Edit gateway",
5+
"gateway_edit_general_info": "General information",
6+
"gateway_edit_name": "Name",
7+
"gateway_edit_address": "IP or Domain",
8+
"gateway_edit_port": "gRPC port",
9+
"gateway_edit_delete": "Delete",
10+
"gateway_edit_success": "Gateway updated",
11+
"gateway_edit_failed": "Failed to update gateway",
312
"gateway_delete_success": "Gateway deleted",
413
"gateway_delete_failed": "Failed to delete gateway"
514
}

web/src/pages/EditEdgePage/EditEdgePage.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => {
9999
...value,
100100
id: edge.id,
101101
});
102+
form.reset(value);
102103
},
103104
});
104105

@@ -141,11 +142,6 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => {
141142
loading: deletePending,
142143
disabled: isSubmitting,
143144
}}
144-
cancelProps={{
145-
onClick: () => {
146-
window.history.back();
147-
},
148-
}}
149145
submitProps={{
150146
loading: isSubmitting,
151147
disabled: isDefault,

0 commit comments

Comments
 (0)