rusty_api/
routes.rs

1/*!
2 * The `routes` module provides functionality for defining and managing API routes.
3 *
4 * This module allows developers to create routes with or without password protection,
5 * and apply them to an Actix Web `ServiceConfig`. It simplifies the process of setting
6 * up API endpoints and ensures secure access to protected routes.
7 *
8 * This module features:
9 * - **Password-Protected Routes**: Easily secure specific routes with a password.
10 * - **Public Routes**: Define routes that are accessible without authentication.
11 * - **Flexible Configuration**: Apply routes to an Actix Web `ServiceConfig` for seamless integration.
12 *
13 * The `Routes` struct serves as a container for all defined routes, allowing for
14 * easy management and configuration.
15 */
16use actix_web::{web, Responder, FromRequest, HttpRequest, HttpResponse, dev::Handler, http::Method};
17use crate::core::auth::{validate_token};
18
19/**
20 * The `Routes` struct is used to manage API routes.
21 *
22 * It allows for the addition of routes with or without password protection,
23 * and provides a method to apply these routes to an Actix Web `ServiceConfig`.
24 *
25 * # Example
26 * ```rust
27 * use rusty_api::{Routes, Method};
28 * use actix_web::{HttpRequest, HttpResponse};
29 * 
30 * async fn public_route(_req: HttpRequest) -> HttpResponse {
31 *     HttpResponse::Ok().body("Public route accessed!")
32 * }
33 * 
34 * async fn protected_route(_req: HttpRequest) -> HttpResponse {
35 *     HttpResponse::Ok().body("Protected route accessed!")
36 * }
37 * 
38 * let routes = Routes::new()
39 *     .add_route(Method::GET, "/public", public_route)
40 *     .add_route_with_password(Method::GET, "/protected", protected_route, "SecretPassword");
41 * ```
42 */
43pub struct Routes {
44    routes: Vec<Box<dyn Fn(&mut web::ServiceConfig) + Send + Sync>>,
45}
46
47impl Routes {
48    /**
49     * Create a new `Routes` instance.
50     *
51     * This initializes an empty collection of routes that can be configured
52     * and applied to an Actix Web `ServiceConfig`, via the `Api` struct.
53     *
54     * # Example
55     * ```rust
56     * use rusty_api::Routes;
57     * use rusty_api::Api;
58     *
59     * let routes = Routes::new();
60     * let api = Api::new() 
61     *     .configure_routes(routes);
62     * ```
63     */
64    pub fn new() -> Self {
65        Self { routes: Vec::new() }
66    }
67
68    /**
69     * Add a new route to the `Routes` instance with password protection.
70     *
71     * This method allows you to define a route that requires a password to access.
72     * The password is passed as a query parameter in the request.
73     *
74     * # Arguments
75     * - `method`: The HTTP method for the route (e.g., GET, POST).
76     * - `path`: The URL path for the route.
77     * - `handler`: The handler function for the route.
78     * - `password`: The password required to access the route.
79     *
80     * # Example
81     * ```rust
82     * use rusty_api::{Routes, HttpRequest, HttpResponse, Method};
83     *
84     * async fn protected_route(_req: HttpRequest) -> HttpResponse {
85     *    HttpResponse::Ok().body("Protected route accessed!")
86     * }
87     *
88     * let routes = Routes::new()
89     *    .add_route_with_password(Method::GET, "/protected", protected_route, "SecretPassword");
90     * ```
91     */
92    pub fn add_route_with_password<H, Args, R>(
93        self,
94        method: Method,
95        path: &'static str,
96        handler: H,
97        password: &'static str,
98    ) -> Self
99    where
100        H: Handler<Args, Output = R> + Clone + Send + Sync + 'static,
101        Args: FromRequest + 'static,
102        R: Responder + 'static,
103    {
104        self.add_route_internal(method, path, handler, Some(password))
105    }
106
107    /**
108     * Add a new route to the `Routes` instance without password protection.
109     *
110     * This method allows you to define a public route that does not require authentication.
111     *
112     * # Arguments
113     * - `method`: The HTTP method for the route (e.g., GET, POST).
114     * - `path`: The URL path for the route.
115     * - `handler`: The handler function for the route.
116     *
117     * # Example
118     * ```rust
119     * use rusty_api::{Routes, HttpRequest, HttpResponse, Method};
120     * 
121     * async fn public_route(_req: HttpRequest) -> HttpResponse {
122     *    HttpResponse::Ok().body("Public route accessed!")
123     * }
124     *
125     * let routes = Routes::new()
126     *   .add_route(Method::GET, "/public", public_route);
127     * ```
128     */
129    pub fn add_route<H, Args, R>(self, method: Method, path: &'static str, handler: H) -> Self
130    where
131        H: Handler<Args, Output = R> + Clone + Send + Sync + 'static,
132        Args: FromRequest + 'static,
133        R: Responder + 'static,
134    {
135        self.add_route_internal(method, path, handler, None)
136    }
137
138    /**
139     * Add a new route to the `Routes` instance with authentication.
140     *
141     * This method allows you to define a route that requires authentication via a token.
142     * The token is passed in the `Authorization` header of the request.
143     *
144     * # Arguments
145     * - `method`: The HTTP method for the route (e.g., GET, POST).
146     * - `path`: The URL path for the route.
147     * - `handler`: The handler function for the route.
148     *
149     * # Example
150     * ```rust
151     * use rusty_api::{Routes, HttpRequest, HttpResponse, Method};
152     *
153     * async fn auth_route(_req: HttpRequest, userId: i32) -> HttpResponse {
154     *    HttpResponse::Ok().body(format!("Authenticated user ID: {}", userId))
155     * }
156     *
157     * let routes = Routes::new()
158     *    .add_route_with_auth(Method::GET, "/auth", auth_route);
159     * ```
160     */
161    pub fn add_route_with_auth<H, R>(mut self, method: Method, path: &'static str, handler: H) -> Self
162    where
163        H: Fn(HttpRequest, i32) -> R + Clone + Send + Sync + 'static,
164        R: futures_util::Future<Output = HttpResponse> + 'static,
165    {
166        let wrapped_handler = move |req: HttpRequest| {
167            let handler = handler.clone();
168            async move {
169                // Extract and validate the token
170                let token = match req
171                    .headers()
172                    .get("Authorization")
173                    .and_then(|h| h.to_str().ok())
174                    .and_then(|h| h.strip_prefix("Bearer "))
175                {
176                    Some(token) => token,
177                    None => return HttpResponse::Unauthorized().body("Missing or invalid token"),
178                };
179
180                // Validate the token and extract the user ID
181                let user_id = match validate_token(token) {
182                    Ok(claims) => claims.sub,
183                    Err(_) => return HttpResponse::Unauthorized().body("Invalid token"),
184                };
185
186                // Call the handler with the user ID
187                handler(req, user_id).await
188            }
189        };
190
191        let m = method.clone();
192        let route = {
193            let wrapped_handler = wrapped_handler.clone(); // Clone the handler inside the closure
194            move |cfg: &mut web::ServiceConfig| {
195                cfg.service(
196                    web::resource(path).route(web::method(m.clone()).to(wrapped_handler.clone()))
197                );
198            }
199        };
200
201        self.routes.push(Box::new(route));
202        self
203    }
204
205    /// Internal function to handle adding routes with or without passwords.
206    fn add_route_internal<H, Args, R>(
207        mut self,
208        method: Method,
209        path: &'static str,
210        handler: H,
211        password: Option<&'static str>,
212    ) -> Self
213    where
214        H: Handler<Args, Output = R> + Clone + Send + Sync + 'static,
215        Args: FromRequest + 'static,
216        R: Responder + 'static,
217    {
218        let handler = handler.clone(); // Clone the handler to avoid moving it
219        let wrapped_handler = move |req: HttpRequest, args: Args| {
220            let handler = handler.clone(); // Clone the handler inside the closure
221            async move {
222                if let Some(expected_password) = password {
223                    if !check_password(&req, expected_password) {
224                        return HttpResponse::Unauthorized().body("Invalid password").into();
225                    }
226                }
227                // Call the original handler and convert its output to an HttpResponse
228                handler.call(args).await.respond_to(&req).map_into_boxed_body()
229            }
230        };
231
232        let m = method.clone();
233        let route = move |cfg: &mut web::ServiceConfig| {
234            let wrapped_handler = wrapped_handler.clone(); // Clone the wrapped handler inside the route closure
235            cfg.service(
236                web::resource(path).route(web::method(m.clone()).to(wrapped_handler.clone()))
237            );
238        };
239        self.routes.push(Box::new(route));
240        self
241    }
242
243    /**
244     * Apply the routes to a `ServiceConfig`.
245     *
246     * This method iterates over all defined routes and applies them to the
247     * provided Axtix Web `ServiceConfig`. It is used internally by the `Api` struct.
248     *
249     * # Arguments
250     * - `cfg`: A mutable reference to the `ServiceConfig` to which the routes will be applied.
251     *
252     * # Example
253     * ```rust
254     * use rusty_api::{Routes, Api};
255     *
256     * let routes = Routes::new();
257     *
258     * let api = Api::new()
259     *    .configure_routes(routes); // The configure_routes method calls the configure method internally.
260     * ```
261     */
262    pub fn configure(&self, cfg: &mut web::ServiceConfig) {
263        for route in &self.routes {
264            route(cfg);
265        }
266    }
267}
268
269/// Check if the request contains the expected password in the query string.
270fn check_password(req: &HttpRequest, expected_password: &str) -> bool {
271    let query_string = req.query_string();
272
273    for pair in query_string.split('&') {
274        let mut key_value = pair.splitn(2, '=');
275        if let (Some(key), Some(value)) = (key_value.next(), key_value.next()) {
276            if key == "password" && value == expected_password {
277                return true;
278            }
279        }
280    }
281
282    false
283}