this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::State,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use serde::{Deserialize, Serialize};
9use serde_json::{Value, json};
10use sqlx::Row;
11use tracing::error;
12
13#[derive(Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct CreateReportInput {
16 pub reason_type: String,
17 pub reason: Option<String>,
18 pub subject: Value,
19}
20
21#[derive(Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct CreateReportOutput {
24 pub id: i64,
25 pub reason_type: String,
26 pub reason: Option<String>,
27 pub subject: Value,
28 pub reported_by: String,
29 pub created_at: String,
30}
31
32pub async fn create_report(
33 State(state): State<AppState>,
34 headers: axum::http::HeaderMap,
35 Json(input): Json<CreateReportInput>,
36) -> Response {
37 let auth_header = headers.get("Authorization");
38 if auth_header.is_none() {
39 return (
40 StatusCode::UNAUTHORIZED,
41 Json(json!({"error": "AuthenticationRequired"})),
42 )
43 .into_response();
44 }
45
46 let token = auth_header
47 .unwrap()
48 .to_str()
49 .unwrap_or("")
50 .replace("Bearer ", "");
51
52 let session = sqlx::query(
53 r#"
54 SELECT s.did, k.key_bytes
55 FROM sessions s
56 JOIN users u ON s.did = u.did
57 JOIN user_keys k ON u.id = k.user_id
58 WHERE s.access_jwt = $1
59 "#,
60 )
61 .bind(&token)
62 .fetch_optional(&state.db)
63 .await;
64
65 let (did, key_bytes) = match session {
66 Ok(Some(row)) => (
67 row.get::<String, _>("did"),
68 row.get::<Vec<u8>, _>("key_bytes"),
69 ),
70 Ok(None) => {
71 return (
72 StatusCode::UNAUTHORIZED,
73 Json(json!({"error": "AuthenticationFailed"})),
74 )
75 .into_response();
76 }
77 Err(e) => {
78 error!("DB error in create_report: {:?}", e);
79 return (
80 StatusCode::INTERNAL_SERVER_ERROR,
81 Json(json!({"error": "InternalError"})),
82 )
83 .into_response();
84 }
85 };
86
87 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
88 return (
89 StatusCode::UNAUTHORIZED,
90 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
91 )
92 .into_response();
93 }
94
95 let valid_reason_types = [
96 "com.atproto.moderation.defs#reasonSpam",
97 "com.atproto.moderation.defs#reasonViolation",
98 "com.atproto.moderation.defs#reasonMisleading",
99 "com.atproto.moderation.defs#reasonSexual",
100 "com.atproto.moderation.defs#reasonRude",
101 "com.atproto.moderation.defs#reasonOther",
102 "com.atproto.moderation.defs#reasonAppeal",
103 ];
104
105 if !valid_reason_types.contains(&input.reason_type.as_str()) {
106 return (
107 StatusCode::BAD_REQUEST,
108 Json(json!({"error": "InvalidRequest", "message": "Invalid reasonType"})),
109 )
110 .into_response();
111 }
112
113 let created_at = chrono::Utc::now();
114 let report_id = created_at.timestamp_millis();
115
116 let insert = sqlx::query(
117 "INSERT INTO reports (id, reason_type, reason, subject_json, reported_by_did, created_at) VALUES ($1, $2, $3, $4, $5, $6)"
118 )
119 .bind(report_id)
120 .bind(&input.reason_type)
121 .bind(&input.reason)
122 .bind(json!(input.subject))
123 .bind(&did)
124 .bind(created_at)
125 .execute(&state.db)
126 .await;
127
128 if let Err(e) = insert {
129 error!("Failed to insert report: {:?}", e);
130 return (
131 StatusCode::INTERNAL_SERVER_ERROR,
132 Json(json!({"error": "InternalError"})),
133 )
134 .into_response();
135 }
136
137 (
138 StatusCode::OK,
139 Json(CreateReportOutput {
140 id: report_id,
141 reason_type: input.reason_type,
142 reason: input.reason,
143 subject: input.subject,
144 reported_by: did,
145 created_at: created_at.to_rfc3339(),
146 }),
147 )
148 .into_response()
149}