mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Redesign sensitive info protection settings
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN feature_settings TEXT NULL AFTER model_capability_settings;
|
||||
|
||||
ALTER TABLE api_keys
|
||||
ADD COLUMN feature_settings TEXT NULL AFTER force_capabilities;
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN IF NOT EXISTS feature_settings jsonb;
|
||||
|
||||
ALTER TABLE public.api_keys
|
||||
ADD COLUMN IF NOT EXISTS feature_settings jsonb;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users ADD COLUMN feature_settings TEXT;
|
||||
ALTER TABLE api_keys ADD COLUMN feature_settings TEXT;
|
||||
@@ -177,6 +177,7 @@ CREATE TABLE IF NOT EXISTS public.api_keys (
|
||||
rate_limit integer DEFAULT 100,
|
||||
concurrent_limit integer,
|
||||
force_capabilities json,
|
||||
feature_settings jsonb,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
last_used_at timestamp with time zone,
|
||||
expires_at timestamp with time zone,
|
||||
@@ -1277,6 +1278,7 @@ CREATE TABLE IF NOT EXISTS public.users (
|
||||
allowed_models json,
|
||||
allowed_models_mode text DEFAULT 'unrestricted'::text NOT NULL,
|
||||
model_capability_settings json,
|
||||
feature_settings jsonb,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
is_deleted boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
|
||||
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
`allowed_api_formats` JSON,
|
||||
`allowed_api_formats_mode` VARCHAR(32) NOT NULL DEFAULT 'unrestricted',
|
||||
`model_capability_settings` JSON,
|
||||
`feature_settings` JSON,
|
||||
`rate_limit` INT,
|
||||
`rate_limit_mode` VARCHAR(32) NOT NULL DEFAULT 'system',
|
||||
`metadata` JSON,
|
||||
@@ -75,6 +76,7 @@ CREATE TABLE IF NOT EXISTS api_keys (
|
||||
`rate_limit` INT DEFAULT 100,
|
||||
`concurrent_limit` INT,
|
||||
`force_capabilities` JSON,
|
||||
`feature_settings` JSON,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`is_locked` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`is_standalone` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
|
||||
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS public.users (
|
||||
allowed_api_formats jsonb,
|
||||
allowed_api_formats_mode character varying(32) DEFAULT 'unrestricted' NOT NULL,
|
||||
model_capability_settings jsonb,
|
||||
feature_settings jsonb,
|
||||
rate_limit integer,
|
||||
rate_limit_mode character varying(32) DEFAULT 'system' NOT NULL,
|
||||
metadata jsonb,
|
||||
@@ -78,6 +79,7 @@ CREATE TABLE IF NOT EXISTS public.api_keys (
|
||||
rate_limit integer DEFAULT 100,
|
||||
concurrent_limit integer,
|
||||
force_capabilities jsonb,
|
||||
feature_settings jsonb,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
is_locked boolean DEFAULT false NOT NULL,
|
||||
is_standalone boolean DEFAULT false NOT NULL,
|
||||
|
||||
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
allowed_api_formats TEXT,
|
||||
allowed_api_formats_mode TEXT NOT NULL DEFAULT 'unrestricted',
|
||||
model_capability_settings TEXT,
|
||||
feature_settings TEXT,
|
||||
rate_limit INTEGER,
|
||||
rate_limit_mode TEXT NOT NULL DEFAULT 'system',
|
||||
metadata TEXT,
|
||||
@@ -73,6 +74,7 @@ CREATE TABLE IF NOT EXISTS api_keys (
|
||||
rate_limit INTEGER DEFAULT 100,
|
||||
concurrent_limit INTEGER,
|
||||
force_capabilities TEXT,
|
||||
feature_settings TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||||
is_standalone INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
@@ -97,6 +97,11 @@ name = "model_capability_settings"
|
||||
type = "json"
|
||||
nullable = true
|
||||
|
||||
[[table.users.columns]]
|
||||
name = "feature_settings"
|
||||
type = "json"
|
||||
nullable = true
|
||||
|
||||
[[table.users.columns]]
|
||||
name = "rate_limit"
|
||||
type = "int32"
|
||||
@@ -333,6 +338,11 @@ name = "force_capabilities"
|
||||
type = "json"
|
||||
nullable = true
|
||||
|
||||
[[table.api_keys.columns]]
|
||||
name = "feature_settings"
|
||||
type = "json"
|
||||
nullable = true
|
||||
|
||||
[[table.api_keys.columns]]
|
||||
name = "is_active"
|
||||
type = "bool"
|
||||
|
||||
@@ -7,7 +7,7 @@ use tracing::info;
|
||||
// Generated by build.rs from schema/bootstrap/postgres.
|
||||
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
||||
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260511130000;
|
||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260512000000;
|
||||
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
SELECT COUNT(*)::BIGINT
|
||||
|
||||
@@ -301,6 +301,7 @@ fn empty_database_snapshot_covers_current_cutoff_versions() {
|
||||
20260511000000,
|
||||
20260511120000,
|
||||
20260511130000,
|
||||
20260512000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -567,6 +568,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
||||
20260510120000,
|
||||
20260511120000,
|
||||
20260511130000,
|
||||
20260512000000,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -580,6 +582,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
||||
20260510120000,
|
||||
20260511120000,
|
||||
20260511130000,
|
||||
20260512000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1092,6 +1095,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
|
||||
20260511000000,
|
||||
20260511120000,
|
||||
20260511130000,
|
||||
20260512000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -941,6 +941,32 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
|
||||
Ok(Some(export.clone()))
|
||||
}
|
||||
|
||||
async fn set_user_api_key_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let mut index = self
|
||||
.index
|
||||
.write()
|
||||
.expect("auth api key snapshot repository lock");
|
||||
let Some(snapshot) = index.by_api_key_id.get(api_key_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if snapshot.user_id != user_id || snapshot.api_key_is_standalone {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(export) = index.export_by_api_key_id.get_mut(api_key_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
export.feature_settings = match feature_settings {
|
||||
Some(serde_json::Value::Null) | None => None,
|
||||
Some(value) => Some(value),
|
||||
};
|
||||
Ok(Some(export.clone()))
|
||||
}
|
||||
|
||||
async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -980,6 +1006,31 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
|
||||
index.touch_counts.remove(api_key_id);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn set_standalone_api_key_feature_settings(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let mut index = self
|
||||
.index
|
||||
.write()
|
||||
.expect("auth api key snapshot repository lock");
|
||||
let Some(snapshot) = index.by_api_key_id.get(api_key_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !snapshot.api_key_is_standalone {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(export) = index.export_by_api_key_id.get_mut(api_key_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
export.feature_settings = match feature_settings {
|
||||
Some(serde_json::Value::Null) | None => None,
|
||||
Some(value) => Some(value),
|
||||
};
|
||||
Ok(Some(export.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -52,6 +52,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
api_keys.expires_at AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -112,11 +113,11 @@ impl MysqlAuthApiKeyReadRepository {
|
||||
INSERT INTO api_keys (
|
||||
id, user_id, key_hash, key_encrypted, name, allowed_providers,
|
||||
allowed_api_formats, allowed_models, rate_limit, concurrent_limit,
|
||||
force_capabilities, is_active, expires_at, auto_delete_on_expiry,
|
||||
force_capabilities, feature_settings, is_active, expires_at, auto_delete_on_expiry,
|
||||
total_requests, total_tokens, total_cost_usd, is_standalone,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&record.api_key_id)
|
||||
@@ -642,6 +643,34 @@ WHERE id = ?
|
||||
self.reload_export_by_id(api_key_id).await
|
||||
}
|
||||
|
||||
async fn set_user_api_key_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE api_keys
|
||||
SET feature_settings = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
AND user_id = ?
|
||||
AND is_standalone = 0
|
||||
"#,
|
||||
)
|
||||
.bind(optional_json_to_string(
|
||||
&feature_settings,
|
||||
"api_keys.feature_settings",
|
||||
)?)
|
||||
.bind(current_unix_secs() as i64)
|
||||
.bind(api_key_id)
|
||||
.bind(user_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
self.reload_export_by_id(api_key_id).await
|
||||
}
|
||||
|
||||
async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -653,6 +682,31 @@ WHERE id = ?
|
||||
async fn delete_standalone_api_key(&self, api_key_id: &str) -> Result<bool, DataLayerError> {
|
||||
self.delete_api_key(api_key_id, None, true).await
|
||||
}
|
||||
|
||||
async fn set_standalone_api_key_feature_settings(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE api_keys
|
||||
SET feature_settings = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_standalone = 1
|
||||
"#,
|
||||
)
|
||||
.bind(optional_json_to_string(
|
||||
&feature_settings,
|
||||
"api_keys.feature_settings",
|
||||
)?)
|
||||
.bind(current_unix_secs() as i64)
|
||||
.bind(api_key_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
self.reload_export_by_id(api_key_id).await
|
||||
}
|
||||
}
|
||||
|
||||
impl MysqlAuthApiKeyReadRepository {
|
||||
@@ -875,6 +929,10 @@ fn map_auth_api_key_snapshot_row(
|
||||
fn map_auth_api_key_export_row(
|
||||
row: &MySqlRow,
|
||||
) -> Result<StoredAuthApiKeyExportRecord, DataLayerError> {
|
||||
let feature_settings = optional_json_from_string(
|
||||
row.try_get("feature_settings").map_sql_err()?,
|
||||
"api_keys.feature_settings",
|
||||
)?;
|
||||
StoredAuthApiKeyExportRecord::new(
|
||||
row.try_get("user_id").map_sql_err()?,
|
||||
row.try_get("api_key_id").map_sql_err()?,
|
||||
@@ -907,6 +965,7 @@ fn map_auth_api_key_export_row(
|
||||
row.try_get("total_cost_usd").map_sql_err()?,
|
||||
row.try_get("is_standalone").map_sql_err()?,
|
||||
)
|
||||
.map(|record| record.with_feature_settings(feature_settings))
|
||||
.and_then(|record| {
|
||||
record.with_activity_timestamps(
|
||||
row.try_get("last_used_at_unix_secs").map_sql_err()?,
|
||||
|
||||
@@ -146,6 +146,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -175,6 +176,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -203,6 +205,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -231,6 +234,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -259,6 +263,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -331,6 +336,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -366,6 +372,7 @@ INSERT INTO api_keys (
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
expires_at,
|
||||
is_locked,
|
||||
@@ -389,10 +396,11 @@ VALUES (
|
||||
$9,
|
||||
$10,
|
||||
$11,
|
||||
NULL,
|
||||
$12,
|
||||
FALSE,
|
||||
FALSE,
|
||||
$13,
|
||||
FALSE,
|
||||
FALSE,
|
||||
$14,
|
||||
$15,
|
||||
$16,
|
||||
@@ -412,6 +420,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -437,6 +446,7 @@ INSERT INTO api_keys (
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
expires_at,
|
||||
is_locked,
|
||||
@@ -460,10 +470,11 @@ VALUES (
|
||||
$9,
|
||||
$10,
|
||||
$11,
|
||||
NULL,
|
||||
$12,
|
||||
$13,
|
||||
FALSE,
|
||||
TRUE,
|
||||
$13,
|
||||
$14,
|
||||
$15,
|
||||
$16,
|
||||
@@ -483,6 +494,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -517,6 +529,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -555,6 +568,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -587,6 +601,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -618,6 +633,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -660,6 +676,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -692,6 +709,7 @@ RETURNING
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings,
|
||||
is_active,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
@@ -704,6 +722,25 @@ RETURNING
|
||||
is_standalone
|
||||
"#;
|
||||
|
||||
const SET_USER_API_KEY_FEATURE_SETTINGS_SQL: &str = r#"
|
||||
UPDATE api_keys
|
||||
SET
|
||||
feature_settings = $3,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $1
|
||||
AND id = $2
|
||||
AND is_standalone = FALSE
|
||||
"#;
|
||||
|
||||
const SET_STANDALONE_API_KEY_FEATURE_SETTINGS_SQL: &str = r#"
|
||||
UPDATE api_keys
|
||||
SET
|
||||
feature_settings = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND is_standalone = TRUE
|
||||
"#;
|
||||
|
||||
const NULL_USAGE_API_KEY_FK_SQL: &str = r#"
|
||||
UPDATE usage
|
||||
SET api_key_id = NULL
|
||||
@@ -1322,6 +1359,30 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
|
||||
row.as_ref().map(map_auth_api_key_export_row).transpose()
|
||||
}
|
||||
|
||||
async fn set_user_api_key_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let result = sqlx::query(SET_USER_API_KEY_FEATURE_SETTINGS_SQL)
|
||||
.bind(user_id)
|
||||
.bind(api_key_id)
|
||||
.bind(feature_settings)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let api_key_ids = [api_key_id.to_string()];
|
||||
Ok(self
|
||||
.list_export_api_keys_by_ids(&api_key_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|record| record.user_id == user_id && !record.is_standalone))
|
||||
}
|
||||
|
||||
async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -1353,6 +1414,28 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn set_standalone_api_key_feature_settings(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let result = sqlx::query(SET_STANDALONE_API_KEY_FEATURE_SETTINGS_SQL)
|
||||
.bind(api_key_id)
|
||||
.bind(feature_settings)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let api_key_ids = [api_key_id.to_string()];
|
||||
Ok(self
|
||||
.list_export_api_keys_by_ids(&api_key_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|record| record.is_standalone))
|
||||
}
|
||||
|
||||
async fn delete_standalone_api_key(&self, api_key_id: &str) -> Result<bool, DataLayerError> {
|
||||
let mut tx = self.pool.begin().await.map_postgres_err()?;
|
||||
sqlx::query(NULL_USAGE_API_KEY_FK_SQL)
|
||||
@@ -1419,6 +1502,7 @@ fn map_auth_api_key_snapshot_row(
|
||||
fn map_auth_api_key_export_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<StoredAuthApiKeyExportRecord, DataLayerError> {
|
||||
let feature_settings = row_get(row, "feature_settings")?;
|
||||
StoredAuthApiKeyExportRecord::new(
|
||||
row_get(row, "user_id")?,
|
||||
row_get(row, "api_key_id")?,
|
||||
@@ -1439,6 +1523,7 @@ fn map_auth_api_key_export_row(
|
||||
row_get(row, "total_cost_usd")?,
|
||||
row_get(row, "is_standalone")?,
|
||||
)
|
||||
.map(|record| record.with_feature_settings(feature_settings))
|
||||
.and_then(|record| {
|
||||
record.with_activity_timestamps(
|
||||
row_get(row, "last_used_at_unix_secs")?,
|
||||
|
||||
@@ -52,6 +52,7 @@ SELECT
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.feature_settings,
|
||||
api_keys.is_active,
|
||||
api_keys.expires_at AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
@@ -112,11 +113,11 @@ impl SqliteAuthApiKeyReadRepository {
|
||||
INSERT INTO api_keys (
|
||||
id, user_id, key_hash, key_encrypted, name, allowed_providers,
|
||||
allowed_api_formats, allowed_models, rate_limit, concurrent_limit,
|
||||
force_capabilities, is_active, expires_at, auto_delete_on_expiry,
|
||||
force_capabilities, feature_settings, is_active, expires_at, auto_delete_on_expiry,
|
||||
total_requests, total_tokens, total_cost_usd, is_standalone,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&record.api_key_id)
|
||||
@@ -642,6 +643,34 @@ WHERE id = ?
|
||||
self.reload_export_by_id(api_key_id).await
|
||||
}
|
||||
|
||||
async fn set_user_api_key_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE api_keys
|
||||
SET feature_settings = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
AND user_id = ?
|
||||
AND is_standalone = 0
|
||||
"#,
|
||||
)
|
||||
.bind(optional_json_to_string(
|
||||
&feature_settings,
|
||||
"api_keys.feature_settings",
|
||||
)?)
|
||||
.bind(current_unix_secs() as i64)
|
||||
.bind(api_key_id)
|
||||
.bind(user_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
self.reload_export_by_id(api_key_id).await
|
||||
}
|
||||
|
||||
async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -653,6 +682,31 @@ WHERE id = ?
|
||||
async fn delete_standalone_api_key(&self, api_key_id: &str) -> Result<bool, DataLayerError> {
|
||||
self.delete_api_key(api_key_id, None, true).await
|
||||
}
|
||||
|
||||
async fn set_standalone_api_key_feature_settings(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE api_keys
|
||||
SET feature_settings = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_standalone = 1
|
||||
"#,
|
||||
)
|
||||
.bind(optional_json_to_string(
|
||||
&feature_settings,
|
||||
"api_keys.feature_settings",
|
||||
)?)
|
||||
.bind(current_unix_secs() as i64)
|
||||
.bind(api_key_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
self.reload_export_by_id(api_key_id).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SqliteAuthApiKeyReadRepository {
|
||||
@@ -875,6 +929,10 @@ fn map_auth_api_key_snapshot_row(
|
||||
fn map_auth_api_key_export_row(
|
||||
row: &SqliteRow,
|
||||
) -> Result<StoredAuthApiKeyExportRecord, DataLayerError> {
|
||||
let feature_settings = optional_json_from_string(
|
||||
row.try_get("feature_settings").map_sql_err()?,
|
||||
"api_keys.feature_settings",
|
||||
)?;
|
||||
StoredAuthApiKeyExportRecord::new(
|
||||
row.try_get("user_id").map_sql_err()?,
|
||||
row.try_get("api_key_id").map_sql_err()?,
|
||||
@@ -907,6 +965,7 @@ fn map_auth_api_key_export_row(
|
||||
sqlite_real(row, "total_cost_usd")?,
|
||||
row.try_get("is_standalone").map_sql_err()?,
|
||||
)
|
||||
.map(|record| record.with_feature_settings(feature_settings))
|
||||
.and_then(|record| {
|
||||
record.with_activity_timestamps(
|
||||
row.try_get("last_used_at_unix_secs").map_sql_err()?,
|
||||
|
||||
@@ -335,6 +335,7 @@ pub struct StoredAuthApiKeyExportRecord {
|
||||
pub rate_limit: Option<i32>,
|
||||
pub concurrent_limit: Option<i32>,
|
||||
pub force_capabilities: Option<serde_json::Value>,
|
||||
pub feature_settings: Option<serde_json::Value>,
|
||||
pub is_active: bool,
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub auto_delete_on_expiry: bool,
|
||||
@@ -405,6 +406,7 @@ impl StoredAuthApiKeyExportRecord {
|
||||
rate_limit,
|
||||
concurrent_limit,
|
||||
force_capabilities,
|
||||
feature_settings: None,
|
||||
is_active,
|
||||
expires_at_unix_secs: expires_at_unix_secs
|
||||
.map(|value| parse_u64_i64(value, "api_keys.expires_at_unix_secs"))
|
||||
@@ -437,6 +439,11 @@ impl StoredAuthApiKeyExportRecord {
|
||||
.transpose()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_feature_settings(mut self, feature_settings: Option<serde_json::Value>) -> Self {
|
||||
self.feature_settings = normalize_optional_json(feature_settings);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
@@ -651,6 +658,13 @@ pub trait AuthApiKeyWriteRepository: Send + Sync {
|
||||
force_capabilities: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
|
||||
|
||||
async fn set_user_api_key_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
|
||||
|
||||
async fn delete_user_api_key(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -661,6 +675,12 @@ pub trait AuthApiKeyWriteRepository: Send + Sync {
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
) -> Result<bool, crate::DataLayerError>;
|
||||
|
||||
async fn set_standalone_api_key_feature_settings(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
feature_settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
|
||||
}
|
||||
|
||||
pub trait AuthRepository:
|
||||
@@ -738,6 +758,13 @@ fn parse_u64_i64(value: i64, field_name: &str) -> Result<u64, crate::DataLayerEr
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_optional_json(value: Option<serde_json::Value>) -> Option<serde_json::Value> {
|
||||
match value {
|
||||
Some(serde_json::Value::Null) | None => None,
|
||||
Some(value) => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct InMemoryUserReadRepository {
|
||||
preferences_by_user_id: RwLock<BTreeMap<String, StoredUserPreferenceRecord>>,
|
||||
sessions_by_id: RwLock<BTreeMap<String, StoredUserSessionRecord>>,
|
||||
model_settings_by_user_id: RwLock<BTreeMap<String, serde_json::Value>>,
|
||||
feature_settings_by_user_id: RwLock<BTreeMap<String, serde_json::Value>>,
|
||||
groups_by_id: RwLock<BTreeMap<String, StoredUserGroup>>,
|
||||
group_members: RwLock<BTreeMap<(String, String), chrono::DateTime<chrono::Utc>>>,
|
||||
export_rows: RwLock<Vec<StoredUserExportRow>>,
|
||||
@@ -61,6 +62,7 @@ impl InMemoryUserReadRepository {
|
||||
preferences_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
sessions_by_id: RwLock::new(BTreeMap::new()),
|
||||
model_settings_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
feature_settings_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
groups_by_id: RwLock::new(BTreeMap::new()),
|
||||
group_members: RwLock::new(BTreeMap::new()),
|
||||
export_rows: RwLock::new(Vec::new()),
|
||||
@@ -96,6 +98,7 @@ impl InMemoryUserReadRepository {
|
||||
preferences_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
sessions_by_id: RwLock::new(BTreeMap::new()),
|
||||
model_settings_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
feature_settings_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
groups_by_id: RwLock::new(BTreeMap::new()),
|
||||
group_members: RwLock::new(BTreeMap::new()),
|
||||
export_rows: RwLock::new(Vec::new()),
|
||||
@@ -117,6 +120,7 @@ impl InMemoryUserReadRepository {
|
||||
preferences_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
sessions_by_id: RwLock::new(BTreeMap::new()),
|
||||
model_settings_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
feature_settings_by_user_id: RwLock::new(BTreeMap::new()),
|
||||
groups_by_id: RwLock::new(BTreeMap::new()),
|
||||
group_members: RwLock::new(BTreeMap::new()),
|
||||
export_rows: RwLock::new(items.into_iter().collect()),
|
||||
@@ -366,6 +370,12 @@ fn memory_export_row_from_auth_user(
|
||||
.expect("user repository lock")
|
||||
.get(&user.id)
|
||||
.cloned();
|
||||
let feature_settings = repository
|
||||
.feature_settings_by_user_id
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.get(&user.id)
|
||||
.cloned();
|
||||
StoredUserExportRow::new(
|
||||
user.id.clone(),
|
||||
user.email.clone(),
|
||||
@@ -383,6 +393,7 @@ fn memory_export_row_from_auth_user(
|
||||
model_capability_settings,
|
||||
user.is_active,
|
||||
)?
|
||||
.with_feature_settings(feature_settings)
|
||||
.with_policy_modes(
|
||||
user.allowed_providers_mode.clone(),
|
||||
user.allowed_api_formats_mode.clone(),
|
||||
@@ -1530,6 +1541,58 @@ impl UserReadRepository for InMemoryUserReadRepository {
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
async fn update_user_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<serde_json::Value>, DataLayerError> {
|
||||
if self.read_only {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let user_exists = self
|
||||
.auth_by_id
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.contains_key(user_id)
|
||||
|| self
|
||||
.export_rows
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.iter()
|
||||
.any(|row| row.id == user_id);
|
||||
if !user_exists {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let normalized = normalize_optional_json_value(settings);
|
||||
let mut feature_settings_by_user = self
|
||||
.feature_settings_by_user_id
|
||||
.write()
|
||||
.expect("user repository lock");
|
||||
match normalized.clone() {
|
||||
Some(value) => {
|
||||
feature_settings_by_user.insert(user_id.to_string(), value);
|
||||
}
|
||||
None => {
|
||||
feature_settings_by_user.remove(user_id);
|
||||
}
|
||||
}
|
||||
drop(feature_settings_by_user);
|
||||
|
||||
if let Some(row) = self
|
||||
.export_rows
|
||||
.write()
|
||||
.expect("user repository lock")
|
||||
.iter_mut()
|
||||
.find(|row| row.id == user_id)
|
||||
{
|
||||
row.feature_settings = normalized.clone();
|
||||
}
|
||||
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
|
||||
@@ -42,6 +42,7 @@ SELECT
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
feature_settings,
|
||||
is_active
|
||||
FROM users
|
||||
"#;
|
||||
@@ -1177,6 +1178,29 @@ WHERE id = ?
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
async fn update_user_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<serde_json::Value>, DataLayerError> {
|
||||
let normalized = normalize_optional_json_value(settings);
|
||||
let result =
|
||||
sqlx::query("UPDATE users SET feature_settings = ?, updated_at = ? WHERE id = ?")
|
||||
.bind(optional_json_string(
|
||||
normalized.clone(),
|
||||
"users.feature_settings",
|
||||
)?)
|
||||
.bind(chrono::Utc::now().timestamp())
|
||||
.bind(user_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
@@ -1849,6 +1873,10 @@ fn map_user_row(row: &MySqlRow) -> Result<StoredUserSummary, DataLayerError> {
|
||||
}
|
||||
|
||||
fn map_user_export_row(row: &MySqlRow) -> Result<StoredUserExportRow, DataLayerError> {
|
||||
let feature_settings = optional_json_from_string(
|
||||
row.try_get("feature_settings").map_sql_err()?,
|
||||
"users.feature_settings",
|
||||
)?;
|
||||
StoredUserExportRow::new(
|
||||
row.try_get("id").map_sql_err()?,
|
||||
row.try_get("email").map_sql_err()?,
|
||||
@@ -1876,6 +1904,7 @@ fn map_user_export_row(row: &MySqlRow) -> Result<StoredUserExportRow, DataLayerE
|
||||
)?,
|
||||
row.try_get("is_active").map_sql_err()?,
|
||||
)
|
||||
.map(|record| record.with_feature_settings(feature_settings))
|
||||
.and_then(|record| {
|
||||
record.with_policy_modes(
|
||||
row.try_get("allowed_providers_mode").map_sql_err()?,
|
||||
|
||||
@@ -56,6 +56,7 @@ SELECT
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
feature_settings,
|
||||
is_active
|
||||
FROM users
|
||||
WHERE is_deleted IS FALSE
|
||||
@@ -81,6 +82,7 @@ SELECT
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
feature_settings,
|
||||
is_active
|
||||
FROM users
|
||||
WHERE is_deleted IS FALSE
|
||||
@@ -105,6 +107,7 @@ SELECT
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
feature_settings,
|
||||
is_active
|
||||
FROM users
|
||||
WHERE is_deleted IS FALSE
|
||||
@@ -154,6 +157,7 @@ SELECT
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
feature_settings,
|
||||
is_active
|
||||
FROM users
|
||||
WHERE is_deleted IS FALSE
|
||||
@@ -1658,6 +1662,31 @@ WHERE id = $1
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub async fn update_user_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<serde_json::Value>, DataLayerError> {
|
||||
let normalized = normalize_optional_json_value(settings);
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET feature_settings = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(normalized.clone())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
@@ -2112,6 +2141,7 @@ fn map_user_row(row: &sqlx::postgres::PgRow) -> Result<StoredUserSummary, DataLa
|
||||
}
|
||||
|
||||
fn map_user_export_row(row: &sqlx::postgres::PgRow) -> Result<StoredUserExportRow, DataLayerError> {
|
||||
let feature_settings = row.try_get("feature_settings").map_postgres_err()?;
|
||||
StoredUserExportRow::new(
|
||||
row.try_get("id").map_postgres_err()?,
|
||||
row.try_get("email").map_postgres_err()?,
|
||||
@@ -2128,6 +2158,7 @@ fn map_user_export_row(row: &sqlx::postgres::PgRow) -> Result<StoredUserExportRo
|
||||
.map_postgres_err()?,
|
||||
row.try_get("is_active").map_postgres_err()?,
|
||||
)
|
||||
.map(|record| record.with_feature_settings(feature_settings))
|
||||
.and_then(|record| {
|
||||
record.with_policy_modes(
|
||||
row.try_get("allowed_providers_mode").map_postgres_err()?,
|
||||
@@ -2602,6 +2633,14 @@ impl UserReadRepository for SqlxUserReadRepository {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_user_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<serde_json::Value>, DataLayerError> {
|
||||
self.update_user_feature_settings(user_id, settings).await
|
||||
}
|
||||
|
||||
async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
|
||||
@@ -42,6 +42,7 @@ SELECT
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
feature_settings,
|
||||
is_active
|
||||
FROM users
|
||||
"#;
|
||||
@@ -1177,6 +1178,29 @@ WHERE id = ?
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
async fn update_user_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<serde_json::Value>,
|
||||
) -> Result<Option<serde_json::Value>, DataLayerError> {
|
||||
let normalized = normalize_optional_json_value(settings);
|
||||
let result =
|
||||
sqlx::query("UPDATE users SET feature_settings = ?, updated_at = ? WHERE id = ?")
|
||||
.bind(optional_json_string(
|
||||
normalized.clone(),
|
||||
"users.feature_settings",
|
||||
)?)
|
||||
.bind(chrono::Utc::now().timestamp())
|
||||
.bind(user_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
@@ -1853,6 +1877,10 @@ fn map_user_row(row: &SqliteRow) -> Result<StoredUserSummary, DataLayerError> {
|
||||
}
|
||||
|
||||
fn map_user_export_row(row: &SqliteRow) -> Result<StoredUserExportRow, DataLayerError> {
|
||||
let feature_settings = optional_json_from_string(
|
||||
row.try_get("feature_settings").map_sql_err()?,
|
||||
"users.feature_settings",
|
||||
)?;
|
||||
StoredUserExportRow::new(
|
||||
row.try_get("id").map_sql_err()?,
|
||||
row.try_get("email").map_sql_err()?,
|
||||
@@ -1880,6 +1908,7 @@ fn map_user_export_row(row: &SqliteRow) -> Result<StoredUserExportRow, DataLayer
|
||||
)?,
|
||||
row.try_get("is_active").map_sql_err()?,
|
||||
)
|
||||
.map(|record| record.with_feature_settings(feature_settings))
|
||||
.and_then(|record| {
|
||||
record.with_policy_modes(
|
||||
row.try_get("allowed_providers_mode").map_sql_err()?,
|
||||
|
||||
@@ -236,6 +236,7 @@ pub struct StoredUserExportRow {
|
||||
pub rate_limit: Option<i32>,
|
||||
pub rate_limit_mode: String,
|
||||
pub model_capability_settings: Option<Value>,
|
||||
pub feature_settings: Option<Value>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
@@ -297,6 +298,7 @@ impl StoredUserExportRow {
|
||||
rate_limit,
|
||||
rate_limit_mode: "system".to_string(),
|
||||
model_capability_settings: normalize_optional_json(model_capability_settings),
|
||||
feature_settings: None,
|
||||
is_active,
|
||||
})
|
||||
.map(|record| record.with_legacy_policy_modes())
|
||||
@@ -322,6 +324,11 @@ impl StoredUserExportRow {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_feature_settings(mut self, feature_settings: Option<Value>) -> Self {
|
||||
self.feature_settings = normalize_optional_json(feature_settings);
|
||||
self
|
||||
}
|
||||
|
||||
fn with_legacy_policy_modes(mut self) -> Self {
|
||||
self.allowed_providers_mode = legacy_list_policy_mode(&self.allowed_providers);
|
||||
self.allowed_api_formats_mode = legacy_list_policy_mode(&self.allowed_api_formats);
|
||||
@@ -881,6 +888,12 @@ pub trait UserReadRepository: Send + Sync {
|
||||
settings: Option<Value>,
|
||||
) -> Result<Option<Value>, crate::DataLayerError>;
|
||||
|
||||
async fn update_user_feature_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
settings: Option<Value>,
|
||||
) -> Result<Option<Value>, crate::DataLayerError>;
|
||||
|
||||
async fn create_local_auth_user(
|
||||
&self,
|
||||
email: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user