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}