rocket_cli/templates/mongo_db/
files.rs

1pub const CARGO_TOML: &str = r#"[package]
2name = "{{project_name}}"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7argon2 = "0.5.3"
8bcrypt = "0.16.0"
9bson = { version = "2.13.0", features = ["chrono-0_4"] }
10chrono = { version = "0.4.39", features = ["serde"] }
11dotenvy = "0.15.7"
12futures = "0.3.31"
13jsonwebtoken = "9.3.0"
14mongodb = "3.1.1"
15rand = "0.8.5"
16regex = "1.11.1"
17rocket = { version = "0.5.1", features = ["json"] }
18schemars = "0.8.21"
19serde = { version = "1.0.216", features = ["derive"] }
20tokio = { version = "1.42.0", features = ["full"] }
21sha2 = "0.10.8"
22"#;
23
24pub const MAIN_RS: &str = r#"#[macro_use] 
25extern crate rocket;
26
27mod auth;
28mod catchers;
29mod db;
30mod fairings;
31mod guards;
32mod middleware;
33mod models;
34mod options;
35mod repositories;
36mod routes;
37
38#[launch]
39fn rocket() -> _ {
40    rocket::build()
41        .attach(db::init())
42        .attach(fairings::Cors)
43        .register(
44            "/",
45            catchers![
46                catchers::bad_request,
47                catchers::unauthorized,
48                catchers::forbidden,
49                catchers::not_found,
50                catchers::method_not_allowed,
51                catchers::request_timeout,
52                catchers::conflict,
53                catchers::payload_too_large,
54                catchers::unsupported_media_type,
55                catchers::teapot,
56                catchers::too_many_requests,
57                catchers::internal_error,
58                catchers::bad_gateway,
59                catchers::service_unavailable,
60                catchers::gateway_timeout
61            ],
62        )
63        .mount("/", routes![options::options])
64        .mount("/", routes::user_routes())
65}
66"#;
67
68pub const CATACHERS: &str = r#"use rocket::catch;
69
70#[catch(400)]
71pub async fn bad_request() -> &'static str {
72    "Bad Request."
73}
74
75#[catch(401)]
76pub async fn unauthorized() -> &'static str {
77    "Unauthorized access."
78}
79
80#[catch(403)]
81pub async fn forbidden() -> &'static str {
82    "You don't have permission to access this resource."
83}
84
85#[catch(404)]
86pub async fn not_found() -> &'static str {
87    "Resource not found."
88}
89
90#[catch(405)]
91pub async fn method_not_allowed() -> &'static str {
92    "Method Not Allowed."
93}
94
95#[catch(408)]
96pub async fn request_timeout() -> &'static str {
97    "Request Timeout."
98}
99
100#[catch(409)]
101pub async fn conflict() -> &'static str {
102    "The request could not be completed due to a conflict."
103}
104
105#[catch(413)]
106pub async fn payload_too_large() -> &'static str {
107    "Payload Too Large."
108}
109
110#[catch(415)]
111pub async fn unsupported_media_type() -> &'static str {
112    "Unsupported Media Type."
113}
114
115#[catch(418)]
116pub async fn teapot() -> &'static str {
117    "I'm a teapot."
118}
119
120#[catch(429)]
121pub async fn too_many_requests() -> &'static str {
122    "Too Many Requests."
123}
124
125#[catch(500)]
126pub async fn internal_error() -> &'static str {
127    "Internal Server Error."
128}
129
130#[catch(502)]
131pub async fn bad_gateway() -> &'static str {
132    "Bad Gateway."
133}
134
135#[catch(503)]
136pub async fn service_unavailable() -> &'static str {
137    "Service Unavailable."
138}
139
140#[catch(504)]
141pub async fn gateway_timeout() -> &'static str {
142    "Gateway Timeout."
143}
144"#;
145
146pub const MODELS: &str = r#"use chrono::{DateTime, Utc};
147use mongodb::bson::oid::ObjectId;
148use schemars::JsonSchema;
149use serde::{Deserialize, Serialize};
150
151#[derive(Debug, Serialize, Deserialize, Clone)]
152pub struct UserDocument {
153    #[serde(rename = "_id")]
154    pub id: ObjectId,
155    pub username: String,
156    pub email: String,
157    pub password: String,
158    #[serde(
159        with = "bson::serde_helpers::chrono_datetime_as_bson_datetime",
160        rename = "createdAt"
161    )]
162    pub created_at: DateTime<Utc>,
163}
164
165#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
166pub struct User {
167    #[serde(rename = "_id")]
168    pub id: String,
169    pub username: String,
170    pub email: String,
171    pub password: String,
172    #[serde(rename = "createdAt")]
173    pub created_at: String,
174}
175
176#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
177pub struct UserInfo {
178    #[serde(rename = "_id")]
179    pub id: String,
180    pub username: String,
181    pub email: String,
182    #[serde(rename = "createdAt")]
183    pub created_at: String,
184}
185
186#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
187pub struct LoginCredentials {
188    pub email: String,
189    pub password: String,
190}
191
192#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
193pub struct RegistrationCredentials {
194    pub username: String,
195    pub email: String,
196    pub password: String,
197}
198
199#[derive(Debug, Deserialize, Serialize)]
200pub struct SuccessResponse {
201    pub status: u16,
202    pub message: String,
203}
204
205#[derive(Debug, Deserialize, Serialize)]
206pub struct ErrorResponse {
207    pub status: u16,
208    pub message: String,
209}
210"#;
211
212pub const OPTIONS: &str = r#"
213#[rocket::options("/<_route_args..>")]
214pub async fn options(_route_args: Option<std::path::PathBuf>) -> rocket::http::Status {
215    rocket::http::Status::Ok
216}
217"#;
218
219pub const ROUTES_MOD: &str = r#"use crate::auth::{authorize_user, hash_password};
220use crate::guards::AuthClaims;
221use crate::models::{ErrorResponse, SuccessResponse, UserInfo};
222use crate::models::{LoginCredentials, RegistrationCredentials, User, UserDocument};
223use crate::repositories::UserRepository;
224
225use rocket::http::Status;
226use rocket::http::{Cookie, CookieJar, SameSite};
227use rocket::serde::json::Json;
228use rocket::{State, delete, get, post, put, routes};
229
230use std::sync::Arc;
231
232/// Registers a new user.
233#[post("/register", data = "<credentials>")]
234pub async fn register(
235    repo: &State<Arc<UserRepository>>,
236    credentials: Json<RegistrationCredentials>,
237) -> Result<Json<SuccessResponse>, Json<ErrorResponse>> {
238    if let Ok(Some(_)) = repo.get_user_by_email(&credentials.email).await {
239        return Err(Json(ErrorResponse {
240            status: Status::Conflict.code,
241            message: "A user with this email already exists".to_string(),
242        }));
243    }
244
245    let hashed_password = match hash_password(credentials.password.clone()) {
246        Ok(hash) => hash,
247        Err(_) => {
248            return Err(Json(ErrorResponse {
249                status: Status::InternalServerError.code,
250                message: "Something went wrong, please try again later".to_string(),
251            }));
252        }
253    };
254
255    let _ = match repo
256        .create_user(&credentials.username, &credentials.email, &hashed_password)
257        .await
258    {
259        Ok(user) => user,
260        Err(_) => {
261            return Err(Json(ErrorResponse {
262                status: Status::InternalServerError.code,
263                message: "Failed to register account".to_string(),
264            }));
265        }
266    };
267
268    Ok(Json(SuccessResponse {
269        status: Status::Ok.code,
270        message: "User registered successfully".to_string(),
271    }))
272}
273
274/// Authenticates a user and sets an authentication cookie.
275#[post("/login", data = "<credentials>")]
276pub async fn login(
277    repo: &State<Arc<UserRepository>>,
278    credentials: Json<LoginCredentials>,
279    cookies: &CookieJar<'_>,
280) -> Result<Json<SuccessResponse>, Json<ErrorResponse>> {
281    let user_document = match repo.get_user_by_email(&credentials.email).await {
282        Ok(Some(user_document)) => user_document,
283        Ok(None) => {
284            return Err(Json(ErrorResponse {
285                status: Status::Unauthorized.code,
286                message: "Invalid email or password".to_string(),
287            }));
288        }
289        Err(_) => {
290            return Err(Json(ErrorResponse {
291                status: Status::InternalServerError.code,
292                message: "Something went wrong, please try again later".to_string(),
293            }));
294        }
295    };
296
297    let user = User {
298        id: user_document.id.to_string(),
299        username: user_document.username.to_string(),
300        email: user_document.email.clone(),
301        password: user_document.password.clone(),
302        created_at: user_document.created_at.to_rfc3339(),
303    };
304
305    let token = match authorize_user(&user, &credentials).await {
306        Ok(token) => token,
307        Err(_) => {
308            return Err(Json(ErrorResponse {
309                status: Status::Unauthorized.code,
310                message: "Invalid email or password".to_string(),
311            }));
312        }
313    };
314
315    // Set the token cookie (HTTP-only, secure)
316    #[allow(deprecated)]
317    let cookie = Cookie::build(("auth_token", token.clone()))
318        .http_only(true)
319        .secure(false) // Ensure your app uses HTTPS
320        .same_site(SameSite::Lax)
321        .path("/")
322        .finish();
323
324    cookies.add(cookie);
325
326    Ok(Json(SuccessResponse {
327        status: Status::Ok.code,
328        message: "Login successful".to_string(),
329    }))
330}
331
332/// Logs out the current user by removing the authentication cookie.
333#[post("/logout")]
334pub fn logout(cookies: &CookieJar<'_>) -> Json<SuccessResponse> {
335    cookies.remove(Cookie::build(("auth_token", "")).path("/").build());
336    Json(SuccessResponse {
337        status: 200,
338        message: "Logged out successfully".to_string(),
339    })
340}
341
342/// Retrieves a single user by ID (requires authentication).
343#[get("/users/<id>")]
344pub async fn get_user(
345    _auth: AuthClaims,
346    repo: &State<Arc<UserRepository>>,
347    id: &str,
348) -> Result<Json<UserDocument>, Json<ErrorResponse>> {
349    let user = match repo.get_user_by_id(&id).await {
350        Ok(Some(user)) => user,
351        Ok(None) => {
352            return Err(Json(ErrorResponse {
353                status: Status::NotFound.code,
354                message: "User not found".to_string(),
355            }));
356        }
357        Err(_) => {
358            return Err(Json(ErrorResponse {
359                status: Status::InternalServerError.code,
360                message: "Something went wrong, please try again later".to_string(),
361            }));
362        }
363    };
364
365    Ok(Json(user))
366}
367
368/// Retrieves a single user by email (requires authentication).
369#[get("/user/<email>")]
370pub async fn get_user_by_email(
371    _auth: AuthClaims,
372    repo: &State<Arc<UserRepository>>,
373    email: &str,
374) -> Result<Json<UserInfo>, Json<ErrorResponse>> {
375    let user = match repo.get_user_by_email(&email).await {
376        Ok(Some(user)) => user,
377        Ok(None) => {
378            return Err(Json(ErrorResponse {
379                status: Status::NotFound.code,
380                message: "User not found".to_string(),
381            }));
382        }
383        Err(_) => {
384            return Err(Json(ErrorResponse {
385                status: Status::InternalServerError.code,
386                message: "Something went wrong, please try again later".to_string(),
387            }));
388        }
389    };
390
391    Ok(Json(UserInfo {
392        id: user.id.to_string(),
393        username: user.username,
394        email: user.email,
395        created_at: user.created_at.to_string(),
396    }))
397}
398
399/// Updates an existing user's information by ID (requires authentication).
400#[put("/update/<id>", data = "<credentials>")]
401pub async fn update_user(
402    _auth: AuthClaims,
403    repo: &State<Arc<UserRepository>>,
404    id: &str,
405    credentials: Json<RegistrationCredentials>,
406) -> Result<Json<UserDocument>, Json<ErrorResponse>> {
407    // Check if the email is already in use by another user
408    if let Ok(Some(existing_user)) = repo.get_user_by_email(&credentials.email).await {
409        // If the email exists and it's not the user being updated
410        if existing_user.id.to_string() != id {
411            return Err(Json(ErrorResponse {
412                status: Status::Conflict.code,
413                message: "A user with this email already exists".to_string(),
414            }));
415        }
416    }
417
418    let hashed_password = match hash_password(credentials.password.clone()) {
419        Ok(hash) => hash,
420        Err(_) => {
421            return Err(Json(ErrorResponse {
422                status: Status::InternalServerError.code,
423                message: "Something went wrong, please try again later".to_string(),
424            }));
425        }
426    };
427
428    let user = match repo
429        .update_user(
430            &id,
431            Some(&credentials.username),
432            Some(&credentials.email),
433            Some(&hashed_password),
434        )
435        .await
436    {
437        Ok(Some(user)) => user,
438        Ok(None) => {
439            return Err(Json(ErrorResponse {
440                status: Status::NotFound.code,
441                message: "User not found".to_string(),
442            }));
443        }
444        Err(_) => {
445            return Err(Json(ErrorResponse {
446                status: Status::InternalServerError.code,
447                message: "Something went wrong, please try again later".to_string(),
448            }));
449        }
450    };
451
452    Ok(Json(user))
453}
454
455/// Deletes a user by ID (requires authentication).
456#[delete("/delete/<id>")]
457pub async fn delete_user(
458    _auth: AuthClaims,
459    repo: &State<Arc<UserRepository>>,
460    id: &str,
461) -> Result<Json<SuccessResponse>, Json<ErrorResponse>> {
462    match repo.delete_user(&id).await {
463        Ok(Some(_)) => Ok(Json(SuccessResponse {
464            status: Status::Ok.code,
465            message: "User deleted successfully".to_string(),
466        })),
467        Ok(None) => {
468            return Err(Json(ErrorResponse {
469                status: Status::NotFound.code,
470                message: "User not found".to_string(),
471            }));
472        }
473        Err(_) => {
474            return Err(Json(ErrorResponse {
475                status: Status::InternalServerError.code,
476                message: "Something went wrong, please try again later".to_string(),
477            }));
478        }
479    }
480}
481
482/// Collects all user-related routes for mounting.
483pub fn user_routes() -> Vec<rocket::Route> {
484    routes![
485        register,
486        login,
487        logout,
488        get_user,
489        get_user_by_email,
490        update_user,
491        delete_user
492    ]
493}
494"#;
495
496pub const DB: &str = r#"use dotenvy::dotenv;
497use mongodb::{Client, options::ClientOptions};
498use rocket::fairing::AdHoc;
499use std::sync::Arc;
500
501use crate::repositories::UserRepository;
502
503pub fn init() -> AdHoc {
504    AdHoc::on_ignite(
505        "Establish connection with Database cluster",
506        |rocket| async {
507            match connect().await {
508                Ok(user_repository) => rocket.manage(user_repository),
509                Err(error) => {
510                    panic!("Cannot connect to instance -> {:?}", error)
511                }
512            }
513        },
514    )
515}
516
517async fn connect() -> mongodb::error::Result<Arc<UserRepository>> {
518    dotenv().ok();
519    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set...");
520    let database_name = std::env::var("DATABASE").expect("DATABASE must be set...");
521    let client_options = ClientOptions::parse(database_url).await?;
522    let client = Client::with_options(client_options)?;
523    let _database = client.database(&database_name);
524
525    Ok(Arc::new(UserRepository::new(
526        &client,
527        &database_name,
528        "users",
529    )))
530}
531"#;
532
533pub const REPOSITORIES: &str = r#"#![allow(unused)]
534use chrono::{DateTime, Utc};
535use futures::stream::TryStreamExt;
536use mongodb::{
537    bson::{doc, oid::ObjectId},
538    error::{Error, Result},
539    options::ClientOptions,
540    Client, Collection,
541};
542use serde::{Deserialize, Serialize};
543
544use crate::models::UserDocument;
545
546#[derive(Debug)]
547pub struct UserRepository {
548    collection: Collection<UserDocument>,
549}
550
551impl UserRepository {
552    pub fn new(client: &Client, db_name: &str, collection_name: &str) -> Self {
553        let collection = client
554            .database(db_name)
555            .collection::<UserDocument>(collection_name);
556        Self { collection }
557    }
558
559    /// CREATE a new user
560    pub async fn create_user(
561        &self,
562        username: &str,
563        email: &str,
564        password: &str,
565    ) -> Result<UserDocument> {
566        if let Some(_) = self.collection.find_one(doc! { "email": email }).await? {
567            return Err(Error::from(std::io::Error::new(
568                std::io::ErrorKind::AlreadyExists,
569                "A user with this email already exists.",
570            )));
571        }
572
573        let user = UserDocument {
574            id: ObjectId::new(),
575            username: username.to_string(),
576            email: email.to_string(),
577            password: password.to_string(),
578            created_at: Utc::now(),
579        };
580
581        self.collection.insert_one(&user).await?;
582
583        Ok(user)
584    }
585
586    /// GET user by id
587    pub async fn get_user_by_id(&self, id: &str) -> Result<Option<UserDocument>> {
588        match ObjectId::parse_str(id) {
589            Ok(object_id) => {
590                let filter = doc! { "_id": object_id };
591                let user = self.collection.find_one(filter).await?;
592                Ok(user)
593            }
594            Err(_) => Ok(None), // Invalid ID treated as "not found"
595        }
596    }
597
598    /// GET user by email
599    pub async fn get_user_by_email(&self, email: &str) -> Result<Option<UserDocument>> {
600        let filter = doc! { "email": email };
601        let user = self.collection.find_one(filter).await?;
602        Ok(user)
603    }
604
605    /// UPDATE a user
606    pub async fn update_user(
607        &self,
608        id: &str,
609        username: Option<&str>,
610        email: Option<&str>,
611        password: Option<&str>,
612    ) -> Result<Option<UserDocument>> {
613        let object_id = match ObjectId::parse_str(id) {
614            Ok(oid) => oid,
615            Err(_) => return Ok(None),
616        };
617
618        let mut update_doc = doc! {};
619
620        if let Some(username) = username {
621            update_doc.insert("username", username);
622        }
623
624        if let Some(email) = email {
625            update_doc.insert("email", email);
626        }
627
628        if let Some(password) = password {
629            update_doc.insert("password", password);
630        }
631
632        if update_doc.is_empty() {
633            return Ok(None);
634        }
635
636        let filter = doc! { "_id": object_id };
637        let update = doc! { "$set": update_doc };
638
639        let user = self.collection.find_one_and_update(filter, update).await?;
640        Ok(user)
641    }
642
643    /// DELETE a user
644    pub async fn delete_user(&self, id: &str) -> Result<Option<UserDocument>> {
645        let object_id = match ObjectId::parse_str(id) {
646            Ok(oid) => oid,
647            Err(_) => return Ok(None),
648        };
649
650        let filter = doc! { "_id": object_id };
651        let user = self.collection.find_one_and_delete(filter).await?;
652        Ok(user)
653    }
654
655    /// GET all users
656    pub async fn list_users(&self) -> Result<Vec<UserDocument>> {
657        let filter = doc! {};
658        let mut cursor = self.collection.find(filter).await?;
659        let mut users = Vec::new();
660
661        while let Some(user) = cursor.try_next().await? {
662            users.push(user);
663        }
664
665        Ok(users)
666    }
667}
668"#;