Save submitted identity proofs to database

This commit is contained in:
silverpill 2022-04-26 14:12:26 +00:00
parent fd6b71073a
commit 8deea0c867
12 changed files with 140 additions and 19 deletions

View file

@ -664,6 +664,16 @@ components:
description: The location of the user's profile page. description: The location of the user's profile page.
type: string type: string
example: https://example.com/@user 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: wallet_address:
description: Ethereum wallet address. description: Ethereum wallet address.
type: string type: string
@ -685,6 +695,19 @@ components:
url: url:
description: The location of the original full-size attachment. description: The location of the original full-size attachment.
type: string 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: Instance:
type: object type: object
properties: properties:

View file

@ -0,0 +1 @@
ALTER TABLE actor_profile ADD COLUMN identity_proofs JSONB NOT NULL DEFAULT '[]';

View file

@ -7,6 +7,7 @@ CREATE TABLE actor_profile (
bio_source TEXT, bio_source TEXT,
avatar_file_name VARCHAR(100), avatar_file_name VARCHAR(100),
banner_file_name VARCHAR(100), banner_file_name VARCHAR(100),
identity_proofs JSONB NOT NULL DEFAULT '[]',
extra_fields JSONB NOT NULL DEFAULT '[]', extra_fields JSONB NOT NULL DEFAULT '[]',
follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0, follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0,
following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0, following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0,

View file

@ -159,6 +159,7 @@ pub async fn fetch_profile_by_actor_id(
bio: actor.summary.clone(), bio: actor.summary.clone(),
avatar, avatar,
banner, banner,
identity_proofs: vec![],
extra_fields, extra_fields,
actor_json: Some(actor), actor_json: Some(actor),
}; };

View file

@ -720,6 +720,7 @@ pub async fn receive_activity(
bio_source: actor.summary.clone(), bio_source: actor.summary.clone(),
avatar, avatar,
banner, banner,
identity_proofs: vec![],
extra_fields, extra_fields,
actor_json: Some(actor), actor_json: Some(actor),
}; };

View file

