feat(saas): wire llm_routing into account CRUD and auth responses
- Add llm_routing to all list_accounts/get_account SQL queries and JSON responses - Add llm_routing to UpdateAccountRequest with COALESCE update - Add llm_routing to AccountPublic struct in auth types - Wire llm_routing into register (default 'local'), login, and me handlers - Add llm_routing field to AccountRow, AccountAuthRow, AccountLoginRow model structs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)"
|
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)"
|
||||||
).bind(role).bind(status).bind(&pattern).fetch_one(db).await?;
|
).bind(role).bind(status).bind(&pattern).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)
|
FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)
|
||||||
ORDER BY created_at DESC LIMIT $4 OFFSET $5"
|
ORDER BY created_at DESC LIMIT $4 OFFSET $5"
|
||||||
).bind(role).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(role).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -35,7 +35,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2"
|
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2"
|
||||||
).bind(role).bind(status).fetch_one(db).await?;
|
).bind(role).bind(status).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE role = $1 AND status = $2
|
FROM accounts WHERE role = $1 AND status = $2
|
||||||
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||||
).bind(role).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(role).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -48,7 +48,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)"
|
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)"
|
||||||
).bind(role).bind(&pattern).fetch_one(db).await?;
|
).bind(role).bind(&pattern).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)
|
FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)
|
||||||
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||||
).bind(role).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(role).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -61,7 +61,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)"
|
"SELECT COUNT(*) FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)"
|
||||||
).bind(status).bind(&pattern).fetch_one(db).await?;
|
).bind(status).bind(&pattern).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)
|
FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)
|
||||||
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||||
).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -73,7 +73,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE role = $1"
|
"SELECT COUNT(*) FROM accounts WHERE role = $1"
|
||||||
).bind(role).fetch_one(db).await?;
|
).bind(role).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE role = $1
|
FROM accounts WHERE role = $1
|
||||||
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||||
).bind(role).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(role).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -85,7 +85,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE status = $1"
|
"SELECT COUNT(*) FROM accounts WHERE status = $1"
|
||||||
).bind(status).fetch_one(db).await?;
|
).bind(status).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE status = $1
|
FROM accounts WHERE status = $1
|
||||||
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||||
).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -98,7 +98,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)"
|
"SELECT COUNT(*) FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)"
|
||||||
).bind(&pattern).fetch_one(db).await?;
|
).bind(&pattern).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)
|
FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)
|
||||||
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||||
).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
@@ -110,7 +110,7 @@ pub async fn list_accounts(
|
|||||||
"SELECT COUNT(*) FROM accounts"
|
"SELECT COUNT(*) FROM accounts"
|
||||||
).fetch_one(db).await?;
|
).fetch_one(db).await?;
|
||||||
let rows = sqlx::query_as::<_, AccountRow>(
|
let rows = sqlx::query_as::<_, AccountRow>(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts ORDER BY created_at DESC LIMIT $1 OFFSET $2"
|
FROM accounts ORDER BY created_at DESC LIMIT $1 OFFSET $2"
|
||||||
).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||||
(total, rows)
|
(total, rows)
|
||||||
@@ -123,7 +123,7 @@ pub async fn list_accounts(
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"id": r.id, "username": r.username, "email": r.email, "display_name": r.display_name,
|
"id": r.id, "username": r.username, "email": r.email, "display_name": r.display_name,
|
||||||
"role": r.role, "status": r.status, "totp_enabled": r.totp_enabled,
|
"role": r.role, "status": r.status, "totp_enabled": r.totp_enabled,
|
||||||
"last_login_at": r.last_login_at, "created_at": r.created_at,
|
"last_login_at": r.last_login_at, "created_at": r.created_at, "llm_routing": r.llm_routing,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -134,7 +134,7 @@ pub async fn list_accounts(
|
|||||||
pub async fn get_account(db: &PgPool, account_id: &str) -> SaasResult<serde_json::Value> {
|
pub async fn get_account(db: &PgPool, account_id: &str) -> SaasResult<serde_json::Value> {
|
||||||
let row: Option<AccountRow> =
|
let row: Option<AccountRow> =
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at, llm_routing
|
||||||
FROM accounts WHERE id = $1"
|
FROM accounts WHERE id = $1"
|
||||||
)
|
)
|
||||||
.bind(account_id)
|
.bind(account_id)
|
||||||
@@ -146,7 +146,7 @@ pub async fn get_account(db: &PgPool, account_id: &str) -> SaasResult<serde_json
|
|||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"id": r.id, "username": r.username, "email": r.email, "display_name": r.display_name,
|
"id": r.id, "username": r.username, "email": r.email, "display_name": r.display_name,
|
||||||
"role": r.role, "status": r.status, "totp_enabled": r.totp_enabled,
|
"role": r.role, "status": r.status, "totp_enabled": r.totp_enabled,
|
||||||
"last_login_at": r.last_login_at, "created_at": r.created_at,
|
"last_login_at": r.last_login_at, "created_at": r.created_at, "llm_routing": r.llm_routing,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,13 +165,15 @@ pub async fn update_account(
|
|||||||
email = COALESCE($2, email),
|
email = COALESCE($2, email),
|
||||||
role = COALESCE($3, role),
|
role = COALESCE($3, role),
|
||||||
avatar_url = COALESCE($4, avatar_url),
|
avatar_url = COALESCE($4, avatar_url),
|
||||||
updated_at = $5
|
llm_routing = COALESCE($5, llm_routing),
|
||||||
WHERE id = $6"
|
updated_at = $6
|
||||||
|
WHERE id = $7"
|
||||||
)
|
)
|
||||||
.bind(req.display_name.as_deref())
|
.bind(req.display_name.as_deref())
|
||||||
.bind(req.email.as_deref())
|
.bind(req.email.as_deref())
|
||||||
.bind(req.role.as_deref())
|
.bind(req.role.as_deref())
|
||||||
.bind(req.avatar_url.as_deref())
|
.bind(req.avatar_url.as_deref())
|
||||||
|
.bind(req.llm_routing.as_deref())
|
||||||
.bind(&now)
|
.bind(&now)
|
||||||
.bind(account_id)
|
.bind(account_id)
|
||||||
.execute(db).await?;
|
.execute(db).await?;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub struct UpdateAccountRequest {
|
|||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub role: Option<String>,
|
pub role: Option<String>,
|
||||||
pub avatar_url: Option<String>,
|
pub avatar_url: Option<String>,
|
||||||
|
pub llm_routing: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ pub async fn register(
|
|||||||
let now = chrono::Utc::now().to_rfc3339();
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO accounts (id, username, email, password_hash, display_name, role, status, created_at, updated_at)
|
"INSERT INTO accounts (id, username, email, password_hash, display_name, role, status, created_at, updated_at, llm_routing)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $7)"
|
VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, $7, 'local')"
|
||||||
)
|
)
|
||||||
.bind(&account_id)
|
.bind(&account_id)
|
||||||
.bind(&req.username)
|
.bind(&req.username)
|
||||||
@@ -159,6 +159,7 @@ pub async fn register(
|
|||||||
status: "active".into(),
|
status: "active".into(),
|
||||||
totp_enabled: false,
|
totp_enabled: false,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
|
llm_routing: "local".into(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let jar = set_auth_cookies(jar, &resp.token, &refresh_token);
|
let jar = set_auth_cookies(jar, &resp.token, &refresh_token);
|
||||||
@@ -176,7 +177,7 @@ pub async fn login(
|
|||||||
let row: Option<AccountLoginRow> =
|
let row: Option<AccountLoginRow> =
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled,
|
"SELECT id, username, email, display_name, role, status, totp_enabled,
|
||||||
password_hash, totp_secret, created_at
|
password_hash, totp_secret, created_at, llm_routing
|
||||||
FROM accounts WHERE username = $1 OR email = $1"
|
FROM accounts WHERE username = $1 OR email = $1"
|
||||||
)
|
)
|
||||||
.bind(&req.username)
|
.bind(&req.username)
|
||||||
@@ -245,6 +246,7 @@ pub async fn login(
|
|||||||
account: AccountPublic {
|
account: AccountPublic {
|
||||||
id: r.id, username: r.username, email: r.email, display_name: r.display_name,
|
id: r.id, username: r.username, email: r.email, display_name: r.display_name,
|
||||||
role: r.role, status: r.status, totp_enabled: r.totp_enabled, created_at: r.created_at,
|
role: r.role, status: r.status, totp_enabled: r.totp_enabled, created_at: r.created_at,
|
||||||
|
llm_routing: r.llm_routing,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let jar = set_auth_cookies(jar, &resp.token, &refresh_token);
|
let jar = set_auth_cookies(jar, &resp.token, &refresh_token);
|
||||||
@@ -349,7 +351,7 @@ pub async fn me(
|
|||||||
) -> SaasResult<Json<AccountPublic>> {
|
) -> SaasResult<Json<AccountPublic>> {
|
||||||
let row: Option<AccountAuthRow> =
|
let row: Option<AccountAuthRow> =
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"SELECT id, username, email, display_name, role, status, totp_enabled, created_at
|
"SELECT id, username, email, display_name, role, status, totp_enabled, created_at, llm_routing
|
||||||
FROM accounts WHERE id = $1"
|
FROM accounts WHERE id = $1"
|
||||||
)
|
)
|
||||||
.bind(&ctx.account_id)
|
.bind(&ctx.account_id)
|
||||||
@@ -361,6 +363,7 @@ pub async fn me(
|
|||||||
Ok(Json(AccountPublic {
|
Ok(Json(AccountPublic {
|
||||||
id: r.id, username: r.username, email: r.email, display_name: r.display_name,
|
id: r.id, username: r.username, email: r.email, display_name: r.display_name,
|
||||||
role: r.role, status: r.status, totp_enabled: r.totp_enabled, created_at: r.created_at,
|
role: r.role, status: r.status, totp_enabled: r.totp_enabled, created_at: r.created_at,
|
||||||
|
llm_routing: r.llm_routing,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ pub struct AccountPublic {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
pub totp_enabled: bool,
|
pub totp_enabled: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
|
pub llm_routing: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 认证上下文 (注入到 request extensions)
|
/// 认证上下文 (注入到 request extensions)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct AccountRow {
|
|||||||
pub totp_enabled: bool,
|
pub totp_enabled: bool,
|
||||||
pub last_login_at: Option<String>,
|
pub last_login_at: Option<String>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
|
pub llm_routing: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// accounts 表行 (不含 last_login_at,用于 auth/me 等场景)
|
/// accounts 表行 (不含 last_login_at,用于 auth/me 等场景)
|
||||||
@@ -27,6 +28,7 @@ pub struct AccountAuthRow {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
pub totp_enabled: bool,
|
pub totp_enabled: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
|
pub llm_routing: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Login 一次性查询行(合并用户信息 + password_hash + totp_secret)
|
/// Login 一次性查询行(合并用户信息 + password_hash + totp_secret)
|
||||||
@@ -42,6 +44,7 @@ pub struct AccountLoginRow {
|
|||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub totp_secret: Option<String>,
|
pub totp_secret: Option<String>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
|
pub llm_routing: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// operation_logs 表行
|
/// operation_logs 表行
|
||||||
|
|||||||
Reference in New Issue
Block a user