@ -10,7 +10,7 @@
//! ```
//! ```
use quotesdb ::{ CreateQuoteInput , Quote , UpdateQuoteInput } ;
use quotesdb ::{ CreateQuoteInput , Quote , UpdateQuoteInput } ;
use serde ::Deserialize ;
use serde ::{ Deserialize , Serialize } ;
/// Response type for `GET /api/quotes` (paginated list).
/// Response type for `GET /api/quotes` (paginated list).
#[ derive(Debug, Clone, Deserialize) ]
#[ derive(Debug, Clone, Deserialize) ]
@ -44,6 +44,35 @@ pub enum ApiError {
/// Failed to parse the response body as JSON.
/// Failed to parse the response body as JSON.
#[ error( " parse error: {0} " ) ]
#[ error( " parse error: {0} " ) ]
Parse ( String ) ,
Parse ( String ) ,
/// The server returned 403 Forbidden (wrong admin or auth code).
#[ error( " forbidden: wrong auth code " ) ]
Forbidden ,
}
/// Response from `GET /api/status`.
#[ derive(Deserialize, Clone, PartialEq) ]
pub struct StatusResponse {
/// Whether new quote submissions are currently locked by an admin.
pub submissions_locked : bool ,
}
/// Response from `POST /api/admin/reset-auth-code`.
#[ derive(Deserialize) ]
struct ResetAuthCodeResponse {
pub auth_code : String ,
}
/// Response from `POST /api/admin/lock` or `POST /api/admin/unlock`.
#[ derive(Deserialize) ]
struct LockResponse {
pub submissions_locked : bool ,
}
/// Body sent to `POST /api/admin/reset-auth-code`.
#[ derive(Serialize) ]
struct ResetAuthCodeBody < ' a > {
#[ serde(skip_serializing_if = " Option::is_none " ) ]
new_code : Option < & ' a str > ,
}
}
/// Fetch a paginated list of quotes.
/// Fetch a paginated list of quotes.
@ -162,6 +191,125 @@ pub async fn delete_quote(id: &str, auth_code: &str) -> Result<(), ApiError> {
}
}
}
}
/// Fetch the current submission lock state from `GET /api/status`.
///
/// No authentication required. Returns [`StatusResponse`] containing
/// `submissions_locked`. Returns [`ApiError::Network`] on connection failure
/// or [`ApiError::Parse`] if the response is not valid JSON.
pub async fn get_status ( ) -> Result < StatusResponse , ApiError > {
fetch_json ( "/api/status" ) . await
}
/// Call `POST /api/admin/reset-auth-code` to rotate a quote's auth code.
///
/// # Arguments
/// - `current` — the current auth code for the quote (sent as `X-Auth-Code` header).
/// - `new_code` — an optional new passphrase; if `None` one is generated server-side.
/// - `admin_code` — the admin super-auth code (sent as `X-Admin-Code` header).
///
/// Returns the new auth code string on HTTP 200, or:
/// - [`ApiError::Forbidden`] on HTTP 403 (wrong admin code),
/// - [`ApiError::Server`] for other non-200 responses,
/// - [`ApiError::Network`] / [`ApiError::Parse`] for connection/parse errors.
pub async fn admin_reset_auth_code (
current : & str ,
new_code : Option < & str > ,
admin_code : & str ,
) -> Result < String , ApiError > {
let body = ResetAuthCodeBody { new_code } ;
let resp = gloo ::net ::http ::Request ::post ( "/api/admin/reset-auth-code" )
. header ( "X-Auth-Code" , current )
. header ( "X-Admin-Code" , admin_code )
. json ( & body )
. map_err ( | e | ApiError ::Network ( e . to_string ( ) ) ) ?
. send ( )
. await
. map_err ( | e | ApiError ::Network ( e . to_string ( ) ) ) ? ;
match resp . status ( ) {
200 = > {
let parsed : ResetAuthCodeResponse = resp
. json ( )
. await
. map_err ( | e | ApiError ::Parse ( e . to_string ( ) ) ) ? ;
Ok ( parsed . auth_code )
}
403 = > Err ( ApiError ::Forbidden ) ,
status = > {
let msg = resp . text ( ) . await . unwrap_or_default ( ) ;
Err ( ApiError ::Server {
status ,
message : msg ,
} )
}
}
}
/// Call `POST /api/admin/lock` to prevent new quote submissions.
///
/// Sends `X-Admin-Code: admin_code` in the request header. No request body.
///
/// Returns `Ok(true)` (submissions now locked) on HTTP 200, or:
/// - [`ApiError::Forbidden`] on HTTP 403 (wrong admin code),
/// - [`ApiError::Server`] for other non-200 responses,
/// - [`ApiError::Network`] / [`ApiError::Parse`] for connection/parse errors.
pub async fn admin_lock ( admin_code : & str ) -> Result < bool , ApiError > {
let resp = gloo ::net ::http ::Request ::post ( "/api/admin/lock" )
. header ( "X-Admin-Code" , admin_code )
. send ( )
. await
. map_err ( | e | ApiError ::Network ( e . to_string ( ) ) ) ? ;
match resp . status ( ) {
200 = > {
let parsed : LockResponse = resp
. json ( )
. await
. map_err ( | e | ApiError ::Parse ( e . to_string ( ) ) ) ? ;
Ok ( parsed . submissions_locked )
}
403 = > Err ( ApiError ::Forbidden ) ,
status = > {
let msg = resp . text ( ) . await . unwrap_or_default ( ) ;
Err ( ApiError ::Server {
status ,
message : msg ,
} )
}
}
}
/// Call `POST /api/admin/unlock` to allow new quote submissions again.
///
/// Sends `X-Admin-Code: admin_code` in the request header. No request body.
///
/// Returns `Ok(false)` (submissions now unlocked) on HTTP 200, or:
/// - [`ApiError::Forbidden`] on HTTP 403 (wrong admin code),
/// - [`ApiError::Server`] for other non-200 responses,
/// - [`ApiError::Network`] / [`ApiError::Parse`] for connection/parse errors.
pub async fn admin_unlock ( admin_code : & str ) -> Result < bool , ApiError > {
let resp = gloo ::net ::http ::Request ::post ( "/api/admin/unlock" )
. header ( "X-Admin-Code" , admin_code )
. send ( )
. await
. map_err ( | e | ApiError ::Network ( e . to_string ( ) ) ) ? ;
match resp . status ( ) {
200 = > {
let parsed : LockResponse = resp
. json ( )
. await
. map_err ( | e | ApiError ::Parse ( e . to_string ( ) ) ) ? ;
Ok ( parsed . submissions_locked )
}
403 = > Err ( ApiError ::Forbidden ) ,
status = > {
let msg = resp . text ( ) . await . unwrap_or_default ( ) ;
Err ( ApiError ::Server {
status ,
message : msg ,
} )
}
}
}
/// Internal helper: GET a URL and deserialise the response body as JSON.
/// Internal helper: GET a URL and deserialise the response body as JSON.
///
///
/// Returns `ApiError` on non-2xx status or deserialisation failure.
/// Returns `ApiError` on non-2xx status or deserialisation failure.