Save submitted identity proofs to database
This commit is contained in:
parent
fd6b71073a
commit
8deea0c867
12 changed files with 140 additions and 19 deletions
|
@ -664,6 +664,16 @@ components:
|
|||
description: The location of the user's profile page.
|
||||
type: string
|
||||
example: https://example.com/@user
|
||||
identity_proofs:
|
||||
description: Identity proofs.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Field'
|
||||
fields:
|
||||
description: Additional metadata attached to a profile as name-value pairs.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Field'
|
||||
wallet_address:
|
||||
description: Ethereum wallet address.
|
||||
type: string
|
||||
|
@ -685,6 +695,19 @@ components:
|
|||
url:
|
||||
description: The location of the original full-size attachment.
|
||||
type: string
|
||||
Field:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: The key of a given field's key-value pair.
|
||||
type: string
|
||||
value:
|
||||
description: The value associated with the name key.
|
||||
type: string
|
||||
verified_at:
|
||||
description: Timestamp of when the server verified the field value.
|
||||
type: string
|
||||
format: dateTime
|
||||
Instance:
|
||||
type: object
|
||||
properties:
|
||||
|
|
1
migrations/V0023__actor_profile__add_identity_proofs.sql
Normal file
1
migrations/V0023__actor_profile__add_identity_proofs.sql
Normal file
|
@ -0,0 +1 @@
|
|||
ALTER TABLE actor_profile ADD COLUMN identity_proofs JSONB NOT NULL DEFAULT '[]';
|
|
@ -7,6 +7,7 @@ CREATE TABLE actor_profile (
|
|||
bio_source TEXT,
|
||||
avatar_file_name VARCHAR(100),
|
||||
banner_file_name VARCHAR(100),
|
||||
identity_proofs JSONB NOT NULL DEFAULT '[]',
|
||||
extra_fields JSONB NOT NULL DEFAULT '[]',
|
||||
follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0,
|
||||
following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0,
|
||||
|
|
|
@ -159,6 +159,7 @@ pub async fn fetch_profile_by_actor_id(
|
|||
bio: actor.summary.clone(),
|
||||
avatar,
|
||||
banner,
|
||||
identity_proofs: vec![],
|
||||
extra_fields,
|
||||
actor_json: Some(actor),
|
||||
};
|
||||
|
|
|
@ -720,6 +720,7 @@ pub async fn receive_activity(
|
|||
bio_source: actor.summary.clone(),
|
||||
avatar,
|
||||
banner,
|
||||
identity_proofs: vec![],
|
||||
extra_fields,
|
||||
actor_json: Some(actor),
|
||||
};
|
||||
|
|
|
@ -5,9 +5,11 @@ use serde::{Deserialize, Serialize};
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::ValidationError;
|
||||
use crate::models::profiles::currencies::get_identity_proof_field_name;
|
||||
use crate::models::profiles::types::{
|
||||
DbActorProfile,
|
||||
ExtraField,
|
||||
IdentityProof,
|
||||
ProfileUpdateData,
|
||||
};
|
||||
use crate::models::profiles::validators::validate_username;
|
||||
|
@ -17,10 +19,12 @@ use crate::models::users::types::{
|
|||
};
|
||||
use crate::utils::files::{FileError, save_validated_b64_file, get_file_url};
|
||||
|
||||
/// https://docs.joinmastodon.org/entities/field/
|
||||
#[derive(Serialize)]
|
||||
pub struct AccountField {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
verified_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// https://docs.joinmastodon.org/entities/source/
|
||||
|
@ -42,6 +46,7 @@ pub struct Account {
|
|||
pub note: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub header: Option<String>,
|
||||
pub identity_proofs: Vec<AccountField>,
|
||||
pub fields: Vec<AccountField>,
|
||||
pub followers_count: i32,
|
||||
pub following_count: i32,
|
||||
|
@ -59,9 +64,30 @@ impl Account {
|
|||
.map(|name| get_file_url(instance_url, name));
|
||||
let header_url = profile.banner_file_name.as_ref()
|
||||
.map(|name| get_file_url(instance_url, name));
|
||||
let fields = profile.extra_fields.into_inner().into_iter()
|
||||
.map(|field| AccountField { name: field.name, value: field.value })
|
||||
.collect();
|
||||
|
||||
let mut identity_proofs = vec![];
|
||||
for proof in profile.identity_proofs.into_inner() {
|
||||
// Skip proof if it doesn't map to field name
|
||||
if let Some(field_name) = get_identity_proof_field_name(&proof.proof_type) {
|
||||
let field = AccountField {
|
||||
name: field_name,
|
||||
value: proof.issuer.address,
|
||||
// Use current time because DID proofs are always valid
|
||||
verified_at: Some(Utc::now()),
|
||||
};
|
||||
identity_proofs.push(field);
|
||||
};
|
||||
};
|
||||
let mut extra_fields = vec![];
|
||||
for extra_field in profile.extra_fields.into_inner() {
|
||||
let field = AccountField {
|
||||
name: extra_field.name,
|
||||
value: extra_field.value,
|
||||
verified_at: None,
|
||||
};
|
||||
extra_fields.push(field);
|
||||
};
|
||||
|
||||
Self {
|
||||
id: profile.id,
|
||||
username: profile.username,
|
||||
|
@ -72,7 +98,8 @@ impl Account {
|
|||
note: profile.bio,
|
||||
avatar: avatar_url,
|
||||
header: header_url,
|
||||
fields,
|
||||
identity_proofs,
|
||||
fields: extra_fields,
|
||||
followers_count: profile.follower_count,
|
||||
following_count: profile.following_count,
|
||||
statuses_count: profile.post_count,
|
||||
|
@ -87,6 +114,7 @@ impl Account {
|
|||
.map(|field| AccountField {
|
||||
name: field.name,
|
||||
value: field.value_source.unwrap_or(field.value),
|
||||
verified_at: None,
|
||||
})
|
||||
.collect();
|
||||
let source = Source {
|
||||
|
@ -163,6 +191,7 @@ impl AccountUpdateData {
|
|||
self,
|
||||
current_avatar: &Option<String>,
|
||||
current_banner: &Option<String>,
|
||||
current_identity_proofs: &[IdentityProof],
|
||||
media_dir: &Path,
|
||||
) -> Result<ProfileUpdateData, FileError> {
|
||||
let avatar = process_b64_image_field_value(
|
||||
|
@ -171,6 +200,7 @@ impl AccountUpdateData {
|
|||
let banner = process_b64_image_field_value(
|
||||
self.header, current_banner.clone(), media_dir,
|
||||
)?;
|
||||
let identity_proofs = current_identity_proofs.to_vec();
|
||||
let extra_fields = self.fields_attributes.unwrap_or(vec![]);
|
||||
let profile_data = ProfileUpdateData {
|
||||
display_name: self.display_name,
|
||||
|
@ -178,8 +208,9 @@ impl AccountUpdateData {
|
|||
bio_source: self.note_source,
|
||||
avatar,
|
||||
banner,
|
||||
identity_proofs,
|
||||
extra_fields,
|
||||
actor_json: None,
|
||||
actor_json: None, // always None for local profiles
|
||||
};
|
||||
Ok(profile_data)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use crate::errors::{DatabaseError, HttpError, ValidationError};
|
|||
use crate::ethereum::eip4361::verify_eip4361_signature;
|
||||
use crate::ethereum::gate::is_allowed_user;
|
||||
use crate::ethereum::identity::{
|
||||
ETHEREUM_EIP191_PROOF,
|
||||
DidPkh,
|
||||
create_identity_claim,
|
||||
verify_identity_proof,
|
||||
|
@ -32,6 +33,7 @@ use crate::models::profiles::queries::{
|
|||
get_wallet_address,
|
||||
update_profile,
|
||||
};
|
||||
use crate::models::profiles::types::{IdentityProof, ProfileUpdateData};
|
||||
use crate::models::relationships::queries::{
|
||||
create_follow_request,
|
||||
follow,
|
||||
|
@ -184,6 +186,7 @@ async fn update_credentials(
|
|||
.into_profile_data(
|
||||
¤t_user.profile.avatar_file_name,
|
||||
¤t_user.profile.banner_file_name,
|
||||
¤t_user.profile.identity_proofs.into_inner(),
|
||||
&config.media_dir(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
|
@ -246,7 +249,7 @@ async fn create_identity_proof(
|
|||
proof_data: web::Json<IdentityProofData>,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let mut current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let actor_id = current_user.profile.actor_id(&config.instance_url());
|
||||
let wallet_address = current_user.wallet_address.as_ref()
|
||||
.ok_or(HttpError::PermissionError)?;
|
||||
|
@ -256,6 +259,29 @@ async fn create_identity_proof(
|
|||
&did,
|
||||
&proof_data.signature,
|
||||
)?;
|
||||
let proof = IdentityProof {
|
||||
issuer: did,
|
||||
proof_type: ETHEREUM_EIP191_PROOF.to_string(),
|
||||
value: proof_data.signature.clone(),
|
||||
};
|
||||
let mut profile_data = ProfileUpdateData::from(¤t_user.profile);
|
||||
match profile_data.identity_proofs.iter_mut()
|
||||
.find(|item| item.issuer == proof.issuer) {
|
||||
Some(mut item) => {
|
||||
// Replace
|
||||
item.proof_type = proof.proof_type;
|
||||
item.value = proof.value;
|
||||
},
|
||||
None => {
|
||||
// Add new proof
|
||||
profile_data.identity_proofs.push(proof);
|
||||
},
|
||||
};
|
||||
current_user.profile = update_profile(
|
||||
db_client,
|
||||
¤t_user.id,
|
||||
profile_data,
|
||||
).await?;
|
||||
let account = Account::from_user(current_user, &config.instance_url());
|
||||
Ok(HttpResponse::Ok().json(account))
|
||||
}
|
||||
|
|
13
src/models/profiles/currencies.rs
Normal file
13
src/models/profiles/currencies.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use crate::ethereum::identity::ETHEREUM_EIP191_PROOF;
|
||||
|
||||
pub fn get_currency_field_name(currency_code: &str) -> String {
|
||||
format!("${}", currency_code.to_uppercase())
|
||||
}
|
||||
|
||||
pub fn get_identity_proof_field_name(proof_type: &str) -> Option<String> {
|
||||
let field_name = match proof_type {
|
||||
ETHEREUM_EIP191_PROOF => "$ETH".to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
Some(field_name)
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod currencies;
|
||||
pub mod queries;
|
||||
pub mod types;
|
||||
pub mod validators;
|
||||
|
|
|
@ -10,10 +10,11 @@ use crate::models::cleanup::{
|
|||
};
|
||||
use crate::models::relationships::types::RelationshipType;
|
||||
use crate::utils::id::new_uuid;
|
||||
use super::currencies::get_currency_field_name;
|
||||
use super::types::{
|
||||
get_currency_field_name,
|
||||
ExtraFields,
|
||||
DbActorProfile,
|
||||
ExtraFields,
|
||||
IdentityProofs,
|
||||
ProfileCreateData,
|
||||
ProfileUpdateData,
|
||||
};
|
||||
|
@ -24,15 +25,15 @@ pub async fn create_profile(
|
|||
profile_data: ProfileCreateData,
|
||||
) -> Result<DbActorProfile, DatabaseError> {
|
||||
let profile_id = new_uuid();
|
||||
let extra_fields = ExtraFields(profile_data.extra_fields.clone());
|
||||
let row = db_client.query_one(
|
||||
"
|
||||
INSERT INTO actor_profile (
|
||||
id, username, display_name, acct, bio, bio_source,
|
||||
avatar_file_name, banner_file_name, extra_fields,
|
||||
avatar_file_name, banner_file_name,
|
||||
identity_proofs, extra_fields,
|
||||
actor_json
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING actor_profile
|
||||
",
|
||||
&[
|
||||
|
@ -44,7 +45,8 @@ pub async fn create_profile(
|
|||
&profile_data.bio,
|
||||
&profile_data.avatar,
|
||||
&profile_data.banner,
|
||||
&extra_fields,
|
||||
&IdentityProofs(profile_data.identity_proofs),
|
||||
&ExtraFields(profile_data.extra_fields),
|
||||
&profile_data.actor_json,
|
||||
],
|
||||
).await.map_err(catch_unique_violation("profile"))?;
|
||||
|
@ -66,9 +68,10 @@ pub async fn update_profile(
|
|||
bio_source = $3,
|
||||
avatar_file_name = $4,
|
||||
banner_file_name = $5,
|
||||
extra_fields = $6,
|
||||
actor_json = $7
|
||||
WHERE id = $8
|
||||
identity_proofs = $6,
|
||||
extra_fields = $7,
|
||||
actor_json = $8
|
||||
WHERE id = $9
|
||||
RETURNING actor_profile
|
||||
",
|
||||
&[
|
||||
|
@ -77,6 +80,7 @@ pub async fn update_profile(
|
|||
&data.bio_source,
|
||||
&data.avatar,
|
||||
&data.banner,
|
||||
&IdentityProofs(data.identity_proofs),
|
||||
&ExtraFields(data.extra_fields),
|
||||
&data.actor_json,
|
||||
&profile_id,
|
||||
|
@ -490,6 +494,8 @@ mod tests {
|
|||
let db_client = create_test_database().await;
|
||||
let profile = create_profile(&db_client, profile_data).await.unwrap();
|
||||
assert_eq!(profile.username, "test");
|
||||
assert_eq!(profile.identity_proofs.into_inner().len(), 0);
|
||||
assert_eq!(profile.extra_fields.into_inner().len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
@ -52,10 +52,6 @@ impl ExtraFields {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_currency_field_name(currency_code: &str) -> String {
|
||||
format!("${}", currency_code.to_uppercase())
|
||||
}
|
||||
|
||||
json_from_sql!(ExtraFields);
|
||||
json_to_sql!(ExtraFields);
|
||||
|
||||
|
@ -73,6 +69,7 @@ pub struct DbActorProfile {
|
|||
pub bio_source: Option<String>, // plaintext or markdown
|
||||
pub avatar_file_name: Option<String>,
|
||||
pub banner_file_name: Option<String>,
|
||||
pub identity_proofs: IdentityProofs,
|
||||
pub extra_fields: ExtraFields,
|
||||
pub follower_count: i32,
|
||||
pub following_count: i32,
|
||||
|
@ -128,6 +125,7 @@ impl Default for DbActorProfile {
|
|||
bio_source: None,
|
||||
avatar_file_name: None,
|
||||
banner_file_name: None,
|
||||
identity_proofs: IdentityProofs(vec![]),
|
||||
extra_fields: ExtraFields(vec![]),
|
||||
follower_count: 0,
|
||||
following_count: 0,
|
||||
|
@ -147,6 +145,7 @@ pub struct ProfileCreateData {
|
|||
pub bio: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub identity_proofs: Vec<IdentityProof>,
|
||||
pub extra_fields: Vec<ExtraField>,
|
||||
pub actor_json: Option<Actor>,
|
||||
}
|
||||
|
@ -172,6 +171,7 @@ pub struct ProfileUpdateData {
|
|||
pub bio_source: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub identity_proofs: Vec<IdentityProof>,
|
||||
pub extra_fields: Vec<ExtraField>,
|
||||
pub actor_json: Option<Actor>,
|
||||
}
|
||||
|
@ -192,6 +192,22 @@ impl ProfileUpdateData {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&DbActorProfile> for ProfileUpdateData {
|
||||
fn from(profile: &DbActorProfile) -> Self {
|
||||
let profile = profile.clone();
|
||||
Self {
|
||||
display_name: profile.display_name,
|
||||
bio: profile.bio,
|
||||
bio_source: profile.bio_source,
|
||||
avatar: profile.avatar_file_name,
|
||||
banner: profile.banner_file_name,
|
||||
identity_proofs: profile.identity_proofs.into_inner(),
|
||||
extra_fields: profile.extra_fields.into_inner(),
|
||||
actor_json: profile.actor_json,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::activitypub::actor::Actor;
|
||||
|
|
|
@ -80,6 +80,7 @@ pub async fn create_user(
|
|||
bio: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
identity_proofs: vec![],
|
||||
extra_fields: vec![],
|
||||
actor_json: None,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue