mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a835c1b71 |
Generated
+1
@@ -5490,6 +5490,7 @@ dependencies = [
|
||||
"jemalloc_pprof",
|
||||
"json-patch 4.1.0",
|
||||
"labrinth",
|
||||
"language-tags",
|
||||
"lettre",
|
||||
"meilisearch-sdk",
|
||||
"modrinth-content-management",
|
||||
|
||||
@@ -110,6 +110,7 @@ image = { version = "0.25.8", default-features = false, features = ["rayon"] }
|
||||
indexmap = "2.11.4"
|
||||
indicatif = "0.18.0"
|
||||
itertools = "0.14.0"
|
||||
language-tags = "0.3.2"
|
||||
jemalloc_pprof = "0.8.1"
|
||||
json-patch = { version = "4.1.0", default-features = false }
|
||||
lettre = { version = "0.11.19", default-features = false, features = [
|
||||
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tINSERT INTO user_preferences (user_id, preferences)\n\t\t\tVALUES ($1, $2)\n\t\t\tON CONFLICT (user_id) DO UPDATE\n\t\t\tSET preferences = EXCLUDED.preferences\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "121602731987ccfa487f46bb86bb62cd913c5760c6ac0e6d1abda818673e83f6"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tINSERT INTO user_preferences (user_id, preferences)\n\t\t\tVALUES ($1, $2)\n\t\t\tON CONFLICT (user_id) DO NOTHING\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "20616b23f6948c1efdce94e44922d09daa809cb04f6de1824c3e9b924f193ba7"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT preferences AS \"preferences: Json<UserPreferences>\"\n\t\t\tFROM user_preferences\n\t\t\tWHERE user_id = $1\n\t\t\tFOR UPDATE\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "preferences: Json<UserPreferences>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "59034736065e7bec7ed838a870a5d389108e90adfee265792395af483b84091a"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT preferences AS \"preferences: Json<UserPreferences>\"\n\t\t\tFROM user_preferences\n\t\t\tWHERE user_id = $1\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "preferences: Json<UserPreferences>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "674d05dc957468ad195d8704dfc66e035f5812244a7e69d81af1fa0989ffe9eb"
|
||||
}
|
||||
@@ -71,6 +71,7 @@ image = { workspace = true, features = [
|
||||
"webp",
|
||||
] }
|
||||
itertools = { workspace = true }
|
||||
language-tags = { workspace = true }
|
||||
json-patch = { workspace = true }
|
||||
lettre = { workspace = true }
|
||||
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE user_preferences (
|
||||
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
preferences JSONB NOT NULL,
|
||||
CONSTRAINT user_preferences_object CHECK (
|
||||
jsonb_typeof(preferences) = 'object'
|
||||
)
|
||||
);
|
||||
@@ -37,6 +37,7 @@ pub mod team_item;
|
||||
pub mod thread_item;
|
||||
pub mod user_item;
|
||||
pub mod user_limits;
|
||||
pub mod user_preferences_item;
|
||||
pub mod user_subscription_item;
|
||||
pub mod users_compliance;
|
||||
pub mod users_notifications_preferences_item;
|
||||
@@ -58,6 +59,7 @@ pub use team_item::DBTeam;
|
||||
pub use team_item::DBTeamMember;
|
||||
pub use thread_item::{DBThread, DBThreadMessage};
|
||||
pub use user_item::DBUser;
|
||||
pub use user_preferences_item::DBUserPreferences;
|
||||
pub use version_item::DBVersion;
|
||||
|
||||
pub use moderation_lock_item::{DBModerationLock, ModerationLockWithUser};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use sqlx::types::Json;
|
||||
|
||||
use crate::{
|
||||
database::{Executor, PgTransaction},
|
||||
models::v3::user_preferences::UserPreferences,
|
||||
};
|
||||
|
||||
use super::DBUserId;
|
||||
|
||||
pub struct DBUserPreferences;
|
||||
|
||||
impl DBUserPreferences {
|
||||
pub async fn get<'a, E>(
|
||||
user_id: DBUserId,
|
||||
exec: E,
|
||||
) -> Result<Option<UserPreferences>, sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let preferences = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT preferences AS "preferences: Json<UserPreferences>"
|
||||
FROM user_preferences
|
||||
WHERE user_id = $1
|
||||
"#,
|
||||
user_id.0,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?
|
||||
.map(|preferences| preferences.0);
|
||||
|
||||
Ok(preferences)
|
||||
}
|
||||
|
||||
pub async fn upsert<'a, E>(
|
||||
user_id: DBUserId,
|
||||
preferences: &UserPreferences,
|
||||
exec: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_preferences (user_id, preferences)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET preferences = EXCLUDED.preferences
|
||||
"#,
|
||||
user_id.0,
|
||||
Json(preferences) as Json<&UserPreferences>,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_if_absent(
|
||||
user_id: DBUserId,
|
||||
preferences: &UserPreferences,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_preferences (user_id, preferences)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id) DO NOTHING
|
||||
"#,
|
||||
user_id.0,
|
||||
Json(preferences) as Json<&UserPreferences>,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_for_update(
|
||||
user_id: DBUserId,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<UserPreferences, sqlx::Error> {
|
||||
let preferences = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT preferences AS "preferences: Json<UserPreferences>"
|
||||
FROM user_preferences
|
||||
WHERE user_id = $1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
user_id.0,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(preferences.0)
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@ pub mod sessions;
|
||||
pub mod teams;
|
||||
pub mod threads;
|
||||
pub mod user_limits;
|
||||
pub mod user_preferences;
|
||||
pub mod users;
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
use eyre::{Result, eyre};
|
||||
use language_tags::LanguageTag;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ThemePreferenceValue {
|
||||
Auto,
|
||||
Light,
|
||||
Dark,
|
||||
Oled,
|
||||
Retro,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LayoutPreferenceValue {
|
||||
Grid,
|
||||
Rows,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FriendRequestSource {
|
||||
None,
|
||||
Mutuals,
|
||||
Everyone,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SharedInstanceSource {
|
||||
None,
|
||||
Friends,
|
||||
Everyone,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ThemePreference {
|
||||
pub value: ThemePreferenceValue,
|
||||
pub sync: bool,
|
||||
}
|
||||
|
||||
impl Default for ThemePreference {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: ThemePreferenceValue::Auto,
|
||||
sync: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LanguagePreference {
|
||||
pub value: String,
|
||||
pub sync: bool,
|
||||
}
|
||||
|
||||
impl Default for LanguagePreference {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: "en-US".to_string(),
|
||||
sync: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LayoutPreferences {
|
||||
pub sync: bool,
|
||||
pub mods: LayoutPreferenceValue,
|
||||
pub plugins: LayoutPreferenceValue,
|
||||
pub datapacks: LayoutPreferenceValue,
|
||||
pub shaders: LayoutPreferenceValue,
|
||||
pub resourcepacks: LayoutPreferenceValue,
|
||||
pub modpacks: LayoutPreferenceValue,
|
||||
pub servers: LayoutPreferenceValue,
|
||||
pub users: LayoutPreferenceValue,
|
||||
}
|
||||
|
||||
impl Default for LayoutPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sync: true,
|
||||
mods: LayoutPreferenceValue::Rows,
|
||||
plugins: LayoutPreferenceValue::Rows,
|
||||
datapacks: LayoutPreferenceValue::Rows,
|
||||
shaders: LayoutPreferenceValue::Grid,
|
||||
resourcepacks: LayoutPreferenceValue::Grid,
|
||||
modpacks: LayoutPreferenceValue::Rows,
|
||||
servers: LayoutPreferenceValue::Rows,
|
||||
users: LayoutPreferenceValue::Rows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SidebarPreferences {
|
||||
pub sync: bool,
|
||||
pub right_aligned_search: bool,
|
||||
pub left_aligned_content: bool,
|
||||
}
|
||||
|
||||
impl Default for SidebarPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sync: false,
|
||||
right_aligned_search: false,
|
||||
left_aligned_content: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SocialPreferences {
|
||||
pub friend_request_sources: FriendRequestSource,
|
||||
pub shared_instance_sources: SharedInstanceSource,
|
||||
}
|
||||
|
||||
impl Default for SocialPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
friend_request_sources: FriendRequestSource::Everyone,
|
||||
shared_instance_sources: SharedInstanceSource::Friends,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UserPreferences {
|
||||
pub theme: ThemePreference,
|
||||
pub language: LanguagePreference,
|
||||
pub layouts: LayoutPreferences,
|
||||
pub sidebars: SidebarPreferences,
|
||||
pub social: SocialPreferences,
|
||||
}
|
||||
|
||||
impl Default for UserPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: ThemePreference::default(),
|
||||
language: LanguagePreference::default(),
|
||||
layouts: LayoutPreferences::default(),
|
||||
sidebars: SidebarPreferences::default(),
|
||||
social: SocialPreferences::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserPreferences {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
validate_language(&self.language.value)
|
||||
}
|
||||
|
||||
pub fn apply_patch(&mut self, patch: UserPreferencesPatch) {
|
||||
if let Some(theme) = patch.theme {
|
||||
if let Some(value) = theme.value {
|
||||
self.theme.value = value;
|
||||
}
|
||||
if let Some(sync) = theme.sync {
|
||||
self.theme.sync = sync;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(language) = patch.language {
|
||||
if let Some(value) = language.value {
|
||||
self.language.value = value;
|
||||
}
|
||||
if let Some(sync) = language.sync {
|
||||
self.language.sync = sync;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(layouts) = patch.layouts {
|
||||
if let Some(sync) = layouts.sync {
|
||||
self.layouts.sync = sync;
|
||||
}
|
||||
if let Some(mods) = layouts.mods {
|
||||
self.layouts.mods = mods;
|
||||
}
|
||||
if let Some(plugins) = layouts.plugins {
|
||||
self.layouts.plugins = plugins;
|
||||
}
|
||||
if let Some(datapacks) = layouts.datapacks {
|
||||
self.layouts.datapacks = datapacks;
|
||||
}
|
||||
if let Some(shaders) = layouts.shaders {
|
||||
self.layouts.shaders = shaders;
|
||||
}
|
||||
if let Some(resourcepacks) = layouts.resourcepacks {
|
||||
self.layouts.resourcepacks = resourcepacks;
|
||||
}
|
||||
if let Some(modpacks) = layouts.modpacks {
|
||||
self.layouts.modpacks = modpacks;
|
||||
}
|
||||
if let Some(servers) = layouts.servers {
|
||||
self.layouts.servers = servers;
|
||||
}
|
||||
if let Some(users) = layouts.users {
|
||||
self.layouts.users = users;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sidebars) = patch.sidebars {
|
||||
if let Some(sync) = sidebars.sync {
|
||||
self.sidebars.sync = sync;
|
||||
}
|
||||
if let Some(right_aligned_search) = sidebars.right_aligned_search {
|
||||
self.sidebars.right_aligned_search = right_aligned_search;
|
||||
}
|
||||
if let Some(left_aligned_content) = sidebars.left_aligned_content {
|
||||
self.sidebars.left_aligned_content = left_aligned_content;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(social) = patch.social {
|
||||
if let Some(friend_request_sources) = social.friend_request_sources
|
||||
{
|
||||
self.social.friend_request_sources = friend_request_sources;
|
||||
}
|
||||
if let Some(shared_instance_sources) =
|
||||
social.shared_instance_sources
|
||||
{
|
||||
self.social.shared_instance_sources = shared_instance_sources;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UserPreferencesPatch {
|
||||
pub theme: Option<ThemePreferencePatch>,
|
||||
pub language: Option<LanguagePreferencePatch>,
|
||||
pub layouts: Option<LayoutPreferencesPatch>,
|
||||
pub sidebars: Option<SidebarPreferencesPatch>,
|
||||
pub social: Option<SocialPreferencesPatch>,
|
||||
}
|
||||
|
||||
impl UserPreferencesPatch {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if let Some(language) = self
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| language.value.as_ref())
|
||||
{
|
||||
validate_language(language)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ThemePreferencePatch {
|
||||
pub value: Option<ThemePreferenceValue>,
|
||||
pub sync: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LanguagePreferencePatch {
|
||||
pub value: Option<String>,
|
||||
pub sync: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LayoutPreferencesPatch {
|
||||
pub sync: Option<bool>,
|
||||
pub mods: Option<LayoutPreferenceValue>,
|
||||
pub plugins: Option<LayoutPreferenceValue>,
|
||||
pub datapacks: Option<LayoutPreferenceValue>,
|
||||
pub shaders: Option<LayoutPreferenceValue>,
|
||||
pub resourcepacks: Option<LayoutPreferenceValue>,
|
||||
pub modpacks: Option<LayoutPreferenceValue>,
|
||||
pub servers: Option<LayoutPreferenceValue>,
|
||||
pub users: Option<LayoutPreferenceValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SidebarPreferencesPatch {
|
||||
pub sync: Option<bool>,
|
||||
pub right_aligned_search: Option<bool>,
|
||||
pub left_aligned_content: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SocialPreferencesPatch {
|
||||
pub friend_request_sources: Option<FriendRequestSource>,
|
||||
pub shared_instance_sources: Option<SharedInstanceSource>,
|
||||
}
|
||||
|
||||
fn validate_language(language: &str) -> Result<()> {
|
||||
LanguageTag::parse(language).map(|_| ()).map_err(|_| {
|
||||
eyre!("`language.value` must be a valid BCP 47 language tag")
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,7 @@ pub mod statistics;
|
||||
pub mod tags;
|
||||
pub mod teams;
|
||||
pub mod threads;
|
||||
pub mod user_preferences;
|
||||
pub mod users;
|
||||
pub mod version_creation;
|
||||
pub mod version_file;
|
||||
@@ -64,6 +65,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.configure(tags::config)
|
||||
.configure(teams::config)
|
||||
.configure(threads::config)
|
||||
.configure(user_preferences::config)
|
||||
.configure(users::config)
|
||||
.configure(version_file::config)
|
||||
.configure(versions::config)
|
||||
@@ -197,6 +199,9 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
threads::threads_get_route,
|
||||
threads::thread_send_message_route,
|
||||
threads::message_delete_route,
|
||||
user_preferences::get_user_preferences,
|
||||
user_preferences::put_user_preferences,
|
||||
user_preferences::patch_user_preferences,
|
||||
users::all_projects,
|
||||
users::admin_user_email,
|
||||
users::projects_list_route,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
use actix_web::{HttpRequest, get, patch, put, web};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::{
|
||||
auth::get_user_from_headers,
|
||||
database::{PgPool, ReadOnlyPgPool, models::DBUserPreferences},
|
||||
models::{
|
||||
pats::Scopes,
|
||||
v3::user_preferences::{UserPreferences, UserPreferencesPatch},
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
util::error::Context,
|
||||
};
|
||||
|
||||
use super::ApiError;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(get_user_preferences)
|
||||
.service(put_user_preferences)
|
||||
.service(patch_user_preferences);
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "users",
|
||||
responses((status = OK, body = UserPreferences))
|
||||
)]
|
||||
#[get("/user/preferences")]
|
||||
pub async fn get_user_preferences(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<UserPreferences>, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::USER_READ,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let preferences = DBUserPreferences::get(user.id.into(), &***ro_pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch user preferences")?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(web::Json(preferences))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "users",
|
||||
request_body = UserPreferences,
|
||||
responses((status = OK, body = UserPreferences))
|
||||
)]
|
||||
#[put("/user/preferences")]
|
||||
pub async fn put_user_preferences(
|
||||
req: HttpRequest,
|
||||
web::Json(preferences): web::Json<UserPreferences>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<UserPreferences>, ApiError> {
|
||||
preferences.validate().map_err(ApiError::Request)?;
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
DBUserPreferences::upsert(user.id.into(), &preferences, &**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to store user preferences")?;
|
||||
|
||||
Ok(web::Json(preferences))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "users",
|
||||
request_body = UserPreferencesPatch,
|
||||
responses((status = OK, body = UserPreferences))
|
||||
)]
|
||||
#[patch("/user/preferences")]
|
||||
pub async fn patch_user_preferences(
|
||||
req: HttpRequest,
|
||||
web::Json(patch): web::Json<UserPreferencesPatch>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<UserPreferences>, ApiError> {
|
||||
patch.validate().map_err(ApiError::Request)?;
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::USER_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let user_id = user.id.into();
|
||||
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to begin user preferences transaction")?;
|
||||
|
||||
DBUserPreferences::insert_if_absent(
|
||||
user_id,
|
||||
&UserPreferences::default(),
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to initialize user preferences")?;
|
||||
|
||||
let mut preferences =
|
||||
DBUserPreferences::get_for_update(user_id, &mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch user preferences for update")?;
|
||||
preferences.apply_patch(patch);
|
||||
|
||||
DBUserPreferences::upsert(user_id, &preferences, &mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to store user preferences")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit user preferences transaction")?;
|
||||
|
||||
Ok(web::Json(preferences))
|
||||
}
|
||||
Reference in New Issue
Block a user