mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-06 10:16:59 +00:00
fix: consolidate auth extractors & standardize usage
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAllowDeactivated;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -32,13 +32,9 @@ fn get_age_from_datestring(birth_date: &str) -> Option<i32> {
|
||||
pub struct GetPreferencesOutput {
|
||||
pub preferences: Vec<Value>,
|
||||
}
|
||||
pub async fn get_preferences(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
let has_full_access = auth_user.permissions().has_full_access();
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await {
|
||||
pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Active>) -> Response {
|
||||
let has_full_access = auth.permissions().has_full_access();
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(Some("User not found".into())).into_response();
|
||||
@@ -93,12 +89,11 @@ pub struct PutPreferencesInput {
|
||||
}
|
||||
pub async fn put_preferences(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<PutPreferencesInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
let has_full_access = auth_user.permissions().has_full_access();
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await {
|
||||
let has_full_access = auth.permissions().has_full_access();
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(Some("User not found".into())).into_response();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use axum::{
|
||||
@@ -18,28 +18,30 @@ pub struct DeleteAccountInput {
|
||||
|
||||
pub async fn delete_account(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<DeleteAccountInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let did = &input.did;
|
||||
let (user_id, handle) = match state.user_repo.get_id_and_handle_by_did(did).await {
|
||||
Ok(Some(row)) => (row.id, row.handle),
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let (user_id, handle) = state
|
||||
.user_repo
|
||||
.get_id_and_handle_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error in delete_account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if let Err(e) = state
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)
|
||||
.map(|row| (row.id, row.handle))?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.admin_delete_account_complete(user_id, did)
|
||||
.await
|
||||
{
|
||||
error!("Failed to delete account {}: {:?}", did, e);
|
||||
return ApiError::InternalError(Some("Failed to delete account".into())).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to delete account {}: {:?}", did, e);
|
||||
ApiError::InternalError(Some("Failed to delete account".into()))
|
||||
})?;
|
||||
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await
|
||||
{
|
||||
@@ -49,5 +51,5 @@ pub async fn delete_account(
|
||||
);
|
||||
}
|
||||
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::{ApiError, AtpJson};
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use axum::{
|
||||
@@ -28,29 +28,24 @@ pub struct SendEmailOutput {
|
||||
|
||||
pub async fn send_email(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
AtpJson(input): AtpJson<SendEmailInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let content = input.content.trim();
|
||||
if content.is_empty() {
|
||||
return ApiError::InvalidRequest("content is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("content is required".into()));
|
||||
}
|
||||
let user = match state.user_repo.get_by_did(&input.recipient_did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&input.recipient_did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error in send_email: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let email = match user.email {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return ApiError::NoEmail.into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let email = user.email.ok_or(ApiError::NoEmail)?;
|
||||
let (user_id, handle) = (user.id, user.handle);
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let subject = input
|
||||
@@ -76,11 +71,11 @@ pub async fn send_email(
|
||||
handle,
|
||||
input.recipient_did
|
||||
);
|
||||
(StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()
|
||||
Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to enqueue admin email: {:?}", e);
|
||||
(StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()
|
||||
Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle};
|
||||
use axum::{
|
||||
@@ -67,26 +67,23 @@ pub struct GetAccountInfosOutput {
|
||||
|
||||
pub async fn get_account_info(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Query(params): Query<GetAccountInfoParams>,
|
||||
) -> Response {
|
||||
let account = match state
|
||||
) -> Result<Response, ApiError> {
|
||||
let account = state
|
||||
.infra_repo
|
||||
.get_admin_account_info_by_did(¶ms.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(a)) => a,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error in get_account_info: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let invited_by = get_invited_by(&state, account.id).await;
|
||||
let invites = get_invites_for_user(&state, account.id).await;
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(AccountInfo {
|
||||
did: account.did,
|
||||
@@ -105,7 +102,7 @@ pub async fn get_account_info(
|
||||
invites,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> {
|
||||
@@ -200,30 +197,27 @@ async fn get_invite_code_info(state: &AppState, code: &str) -> Option<InviteCode
|
||||
|
||||
pub async fn get_account_infos(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
RawQuery(raw_query): RawQuery,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
|
||||
.into_iter()
|
||||
.filter(|d| !d.is_empty())
|
||||
.collect();
|
||||
|
||||
if dids.is_empty() {
|
||||
return ApiError::InvalidRequest("dids is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("dids is required".into()));
|
||||
}
|
||||
|
||||
let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect();
|
||||
let accounts = match state
|
||||
let accounts = state
|
||||
.infra_repo
|
||||
.get_admin_account_infos_by_dids(&dids_typed)
|
||||
.await
|
||||
{
|
||||
Ok(accounts) => accounts,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("Failed to fetch account infos: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect();
|
||||
|
||||
@@ -316,5 +310,5 @@ pub async fn get_account_infos(
|
||||
})
|
||||
.collect();
|
||||
|
||||
(StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
|
||||
Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle};
|
||||
use axum::{
|
||||
@@ -50,14 +50,14 @@ pub struct SearchAccountsOutput {
|
||||
|
||||
pub async fn search_accounts(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Query(params): Query<SearchAccountsParams>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let limit = params.limit.clamp(1, 100);
|
||||
let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
|
||||
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
|
||||
let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok());
|
||||
let result = state
|
||||
let rows = state
|
||||
.user_repo
|
||||
.search_accounts(
|
||||
cursor_did.as_ref(),
|
||||
@@ -65,44 +65,41 @@ pub async fn search_accounts(
|
||||
handle_filter.as_deref(),
|
||||
limit + 1,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let has_more = rows.len() > limit as usize;
|
||||
let accounts: Vec<AccountView> = rows
|
||||
.into_iter()
|
||||
.take(limit as usize)
|
||||
.map(|row| AccountView {
|
||||
did: row.did.clone(),
|
||||
handle: row.handle,
|
||||
email: row.email,
|
||||
indexed_at: row.created_at.to_rfc3339(),
|
||||
email_confirmed_at: if row.email_verified {
|
||||
Some(row.created_at.to_rfc3339())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
|
||||
invites_disabled: row.invites_disabled,
|
||||
})
|
||||
.collect();
|
||||
let next_cursor = if has_more {
|
||||
accounts.last().map(|a| a.did.to_string())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error in search_accounts: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let has_more = rows.len() > limit as usize;
|
||||
let accounts: Vec<AccountView> = rows
|
||||
.into_iter()
|
||||
.take(limit as usize)
|
||||
.map(|row| AccountView {
|
||||
did: row.did.clone(),
|
||||
handle: row.handle,
|
||||
email: row.email,
|
||||
indexed_at: row.created_at.to_rfc3339(),
|
||||
email_confirmed_at: if row.email_verified {
|
||||
Some(row.created_at.to_rfc3339())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(SearchAccountsOutput {
|
||||
cursor: next_cursor,
|
||||
accounts,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in search_accounts: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
},
|
||||
deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
|
||||
invites_disabled: row.invites_disabled,
|
||||
})
|
||||
.collect();
|
||||
let next_cursor = if has_more {
|
||||
accounts.last().map(|a| a.did.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(SearchAccountsOutput {
|
||||
cursor: next_cursor,
|
||||
accounts,
|
||||
}),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use axum::{
|
||||
@@ -19,28 +19,30 @@ pub struct UpdateAccountEmailInput {
|
||||
|
||||
pub async fn update_account_email(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<UpdateAccountEmailInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let account = input.account.trim();
|
||||
let email = input.email.trim();
|
||||
if account.is_empty() || email.is_empty() {
|
||||
return ApiError::InvalidRequest("account and email are required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"account and email are required".into(),
|
||||
));
|
||||
}
|
||||
let account_did: Did = match account.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let account_did: Did = account
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.admin_update_email(&account_did, email)
|
||||
.await
|
||||
{
|
||||
Ok(0) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(_) => EmptyResponse::ok().into_response(),
|
||||
Ok(0) => Err(ApiError::AccountNotFound),
|
||||
Ok(_) => Ok(EmptyResponse::ok().into_response()),
|
||||
Err(e) => {
|
||||
error!("DB error updating email: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,19 +55,19 @@ pub struct UpdateAccountHandleInput {
|
||||
|
||||
pub async fn update_account_handle(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<UpdateAccountHandleInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let did = &input.did;
|
||||
let input_handle = input.handle.trim();
|
||||
if input_handle.is_empty() {
|
||||
return ApiError::InvalidRequest("handle is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("handle is required".into()));
|
||||
}
|
||||
if !input_handle
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
|
||||
{
|
||||
return ApiError::InvalidHandle(None).into_response();
|
||||
return Err(ApiError::InvalidHandle(None));
|
||||
}
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname);
|
||||
@@ -75,24 +77,27 @@ pub async fn update_account_handle(
|
||||
input_handle.to_string()
|
||||
};
|
||||
let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten();
|
||||
let user_id = match state.user_repo.get_id_by_did(did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => return ApiError::AccountNotFound.into_response(),
|
||||
};
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
let handle_for_check = Handle::new_unchecked(&handle);
|
||||
if let Ok(true) = state
|
||||
.user_repo
|
||||
.check_handle_exists(&handle_for_check, user_id)
|
||||
.await
|
||||
{
|
||||
return ApiError::HandleTaken.into_response();
|
||||
return Err(ApiError::HandleTaken);
|
||||
}
|
||||
match state
|
||||
.user_repo
|
||||
.admin_update_handle(did, &handle_for_check)
|
||||
.await
|
||||
{
|
||||
Ok(0) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(0) => Err(ApiError::AccountNotFound),
|
||||
Ok(_) => {
|
||||
if let Some(old) = old_handle {
|
||||
let _ = state.cache.delete(&format!("handle:{}", old)).await;
|
||||
@@ -115,11 +120,11 @@ pub async fn update_account_handle(
|
||||
{
|
||||
warn!("Failed to update PLC handle for admin handle update: {}", e);
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error updating handle: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,31 +137,29 @@ pub struct UpdateAccountPasswordInput {
|
||||
|
||||
pub async fn update_account_password(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<UpdateAccountPasswordInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let did = &input.did;
|
||||
let password = input.password.trim();
|
||||
if password.is_empty() {
|
||||
return ApiError::InvalidRequest("password is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("password is required".into()));
|
||||
}
|
||||
let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.admin_update_password(did, &password_hash)
|
||||
.await
|
||||
{
|
||||
Ok(0) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(_) => EmptyResponse::ok().into_response(),
|
||||
Ok(0) => Err(ApiError::AccountNotFound),
|
||||
Ok(_) => Ok(EmptyResponse::ok().into_response()),
|
||||
Err(e) => {
|
||||
error!("DB error updating password: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -78,7 +78,7 @@ pub async fn get_server_config(
|
||||
|
||||
pub async fn update_server_config(
|
||||
State(state): State<AppState>,
|
||||
_admin: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(req): Json<UpdateServerConfigRequest>,
|
||||
) -> Result<Json<UpdateServerConfigResponse>, ApiError> {
|
||||
if let Some(server_name) = req.server_name {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -21,9 +21,9 @@ pub struct DisableInviteCodesInput {
|
||||
|
||||
pub async fn disable_invite_codes(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<DisableInviteCodesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Some(codes) = &input.codes
|
||||
&& let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await
|
||||
{
|
||||
@@ -40,7 +40,7 @@ pub async fn disable_invite_codes(
|
||||
error!("DB error disabling invite codes by account: {:?}", e);
|
||||
}
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -78,26 +78,23 @@ pub struct GetInviteCodesOutput {
|
||||
|
||||
pub async fn get_invite_codes(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Query(params): Query<GetInviteCodesParams>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let limit = params.limit.unwrap_or(100).clamp(1, 500);
|
||||
let sort_order = match params.sort.as_deref() {
|
||||
Some("usage") => InviteCodeSortOrder::Usage,
|
||||
_ => InviteCodeSortOrder::Recent,
|
||||
};
|
||||
|
||||
let codes_rows = match state
|
||||
let codes_rows = state
|
||||
.infra_repo
|
||||
.list_invite_codes(params.cursor.as_deref(), limit, sort_order)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching invite codes: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect();
|
||||
let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
|
||||
@@ -155,14 +152,14 @@ pub async fn get_invite_codes(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(GetInviteCodesOutput {
|
||||
cursor: next_cursor,
|
||||
codes,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -172,27 +169,27 @@ pub struct DisableAccountInvitesInput {
|
||||
|
||||
pub async fn disable_account_invites(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<DisableAccountInvitesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let account = input.account.trim();
|
||||
if account.is_empty() {
|
||||
return ApiError::InvalidRequest("account is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("account is required".into()));
|
||||
}
|
||||
let account_did: tranquil_types::Did = match account.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let account_did: tranquil_types::Did = account
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.set_invites_disabled(&account_did, true)
|
||||
.await
|
||||
{
|
||||
Ok(true) => EmptyResponse::ok().into_response(),
|
||||
Ok(false) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(true) => Ok(EmptyResponse::ok().into_response()),
|
||||
Ok(false) => Err(ApiError::AccountNotFound),
|
||||
Err(e) => {
|
||||
error!("DB error disabling account invites: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,27 +201,27 @@ pub struct EnableAccountInvitesInput {
|
||||
|
||||
pub async fn enable_account_invites(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<EnableAccountInvitesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let account = input.account.trim();
|
||||
if account.is_empty() {
|
||||
return ApiError::InvalidRequest("account is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("account is required".into()));
|
||||
}
|
||||
let account_did: tranquil_types::Did = match account.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let account_did: tranquil_types::Did = account
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.set_invites_disabled(&account_did, false)
|
||||
.await
|
||||
{
|
||||
Ok(true) => EmptyResponse::ok().into_response(),
|
||||
Ok(false) => ApiError::AccountNotFound.into_response(),
|
||||
Ok(true) => Ok(EmptyResponse::ok().into_response()),
|
||||
Ok(false) => Err(ApiError::AccountNotFound),
|
||||
Err(e) => {
|
||||
error!("DB error enabling account invites: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -16,17 +17,20 @@ pub struct ServerStatsResponse {
|
||||
pub blob_storage_bytes: i64,
|
||||
}
|
||||
|
||||
pub async fn get_server_stats(State(state): State<AppState>, _auth: BearerAuthAdmin) -> Response {
|
||||
pub async fn get_server_stats(
|
||||
State(state): State<AppState>,
|
||||
_auth: Auth<Admin>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let user_count = state.user_repo.count_users().await.unwrap_or(0);
|
||||
let repo_count = state.repo_repo.count_repos().await.unwrap_or(0);
|
||||
let record_count = state.repo_repo.count_all_records().await.unwrap_or(0);
|
||||
let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0);
|
||||
|
||||
Json(ServerStatsResponse {
|
||||
Ok(Json(ServerStatsResponse {
|
||||
user_count,
|
||||
repo_count,
|
||||
record_count,
|
||||
blob_storage_bytes,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAdmin;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{CidLink, Did};
|
||||
use axum::{
|
||||
@@ -35,17 +35,18 @@ pub struct StatusAttr {
|
||||
|
||||
pub async fn get_subject_status(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Query(params): Query<GetSubjectStatusParams>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
|
||||
return ApiError::InvalidRequest("Must provide did, uri, or blob".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Must provide did, uri, or blob".into(),
|
||||
));
|
||||
}
|
||||
if let Some(did_str) = ¶ms.did {
|
||||
let did: Did = match did_str.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let did: Did = did_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
|
||||
match state.user_repo.get_status_by_did(&did).await {
|
||||
Ok(Some(status)) => {
|
||||
let deactivated = status.deactivated_at.map(|_| StatusAttr {
|
||||
@@ -56,7 +57,7 @@ pub async fn get_subject_status(
|
||||
applied: true,
|
||||
r#ref: Some(r.clone()),
|
||||
});
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(SubjectStatus {
|
||||
subject: json!({
|
||||
@@ -67,29 +68,28 @@ pub async fn get_subject_status(
|
||||
deactivated,
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
Ok(None) => {
|
||||
return ApiError::SubjectNotFound.into_response();
|
||||
return Err(ApiError::SubjectNotFound);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in get_subject_status: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(uri_str) = ¶ms.uri {
|
||||
let cid: CidLink = match uri_str.parse() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(),
|
||||
};
|
||||
let cid: CidLink = uri_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?;
|
||||
match state.repo_repo.get_record_by_cid(&cid).await {
|
||||
Ok(Some(record)) => {
|
||||
let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr {
|
||||
applied: true,
|
||||
r#ref: Some(r.clone()),
|
||||
});
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(SubjectStatus {
|
||||
subject: json!({
|
||||
@@ -101,36 +101,31 @@ pub async fn get_subject_status(
|
||||
deactivated: None,
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
Ok(None) => {
|
||||
return ApiError::RecordNotFound.into_response();
|
||||
return Err(ApiError::RecordNotFound);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in get_subject_status: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(blob_cid_str) = ¶ms.blob {
|
||||
let blob_cid: CidLink = match blob_cid_str.parse() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(),
|
||||
};
|
||||
let did = match ¶ms.did {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return ApiError::InvalidRequest("Must provide a did to request blob state".into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let blob_cid: CidLink = blob_cid_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?;
|
||||
let did = params.did.as_ref().ok_or_else(|| {
|
||||
ApiError::InvalidRequest("Must provide a did to request blob state".into())
|
||||
})?;
|
||||
match state.blob_repo.get_blob_with_takedown(&blob_cid).await {
|
||||
Ok(Some(blob)) => {
|
||||
let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr {
|
||||
applied: true,
|
||||
r#ref: Some(r.clone()),
|
||||
});
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(SubjectStatus {
|
||||
subject: json!({
|
||||
@@ -142,18 +137,18 @@ pub async fn get_subject_status(
|
||||
deactivated: None,
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
Ok(None) => {
|
||||
return ApiError::BlobNotFound(None).into_response();
|
||||
return Err(ApiError::BlobNotFound(None));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in get_subject_status: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
ApiError::InvalidRequest("Invalid subject type".into()).into_response()
|
||||
Err(ApiError::InvalidRequest("Invalid subject type".into()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -172,9 +167,9 @@ pub struct StatusAttrInput {
|
||||
|
||||
pub async fn update_subject_status(
|
||||
State(state): State<AppState>,
|
||||
_auth: BearerAuthAdmin,
|
||||
_auth: Auth<Admin>,
|
||||
Json(input): Json<UpdateSubjectStatusInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
|
||||
match subject_type {
|
||||
Some("com.atproto.admin.defs#repoRef") => {
|
||||
@@ -187,13 +182,14 @@ pub async fn update_subject_status(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(e) = state.user_repo.set_user_takedown(&did, takedown_ref).await {
|
||||
error!("Failed to update user takedown status for {}: {:?}", did, e);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to update takedown status".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.set_user_takedown(&did, takedown_ref)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update user takedown status for {}: {:?}", did, e);
|
||||
ApiError::InternalError(Some("Failed to update takedown status".into()))
|
||||
})?;
|
||||
}
|
||||
if let Some(deactivated) = &input.deactivated {
|
||||
let result = if deactivated.applied {
|
||||
@@ -201,16 +197,13 @@ pub async fn update_subject_status(
|
||||
} else {
|
||||
state.user_repo.activate_account(&did).await
|
||||
};
|
||||
if let Err(e) = result {
|
||||
result.map_err(|e| {
|
||||
error!(
|
||||
"Failed to update user deactivation status for {}: {:?}",
|
||||
did, e
|
||||
);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to update deactivation status".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
ApiError::InternalError(Some("Failed to update deactivation status".into()))
|
||||
})?;
|
||||
}
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let status = if takedown.applied {
|
||||
@@ -249,7 +242,7 @@ pub async fn update_subject_status(
|
||||
if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await {
|
||||
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
|
||||
}
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"subject": input.subject,
|
||||
@@ -262,41 +255,34 @@ pub async fn update_subject_status(
|
||||
}))
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
Some("com.atproto.repo.strongRef") => {
|
||||
let uri_str = input.subject.get("uri").and_then(|u| u.as_str());
|
||||
if let Some(uri_str) = uri_str {
|
||||
let cid: CidLink = match uri_str.parse() {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid CID format".into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let cid: CidLink = uri_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?;
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.applied {
|
||||
takedown.r#ref.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(e) = state
|
||||
state
|
||||
.repo_repo
|
||||
.set_record_takedown(&cid, takedown_ref)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to update record takedown status for {}: {:?}",
|
||||
uri_str, e
|
||||
);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to update takedown status".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to update record takedown status for {}: {:?}",
|
||||
uri_str, e
|
||||
);
|
||||
ApiError::InternalError(Some("Failed to update takedown status".into()))
|
||||
})?;
|
||||
}
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"subject": input.subject,
|
||||
@@ -306,41 +292,34 @@ pub async fn update_subject_status(
|
||||
}))
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
Some("com.atproto.admin.defs#repoBlobRef") => {
|
||||
let cid_str = input.subject.get("cid").and_then(|c| c.as_str());
|
||||
if let Some(cid_str) = cid_str {
|
||||
let cid: CidLink = match cid_str.parse() {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid CID format".into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let cid: CidLink = cid_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?;
|
||||
if let Some(takedown) = &input.takedown {
|
||||
let takedown_ref = if takedown.applied {
|
||||
takedown.r#ref.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(e) = state
|
||||
state
|
||||
.blob_repo
|
||||
.update_blob_takedown(&cid, takedown_ref)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to update blob takedown status for {}: {:?}",
|
||||
cid_str, e
|
||||
);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to update takedown status".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to update blob takedown status for {}: {:?}",
|
||||
cid_str, e
|
||||
);
|
||||
ApiError::InternalError(Some("Failed to update takedown status".into()))
|
||||
})?;
|
||||
}
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"subject": input.subject,
|
||||
@@ -350,10 +329,10 @@ pub async fn update_subject_status(
|
||||
}))
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
ApiError::InvalidRequest("Invalid subject type".into()).into_response()
|
||||
Err(ApiError::InvalidRequest("Invalid subject type".into()))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::{EmptyResponse, EnabledResponse};
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::scheduled::generate_full_backup;
|
||||
use crate::state::AppState;
|
||||
use crate::storage::{BackupStorage, backup_retention_count};
|
||||
@@ -35,24 +35,27 @@ pub struct ListBackupsOutput {
|
||||
pub backup_enabled: bool,
|
||||
}
|
||||
|
||||
pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let (user_id, backup_enabled) =
|
||||
match state.backup_repo.get_user_backup_status(&auth.0.did).await {
|
||||
Ok(Some(status)) => status,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
pub async fn list_backups(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.did).await
|
||||
{
|
||||
Ok(Some(status)) => status,
|
||||
Ok(None) => {
|
||||
return Ok(ApiError::AccountNotFound.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let backups = match state.backup_repo.list_backups_for_user(user_id).await {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
error!("DB error fetching backups: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,14 +71,14 @@ pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Re
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(ListBackupsOutput {
|
||||
backups: backup_list,
|
||||
backup_enabled,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -85,35 +88,35 @@ pub struct GetBackupQuery {
|
||||
|
||||
pub async fn get_backup(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Query(query): Query<GetBackupQuery>,
|
||||
) -> Response {
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let backup_id = match uuid::Uuid::parse_str(&query.id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid backup ID".into()).into_response();
|
||||
return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let backup_info = match state
|
||||
.backup_repo
|
||||
.get_backup_storage_info(backup_id, &auth.0.did)
|
||||
.get_backup_storage_info(backup_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
return ApiError::BackupNotFound.into_response();
|
||||
return Ok(ApiError::BackupNotFound.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching backup: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let backup_storage = match state.backup_storage.as_ref() {
|
||||
Some(storage) => storage,
|
||||
None => {
|
||||
return ApiError::BackupsDisabled.into_response();
|
||||
return Ok(ApiError::BackupsDisabled.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,12 +124,13 @@ pub async fn get_backup(
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Failed to fetch backup from storage: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to retrieve backup".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
[
|
||||
(axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"),
|
||||
@@ -137,7 +141,7 @@ pub async fn get_backup(
|
||||
],
|
||||
car_bytes,
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -149,40 +153,45 @@ pub struct CreateBackupOutput {
|
||||
pub block_count: i32,
|
||||
}
|
||||
|
||||
pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
pub async fn create_backup(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let backup_storage = match state.backup_storage.as_ref() {
|
||||
Some(storage) => storage,
|
||||
None => {
|
||||
return ApiError::BackupsDisabled.into_response();
|
||||
return Ok(ApiError::BackupsDisabled.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let user = match state.backup_repo.get_user_for_backup(&auth.0.did).await {
|
||||
let user = match state.backup_repo.get_user_for_backup(&auth.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
return Ok(ApiError::AccountNotFound.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
if user.deactivated_at.is_some() {
|
||||
return ApiError::AccountDeactivated.into_response();
|
||||
return Ok(ApiError::AccountDeactivated.into_response());
|
||||
}
|
||||
|
||||
let repo_rev = match &user.repo_rev {
|
||||
Some(rev) => rev.clone(),
|
||||
None => {
|
||||
return ApiError::RepoNotReady.into_response();
|
||||
return Ok(ApiError::RepoNotReady.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let head_cid = match Cid::from_str(&user.repo_root_cid) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -197,8 +206,9 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Failed to generate CAR: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to generate backup".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to generate backup".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,7 +222,9 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
Ok(key) => key,
|
||||
Err(e) => {
|
||||
error!("Failed to upload backup: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to store backup".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to store backup".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,7 +250,9 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
"Failed to rollback orphaned backup from S3"
|
||||
);
|
||||
}
|
||||
return ApiError::InternalError(Some("Failed to record backup".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to record backup".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -261,7 +275,7 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup");
|
||||
}
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(CreateBackupOutput {
|
||||
id: backup_id.to_string(),
|
||||
@@ -270,7 +284,7 @@ pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
block_count,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn cleanup_old_backups(
|
||||
@@ -310,33 +324,33 @@ pub struct DeleteBackupQuery {
|
||||
|
||||
pub async fn delete_backup(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Query(query): Query<DeleteBackupQuery>,
|
||||
) -> Response {
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let backup_id = match uuid::Uuid::parse_str(&query.id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid backup ID".into()).into_response();
|
||||
return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let backup = match state
|
||||
.backup_repo
|
||||
.get_backup_for_deletion(backup_id, &auth.0.did)
|
||||
.get_backup_for_deletion(backup_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
return ApiError::BackupNotFound.into_response();
|
||||
return Ok(ApiError::BackupNotFound.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching backup: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
if backup.deactivated_at.is_some() {
|
||||
return ApiError::AccountDeactivated.into_response();
|
||||
return Ok(ApiError::AccountDeactivated.into_response());
|
||||
}
|
||||
|
||||
if let Some(backup_storage) = state.backup_storage.as_ref()
|
||||
@@ -351,12 +365,12 @@ pub async fn delete_backup(
|
||||
|
||||
if let Err(e) = state.backup_repo.delete_backup(backup.id).await {
|
||||
error!("DB error deleting backup: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to delete backup".into())).into_response();
|
||||
return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response());
|
||||
}
|
||||
|
||||
info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup");
|
||||
info!(did = %auth.did, backup_id = %backup_id, "Deleted backup");
|
||||
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -367,51 +381,56 @@ pub struct SetBackupEnabledInput {
|
||||
|
||||
pub async fn set_backup_enabled(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<SetBackupEnabledInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let deactivated_at = match state
|
||||
.backup_repo
|
||||
.get_user_deactivated_status(&auth.0.did)
|
||||
.get_user_deactivated_status(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(status)) => status,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
return Ok(ApiError::AccountNotFound.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
if deactivated_at.is_some() {
|
||||
return ApiError::AccountDeactivated.into_response();
|
||||
return Ok(ApiError::AccountDeactivated.into_response());
|
||||
}
|
||||
|
||||
if let Err(e) = state
|
||||
.backup_repo
|
||||
.update_backup_enabled(&auth.0.did, input.enabled)
|
||||
.update_backup_enabled(&auth.did, input.enabled)
|
||||
.await
|
||||
{
|
||||
error!("DB error updating backup_enabled: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to update setting".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to update setting".into())).into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting");
|
||||
info!(did = %auth.did, enabled = input.enabled, "Updated backup_enabled setting");
|
||||
|
||||
EnabledResponse::response(input.enabled).into_response()
|
||||
Ok(EnabledResponse::response(input.enabled).into_response())
|
||||
}
|
||||
|
||||
pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let user_id = match state.backup_repo.get_user_id_by_did(&auth.0.did).await {
|
||||
pub async fn export_blobs(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
return Ok(ApiError::AccountNotFound.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -419,12 +438,12 @@ pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Re
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
error!("DB error fetching blobs: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
if blobs.is_empty() {
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
[
|
||||
(axum::http::header::CONTENT_TYPE, "application/zip"),
|
||||
@@ -435,7 +454,7 @@ pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Re
|
||||
],
|
||||
Vec::<u8>::new(),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let mut zip_buffer = std::io::Cursor::new(Vec::new());
|
||||
@@ -513,16 +532,17 @@ pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Re
|
||||
|
||||
if let Err(e) = zip.finish() {
|
||||
error!("Failed to finish zip: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to create zip file".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to create zip file".into())).into_response(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let zip_bytes = zip_buffer.into_inner();
|
||||
|
||||
info!(did = %auth.0.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs");
|
||||
info!(did = %auth.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs");
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
[
|
||||
(axum::http::header::CONTENT_TYPE, "application/zip"),
|
||||
@@ -533,7 +553,7 @@ pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Re
|
||||
],
|
||||
zip_bytes,
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
fn mime_to_extension(mime_type: &str) -> &'static str {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::create_signed_commit;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::{Did, Handle, Nsid, Rkey};
|
||||
@@ -33,21 +33,25 @@ pub struct ListControllersResponse {
|
||||
pub controllers: Vec<ControllerInfo>,
|
||||
}
|
||||
|
||||
pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
pub async fn list_controllers(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let controllers = match state
|
||||
.delegation_repo
|
||||
.get_delegations_for_account(&auth.0.did)
|
||||
.get_delegations_for_account(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to list controllers: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to list controllers".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to list controllers".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Json(ListControllersResponse {
|
||||
Ok(Json(ListControllersResponse {
|
||||
controllers: controllers
|
||||
.into_iter()
|
||||
.map(|c| ControllerInfo {
|
||||
@@ -59,7 +63,7 @@ pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -70,11 +74,11 @@ pub struct AddControllerInput {
|
||||
|
||||
pub async fn add_controller(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<AddControllerInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) {
|
||||
return ApiError::InvalidScopes(e).into_response();
|
||||
return Ok(ApiError::InvalidScopes(e).into_response());
|
||||
}
|
||||
|
||||
let controller_exists = state
|
||||
@@ -86,24 +90,22 @@ pub async fn add_controller(
|
||||
.is_some();
|
||||
|
||||
if !controller_exists {
|
||||
return ApiError::ControllerNotFound.into_response();
|
||||
return Ok(ApiError::ControllerNotFound.into_response());
|
||||
}
|
||||
|
||||
match state
|
||||
.delegation_repo
|
||||
.controls_any_accounts(&auth.0.did)
|
||||
.await
|
||||
{
|
||||
match state.delegation_repo.controls_any_accounts(&auth.did).await {
|
||||
Ok(true) => {
|
||||
return ApiError::InvalidDelegation(
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
"Cannot add controllers to an account that controls other accounts".into(),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to check delegation status: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to verify delegation status".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to verify delegation status".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
Ok(false) => {}
|
||||
}
|
||||
@@ -114,15 +116,17 @@ pub async fn add_controller(
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
return ApiError::InvalidDelegation(
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
"Cannot add a controlled account as a controller".into(),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to check controller status: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to verify controller status".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to verify controller status".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
Ok(false) => {}
|
||||
}
|
||||
@@ -130,10 +134,10 @@ pub async fn add_controller(
|
||||
match state
|
||||
.delegation_repo
|
||||
.create_delegation(
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
&input.controller_did,
|
||||
&input.granted_scopes,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -141,8 +145,8 @@ pub async fn add_controller(
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&auth.0.did,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::GrantCreated,
|
||||
Some(serde_json::json!({
|
||||
@@ -153,17 +157,17 @@ pub async fn add_controller(
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add controller: {:?}", e);
|
||||
ApiError::InternalError(Some("Failed to add controller".into())).into_response()
|
||||
Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,32 +179,32 @@ pub struct RemoveControllerInput {
|
||||
|
||||
pub async fn remove_controller(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RemoveControllerInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.revoke_delegation(&auth.0.did, &input.controller_did, &auth.0.did)
|
||||
.revoke_delegation(&auth.did, &input.controller_did, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let revoked_app_passwords = state
|
||||
.session_repo
|
||||
.delete_app_passwords_by_controller(&auth.0.did, &input.controller_did)
|
||||
.delete_app_passwords_by_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0) as usize;
|
||||
|
||||
let revoked_oauth_tokens = state
|
||||
.oauth_repo
|
||||
.revoke_tokens_for_controller(&auth.0.did, &input.controller_did)
|
||||
.revoke_tokens_for_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&auth.0.did,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::GrantRevoked,
|
||||
Some(serde_json::json!({
|
||||
@@ -212,18 +216,18 @@ pub async fn remove_controller(
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
Ok(false) => ApiError::DelegationNotFound.into_response(),
|
||||
Ok(false) => Ok(ApiError::DelegationNotFound.into_response()),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove controller: {:?}", e);
|
||||
ApiError::InternalError(Some("Failed to remove controller".into())).into_response()
|
||||
Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,24 +240,24 @@ pub struct UpdateControllerScopesInput {
|
||||
|
||||
pub async fn update_controller_scopes(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateControllerScopesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) {
|
||||
return ApiError::InvalidScopes(e).into_response();
|
||||
return Ok(ApiError::InvalidScopes(e).into_response());
|
||||
}
|
||||
|
||||
match state
|
||||
.delegation_repo
|
||||
.update_delegation_scopes(&auth.0.did, &input.controller_did, &input.granted_scopes)
|
||||
.update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&auth.0.did,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::ScopesModified,
|
||||
Some(serde_json::json!({
|
||||
@@ -264,19 +268,21 @@ pub async fn update_controller_scopes(
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
Ok(false) => ApiError::DelegationNotFound.into_response(),
|
||||
Ok(false) => Ok(ApiError::DelegationNotFound.into_response()),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to update controller scopes: {:?}", e);
|
||||
ApiError::InternalError(Some("Failed to update controller scopes".into()))
|
||||
.into_response()
|
||||
Ok(
|
||||
ApiError::InternalError(Some("Failed to update controller scopes".into()))
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,21 +301,26 @@ pub struct ListControlledAccountsResponse {
|
||||
pub accounts: Vec<DelegatedAccountInfo>,
|
||||
}
|
||||
|
||||
pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
pub async fn list_controlled_accounts(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let accounts = match state
|
||||
.delegation_repo
|
||||
.get_accounts_controlled_by(&auth.0.did)
|
||||
.get_accounts_controlled_by(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to list controlled accounts: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to list controlled accounts".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to list controlled accounts".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Json(ListControlledAccountsResponse {
|
||||
Ok(Json(ListControlledAccountsResponse {
|
||||
accounts: accounts
|
||||
.into_iter()
|
||||
.map(|a| DelegatedAccountInfo {
|
||||
@@ -320,7 +331,7 @@ pub async fn list_controlled_accounts(State(state): State<AppState>, auth: Beare
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -355,31 +366,33 @@ pub struct GetAuditLogResponse {
|
||||
|
||||
pub async fn get_audit_log(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Query(params): Query<AuditLogParams>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let limit = params.limit.clamp(1, 100);
|
||||
let offset = params.offset.max(0);
|
||||
|
||||
let entries = match state
|
||||
.delegation_repo
|
||||
.get_audit_log_for_account(&auth.0.did, limit, offset)
|
||||
.get_audit_log_for_account(&auth.did, limit, offset)
|
||||
.await
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get audit log: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to get audit log".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get audit log".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let total = state
|
||||
.delegation_repo
|
||||
.count_audit_log_entries(&auth.0.did)
|
||||
.count_audit_log_entries(&auth.did)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
Json(GetAuditLogResponse {
|
||||
Ok(Json(GetAuditLogResponse {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|e| AuditLogEntry {
|
||||
@@ -394,7 +407,7 @@ pub async fn get_audit_log(
|
||||
.collect(),
|
||||
total,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -444,36 +457,38 @@ pub struct CreateDelegatedAccountResponse {
|
||||
pub async fn create_delegated_account(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<CreateDelegatedAccountInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let client_ip = extract_client_ip(&headers);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::AccountCreation, &client_ip)
|
||||
.await
|
||||
{
|
||||
warn!(ip = %client_ip, "Delegated account creation rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(Some(
|
||||
return Ok(ApiError::RateLimitExceeded(Some(
|
||||
"Too many account creation attempts. Please try again later.".into(),
|
||||
))
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
|
||||
if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) {
|
||||
return ApiError::InvalidScopes(e).into_response();
|
||||
return Ok(ApiError::InvalidScopes(e).into_response());
|
||||
}
|
||||
|
||||
match state.delegation_repo.has_any_controllers(&auth.0.did).await {
|
||||
match state.delegation_repo.has_any_controllers(&auth.did).await {
|
||||
Ok(true) => {
|
||||
return ApiError::InvalidDelegation(
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
"Cannot create delegated accounts from a controlled account".into(),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to check controller status: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to verify controller status".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to verify controller status".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
Ok(false) => {}
|
||||
}
|
||||
@@ -494,7 +509,7 @@ pub async fn create_delegated_account(
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => format!("{}.{}", h, hostname_for_handles),
|
||||
Err(e) => {
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -509,7 +524,7 @@ pub async fn create_delegated_account(
|
||||
if let Some(ref email) = email
|
||||
&& !crate::api::validation::is_valid_email(email)
|
||||
{
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
return Ok(ApiError::InvalidEmail.into_response());
|
||||
}
|
||||
|
||||
if let Some(ref code) = input.invite_code {
|
||||
@@ -520,14 +535,14 @@ pub async fn create_delegated_account(
|
||||
.unwrap_or(false);
|
||||
|
||||
if !valid {
|
||||
return ApiError::InvalidInviteCode.into_response();
|
||||
return Ok(ApiError::InvalidInviteCode.into_response());
|
||||
}
|
||||
} else {
|
||||
let invite_required = std::env::var("INVITE_CODE_REQUIRED")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
if invite_required {
|
||||
return ApiError::InviteCodeRequired.into_response();
|
||||
return Ok(ApiError::InviteCodeRequired.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +557,7 @@ pub async fn create_delegated_account(
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Error creating signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -558,8 +573,10 @@ pub async fn create_delegated_account(
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error creating PLC genesis operation: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to create PLC operation".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to create PLC operation".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -569,22 +586,22 @@ pub async fn create_delegated_account(
|
||||
.await
|
||||
{
|
||||
error!("Failed to submit PLC genesis operation: {:?}", e);
|
||||
return ApiError::UpstreamErrorMsg(format!(
|
||||
return Ok(ApiError::UpstreamErrorMsg(format!(
|
||||
"Failed to register DID with PLC directory: {}",
|
||||
e
|
||||
))
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let did = Did::new_unchecked(&genesis_result.did);
|
||||
let handle = Handle::new_unchecked(&handle);
|
||||
info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account");
|
||||
info!(did = %did, handle = %handle, controller = %&auth.did, "Created DID for delegated account");
|
||||
|
||||
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Error encrypting signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -593,7 +610,7 @@ pub async fn create_delegated_account(
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error persisting MST: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
@@ -602,14 +619,14 @@ pub async fn create_delegated_account(
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Error creating genesis commit: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error saving genesis commit: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
@@ -618,7 +635,7 @@ pub async fn create_delegated_account(
|
||||
handle: handle.clone(),
|
||||
email: email.clone(),
|
||||
did: did.clone(),
|
||||
controller_did: auth.0.did.clone(),
|
||||
controller_did: auth.did.clone(),
|
||||
controller_scopes: input.controller_scopes.clone(),
|
||||
encrypted_key_bytes,
|
||||
encryption_version: crate::config::ENCRYPTION_VERSION,
|
||||
@@ -635,14 +652,14 @@ pub async fn create_delegated_account(
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return ApiError::HandleNotAvailable(None).into_response();
|
||||
return Ok(ApiError::HandleNotAvailable(None).into_response());
|
||||
}
|
||||
Err(tranquil_db_traits::CreateAccountError::EmailTaken) => {
|
||||
return ApiError::EmailTaken.into_response();
|
||||
return Ok(ApiError::EmailTaken.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error creating delegated account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -678,8 +695,8 @@ pub async fn create_delegated_account(
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&did,
|
||||
&auth.0.did,
|
||||
Some(&auth.0.did),
|
||||
&auth.did,
|
||||
Some(&auth.did),
|
||||
DelegationActionType::GrantCreated,
|
||||
Some(json!({
|
||||
"account_created": true,
|
||||
@@ -690,7 +707,7 @@ pub async fn create_delegated_account(
|
||||
)
|
||||
.await;
|
||||
|
||||
info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created");
|
||||
info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created");
|
||||
|
||||
Json(CreateDelegatedAccountResponse { did, handle }).into_response()
|
||||
Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response())
|
||||
}
|
||||
|
||||
@@ -543,6 +543,13 @@ impl From<crate::auth::extractor::AuthError> for ApiError {
|
||||
crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated,
|
||||
crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown,
|
||||
crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired,
|
||||
crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed(
|
||||
Some("Service authentication not allowed for this endpoint".to_string()),
|
||||
),
|
||||
crate::auth::extractor::AuthError::SigningKeyRequired => Self::InvalidSigningKey,
|
||||
crate::auth::extractor::AuthError::InsufficientScope(msg) => {
|
||||
Self::InsufficientScope(Some(msg))
|
||||
}
|
||||
crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => {
|
||||
Self::OAuthExpiredToken(Some(msg))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::{ApiError, DidResponse, EmptyResponse};
|
||||
use crate::auth::BearerAuthAllowDeactivated;
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::plc::signing_key_to_did_key;
|
||||
use crate::state::AppState;
|
||||
use crate::types::Handle;
|
||||
@@ -518,31 +518,25 @@ pub struct AtprotoPds {
|
||||
|
||||
pub async fn get_recommended_did_credentials(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
let handle = match state.user_repo.get_handle_by_did(&auth_user.did).await {
|
||||
Ok(Some(h)) => h,
|
||||
Ok(None) => return ApiError::InternalError(None).into_response(),
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let key_bytes = match auth_user.key_bytes {
|
||||
Some(kb) => kb,
|
||||
None => {
|
||||
return ApiError::AuthenticationFailed(Some(
|
||||
"OAuth tokens cannot get DID credentials".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let handle = state
|
||||
.user_repo
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(None))?
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
|
||||
let key_bytes = auth.key_bytes.clone().ok_or_else(|| {
|
||||
ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into()))
|
||||
})?;
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) {
|
||||
Ok(k) => k,
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)
|
||||
.map_err(|_| ApiError::InternalError(None))?;
|
||||
let did_key = signing_key_to_did_key(&signing_key);
|
||||
let rotation_keys = if auth_user.did.starts_with("did:web:") {
|
||||
let rotation_keys = if auth.did.starts_with("did:web:") {
|
||||
vec![]
|
||||
} else {
|
||||
let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") {
|
||||
@@ -556,7 +550,7 @@ pub async fn get_recommended_did_credentials(
|
||||
};
|
||||
vec![server_rotation_key]
|
||||
};
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(GetRecommendedDidCredentialsOutput {
|
||||
rotation_keys,
|
||||
@@ -570,7 +564,7 @@ pub async fn get_recommended_did_credentials(
|
||||
},
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -580,68 +574,70 @@ pub struct UpdateHandleInput {
|
||||
|
||||
pub async fn update_handle(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<UpdateHandleInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Handle,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
let did = auth_user.did;
|
||||
let did = auth.did.clone();
|
||||
if !state
|
||||
.check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did)
|
||||
.await
|
||||
{
|
||||
return ApiError::RateLimitExceeded(Some(
|
||||
return Err(ApiError::RateLimitExceeded(Some(
|
||||
"Too many handle updates. Try again later.".into(),
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
if !state
|
||||
.check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did)
|
||||
.await
|
||||
{
|
||||
return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into()))
|
||||
.into_response();
|
||||
return Err(ApiError::RateLimitExceeded(Some(
|
||||
"Daily handle update limit exceeded.".into(),
|
||||
)));
|
||||
}
|
||||
let user_row = match state.user_repo.get_id_and_handle_by_did(&did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return ApiError::InternalError(None).into_response(),
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let user_row = state
|
||||
.user_repo
|
||||
.get_id_and_handle_by_did(&did)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(None))?
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
let user_id = user_row.id;
|
||||
let current_handle = user_row.handle;
|
||||
let new_handle = input.handle.trim().to_ascii_lowercase();
|
||||
if new_handle.is_empty() {
|
||||
return ApiError::InvalidRequest("handle is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("handle is required".into()));
|
||||
}
|
||||
if !new_handle
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
|
||||
{
|
||||
return ApiError::InvalidHandle(Some("Handle contains invalid characters".into()))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidHandle(Some(
|
||||
"Handle contains invalid characters".into(),
|
||||
)));
|
||||
}
|
||||
if new_handle.split('.').any(|segment| segment.is_empty()) {
|
||||
return ApiError::InvalidHandle(Some("Handle contains empty segment".into()))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidHandle(Some(
|
||||
"Handle contains empty segment".into(),
|
||||
)));
|
||||
}
|
||||
if new_handle
|
||||
.split('.')
|
||||
.any(|segment| segment.starts_with('-') || segment.ends_with('-'))
|
||||
{
|
||||
return ApiError::InvalidHandle(Some(
|
||||
return Err(ApiError::InvalidHandle(Some(
|
||||
"Handle segment cannot start or end with hyphen".into(),
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
if crate::moderation::has_explicit_slur(&new_handle) {
|
||||
return ApiError::InvalidHandle(Some("Inappropriate language in handle".into()))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidHandle(Some(
|
||||
"Inappropriate language in handle".into(),
|
||||
)));
|
||||
}
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname);
|
||||
@@ -667,19 +663,18 @@ pub async fn update_handle(
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
}
|
||||
return EmptyResponse::ok().into_response();
|
||||
return Ok(EmptyResponse::ok().into_response());
|
||||
}
|
||||
if short_part.contains('.') {
|
||||
return ApiError::InvalidHandle(Some(
|
||||
return Err(ApiError::InvalidHandle(Some(
|
||||
"Nested subdomains are not allowed. Use a simple handle without dots.".into(),
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
if short_part.len() < 3 {
|
||||
return ApiError::InvalidHandle(Some("Handle too short".into())).into_response();
|
||||
return Err(ApiError::InvalidHandle(Some("Handle too short".into())));
|
||||
}
|
||||
if short_part.len() > 18 {
|
||||
return ApiError::InvalidHandle(Some("Handle too long".into())).into_response();
|
||||
return Err(ApiError::InvalidHandle(Some("Handle too long".into())));
|
||||
}
|
||||
full_handle
|
||||
} else {
|
||||
@@ -691,74 +686,65 @@ pub async fn update_handle(
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
}
|
||||
return EmptyResponse::ok().into_response();
|
||||
return Ok(EmptyResponse::ok().into_response());
|
||||
}
|
||||
match crate::handle::verify_handle_ownership(&new_handle, &did).await {
|
||||
Ok(()) => {}
|
||||
Err(crate::handle::HandleResolutionError::NotFound) => {
|
||||
return ApiError::HandleNotAvailable(None).into_response();
|
||||
return Err(ApiError::HandleNotAvailable(None));
|
||||
}
|
||||
Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
|
||||
return ApiError::HandleNotAvailable(Some(format!(
|
||||
return Err(ApiError::HandleNotAvailable(Some(format!(
|
||||
"Handle points to different DID. Expected {}, got {}",
|
||||
expected, actual
|
||||
)))
|
||||
.into_response();
|
||||
))));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Handle verification failed: {}", e);
|
||||
return ApiError::HandleNotAvailable(Some(format!(
|
||||
return Err(ApiError::HandleNotAvailable(Some(format!(
|
||||
"Handle verification failed: {}",
|
||||
e
|
||||
)))
|
||||
.into_response();
|
||||
))));
|
||||
}
|
||||
}
|
||||
new_handle.clone()
|
||||
};
|
||||
let handle_typed: Handle = match handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
|
||||
}
|
||||
};
|
||||
let handle_exists = match state
|
||||
let handle_typed: Handle = handle
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?;
|
||||
let handle_exists = state
|
||||
.user_repo
|
||||
.check_handle_exists(&handle_typed, user_id)
|
||||
.await
|
||||
{
|
||||
Ok(exists) => exists,
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
.map_err(|_| ApiError::InternalError(None))?;
|
||||
if handle_exists {
|
||||
return ApiError::HandleTaken.into_response();
|
||||
return Err(ApiError::HandleTaken);
|
||||
}
|
||||
let result = state.user_repo.update_handle(user_id, &handle_typed).await;
|
||||
match result {
|
||||
Ok(_) => {
|
||||
if !current_handle.is_empty() {
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&format!("handle:{}", current_handle))
|
||||
.await;
|
||||
}
|
||||
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed))
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
}
|
||||
if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await {
|
||||
warn!("Failed to update PLC handle: {}", e);
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
state
|
||||
.user_repo
|
||||
.update_handle(user_id, &handle_typed)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error updating handle: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if !current_handle.is_empty() {
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&format!("handle:{}", current_handle))
|
||||
.await;
|
||||
}
|
||||
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
}
|
||||
if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await {
|
||||
warn!("Failed to update PLC handle: {}", e);
|
||||
}
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
pub async fn update_plc_handle(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuthAllowDeactivated;
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::State,
|
||||
@@ -15,35 +15,37 @@ fn generate_plc_token() -> String {
|
||||
|
||||
pub async fn request_plc_operation_signature(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Wildcard,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
let user_id = match state.user_repo.get_id_by_did(&auth_user.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await;
|
||||
let plc_token = generate_plc_token();
|
||||
let expires_at = Utc::now() + Duration::minutes(10);
|
||||
if let Err(e) = state
|
||||
state
|
||||
.infra_repo
|
||||
.insert_plc_token(user_id, &plc_token, expires_at)
|
||||
.await
|
||||
{
|
||||
error!("Failed to create PLC token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to create PLC token: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation(
|
||||
state.user_repo.as_ref(),
|
||||
@@ -56,9 +58,6 @@ pub async fn request_plc_operation_signature(
|
||||
{
|
||||
warn!("Failed to enqueue PLC operation notification: {:?}", e);
|
||||
}
|
||||
info!(
|
||||
"PLC operation signature requested for user {}",
|
||||
auth_user.did
|
||||
);
|
||||
EmptyResponse::ok().into_response()
|
||||
info!("PLC operation signature requested for user {}", auth.did);
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::auth::BearerAuthAllowDeactivated;
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::circuit_breaker::with_circuit_breaker;
|
||||
use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation};
|
||||
use crate::state::AppState;
|
||||
@@ -40,93 +40,81 @@ pub struct SignPlcOperationOutput {
|
||||
|
||||
pub async fn sign_plc_operation(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<SignPlcOperationInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Wildcard,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
let did = &auth_user.did;
|
||||
let did = &auth.did;
|
||||
if did.starts_with("did:web:") {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"PLC operations are only valid for did:plc identities".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
let token = match &input.token {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return ApiError::InvalidRequest(
|
||||
"Email confirmation token required to sign PLC operations".into(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let user_id = match state.user_repo.get_id_by_did(did).await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let token = input.token.as_ref().ok_or_else(|| {
|
||||
ApiError::InvalidRequest("Email confirmation token required to sign PLC operations".into())
|
||||
})?;
|
||||
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let token_expiry = match state.infra_repo.get_plc_token_expiry(user_id, token).await {
|
||||
Ok(Some(expiry)) => expiry,
|
||||
Ok(None) => {
|
||||
return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let token_expiry = state
|
||||
.infra_repo
|
||||
.get_plc_token_expiry(user_id, token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?;
|
||||
|
||||
if Utc::now() > token_expiry {
|
||||
let _ = state.infra_repo.delete_plc_token(user_id, token).await;
|
||||
return ApiError::ExpiredToken(Some("Token has expired".into())).into_response();
|
||||
return Err(ApiError::ExpiredToken(Some("Token has expired".into())));
|
||||
}
|
||||
let key_row = match state.user_repo.get_user_key_by_id(user_id).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::InternalError(Some("User signing key not found".into()))
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let key_row = state
|
||||
.user_repo
|
||||
.get_user_key_by_id(user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
{
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?;
|
||||
|
||||
let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let signing_key = match SigningKey::from_slice(&key_bytes) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to create signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| {
|
||||
error!("Failed to create signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let did_clone = did.clone();
|
||||
let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
||||
let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
||||
plc_client.get_last_op(&did_clone).await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(op) => op,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
};
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if last_op.is_tombstone() {
|
||||
return ApiError::from(PlcError::Tombstoned).into_response();
|
||||
return Err(ApiError::from(PlcError::Tombstoned));
|
||||
}
|
||||
let services = input.services.map(|s| {
|
||||
s.into_iter()
|
||||
@@ -141,36 +129,33 @@ pub async fn sign_plc_operation(
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let unsigned_op = match create_update_op(
|
||||
let unsigned_op = create_update_op(
|
||||
&last_op,
|
||||
input.rotation_keys,
|
||||
input.verification_methods,
|
||||
input.also_known_as,
|
||||
services,
|
||||
) {
|
||||
Ok(op) => op,
|
||||
Err(PlcError::Tombstoned) => {
|
||||
return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()),
|
||||
_ => {
|
||||
error!("Failed to create PLC operation: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
ApiError::InternalError(None)
|
||||
}
|
||||
};
|
||||
let signed_op = match sign_operation(&unsigned_op, &signing_key) {
|
||||
Ok(op) => op,
|
||||
Err(e) => {
|
||||
error!("Failed to sign PLC operation: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
})?;
|
||||
|
||||
let signed_op = sign_operation(&unsigned_op, &signing_key).map_err(|e| {
|
||||
error!("Failed to sign PLC operation: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let _ = state.infra_repo.delete_plc_token(user_id, token).await;
|
||||
info!("Signed PLC operation for user {}", did);
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(SignPlcOperationOutput {
|
||||
operation: signed_op,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::{ApiError, EmptyResponse};
|
||||
use crate::auth::BearerAuthAllowDeactivated;
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::circuit_breaker::with_circuit_breaker;
|
||||
use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation};
|
||||
use crate::state::AppState;
|
||||
@@ -20,64 +20,59 @@ pub struct SubmitPlcOperationInput {
|
||||
|
||||
pub async fn submit_plc_operation(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<SubmitPlcOperationInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Wildcard,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
let did = &auth_user.did;
|
||||
let did = &auth.did;
|
||||
if did.starts_with("did:web:") {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"PLC operations are only valid for did:plc identities".into(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = validate_plc_operation(&input.operation) {
|
||||
return ApiError::InvalidRequest(format!("Invalid operation: {}", e)).into_response();
|
||||
));
|
||||
}
|
||||
validate_plc_operation(&input.operation)
|
||||
.map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?;
|
||||
|
||||
let op = &input.operation;
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let public_url = format!("https://{}", hostname);
|
||||
let user = match state.user_repo.get_id_and_handle_by_did(did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_id_and_handle_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_row = match state.user_repo.get_user_key_by_id(user.id).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::InternalError(Some("User signing key not found".into()))
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let key_row = state
|
||||
.user_repo
|
||||
.get_user_key_by_id(user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
{
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?;
|
||||
|
||||
let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let signing_key = match SigningKey::from_slice(&key_bytes) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to create signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| {
|
||||
error!("Failed to create signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let user_did_key = signing_key_to_did_key(&signing_key);
|
||||
let server_rotation_key =
|
||||
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
|
||||
@@ -86,10 +81,9 @@ pub async fn submit_plc_operation(
|
||||
.iter()
|
||||
.any(|k| k.as_str() == Some(&server_rotation_key));
|
||||
if !has_server_key {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Rotation keys do not include server's rotation key".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(services) = op.get("services").and_then(|v| v.as_object())
|
||||
@@ -98,20 +92,23 @@ pub async fn submit_plc_operation(
|
||||
let service_type = pds.get("type").and_then(|v| v.as_str());
|
||||
let endpoint = pds.get("endpoint").and_then(|v| v.as_str());
|
||||
if service_type != Some("AtprotoPersonalDataServer") {
|
||||
return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Incorrect type on atproto_pds service".into(),
|
||||
));
|
||||
}
|
||||
if endpoint != Some(&public_url) {
|
||||
return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Incorrect endpoint on atproto_pds service".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object())
|
||||
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str())
|
||||
&& atproto_key != user_did_key
|
||||
{
|
||||
return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Incorrect signing key in verificationMethods".into(),
|
||||
));
|
||||
}
|
||||
if let Some(also_known_as) = (!user.handle.is_empty())
|
||||
.then(|| op.get("alsoKnownAs").and_then(|v| v.as_array()))
|
||||
@@ -120,22 +117,22 @@ pub async fn submit_plc_operation(
|
||||
let expected_handle = format!("at://{}", user.handle);
|
||||
let first_aka = also_known_as.first().and_then(|v| v.as_str());
|
||||
if first_aka != Some(&expected_handle) {
|
||||
return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Incorrect handle in alsoKnownAs".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let operation_clone = input.operation.clone();
|
||||
let did_clone = did.clone();
|
||||
if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
||||
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
||||
plc_client
|
||||
.send_operation(&did_clone, &operation_clone)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
return ApiError::from(e).into_response();
|
||||
}
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
match state
|
||||
.repo_repo
|
||||
.insert_identity_event(did, Some(&user.handle))
|
||||
@@ -157,5 +154,5 @@ pub async fn submit_plc_operation(
|
||||
warn!(did = %did, "Failed to refresh DID cache after PLC update");
|
||||
}
|
||||
info!(did = %did, "PLC operation submitted successfully");
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::api::proxy_client::{is_ssrf_safe, proxy_client};
|
||||
use crate::auth::extractor::BearerAuthAllowTakendown;
|
||||
use crate::auth::{AnyUser, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -42,18 +42,16 @@ fn get_report_service_config() -> Option<(String, String)> {
|
||||
|
||||
pub async fn create_report(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowTakendown,
|
||||
auth: Auth<AnyUser>,
|
||||
Json(input): Json<CreateReportInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
let did = &auth_user.did;
|
||||
let did = &auth.did;
|
||||
|
||||
if let Some((service_url, service_did)) = get_report_service_config() {
|
||||
return proxy_to_report_service(&state, &auth_user, &service_url, &service_did, &input)
|
||||
.await;
|
||||
return proxy_to_report_service(&state, &auth, &service_url, &service_did, &input).await;
|
||||
}
|
||||
|
||||
create_report_locally(&state, did, auth_user.is_takendown(), input).await
|
||||
create_report_locally(&state, did, auth.status.is_takendown(), input).await
|
||||
}
|
||||
|
||||
async fn proxy_to_report_service(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -23,16 +23,17 @@ pub struct NotificationPrefsResponse {
|
||||
pub signal_verified: bool,
|
||||
}
|
||||
|
||||
pub async fn get_notification_prefs(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let user = auth.0;
|
||||
let prefs = match state.user_repo.get_notification_prefs(&user.did).await {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
|
||||
}
|
||||
};
|
||||
Json(NotificationPrefsResponse {
|
||||
pub async fn get_notification_prefs(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let prefs = state
|
||||
.user_repo
|
||||
.get_notification_prefs(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
Ok(Json(NotificationPrefsResponse {
|
||||
preferred_channel: prefs.preferred_channel,
|
||||
email: prefs.email,
|
||||
discord_id: prefs.discord_id,
|
||||
@@ -42,7 +43,7 @@ pub async fn get_notification_prefs(State(state): State<AppState>, auth: BearerA
|
||||
signal_number: prefs.signal_number,
|
||||
signal_verified: prefs.signal_verified,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -62,23 +63,22 @@ pub struct GetNotificationHistoryResponse {
|
||||
pub notifications: Vec<NotificationHistoryEntry>,
|
||||
}
|
||||
|
||||
pub async fn get_notification_history(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let user = auth.0;
|
||||
pub async fn get_notification_history(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let rows = match state.infra_repo.get_notification_history(user_id, 50).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
|
||||
}
|
||||
};
|
||||
let rows = state
|
||||
.infra_repo
|
||||
.get_notification_history(user_id, 50)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
|
||||
let sensitive_types = [
|
||||
"email_verification",
|
||||
@@ -111,7 +111,7 @@ pub async fn get_notification_history(State(state): State<AppState>, auth: Beare
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(GetNotificationHistoryResponse { notifications }).into_response()
|
||||
Ok(Json(GetNotificationHistoryResponse { notifications }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -184,18 +184,15 @@ pub async fn request_channel_verification(
|
||||
|
||||
pub async fn update_notification_prefs(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateNotificationPrefsInput>,
|
||||
) -> Response {
|
||||
let user = auth.0;
|
||||
|
||||
let user_row = match state.user_repo.get_id_handle_email_by_did(&user.did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
|
||||
}
|
||||
};
|
||||
) -> Result<Response, ApiError> {
|
||||
let user_row = state
|
||||
.user_repo
|
||||
.get_id_handle_email_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let user_id = user_row.id;
|
||||
let handle = user_row.handle;
|
||||
@@ -206,119 +203,106 @@ pub async fn update_notification_prefs(
|
||||
if let Some(ref channel) = input.preferred_channel {
|
||||
let valid_channels = ["email", "discord", "telegram", "signal"];
|
||||
if !valid_channels.contains(&channel.as_str()) {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid channel. Must be one of: email, discord, telegram, signal".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.update_preferred_comms_channel(&user.did, channel)
|
||||
.update_preferred_comms_channel(&auth.did, channel)
|
||||
.await
|
||||
{
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response();
|
||||
}
|
||||
info!(did = %user.did, channel = %channel, "Updated preferred notification channel");
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, channel = %channel, "Updated preferred notification channel");
|
||||
}
|
||||
|
||||
if let Some(ref new_email) = input.email {
|
||||
let email_clean = new_email.trim().to_lowercase();
|
||||
if email_clean.is_empty() {
|
||||
return ApiError::InvalidRequest("Email cannot be empty".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("Email cannot be empty".into()));
|
||||
}
|
||||
|
||||
if !crate::api::validation::is_valid_email(&email_clean) {
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
|
||||
if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) {
|
||||
info!(did = %user.did, "Email unchanged, skipping");
|
||||
} else {
|
||||
if let Err(e) = request_channel_verification(
|
||||
if current_email.as_ref().map(|e| e.to_lowercase()) != Some(email_clean.clone()) {
|
||||
request_channel_verification(
|
||||
&state,
|
||||
user_id,
|
||||
&user.did,
|
||||
&auth.did,
|
||||
"email",
|
||||
&email_clean,
|
||||
Some(&handle),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::InternalError(Some(e)).into_response();
|
||||
}
|
||||
.map_err(|e| ApiError::InternalError(Some(e)))?;
|
||||
verification_required.push("email".to_string());
|
||||
info!(did = %user.did, "Requested email verification");
|
||||
info!(did = %auth.did, "Requested email verification");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref discord_id) = input.discord_id {
|
||||
if discord_id.is_empty() {
|
||||
if let Err(e) = state.user_repo.clear_discord(user_id).await {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e)))
|
||||
.into_response();
|
||||
}
|
||||
info!(did = %user.did, "Cleared Discord ID");
|
||||
state
|
||||
.user_repo
|
||||
.clear_discord(user_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Discord ID");
|
||||
} else {
|
||||
if let Err(e) = request_channel_verification(
|
||||
&state, user_id, &user.did, "discord", discord_id, None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::InternalError(Some(e)).into_response();
|
||||
}
|
||||
request_channel_verification(&state, user_id, &auth.did, "discord", discord_id, None)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(e)))?;
|
||||
verification_required.push("discord".to_string());
|
||||
info!(did = %user.did, "Requested Discord verification");
|
||||
info!(did = %auth.did, "Requested Discord verification");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref telegram) = input.telegram_username {
|
||||
let telegram_clean = telegram.trim_start_matches('@');
|
||||
if telegram_clean.is_empty() {
|
||||
if let Err(e) = state.user_repo.clear_telegram(user_id).await {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e)))
|
||||
.into_response();
|
||||
}
|
||||
info!(did = %user.did, "Cleared Telegram username");
|
||||
state
|
||||
.user_repo
|
||||
.clear_telegram(user_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Telegram username");
|
||||
} else {
|
||||
if let Err(e) = request_channel_verification(
|
||||
request_channel_verification(
|
||||
&state,
|
||||
user_id,
|
||||
&user.did,
|
||||
&auth.did,
|
||||
"telegram",
|
||||
telegram_clean,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::InternalError(Some(e)).into_response();
|
||||
}
|
||||
.map_err(|e| ApiError::InternalError(Some(e)))?;
|
||||
verification_required.push("telegram".to_string());
|
||||
info!(did = %user.did, "Requested Telegram verification");
|
||||
info!(did = %auth.did, "Requested Telegram verification");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref signal) = input.signal_number {
|
||||
if signal.is_empty() {
|
||||
if let Err(e) = state.user_repo.clear_signal(user_id).await {
|
||||
return ApiError::InternalError(Some(format!("Database error: {}", e)))
|
||||
.into_response();
|
||||
}
|
||||
info!(did = %user.did, "Cleared Signal number");
|
||||
state
|
||||
.user_repo
|
||||
.clear_signal(user_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Signal number");
|
||||
} else {
|
||||
if let Err(e) =
|
||||
request_channel_verification(&state, user_id, &user.did, "signal", signal, None)
|
||||
.await
|
||||
{
|
||||
return ApiError::InternalError(Some(e)).into_response();
|
||||
}
|
||||
request_channel_verification(&state, user_id, &auth.did, "signal", signal, None)
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(e)))?;
|
||||
verification_required.push("signal".to_string());
|
||||
info!(did = %user.did, "Requested Signal verification");
|
||||
info!(did = %auth.did, "Requested Signal verification");
|
||||
}
|
||||
}
|
||||
|
||||
Json(UpdateNotificationPrefsResponse {
|
||||
Ok(Json(UpdateNotificationPrefsResponse {
|
||||
success: true,
|
||||
verification_required,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ async fn proxy_handler(
|
||||
{
|
||||
Ok(auth_user) => {
|
||||
if let Err(e) = crate::auth::scope_check::check_rpc_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.is_oauth(),
|
||||
auth_user.scope.as_deref(),
|
||||
&resolved.did,
|
||||
method,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult};
|
||||
use crate::auth::{Auth, AuthAny, NotTakendown, Permissive};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{CidLink, Did};
|
||||
@@ -44,25 +44,30 @@ fn detect_mime_type(data: &[u8], client_hint: &str) -> String {
|
||||
pub async fn upload_blob(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BlobAuth,
|
||||
auth: AuthAny<Permissive>,
|
||||
body: Body,
|
||||
) -> Response {
|
||||
let (did, controller_did): (Did, Option<Did>) = match auth.0 {
|
||||
BlobAuthResult::Service { did } => (did, None),
|
||||
BlobAuthResult::User(auth_user) => {
|
||||
) -> Result<Response, ApiError> {
|
||||
let (did, controller_did): (Did, Option<Did>) = match &auth {
|
||||
AuthAny::Service(service) => {
|
||||
service.require_lxm("com.atproto.repo.uploadBlob")?;
|
||||
(service.did.clone(), None)
|
||||
}
|
||||
AuthAny::User(user) => {
|
||||
if user.status.is_takendown() {
|
||||
return Err(ApiError::AccountTakedown);
|
||||
}
|
||||
let mime_type_for_check = headers
|
||||
.get("content-type")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("application/octet-stream");
|
||||
if let Err(e) = crate::auth::scope_check::check_blob_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
user.is_oauth(),
|
||||
user.scope.as_deref(),
|
||||
mime_type_for_check,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
let ctrl_did = auth_user.controller_did.clone();
|
||||
(auth_user.did, ctrl_did)
|
||||
(user.did.clone(), user.controller_did.clone())
|
||||
}
|
||||
};
|
||||
|
||||
@@ -72,7 +77,7 @@ pub async fn upload_blob(
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return ApiError::Forbidden.into_response();
|
||||
return Err(ApiError::Forbidden);
|
||||
}
|
||||
|
||||
let client_mime_hint = headers
|
||||
@@ -80,12 +85,13 @@ pub async fn upload_blob(
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("application/octet-stream");
|
||||
|
||||
let user_id = match state.user_repo.get_id_by_did(&did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
|
||||
let temp_key = format!("temp/{}", uuid::Uuid::new_v4());
|
||||
let max_size = get_max_blob_size() as u64;
|
||||
@@ -98,22 +104,22 @@ pub async fn upload_blob(
|
||||
|
||||
info!("Starting streaming blob upload to temp key: {}", temp_key);
|
||||
|
||||
let upload_result = match state.blob_store.put_stream(&temp_key, pinned_stream).await {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let upload_result = state
|
||||
.blob_store
|
||||
.put_stream(&temp_key, pinned_stream)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to stream blob to storage: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to store blob".into())).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(Some("Failed to store blob".into()))
|
||||
})?;
|
||||
|
||||
let size = upload_result.size;
|
||||
if size > max_size {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
return ApiError::InvalidRequest(format!(
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Blob size {} exceeds maximum of {} bytes",
|
||||
size, max_size
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
|
||||
let mime_type = match state.blob_store.get_head(&temp_key, 8192).await {
|
||||
@@ -129,7 +135,7 @@ pub async fn upload_blob(
|
||||
Err(e) => {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
error!("Failed to create multihash for blob: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to hash blob".into())).into_response();
|
||||
return Err(ApiError::InternalError(Some("Failed to hash blob".into())));
|
||||
}
|
||||
};
|
||||
let cid = Cid::new_v1(0x55, multihash);
|
||||
@@ -152,14 +158,14 @@ pub async fn upload_blob(
|
||||
Err(e) => {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
error!("Failed to insert blob record: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await {
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
error!("Failed to copy blob to final location: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to store blob".into())).into_response();
|
||||
return Err(ApiError::InternalError(Some("Failed to store blob".into())));
|
||||
}
|
||||
|
||||
let _ = state.blob_store.delete(&temp_key).await;
|
||||
@@ -183,7 +189,7 @@ pub async fn upload_blob(
|
||||
.await;
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"blob": {
|
||||
"$type": "blob",
|
||||
"ref": {
|
||||
@@ -193,7 +199,7 @@ pub async fn upload_blob(
|
||||
"size": size
|
||||
}
|
||||
}))
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -218,32 +224,31 @@ pub struct ListMissingBlobsOutput {
|
||||
|
||||
pub async fn list_missing_blobs(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
auth: Auth<NotTakendown>,
|
||||
Query(params): Query<ListMissingBlobsParams>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
let did = &auth_user.did;
|
||||
let user = match state.user_repo.get_by_did(did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return ApiError::InternalError(None).into_response(),
|
||||
Err(e) => {
|
||||
) -> Result<Response, ApiError> {
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
|
||||
let limit = params.limit.unwrap_or(500).clamp(1, 1000);
|
||||
let cursor = params.cursor.as_deref();
|
||||
let missing = match state
|
||||
let missing = state
|
||||
.blob_repo
|
||||
.list_missing_blobs(user.id, cursor, limit + 1)
|
||||
.await
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching missing blobs: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let has_more = missing.len() > limit as usize;
|
||||
let blobs: Vec<RecordBlob> = missing
|
||||
.into_iter()
|
||||
@@ -258,12 +263,12 @@ pub async fn list_missing_blobs(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(ListMissingBlobsOutput {
|
||||
cursor: next_cursor,
|
||||
blobs,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::create_signed_commit;
|
||||
use crate::auth::BearerAuthAllowDeactivated;
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::state::AppState;
|
||||
use crate::sync::import::{ImportError, apply_import, parse_car};
|
||||
use crate::sync::verify::CarVerifier;
|
||||
@@ -23,56 +23,57 @@ const DEFAULT_MAX_BLOCKS: usize = 500000;
|
||||
|
||||
pub async fn import_repo(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuthAllowDeactivated,
|
||||
auth: Auth<NotTakendown>,
|
||||
body: Bytes,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS")
|
||||
.map(|v| v != "false" && v != "0")
|
||||
.unwrap_or(true);
|
||||
if !accepting_imports {
|
||||
return ApiError::InvalidRequest("Service is not accepting repo imports".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Service is not accepting repo imports".into(),
|
||||
));
|
||||
}
|
||||
let max_size: usize = std::env::var("MAX_IMPORT_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAX_IMPORT_SIZE);
|
||||
if body.len() > max_size {
|
||||
return ApiError::PayloadTooLarge(format!(
|
||||
return Err(ApiError::PayloadTooLarge(format!(
|
||||
"Import size exceeds limit of {} bytes",
|
||||
max_size
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
let auth_user = auth.0;
|
||||
let did = &auth_user.did;
|
||||
let user = match state.user_repo.get_by_did(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
if user.takedown_ref.is_some() {
|
||||
return ApiError::AccountTakedown.into_response();
|
||||
return Err(ApiError::AccountTakedown);
|
||||
}
|
||||
let user_id = user.id;
|
||||
let (root, blocks) = match parse_car(&body).await {
|
||||
Ok((r, b)) => (r, b),
|
||||
Err(ImportError::InvalidRootCount) => {
|
||||
return ApiError::InvalidRequest("Expected exactly one root in CAR file".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Expected exactly one root in CAR file".into(),
|
||||
));
|
||||
}
|
||||
Err(ImportError::CarParse(msg)) => {
|
||||
return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Failed to parse CAR file: {}",
|
||||
msg
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("CAR parsing error: {:?}", e);
|
||||
return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response();
|
||||
return Err(ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)));
|
||||
}
|
||||
};
|
||||
info!(
|
||||
@@ -82,20 +83,21 @@ pub async fn import_repo(
|
||||
root
|
||||
);
|
||||
let Some(root_block) = blocks.get(&root) else {
|
||||
return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Root block not found in CAR file".into(),
|
||||
));
|
||||
};
|
||||
let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) {
|
||||
Ok(commit) => commit.did().to_string(),
|
||||
Err(e) => {
|
||||
return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response();
|
||||
return Err(ApiError::InvalidRequest(format!("Invalid commit: {}", e)));
|
||||
}
|
||||
};
|
||||
if commit_did != *did {
|
||||
return ApiError::InvalidRepo(format!(
|
||||
return Err(ApiError::InvalidRepo(format!(
|
||||
"CAR file is for DID {} but you are authenticated as {}",
|
||||
commit_did, did
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
@@ -117,20 +119,23 @@ pub async fn import_repo(
|
||||
commit_did,
|
||||
expected_did,
|
||||
}) => {
|
||||
return ApiError::InvalidRepo(format!(
|
||||
return Err(ApiError::InvalidRepo(format!(
|
||||
"CAR file is for DID {} but you are authenticated as {}",
|
||||
commit_did, expected_did
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => {
|
||||
return ApiError::InvalidRequest(format!("MST validation failed: {}", msg))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"MST validation failed: {}",
|
||||
msg
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("CAR structure verification error: {:?}", e);
|
||||
return ApiError::InvalidRequest(format!("CAR verification failed: {}", e))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"CAR verification failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -147,37 +152,40 @@ pub async fn import_repo(
|
||||
commit_did,
|
||||
expected_did,
|
||||
}) => {
|
||||
return ApiError::InvalidRepo(format!(
|
||||
return Err(ApiError::InvalidRepo(format!(
|
||||
"CAR file is for DID {} but you are authenticated as {}",
|
||||
commit_did, expected_did
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::InvalidSignature) => {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"CAR file commit signature verification failed".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => {
|
||||
warn!("DID resolution failed during import verification: {}", msg);
|
||||
return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Failed to verify DID: {}",
|
||||
msg
|
||||
)));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::NoSigningKey) => {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"DID document does not contain a signing key".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => {
|
||||
return ApiError::InvalidRequest(format!("MST validation failed: {}", msg))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"MST validation failed: {}",
|
||||
msg
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("CAR verification error: {:?}", e);
|
||||
return ApiError::InvalidRequest(format!("CAR verification failed: {}", e))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"CAR verification failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,74 +235,65 @@ pub async fn import_repo(
|
||||
}
|
||||
}
|
||||
}
|
||||
let key_row = match state.user_repo.get_user_with_key_by_did(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
error!("No signing key found for user {}", did);
|
||||
return ApiError::InternalError(Some("Signing key not found".into()))
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let key_row = state
|
||||
.user_repo
|
||||
.get_user_with_key_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
error!("No signing key found for user {}", did);
|
||||
ApiError::InternalError(Some("Signing key not found".into()))
|
||||
})?;
|
||||
let key_bytes =
|
||||
match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt signing key: {}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let signing_key = match SigningKey::from_slice(&key_bytes) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Invalid signing key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| {
|
||||
error!("Invalid signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let new_rev = Tid::now(LimitedU32::MIN);
|
||||
let new_rev_str = new_rev.to_string();
|
||||
let (commit_bytes, _sig) = match create_signed_commit(
|
||||
let (commit_bytes, _sig) = create_signed_commit(
|
||||
did,
|
||||
import_result.data_cid,
|
||||
&new_rev_str,
|
||||
None,
|
||||
&signing_key,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Failed to create new commit: {}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let new_root_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
|
||||
Ok(cid) => cid,
|
||||
Err(e) => {
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Failed to create new commit: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let new_root_cid: cid::Cid =
|
||||
state.block_store.put(&commit_bytes).await.map_err(|e| {
|
||||
error!("Failed to store new commit block: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string());
|
||||
if let Err(e) = state
|
||||
state
|
||||
.repo_repo
|
||||
.update_repo_root(user_id, &new_root_cid_link, &new_rev_str)
|
||||
.await
|
||||
{
|
||||
error!("Failed to update repo root: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to update repo root: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect();
|
||||
all_block_cids.push(new_root_cid.to_bytes());
|
||||
if let Err(e) = state
|
||||
state
|
||||
.repo_repo
|
||||
.insert_user_blocks(user_id, &all_block_cids, &new_rev_str)
|
||||
.await
|
||||
{
|
||||
error!("Failed to insert user_blocks: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to insert user_blocks: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let new_root_str = new_root_cid.to_string();
|
||||
info!(
|
||||
"Created new commit for imported repo: cid={}, rev={}",
|
||||
@@ -324,41 +323,40 @@ pub async fn import_repo(
|
||||
);
|
||||
}
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Err(ImportError::SizeLimitExceeded) => {
|
||||
ApiError::PayloadTooLarge(format!("Import exceeds block limit of {}", max_blocks))
|
||||
.into_response()
|
||||
}
|
||||
Err(ImportError::RepoNotFound) => {
|
||||
ApiError::RepoNotFound(Some("Repository not initialized for this account".into()))
|
||||
.into_response()
|
||||
}
|
||||
Err(ImportError::InvalidCbor(msg)) => {
|
||||
ApiError::InvalidRequest(format!("Invalid CBOR data: {}", msg)).into_response()
|
||||
}
|
||||
Err(ImportError::InvalidCommit(msg)) => {
|
||||
ApiError::InvalidRequest(format!("Invalid commit structure: {}", msg)).into_response()
|
||||
}
|
||||
Err(ImportError::BlockNotFound(cid)) => {
|
||||
ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid))
|
||||
.into_response()
|
||||
}
|
||||
Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some(
|
||||
Err(ImportError::SizeLimitExceeded) => Err(ApiError::PayloadTooLarge(format!(
|
||||
"Import exceeds block limit of {}",
|
||||
max_blocks
|
||||
))),
|
||||
Err(ImportError::RepoNotFound) => Err(ApiError::RepoNotFound(Some(
|
||||
"Repository not initialized for this account".into(),
|
||||
))),
|
||||
Err(ImportError::InvalidCbor(msg)) => Err(ApiError::InvalidRequest(format!(
|
||||
"Invalid CBOR data: {}",
|
||||
msg
|
||||
))),
|
||||
Err(ImportError::InvalidCommit(msg)) => Err(ApiError::InvalidRequest(format!(
|
||||
"Invalid commit structure: {}",
|
||||
msg
|
||||
))),
|
||||
Err(ImportError::BlockNotFound(cid)) => Err(ApiError::InvalidRequest(format!(
|
||||
"Referenced block not found in CAR: {}",
|
||||
cid
|
||||
))),
|
||||
Err(ImportError::ConcurrentModification) => Err(ApiError::InvalidSwap(Some(
|
||||
"Repository is being modified by another operation, please retry".into(),
|
||||
))
|
||||
.into_response(),
|
||||
Err(ImportError::VerificationFailed(ve)) => {
|
||||
ApiError::InvalidRequest(format!("CAR verification failed: {}", ve)).into_response()
|
||||
}
|
||||
Err(ImportError::DidMismatch { car_did, auth_did }) => ApiError::InvalidRequest(format!(
|
||||
"CAR is for {} but authenticated as {}",
|
||||
car_did, auth_did
|
||||
))
|
||||
.into_response(),
|
||||
))),
|
||||
Err(ImportError::VerificationFailed(ve)) => Err(ApiError::InvalidRequest(format!(
|
||||
"CAR verification failed: {}",
|
||||
ve
|
||||
))),
|
||||
Err(ImportError::DidMismatch { car_did, auth_did }) => Err(ApiError::InvalidRequest(
|
||||
format!("CAR is for {} but authenticated as {}", car_did, auth_did),
|
||||
)),
|
||||
Err(e) => {
|
||||
error!("Import error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::validation::validate_record_with_status;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids};
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
@@ -262,22 +262,22 @@ pub struct CommitInfo {
|
||||
|
||||
pub async fn apply_writes(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<ApplyWritesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
info!(
|
||||
"apply_writes called: repo={}, writes={}",
|
||||
input.repo,
|
||||
input.writes.len()
|
||||
);
|
||||
let auth_user = auth.0;
|
||||
let did = auth_user.did.clone();
|
||||
let is_oauth = auth_user.is_oauth;
|
||||
let scope = auth_user.scope;
|
||||
let controller_did = auth_user.controller_did.clone();
|
||||
let did = auth.did.clone();
|
||||
let is_oauth = auth.is_oauth();
|
||||
let scope = auth.scope.clone();
|
||||
let controller_did = auth.controller_did.clone();
|
||||
if input.repo.as_str() != did {
|
||||
return ApiError::InvalidRepo("Repo does not match authenticated user".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRepo(
|
||||
"Repo does not match authenticated user".into(),
|
||||
));
|
||||
}
|
||||
if state
|
||||
.user_repo
|
||||
@@ -285,7 +285,7 @@ pub async fn apply_writes(
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return ApiError::AccountMigrated.into_response();
|
||||
return Err(ApiError::AccountMigrated);
|
||||
}
|
||||
let is_verified = state
|
||||
.user_repo
|
||||
@@ -298,14 +298,16 @@ pub async fn apply_writes(
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !is_verified && !is_delegated {
|
||||
return ApiError::AccountNotVerified.into_response();
|
||||
return Err(ApiError::AccountNotVerified);
|
||||
}
|
||||
if input.writes.is_empty() {
|
||||
return ApiError::InvalidRequest("writes array is empty".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("writes array is empty".into()));
|
||||
}
|
||||
if input.writes.len() > MAX_BATCH_WRITES {
|
||||
return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES))
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Too many writes (max {})",
|
||||
MAX_BATCH_WRITES
|
||||
)));
|
||||
}
|
||||
|
||||
let has_custom_scope = scope
|
||||
@@ -374,38 +376,40 @@ pub async fn apply_writes(
|
||||
})
|
||||
.next()
|
||||
{
|
||||
return err;
|
||||
return Ok(err);
|
||||
}
|
||||
}
|
||||
|
||||
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => return ApiError::InternalError(Some("User not found".into())).into_response(),
|
||||
};
|
||||
let root_cid_str = match state.repo_repo.get_repo_root_cid_by_user_id(user_id).await {
|
||||
Ok(Some(cid_str)) => cid_str,
|
||||
_ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(),
|
||||
};
|
||||
let current_root_cid = match Cid::from_str(&root_cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response();
|
||||
}
|
||||
};
|
||||
let user_id: uuid::Uuid = state
|
||||
.user_repo
|
||||
.get_id_by_did(&did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or_else(|| ApiError::InternalError(Some("User not found".into())))?;
|
||||
let root_cid_str = state
|
||||
.repo_repo
|
||||
.get_repo_root_cid_by_user_id(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())))?;
|
||||
let current_root_cid = Cid::from_str(&root_cid_str)
|
||||
.map_err(|_| ApiError::InternalError(Some("Invalid repo root CID".into())))?;
|
||||
if let Some(swap_commit) = &input.swap_commit
|
||||
&& Cid::from_str(swap_commit).ok() != Some(current_root_cid)
|
||||
{
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Err(ApiError::InvalidSwap(Some("Repo has been modified".into())));
|
||||
}
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
};
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
_ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
};
|
||||
let commit_bytes = tracking_store
|
||||
.get(¤t_root_cid)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or_else(|| ApiError::InternalError(Some("Commit block not found".into())))?;
|
||||
let commit = Commit::from_cbor(&commit_bytes)
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to parse commit".into())))?;
|
||||
let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let WriteAccumulator {
|
||||
@@ -424,34 +428,27 @@ pub async fn apply_writes(
|
||||
.await
|
||||
{
|
||||
Ok(acc) => acc,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let new_mst_root = match mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
|
||||
}
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
let new_mst_root = mst
|
||||
.persist()
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(Some("Failed to persist MST".into())))?;
|
||||
let (new_mst_blocks, old_mst_blocks) = {
|
||||
let mut new_blocks = std::collections::BTreeMap::new();
|
||||
let mut old_blocks = std::collections::BTreeMap::new();
|
||||
for key in &modified_keys {
|
||||
if mst.blocks_for_path(key, &mut new_blocks).await.is_err() {
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to get new MST blocks for path".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
if original_mst
|
||||
mst.blocks_for_path(key, &mut new_blocks)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
})?;
|
||||
original_mst
|
||||
.blocks_for_path(key, &mut old_blocks)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to get old MST blocks for path".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
.map_err(|_| {
|
||||
ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
|
||||
})?;
|
||||
}
|
||||
(new_blocks, old_blocks)
|
||||
};
|
||||
@@ -503,12 +500,13 @@ pub async fn apply_writes(
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) if e.contains("ConcurrentModification") => {
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Err(ApiError::InvalidSwap(Some("Repo has been modified".into())));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Commit failed: {}", e);
|
||||
return ApiError::InternalError(Some("Failed to commit changes".into()))
|
||||
.into_response();
|
||||
return Err(ApiError::InternalError(Some(
|
||||
"Failed to commit changes".into(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -557,7 +555,7 @@ pub async fn apply_writes(
|
||||
.await;
|
||||
}
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(ApplyWritesOutput {
|
||||
commit: CommitInfo {
|
||||
@@ -567,5 +565,5 @@ pub async fn apply_writes(
|
||||
results,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log};
|
||||
use crate::api::repo::record::write::{CommitInfo, prepare_repo_write};
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
@@ -40,41 +40,49 @@ pub struct DeleteRecordOutput {
|
||||
|
||||
pub async fn delete_record(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<DeleteRecordInput>,
|
||||
) -> Response {
|
||||
let auth = match prepare_repo_write(&state, auth.0, &input.repo).await {
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await {
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
Err(err_res) => return Ok(err_res),
|
||||
};
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_repo_scope(
|
||||
auth.is_oauth,
|
||||
auth.scope.as_deref(),
|
||||
repo_auth.is_oauth,
|
||||
repo_auth.scope.as_deref(),
|
||||
crate::oauth::RepoAction::Delete,
|
||||
&input.collection,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let did = auth.did;
|
||||
let user_id = auth.user_id;
|
||||
let current_root_cid = auth.current_root_cid;
|
||||
let controller_did = auth.controller_did;
|
||||
let did = repo_auth.did;
|
||||
let user_id = repo_auth.user_id;
|
||||
let current_root_cid = repo_auth.current_root_cid;
|
||||
let controller_did = repo_auth.controller_did;
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit
|
||||
&& Cid::from_str(swap_commit).ok() != Some(current_root_cid)
|
||||
{
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response());
|
||||
}
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
_ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let key = format!("{}/{}", input.collection, input.rkey);
|
||||
@@ -82,29 +90,34 @@ pub async fn delete_record(
|
||||
let expected_cid = Cid::from_str(swap_record_str).ok();
|
||||
let actual_cid = mst.get(&key).await.ok().flatten();
|
||||
if expected_cid != actual_cid {
|
||||
return ApiError::InvalidSwap(Some(
|
||||
return Ok(ApiError::InvalidSwap(Some(
|
||||
"Record has been modified or does not exist".into(),
|
||||
))
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
if prev_record_cid.is_none() {
|
||||
return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response();
|
||||
return Ok((StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response());
|
||||
}
|
||||
let new_mst = match mst.delete(&key).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to delete from MST: {:?}", e);
|
||||
return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e)))
|
||||
.into_response();
|
||||
return Ok(ApiError::InternalError(Some(format!(
|
||||
"Failed to delete from MST: {:?}",
|
||||
e
|
||||
)))
|
||||
.into_response());
|
||||
}
|
||||
};
|
||||
let new_mst_root = match new_mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to persist MST: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to persist MST".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let collection_for_audit = input.collection.to_string();
|
||||
@@ -121,16 +134,20 @@ pub async fn delete_record(
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
if mst
|
||||
.blocks_for_path(&key, &mut old_mst_blocks)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
let mut relevant_blocks = new_mst_blocks.clone();
|
||||
relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone())));
|
||||
@@ -169,9 +186,9 @@ pub async fn delete_record(
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) if e.contains("ConcurrentModification") => {
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response());
|
||||
}
|
||||
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
|
||||
Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()),
|
||||
};
|
||||
|
||||
if let Some(ref controller) = controller_did {
|
||||
@@ -202,7 +219,7 @@ pub async fn delete_record(
|
||||
error!("Failed to remove backlinks for {}: {}", deleted_uri, e);
|
||||
}
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(DeleteRecordOutput {
|
||||
commit: Some(CommitInfo {
|
||||
@@ -211,7 +228,7 @@ pub async fn delete_record(
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
use crate::types::Did;
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{
|
||||
CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids,
|
||||
};
|
||||
use crate::auth::{AuthenticatedUser, BearerAuth};
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
@@ -34,7 +34,7 @@ pub struct RepoWriteAuth {
|
||||
|
||||
pub async fn prepare_repo_write(
|
||||
state: &AppState,
|
||||
auth_user: AuthenticatedUser,
|
||||
auth_user: &crate::auth::AuthenticatedUser,
|
||||
repo: &AtIdentifier,
|
||||
) -> Result<RepoWriteAuth, Response> {
|
||||
if repo.as_str() != auth_user.did.as_str() {
|
||||
@@ -90,8 +90,8 @@ pub async fn prepare_repo_write(
|
||||
did: auth_user.did.clone(),
|
||||
user_id,
|
||||
current_root_cid,
|
||||
is_oauth: auth_user.is_oauth,
|
||||
scope: auth_user.scope,
|
||||
is_oauth: auth_user.is_oauth(),
|
||||
scope: auth_user.scope.clone(),
|
||||
controller_did: auth_user.controller_did.clone(),
|
||||
})
|
||||
}
|
||||
@@ -124,32 +124,32 @@ pub struct CreateRecordOutput {
|
||||
}
|
||||
pub async fn create_record(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<CreateRecordInput>,
|
||||
) -> Response {
|
||||
let auth = match prepare_repo_write(&state, auth.0, &input.repo).await {
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await {
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
Err(err_res) => return Ok(err_res),
|
||||
};
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_repo_scope(
|
||||
auth.is_oauth,
|
||||
auth.scope.as_deref(),
|
||||
repo_auth.is_oauth,
|
||||
repo_auth.scope.as_deref(),
|
||||
crate::oauth::RepoAction::Create,
|
||||
&input.collection,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let did = auth.did;
|
||||
let user_id = auth.user_id;
|
||||
let current_root_cid = auth.current_root_cid;
|
||||
let controller_did = auth.controller_did;
|
||||
let did = repo_auth.did;
|
||||
let user_id = repo_auth.user_id;
|
||||
let current_root_cid = repo_auth.current_root_cid;
|
||||
let controller_did = repo_auth.controller_did;
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit
|
||||
&& Cid::from_str(swap_commit).ok() != Some(current_root_cid)
|
||||
{
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response());
|
||||
}
|
||||
|
||||
let validation_status = if input.validate == Some(false) {
|
||||
@@ -163,7 +163,7 @@ pub async fn create_record(
|
||||
require_lexicon,
|
||||
) {
|
||||
Ok(status) => Some(status),
|
||||
Err(err_response) => return *err_response,
|
||||
Err(err_response) => return Ok(*err_response),
|
||||
}
|
||||
};
|
||||
let rkey = input.rkey.unwrap_or_else(Rkey::generate);
|
||||
@@ -171,11 +171,19 @@ pub async fn create_record(
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
_ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let initial_mst_root = commit.data;
|
||||
@@ -197,7 +205,7 @@ pub async fn create_record(
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to check backlink conflicts: {}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -250,13 +258,14 @@ pub async fn create_record(
|
||||
let record_ipld = crate::util::json_to_ipld(&input.record);
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() {
|
||||
return ApiError::InvalidRecord("Failed to serialize record".into()).into_response();
|
||||
return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response());
|
||||
}
|
||||
let record_cid = match tracking_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
_ => {
|
||||
return ApiError::InternalError(Some("Failed to save record block".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to save record block".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let key = format!("{}/{}", input.collection, rkey);
|
||||
@@ -271,11 +280,17 @@ pub async fn create_record(
|
||||
|
||||
let new_mst = match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
_ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(ApiError::InternalError(Some("Failed to add to MST".into())).into_response());
|
||||
}
|
||||
};
|
||||
let new_mst_root = match new_mst.persist().await {
|
||||
Ok(c) => c,
|
||||
_ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to persist MST".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ops.push(RecordOp::Create {
|
||||
@@ -290,8 +305,10 @@ pub async fn create_record(
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut relevant_blocks = new_mst_blocks.clone();
|
||||
@@ -333,9 +350,9 @@ pub async fn create_record(
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) if e.contains("ConcurrentModification") => {
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response());
|
||||
}
|
||||
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
|
||||
Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()),
|
||||
};
|
||||
|
||||
for conflict_uri in conflict_uris_to_cleanup {
|
||||
@@ -375,7 +392,7 @@ pub async fn create_record(
|
||||
error!("Failed to add backlinks for {}: {}", created_uri, e);
|
||||
}
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(CreateRecordOutput {
|
||||
uri: created_uri,
|
||||
@@ -387,7 +404,7 @@ pub async fn create_record(
|
||||
validation_status: validation_status.map(|s| s.to_string()),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
@@ -414,49 +431,57 @@ pub struct PutRecordOutput {
|
||||
}
|
||||
pub async fn put_record(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<PutRecordInput>,
|
||||
) -> Response {
|
||||
let auth = match prepare_repo_write(&state, auth.0, &input.repo).await {
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await {
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
Err(err_res) => return Ok(err_res),
|
||||
};
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_repo_scope(
|
||||
auth.is_oauth,
|
||||
auth.scope.as_deref(),
|
||||
repo_auth.is_oauth,
|
||||
repo_auth.scope.as_deref(),
|
||||
crate::oauth::RepoAction::Create,
|
||||
&input.collection,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
if let Err(e) = crate::auth::scope_check::check_repo_scope(
|
||||
auth.is_oauth,
|
||||
auth.scope.as_deref(),
|
||||
repo_auth.is_oauth,
|
||||
repo_auth.scope.as_deref(),
|
||||
crate::oauth::RepoAction::Update,
|
||||
&input.collection,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let did = auth.did;
|
||||
let user_id = auth.user_id;
|
||||
let current_root_cid = auth.current_root_cid;
|
||||
let controller_did = auth.controller_did;
|
||||
let did = repo_auth.did;
|
||||
let user_id = repo_auth.user_id;
|
||||
let current_root_cid = repo_auth.current_root_cid;
|
||||
let controller_did = repo_auth.controller_did;
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit
|
||||
&& Cid::from_str(swap_commit).ok() != Some(current_root_cid)
|
||||
{
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response());
|
||||
}
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Commit block not found".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
_ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
_ => {
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let key = format!("{}/{}", input.collection, input.rkey);
|
||||
@@ -471,34 +496,35 @@ pub async fn put_record(
|
||||
require_lexicon,
|
||||
) {
|
||||
Ok(status) => Some(status),
|
||||
Err(err_response) => return *err_response,
|
||||
Err(err_response) => return Ok(*err_response),
|
||||
}
|
||||
};
|
||||
if let Some(swap_record_str) = &input.swap_record {
|
||||
let expected_cid = Cid::from_str(swap_record_str).ok();
|
||||
let actual_cid = mst.get(&key).await.ok().flatten();
|
||||
if expected_cid != actual_cid {
|
||||
return ApiError::InvalidSwap(Some(
|
||||
return Ok(ApiError::InvalidSwap(Some(
|
||||
"Record has been modified or does not exist".into(),
|
||||
))
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
let existing_cid = mst.get(&key).await.ok().flatten();
|
||||
let record_ipld = crate::util::json_to_ipld(&input.record);
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() {
|
||||
return ApiError::InvalidRecord("Failed to serialize record".into()).into_response();
|
||||
return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response());
|
||||
}
|
||||
let record_cid = match tracking_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
_ => {
|
||||
return ApiError::InternalError(Some("Failed to save record block".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to save record block".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if existing_cid == Some(record_cid) {
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(PutRecordOutput {
|
||||
uri: AtUri::from_parts(&did, &input.collection, &input.rkey),
|
||||
@@ -507,29 +533,32 @@ pub async fn put_record(
|
||||
validation_status: validation_status.map(|s| s.to_string()),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
let new_mst = if existing_cid.is_some() {
|
||||
match mst.update(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Failed to update MST".into()))
|
||||
.into_response();
|
||||
let new_mst =
|
||||
if existing_cid.is_some() {
|
||||
match mst.update(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
return Ok(ApiError::InternalError(Some("Failed to update MST".into()))
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Failed to add to MST".into()))
|
||||
.into_response();
|
||||
} else {
|
||||
match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
return Ok(ApiError::InternalError(Some("Failed to add to MST".into()))
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
let new_mst_root = match new_mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to persist MST".into())).into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let op = if existing_cid.is_some() {
|
||||
@@ -553,16 +582,20 @@ pub async fn put_record(
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
if mst
|
||||
.blocks_for_path(&key, &mut old_mst_blocks)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
|
||||
.into_response();
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
let mut relevant_blocks = new_mst_blocks.clone();
|
||||
relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone())));
|
||||
@@ -604,9 +637,9 @@ pub async fn put_record(
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) if e.contains("ConcurrentModification") => {
|
||||
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
|
||||
return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response());
|
||||
}
|
||||
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
|
||||
Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()),
|
||||
};
|
||||
|
||||
if let Some(ref controller) = controller_did {
|
||||
@@ -628,7 +661,7 @@ pub async fn put_record(
|
||||
.await;
|
||||
}
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(PutRecordOutput {
|
||||
uri: AtUri::from_parts(&did, &input.collection, &input.rkey),
|
||||
@@ -640,5 +673,5 @@ pub async fn put_record(
|
||||
validation_status: validation_status.map(|s| s.to_string()),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Active, Auth, NotTakendown};
|
||||
use crate::cache::Cache;
|
||||
use crate::plc::PlcClient;
|
||||
use crate::state::AppState;
|
||||
@@ -40,18 +41,18 @@ pub struct CheckAccountStatusOutput {
|
||||
|
||||
pub async fn check_account_status(
|
||||
State(state): State<AppState>,
|
||||
auth: crate::auth::BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
let did = auth.0.did;
|
||||
let user_id = match state.user_repo.get_id_by_did(&did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let did = &auth.did;
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.map_err(|_| ApiError::InternalError(None))?
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
let is_active = state
|
||||
.user_repo
|
||||
.is_account_active_by_did(&did)
|
||||
.is_account_active_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
@@ -95,8 +96,8 @@ pub async fn check_account_status(
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let valid_did =
|
||||
is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await;
|
||||
(
|
||||
is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), did).await;
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(CheckAccountStatusOutput {
|
||||
activated: is_active,
|
||||
@@ -110,7 +111,7 @@ pub async fn check_account_status(
|
||||
imported_blobs,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn is_valid_did_for_service(
|
||||
@@ -305,26 +306,25 @@ async fn assert_valid_did_document_for_service(
|
||||
|
||||
pub async fn activate_account(
|
||||
State(state): State<AppState>,
|
||||
auth: crate::auth::BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
info!("[MIGRATION] activateAccount called");
|
||||
let auth_user = auth.0;
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Authenticated user did={}",
|
||||
auth_user.did
|
||||
auth.did
|
||||
);
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Repo,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
info!("[MIGRATION] activateAccount: Scope check failed");
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let did = auth_user.did;
|
||||
let did = auth.did.clone();
|
||||
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Validating DID document for did={}",
|
||||
@@ -344,7 +344,7 @@ pub async fn activate_account(
|
||||
did,
|
||||
did_validation_start.elapsed()
|
||||
);
|
||||
return e.into_response();
|
||||
return Err(e);
|
||||
}
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})",
|
||||
@@ -450,14 +450,14 @@ pub async fn activate_account(
|
||||
);
|
||||
}
|
||||
info!("[MIGRATION] activateAccount: SUCCESS for did={}", did);
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"[MIGRATION] activateAccount: DB error activating account: {:?}",
|
||||
e
|
||||
);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,18 +470,16 @@ pub struct DeactivateAccountInput {
|
||||
|
||||
pub async fn deactivate_account(
|
||||
State(state): State<AppState>,
|
||||
auth: crate::auth::BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<DeactivateAccountInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Repo,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let delete_after: Option<chrono::DateTime<chrono::Utc>> = input
|
||||
@@ -490,7 +488,7 @@ pub async fn deactivate_account(
|
||||
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||
|
||||
let did = auth_user.did;
|
||||
let did = auth.did.clone();
|
||||
|
||||
let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten();
|
||||
|
||||
@@ -511,47 +509,48 @@ pub async fn deactivate_account(
|
||||
{
|
||||
warn!("Failed to sequence account deactivated event: {}", e);
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Ok(false) => EmptyResponse::ok().into_response(),
|
||||
Ok(false) => Ok(EmptyResponse::ok().into_response()),
|
||||
Err(e) => {
|
||||
error!("DB error deactivating account: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_account_delete(
|
||||
State(state): State<AppState>,
|
||||
auth: crate::auth::BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
let did = &auth.0.did;
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let did = &auth.did;
|
||||
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await {
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
return Ok(crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
let user_id = match state.user_repo.get_id_by_did(did).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let user_id = state
|
||||
.user_repo
|
||||
.get_id_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(ApiError::InternalError(None))?;
|
||||
let confirmation_token = Uuid::new_v4().to_string();
|
||||
let expires_at = Utc::now() + Duration::minutes(15);
|
||||
if let Err(e) = state
|
||||
state
|
||||
.infra_repo
|
||||
.create_deletion_request(&confirmation_token, did, expires_at)
|
||||
.await
|
||||
{
|
||||
error!("DB error creating deletion token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("DB error creating deletion token: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion(
|
||||
state.user_repo.as_ref(),
|
||||
@@ -565,7 +564,7 @@ pub async fn request_account_delete(
|
||||
warn!("Failed to enqueue account deletion notification: {:?}", e);
|
||||
}
|
||||
info!("Account deletion requested for user {}", did);
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{BearerAuth, generate_app_password};
|
||||
use crate::auth::{Active, Auth, generate_app_password};
|
||||
use crate::delegation::{DelegationActionType, intersect_scopes};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use axum::{
|
||||
@@ -33,39 +33,40 @@ pub struct ListAppPasswordsOutput {
|
||||
|
||||
pub async fn list_app_passwords(
|
||||
State(state): State<AppState>,
|
||||
BearerAuth(auth_user): BearerAuth,
|
||||
) -> Response {
|
||||
let user = match state.user_repo.get_by_did(&auth_user.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error getting user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
match state.session_repo.list_app_passwords(user.id).await {
|
||||
Ok(rows) => {
|
||||
let passwords: Vec<AppPassword> = rows
|
||||
.iter()
|
||||
.map(|row| AppPassword {
|
||||
name: row.name.clone(),
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
privileged: row.privileged,
|
||||
scopes: row.scopes.clone(),
|
||||
created_by_controller: row
|
||||
.created_by_controller_did
|
||||
.as_ref()
|
||||
.map(|d| d.to_string()),
|
||||
})
|
||||
.collect();
|
||||
Json(ListAppPasswordsOutput { passwords }).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let rows = state
|
||||
.session_repo
|
||||
.list_app_passwords(user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error listing app passwords: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let passwords: Vec<AppPassword> = rows
|
||||
.iter()
|
||||
.map(|row| AppPassword {
|
||||
name: row.name.clone(),
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
privileged: row.privileged,
|
||||
scopes: row.scopes.clone(),
|
||||
created_by_controller: row
|
||||
.created_by_controller_did
|
||||
.as_ref()
|
||||
.map(|d| d.to_string()),
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(ListAppPasswordsOutput { passwords }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -89,49 +90,50 @@ pub struct CreateAppPasswordOutput {
|
||||
pub async fn create_app_password(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
BearerAuth(auth_user): BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<CreateAppPasswordInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::AppPassword, &client_ip)
|
||||
.await
|
||||
{
|
||||
warn!(ip = %client_ip, "App password creation rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_by_did(&auth_user.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error getting user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let name = input.name.trim();
|
||||
if name.is_empty() {
|
||||
return ApiError::InvalidRequest("name is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("name is required".into()));
|
||||
}
|
||||
|
||||
match state
|
||||
if state
|
||||
.session_repo
|
||||
.get_app_password_by_name(user.id, name)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(),
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error checking app password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Ok(None) => {}
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ApiError::DuplicateAppPassword);
|
||||
}
|
||||
|
||||
let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did {
|
||||
let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did {
|
||||
let grant = state
|
||||
.delegation_repo
|
||||
.get_delegation(&auth_user.did, controller)
|
||||
.get_delegation(&auth.did, controller)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
@@ -141,7 +143,7 @@ pub async fn create_app_password(
|
||||
let intersected = intersect_scopes(requested, &granted_scopes);
|
||||
|
||||
if intersected.is_empty() && !granted_scopes.is_empty() {
|
||||
return ApiError::InsufficientScope(None).into_response();
|
||||
return Err(ApiError::InsufficientScope(None));
|
||||
}
|
||||
|
||||
let scope_result = if intersected.is_empty() {
|
||||
@@ -157,21 +159,17 @@ pub async fn create_app_password(
|
||||
let password = generate_app_password();
|
||||
|
||||
let password_clone = password.clone();
|
||||
let password_hash = match tokio::task::spawn_blocking(move || {
|
||||
bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(e)) => {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let password_hash =
|
||||
tokio::task::spawn_blocking(move || bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let privileged = input.privileged.unwrap_or(false);
|
||||
let created_at = chrono::Utc::now();
|
||||
@@ -185,40 +183,41 @@ pub async fn create_app_password(
|
||||
created_by_controller_did: controller_did.clone(),
|
||||
};
|
||||
|
||||
match state.session_repo.create_app_password(&create_data).await {
|
||||
Ok(_) => {
|
||||
if let Some(ref controller) = controller_did {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&auth_user.did,
|
||||
controller,
|
||||
Some(controller),
|
||||
DelegationActionType::AccountAction,
|
||||
Some(json!({
|
||||
"action": "create_app_password",
|
||||
"name": name,
|
||||
"scopes": final_scopes
|
||||
})),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Json(CreateAppPasswordOutput {
|
||||
name: name.to_string(),
|
||||
password,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
privileged,
|
||||
scopes: final_scopes,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
state
|
||||
.session_repo
|
||||
.create_app_password(&create_data)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error creating app password: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Some(ref controller) = controller_did {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&auth.did,
|
||||
controller,
|
||||
Some(controller),
|
||||
DelegationActionType::AccountAction,
|
||||
Some(json!({
|
||||
"action": "create_app_password",
|
||||
"name": name,
|
||||
"scopes": final_scopes
|
||||
})),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(Json(CreateAppPasswordOutput {
|
||||
name: name.to_string(),
|
||||
password,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
privileged,
|
||||
scopes: final_scopes,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -228,40 +227,41 @@ pub struct RevokeAppPasswordInput {
|
||||
|
||||
pub async fn revoke_app_password(
|
||||
State(state): State<AppState>,
|
||||
BearerAuth(auth_user): BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RevokeAppPasswordInput>,
|
||||
) -> Response {
|
||||
let user = match state.user_repo.get_by_did(&auth_user.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
) -> Result<Response, ApiError> {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error getting user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let name = input.name.trim();
|
||||
if name.is_empty() {
|
||||
return ApiError::InvalidRequest("name is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("name is required".into()));
|
||||
}
|
||||
|
||||
let sessions_to_invalidate = state
|
||||
.session_repo
|
||||
.get_session_jtis_by_app_password(&auth_user.did, name)
|
||||
.get_session_jtis_by_app_password(&auth.did, name)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.session_repo
|
||||
.delete_sessions_by_app_password(&auth_user.did, name)
|
||||
.delete_sessions_by_app_password(&auth.did, name)
|
||||
.await
|
||||
{
|
||||
error!("DB error revoking sessions for app password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("DB error revoking sessions for app password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
futures::future::join_all(sessions_to_invalidate.iter().map(|jti| {
|
||||
let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti);
|
||||
let cache_key = format!("auth:session:{}:{}", &auth.did, jti);
|
||||
let cache = state.cache.clone();
|
||||
async move {
|
||||
let _ = cache.delete(&cache_key).await;
|
||||
@@ -269,10 +269,14 @@ pub async fn revoke_app_password(
|
||||
}))
|
||||
.await;
|
||||
|
||||
if let Err(e) = state.session_repo.delete_app_password(user.id, name).await {
|
||||
error!("DB error revoking app password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
state
|
||||
.session_repo
|
||||
.delete_app_password(user.id, name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error revoking app password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -45,48 +45,48 @@ pub struct RequestEmailUpdateInput {
|
||||
pub async fn request_email_update(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
input: Option<Json<RequestEmailUpdateInput>>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
|
||||
.await
|
||||
{
|
||||
warn!(ip = %client_ip, "Email update rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth.0.is_oauth,
|
||||
auth.0.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_email_info_by_did(&auth.0.did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_email_info_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let Some(current_email) = user.email else {
|
||||
return ApiError::InvalidRequest("account does not have an email address".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"account does not have an email address".into(),
|
||||
));
|
||||
};
|
||||
|
||||
let token_required = user.email_verified;
|
||||
|
||||
if token_required {
|
||||
let code = crate::auth::verification_token::generate_channel_update_token(
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
"email_update",
|
||||
¤t_email.to_lowercase(),
|
||||
);
|
||||
@@ -103,7 +103,7 @@ pub async fn request_email_update(
|
||||
authorized: false,
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&pending) {
|
||||
let cache_key = email_update_cache_key(&auth.0.did);
|
||||
let cache_key = email_update_cache_key(&auth.did);
|
||||
if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await {
|
||||
warn!("Failed to cache pending email update: {:?}", e);
|
||||
}
|
||||
@@ -127,7 +127,7 @@ pub async fn request_email_update(
|
||||
}
|
||||
|
||||
info!("Email update requested for user {}", user.id);
|
||||
TokenRequiredResponse::response(token_required).into_response()
|
||||
Ok(TokenRequiredResponse::response(token_required).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -140,51 +140,50 @@ pub struct ConfirmEmailInput {
|
||||
pub async fn confirm_email(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<ConfirmEmailInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
|
||||
.await
|
||||
{
|
||||
warn!(ip = %client_ip, "Confirm email rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth.0.is_oauth,
|
||||
auth.0.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let did = &auth.0.did;
|
||||
let user = match state.user_repo.get_email_info_by_did(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_email_info_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let Some(ref email) = user.email else {
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
return Err(ApiError::InvalidEmail);
|
||||
};
|
||||
let current_email = email.to_lowercase();
|
||||
|
||||
let provided_email = input.email.trim().to_lowercase();
|
||||
if provided_email != current_email {
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
|
||||
if user.email_verified {
|
||||
return EmptyResponse::ok().into_response();
|
||||
return Ok(EmptyResponse::ok().into_response());
|
||||
}
|
||||
|
||||
let confirmation_code =
|
||||
@@ -199,24 +198,28 @@ pub async fn confirm_email(
|
||||
match verified {
|
||||
Ok(token_data) => {
|
||||
if token_data.did != did.as_str() {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
return Err(ApiError::ExpiredToken(None));
|
||||
}
|
||||
Err(_) => {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = state.user_repo.set_email_verified(user.id, true).await {
|
||||
error!("DB error confirming email: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.set_email_verified(user.id, true)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error confirming email: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!("Email confirmed for user {}", user.id);
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -230,31 +233,28 @@ pub struct UpdateEmailInput {
|
||||
|
||||
pub async fn update_email(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateEmailInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let did = &auth_user.did;
|
||||
let user = match state.user_repo.get_email_info_by_did(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let did = &auth.did;
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_email_info_by_did(did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let user_id = user.id;
|
||||
let current_email = user.email.clone();
|
||||
@@ -262,16 +262,15 @@ pub async fn update_email(
|
||||
let new_email = input.email.trim().to_lowercase();
|
||||
|
||||
if !crate::api::validation::is_valid_email(&new_email) {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"This email address is not supported, please use a different email.".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ref current) = current_email
|
||||
&& new_email == current.to_lowercase()
|
||||
{
|
||||
return EmptyResponse::ok().into_response();
|
||||
return Ok(EmptyResponse::ok().into_response());
|
||||
}
|
||||
|
||||
if email_verified {
|
||||
@@ -290,7 +289,7 @@ pub async fn update_email(
|
||||
|
||||
if !authorized_via_link {
|
||||
let Some(ref t) = input.token else {
|
||||
return ApiError::TokenRequired.into_response();
|
||||
return Err(ApiError::TokenRequired);
|
||||
};
|
||||
let confirmation_token =
|
||||
crate::auth::verification_token::normalize_token_input(t.trim());
|
||||
@@ -309,23 +308,27 @@ pub async fn update_email(
|
||||
match verified {
|
||||
Ok(token_data) => {
|
||||
if token_data.did != did.as_str() {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
return Err(ApiError::ExpiredToken(None));
|
||||
}
|
||||
Err(_) => {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = state.user_repo.update_email(user_id, &new_email).await {
|
||||
error!("DB error updating email: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.update_email(user_id, &new_email)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error updating email: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let verification_token =
|
||||
crate::auth::verification_token::generate_signup_token(did, "email", &new_email);
|
||||
@@ -358,7 +361,7 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
info!("Email updated for user {}", user_id);
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -497,46 +500,46 @@ pub async fn authorize_email_update(
|
||||
pub async fn check_email_update_status(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BearerAuth,
|
||||
) -> Response {
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::VerificationCheck, &client_ip)
|
||||
.await
|
||||
{
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth.0.is_oauth,
|
||||
auth.0.scope.as_deref(),
|
||||
auth.is_oauth(),
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Read,
|
||||
) {
|
||||
return e;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
let cache_key = email_update_cache_key(&auth.0.did);
|
||||
let cache_key = email_update_cache_key(&auth.did);
|
||||
let pending_json = match state.cache.get(&cache_key).await {
|
||||
Some(json) => json,
|
||||
None => {
|
||||
return Json(json!({ "pending": false, "authorized": false })).into_response();
|
||||
return Ok(Json(json!({ "pending": false, "authorized": false })).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return Json(json!({ "pending": false, "authorized": false })).into_response();
|
||||
return Ok(Json(json!({ "pending": false, "authorized": false })).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"pending": true,
|
||||
"authorized": pending.authorized,
|
||||
"newEmail": pending.new_email,
|
||||
}))
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::extractor::BearerAuthAdmin;
|
||||
use crate::auth::{Active, Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use axum::{
|
||||
@@ -44,19 +43,20 @@ pub struct CreateInviteCodeOutput {
|
||||
|
||||
pub async fn create_invite_code(
|
||||
State(state): State<AppState>,
|
||||
BearerAuthAdmin(auth_user): BearerAuthAdmin,
|
||||
auth: Auth<Admin>,
|
||||
Json(input): Json<CreateInviteCodeInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if input.use_count < 1 {
|
||||
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"useCount must be at least 1".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let for_account: Did = match &input.for_account {
|
||||
Some(acct) => match acct.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
},
|
||||
None => auth_user.did.clone(),
|
||||
Some(acct) => acct
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?,
|
||||
None => auth.did.clone(),
|
||||
};
|
||||
let code = gen_invite_code();
|
||||
|
||||
@@ -65,14 +65,14 @@ pub async fn create_invite_code(
|
||||
.create_invite_code(&code, input.use_count, Some(&for_account))
|
||||
.await
|
||||
{
|
||||
Ok(true) => Json(CreateInviteCodeOutput { code }).into_response(),
|
||||
Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()),
|
||||
Ok(false) => {
|
||||
error!("No admin user found to create invite code");
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error creating invite code: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,36 +98,37 @@ pub struct AccountCodes {
|
||||
|
||||
pub async fn create_invite_codes(
|
||||
State(state): State<AppState>,
|
||||
BearerAuthAdmin(auth_user): BearerAuthAdmin,
|
||||
auth: Auth<Admin>,
|
||||
Json(input): Json<CreateInviteCodesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if input.use_count < 1 {
|
||||
return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"useCount must be at least 1".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let code_count = input.code_count.unwrap_or(1).max(1);
|
||||
let for_accounts: Vec<Did> = match &input.for_accounts {
|
||||
Some(accounts) if !accounts.is_empty() => {
|
||||
let parsed: Result<Vec<Did>, _> = accounts.iter().map(|a| a.parse()).collect();
|
||||
match parsed {
|
||||
Ok(dids) => dids,
|
||||
Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(),
|
||||
}
|
||||
}
|
||||
_ => vec![auth_user.did.clone()],
|
||||
Some(accounts) if !accounts.is_empty() => accounts
|
||||
.iter()
|
||||
.map(|a| a.parse())
|
||||
.collect::<Result<Vec<Did>, _>>()
|
||||
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?,
|
||||
_ => vec![auth.did.clone()],
|
||||
};
|
||||
|
||||
let admin_user_id = match state.user_repo.get_any_admin_user_id().await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => {
|
||||
error!("No admin user found to create invite codes");
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let admin_user_id = state
|
||||
.user_repo
|
||||
.get_any_admin_user_id()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error looking up admin user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
error!("No admin user found to create invite codes");
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| {
|
||||
let infra_repo = state.infra_repo.clone();
|
||||
@@ -146,13 +147,13 @@ pub async fn create_invite_codes(
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(result_codes) => Json(CreateInviteCodesOutput {
|
||||
Ok(result_codes) => Ok(Json(CreateInviteCodesOutput {
|
||||
codes: result_codes,
|
||||
})
|
||||
.into_response(),
|
||||
.into_response()),
|
||||
Err(e) => {
|
||||
error!("DB error creating invite codes: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,22 +193,19 @@ pub struct GetAccountInviteCodesOutput {
|
||||
|
||||
pub async fn get_account_invite_codes(
|
||||
State(state): State<AppState>,
|
||||
BearerAuth(auth_user): BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let include_used = params.include_used.unwrap_or(true);
|
||||
|
||||
let codes_info = match state
|
||||
let codes_info = state
|
||||
.infra_repo
|
||||
.get_invite_codes_for_account(&auth_user.did)
|
||||
.get_invite_codes_for_account(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching invite codes: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let filtered_codes: Vec<_> = codes_info
|
||||
.into_iter()
|
||||
@@ -254,5 +252,5 @@ pub async fn get_account_invite_codes(
|
||||
.await;
|
||||
|
||||
let codes: Vec<InviteCode> = codes.into_iter().flatten().collect();
|
||||
Json(GetAccountInviteCodesOutput { codes }).into_response()
|
||||
Ok(Json(GetAccountInviteCodesOutput { codes }).into_response())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -36,35 +36,30 @@ pub struct UpdateDidDocumentOutput {
|
||||
|
||||
pub async fn update_did_document(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateDidDocumentInput>,
|
||||
) -> Response {
|
||||
let auth_user = auth.0;
|
||||
|
||||
if !auth_user.did.starts_with("did:web:") {
|
||||
return ApiError::InvalidRequest(
|
||||
) -> Result<Response, ApiError> {
|
||||
if !auth.did.starts_with("did:web:") {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"DID document updates are only available for did:web accounts".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_user_for_did_doc(&auth_user.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_user_for_did_doc(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("DB error getting user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if user.deactivated_at.is_some() {
|
||||
return ApiError::AccountDeactivated.into_response();
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
if let Some(ref methods) = input.verification_methods {
|
||||
if methods.is_empty() {
|
||||
return ApiError::InvalidRequest("verification_methods cannot be empty".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"verification_methods cannot be empty".into(),
|
||||
));
|
||||
}
|
||||
let validation_error = methods.iter().find_map(|method| {
|
||||
if method.id.is_empty() {
|
||||
@@ -80,22 +75,24 @@ pub async fn update_did_document(
|
||||
}
|
||||
});
|
||||
if let Some(err) = validation_error {
|
||||
return ApiError::InvalidRequest(err.into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(err.into()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref handles) = input.also_known_as
|
||||
&& handles.iter().any(|h| !h.starts_with("at://"))
|
||||
{
|
||||
return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"alsoKnownAs entries must be at:// URIs".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ref endpoint) = input.service_endpoint {
|
||||
let endpoint = endpoint.trim();
|
||||
if !endpoint.starts_with("https://") {
|
||||
return ApiError::InvalidRequest("serviceEndpoint must start with https://".into())
|
||||
.into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"serviceEndpoint must start with https://".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,54 +103,54 @@ pub async fn update_did_document(
|
||||
|
||||
let also_known_as: Option<Vec<String>> = input.also_known_as.clone();
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.upsert_did_web_overrides(user.id, verification_methods_json, also_known_as)
|
||||
.await
|
||||
{
|
||||
tracing::error!("DB error upserting did_web_overrides: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
tracing::error!("DB error upserting did_web_overrides: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Some(ref endpoint) = input.service_endpoint {
|
||||
let endpoint_clean = endpoint.trim().trim_end_matches('/');
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.update_migrated_to_pds(&auth_user.did, endpoint_clean)
|
||||
.update_migrated_to_pds(&auth.did, endpoint_clean)
|
||||
.await
|
||||
{
|
||||
tracing::error!("DB error updating service endpoint: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
tracing::error!("DB error updating service endpoint: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
}
|
||||
|
||||
let did_doc = build_did_document(&state, &auth_user.did).await;
|
||||
let did_doc = build_did_document(&state, &auth.did).await;
|
||||
|
||||
tracing::info!("Updated DID document for {}", &auth_user.did);
|
||||
tracing::info!("Updated DID document for {}", &auth.did);
|
||||
|
||||
(
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(UpdateDidDocumentOutput {
|
||||
success: true,
|
||||
did_document: did_doc,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn get_did_document(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let auth_user = auth.0;
|
||||
|
||||
if !auth_user.did.starts_with("did:web:") {
|
||||
return ApiError::InvalidRequest(
|
||||
pub async fn get_did_document(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if !auth.did.starts_with("did:web:") {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"This endpoint is only available for did:web accounts".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
|
||||
let did_doc = build_did_document(&state, &auth_user.did).await;
|
||||
let did_doc = build_did_document(&state, &auth.did).await;
|
||||
|
||||
(StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()
|
||||
Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response())
|
||||
}
|
||||
|
||||
async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::webauthn::WebAuthnConfig;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -34,32 +34,29 @@ pub struct StartRegistrationResponse {
|
||||
|
||||
pub async fn start_passkey_registration(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<StartRegistrationInput>,
|
||||
) -> Response {
|
||||
let webauthn = match get_webauthn() {
|
||||
Ok(w) => w,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
) -> Result<Response, ApiError> {
|
||||
let webauthn = get_webauthn()?;
|
||||
|
||||
let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await {
|
||||
Ok(Some(h)) => h,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let handle = state
|
||||
.user_repo
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let existing_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await {
|
||||
Ok(passkeys) => passkeys,
|
||||
Err(e) => {
|
||||
let existing_passkeys = state
|
||||
.user_repo
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching existing passkeys: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let exclude_credentials: Vec<CredentialID> = existing_passkeys
|
||||
.iter()
|
||||
@@ -68,42 +65,32 @@ pub async fn start_passkey_registration(
|
||||
|
||||
let display_name = input.friendly_name.as_deref().unwrap_or(&handle);
|
||||
|
||||
let (ccr, reg_state) = match webauthn.start_registration(
|
||||
&auth.0.did,
|
||||
&handle,
|
||||
display_name,
|
||||
exclude_credentials,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let (ccr, reg_state) = webauthn
|
||||
.start_registration(&auth.did, &handle, display_name, exclude_credentials)
|
||||
.map_err(|e| {
|
||||
error!("Failed to start passkey registration: {}", e);
|
||||
return ApiError::InternalError(Some("Failed to start registration".into()))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(Some("Failed to start registration".into()))
|
||||
})?;
|
||||
|
||||
let state_json = match serde_json::to_string(®_state) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let state_json = serde_json::to_string(®_state).map_err(|e| {
|
||||
error!("Failed to serialize registration state: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.save_webauthn_challenge(&auth.0.did, "registration", &state_json)
|
||||
.save_webauthn_challenge(&auth.did, "registration", &state_json)
|
||||
.await
|
||||
{
|
||||
error!("Failed to save registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to save registration state: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({}));
|
||||
|
||||
info!(did = %auth.0.did, "Passkey registration started");
|
||||
info!(did = %auth.did, "Passkey registration started");
|
||||
|
||||
Json(StartRegistrationResponse { options }).into_response()
|
||||
Ok(Json(StartRegistrationResponse { options }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -122,81 +109,62 @@ pub struct FinishRegistrationResponse {
|
||||
|
||||
pub async fn finish_passkey_registration(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<FinishRegistrationInput>,
|
||||
) -> Response {
|
||||
let webauthn = match get_webauthn() {
|
||||
Ok(w) => w,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
) -> Result<Response, ApiError> {
|
||||
let webauthn = get_webauthn()?;
|
||||
|
||||
let reg_state_json = match state
|
||||
let reg_state_json = state
|
||||
.user_repo
|
||||
.load_webauthn_challenge(&auth.0.did, "registration")
|
||||
.load_webauthn_challenge(&auth.did, "registration")
|
||||
.await
|
||||
{
|
||||
Ok(Some(json)) => json,
|
||||
Ok(None) => {
|
||||
return ApiError::NoRegistrationInProgress.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error loading registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::NoRegistrationInProgress)?;
|
||||
|
||||
let reg_state: SecurityKeyRegistration = match serde_json::from_str(®_state_json) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let reg_state: SecurityKeyRegistration =
|
||||
serde_json::from_str(®_state_json).map_err(|e| {
|
||||
error!("Failed to deserialize registration state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to parse credential: {:?}", e);
|
||||
return ApiError::InvalidCredential.into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InvalidCredential
|
||||
})?;
|
||||
|
||||
let passkey = match webauthn.finish_registration(&credential, ®_state) {
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
let passkey = webauthn
|
||||
.finish_registration(&credential, ®_state)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to finish passkey registration: {}", e);
|
||||
return ApiError::RegistrationFailed.into_response();
|
||||
}
|
||||
};
|
||||
ApiError::RegistrationFailed
|
||||
})?;
|
||||
|
||||
let public_key = match serde_json::to_vec(&passkey) {
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize passkey: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let public_key = serde_json::to_vec(&passkey).map_err(|e| {
|
||||
error!("Failed to serialize passkey: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let passkey_id = match state
|
||||
let passkey_id = state
|
||||
.user_repo
|
||||
.save_passkey(
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
passkey.cred_id(),
|
||||
&public_key,
|
||||
input.friendly_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("Failed to save passkey: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.delete_webauthn_challenge(&auth.0.did, "registration")
|
||||
.delete_webauthn_challenge(&auth.did, "registration")
|
||||
.await
|
||||
{
|
||||
warn!("Failed to delete registration state: {:?}", e);
|
||||
@@ -207,13 +175,13 @@ pub async fn finish_passkey_registration(
|
||||
passkey.cred_id(),
|
||||
);
|
||||
|
||||
info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered");
|
||||
info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered");
|
||||
|
||||
Json(FinishRegistrationResponse {
|
||||
Ok(Json(FinishRegistrationResponse {
|
||||
id: passkey_id.to_string(),
|
||||
credential_id: credential_id_base64,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -232,14 +200,18 @@ pub struct ListPasskeysResponse {
|
||||
pub passkeys: Vec<PasskeyInfo>,
|
||||
}
|
||||
|
||||
pub async fn list_passkeys(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await {
|
||||
Ok(pks) => pks,
|
||||
Err(e) => {
|
||||
pub async fn list_passkeys(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let passkeys = state
|
||||
.user_repo
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching passkeys: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let passkey_infos: Vec<PasskeyInfo> = passkeys
|
||||
.into_iter()
|
||||
@@ -252,10 +224,10 @@ pub async fn list_passkeys(State(state): State<AppState>, auth: BearerAuth) -> R
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ListPasskeysResponse {
|
||||
Ok(Json(ListPasskeysResponse {
|
||||
passkeys: passkey_infos,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -266,45 +238,39 @@ pub struct DeletePasskeyInput {
|
||||
|
||||
pub async fn delete_passkey(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<DeletePasskeyInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
) -> Result<Response, ApiError> {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
return Ok(crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await {
|
||||
return Ok(crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
let id: uuid::Uuid = match input.id.parse() {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidId.into_response();
|
||||
}
|
||||
};
|
||||
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
|
||||
|
||||
match state.user_repo.delete_passkey(id, &auth.0.did).await {
|
||||
match state.user_repo.delete_passkey(id, &auth.did).await {
|
||||
Ok(true) => {
|
||||
info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted");
|
||||
EmptyResponse::ok().into_response()
|
||||
info!(did = %auth.did, passkey_id = %id, "Passkey deleted");
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Ok(false) => ApiError::PasskeyNotFound.into_response(),
|
||||
Ok(false) => Err(ApiError::PasskeyNotFound),
|
||||
Err(e) => {
|
||||
error!("DB error deleting passkey: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,29 +284,24 @@ pub struct UpdatePasskeyInput {
|
||||
|
||||
pub async fn update_passkey(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdatePasskeyInput>,
|
||||
) -> Response {
|
||||
let id: uuid::Uuid = match input.id.parse() {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidId.into_response();
|
||||
}
|
||||
};
|
||||
) -> Result<Response, ApiError> {
|
||||
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.update_passkey_name(id, &auth.0.did, &input.friendly_name)
|
||||
.update_passkey_name(id, &auth.did, &input.friendly_name)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed");
|
||||
EmptyResponse::ok().into_response()
|
||||
info!(did = %auth.did, passkey_id = %id, "Passkey renamed");
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Ok(false) => ApiError::PasskeyNotFound.into_response(),
|
||||
Ok(false) => Err(ApiError::PasskeyNotFound),
|
||||
Err(e) => {
|
||||
error!("DB error updating passkey: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse};
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::PlainPassword;
|
||||
use crate::validation::validate_password;
|
||||
@@ -227,153 +227,158 @@ pub struct ChangePasswordInput {
|
||||
|
||||
pub async fn change_password(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<ChangePasswordInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
) -> Result<Response, ApiError> {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
return Ok(crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
let current_password = &input.current_password;
|
||||
let new_password = &input.new_password;
|
||||
if current_password.is_empty() {
|
||||
return ApiError::InvalidRequest("currentPassword is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"currentPassword is required".into(),
|
||||
));
|
||||
}
|
||||
if new_password.is_empty() {
|
||||
return ApiError::InvalidRequest("newPassword is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("newPassword is required".into()));
|
||||
}
|
||||
if let Err(e) = validate_password(new_password) {
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
return Err(ApiError::InvalidRequest(e.to_string()));
|
||||
}
|
||||
let user = match state
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_id_and_password_hash_by_did(&auth.0.did)
|
||||
.get_id_and_password_hash_by_did(&auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error in change_password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let (user_id, password_hash) = (user.id, user.password_hash);
|
||||
let valid = match verify(current_password, &password_hash) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Password verification error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let valid = verify(current_password, &password_hash).map_err(|e| {
|
||||
error!("Password verification error: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
if !valid {
|
||||
return ApiError::InvalidPassword("Current password is incorrect".into()).into_response();
|
||||
return Err(ApiError::InvalidPassword(
|
||||
"Current password is incorrect".into(),
|
||||
));
|
||||
}
|
||||
let new_password_clone = new_password.to_string();
|
||||
let new_hash =
|
||||
match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(e)) => {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if let Err(e) = state
|
||||
let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
state
|
||||
.user_repo
|
||||
.update_password_hash(user_id, &new_hash)
|
||||
.await
|
||||
{
|
||||
error!("DB error updating password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
info!(did = %&auth.0.did, "Password changed successfully");
|
||||
EmptyResponse::ok().into_response()
|
||||
.map_err(|e| {
|
||||
error!("DB error updating password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.did, "Password changed successfully");
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
match state.user_repo.has_password_by_did(&auth.0.did).await {
|
||||
Ok(Some(has)) => HasPasswordResponse::response(has).into_response(),
|
||||
Ok(None) => ApiError::AccountNotFound.into_response(),
|
||||
pub async fn get_password_status(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
match state.user_repo.has_password_by_did(&auth.did).await {
|
||||
Ok(Some(has)) => Ok(HasPasswordResponse::response(has).into_response()),
|
||||
Ok(None) => Err(ApiError::AccountNotFound),
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
pub async fn remove_password(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
return Ok(crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
if crate::api::server::reauth::check_reauth_required_cached(
|
||||
&*state.session_repo,
|
||||
&state.cache,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
return Ok(crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
let has_passkeys = state
|
||||
.user_repo
|
||||
.has_passkeys(&auth.0.did)
|
||||
.has_passkeys(&auth.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !has_passkeys {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"You must have at least one passkey registered before removing your password".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_password_info_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
if user.password_hash.is_none() {
|
||||
return ApiError::InvalidRequest("Account already has no password".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Account already has no password".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(e) = state.user_repo.remove_user_password(user.id).await {
|
||||
error!("DB error removing password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.remove_user_password(user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error removing password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.0.did, "Password removed - account is now passkey-only");
|
||||
SuccessResponse::ok().into_response()
|
||||
info!(did = %&auth.did, "Password removed - account is now passkey-only");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -384,24 +389,24 @@ pub struct SetPasswordInput {
|
||||
|
||||
pub async fn set_password(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<SetPasswordInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let has_password = state
|
||||
.user_repo
|
||||
.has_password_by_did(&auth.0.did)
|
||||
.has_password_by_did(&auth.did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
let has_passkeys = state
|
||||
.user_repo
|
||||
.has_passkeys(&auth.0.did)
|
||||
.has_passkeys(&auth.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let has_totp = state
|
||||
.user_repo
|
||||
.has_totp_enabled(&auth.0.did)
|
||||
.has_totp_enabled(&auth.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -411,67 +416,63 @@ pub async fn set_password(
|
||||
&& crate::api::server::reauth::check_reauth_required_cached(
|
||||
&*state.session_repo,
|
||||
&state.cache,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
return Ok(crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
let new_password = &input.new_password;
|
||||
if new_password.is_empty() {
|
||||
return ApiError::InvalidRequest("newPassword is required".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("newPassword is required".into()));
|
||||
}
|
||||
if let Err(e) = validate_password(new_password) {
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
return Err(ApiError::InvalidRequest(e.to_string()));
|
||||
}
|
||||
|
||||
let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
let user = state
|
||||
.user_repo
|
||||
.get_password_info_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
if user.password_hash.is_some() {
|
||||
return ApiError::InvalidRequest(
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Account already has a password. Use changePassword instead.".into(),
|
||||
)
|
||||
.into_response();
|
||||
));
|
||||
}
|
||||
|
||||
let new_password_clone = new_password.to_string();
|
||||
let new_hash =
|
||||
match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(e)) => {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.set_new_user_password(user.id, &new_hash)
|
||||
.await
|
||||
{
|
||||
error!("DB error setting password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("DB error setting password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.0.did, "Password set for passkey-only account");
|
||||
SuccessResponse::ok().into_response()
|
||||
info!(did = %&auth.did, "Password set for passkey-only account");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_db_traits::{SessionRepository, UserRepository};
|
||||
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::PlainPassword;
|
||||
|
||||
@@ -24,25 +24,29 @@ pub struct ReauthStatusResponse {
|
||||
pub available_methods: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn get_reauth_status(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let last_reauth_at = match state.session_repo.get_last_reauth_at(&auth.0.did).await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
pub async fn get_reauth_status(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let last_reauth_at = state
|
||||
.session_repo
|
||||
.get_last_reauth_at(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let reauth_required = is_reauth_required(last_reauth_at);
|
||||
let available_methods =
|
||||
get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.0.did).await;
|
||||
get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.did).await;
|
||||
|
||||
Json(ReauthStatusResponse {
|
||||
Ok(Json(ReauthStatusResponse {
|
||||
last_reauth_at,
|
||||
reauth_required,
|
||||
available_methods,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -59,26 +63,25 @@ pub struct ReauthResponse {
|
||||
|
||||
pub async fn reauth_password(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<PasswordReauthInput>,
|
||||
) -> Response {
|
||||
let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await {
|
||||
Ok(Some(hash)) => hash,
|
||||
Ok(None) => {
|
||||
return ApiError::AccountNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
) -> Result<Response, ApiError> {
|
||||
let password_hash = state
|
||||
.user_repo
|
||||
.get_password_hash_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false);
|
||||
|
||||
if !password_valid {
|
||||
let app_password_hashes = state
|
||||
.session_repo
|
||||
.get_app_password_hashes_by_did(&auth.0.did)
|
||||
.get_app_password_hashes_by_did(&auth.did)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -87,21 +90,20 @@ pub async fn reauth_password(
|
||||
});
|
||||
|
||||
if !app_password_valid {
|
||||
warn!(did = %&auth.0.did, "Re-auth failed: invalid password");
|
||||
return ApiError::InvalidPassword("Password is incorrect".into()).into_response();
|
||||
warn!(did = %&auth.did, "Re-auth failed: invalid password");
|
||||
return Err(ApiError::InvalidPassword("Password is incorrect".into()));
|
||||
}
|
||||
}
|
||||
|
||||
match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await {
|
||||
Ok(reauthed_at) => {
|
||||
info!(did = %&auth.0.did, "Re-auth successful via password");
|
||||
Json(ReauthResponse { reauthed_at }).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error updating reauth: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.did, "Re-auth successful via password");
|
||||
Ok(Json(ReauthResponse { reauthed_at }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -112,39 +114,39 @@ pub struct TotpReauthInput {
|
||||
|
||||
pub async fn reauth_totp(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<TotpReauthInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.did)
|
||||
.await
|
||||
{
|
||||
warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(Some(
|
||||
warn!(did = %&auth.did, "TOTP verification rate limit exceeded");
|
||||
return Err(ApiError::RateLimitExceeded(Some(
|
||||
"Too many verification attempts. Please try again in a few minutes.".into(),
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
|
||||
let valid =
|
||||
crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.0.did, &input.code)
|
||||
crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code)
|
||||
.await;
|
||||
|
||||
if !valid {
|
||||
warn!(did = %&auth.0.did, "Re-auth failed: invalid TOTP code");
|
||||
return ApiError::InvalidCode(Some("Invalid TOTP or backup code".into())).into_response();
|
||||
warn!(did = %&auth.did, "Re-auth failed: invalid TOTP code");
|
||||
return Err(ApiError::InvalidCode(Some(
|
||||
"Invalid TOTP or backup code".into(),
|
||||
)));
|
||||
}
|
||||
|
||||
match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await {
|
||||
Ok(reauthed_at) => {
|
||||
info!(did = %&auth.0.did, "Re-auth successful via TOTP");
|
||||
Json(ReauthResponse { reauthed_at }).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error updating reauth: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.did, "Re-auth successful via TOTP");
|
||||
Ok(Json(ReauthResponse { reauthed_at }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -153,19 +155,23 @@ pub struct PasskeyReauthStartResponse {
|
||||
pub options: serde_json::Value,
|
||||
}
|
||||
|
||||
pub async fn reauth_passkey_start(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
pub async fn reauth_passkey_start(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
let stored_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await {
|
||||
Ok(pks) => pks,
|
||||
Err(e) => {
|
||||
let stored_passkeys = state
|
||||
.user_repo
|
||||
.get_passkeys_for_user(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to get passkeys: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if stored_passkeys.is_empty() {
|
||||
return ApiError::NoPasskeys.into_response();
|
||||
return Err(ApiError::NoPasskeys);
|
||||
}
|
||||
|
||||
let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys
|
||||
@@ -174,44 +180,37 @@ pub async fn reauth_passkey_start(State(state): State<AppState>, auth: BearerAut
|
||||
.collect();
|
||||
|
||||
if passkeys.is_empty() {
|
||||
return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response();
|
||||
return Err(ApiError::InternalError(Some(
|
||||
"Failed to load passkeys".into(),
|
||||
)));
|
||||
}
|
||||
|
||||
let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Failed to create WebAuthn config: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| {
|
||||
error!("Failed to create WebAuthn config: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let (rcr, auth_state) = match webauthn.start_authentication(passkeys) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Failed to start passkey authentication: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let (rcr, auth_state) = webauthn.start_authentication(passkeys).map_err(|e| {
|
||||
error!("Failed to start passkey authentication: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let state_json = match serde_json::to_string(&auth_state) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize authentication state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let state_json = serde_json::to_string(&auth_state).map_err(|e| {
|
||||
error!("Failed to serialize authentication state: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.save_webauthn_challenge(&auth.0.did, "authentication", &state_json)
|
||||
.save_webauthn_challenge(&auth.did, "authentication", &state_json)
|
||||
.await
|
||||
{
|
||||
error!("Failed to save authentication state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to save authentication state: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({}));
|
||||
Json(PasskeyReauthStartResponse { options }).into_response()
|
||||
Ok(Json(PasskeyReauthStartResponse { options }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -222,60 +221,44 @@ pub struct PasskeyReauthFinishInput {
|
||||
|
||||
pub async fn reauth_passkey_finish(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<PasskeyReauthFinishInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
let auth_state_json = match state
|
||||
let auth_state_json = state
|
||||
.user_repo
|
||||
.load_webauthn_challenge(&auth.0.did, "authentication")
|
||||
.load_webauthn_challenge(&auth.did, "authentication")
|
||||
.await
|
||||
{
|
||||
Ok(Some(json)) => json,
|
||||
Ok(None) => {
|
||||
return ApiError::NoChallengeInProgress.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("Failed to load authentication state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::NoChallengeInProgress)?;
|
||||
|
||||
let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication =
|
||||
match serde_json::from_str(&auth_state_json) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to deserialize authentication state: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
serde_json::from_str(&auth_state_json).map_err(|e| {
|
||||
error!("Failed to deserialize authentication state: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let credential: webauthn_rs::prelude::PublicKeyCredential =
|
||||
match serde_json::from_value(input.credential) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse credential: {:?}", e);
|
||||
return ApiError::InvalidCredential.into_response();
|
||||
}
|
||||
};
|
||||
serde_json::from_value(input.credential).map_err(|e| {
|
||||
warn!("Failed to parse credential: {:?}", e);
|
||||
ApiError::InvalidCredential
|
||||
})?;
|
||||
|
||||
let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Failed to create WebAuthn config: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| {
|
||||
error!("Failed to create WebAuthn config: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let auth_result = match webauthn.finish_authentication(&credential, &auth_state) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!(did = %&auth.0.did, "Passkey re-auth failed: {:?}", e);
|
||||
return ApiError::AuthenticationFailed(Some("Passkey authentication failed".into()))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let auth_result = webauthn
|
||||
.finish_authentication(&credential, &auth_state)
|
||||
.map_err(|e| {
|
||||
warn!(did = %&auth.did, "Passkey re-auth failed: {:?}", e);
|
||||
ApiError::AuthenticationFailed(Some("Passkey authentication failed".into()))
|
||||
})?;
|
||||
|
||||
let cred_id_bytes = auth_result.cred_id().as_ref();
|
||||
match state
|
||||
@@ -284,12 +267,12 @@ pub async fn reauth_passkey_finish(
|
||||
.await
|
||||
{
|
||||
Ok(false) => {
|
||||
warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key");
|
||||
warn!(did = %&auth.did, "Passkey counter anomaly detected - possible cloned key");
|
||||
let _ = state
|
||||
.user_repo
|
||||
.delete_webauthn_challenge(&auth.0.did, "authentication")
|
||||
.delete_webauthn_challenge(&auth.did, "authentication")
|
||||
.await;
|
||||
return ApiError::PasskeyCounterAnomaly.into_response();
|
||||
return Err(ApiError::PasskeyCounterAnomaly);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to update passkey counter: {:?}", e);
|
||||
@@ -299,19 +282,18 @@ pub async fn reauth_passkey_finish(
|
||||
|
||||
let _ = state
|
||||
.user_repo
|
||||
.delete_webauthn_challenge(&auth.0.did, "authentication")
|
||||
.delete_webauthn_challenge(&auth.did, "authentication")
|
||||
.await;
|
||||
|
||||
match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await {
|
||||
Ok(reauthed_at) => {
|
||||
info!(did = %&auth.0.did, "Re-auth successful via passkey");
|
||||
Json(ReauthResponse { reauthed_at }).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error updating reauth: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.did, "Re-auth successful via passkey");
|
||||
Ok(Json(ReauthResponse { reauthed_at }).into_response())
|
||||
}
|
||||
|
||||
pub async fn update_last_reauth_cached(
|
||||
|
||||
@@ -95,12 +95,12 @@ pub async fn get_service_auth(
|
||||
{
|
||||
Ok(result) => crate::auth::AuthenticatedUser {
|
||||
did: Did::new_unchecked(result.did),
|
||||
is_oauth: true,
|
||||
is_admin: false,
|
||||
status: AccountStatus::Active,
|
||||
scope: result.scope,
|
||||
key_bytes: None,
|
||||
controller_did: None,
|
||||
auth_source: crate::auth::AuthSource::OAuth,
|
||||
},
|
||||
Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => {
|
||||
return (
|
||||
@@ -131,7 +131,7 @@ pub async fn get_service_auth(
|
||||
};
|
||||
info!(
|
||||
did = %&auth_user.did,
|
||||
is_oauth = auth_user.is_oauth,
|
||||
is_oauth = auth_user.is_oauth(),
|
||||
has_key = auth_user.key_bytes.is_some(),
|
||||
"getServiceAuth auth validated"
|
||||
);
|
||||
@@ -180,14 +180,14 @@ pub async fn get_service_auth(
|
||||
|
||||
if let Some(method) = lxm {
|
||||
if let Err(e) = crate::auth::scope_check::check_rpc_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.is_oauth(),
|
||||
auth_user.scope.as_deref(),
|
||||
¶ms.aud,
|
||||
method,
|
||||
) {
|
||||
return e;
|
||||
}
|
||||
} else if auth_user.is_oauth {
|
||||
} else if auth_user.is_oauth() {
|
||||
let permissions = auth_user.permissions();
|
||||
if !permissions.has_full_access() {
|
||||
return ApiError::InvalidRequest(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::{EmptyResponse, SuccessResponse};
|
||||
use crate::auth::{BearerAuth, BearerAuthAllowDeactivated};
|
||||
use crate::auth::{Active, Auth, NotTakendown};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::{AccountState, Did, Handle, PlainPassword};
|
||||
use axum::{
|
||||
@@ -279,15 +279,15 @@ pub async fn create_session(
|
||||
|
||||
pub async fn get_session(
|
||||
State(state): State<AppState>,
|
||||
BearerAuthAllowDeactivated(auth_user): BearerAuthAllowDeactivated,
|
||||
) -> Response {
|
||||
let permissions = auth_user.permissions();
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let permissions = auth.permissions();
|
||||
let can_read_email = permissions.allows_email_read();
|
||||
|
||||
let did_for_doc = auth_user.did.clone();
|
||||
let did_for_doc = auth.did.clone();
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
let (db_result, did_doc) = tokio::join!(
|
||||
state.user_repo.get_session_info_by_did(&auth_user.did),
|
||||
state.user_repo.get_session_info_by_did(&auth.did),
|
||||
did_resolver.resolve_did_document(&did_for_doc)
|
||||
);
|
||||
match db_result {
|
||||
@@ -316,7 +316,7 @@ pub async fn get_session(
|
||||
let email_confirmed_value = can_read_email && row.email_verified;
|
||||
let mut response = json!({
|
||||
"handle": handle,
|
||||
"did": &auth_user.did,
|
||||
"did": &auth.did,
|
||||
"active": account_state.is_active(),
|
||||
"preferredChannel": preferred_channel,
|
||||
"preferredChannelVerified": preferred_channel_verified,
|
||||
@@ -337,12 +337,12 @@ pub async fn get_session(
|
||||
if let Some(doc) = did_doc {
|
||||
response["didDoc"] = doc;
|
||||
}
|
||||
Json(response).into_response()
|
||||
Ok(Json(response).into_response())
|
||||
}
|
||||
Ok(None) => ApiError::AuthenticationFailed(None).into_response(),
|
||||
Ok(None) => Err(ApiError::AuthenticationFailed(None)),
|
||||
Err(e) => {
|
||||
error!("Database error in get_session: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
Err(ApiError::InternalError(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,18 +350,14 @@ pub async fn get_session(
|
||||
pub async fn delete_session(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
_auth: BearerAuth,
|
||||
) -> Response {
|
||||
let extracted = match crate::auth::extract_auth_token_from_header(
|
||||
_auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let extracted = crate::auth::extract_auth_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => return ApiError::AuthenticationRequired.into_response(),
|
||||
};
|
||||
let jti = match crate::auth::get_jti_from_token(&extracted.token) {
|
||||
Ok(jti) => jti,
|
||||
Err(_) => return ApiError::AuthenticationFailed(None).into_response(),
|
||||
};
|
||||
)
|
||||
.ok_or(ApiError::AuthenticationRequired)?;
|
||||
let jti = crate::auth::get_jti_from_token(&extracted.token)
|
||||
.map_err(|_| ApiError::AuthenticationFailed(None))?;
|
||||
let did = crate::auth::get_did_from_token(&extracted.token).ok();
|
||||
match state.session_repo.delete_session_by_access_jti(&jti).await {
|
||||
Ok(rows) if rows > 0 => {
|
||||
@@ -369,10 +365,10 @@ pub async fn delete_session(
|
||||
let session_cache_key = format!("auth:session:{}:{}", did, jti);
|
||||
let _ = state.cache.delete(&session_cache_key).await;
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
Ok(_) => ApiError::AuthenticationFailed(None).into_response(),
|
||||
Err(_) => ApiError::AuthenticationFailed(None).into_response(),
|
||||
Ok(_) => Err(ApiError::AuthenticationFailed(None)),
|
||||
Err(_) => Err(ApiError::AuthenticationFailed(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,29 +792,31 @@ pub struct ListSessionsOutput {
|
||||
pub async fn list_sessions(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: BearerAuth,
|
||||
) -> Response {
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let current_jti = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.and_then(|token| crate::auth::get_jti_from_token(token).ok());
|
||||
|
||||
let jwt_rows = match state.session_repo.list_sessions_by_did(&auth.0.did).await {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
let jwt_rows = state
|
||||
.session_repo
|
||||
.list_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching JWT sessions: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let oauth_rows = match state.oauth_repo.list_sessions_by_did(&auth.0.did).await {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
let oauth_rows = state
|
||||
.oauth_repo
|
||||
.list_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching OAuth sessions: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo {
|
||||
id: format!("jwt:{}", row.id),
|
||||
@@ -829,7 +827,7 @@ pub async fn list_sessions(
|
||||
is_current: current_jti.as_ref() == Some(&row.access_jti),
|
||||
});
|
||||
|
||||
let is_oauth = auth.0.is_oauth;
|
||||
let is_oauth = auth.is_oauth();
|
||||
let oauth_sessions = oauth_rows.into_iter().map(|row| {
|
||||
let client_name = extract_client_name(&row.client_id);
|
||||
let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str());
|
||||
@@ -846,7 +844,7 @@ pub async fn list_sessions(
|
||||
let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect();
|
||||
sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
(StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()
|
||||
Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response())
|
||||
}
|
||||
|
||||
fn extract_client_name(client_id: &str) -> String {
|
||||
@@ -867,106 +865,107 @@ pub struct RevokeSessionInput {
|
||||
|
||||
pub async fn revoke_session(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RevokeSessionInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") {
|
||||
let Ok(session_id) = jwt_id.parse::<i32>() else {
|
||||
return ApiError::InvalidRequest("Invalid session ID".into()).into_response();
|
||||
};
|
||||
let access_jti = match state
|
||||
let session_id: i32 = jwt_id
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?;
|
||||
let access_jti = state
|
||||
.session_repo
|
||||
.get_session_access_jti_by_id(session_id, &auth.0.did)
|
||||
.get_session_access_jti_by_id(session_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(jti)) => jti,
|
||||
Ok(None) => {
|
||||
return ApiError::SessionNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error in revoke_session: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if let Err(e) = state.session_repo.delete_session_by_id(session_id).await {
|
||||
error!("DB error deleting session: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let cache_key = format!("auth:session:{}:{}", &auth.0.did, access_jti);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::SessionNotFound)?;
|
||||
state
|
||||
.session_repo
|
||||
.delete_session_by_id(session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error deleting session: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let cache_key = format!("auth:session:{}:{}", &auth.did, access_jti);
|
||||
if let Err(e) = state.cache.delete(&cache_key).await {
|
||||
warn!("Failed to invalidate session cache: {:?}", e);
|
||||
}
|
||||
info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked");
|
||||
info!(did = %&auth.did, session_id = %session_id, "JWT session revoked");
|
||||
} else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") {
|
||||
let Ok(session_id) = oauth_id.parse::<i32>() else {
|
||||
return ApiError::InvalidRequest("Invalid session ID".into()).into_response();
|
||||
};
|
||||
match state
|
||||
let session_id: i32 = oauth_id
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?;
|
||||
let deleted = state
|
||||
.oauth_repo
|
||||
.delete_session_by_id(session_id, &auth.0.did)
|
||||
.delete_session_by_id(session_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(0) => {
|
||||
return ApiError::SessionNotFound.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error deleting OAuth session: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
_ => {}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
if deleted == 0 {
|
||||
return Err(ApiError::SessionNotFound);
|
||||
}
|
||||
info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked");
|
||||
info!(did = %&auth.did, session_id = %session_id, "OAuth session revoked");
|
||||
} else {
|
||||
return ApiError::InvalidRequest("Invalid session ID format".into()).into_response();
|
||||
return Err(ApiError::InvalidRequest("Invalid session ID format".into()));
|
||||
}
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
pub async fn revoke_all_sessions(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: BearerAuth,
|
||||
) -> Response {
|
||||
let current_jti = crate::auth::extract_auth_token_from_header(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let jti = crate::auth::extract_auth_token_from_header(
|
||||
headers.get("authorization").and_then(|v| v.to_str().ok()),
|
||||
)
|
||||
.and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok());
|
||||
.and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok())
|
||||
.ok_or(ApiError::InvalidToken(None))?;
|
||||
|
||||
let Some(ref jti) = current_jti else {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
};
|
||||
|
||||
if auth.0.is_oauth {
|
||||
if let Err(e) = state.session_repo.delete_sessions_by_did(&auth.0.did).await {
|
||||
error!("DB error revoking JWT sessions: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let jti_typed = TokenId::from(jti.clone());
|
||||
if let Err(e) = state
|
||||
.oauth_repo
|
||||
.delete_sessions_by_did_except(&auth.0.did, &jti_typed)
|
||||
.await
|
||||
{
|
||||
error!("DB error revoking OAuth sessions: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = state
|
||||
if auth.is_oauth() {
|
||||
state
|
||||
.session_repo
|
||||
.delete_sessions_by_did_except_jti(&auth.0.did, jti)
|
||||
.delete_sessions_by_did(&auth.did)
|
||||
.await
|
||||
{
|
||||
error!("DB error revoking JWT sessions: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
if let Err(e) = state.oauth_repo.delete_sessions_by_did(&auth.0.did).await {
|
||||
error!("DB error revoking OAuth sessions: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("DB error revoking JWT sessions: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let jti_typed = TokenId::from(jti.clone());
|
||||
state
|
||||
.oauth_repo
|
||||
.delete_sessions_by_did_except(&auth.did, &jti_typed)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error revoking OAuth sessions: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
} else {
|
||||
state
|
||||
.session_repo
|
||||
.delete_sessions_by_did_except_jti(&auth.did, &jti)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error revoking JWT sessions: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
state
|
||||
.oauth_repo
|
||||
.delete_sessions_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error revoking OAuth sessions: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
}
|
||||
|
||||
info!(did = %&auth.0.did, "All other sessions revoked");
|
||||
SuccessResponse::ok().into_response()
|
||||
info!(did = %&auth.did, "All other sessions revoked");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -978,20 +977,22 @@ pub struct LegacyLoginPreferenceOutput {
|
||||
|
||||
pub async fn get_legacy_login_preference(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
) -> Response {
|
||||
match state.user_repo.get_legacy_login_pref(&auth.0.did).await {
|
||||
Ok(Some(pref)) => Json(LegacyLoginPreferenceOutput {
|
||||
allow_legacy_login: pref.allow_legacy_login,
|
||||
has_mfa: pref.has_mfa,
|
||||
})
|
||||
.into_response(),
|
||||
Ok(None) => ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let pref = state
|
||||
.user_repo
|
||||
.get_legacy_login_pref(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
Ok(Json(LegacyLoginPreferenceOutput {
|
||||
allow_legacy_login: pref.allow_legacy_login,
|
||||
has_mfa: pref.has_mfa,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1002,51 +1003,48 @@ pub struct UpdateLegacyLoginInput {
|
||||
|
||||
pub async fn update_legacy_login_preference(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateLegacyLoginInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
) -> Result<Response, ApiError> {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
return Ok(crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await {
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await {
|
||||
return Ok(crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
match state
|
||||
let updated = state
|
||||
.user_repo
|
||||
.update_legacy_login(&auth.0.did, input.allow_legacy_login)
|
||||
.update_legacy_login(&auth.did, input.allow_legacy_login)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
info!(
|
||||
did = %&auth.0.did,
|
||||
allow_legacy_login = input.allow_legacy_login,
|
||||
"Legacy login preference updated"
|
||||
);
|
||||
Json(json!({
|
||||
"allowLegacyLogin": input.allow_legacy_login
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
Ok(false) => ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
if !updated {
|
||||
return Err(ApiError::AccountNotFound);
|
||||
}
|
||||
info!(
|
||||
did = %&auth.did,
|
||||
allow_legacy_login = input.allow_legacy_login,
|
||||
"Legacy login preference updated"
|
||||
);
|
||||
Ok(Json(json!({
|
||||
"allowLegacyLogin": input.allow_legacy_login
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
use crate::comms::VALID_LOCALES;
|
||||
@@ -1059,37 +1057,34 @@ pub struct UpdateLocaleInput {
|
||||
|
||||
pub async fn update_locale(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateLocaleInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) {
|
||||
return ApiError::InvalidRequest(format!(
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Invalid locale. Valid options: {}",
|
||||
VALID_LOCALES.join(", ")
|
||||
))
|
||||
.into_response();
|
||||
)));
|
||||
}
|
||||
|
||||
match state
|
||||
let updated = state
|
||||
.user_repo
|
||||
.update_locale(&auth.0.did, &input.preferred_locale)
|
||||
.update_locale(&auth.did, &input.preferred_locale)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
info!(
|
||||
did = %&auth.0.did,
|
||||
locale = %input.preferred_locale,
|
||||
"User locale preference updated"
|
||||
);
|
||||
Json(json!({
|
||||
"preferredLocale": input.preferred_locale
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
Ok(false) => ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error updating locale: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
if !updated {
|
||||
return Err(ApiError::AccountNotFound);
|
||||
}
|
||||
info!(
|
||||
did = %&auth.did,
|
||||
locale = %input.preferred_locale,
|
||||
"User locale preference updated"
|
||||
);
|
||||
Ok(Json(json!({
|
||||
"preferredLocale": input.preferred_locale
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::auth::{
|
||||
decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64,
|
||||
generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format,
|
||||
@@ -26,66 +26,63 @@ pub struct CreateTotpSecretResponse {
|
||||
pub qr_base64: String,
|
||||
}
|
||||
|
||||
pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
match state.user_repo.get_totp_record(&auth.0.did).await {
|
||||
Ok(Some(record)) if record.verified => return ApiError::TotpAlreadyEnabled.into_response(),
|
||||
pub async fn create_totp_secret(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
match state.user_repo.get_totp_record(&auth.did).await {
|
||||
Ok(Some(record)) if record.verified => return Err(ApiError::TotpAlreadyEnabled),
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("DB error checking TOTP: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
}
|
||||
|
||||
let secret = generate_totp_secret();
|
||||
|
||||
let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await {
|
||||
Ok(Some(h)) => h,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let handle = state
|
||||
.user_repo
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching handle: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let uri = generate_totp_uri(&secret, &handle, &hostname);
|
||||
|
||||
let qr_code = match generate_qr_png_base64(&secret, &handle, &hostname) {
|
||||
Ok(qr) => qr,
|
||||
Err(e) => {
|
||||
error!("Failed to generate QR code: {:?}", e);
|
||||
return ApiError::InternalError(Some("Failed to generate QR code".into()))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let qr_code = generate_qr_png_base64(&secret, &handle, &hostname).map_err(|e| {
|
||||
error!("Failed to generate QR code: {:?}", e);
|
||||
ApiError::InternalError(Some("Failed to generate QR code".into()))
|
||||
})?;
|
||||
|
||||
let encrypted_secret = match encrypt_totp_secret(&secret) {
|
||||
Ok(enc) => enc,
|
||||
Err(e) => {
|
||||
error!("Failed to encrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let encrypted_secret = encrypt_totp_secret(&secret).map_err(|e| {
|
||||
error!("Failed to encrypt TOTP secret: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.upsert_totp_secret(&auth.0.did, &encrypted_secret, ENCRYPTION_VERSION)
|
||||
.upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION)
|
||||
.await
|
||||
{
|
||||
error!("Failed to store TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to store TOTP secret: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret);
|
||||
|
||||
info!(did = %&auth.0.did, "TOTP secret created (pending verification)");
|
||||
info!(did = %&auth.did, "TOTP secret created (pending verification)");
|
||||
|
||||
Json(CreateTotpSecretResponse {
|
||||
Ok(Json(CreateTotpSecretResponse {
|
||||
secret: secret_base32,
|
||||
uri,
|
||||
qr_base64: qr_code,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -101,69 +98,68 @@ pub struct EnableTotpResponse {
|
||||
|
||||
pub async fn enable_totp(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<EnableTotpInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.did)
|
||||
.await
|
||||
{
|
||||
warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
warn!(did = %&auth.did, "TOTP verification rate limit exceeded");
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await {
|
||||
let totp_record = match state.user_repo.get_totp_record(&auth.did).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return ApiError::TotpNotEnabled.into_response(),
|
||||
Ok(None) => return Err(ApiError::TotpNotEnabled),
|
||||
Err(e) => {
|
||||
error!("DB error fetching TOTP: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
if totp_record.verified {
|
||||
return ApiError::TotpAlreadyEnabled.into_response();
|
||||
return Err(ApiError::TotpAlreadyEnabled);
|
||||
}
|
||||
|
||||
let secret = match decrypt_totp_secret(
|
||||
let secret = decrypt_totp_secret(
|
||||
&totp_record.secret_encrypted,
|
||||
totp_record.encryption_version,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let code = input.code.trim();
|
||||
if !verify_totp_code(&secret, code) {
|
||||
return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response();
|
||||
return Err(ApiError::InvalidCode(Some(
|
||||
"Invalid verification code".into(),
|
||||
)));
|
||||
}
|
||||
|
||||
let backup_codes = generate_backup_codes();
|
||||
let backup_hashes: Result<Vec<_>, _> =
|
||||
backup_codes.iter().map(|c| hash_backup_code(c)).collect();
|
||||
let backup_hashes = match backup_hashes {
|
||||
Ok(hashes) => hashes,
|
||||
Err(e) => {
|
||||
let backup_hashes: Vec<_> = backup_codes
|
||||
.iter()
|
||||
.map(|c| hash_backup_code(c))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash backup code: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.enable_totp_with_backup_codes(&auth.0.did, &backup_hashes)
|
||||
.enable_totp_with_backup_codes(&auth.did, &backup_hashes)
|
||||
.await
|
||||
{
|
||||
error!("Failed to enable TOTP: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to enable TOTP: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len());
|
||||
info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len());
|
||||
|
||||
Json(EnableTotpResponse { backup_codes }).into_response()
|
||||
Ok(Json(EnableTotpResponse { backup_codes }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -174,84 +170,84 @@ pub struct DisableTotpInput {
|
||||
|
||||
pub async fn disable_totp(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<DisableTotpInput>,
|
||||
) -> Response {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did)
|
||||
.await
|
||||
) -> Result<Response, ApiError> {
|
||||
if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await
|
||||
{
|
||||
return crate::api::server::reauth::legacy_mfa_required_response(
|
||||
return Ok(crate::api::server::reauth::legacy_mfa_required_response(
|
||||
&*state.user_repo,
|
||||
&*state.session_repo,
|
||||
&auth.0.did,
|
||||
&auth.did,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.did)
|
||||
.await
|
||||
{
|
||||
warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
warn!(did = %&auth.did, "TOTP verification rate limit exceeded");
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await {
|
||||
Ok(Some(hash)) => hash,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let password_hash = state
|
||||
.user_repo
|
||||
.get_password_hash_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false);
|
||||
if !password_valid {
|
||||
return ApiError::InvalidPassword("Password is incorrect".into()).into_response();
|
||||
return Err(ApiError::InvalidPassword("Password is incorrect".into()));
|
||||
}
|
||||
|
||||
let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await {
|
||||
let totp_record = match state.user_repo.get_totp_record(&auth.did).await {
|
||||
Ok(Some(row)) if row.verified => row,
|
||||
Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(),
|
||||
Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled),
|
||||
Err(e) => {
|
||||
error!("DB error fetching TOTP: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
let code = input.code.trim();
|
||||
let code_valid = if is_backup_code_format(code) {
|
||||
verify_backup_code_for_user(&state, &auth.0.did, code).await
|
||||
verify_backup_code_for_user(&state, &auth.did, code).await
|
||||
} else {
|
||||
let secret = match decrypt_totp_secret(
|
||||
let secret = decrypt_totp_secret(
|
||||
&totp_record.secret_encrypted,
|
||||
totp_record.encryption_version,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
verify_totp_code(&secret, code)
|
||||
};
|
||||
|
||||
if !code_valid {
|
||||
return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response();
|
||||
return Err(ApiError::InvalidCode(Some(
|
||||
"Invalid verification code".into(),
|
||||
)));
|
||||
}
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.delete_totp_and_backup_codes(&auth.0.did)
|
||||
.delete_totp_and_backup_codes(&auth.did)
|
||||
.await
|
||||
{
|
||||
error!("Failed to delete TOTP: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to delete TOTP: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.0.did, "TOTP disabled");
|
||||
info!(did = %&auth.did, "TOTP disabled");
|
||||
|
||||
EmptyResponse::ok().into_response()
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -262,30 +258,34 @@ pub struct GetTotpStatusResponse {
|
||||
pub backup_codes_remaining: i64,
|
||||
}
|
||||
|
||||
pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
let enabled = match state.user_repo.get_totp_record(&auth.0.did).await {
|
||||
pub async fn get_totp_status(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let enabled = match state.user_repo.get_totp_record(&auth.did).await {
|
||||
Ok(Some(row)) => row.verified,
|
||||
Ok(None) => false,
|
||||
Err(e) => {
|
||||
error!("DB error fetching TOTP status: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
let backup_count = match state.user_repo.count_unused_backup_codes(&auth.0.did).await {
|
||||
Ok(count) => count,
|
||||
Err(e) => {
|
||||
let backup_count = state
|
||||
.user_repo
|
||||
.count_unused_backup_codes(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error counting backup codes: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
Json(GetTotpStatusResponse {
|
||||
Ok(Json(GetTotpStatusResponse {
|
||||
enabled,
|
||||
has_backup_codes: backup_count > 0,
|
||||
backup_codes_remaining: backup_count,
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -302,79 +302,79 @@ pub struct RegenerateBackupCodesResponse {
|
||||
|
||||
pub async fn regenerate_backup_codes(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RegenerateBackupCodesInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
|
||||
.check_rate_limit(RateLimitKind::TotpVerify, &auth.did)
|
||||
.await
|
||||
{
|
||||
warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded");
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
warn!(did = %&auth.did, "TOTP verification rate limit exceeded");
|
||||
return Err(ApiError::RateLimitExceeded(None));
|
||||
}
|
||||
|
||||
let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await {
|
||||
Ok(Some(hash)) => hash,
|
||||
Ok(None) => return ApiError::AccountNotFound.into_response(),
|
||||
Err(e) => {
|
||||
let password_hash = state
|
||||
.user_repo
|
||||
.get_password_hash_by_did(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching user: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.ok_or(ApiError::AccountNotFound)?;
|
||||
|
||||
let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false);
|
||||
if !password_valid {
|
||||
return ApiError::InvalidPassword("Password is incorrect".into()).into_response();
|
||||
return Err(ApiError::InvalidPassword("Password is incorrect".into()));
|
||||
}
|
||||
|
||||
let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await {
|
||||
let totp_record = match state.user_repo.get_totp_record(&auth.did).await {
|
||||
Ok(Some(row)) if row.verified => row,
|
||||
Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(),
|
||||
Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled),
|
||||
Err(e) => {
|
||||
error!("DB error fetching TOTP: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
let secret = match decrypt_totp_secret(
|
||||
let secret = decrypt_totp_secret(
|
||||
&totp_record.secret_encrypted,
|
||||
totp_record.encryption_version,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt TOTP secret: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let code = input.code.trim();
|
||||
if !verify_totp_code(&secret, code) {
|
||||
return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response();
|
||||
return Err(ApiError::InvalidCode(Some(
|
||||
"Invalid verification code".into(),
|
||||
)));
|
||||
}
|
||||
|
||||
let backup_codes = generate_backup_codes();
|
||||
let backup_hashes: Result<Vec<_>, _> =
|
||||
backup_codes.iter().map(|c| hash_backup_code(c)).collect();
|
||||
let backup_hashes = match backup_hashes {
|
||||
Ok(hashes) => hashes,
|
||||
Err(e) => {
|
||||
let backup_hashes: Vec<_> = backup_codes
|
||||
.iter()
|
||||
.map(|c| hash_backup_code(c))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash backup code: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
if let Err(e) = state
|
||||
state
|
||||
.user_repo
|
||||
.replace_backup_codes(&auth.0.did, &backup_hashes)
|
||||
.replace_backup_codes(&auth.did, &backup_hashes)
|
||||
.await
|
||||
{
|
||||
error!("Failed to regenerate backup codes: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Failed to regenerate backup codes: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.0.did, "Backup codes regenerated");
|
||||
info!(did = %&auth.did, "Backup codes regenerated");
|
||||
|
||||
Json(RegenerateBackupCodesResponse { backup_codes }).into_response()
|
||||
Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response())
|
||||
}
|
||||
|
||||
async fn verify_backup_code_for_user(
|
||||
|
||||
@@ -11,7 +11,7 @@ use tracing::{error, info};
|
||||
use tranquil_db_traits::OAuthRepository;
|
||||
use tranquil_types::DeviceId;
|
||||
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
|
||||
const TRUST_DURATION_DAYS: i64 = 30;
|
||||
@@ -71,32 +71,36 @@ pub struct ListTrustedDevicesResponse {
|
||||
pub devices: Vec<TrustedDevice>,
|
||||
}
|
||||
|
||||
pub async fn list_trusted_devices(State(state): State<AppState>, auth: BearerAuth) -> Response {
|
||||
match state.oauth_repo.list_trusted_devices(&auth.0.did).await {
|
||||
Ok(rows) => {
|
||||
let devices = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let trust_state =
|
||||
DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until);
|
||||
TrustedDevice {
|
||||
id: row.id,
|
||||
user_agent: row.user_agent,
|
||||
friendly_name: row.friendly_name,
|
||||
trusted_at: row.trusted_at,
|
||||
trusted_until: row.trusted_until,
|
||||
last_seen_at: row.last_seen_at,
|
||||
trust_state,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Json(ListTrustedDevicesResponse { devices }).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
pub async fn list_trusted_devices(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let rows = state
|
||||
.oauth_repo
|
||||
.list_trusted_devices(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let devices = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until);
|
||||
TrustedDevice {
|
||||
id: row.id,
|
||||
user_agent: row.user_agent,
|
||||
friendly_name: row.friendly_name,
|
||||
trusted_at: row.trusted_at,
|
||||
trusted_until: row.trusted_until,
|
||||
last_seen_at: row.last_seen_at,
|
||||
trust_state,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(ListTrustedDevicesResponse { devices }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -107,35 +111,36 @@ pub struct RevokeTrustedDeviceInput {
|
||||
|
||||
pub async fn revoke_trusted_device(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<RevokeTrustedDeviceInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let device_id = DeviceId::from(input.device_id.clone());
|
||||
match state
|
||||
.oauth_repo
|
||||
.device_belongs_to_user(&device_id, &auth.0.did)
|
||||
.device_belongs_to_user(&device_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
return ApiError::DeviceNotFound.into_response();
|
||||
return Err(ApiError::DeviceNotFound);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
}
|
||||
|
||||
match state.oauth_repo.revoke_device_trust(&device_id).await {
|
||||
Ok(()) => {
|
||||
info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked");
|
||||
SuccessResponse::ok().into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
state
|
||||
.oauth_repo
|
||||
.revoke_device_trust(&device_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -147,39 +152,36 @@ pub struct UpdateTrustedDeviceInput {
|
||||
|
||||
pub async fn update_trusted_device(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<UpdateTrustedDeviceInput>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ApiError> {
|
||||
let device_id = DeviceId::from(input.device_id.clone());
|
||||
match state
|
||||
.oauth_repo
|
||||
.device_belongs_to_user(&device_id, &auth.0.did)
|
||||
.device_belongs_to_user(&device_id, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
return ApiError::DeviceNotFound.into_response();
|
||||
return Err(ApiError::DeviceNotFound);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
}
|
||||
|
||||
match state
|
||||
state
|
||||
.oauth_repo
|
||||
.update_device_friendly_name(&device_id, input.friendly_name.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated");
|
||||
SuccessResponse::ok().into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
.map_err(|e| {
|
||||
error!("DB error: {:?}", e);
|
||||
ApiError::InternalError(None).into_response()
|
||||
}
|
||||
}
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated");
|
||||
Ok(SuccessResponse::ok().into_response())
|
||||
}
|
||||
|
||||
pub async fn get_device_trust_state(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{BearerAuth, OptionalBearerAuth};
|
||||
use crate::auth::{Active, Auth, Permissive};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -21,9 +21,9 @@ pub struct CheckSignupQueueOutput {
|
||||
pub estimated_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn check_signup_queue(auth: OptionalBearerAuth) -> Response {
|
||||
if let Some(user) = auth.0
|
||||
&& user.is_oauth
|
||||
pub async fn check_signup_queue(auth: Option<Auth<Permissive>>) -> Response {
|
||||
if let Some(ref user) = auth
|
||||
&& user.is_oauth()
|
||||
{
|
||||
return ApiError::Forbidden.into_response();
|
||||
}
|
||||
@@ -49,11 +49,9 @@ pub struct DereferenceScopeOutput {
|
||||
|
||||
pub async fn dereference_scope(
|
||||
State(state): State<AppState>,
|
||||
auth: BearerAuth,
|
||||
_auth: Auth<Active>,
|
||||
Json(input): Json<DereferenceScopeInput>,
|
||||
) -> Response {
|
||||
let _ = auth;
|
||||
|
||||
) -> Result<Response, ApiError> {
|
||||
let scope_parts: Vec<&str> = input.scope.split_whitespace().collect();
|
||||
let mut resolved_scopes: Vec<String> = Vec::new();
|
||||
|
||||
@@ -118,8 +116,8 @@ pub async fn dereference_scope(
|
||||
}
|
||||
}
|
||||
|
||||
Json(DereferenceScopeOutput {
|
||||
Ok(Json(DereferenceScopeOutput {
|
||||
scope: resolved_scopes.join(" "),
|
||||
})
|
||||
.into_response()
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ async fn test_oauth_token_works_with_bearer_auth() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with BearerAuth extractor");
|
||||
assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with RequiredAuth extractor");
|
||||
let body: Value = res.json().await.unwrap();
|
||||
assert_eq!(body["did"].as_str().unwrap(), did);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
extract::{FromRequestParts, OptionalFromRequestParts},
|
||||
http::{StatusCode, header::AUTHORIZATION, request::Parts},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use super::{
|
||||
AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token,
|
||||
validate_bearer_token, validate_bearer_token_allow_deactivated,
|
||||
validate_bearer_token_allow_takendown,
|
||||
AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier,
|
||||
is_service_token, validate_bearer_token_for_service_auth,
|
||||
};
|
||||
use crate::api::error::ApiError;
|
||||
use crate::oauth::scopes::{RepoAction, ScopePermissions};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use crate::util::build_full_url;
|
||||
|
||||
pub struct BearerAuth(pub AuthenticatedUser);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthError {
|
||||
MissingToken,
|
||||
@@ -26,6 +26,9 @@ pub enum AuthError {
|
||||
AccountDeactivated,
|
||||
AccountTakedown,
|
||||
AdminRequired,
|
||||
ServiceAuthNotAllowed,
|
||||
SigningKeyRequired,
|
||||
InsufficientScope(String),
|
||||
OAuthExpiredToken(String),
|
||||
UseDpopNonce(String),
|
||||
InvalidDpopProof(String),
|
||||
@@ -56,30 +59,15 @@ impl IntoResponse for AuthError {
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Self::InsufficientScope(msg) => ApiError::InsufficientScope(Some(msg)).into_response(),
|
||||
other => ApiError::from(other).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> {
|
||||
let auth_header = auth_header.trim();
|
||||
|
||||
if auth_header.len() < 8 {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let prefix = &auth_header[..7];
|
||||
if !prefix.eq_ignore_ascii_case("bearer ") {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let token = auth_header[7..].trim();
|
||||
if token.is_empty() {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
pub struct ExtractedToken {
|
||||
pub token: String,
|
||||
pub is_dpop: bool,
|
||||
}
|
||||
|
||||
pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> {
|
||||
@@ -102,11 +90,6 @@ pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<Str
|
||||
Some(token.to_string())
|
||||
}
|
||||
|
||||
pub struct ExtractedToken {
|
||||
pub token: String,
|
||||
pub is_dpop: bool,
|
||||
}
|
||||
|
||||
pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> {
|
||||
let header = auth_header?;
|
||||
let header = header.trim();
|
||||
@@ -136,10 +119,92 @@ pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<Extra
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StatusCheckFlags {
|
||||
allow_deactivated: bool,
|
||||
allow_takendown: bool,
|
||||
pub trait AuthPolicy: Send + Sync + 'static {
|
||||
fn validate(user: &AuthenticatedUser) -> Result<(), AuthError>;
|
||||
}
|
||||
|
||||
pub struct Permissive;
|
||||
|
||||
impl AuthPolicy for Permissive {
|
||||
fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Active;
|
||||
|
||||
impl AuthPolicy for Active {
|
||||
fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> {
|
||||
if user.status.is_deactivated() {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
if user.status.is_takendown() {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotTakendown;
|
||||
|
||||
impl AuthPolicy for NotTakendown {
|
||||
fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> {
|
||||
if user.status.is_takendown() {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyUser;
|
||||
|
||||
impl AuthPolicy for AnyUser {
|
||||
fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Admin;
|
||||
|
||||
impl AuthPolicy for Admin {
|
||||
fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> {
|
||||
if user.status.is_deactivated() {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
if user.status.is_takendown() {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
if !user.is_admin {
|
||||
return Err(AuthError::AdminRequired);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticatedUser {
|
||||
pub fn require_active(&self) -> Result<&Self, ApiError> {
|
||||
if self.status.is_deactivated() {
|
||||
return Err(ApiError::AccountDeactivated);
|
||||
}
|
||||
if self.status.is_takendown() {
|
||||
return Err(ApiError::AccountTakedown);
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn require_not_takendown(&self) -> Result<&Self, ApiError> {
|
||||
if self.status.is_takendown() {
|
||||
return Err(ApiError::AccountTakedown);
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn require_admin(&self) -> Result<&Self, ApiError> {
|
||||
if !self.is_admin {
|
||||
return Err(ApiError::AdminRequired);
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_oauth_token_and_build_user(
|
||||
@@ -148,7 +213,6 @@ async fn verify_oauth_token_and_build_user(
|
||||
dpop_proof: Option<&str>,
|
||||
method: &str,
|
||||
uri: &str,
|
||||
flags: StatusCheckFlags,
|
||||
) -> Result<AuthenticatedUser, AuthError> {
|
||||
match crate::oauth::verify::verify_oauth_access_token(
|
||||
state.oauth_repo.as_ref(),
|
||||
@@ -171,22 +235,16 @@ async fn verify_oauth_token_and_build_user(
|
||||
user_info.takedown_ref.as_deref(),
|
||||
user_info.deactivated_at,
|
||||
);
|
||||
if !flags.allow_deactivated && status.is_deactivated() {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
if !flags.allow_takendown && status.is_takendown() {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
Ok(AuthenticatedUser {
|
||||
did: result.did,
|
||||
key_bytes: user_info.key_bytes.and_then(|kb| {
|
||||
crate::config::decrypt_key(&kb, user_info.encryption_version).ok()
|
||||
}),
|
||||
is_oauth: true,
|
||||
is_admin: user_info.is_admin,
|
||||
status,
|
||||
scope: result.scope,
|
||||
controller_did: None,
|
||||
auth_source: AuthSource::OAuth,
|
||||
})
|
||||
}
|
||||
Err(crate::oauth::OAuthError::ExpiredToken(msg)) => Err(AuthError::OAuthExpiredToken(msg)),
|
||||
@@ -198,302 +256,158 @@ async fn verify_oauth_token_and_build_user(
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequestParts<AppState> for BearerAuth {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let method = parts.method.as_str();
|
||||
let uri = build_full_url(&parts.uri.to_string());
|
||||
|
||||
match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await {
|
||||
Ok(user) if !user.is_oauth => {
|
||||
return if user.status.is_deactivated() {
|
||||
Err(AuthError::AccountDeactivated)
|
||||
} else if user.status.is_takendown() {
|
||||
Err(AuthError::AccountTakedown)
|
||||
} else {
|
||||
Ok(BearerAuth(user))
|
||||
};
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(super::TokenValidationError::AccountDeactivated) => {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
Err(super::TokenValidationError::AccountTakedown) => {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
Err(super::TokenValidationError::TokenExpired) => {
|
||||
info!("JWT access token expired in BearerAuth, returning ExpiredToken");
|
||||
return Err(AuthError::TokenExpired);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
verify_oauth_token_and_build_user(
|
||||
state,
|
||||
&extracted.token,
|
||||
dpop_proof,
|
||||
method,
|
||||
&uri,
|
||||
StatusCheckFlags::default(),
|
||||
)
|
||||
async fn verify_service_token(token: &str) -> Result<ServiceTokenClaims, AuthError> {
|
||||
let verifier = ServiceTokenVerifier::new();
|
||||
let claims = verifier
|
||||
.verify_service_token(token, None)
|
||||
.await
|
||||
.map(BearerAuth)
|
||||
}
|
||||
.map_err(|e| {
|
||||
error!("Service token verification failed: {:?}", e);
|
||||
AuthError::AuthenticationFailed
|
||||
})?;
|
||||
|
||||
debug!("Service token verified for DID: {}", claims.iss);
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
pub struct BearerAuthAllowDeactivated(pub AuthenticatedUser);
|
||||
|
||||
impl FromRequestParts<AppState> for BearerAuthAllowDeactivated {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let method = parts.method.as_str();
|
||||
let uri = build_full_url(&parts.uri.to_string());
|
||||
|
||||
match validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token)
|
||||
.await
|
||||
{
|
||||
Ok(user) if !user.is_oauth => {
|
||||
return if user.status.is_takendown() {
|
||||
Err(AuthError::AccountTakedown)
|
||||
} else {
|
||||
Ok(BearerAuthAllowDeactivated(user))
|
||||
};
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(super::TokenValidationError::AccountTakedown) => {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
Err(super::TokenValidationError::TokenExpired) => {
|
||||
return Err(AuthError::TokenExpired);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
verify_oauth_token_and_build_user(
|
||||
state,
|
||||
&extracted.token,
|
||||
dpop_proof,
|
||||
method,
|
||||
&uri,
|
||||
StatusCheckFlags {
|
||||
allow_deactivated: true,
|
||||
allow_takendown: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(BearerAuthAllowDeactivated)
|
||||
}
|
||||
enum ExtractedAuth {
|
||||
User(AuthenticatedUser),
|
||||
Service(ServiceTokenClaims),
|
||||
}
|
||||
|
||||
pub struct BearerAuthAllowTakendown(pub AuthenticatedUser);
|
||||
async fn extract_auth_internal(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<ExtractedAuth, AuthError> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
impl FromRequestParts<AppState> for BearerAuthAllowTakendown {
|
||||
type Rejection = AuthError;
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let method = parts.method.as_str();
|
||||
let uri = build_full_url(&parts.uri.to_string());
|
||||
|
||||
match validate_bearer_token_allow_takendown(state.user_repo.as_ref(), &extracted.token)
|
||||
.await
|
||||
{
|
||||
Ok(user) if !user.is_oauth => {
|
||||
return if user.status.is_deactivated() {
|
||||
Err(AuthError::AccountDeactivated)
|
||||
} else {
|
||||
Ok(BearerAuthAllowTakendown(user))
|
||||
};
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(super::TokenValidationError::AccountDeactivated) => {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
Err(super::TokenValidationError::TokenExpired) => {
|
||||
return Err(AuthError::TokenExpired);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
verify_oauth_token_and_build_user(
|
||||
state,
|
||||
&extracted.token,
|
||||
dpop_proof,
|
||||
method,
|
||||
&uri,
|
||||
StatusCheckFlags {
|
||||
allow_deactivated: false,
|
||||
allow_takendown: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(BearerAuthAllowTakendown)
|
||||
if is_service_token(&extracted.token) {
|
||||
let claims = verify_service_token(&extracted.token).await?;
|
||||
return Ok(ExtractedAuth::Service(claims));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BearerAuthAdmin(pub AuthenticatedUser);
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let method = parts.method.as_str();
|
||||
let uri = build_full_url(&parts.uri.to_string());
|
||||
|
||||
impl FromRequestParts<AppState> for BearerAuthAdmin {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let method = parts.method.as_str();
|
||||
let uri = build_full_url(&parts.uri.to_string());
|
||||
|
||||
match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await {
|
||||
Ok(user) if !user.is_oauth => {
|
||||
if user.status.is_deactivated() {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
if user.status.is_takendown() {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
if !user.is_admin {
|
||||
return Err(AuthError::AdminRequired);
|
||||
}
|
||||
return Ok(BearerAuthAdmin(user));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(super::TokenValidationError::AccountDeactivated) => {
|
||||
return Err(AuthError::AccountDeactivated);
|
||||
}
|
||||
Err(super::TokenValidationError::AccountTakedown) => {
|
||||
return Err(AuthError::AccountTakedown);
|
||||
}
|
||||
Err(super::TokenValidationError::TokenExpired) => {
|
||||
return Err(AuthError::TokenExpired);
|
||||
}
|
||||
Err(_) => {}
|
||||
match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await {
|
||||
Ok(user) if !user.auth_source.is_oauth() => {
|
||||
return Ok(ExtractedAuth::User(user));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(super::TokenValidationError::TokenExpired) => {
|
||||
info!("JWT access token expired, returning ExpiredToken");
|
||||
return Err(AuthError::TokenExpired);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
let user = verify_oauth_token_and_build_user(
|
||||
state,
|
||||
&extracted.token,
|
||||
dpop_proof,
|
||||
method,
|
||||
&uri,
|
||||
StatusCheckFlags::default(),
|
||||
)
|
||||
let user = verify_oauth_token_and_build_user(state, &extracted.token, dpop_proof, method, &uri)
|
||||
.await?;
|
||||
Ok(ExtractedAuth::User(user))
|
||||
}
|
||||
|
||||
if !user.is_admin {
|
||||
return Err(AuthError::AdminRequired);
|
||||
}
|
||||
Ok(BearerAuthAdmin(user))
|
||||
async fn extract_user_auth_internal(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<AuthenticatedUser, AuthError> {
|
||||
match extract_auth_internal(parts, state).await? {
|
||||
ExtractedAuth::User(user) => Ok(user),
|
||||
ExtractedAuth::Service(_) => Err(AuthError::ServiceAuthNotAllowed),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OptionalBearerAuth(pub Option<AuthenticatedUser>);
|
||||
pub struct Auth<P: AuthPolicy = Active>(pub AuthenticatedUser, PhantomData<P>);
|
||||
|
||||
impl FromRequestParts<AppState> for OptionalBearerAuth {
|
||||
impl<P: AuthPolicy> Auth<P> {
|
||||
pub fn into_inner(self) -> AuthenticatedUser {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn needs_scope_check(&self) -> bool {
|
||||
self.0.is_oauth()
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> ScopePermissions {
|
||||
self.0.permissions()
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> {
|
||||
if !self.needs_scope_check() {
|
||||
return Ok(());
|
||||
}
|
||||
self.permissions()
|
||||
.assert_repo(action, collection)
|
||||
.map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AuthPolicy> std::ops::Deref for Auth<P> {
|
||||
type Target = AuthenticatedUser;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AuthPolicy> FromRequestParts<AppState> for Auth<P> {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = match parts.headers.get(AUTHORIZATION) {
|
||||
Some(h) => match h.to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(OptionalBearerAuth(None)),
|
||||
},
|
||||
None => return Ok(OptionalBearerAuth(None)),
|
||||
};
|
||||
let user = extract_user_auth_internal(parts, state).await?;
|
||||
P::validate(&user)?;
|
||||
Ok(Auth(user, PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
let extracted = match extract_auth_token_from_header(Some(auth_header)) {
|
||||
Some(e) => e,
|
||||
None => return Ok(OptionalBearerAuth(None)),
|
||||
};
|
||||
impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for Auth<P> {
|
||||
type Rejection = AuthError;
|
||||
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let method = parts.method.as_str();
|
||||
let uri = build_full_url(&parts.uri.to_string());
|
||||
|
||||
if let Ok(user) = validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await
|
||||
&& !user.is_oauth
|
||||
{
|
||||
return if user.status.is_deactivated() || user.status.is_takendown() {
|
||||
Ok(OptionalBearerAuth(None))
|
||||
} else {
|
||||
Ok(OptionalBearerAuth(Some(user)))
|
||||
};
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
match extract_user_auth_internal(parts, state).await {
|
||||
Ok(user) => {
|
||||
P::validate(&user)?;
|
||||
Ok(Some(Auth(user, PhantomData)))
|
||||
}
|
||||
Err(AuthError::MissingToken) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
|
||||
Ok(OptionalBearerAuth(
|
||||
verify_oauth_token_and_build_user(
|
||||
state,
|
||||
&extracted.token,
|
||||
dpop_proof,
|
||||
method,
|
||||
&uri,
|
||||
StatusCheckFlags::default(),
|
||||
)
|
||||
.await
|
||||
.ok(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServiceAuth {
|
||||
pub claims: ServiceTokenClaims,
|
||||
pub did: Did,
|
||||
pub claims: ServiceTokenClaims,
|
||||
}
|
||||
|
||||
impl ServiceAuth {
|
||||
pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> {
|
||||
match &self.claims.lxm {
|
||||
Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()),
|
||||
Some(lxm) => Err(ApiError::AuthorizationError(format!(
|
||||
"Token lxm '{}' does not permit '{}'",
|
||||
lxm, expected_lxm
|
||||
))),
|
||||
None => Err(ApiError::AuthorizationError(
|
||||
"Token missing lxm claim".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequestParts<AppState> for ServiceAuth {
|
||||
@@ -501,157 +415,209 @@ impl FromRequestParts<AppState> for ServiceAuth {
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
_state: &AppState,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
if !is_service_token(&extracted.token) {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let verifier = ServiceTokenVerifier::new();
|
||||
let claims = verifier
|
||||
.verify_service_token(&extracted.token, None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Service token verification failed: {:?}", e);
|
||||
AuthError::AuthenticationFailed
|
||||
})?;
|
||||
|
||||
let did: Did = claims
|
||||
.iss
|
||||
.parse()
|
||||
.map_err(|_| AuthError::AuthenticationFailed)?;
|
||||
|
||||
debug!("Service token verified for DID: {}", did);
|
||||
|
||||
Ok(ServiceAuth { claims, did })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OptionalServiceAuth(pub Option<ServiceTokenClaims>);
|
||||
|
||||
impl FromRequestParts<AppState> for OptionalServiceAuth {
|
||||
type Rejection = std::convert::Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
_state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = match parts.headers.get(AUTHORIZATION) {
|
||||
Some(h) => match h.to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(OptionalServiceAuth(None)),
|
||||
},
|
||||
None => return Ok(OptionalServiceAuth(None)),
|
||||
};
|
||||
|
||||
let extracted = match extract_auth_token_from_header(Some(auth_header)) {
|
||||
Some(e) => e,
|
||||
None => return Ok(OptionalServiceAuth(None)),
|
||||
};
|
||||
|
||||
if !is_service_token(&extracted.token) {
|
||||
return Ok(OptionalServiceAuth(None));
|
||||
}
|
||||
|
||||
let verifier = ServiceTokenVerifier::new();
|
||||
match verifier.verify_service_token(&extracted.token, None).await {
|
||||
Ok(claims) => {
|
||||
debug!("Service token verified for DID: {}", claims.iss);
|
||||
Ok(OptionalServiceAuth(Some(claims)))
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Service token verification failed (optional): {:?}", e);
|
||||
Ok(OptionalServiceAuth(None))
|
||||
match extract_auth_internal(parts, state).await? {
|
||||
ExtractedAuth::Service(claims) => {
|
||||
let did: Did = claims
|
||||
.iss
|
||||
.parse()
|
||||
.map_err(|_| AuthError::AuthenticationFailed)?;
|
||||
Ok(ServiceAuth { did, claims })
|
||||
}
|
||||
ExtractedAuth::User(_) => Err(AuthError::AuthenticationFailed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BlobAuthResult {
|
||||
Service { did: Did },
|
||||
User(AuthenticatedUser),
|
||||
pub enum AuthAny<P: AuthPolicy = Active> {
|
||||
User(Auth<P>),
|
||||
Service(ServiceAuth),
|
||||
}
|
||||
|
||||
pub struct BlobAuth(pub BlobAuthResult);
|
||||
impl<P: AuthPolicy> AuthAny<P> {
|
||||
pub fn did(&self) -> &Did {
|
||||
match self {
|
||||
Self::User(auth) => &auth.did,
|
||||
Self::Service(auth) => &auth.did,
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequestParts<AppState> for BlobAuth {
|
||||
pub fn as_user(&self) -> Option<&Auth<P>> {
|
||||
match self {
|
||||
Self::User(auth) => Some(auth),
|
||||
Self::Service(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_service(&self) -> Option<&ServiceAuth> {
|
||||
match self {
|
||||
Self::User(_) => None,
|
||||
Self::Service(auth) => Some(auth),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_service(&self) -> bool {
|
||||
matches!(self, Self::Service(_))
|
||||
}
|
||||
|
||||
pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> {
|
||||
match self {
|
||||
Self::User(_) => Ok(()),
|
||||
Self::Service(auth) => auth.require_lxm(expected_lxm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AuthPolicy> FromRequestParts<AppState> for AuthAny<P> {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let extracted =
|
||||
extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?;
|
||||
|
||||
if is_service_token(&extracted.token) {
|
||||
debug!("Verifying service token for blob upload");
|
||||
let verifier = ServiceTokenVerifier::new();
|
||||
let claims = verifier
|
||||
.verify_service_token(&extracted.token, Some("com.atproto.repo.uploadBlob"))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Service token verification failed: {:?}", e);
|
||||
AuthError::AuthenticationFailed
|
||||
})?;
|
||||
|
||||
let did: Did = claims
|
||||
.iss
|
||||
.parse()
|
||||
.map_err(|_| AuthError::AuthenticationFailed)?;
|
||||
|
||||
debug!("Service token verified for DID: {}", did);
|
||||
return Ok(BlobAuth(BlobAuthResult::Service { did }));
|
||||
match extract_auth_internal(parts, state).await? {
|
||||
ExtractedAuth::User(user) => {
|
||||
P::validate(&user)?;
|
||||
Ok(AuthAny::User(Auth(user, PhantomData)))
|
||||
}
|
||||
ExtractedAuth::Service(claims) => {
|
||||
let did: Did = claims
|
||||
.iss
|
||||
.parse()
|
||||
.map_err(|_| AuthError::AuthenticationFailed)?;
|
||||
Ok(AuthAny::Service(ServiceAuth { did, claims }))
|
||||
}
|
||||
}
|
||||
|
||||
let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok());
|
||||
let uri = build_full_url("/xrpc/com.atproto.repo.uploadBlob");
|
||||
|
||||
if let Ok(user) =
|
||||
validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token)
|
||||
.await
|
||||
&& !user.is_oauth
|
||||
{
|
||||
return if user.status.is_takendown() {
|
||||
Err(AuthError::AccountTakedown)
|
||||
} else {
|
||||
Ok(BlobAuth(BlobAuthResult::User(user)))
|
||||
};
|
||||
}
|
||||
|
||||
verify_oauth_token_and_build_user(
|
||||
state,
|
||||
&extracted.token,
|
||||
dpop_proof,
|
||||
"POST",
|
||||
&uri,
|
||||
StatusCheckFlags {
|
||||
allow_deactivated: true,
|
||||
allow_takendown: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|user| BlobAuth(BlobAuthResult::User(user)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for AuthAny<P> {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
match extract_auth_internal(parts, state).await {
|
||||
Ok(ExtractedAuth::User(user)) => {
|
||||
P::validate(&user)?;
|
||||
Ok(Some(AuthAny::User(Auth(user, PhantomData))))
|
||||
}
|
||||
Ok(ExtractedAuth::Service(claims)) => {
|
||||
let did: Did = claims
|
||||
.iss
|
||||
.parse()
|
||||
.map_err(|_| AuthError::AuthenticationFailed)?;
|
||||
Ok(Some(AuthAny::Service(ServiceAuth { did, claims })))
|
||||
}
|
||||
Err(AuthError::MissingToken) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SigningAuth<P: AuthPolicy = Active> {
|
||||
pub did: Did,
|
||||
pub key_bytes: Vec<u8>,
|
||||
pub is_admin: bool,
|
||||
pub status: AccountStatus,
|
||||
pub scope: Option<String>,
|
||||
pub controller_did: Option<Did>,
|
||||
is_oauth: bool,
|
||||
_policy: PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<P: AuthPolicy> SigningAuth<P> {
|
||||
pub fn needs_scope_check(&self) -> bool {
|
||||
self.is_oauth
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> ScopePermissions {
|
||||
if let Some(ref scope) = self.scope
|
||||
&& scope != super::SCOPE_ACCESS
|
||||
{
|
||||
return ScopePermissions::from_scope_string(Some(scope));
|
||||
}
|
||||
if !self.is_oauth {
|
||||
return ScopePermissions::from_scope_string(Some("atproto"));
|
||||
}
|
||||
ScopePermissions::from_scope_string(self.scope.as_deref())
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> {
|
||||
if !self.needs_scope_check() {
|
||||
return Ok(());
|
||||
}
|
||||
self.permissions()
|
||||
.assert_repo(action, collection)
|
||||
.map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AuthPolicy> FromRequestParts<AppState> for SigningAuth<P> {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let user = extract_user_auth_internal(parts, state).await?;
|
||||
P::validate(&user)?;
|
||||
|
||||
let key_bytes = match user.key_bytes {
|
||||
Some(kb) => kb,
|
||||
None => {
|
||||
let user_with_key = state
|
||||
.user_repo
|
||||
.get_with_key_by_did(&user.did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or(AuthError::SigningKeyRequired)?;
|
||||
crate::config::decrypt_key(
|
||||
&user_with_key.key_bytes,
|
||||
user_with_key.encryption_version,
|
||||
)
|
||||
.map_err(|_| AuthError::SigningKeyRequired)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(SigningAuth {
|
||||
did: user.did,
|
||||
key_bytes,
|
||||
is_admin: user.is_admin,
|
||||
status: user.status,
|
||||
scope: user.scope,
|
||||
controller_did: user.controller_did,
|
||||
is_oauth: user.auth_source.is_oauth(),
|
||||
_policy: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> {
|
||||
let auth_header = auth_header.trim();
|
||||
|
||||
if auth_header.len() < 8 {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let prefix = &auth_header[..7];
|
||||
if !prefix.eq_ignore_ascii_case("bearer ") {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let token = auth_header[7..].trim();
|
||||
if token.is_empty() {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::AccountStatus;
|
||||
use crate::api::ApiError;
|
||||
use crate::cache::Cache;
|
||||
use crate::oauth::scopes::ScopePermissions;
|
||||
use crate::types::Did;
|
||||
@@ -16,9 +17,9 @@ pub mod verification_token;
|
||||
pub mod webauthn;
|
||||
|
||||
pub use extractor::{
|
||||
AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult,
|
||||
ExtractedToken, OptionalBearerAuth, OptionalServiceAuth, ServiceAuth,
|
||||
extract_auth_token_from_header, extract_bearer_token_from_header,
|
||||
Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown,
|
||||
Permissive, ServiceAuth, SigningAuth, extract_auth_token_from_header,
|
||||
extract_bearer_token_from_header,
|
||||
};
|
||||
pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token};
|
||||
|
||||
@@ -94,14 +95,80 @@ impl fmt::Display for TokenValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AuthSource {
|
||||
Session,
|
||||
OAuth,
|
||||
Service { claims: ServiceTokenClaims },
|
||||
}
|
||||
|
||||
impl AuthSource {
|
||||
pub fn is_oauth(&self) -> bool {
|
||||
matches!(self, Self::OAuth)
|
||||
}
|
||||
|
||||
pub fn is_service(&self) -> bool {
|
||||
matches!(self, Self::Service { .. })
|
||||
}
|
||||
|
||||
pub fn service_claims(&self) -> Option<&ServiceTokenClaims> {
|
||||
match self {
|
||||
Self::Service { claims } => Some(claims),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthenticatedUser {
|
||||
pub did: Did,
|
||||
pub key_bytes: Option<Vec<u8>>,
|
||||
pub is_oauth: bool,
|
||||
pub is_admin: bool,
|
||||
pub status: AccountStatus,
|
||||
pub scope: Option<String>,
|
||||
pub controller_did: Option<Did>,
|
||||
pub auth_source: AuthSource,
|
||||
}
|
||||
|
||||
impl AuthenticatedUser {
|
||||
pub fn is_oauth(&self) -> bool {
|
||||
self.auth_source.is_oauth()
|
||||
}
|
||||
|
||||
pub fn is_service(&self) -> bool {
|
||||
self.auth_source.is_service()
|
||||
}
|
||||
|
||||
pub fn service_claims(&self) -> Option<&ServiceTokenClaims> {
|
||||
self.auth_source.service_claims()
|
||||
}
|
||||
|
||||
pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> {
|
||||
match self.auth_source.service_claims() {
|
||||
Some(claims) => match &claims.lxm {
|
||||
Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()),
|
||||
Some(lxm) => Err(ApiError::AuthorizationError(format!(
|
||||
"Token lxm '{}' does not permit '{}'",
|
||||
lxm, expected_lxm
|
||||
))),
|
||||
None => Err(ApiError::AuthorizationError(
|
||||
"Token missing lxm claim".to_string(),
|
||||
)),
|
||||
},
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn require_user(&self) -> Result<&Self, ApiError> {
|
||||
if self.is_service() {
|
||||
return Err(ApiError::AuthenticationFailed(Some(
|
||||
"User authentication required".to_string(),
|
||||
)));
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn as_user(&self) -> Option<&Self> {
|
||||
if self.is_service() { None } else { Some(self) }
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticatedUser {
|
||||
@@ -111,7 +178,7 @@ impl AuthenticatedUser {
|
||||
{
|
||||
return ScopePermissions::from_scope_string(Some(scope));
|
||||
}
|
||||
if !self.is_oauth {
|
||||
if !self.is_oauth() {
|
||||
return ScopePermissions::from_scope_string(Some("atproto"));
|
||||
}
|
||||
ScopePermissions::from_scope_string(self.scope.as_deref())
|
||||
@@ -349,11 +416,11 @@ async fn validate_bearer_token_with_options_internal(
|
||||
return Ok(AuthenticatedUser {
|
||||
did: did.clone(),
|
||||
key_bytes: Some(decrypted_key),
|
||||
is_oauth: false,
|
||||
is_admin,
|
||||
status,
|
||||
scope: token_data.claims.scope.clone(),
|
||||
controller_did,
|
||||
auth_source: AuthSource::Session,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -397,11 +464,11 @@ async fn validate_bearer_token_with_options_internal(
|
||||
return Ok(AuthenticatedUser {
|
||||
did: Did::new_unchecked(oauth_token.did),
|
||||
key_bytes,
|
||||
is_oauth: true,
|
||||
is_admin: oauth_token.is_admin,
|
||||
status,
|
||||
scope: oauth_info.scope,
|
||||
controller_did: oauth_info.controller_did.map(Did::new_unchecked),
|
||||
auth_source: AuthSource::OAuth,
|
||||
});
|
||||
} else {
|
||||
return Err(TokenValidationError::TokenExpired);
|
||||
@@ -481,11 +548,11 @@ pub async fn validate_token_with_dpop(
|
||||
Ok(AuthenticatedUser {
|
||||
did: Did::new_unchecked(result.did),
|
||||
key_bytes,
|
||||
is_oauth: true,
|
||||
is_admin: user_info.is_admin,
|
||||
status,
|
||||
scope: result.scope,
|
||||
controller_did: None,
|
||||
auth_source: AuthSource::OAuth,
|
||||
})
|
||||
}
|
||||
Err(crate::oauth::OAuthError::ExpiredToken(_)) => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code};
|
||||
use crate::oauth::{
|
||||
AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId,
|
||||
db::should_show_consent,
|
||||
scopes::expand_include_scopes,
|
||||
db::should_show_consent, scopes::expand_include_scopes,
|
||||
};
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
@@ -3645,9 +3644,9 @@ pub async fn register_complete(
|
||||
pub async fn establish_session(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: crate::auth::BearerAuth,
|
||||
auth: crate::auth::Auth<crate::auth::Active>,
|
||||
) -> Response {
|
||||
let did = &auth.0.did;
|
||||
let did = &auth.did;
|
||||
|
||||
let existing_device = extract_device_cookie(&headers);
|
||||
|
||||
@@ -3670,7 +3669,11 @@ pub async fn establish_session(
|
||||
};
|
||||
let device_typed = DeviceIdType::from(new_id.0.clone());
|
||||
|
||||
if let Err(e) = state.oauth_repo.create_device(&device_typed, &device_data).await {
|
||||
if let Err(e) = state
|
||||
.oauth_repo
|
||||
.create_device(&device_typed, &device_data)
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = ?e, "Failed to create device");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -3682,7 +3685,11 @@ pub async fn establish_session(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await {
|
||||
if let Err(e) = state
|
||||
.oauth_repo
|
||||
.upsert_account_device(did, &device_typed)
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = ?e, "Failed to link device to account");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use crate::types::PlainPassword;
|
||||
@@ -463,10 +463,10 @@ pub struct DelegationTokenAuthSubmit {
|
||||
pub async fn delegation_auth_token(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: BearerAuth,
|
||||
auth: Auth<Active>,
|
||||
Json(form): Json<DelegationTokenAuthSubmit>,
|
||||
) -> Response {
|
||||
let controller_did = auth.0.did;
|
||||
let controller_did = &auth.did;
|
||||
|
||||
let delegated_did: Did = match form.delegated_did.parse() {
|
||||
Ok(d) => d,
|
||||
@@ -510,7 +510,7 @@ pub async fn delegation_auth_token(
|
||||
|
||||
let grant = match state
|
||||
.delegation_repo
|
||||
.get_delegation(&delegated_did, &controller_did)
|
||||
.get_delegation(&delegated_did, controller_did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(g)) => g,
|
||||
@@ -551,7 +551,7 @@ pub async fn delegation_auth_token(
|
||||
|
||||
if state
|
||||
.oauth_repo
|
||||
.set_controller_did(&request_id, &controller_did)
|
||||
.set_controller_did(&request_id, controller_did)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
@@ -574,8 +574,8 @@ pub async fn delegation_auth_token(
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
&delegated_did,
|
||||
&controller_did,
|
||||
Some(&controller_did),
|
||||
controller_did,
|
||||
Some(controller_did),
|
||||
DelegationActionType::TokenIssued,
|
||||
Some(serde_json::json!({
|
||||
"client_id": request.client_id,
|
||||
|
||||
@@ -396,7 +396,7 @@ async fn try_legacy_auth(
|
||||
token: &str,
|
||||
) -> Result<LegacyAuthResult, ()> {
|
||||
match crate::auth::validate_bearer_token(user_repo, token).await {
|
||||
Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }),
|
||||
Ok(user) if !user.is_oauth() => Ok(LegacyAuthResult { did: user.did }),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ pub struct LinkedAccountsResponse {
|
||||
|
||||
pub async fn get_linked_accounts(
|
||||
State(state): State<AppState>,
|
||||
crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth,
|
||||
auth: crate::auth::Auth<crate::auth::Active>,
|
||||
) -> Result<Json<LinkedAccountsResponse>, ApiError> {
|
||||
let identities = state
|
||||
.sso_repo
|
||||
@@ -679,7 +679,7 @@ pub struct UnlinkAccountResponse {
|
||||
|
||||
pub async fn unlink_account(
|
||||
State(state): State<AppState>,
|
||||
crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth,
|
||||
auth: crate::auth::Auth<crate::auth::Active>,
|
||||
Json(input): Json<UnlinkAccountRequest>,
|
||||
) -> Result<Json<UnlinkAccountResponse>, ApiError> {
|
||||
if !state
|
||||
|
||||
@@ -149,7 +149,12 @@ async fn fetch_lexicon_via_atproto(nsid: &str) -> Result<LexiconDoc, String> {
|
||||
return Err(format!("Invalid NSID format: {}", nsid));
|
||||
}
|
||||
|
||||
let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join(".");
|
||||
let authority = parts[..2]
|
||||
.iter()
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS");
|
||||
|
||||
let did = resolve_lexicon_did_authority(&authority).await?;
|
||||
@@ -279,40 +284,42 @@ fn build_expanded_scopes(
|
||||
) -> String {
|
||||
let mut scopes: Vec<String> = Vec::new();
|
||||
|
||||
permissions.iter().for_each(|perm| match perm.resource.as_str() {
|
||||
"repo" => {
|
||||
if let Some(collections) = &perm.collection {
|
||||
let actions: Vec<&str> = perm
|
||||
.action
|
||||
.as_ref()
|
||||
.map(|a| a.iter().map(String::as_str).collect())
|
||||
.unwrap_or_else(|| DEFAULT_ACTIONS.to_vec());
|
||||
permissions
|
||||
.iter()
|
||||
.for_each(|perm| match perm.resource.as_str() {
|
||||
"repo" => {
|
||||
if let Some(collections) = &perm.collection {
|
||||
let actions: Vec<&str> = perm
|
||||
.action
|
||||
.as_ref()
|
||||
.map(|a| a.iter().map(String::as_str).collect())
|
||||
.unwrap_or_else(|| DEFAULT_ACTIONS.to_vec());
|
||||
|
||||
collections
|
||||
.iter()
|
||||
.filter(|coll| is_under_authority(coll, namespace_authority))
|
||||
.for_each(|coll| {
|
||||
actions.iter().for_each(|action| {
|
||||
scopes.push(format!("repo:{}?action={}", coll, action));
|
||||
collections
|
||||
.iter()
|
||||
.filter(|coll| is_under_authority(coll, namespace_authority))
|
||||
.for_each(|coll| {
|
||||
actions.iter().for_each(|action| {
|
||||
scopes.push(format!("repo:{}?action={}", coll, action));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"rpc" => {
|
||||
if let Some(lxms) = &perm.lxm {
|
||||
let perm_aud = perm.aud.as_deref().or(default_aud);
|
||||
"rpc" => {
|
||||
if let Some(lxms) = &perm.lxm {
|
||||
let perm_aud = perm.aud.as_deref().or(default_aud);
|
||||
|
||||
lxms.iter().for_each(|lxm| {
|
||||
let scope = match perm_aud {
|
||||
Some(aud) => format!("rpc:{}?aud={}", lxm, aud),
|
||||
None => format!("rpc:{}", lxm),
|
||||
};
|
||||
scopes.push(scope);
|
||||
});
|
||||
lxms.iter().for_each(|lxm| {
|
||||
let scope = match perm_aud {
|
||||
Some(aud) => format!("rpc:{}?aud={}", lxm, aud),
|
||||
None => format!("rpc:{}", lxm),
|
||||
};
|
||||
scopes.push(scope);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
_ => {}
|
||||
});
|
||||
|
||||
scopes.join(" ")
|
||||
}
|
||||
@@ -334,7 +341,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_include_scope_with_multiple_params() {
|
||||
let (nsid, aud) = parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux");
|
||||
let (nsid, aud) =
|
||||
parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux");
|
||||
assert_eq!(nsid, "io.atcr.authFullApp");
|
||||
assert_eq!(aud, Some("did:web:example.com"));
|
||||
}
|
||||
@@ -443,7 +451,8 @@ mod tests {
|
||||
aud: None,
|
||||
}];
|
||||
|
||||
let expanded = build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr");
|
||||
let expanded =
|
||||
build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr");
|
||||
assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com"));
|
||||
}
|
||||
|
||||
@@ -583,7 +592,8 @@ mod tests {
|
||||
cache_key.to_string(),
|
||||
CachedLexicon {
|
||||
expanded_scope: "old_value".to_string(),
|
||||
cached_at: std::time::Instant::now() - std::time::Duration::from_secs(CACHE_TTL_SECS + 1),
|
||||
cached_at: std::time::Instant::now()
|
||||
- std::time::Duration::from_secs(CACHE_TTL_SECS + 1),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -601,12 +611,22 @@ mod tests {
|
||||
fn test_nsid_authority_extraction_for_dns() {
|
||||
let nsid = "io.atcr.authFullApp";
|
||||
let parts: Vec<&str> = nsid.split('.').collect();
|
||||
let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join(".");
|
||||
let authority = parts[..2]
|
||||
.iter()
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
assert_eq!(authority, "atcr.io");
|
||||
|
||||
let nsid2 = "app.bsky.feed.post";
|
||||
let parts2: Vec<&str> = nsid2.split('.').collect();
|
||||
let authority2 = parts2[..2].iter().rev().cloned().collect::<Vec<_>>().join(".");
|
||||
let authority2 = parts2[..2]
|
||||
.iter()
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
assert_eq!(authority2, "bsky.app");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,5 +556,4 @@ mod tests {
|
||||
"app.bsky.feed.getAuthorFeed"
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user