road_runner_common/
auth.rs1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::error::AppError;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct UserContext {
8 pub user_id: Uuid,
9 pub email: String,
10
11 #[serde(default)]
12 pub roles: Vec<String>,
13
14 #[serde(default)]
15 pub scopes: Vec<String>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub tenant_id: Option<String>,
19}
20
21impl UserContext {
22 pub fn has_role(&self, role: &str) -> bool {
23 self.roles.iter().any(|r| r == role)
24 }
25
26 pub fn require_role(&self, role: &str) -> Result<(), AppError> {
27 if self.has_role(role) {
28 Ok(())
29 } else {
30 Err(AppError::Forbidden {
31 message: format!("Missing role: {}", role),
32 })
33 }
34 }
35}
36
37#[cfg(feature = "web")]
38mod web_impl {
39 use super::*;
40 use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
41 use futures_util::future::{ready, Ready};
42
43 impl FromRequest for UserContext {
46 type Error = AppError;
47 type Future = Ready<Result<Self, Self::Error>>;
48
49 fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
50 match req.extensions().get::<UserContext>().cloned() {
51 Some(ctx) => ready(Ok(ctx)),
52 None => ready(Err(AppError::Unauthorized {
53 message: "Missing user context".to_string(),
54 })),
55 }
56 }
57 }
58}