Release v0.6.0
Rust CI / Test & Quality Checks (push) Canceled after 0s
Rust CI / Build Docker Image (push) Canceled after 0s

This commit is contained in:
thakares committed 2026-08-09 17:17:57 +05:30
1 parent 7069ca9db7
commit f49698bb5c
75 files changed
+11199 -7508

No files matched your search

+704
View File
@@ -0,0 +1,704 @@
use super::*;
#[derive(Deserialize)]
pub struct UsersQuery {
pub success: Option<String>,
pub error: Option<String>,
}
#[derive(Deserialize)]
pub struct CreateUserForm {
pub username: String,
pub password: String,
pub account_type: String,
pub metadata: String,
pub csrf_token: String,
}
#[derive(Deserialize)]
pub struct UpdateUserStatusForm {
pub status: String,
pub csrf_token: String,
}
#[derive(Deserialize)]
pub struct UpdateUserTypeForm {
pub account_type: String,
pub csrf_token: String,
}
#[derive(Deserialize)]
pub struct ResetPasswordForm {
pub new_password: String,
pub csrf_token: String,
}
#[derive(Deserialize)]
pub struct DeleteUserForm {
pub csrf_token: String,
}
pub async fn users_get(
State(state): State<AppState>,
jar: CookieJar,
Query(query): Query<UsersQuery>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let users = {
let conn = state.users_db.lock().unwrap();
crate::db::users::list_users(&conn).unwrap_or_default()
};
let csrf_token = generate_csrf_token(&session_id);
let template = crate::templates::UsersTemplate {
admin_username: user.username,
users,
csrf_token,
success: query.success,
error: query.error,
};
template.into_response()
}
pub async fn users_create_post(
State(state): State<AppState>,
jar: CookieJar,
Form(form): Form<CreateUserForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/users?error=Invalid CSRF token").into_response();
}
let username = form.username.trim().to_lowercase();
if username.len() < 3 {
return Redirect::to("/admin/users?error=Username must be at least 3 characters")
.into_response();
}
if !username
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Redirect::to(
"/admin/users?error=Username must contain only alphanumeric characters, hyphens, or underscores",
)
.into_response();
}
if form.password.trim().len() < 8 {
return Redirect::to("/admin/users?error=Password must be at least 8 characters")
.into_response();
}
let account_type = if form.account_type.trim().is_empty() {
"standard"
} else {
form.account_type.trim()
};
let metadata = if form.metadata.trim().is_empty() {
None
} else {
Some(form.metadata.trim())
};
let hash = match hash_password(&form.password) {
Ok(h) => h,
Err(_) => return Redirect::to("/admin/users?error=Internal hashing error").into_response(),
};
let new_user = {
let conn = state.users_db.lock().unwrap();
match crate::db::users::create_user(&conn, &username, &hash, account_type, metadata) {
Ok(u) => u,
Err(rusqlite::Error::SqliteFailure(err, _))
if err.code == rusqlite::ErrorCode::ConstraintViolation =>
{
return Redirect::to("/admin/users?error=Username already exists").into_response();
}
Err(e) => {
return Redirect::to(&format!("/admin/users?error=Database error: {}", e))
.into_response();
}
}
};
if let Err(e) = state.db.init_user_databases(new_user.id) {
return Redirect::to(&format!(
"/admin/users?error=Failed to initialize user databases: {}",
e
))
.into_response();
}
{
let conn = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn,
&state,
&user.username,
"USER_CREATION",
Some("user"),
Some(&new_user.id.to_string()),
None,
None,
);
}
Redirect::to("/admin/users?success=User created successfully").into_response()
}
pub async fn users_update_status_post(
State(state): State<AppState>,
jar: CookieJar,
Path(id): Path<i64>,
Form(form): Form<UpdateUserStatusForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/users?error=Invalid CSRF token").into_response();
}
let status = form.status.trim().to_lowercase();
if !["active", "disabled", "suspended", "pending", "deleted"].contains(&status.as_str()) {
return Redirect::to("/admin/users?error=Invalid user status").into_response();
}
let conn = state.users_db.lock().unwrap();
match crate::db::users::update_user_status(&conn, id, &status) {
Ok(_) => {
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"USER_STATUS_UPDATE",
Some("user"),
Some(&id.to_string()),
None,
None,
);
Redirect::to("/admin/users?success=User status updated").into_response()
}
Err(e) => Redirect::to(&format!(
"/admin/users?error=Failed to update status: {}",
e
))
.into_response(),
}
}
pub async fn users_update_type_post(
State(state): State<AppState>,
jar: CookieJar,
Path(id): Path<i64>,
Form(form): Form<UpdateUserTypeForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/users?error=Invalid CSRF token").into_response();
}
let account_type = form.account_type.trim().to_lowercase();
if !["admin", "standard", "organization", "service", "system"].contains(&account_type.as_str())
{
return Redirect::to("/admin/users?error=Invalid account type").into_response();
}
let conn = state.users_db.lock().unwrap();
match crate::db::users::update_user_account_type(&conn, id, &account_type) {
Ok(_) => {
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"USER_ACCOUNT_TYPE_UPDATE",
Some("user"),
Some(&id.to_string()),
None,
None,
);
Redirect::to("/admin/users?success=User account type updated").into_response()
}
Err(e) => Redirect::to(&format!(
"/admin/users?error=Failed to update account type: {}",
e
))
.into_response(),
}
}
pub async fn users_reset_password_post(
State(state): State<AppState>,
jar: CookieJar,
headers: HeaderMap,
connect_info: Option<ConnectInfo<SocketAddr>>,
Path(id): Path<i64>,
Form(form): Form<ResetPasswordForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/users?error=Invalid CSRF token").into_response();
}
if form.new_password.trim().len() < 8 {
return Redirect::to("/admin/users?error=Password must be at least 8 characters")
.into_response();
}
let hash = match hash_password(&form.new_password) {
Ok(h) => h,
Err(_) => return Redirect::to("/admin/users?error=Internal hashing error").into_response(),
};
let conn = state.users_db.lock().unwrap();
match crate::db::users::reset_user_password(&conn, id, &hash) {
Ok(_) => {
let ip = get_client_ip(&headers, connect_info);
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"USER_PASSWORD_RESET",
Some("user"),
Some(&id.to_string()),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
Redirect::to("/admin/users?success=Password reset successfully").into_response()
}
Err(e) => Redirect::to(&format!(
"/admin/users?error=Failed to reset password: {}",
e
))
.into_response(),
}
}
pub async fn users_delete_post(
State(state): State<AppState>,
jar: CookieJar,
headers: HeaderMap,
connect_info: Option<ConnectInfo<SocketAddr>>,
Path(id): Path<i64>,
Form(form): Form<DeleteUserForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/users?error=Invalid CSRF token").into_response();
}
match crate::web::multi_user::delete_user_resources(&state, id, &user.username, false) {
Ok(_) => {
let ip = get_client_ip(&headers, connect_info);
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"USER_DELETION",
Some("user"),
Some(&id.to_string()),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
Redirect::to("/admin/users?success=User deleted successfully").into_response()
}
Err(err) => Redirect::to(&format!("/admin/users?error={}", err)).into_response(),
}
}
// ---------------------------------------------------------------------------
// GA Hardening UI Handlers and Structs
// ---------------------------------------------------------------------------
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct UserDetailStats {
pub max_urls: i64,
pub max_landings: i64,
pub max_api_tokens: i64,
pub max_storage_mb: i64,
pub current_urls: i64,
pub current_landings: i64,
pub current_api_tokens: i64,
pub current_storage_mb: i64,
pub url_pct: i64,
pub landing_pct: i64,
pub token_pct: i64,
pub storage_pct: i64,
pub total_visits: i64,
}
pub fn get_user_detail_stats(
state: &AppState,
user_id: i64,
) -> Result<UserDetailStats, Box<dyn std::error::Error>> {
use rusqlite::OptionalExtension;
let quotas = {
let conn = state.users_db.lock().unwrap();
conn.query_row(
"SELECT max_urls, max_landings, max_api_tokens, max_storage_mb, current_urls, current_landings, current_api_tokens, current_storage_mb \
FROM quotas WHERE user_id = ?1;",
[user_id],
|row| Ok(crate::models::UserQuotas {
user_id,
max_urls: row.get(0)?,
max_landings: row.get(1)?,
max_api_tokens: row.get(2)?,
max_storage_mb: row.get(3)?,
current_urls: row.get(4)?,
current_landings: row.get(5)?,
current_api_tokens: row.get(6)?,
current_storage_mb: row.get(7)?,
})
).optional()?
};
let quotas = quotas.unwrap_or(crate::models::UserQuotas {
user_id,
max_urls: 100,
max_landings: 10,
max_api_tokens: 5,
max_storage_mb: 100,
current_urls: 0,
current_landings: 0,
current_api_tokens: 0,
current_storage_mb: 0,
});
let total_visits = {
if let Ok(dbs) = state.get_user_dbs(user_id) {
let conn = dbs.analytics.lock().unwrap();
conn.query_row("SELECT COUNT(*) FROM visits;", [], |row| {
row.get::<_, i64>(0)
})
.unwrap_or(0)
} else {
0
}
};
let url_pct = if quotas.max_urls > 0 {
(quotas.current_urls * 100) / quotas.max_urls
} else {
0
};
let landing_pct = if quotas.max_landings > 0 {
(quotas.current_landings * 100) / quotas.max_landings
} else {
0
};
let token_pct = if quotas.max_api_tokens > 0 {
(quotas.current_api_tokens * 100) / quotas.max_api_tokens
} else {
0
};
let storage_pct = if quotas.max_storage_mb > 0 {
(quotas.current_storage_mb * 100) / quotas.max_storage_mb
} else {
0
};
Ok(UserDetailStats {
max_urls: quotas.max_urls,
max_landings: quotas.max_landings,
max_api_tokens: quotas.max_api_tokens,
max_storage_mb: quotas.max_storage_mb,
current_urls: quotas.current_urls,
current_landings: quotas.current_landings,
current_api_tokens: quotas.current_api_tokens,
current_storage_mb: quotas.current_storage_mb,
url_pct,
landing_pct,
token_pct,
storage_pct,
total_visits,
})
}
// GET /admin/users/new
pub async fn users_new_get(
State(state): State<AppState>,
jar: CookieJar,
Query(params): Query<HashMap<String, String>>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let csrf_token = generate_csrf_token(&session_id);
let template = crate::templates::UsersNewTemplate {
admin_username: user.username,
csrf_token,
success: params.get("success").cloned(),
error: params.get("error").cloned(),
};
template.into_response()
}
// GET /admin/users/:id
pub async fn user_detail_get(
State(state): State<AppState>,
jar: CookieJar,
Path(id): Path<i64>,
Query(params): Query<HashMap<String, String>>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let target_user = {
let conn = state.users_db.lock().unwrap();
let u_res = conn.query_row(
"SELECT id, username, password_hash, status, created_at, last_login, account_type, organization_id, metadata \
FROM users WHERE id = ?1;",
[id],
|row| Ok(crate::models::TenantUser {
id: row.get(0)?,
username: row.get(1)?,
password_hash: row.get(2)?,
status: row.get(3)?,
created_at: row.get(4)?,
last_login: row.get(5)?,
account_type: row.get(6)?,
organization_id: row.get(7)?,
metadata: row.get(8)?,
})
);
match u_res {
Ok(u) => u,
Err(_) => return Redirect::to("/admin/users?error=User not found").into_response(),
}
};
let stats = match get_user_detail_stats(&state, id) {
Ok(s) => s,
Err(_) => return Redirect::to("/admin/users?error=Database error").into_response(),
};
let sessions = {
let conn = state.users_db.lock().unwrap();
let mut stmt = conn
.prepare("SELECT id, user_id, expires_at, created_at FROM sessions WHERE user_id = ?1;")
.unwrap();
let rows = stmt
.query_map([id], |row| {
Ok(crate::models::UserSession {
id: row.get(0)?,
user_id: row.get(1)?,
expires_at: row.get(2)?,
created_at: row.get(3)?,
})
})
.unwrap();
rows.filter_map(|r| r.ok()).collect()
};
let tokens = {
let conn = state.users_db.lock().unwrap();
let mut stmt = conn
.prepare(
"SELECT id, user_id, token_hash, created_at FROM api_tokens WHERE user_id = ?1;",
)
.unwrap();
let rows = stmt
.query_map([id], |row| {
Ok(crate::models::UserApiToken {
id: row.get(0)?,
user_id: row.get(1)?,
token_hash: row.get(2)?,
created_at: row.get(3)?,
})
})
.unwrap();
rows.filter_map(|r| r.ok()).collect()
};
let csrf_token = generate_csrf_token(&session_id);
let template = crate::templates::UserDetailTemplate {
admin_username: user.username,
target_user,
stats,
sessions,
tokens,
csrf_token,
success: params.get("success").cloned(),
error: params.get("error").cloned(),
};
template.into_response()
}
// GET /admin/users/:id/edit
pub async fn user_edit_get(
State(state): State<AppState>,
jar: CookieJar,
Path(id): Path<i64>,
Query(params): Query<HashMap<String, String>>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let target_user = {
let conn = state.users_db.lock().unwrap();
let u_res = conn.query_row(
"SELECT id, username, password_hash, status, created_at, last_login, account_type, organization_id, metadata \
FROM users WHERE id = ?1;",
[id],
|row| Ok(crate::models::TenantUser {
id: row.get(0)?,
username: row.get(1)?,
password_hash: row.get(2)?,
status: row.get(3)?,
created_at: row.get(4)?,
last_login: row.get(5)?,
account_type: row.get(6)?,
organization_id: row.get(7)?,
metadata: row.get(8)?,
})
);
match u_res {
Ok(u) => u,
Err(_) => return Redirect::to("/admin/users?error=User not found").into_response(),
}
};
let quotas = {
let conn = state.users_db.lock().unwrap();
conn.query_row(
"SELECT max_urls, max_landings, max_api_tokens, max_storage_mb, current_urls, current_landings, current_api_tokens, current_storage_mb \
FROM quotas WHERE user_id = ?1;",
[id],
|row| Ok(crate::models::UserQuotas {
user_id: id,
max_urls: row.get(0)?,
max_landings: row.get(1)?,
max_api_tokens: row.get(2)?,
max_storage_mb: row.get(3)?,
current_urls: row.get(4)?,
current_landings: row.get(5)?,
current_api_tokens: row.get(6)?,
current_storage_mb: row.get(7)?,
})
).unwrap_or(crate::models::UserQuotas {
user_id: id,
max_urls: 100,
max_landings: 10,
max_api_tokens: 5,
max_storage_mb: 100,
current_urls: 0,
current_landings: 0,
current_api_tokens: 0,
current_storage_mb: 0,
})
};
let csrf_token = generate_csrf_token(&session_id);
let template = crate::templates::UserEditTemplate {
admin_username: user.username,
target_user,
quotas,
csrf_token,
success: params.get("success").cloned(),
error: params.get("error").cloned(),
};
template.into_response()
}
#[derive(Deserialize)]
pub struct UserEditForm {
pub account_type: String,
pub metadata: String,
pub max_urls: i64,
pub max_landings: i64,
pub max_api_tokens: i64,
pub max_storage_mb: i64,
pub csrf_token: String,
}
// POST /admin/users/:id/edit
pub async fn user_edit_post(
State(state): State<AppState>,
jar: CookieJar,
Path(id): Path<i64>,
Form(form): Form<UserEditForm>,
) -> Response {
let (_user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to(&format!(
"/admin/users/{}/edit?error=Invalid CSRF token",
id
))
.into_response();
}
let conn = state.users_db.lock().unwrap();
let _ = conn.execute(
"UPDATE users SET account_type = ?1, metadata = ?2 WHERE id = ?3;",
rusqlite::params![form.account_type, form.metadata, id],
);
let _ = conn.execute(
"INSERT INTO quotas (user_id, max_urls, max_landings, max_api_tokens, max_storage_mb) \
VALUES (?1, ?2, ?3, ?4, ?5) \
ON CONFLICT(user_id) DO UPDATE SET \
max_urls = excluded.max_urls, \
max_landings = excluded.max_landings, \
max_api_tokens = excluded.max_api_tokens, \
max_storage_mb = excluded.max_storage_mb;",
rusqlite::params![
id,
form.max_urls,
form.max_landings,
form.max_api_tokens,
form.max_storage_mb
],
);
Redirect::to(&format!(
"/admin/users/{}?success=User updated successfully",
id
))
.into_response()
}