Skip to content
/ loom Public

Commit 51ac562

Browse files
ghuntleyclaude
andcommitted
Refactor large route files into modular directory structures
Split 14 route files (>1000 lines each) into modular directories: - flags.rs -> flags/{common,crud}.rs - orgs.rs -> orgs/{common,crud,members,settings}.rs - crons.rs -> crons/{common,schedules,runs}.rs - auth.rs -> auth/{common,session,account,profile}.rs - invitations.rs -> invitations/{common,org,accept}.rs - repos.rs -> repos/{common,crud,settings}.rs - admin_flags.rs -> admin_flags/{common,crud}.rs - git.rs -> git/{common,hooks,operations}.rs - teams.rs -> teams/{common,crud,members}.rs - git_browser.rs -> git_browser/{types,helpers,handlers,router}.rs - admin.rs -> admin/{common,users,impersonation,audit}.rs - share.rs -> share/{common,share_links,support_access}.rs - secrets.rs -> secrets/{common,org_secrets,repo_secrets}.rs - wgtunnel.rs -> wgtunnel/{common,devices,sessions,derp,internal}.rs Each module re-exports __path_* types for utoipa OpenAPI documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f229130 commit 51ac562

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+23356
-22314
lines changed

crates/loom-server/src/routes/admin.rs

