Create API endpoint for managing client configurations

This commit is contained in:
silverpill 2023-04-05 23:31:08 +00:00
parent 01494f1770
commit fc82c83421
8 changed files with 139 additions and 2 deletions

View file

@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Created API endpoint for adding aliases. - Created API endpoint for adding aliases.
- Populate `alsoKnownAs` property on actor object with declared aliases. - Populate `alsoKnownAs` property on actor object with declared aliases.
- Support account migration from Mastodon. - Support account migration from Mastodon.
- Created API endpoint for managing client configurations.
### Changed ### Changed

View file

@ -780,6 +780,29 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/Notification' $ref: '#/components/schemas/Notification'
/api/v1/settings/client_config:
post:
summary: Update client configuration.
security:
- tokenAuth: []
requestBody:
content:
application/json:
schema:
description: |
Client configuration.
Should contain a single key identifying type of client.
type: object
example: {"mitra-web":{"theme":"dark"}}
responses:
200:
description: Successful operation.
content:
application/json:
schema:
$ref: '#/components/schemas/CredentialAccount'
400:
description: Invalid request data.
/api/v1/settings/change_password: /api/v1/settings/change_password:
post: post:
summary: Set or change user's password. summary: Set or change user's password.
@ -1504,6 +1527,10 @@ components:
role: role:
description: The role assigned to the currently authorized user. description: The role assigned to the currently authorized user.
$ref: '#/components/schemas/Role' $ref: '#/components/schemas/Role'
client_config:
description: Client configurations.
type: object
example: {"mitra-web":{"theme":"dark"}}
ActivityParameters: ActivityParameters:
type: object type: object
properties: properties:

View file

@ -0,0 +1 @@
ALTER TABLE user_account ADD COLUMN client_config JSONB NOT NULL DEFAULT '{}';

View file

@ -58,6 +58,7 @@ CREATE TABLE user_account (
private_key TEXT NOT NULL, private_key TEXT NOT NULL,
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL, invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL,
user_role SMALLINT NOT NULL, user_role SMALLINT NOT NULL,
client_config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
); );

View file

@ -1,3 +1,4 @@
use serde_json::{Value as JsonValue};
use uuid::Uuid; use uuid::Uuid;
use mitra_utils::{ use mitra_utils::{
@ -17,6 +18,8 @@ use crate::profiles::{
}; };
use super::types::{ use super::types::{
ClientConfig,
DbClientConfig,
DbInviteCode, DbInviteCode,
DbUser, DbUser,
Role, Role,
@ -189,6 +192,26 @@ pub async fn set_user_role(
Ok(()) Ok(())
} }
pub async fn update_client_config(
db_client: &impl DatabaseClient,
user_id: &Uuid,
client_name: &str,
client_config_value: &JsonValue,
) -> Result<ClientConfig, DatabaseError> {
let maybe_row = db_client.query_opt(
"
UPDATE user_account
SET client_config = jsonb_set(client_config, ARRAY[$1], $2, true)
WHERE id = $3
RETURNING client_config
",
&[&client_name, &client_config_value, &user_id],
).await?;
let row = maybe_row.ok_or(DatabaseError::NotFound("user"))?;
let client_config: DbClientConfig = row.try_get("client_config")?;
Ok(client_config.into_inner())
}
pub async fn get_user_by_id( pub async fn get_user_by_id(
db_client: &impl DatabaseClient, db_client: &impl DatabaseClient,
user_id: &Uuid, user_id: &Uuid,
@ -308,6 +331,7 @@ pub async fn get_user_count(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json::json;
use serial_test::serial; use serial_test::serial;
use crate::database::test_utils::create_test_database; use crate::database::test_utils::create_test_database;
use crate::users::types::Role; use crate::users::types::Role;
@ -362,4 +386,25 @@ mod tests {
let user = get_user_by_id(db_client, &user.id).await.unwrap(); let user = get_user_by_id(db_client, &user.id).await.unwrap();
assert_eq!(user.role, Role::ReadOnlyUser); assert_eq!(user.role, Role::ReadOnlyUser);
} }
#[tokio::test]
#[serial]
async fn test_update_client_config() {
let db_client = &mut create_test_database().await;
let user_data = UserCreateData::default();
let user = create_user(db_client, user_data).await.unwrap();
assert_eq!(user.client_config.is_empty(), true);
let client_name = "test";
let client_config_value = json!({"a": 1});
let client_config = update_client_config(
db_client,
&user.id,
client_name,
&client_config_value,
).await.unwrap();
assert_eq!(
client_config.get(client_name).unwrap(),
&client_config_value,
);
}
} }

View file

@ -1,5 +1,9 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use postgres_types::FromSql; use postgres_types::FromSql;
use serde::Deserialize;
use serde_json::{Value as JsonValue};
use uuid::Uuid; use uuid::Uuid;
use mitra_utils::{ use mitra_utils::{
@ -9,6 +13,7 @@ use mitra_utils::{
use crate::database::{ use crate::database::{
int_enum::{int_enum_from_sql, int_enum_to_sql}, int_enum::{int_enum_from_sql, int_enum_to_sql},
json_macro::json_from_sql,
DatabaseTypeError, DatabaseTypeError,
}; };
use crate::profiles::types::DbActorProfile; use crate::profiles::types::DbActorProfile;
@ -100,6 +105,20 @@ impl TryFrom<i16> for Role {
int_enum_from_sql!(Role); int_enum_from_sql!(Role);
int_enum_to_sql!(Role); int_enum_to_sql!(Role);
pub type ClientConfig = HashMap<String, JsonValue>;
#[derive(Deserialize)]
pub struct DbClientConfig(ClientConfig);
impl DbClientConfig {
pub fn into_inner(self) -> ClientConfig {
let Self(client_config) = self;
client_config
}
}
json_from_sql!(DbClientConfig);
#[allow(dead_code)] #[allow(dead_code)]
#[derive(FromSql)] #[derive(FromSql)]
#[postgres(name = "user_account")] #[postgres(name = "user_account")]
@ -110,6 +129,7 @@ pub struct DbUser {
private_key: String, private_key: String,
invite_code: Option<String>, invite_code: Option<String>,
user_role: Role, user_role: Role,
client_config: DbClientConfig,
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
} }
@ -122,6 +142,7 @@ pub struct User {
pub password_hash: Option<String>, pub password_hash: Option<String>,
pub private_key: String, pub private_key: String,
pub role: Role, pub role: Role,
pub client_config: ClientConfig,
pub profile: DbActorProfile, pub profile: DbActorProfile,
} }
@ -137,6 +158,7 @@ impl User {
password_hash: db_user.password_hash, password_hash: db_user.password_hash,
private_key: db_user.private_key, private_key: db_user.private_key,
role: db_user.user_role, role: db_user.user_role,
client_config: db_user.client_config.into_inner(),
profile: db_profile, profile: db_profile,
} }
} }

View file

@ -14,8 +14,9 @@ use mitra_models::{
}, },
subscriptions::types::Subscription, subscriptions::types::Subscription,
users::types::{ users::types::{
Role, ClientConfig,
Permission, Permission,
Role,
User, User,
}, },
}; };
@ -126,6 +127,7 @@ pub struct Account {
// CredentialAccount attributes // CredentialAccount attributes
pub source: Option<Source>, pub source: Option<Source>,
pub role: Option<ApiRole>, pub role: Option<ApiRole>,
pub client_config: Option<ClientConfig>,
} }
impl Account { impl Account {
@ -220,6 +222,7 @@ impl Account {
statuses_count: profile.post_count, statuses_count: profile.post_count,
source: None, source: None,
role: None, role: None,
client_config: None,
} }
} }
@ -248,6 +251,7 @@ impl Account {
); );
account.source = Some(source); account.source = Some(source);
account.role = Some(role); account.role = Some(role);
account.client_config = Some(user.client_config);
account account
} }
} }

