1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat(auth): apply ROLE_USER guard to WRITE routes (captures, tags)

This commit is contained in:
TheGeneralist 2026-06-26 11:56:38 +02:00
parent 36c7d575e2
commit cb376aa986
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4

View file

@ -280,9 +280,11 @@ async fn list_tags(
async fn create_tag_handler(
State(state): State<AppState>,
auth_user: AuthUser,
Path(archive_id): Path<String>,
Json(body): Json<CreateTagBody>,
) -> Result<(StatusCode, Json<archive::Tag>), ApiError> {
auth_user.require_role(ROLE_USER)?;
if body.path.trim().is_empty() {
return Err(ApiError::bad_request("tag path must not be empty"));
}
@ -306,9 +308,11 @@ async fn list_entry_tags(
async fn assign_entry_tag_handler(
State(state): State<AppState>,
auth_user: AuthUser,
Path((archive_id, entry_uid)): Path<(String, String)>,
Json(body): Json<AssignTagBody>,
) -> Result<(StatusCode, Json<archive::Tag>), ApiError> {
auth_user.require_role(ROLE_USER)?;
if body.tag_path.trim().is_empty() {
return Err(ApiError::bad_request("tag_path must not be empty"));
}
@ -322,8 +326,10 @@ async fn assign_entry_tag_handler(
async fn remove_entry_tag_handler(
State(state): State<AppState>,
auth_user: AuthUser,
Path((archive_id, entry_uid, tag_uid)): Path<(String, String, String)>,
) -> Result<StatusCode, ApiError> {
auth_user.require_role(ROLE_USER)?;
let mounted = mounted_archive(&state, &archive_id)?;
let conn = database::open_or_initialize(&mounted.archive_path)?;
if archive::remove_entry_tag(&conn, &entry_uid, &tag_uid)? {
@ -357,9 +363,11 @@ struct CreateTokenBody {
async fn capture_handler(
State(state): State<AppState>,
auth_user: AuthUser,
Path(archive_id): Path<String>,
Json(body): Json<CaptureBody>,
) -> Result<Json<capture::CaptureResult>, ApiError> {
auth_user.require_role(ROLE_USER)?;
if body.locator.trim().is_empty() {
return Err(ApiError::bad_request("locator must not be empty"));
}
@ -1262,7 +1270,7 @@ mod tests {
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation
}
#[tokio::test]
@ -1279,7 +1287,7 @@ mod tests {
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before archive lookup
}
#[tokio::test]
@ -1382,4 +1390,17 @@ mod tests {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn capture_returns_401_for_unauthenticated() {
let (test_app, _dir) = make_test_app();
let response = test_app
.oneshot(Request::builder().method("POST")
.uri("/api/archives/test/captures")
.header("content-type", "application/json")
.body(Body::from(r#"{"locator":"https://example.com"}"#))
.unwrap())
.await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}