Initial public release v0.1.0
This commit is contained in:
commit
6c39e0bfbf
87 files changed
+10867
No files matched your search
@@ -0,0 +1,60 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::Application;
|
||||
|
||||
pub async fn create(
|
||||
pool: &SqlitePool,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
name: &str,
|
||||
slug: &str,
|
||||
) -> Result<Application, sqlx::Error> {
|
||||
sqlx::query_as::<_, Application>(
|
||||
r#"
|
||||
INSERT INTO applications (id, tenant_id, name, slug)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(tenant_id)
|
||||
.bind(name)
|
||||
.bind(slug)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_slug(
|
||||
pool: &SqlitePool,
|
||||
slug: &str,
|
||||
) -> Result<Option<Application>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Application>("SELECT * FROM applications WHERE slug = ?")
|
||||
.bind(slug)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result<Option<Application>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Application>("SELECT * FROM applications WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<Application>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Application>("SELECT * FROM applications WHERE tenant_id = ? ORDER BY name")
|
||||
.bind(tenant_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_enabled(pool: &SqlitePool, id: &str, enabled: bool) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE applications SET enabled = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(enabled)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::AuditLog;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn insert(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
actor_user_id: Option<&str>,
|
||||
target_user_id: Option<&str>,
|
||||
action: &str,
|
||||
resource_type: &str,
|
||||
resource_id: Option<&str>,
|
||||
severity: &str,
|
||||
ip_address: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
metadata_json: Option<&str>,
|
||||
) -> Result<AuditLog, sqlx::Error> {
|
||||
sqlx::query_as::<_, AuditLog>(
|
||||
r#"
|
||||
INSERT INTO audit_logs (
|
||||
id, actor_user_id, target_user_id,
|
||||
action, resource_type, resource_id,
|
||||
severity, ip_address, user_agent, metadata_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(actor_user_id)
|
||||
.bind(target_user_id)
|
||||
.bind(action)
|
||||
.bind(resource_type)
|
||||
.bind(resource_id)
|
||||
.bind(severity)
|
||||
.bind(ip_address)
|
||||
.bind(user_agent)
|
||||
.bind(metadata_json)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_recent(pool: &SqlitePool, limit: i64) -> Result<Vec<AuditLog>, sqlx::Error> {
|
||||
sqlx::query_as::<_, AuditLog>("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT ?")
|
||||
.bind(limit)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod applications;
|
||||
pub mod audit;
|
||||
pub mod permissions;
|
||||
pub mod roles;
|
||||
pub mod service_accounts;
|
||||
pub mod sessions;
|
||||
pub mod tokens;
|
||||
pub mod users;
|
||||
@@ -0,0 +1,41 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
/// Return all permission names held by a user (via their roles).
|
||||
pub async fn list_for_user(pool: &SqlitePool, user_id: &str) -> Result<Vec<String>, sqlx::Error> {
|
||||
let rows: Vec<(String,)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT DISTINCT p.name
|
||||
FROM permissions p
|
||||
JOIN role_permissions rp ON rp.permission_id = p.id
|
||||
JOIN user_roles ur ON ur.role_id = rp.role_id
|
||||
WHERE ur.user_id = ?
|
||||
ORDER BY p.name
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows.into_iter().map(|(name,)| name).collect())
|
||||
}
|
||||
|
||||
/// Check if a user holds a specific named permission.
|
||||
pub async fn user_has_permission(
|
||||
pool: &SqlitePool,
|
||||
user_id: &str,
|
||||
permission_name: &str,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
let row: (i64,) = sqlx::query_as(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM permissions p
|
||||
JOIN role_permissions rp ON rp.permission_id = p.id
|
||||
JOIN user_roles ur ON ur.role_id = rp.role_id
|
||||
WHERE ur.user_id = ? AND p.name = ?
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(permission_name)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(row.0 > 0)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::Role;
|
||||
|
||||
pub async fn list_all(pool: &SqlitePool) -> Result<Vec<Role>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Role>("SELECT * FROM roles ORDER BY name")
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_name(pool: &SqlitePool, name: &str) -> Result<Option<Role>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Role>("SELECT * FROM roles WHERE name = ?")
|
||||
.bind(name)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result<Option<Role>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Role>("SELECT * FROM roles WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_for_user(pool: &SqlitePool, user_id: &str) -> Result<Vec<Role>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Role>(
|
||||
r#"
|
||||
SELECT r.* FROM roles r
|
||||
JOIN user_roles ur ON ur.role_id = r.id
|
||||
WHERE ur.user_id = ?
|
||||
ORDER BY r.name
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn assign_to_user(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
user_id: &str,
|
||||
role_id: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("INSERT OR IGNORE INTO user_roles (user_id, role_id) VALUES (?, ?)")
|
||||
.bind(user_id)
|
||||
.bind(role_id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_from_user(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
user_id: &str,
|
||||
role_id: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("DELETE FROM user_roles WHERE user_id = ? AND role_id = ?")
|
||||
.bind(user_id)
|
||||
.bind(role_id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn admin_role_exists(pool: &SqlitePool) -> Result<bool, sqlx::Error> {
|
||||
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM roles WHERE name = 'admin'")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(row.0 > 0)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::ServiceAccount;
|
||||
|
||||
pub async fn create(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<ServiceAccount, sqlx::Error> {
|
||||
sqlx::query_as::<_, ServiceAccount>(
|
||||
r#"
|
||||
INSERT INTO service_accounts (id, tenant_id, name, description)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(tenant_id)
|
||||
.bind(name)
|
||||
.bind(description)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_id(
|
||||
pool: &SqlitePool,
|
||||
id: &str,
|
||||
) -> Result<Option<ServiceAccount>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ServiceAccount>("SELECT * FROM service_accounts WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<ServiceAccount>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ServiceAccount>(
|
||||
"SELECT * FROM service_accounts WHERE tenant_id = ? ORDER BY name",
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_enabled(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE service_accounts SET enabled = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(enabled)
|
||||
.bind(id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::Session;
|
||||
|
||||
pub async fn create(
|
||||
pool: &SqlitePool,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
token_hash: &str,
|
||||
ip_address: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
expires_at: &str,
|
||||
) -> Result<Session, sqlx::Error> {
|
||||
sqlx::query_as::<_, Session>(
|
||||
r#"
|
||||
INSERT INTO sessions (id, user_id, token_hash, ip_address, user_agent, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(user_id)
|
||||
.bind(token_hash)
|
||||
.bind(ip_address)
|
||||
.bind(user_agent)
|
||||
.bind(expires_at)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_token_hash(
|
||||
pool: &SqlitePool,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<Session>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Session>("SELECT * FROM sessions WHERE token_hash = ? AND revoked = 0")
|
||||
.bind(token_hash)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn revoke(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE sessions SET revoked = 1 WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn revoke_all_for_user(pool: &SqlitePool, user_id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE sessions SET revoked = 1 WHERE user_id = ?")
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_last_seen(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE sessions SET last_seen_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete sessions that are expired or revoked. Called once at startup.
|
||||
pub async fn cleanup_expired(pool: &SqlitePool) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM sessions
|
||||
WHERE revoked = 1
|
||||
OR expires_at < strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::ApiToken;
|
||||
|
||||
pub async fn create(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
name: &str,
|
||||
token_hash: &str,
|
||||
expires_at: Option<&str>,
|
||||
) -> Result<ApiToken, sqlx::Error> {
|
||||
sqlx::query_as::<_, ApiToken>(
|
||||
r#"
|
||||
INSERT INTO api_tokens (id, user_id, name, token_hash, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(user_id)
|
||||
.bind(name)
|
||||
.bind(token_hash)
|
||||
.bind(expires_at)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_hash(
|
||||
pool: &SqlitePool,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<ApiToken>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ApiToken>("SELECT * FROM api_tokens WHERE token_hash = ? AND revoked = 0")
|
||||
.bind(token_hash)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_for_user(pool: &SqlitePool, user_id: &str) -> Result<Vec<ApiToken>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ApiToken>(
|
||||
"SELECT * FROM api_tokens WHERE user_id = ? ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result<Option<ApiToken>, sqlx::Error> {
|
||||
sqlx::query_as::<_, ApiToken>("SELECT * FROM api_tokens WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn revoke(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE api_tokens SET revoked = 1 WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_last_used(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE api_tokens SET last_used_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::User;
|
||||
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result<Option<User>, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_username(
|
||||
pool: &SqlitePool,
|
||||
username: &str,
|
||||
) -> Result<Option<User>, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = ?")
|
||||
.bind(username)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<User>, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE tenant_id = ? ORDER BY created_at DESC")
|
||||
.bind(tenant_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
tenant_id: &str,
|
||||
username: &str,
|
||||
password_hash: &str,
|
||||
) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
INSERT INTO users (id, tenant_id, username, password_hash, status)
|
||||
VALUES (?, ?, ?, ?, 1)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(tenant_id)
|
||||
.bind(username)
|
||||
.bind(password_hash)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_status(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
status: i32,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET status = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(status)
|
||||
.bind(id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_password_hash(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
password_hash: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET password_hash = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(password_hash)
|
||||
.bind(id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_last_login(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||
id: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET last_login_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn username_exists(
|
||||
pool: &SqlitePool,
|
||||
tenant_id: &str,
|
||||
username: &str,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
let row: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM users WHERE tenant_id = ? AND username = ?")
|
||||
.bind(tenant_id)
|
||||
.bind(username)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(row.0 > 0)
|
||||
}
|
||||
|
||||
/// Count users that have the admin role.
|
||||
pub async fn count_admins(pool: &SqlitePool) -> Result<i64, sqlx::Error> {
|
||||
let row: (i64,) = sqlx::query_as(
|
||||
r#"
|
||||
SELECT COUNT(DISTINCT ur.user_id)
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.id = ur.role_id
|
||||
WHERE r.name = 'admin'
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(row.0)
|
||||
}
|
||||
Reference in new issue
Block a user