View file

@ -18,7 +18,11 @@ use mitra_models::{
update_profile, update_profile,
}, },
profiles::types::ProfileUpdateData, profiles::types::ProfileUpdateData,
users::queries::set_user_password, users::queries::{
set_user_password,
update_client_config,
},
users::types::ClientConfig,
}; };
use mitra_utils::passwords::hash_password; use mitra_utils::passwords::hash_password;
@ -48,6 +52,37 @@ use super::types::{
PasswordChangeRequest, PasswordChangeRequest,
}; };
// Similar to Pleroma settings store
// https://docs-develop.pleroma.social/backend/development/API/differences_in_mastoapi_responses/#pleroma-settings-store
#[post("/client_config")]
async fn client_config_view(
auth: BearerAuth,
connection_info: ConnectionInfo,
config: web::Data<Config>,
db_pool: web::Data<DbPool>,
request_data: web::Json<ClientConfig>,
) -> Result<HttpResponse, MastodonError> {
let db_client = &**get_database_client(&db_pool).await?;
let mut current_user = get_current_user(db_client, auth.token()).await?;
if request_data.len() != 1 {
return Err(ValidationError("can't update more than one config").into());
};
let (client_name, client_config_value) =
request_data.iter().next().expect("hashmap entry should exist");
current_user.client_config = update_client_config(
db_client,
&current_user.id,
client_name,
client_config_value,
).await?;
let account = Account::from_user(
&get_request_base_url(connection_info),
&config.instance_url(),
current_user,
);
Ok(HttpResponse::Ok().json(account))
}
#[post("/change_password")] #[post("/change_password")]
async fn change_password_view( async fn change_password_view(
auth: BearerAuth, auth: BearerAuth,
@ -231,6 +266,7 @@ async fn move_followers(
pub fn settings_api_scope() -> Scope { pub fn settings_api_scope() -> Scope {
web::scope("/api/v1/settings") web::scope("/api/v1/settings")
.service(client_config_view)
.service(change_password_view) .service(change_password_view)
.service(add_alias_view) .service(add_alias_view)
.service(export_followers_view) .service(export_followers_view)