Inbound migrations work

This commit is contained in:
lewis
2025-12-18 21:20:41 +02:00
parent d695135a4d
commit 95958bb119
17 changed files with 951 additions and 184 deletions
+6 -6
View File
@@ -32,7 +32,7 @@ pub async fn get_preferences(
.into_response();
}
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(_) => {
return (
@@ -109,7 +109,7 @@ pub async fn put_preferences(
.into_response();
}
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(_) => {
return (
@@ -119,12 +119,12 @@ pub async fn put_preferences(
.into_response();
}
};
let user_id: uuid::Uuid =
match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
let (user_id, is_migration): (uuid::Uuid, bool) =
match sqlx::query!("SELECT id, deactivated_at FROM users WHERE did = $1", auth_user.did)
.fetch_optional(&state.db)
.await
{
Ok(Some(id)) => id,
Ok(Some(row)) => (row.id, row.deactivated_at.is_some()),
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
@@ -166,7 +166,7 @@ pub async fn put_preferences(
)
.into_response();
}
if pref_type == "app.bsky.actor.defs#declaredAgePref" {
if pref_type == "app.bsky.actor.defs#declaredAgePref" && !is_migration {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})),
+215 -88
View File
@@ -1,4 +1,5 @@
use super::did::verify_did_web;
use crate::auth::{ServiceTokenVerifier, extract_bearer_token_from_header, is_service_token};
use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key};
use crate::state::{AppState, RateLimitKind};
use axum::{
@@ -15,7 +16,7 @@ use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::{error, info, warn};
use tracing::{debug, error, info, warn};
fn extract_client_ip(headers: &HeaderMap) -> String {
if let Some(forwarded) = headers.get("x-forwarded-for")
@@ -50,6 +51,10 @@ pub struct CreateAccountInput {
pub struct CreateAccountOutput {
pub handle: String,
pub did: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_jwt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_jwt: Option<String>,
pub verification_required: bool,
pub verification_channel: String,
}
@@ -75,6 +80,58 @@ pub async fn create_account(
)
.into_response();
}
let migration_auth = if let Some(token) =
extract_bearer_token_from_header(headers.get("Authorization").and_then(|h| h.to_str().ok()))
{
if is_service_token(&token) {
let verifier = ServiceTokenVerifier::new();
match verifier
.verify_service_token(&token, Some("com.atproto.server.createAccount"))
.await
{
Ok(claims) => {
debug!("Service token verified for migration: iss={}", claims.iss);
Some(claims.iss)
}
Err(e) => {
error!("Service token verification failed: {:?}", e);
return (
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "AuthenticationFailed",
"message": format!("Service token verification failed: {}", e)
})),
)
.into_response();
}
}
} else {
None
}
} else {
None
};
let is_migration = migration_auth.is_some()
&& input.did.as_ref().map(|d| d.starts_with("did:plc:")).unwrap_or(false);
if is_migration {
let migration_did = input.did.as_ref().unwrap();
let auth_did = migration_auth.as_ref().unwrap();
if migration_did != auth_did {
return (
StatusCode::FORBIDDEN,
Json(json!({
"error": "AuthorizationError",
"message": format!("Service token issuer {} does not match DID {}", auth_did, migration_did)
})),
)
.into_response();
}
info!(did = %migration_did, "Processing account migration");
}
if input.handle.contains('!') || input.handle.contains('@') {
return (
StatusCode::BAD_REQUEST,
@@ -99,46 +156,50 @@ pub async fn create_account(
}
let verification_channel = input.verification_channel.as_deref().unwrap_or("email");
let valid_channels = ["email", "discord", "telegram", "signal"];
if !valid_channels.contains(&verification_channel) {
if !valid_channels.contains(&verification_channel) && !is_migration {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})),
)
.into_response();
}
let verification_recipient = match verification_channel {
"email" => match &input.email {
Some(email) if !email.trim().is_empty() => email.trim().to_string(),
let verification_recipient = if is_migration {
None
} else {
Some(match verification_channel {
"email" => match &input.email {
Some(email) if !email.trim().is_empty() => email.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})),
).into_response(),
},
"discord" => match &input.discord_id {
Some(id) if !id.trim().is_empty() => id.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})),
).into_response(),
},
"telegram" => match &input.telegram_username {
Some(username) if !username.trim().is_empty() => username.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})),
).into_response(),
},
"signal" => match &input.signal_number {
Some(number) if !number.trim().is_empty() => number.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})),
).into_response(),
},
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})),
Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})),
).into_response(),
},
"discord" => match &input.discord_id {
Some(id) if !id.trim().is_empty() => id.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})),
).into_response(),
},
"telegram" => match &input.telegram_username {
Some(username) if !username.trim().is_empty() => username.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})),
).into_response(),
},
"signal" => match &input.signal_number {
Some(number) if !number.trim().is_empty() => number.trim().to_string(),
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})),
).into_response(),
},
_ => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})),
).into_response(),
})
};
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let pds_endpoint = format!("https://{}", hostname);
@@ -246,10 +307,12 @@ pub async fn create_account(
.into_response();
}
d.clone()
} else if d.starts_with("did:plc:") && is_migration {
d.clone()
} else {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc"})),
Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc. For migration with existing did:plc, provide service auth."})),
)
.into_response();
}
@@ -396,13 +459,18 @@ pub async fn create_account(
.await
.map(|c| c.unwrap_or(0) == 0)
.unwrap_or(false);
let deactivated_at: Option<chrono::DateTime<chrono::Utc>> = if is_migration {
Some(chrono::Utc::now())
} else {
None
};
let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
r#"INSERT INTO users (
handle, email, did, password_hash,
preferred_comms_channel,
discord_id, telegram_username, signal_number,
is_admin
) VALUES ($1, $2, $3, $4, $5::comms_channel, $6, $7, $8, $9) RETURNING id"#,
is_admin, deactivated_at, email_verified
) VALUES ($1, $2, $3, $4, $5::comms_channel, $6, $7, $8, $9, $10, $11) RETURNING id"#,
)
.bind(short_handle)
.bind(&email)
@@ -431,6 +499,8 @@ pub async fn create_account(
.filter(|s| !s.is_empty()),
)
.bind(is_first_user)
.bind(deactivated_at)
.bind(is_migration)
.fetch_one(&mut *tx)
.await;
let user_id = match user_insert {
@@ -477,21 +547,23 @@ pub async fn create_account(
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at) VALUES ($1, 'email', $2, $3, $4)",
user_id,
verification_code,
email,
code_expires_at
)
.execute(&mut *tx)
.await {
error!("Error inserting verification code: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
if !is_migration {
if let Err(e) = sqlx::query!(
"INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at) VALUES ($1, 'email', $2, $3, $4)",
user_id,
verification_code,
email,
code_expires_at
)
.into_response();
.execute(&mut *tx)
.await {
error!("Error inserting verification code: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
}
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
Ok(enc) => enc,
@@ -636,50 +708,105 @@ pub async fn create_account(
)
.into_response();
}
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await
{
warn!("Failed to sequence identity event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
{
warn!("Failed to sequence account event for {}: {}", did, e);
}
let profile_record = json!({
"$type": "app.bsky.actor.profile",
"displayName": input.handle
});
if let Err(e) = crate::api::repo::record::create_record_internal(
&state,
&did,
"app.bsky.actor.profile",
"self",
&profile_record,
)
.await
{
warn!("Failed to create default profile for {}: {}", did, e);
}
if let Err(e) = crate::comms::enqueue_signup_verification(
&state.db,
user_id,
verification_channel,
&verification_recipient,
&verification_code,
)
.await
{
warn!(
"Failed to enqueue signup verification notification: {:?}",
e
);
if !is_migration {
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await
{
warn!("Failed to sequence identity event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
{
warn!("Failed to sequence account event for {}: {}", did, e);
}
let profile_record = json!({
"$type": "app.bsky.actor.profile",
"displayName": input.handle
});
if let Err(e) = crate::api::repo::record::create_record_internal(
&state,
&did,
"app.bsky.actor.profile",
"self",
&profile_record,
)
.await
{
warn!("Failed to create default profile for {}: {}", did, e);
}
if let Some(ref recipient) = verification_recipient {
if let Err(e) = crate::comms::enqueue_signup_verification(
&state.db,
user_id,
verification_channel,
recipient,
&verification_code,
)
.await
{
warn!(
"Failed to enqueue signup verification notification: {:?}",
e
);
}
}
}
let (access_jwt, refresh_jwt) = if is_migration {
let access_meta =
match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Error creating access token for migration: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let refresh_meta =
match crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Error creating refresh token for migration: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
did,
access_meta.jti,
refresh_meta.jti,
access_meta.expires_at,
refresh_meta.expires_at
)
.execute(&state.db)
.await
{
error!("Error creating session for migration: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
(Some(access_meta.token), Some(refresh_meta.token))
} else {
(None, None)
};
(
StatusCode::OK,
Json(CreateAccountOutput {
handle: short_handle.to_string(),
handle: full_handle.clone(),
did,
verification_required: true,
access_jwt,
refresh_jwt,
verification_required: !is_migration,
verification_channel: verification_channel.to_string(),
}),
)
+11 -13
View File
@@ -1,4 +1,5 @@
use crate::api::ApiError;
use crate::plc::signing_key_to_did_key;
use crate::state::AppState;
use axum::{
Json,
@@ -309,7 +310,7 @@ pub async fn get_recommended_did_credentials(
.into_response();
}
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
};
@@ -334,24 +335,21 @@ pub async fn get_recommended_did_credentials(
};
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let pds_endpoint = format!("https://{}", hostname);
let secret_key = match k256::SecretKey::from_slice(&key_bytes) {
let full_handle = if user.handle.contains('.') {
user.handle.clone()
} else {
format!("{}.{}", user.handle, hostname)
};
let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) {
Ok(k) => k,
Err(_) => return ApiError::InternalError.into_response(),
};
let public_key = secret_key.public_key();
let encoded = public_key.to_encoded_point(true);
let did_key = format!(
"did:key:zQ3sh{}",
multibase::encode(multibase::Base::Base58Btc, encoded.as_bytes())
.chars()
.skip(1)
.collect::<String>()
);
let did_key = signing_key_to_did_key(&signing_key);
(
StatusCode::OK,
Json(GetRecommendedDidCredentialsOutput {
rotation_keys: vec![did_key.clone()],
also_known_as: vec![format!("at://{}", user.handle)],
also_known_as: vec![format!("at://{}", full_handle)],
verification_methods: VerificationMethods { atproto: did_key },
services: Services {
atproto_pds: AtprotoPds {
@@ -380,7 +378,7 @@ pub async fn update_handle(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let did = match crate::auth::validate_bearer_token(&state.db, &token).await {
let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user.did,
Err(e) => return ApiError::from(e).into_response(),
};
+1 -1
View File
@@ -24,7 +24,7 @@ pub async fn request_plc_operation_signature(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
};
+1 -1
View File
@@ -50,7 +50,7 @@ pub async fn sign_plc_operation(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &bearer).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
};
+38 -33
View File
@@ -29,7 +29,7 @@ pub async fn submit_plc_operation(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &bearer).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
};
@@ -40,7 +40,7 @@ pub async fn submit_plc_operation(
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 sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did)
let user = match sqlx::query!("SELECT id, handle, deactivated_at FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
{
@@ -53,6 +53,7 @@ pub async fn submit_plc_operation(
.into_response();
}
};
let is_migration = user.deactivated_at.is_some();
let key_row = match sqlx::query!(
"SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
user.id
@@ -93,21 +94,23 @@ pub async fn submit_plc_operation(
}
};
let user_did_key = signing_key_to_did_key(&signing_key);
if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) {
let server_rotation_key =
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
let has_server_key = rotation_keys
.iter()
.any(|k| k.as_str() == Some(&server_rotation_key));
if !has_server_key {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": "Rotation keys do not include server's rotation key"
})),
)
.into_response();
if !is_migration {
if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) {
let server_rotation_key =
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
let has_server_key = rotation_keys
.iter()
.any(|k| k.as_str() == Some(&server_rotation_key));
if !has_server_key {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": "Rotation keys do not include server's rotation key"
})),
)
.into_response();
}
}
}
if let Some(services) = op.get("services").and_then(|v| v.as_object())
@@ -135,30 +138,32 @@ pub async fn submit_plc_operation(
.into_response();
}
}
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 {
if !is_migration {
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 (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": "Incorrect signing key in verificationMethods"
})),
)
.into_response();
}
if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) {
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 (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": "Incorrect signing key in verificationMethods"
"message": "Incorrect handle in alsoKnownAs"
})),
)
.into_response();
}
if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) {
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 (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": "Incorrect handle in alsoKnownAs"
})),
)
.into_response();
}
}
let plc_client = PlcClient::new(None);
+61 -17
View File
@@ -1,3 +1,4 @@
use crate::auth::{ServiceTokenVerifier, is_service_token};
use crate::state::AppState;
use axum::body::Bytes;
use axum::{
@@ -13,22 +14,16 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::str::FromStr;
use tracing::error;
use tracing::{debug, error};
const MAX_BLOB_SIZE: usize = 1_000_000;
const MAX_VIDEO_BLOB_SIZE: usize = 100_000_000;
pub async fn upload_blob(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
body: Bytes,
) -> Response {
if body.len() > MAX_BLOB_SIZE {
return (
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({"error": "BlobTooLarge", "message": format!("Blob size {} exceeds maximum of {} bytes", body.len(), MAX_BLOB_SIZE)})),
)
.into_response();
}
let token = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok()),
) {
@@ -41,17 +36,66 @@ pub async fn upload_blob(
.into_response();
}
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
Ok(user) => user,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
let is_service_auth = is_service_token(&token);
let (did, is_migration) = if is_service_auth {
debug!("Verifying service token for blob upload");
let verifier = ServiceTokenVerifier::new();
match verifier
.verify_service_token(&token, Some("com.atproto.repo.uploadBlob"))
.await
{
Ok(claims) => {
debug!("Service token verified for DID: {}", claims.iss);
(claims.iss, false)
}
Err(e) => {
error!("Service token verification failed: {:?}", e);
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed", "message": format!("Service token verification failed: {}", e)})),
)
.into_response();
}
}
} else {
match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => {
let deactivated = sqlx::query_scalar!(
"SELECT deactivated_at FROM users WHERE did = $1",
user.did
)
.fetch_optional(&state.db)
.await
.ok()
.flatten()
.flatten();
(user.did, deactivated.is_some())
}
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
}
};
let did = auth_user.did;
let max_size = if is_service_auth || is_migration {
MAX_VIDEO_BLOB_SIZE
} else {
MAX_BLOB_SIZE
};
if body.len() > max_size {
return (
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({"error": "BlobTooLarge", "message": format!("Blob size {} exceeds maximum of {} bytes", body.len(), max_size)})),
)
.into_response();
}
let mime_type = headers
.get("content-type")
.and_then(|h| h.to_str().ok())
+53 -14
View File
@@ -53,7 +53,7 @@ pub async fn import_repo(
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
};
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
};
@@ -82,16 +82,6 @@ pub async fn import_repo(
.into_response();
}
};
if user.deactivated_at.is_some() {
return (
StatusCode::FORBIDDEN,
Json(json!({
"error": "AccountDeactivated",
"message": "Account is deactivated"
})),
)
.into_response();
}
if user.takedown_ref.is_some() {
return (
StatusCode::FORBIDDEN,
@@ -185,7 +175,58 @@ pub async fn import_repo(
let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
if !skip_verification {
let is_migration = user.deactivated_at.is_some();
if skip_verification {
warn!("Skipping all CAR verification for import (SKIP_IMPORT_VERIFICATION=true)");
} else if is_migration {
debug!("Verifying CAR file structure for migration (skipping signature verification)");
let verifier = CarVerifier::new();
match verifier.verify_car_structure_only(did, &root, &blocks) {
Ok(verified) => {
debug!(
"CAR structure verification successful: rev={}, data_cid={}",
verified.rev, verified.data_cid
);
}
Err(crate::sync::verify::VerifyError::DidMismatch {
commit_did,
expected_did,
}) => {
return (
StatusCode::FORBIDDEN,
Json(json!({
"error": "InvalidRequest",
"message": 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 (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": format!("MST validation failed: {}", msg)
})),
)
.into_response();
}
Err(e) => {
error!("CAR structure verification error: {:?}", e);
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidRequest",
"message": format!("CAR verification failed: {}", e)
})),
)
.into_response();
}
}
} else {
debug!("Verifying CAR file signature and structure for DID {}", did);
let verifier = CarVerifier::new();
match verifier.verify_car(did, &root, &blocks).await {
@@ -264,8 +305,6 @@ pub async fn import_repo(
.into_response();
}
}
} else {
warn!("Skipping CAR signature verification for import (SKIP_IMPORT_VERIFICATION=true)");
}
let max_blocks: usize = std::env::var("MAX_IMPORT_BLOCKS")
.ok()
+7 -5
View File
@@ -1,5 +1,5 @@
use crate::api::ApiError;
use crate::auth::BearerAuth;
use crate::auth::{BearerAuth, BearerAuthAllowDeactivated};
use crate::state::{AppState, RateLimitKind};
use axum::{
Json,
@@ -88,7 +88,7 @@ pub async fn create_session(
k.key_bytes, k.encryption_version
FROM users u
JOIN user_keys k ON u.id = k.user_id
WHERE u.handle = $1 OR u.email = $1"#,
WHERE u.handle = $1 OR u.email = $1 OR u.did = $1"#,
normalized_identifier
)
.fetch_optional(&state.db)
@@ -189,11 +189,11 @@ pub async fn create_session(
pub async fn get_session(
State(state): State<AppState>,
BearerAuth(auth_user): BearerAuth,
BearerAuthAllowDeactivated(auth_user): BearerAuthAllowDeactivated,
) -> Response {
match sqlx::query!(
r#"SELECT
handle, email, email_verified, is_admin,
handle, email, email_verified, is_admin, deactivated_at,
preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
discord_verified, telegram_verified, signal_verified
FROM users WHERE did = $1"#,
@@ -211,6 +211,7 @@ pub async fn get_session(
};
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let handle = full_handle(&row.handle, &pds_hostname);
let is_active = row.deactivated_at.is_none();
Json(json!({
"handle": handle,
"did": auth_user.did,
@@ -219,7 +220,8 @@ pub async fn get_session(
"preferredChannel": preferred_channel,
"preferredChannelVerified": preferred_channel_verified,
"isAdmin": row.is_admin,
"active": true,
"active": is_active,
"status": if is_active { "active" } else { "deactivated" },
"didDoc": {}
})).into_response()
}