Lines changed: 0 additions & 1151 deletions
This file was deleted.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) 2025 Geoffrey Huntley <ghuntley@ghuntley.com>. All rights
2+
// reserved. SPDX-License-Identifier: Proprietary
3+
4+
//! Admin audit log HTTP handlers.
5+
6+
use axum::{
7+
extract::{Query, State},
8+
http::StatusCode,
9+
response::IntoResponse,
10+
Json,
11+
};
12+
13+
use crate::{
14+
api::AppState,
15+
auth_middleware::RequireAuth,
16+
i18n::{resolve_user_locale, t},
17+
};
18+
19+
use super::common::{
20+
AdminErrorResponse, AuditLogEntryResponse, ListAuditLogsParams, ListAuditLogsResponse,
21+
};
22+
23+
/// Query audit logs.
24+
///
25+
/// # Authorization
26+
///
27+
/// Requires `system_admin` or `auditor` role. Returns 403 Forbidden otherwise.
28+
///
29+
/// # Request
30+
///
31+
/// Query parameters:
32+
/// - `event_type` (optional): Filter by event type
33+
/// - `actor_id` (optional): Filter by actor user ID
34+
/// - `resource_type` (optional): Filter by resource type
35+
/// - `resource_id` (optional): Filter by resource ID
36+
/// - `from` (optional): Start of time range (ISO 8601)
37+
/// - `to` (optional): End of time range (ISO 8601)
38+
/// - `limit` (optional): Maximum logs to return (default: 50)
39+
/// - `offset` (optional): Pagination offset (default: 0)
40+
///
41+
/// # Response
42+
///
43+
/// Returns [`ListAuditLogsResponse`] with paginated audit log entries.
44+
///
45+
/// # Errors
46+
///
47+
/// - `401 Unauthorized`: Missing or invalid authentication
48+
/// - `403 Forbidden`: Caller lacks required role
49+
/// - `500 Internal Server Error`: Database error
50+
///
51+
/// # Security
52+
///
53+
/// - Audit logs contain sensitive operational data
54+
/// - Access is restricted to admins and auditors only
55+
#[utoipa::path(
56+
get,
57+
path = "/api/admin/audit-logs",
58+
params(ListAuditLogsParams),
59+
responses(
60+
(status = 200, description = "List of audit logs", body = ListAuditLogsResponse),
61+
(status = 401, description = "Not authenticated", body = AdminErrorResponse),
62+
(status = 403, description = "Not authorized (system_admin or auditor required)", body = AdminErrorResponse)
63+
),
64+
tag = "admin"
65+
)]
66+
#[tracing::instrument(
67+
skip(state),
68+
fields(
69+
actor_id = %current_user.user.id,
70+
event_type = ?params.event_type,
71+
filter_actor_id = ?params.actor_id,
72+
resource_type = ?params.resource_type,
73+
limit = params.limit,
74+
offset = params.offset
75+
)
76+
)]
77+
pub async fn list_audit_logs(
78+
RequireAuth(current_user): RequireAuth,
79+
State(state): State<AppState>,
80+
Query(params): Query<ListAuditLogsParams>,
81+
) -> impl IntoResponse {
82+
let locale = resolve_user_locale(&current_user, &state.default_locale);
83+
84+
if !current_user.user.is_system_admin && !current_user.user.is_auditor {
85+
tracing::warn!(
86+
actor_id = %current_user.user.id,
87+
"Unauthorized audit log access attempt"
88+
);
89+
return (
90+
StatusCode::FORBIDDEN,
91+
Json(AdminErrorResponse {
92+
error: "forbidden".to_string(),
93+
message: t(locale, "server.api.error.forbidden").to_string(),
94+
}),
95+
)
96+
.into_response();
97+
}
98+
99+
let (logs, total) = match state
100+
.audit_repo
101+
.query_logs(
102+
params.event_type.as_deref(),
103+
params.actor_id.as_deref(),
104+
params.resource_type.as_deref(),
105+
params.resource_id.as_deref(),
106+
params.from,
107+
params.to,
108+
Some(params.limit.into()),
109+
Some(params.offset.into()),
110+
)
111+
.await
112+
{
113+
Ok(result) => result,
114+
Err(e) => {
115+
tracing::error!(error = %e, "Failed to query audit logs");
116+
return (
117+
StatusCode::INTERNAL_SERVER_ERROR,
118+
Json(AdminErrorResponse {
119+
error: "internal_error".to_string(),
120+
message: t(locale, "server.api.error.internal").to_string(),
121+
}),
122+
)
123+
.into_response();
124+
}
125+
};
126+
127+
let logs: Vec<AuditLogEntryResponse> = logs
128+
.into_iter()
129+
.map(|l| AuditLogEntryResponse {
130+
id: l.id.to_string(),
131+
timestamp: l.timestamp,
132+
event_type: l.event_type.to_string(),
133+
actor_user_id: l.actor_user_id.map(|id| id.to_string()),
134+
impersonating_user_id: l.impersonating_user_id.map(|id| id.to_string()),
135+
resource_type: l.resource_type,
136+
resource_id: l.resource_id,
137+
action: l.action,
138+
ip_address: l.ip_address,
139+
user_agent: l.user_agent,
140+
details: if l.details == serde_json::Value::Null {
141+
None
142+
} else {
143+
Some(l.details)
144+
},
145+
})
146+
.collect();
147+
148+
tracing::info!(
149+
actor_id = %current_user.user.id,
150+
log_count = logs.len(),
151+
total = total,
152+
"Audit logs queried"
153+
);
154+
155+
(
156+
StatusCode::OK,
157+
Json(ListAuditLogsResponse {
158+
logs,
159+
total,
160+
limit: params.limit,
161+
offset: params.offset,
162+
}),
163+
)
164+
.into_response()
165+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) 2025 Geoffrey Huntley <ghuntley@ghuntley.com>. All rights
2+
// reserved. SPDX-License-Identifier: Proprietary
3+
4+
//! Shared types and helpers for admin handlers.
5+
6+
pub use loom_server_api::admin::{
7+
AdminErrorResponse, AdminSuccessResponse, AdminUserResponse, AuditLogEntryResponse,
8+
DeleteUserResponse, ImpersonateRequest, ImpersonateResponse, ImpersonationState,
9+
ImpersonationUserInfo, ListAuditLogsParams, ListAuditLogsResponse, ListUsersParams,
10+
ListUsersResponse, UpdateRolesRequest,
11+
};
12+
use loom_server_auth::UserId;
13+
use uuid::Uuid;
14+
15+
use crate::i18n::t;
16+
17+
pub fn parse_user_id(id: &str, locale: &str) -> Result<UserId, AdminErrorResponse> {
18+
Uuid::parse_str(id)
19+
.map(UserId::new)
20+
.map_err(|_| AdminErrorResponse {
21+
error: "bad_request".to_string(),
22+
message: t(locale, "server.api.user.invalid_id").to_string(),
23+
})
24+
}

0 commit comments

Comments
 (0)