Skip to main content

allowthem_core/
roles.rs

1use crate::db::Db;
2use crate::error::AuthError;
3use crate::types::{Role, RoleId, RoleName, UserId};
4
5/// Map a SQLite UNIQUE constraint violation on `allowthem_roles.name` to
6/// `AuthError::Conflict`. Other errors pass through as `AuthError::Database`.
7fn map_unique_violation(err: sqlx::Error) -> AuthError {
8    if let sqlx::Error::Database(ref db_err) = err {
9        let msg = db_err.message();
10        if msg.contains("UNIQUE constraint failed") && msg.contains("name") {
11            return AuthError::Conflict("role name already exists".into());
12        }
13    }
14    AuthError::Database(err)
15}
16
17impl Db {
18    /// Create a role with a unique name and optional description.
19    pub async fn create_role(
20        &self,
21        name: &RoleName,
22        description: Option<&str>,
23    ) -> Result<Role, AuthError> {
24        let id = RoleId::new();
25        sqlx::query_as::<_, Role>(
26            "INSERT INTO allowthem_roles (id, name, description) \
27             VALUES (?, ?, ?) \
28             RETURNING id, name, description, created_at",
29        )
30        .bind(id)
31        .bind(name)
32        .bind(description)
33        .fetch_one(self.pool())
34        .await
35        .map_err(map_unique_violation)
36    }
37
38    /// Get a role by ID. Returns `None` if not found.
39    pub async fn get_role(&self, id: &RoleId) -> Result<Option<Role>, AuthError> {
40        sqlx::query_as::<_, Role>(
41            "SELECT id, name, description, created_at FROM allowthem_roles WHERE id = ?",
42        )
43        .bind(*id)
44        .fetch_optional(self.pool())
45        .await
46        .map_err(AuthError::Database)
47    }
48
49    /// Get a role by name. Returns `None` if not found.
50    pub async fn get_role_by_name(&self, name: &RoleName) -> Result<Option<Role>, AuthError> {
51        sqlx::query_as::<_, Role>(
52            "SELECT id, name, description, created_at FROM allowthem_roles WHERE name = ?",
53        )
54        .bind(name)
55        .fetch_optional(self.pool())
56        .await
57        .map_err(AuthError::Database)
58    }
59
60    /// List all roles, ordered by creation time.
61    pub async fn list_roles(&self) -> Result<Vec<Role>, AuthError> {
62        sqlx::query_as::<_, Role>(
63            "SELECT id, name, description, created_at FROM allowthem_roles ORDER BY created_at",
64        )
65        .fetch_all(self.pool())
66        .await
67        .map_err(AuthError::Database)
68    }
69
70    /// Delete a role by ID. Returns `true` if a row was deleted, `false` if not found.
71    ///
72    /// Cascades to `allowthem_user_roles` and `allowthem_role_permissions`.
73    pub async fn delete_role(&self, id: &RoleId) -> Result<bool, AuthError> {
74        let result = sqlx::query("DELETE FROM allowthem_roles WHERE id = ?")
75            .bind(*id)
76            .execute(self.pool())
77            .await
78            .map_err(AuthError::Database)?;
79        Ok(result.rows_affected() > 0)
80    }
81
82    /// Assign a role to a user. Silently succeeds if already assigned (idempotent).
83    pub async fn assign_role(&self, user_id: &UserId, role_id: &RoleId) -> Result<(), AuthError> {
84        sqlx::query("INSERT OR IGNORE INTO allowthem_user_roles (user_id, role_id) VALUES (?, ?)")
85            .bind(*user_id)
86            .bind(*role_id)
87            .execute(self.pool())
88            .await
89            .map_err(AuthError::Database)?;
90        Ok(())
91    }
92
93    /// Unassign a role from a user. Returns `true` if removed, `false` if the assignment
94    /// did not exist.
95    pub async fn unassign_role(
96        &self,
97        user_id: &UserId,
98        role_id: &RoleId,
99    ) -> Result<bool, AuthError> {
100        let result =
101            sqlx::query("DELETE FROM allowthem_user_roles WHERE user_id = ? AND role_id = ?")
102                .bind(*user_id)
103                .bind(*role_id)
104                .execute(self.pool())
105                .await
106                .map_err(AuthError::Database)?;
107        Ok(result.rows_affected() > 0)
108    }
109
110    /// Check whether a user has a specific role by name.
111    pub async fn has_role(
112        &self,
113        user_id: &UserId,
114        role_name: &RoleName,
115    ) -> Result<bool, AuthError> {
116        let count: i64 = sqlx::query_scalar(
117            "SELECT COUNT(*) \
118             FROM allowthem_user_roles ur \
119             JOIN allowthem_roles r ON r.id = ur.role_id \
120             WHERE ur.user_id = ? AND r.name = ?",
121        )
122        .bind(*user_id)
123        .bind(role_name)
124        .fetch_one(self.pool())
125        .await
126        .map_err(AuthError::Database)?;
127        Ok(count > 0)
128    }
129
130    /// Return all roles assigned to a user, ordered by creation time.
131    pub async fn get_user_roles(&self, user_id: &UserId) -> Result<Vec<Role>, AuthError> {
132        sqlx::query_as::<_, Role>(
133            "SELECT r.id, r.name, r.description, r.created_at \
134             FROM allowthem_roles r \
135             JOIN allowthem_user_roles ur ON ur.role_id = r.id \
136             WHERE ur.user_id = ? \
137             ORDER BY r.created_at",
138        )
139        .bind(*user_id)
140        .fetch_all(self.pool())
141        .await
142        .map_err(AuthError::Database)
143    }
144}