@ -5,9 +5,11 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::errors::ValidationError; use crate::errors::ValidationError;
use crate::models::profiles::currencies::get_identity_proof_field_name;
use crate::models::profiles::types::{ use crate::models::profiles::types::{
DbActorProfile, DbActorProfile,
ExtraField, ExtraField,
IdentityProof,
ProfileUpdateData, ProfileUpdateData,
}; };
use crate::models::profiles::validators::validate_username; 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}; use crate::utils::files::{FileError, save_validated_b64_file, get_file_url};
/// https://docs.joinmastodon.org/entities/field/
#[derive(Serialize)] #[derive(Serialize)]
pub struct AccountField { pub struct AccountField {
pub name: String, pub name: String,
pub value: String, pub value: String,
verified_at: Option<DateTime<Utc>>,
} }
/// https://docs.joinmastodon.org/entities/source/ /// https://docs.joinmastodon.org/entities/source/
@ -42,6 +46,7 @@ pub struct Account {
pub note: Option<String>, pub note: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub header: Option<String>, pub header: Option<String>,
pub identity_proofs: Vec<AccountField>,
pub fields: Vec<AccountField>, pub fields: Vec<AccountField>,
pub followers_count: i32, pub followers_count: i32,
pub following_count: i32, pub following_count: i32,
@ -59,9 +64,30 @@ impl Account {
.map(|name| get_file_url(instance_url, name)); .map(|name| get_file_url(instance_url, name));
let header_url = profile.banner_file_name.as_ref() let header_url = profile.banner_file_name.as_ref()
.map(|name| get_file_url(instance_url, name)); .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 }) let mut identity_proofs = vec![];
.collect(); 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 { Self {
id: profile.id, id: profile.id,
username: profile.username, username: profile.username,
@ -72,7 +98,8 @@ impl Account {
note: profile.bio, note: profile.bio,
avatar: avatar_url, avatar: avatar_url,
header: header_url, header: header_url,
fields, identity_proofs,
fields: extra_fields,
followers_count: profile.follower_count, followers_count: profile.follower_count,
following_count: profile.following_count, following_count: profile.following_count,
statuses_count: profile.post_count, statuses_count: profile.post_count,
@ -87,6 +114,7 @@ impl Account {
.map(|field| AccountField { .map(|field| AccountField {
name: field.name, name: field.name,
value: field.value_source.unwrap_or(field.value), value: field.value_source.unwrap_or(field.value),
verified_at: None,
}) })
.collect(); .collect();
let source = Source { let source = Source {
@ -163,6 +191,7 @@ impl AccountUpdateData {
self, self,
current_avatar: &Option<String>, current_avatar: &Option<String>,
current_banner: &Option<String>, current_banner: &Option<String>,
current_identity_proofs: &[IdentityProof],
media_dir: &Path, media_dir: &Path,
) -> Result<ProfileUpdateData, FileError> { ) -> Result<ProfileUpdateData, FileError> {
let avatar = process_b64_image_field_value( let avatar = process_b64_image_field_value(
@ -171,6 +200,7 @@ impl AccountUpdateData {
let banner = process_b64_image_field_value( let banner = process_b64_image_field_value(
self.header, current_banner.clone(), media_dir, 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 extra_fields = self.fields_attributes.unwrap_or(vec![]);
let profile_data = ProfileUpdateData { let profile_data = ProfileUpdateData {
display_name: self.display_name, display_name: self.display_name,
@ -178,8 +208,9 @@ impl AccountUpdateData {
bio_source: self.note_source, bio_source: self.note_source,
avatar, avatar,
banner, banner,
identity_proofs,
extra_fields, extra_fields,
actor_json: None, actor_json: None, // always None for local profiles
}; };
Ok(profile_data) Ok(profile_data)
} }

View file

@ -15,6 +15,7 @@ use crate::errors::{DatabaseError, HttpError, ValidationError};
use crate::ethereum::eip4361::verify_eip4361_signature; use crate::ethereum::eip4361::verify_eip4361_signature;
use crate::ethereum::gate::is_allowed_user; use crate::ethereum::gate::is_allowed_user;
use crate::ethereum::identity::{ use crate::ethereum::identity::{
ETHEREUM_EIP191_PROOF,
DidPkh, DidPkh,
create_identity_claim, create_identity_claim,
verify_identity_proof, verify_identity_proof,
@ -32,6 +33,7 @@ use crate::models::profiles::queries::{
get_wallet_address, get_wallet_address,
update_profile, update_profile,
}; };
use crate::models::profiles::types::{IdentityProof, ProfileUpdateData};
use crate::models::relationships::queries::{ use crate::models::relationships::queries::{
create_follow_request, create_follow_request,
follow, follow,
@ -184,6 +186,7 @@ async fn update_credentials(
.into_profile_data( .into_profile_data(
&current_user.profile.avatar_file_name, &current_user.profile.avatar_file_name,
&current_user.profile.banner_file_name, &current_user.profile.banner_file_name,
&current_user.profile.identity_proofs.into_inner(),
&config.media_dir(), &config.media_dir(),
) )
.map_err(|err| { .map_err(|err| {
@ -246,7 +249,7 @@ async fn create_identity_proof(
proof_data: web::Json<IdentityProofData>, proof_data: web::Json<IdentityProofData>,
) -> Result<HttpResponse, HttpError> { ) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?; 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 actor_id = current_user.profile.actor_id(&config.instance_url());
let wallet_address = current_user.wallet_address.as_ref() let wallet_address = current_user.wallet_address.as_ref()
.ok_or(HttpError::PermissionError)?; .ok_or(HttpError::PermissionError)?;
@ -256,6 +259,29 @@ async fn create_identity_proof(
&did, &did,
&proof_data.signature, &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(&current_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,
&current_user.id,
profile_data,
).await?;
let account = Account::from_user(current_user, &config.instance_url()); let account = Account::from_user(current_user, &config.instance_url());
Ok(HttpResponse::Ok().json(account)) Ok(HttpResponse::Ok().json(account))
} }

View 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)
}

View file

@ -1,3 +1,4 @@
pub mod currencies;
pub mod queries; pub mod queries;
pub mod types; pub mod types;
pub mod validators; pub mod validators;

View file

@ -10,10 +10,11 @@ use crate::models::cleanup::{
}; };
use crate::models::relationships::types::RelationshipType; use crate::models::relationships::types::RelationshipType;
use crate::utils::id::new_uuid; use crate::utils::id::new_uuid;
use super::currencies::get_currency_field_name;
use super::types::{ use super::types::{
get_currency_field_name,
ExtraFields,
DbActorProfile, DbActorProfile,
ExtraFields,
IdentityProofs,
ProfileCreateData, ProfileCreateData,
ProfileUpdateData, ProfileUpdateData,
}; };
@ -24,15 +25,15 @@ pub async fn create_profile(
profile_data: ProfileCreateData, profile_data: ProfileCreateData,
) -> Result<DbActorProfile, DatabaseError> { ) -> Result<DbActorProfile, DatabaseError> {
let profile_id = new_uuid(); let profile_id = new_uuid();
let extra_fields = ExtraFields(profile_data.extra_fields.clone());
let row = db_client.query_one( let row = db_client.query_one(
" "
INSERT INTO actor_profile ( INSERT INTO actor_profile (
id, username, display_name, acct, bio, bio_source, 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 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 RETURNING actor_profile
", ",
&[ &[
@ -44,7 +45,8 @@ pub async fn create_profile(
&profile_data.bio, &profile_data.bio,
&profile_data.avatar, &profile_data.avatar,
&profile_data.banner, &profile_data.banner,
&extra_fields, &IdentityProofs(profile_data.identity_proofs),
&ExtraFields(profile_data.extra_fields),
&profile_data.actor_json, &profile_data.actor_json,
], ],
).await.map_err(catch_unique_violation("profile"))?; ).await.map_err(catch_unique_violation("profile"))?;
@ -66,9 +68,10 @@ pub async fn update_profile(
bio_source = $3, bio_source = $3,
avatar_file_name = $4, avatar_file_name = $4,
banner_file_name = $5, banner_file_name = $5,
extra_fields = $6, identity_proofs = $6,
actor_json = $7 extra_fields = $7,
WHERE id = $8 actor_json = $8
WHERE id = $9
RETURNING actor_profile RETURNING actor_profile
", ",
&[ &[
@ -77,6 +80,7 @@ pub async fn update_profile(
&data.bio_source, &data.bio_source,
&data.avatar, &data.avatar,
&data.banner, &data.banner,
&IdentityProofs(data.identity_proofs),
&ExtraFields(data.extra_fields), &ExtraFields(data.extra_fields),
&data.actor_json, &data.actor_json,
&profile_id, &profile_id,
@ -490,6 +494,8 @@ mod tests {
let db_client = create_test_database().await; let db_client = create_test_database().await;
let profile = create_profile(&db_client, profile_data).await.unwrap(); let profile = create_profile(&db_client, profile_data).await.unwrap();
assert_eq!(profile.username, "test"); 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] #[tokio::test]

View file

@ -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_from_sql!(ExtraFields);
json_to_sql!(ExtraFields); json_to_sql!(ExtraFields);
@ -73,6 +69,7 @@ pub struct DbActorProfile {
pub bio_source: Option<String>, // plaintext or markdown pub bio_source: Option<String>, // plaintext or markdown
pub avatar_file_name: Option<String>, pub avatar_file_name: Option<String>,
pub banner_file_name: Option<String>, pub banner_file_name: Option<String>,
pub identity_proofs: IdentityProofs,
pub extra_fields: ExtraFields, pub extra_fields: ExtraFields,
pub follower_count: i32, pub follower_count: i32,
pub following_count: i32, pub following_count: i32,
@ -128,6 +125,7 @@ impl Default for DbActorProfile {
bio_source: None, bio_source: None,
avatar_file_name: None, avatar_file_name: None,
banner_file_name: None, banner_file_name: None,
identity_proofs: IdentityProofs(vec![]),
extra_fields: ExtraFields(vec![]), extra_fields: ExtraFields(vec![]),
follower_count: 0, follower_count: 0,
following_count: 0, following_count: 0,
@ -147,6 +145,7 @@ pub struct ProfileCreateData {
pub bio: Option<String>, pub bio: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub banner: Option<String>, pub banner: Option<String>,
pub identity_proofs: Vec<IdentityProof>,
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
pub actor_json: Option<Actor>, pub actor_json: Option<Actor>,
} }
@ -172,6 +171,7 @@ pub struct ProfileUpdateData {
pub bio_source: Option<String>, pub bio_source: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub banner: Option<String>, pub banner: Option<String>,
pub identity_proofs: Vec<IdentityProof>,
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
pub actor_json: Option<Actor>, 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)] #[cfg(test)]
mod tests { mod tests {
use crate::activitypub::actor::Actor; use crate::activitypub::actor::Actor;

View file

@ -80,6 +80,7 @@ pub async fn create_user(
bio: None, bio: None,
avatar: None, avatar: None,
banner: None, banner: None,
identity_proofs: vec![],
extra_fields: vec![], extra_fields: vec![],
actor_json: None, actor_json: None,
}; };