rocket_cli/templates/postgres/
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"
9chrono = { version = "0.4.39", features = ["serde"] }
10dotenvy = "0.15.7"
11futures = "0.3.31"
12jsonwebtoken = "9.3.0"
13rand = "0.8.5"
14regex = "1.11.1"
15rocket = { version = "0.5.1", features = ["json"] }
16schemars = "0.8.21"
17serde = { version = "1.0.216", features = ["derive"] }
18tokio = { version = "1.42.0", features = ["full"] }
19sha2 = "0.10.8"
20uuid = { version = "1.11.0", features = ["v4", "serde"] }
21rbatis = "4.6"
22rbdc-pg = "4.6"
23rbs = "4.6"
24"#;
25
26pub const MAIN_RS: &str = r#"#[macro_use] 
27extern crate rocket;
28
29mod auth;
30mod catchers;
31mod db;
32mod fairings;
33mod guards;
34mod middleware;
35mod models;
36mod options;
37mod repositories;
38mod routes;
39
40#[launch]
41fn rocket() -> _ {
42    rocket::build()
43        .attach(db::init())
44        .attach(fairings::Cors)
45        .register(
46            "/",
47            catchers![
48                catchers::bad_request,
49                catchers::unauthorized,
50                catchers::forbidden,
51                catchers::not_found,
52                catchers::method_not_allowed,
53                catchers::request_timeout,
54                catchers::conflict,
55                catchers::payload_too_large,
56                catchers::unsupported_media_type,
57                catchers::teapot,
58                catchers::too_many_requests,
59                catchers::internal_error,
60                catchers::bad_gateway,
61                catchers::service_unavailable,
62                catchers::gateway_timeout
63            ],
64        )
65        .mount("/", routes![options::options])
66        .mount("/", routes::user_routes())
67}
68"#;
69
70pub const CATCHERS: &str = r#"use rocket::catch;
71
72#[catch(400)]
73pub async fn bad_request() -> &'static str {
74    "Bad Request."
75}
76
77#[catch(401)]
78pub async fn unauthorized() -> &'static str {
79    "Unauthorized access."
80}
81
82#[catch(403)]
83pub async fn forbidden() -> &'static str {
84    "You don't have permission to access this resource."
85}
86
87#[catch(404)]
88pub async fn not_found() -> &'static str {
89    "Resource not found."
90}
91
92#[catch(405)]
93pub async fn method_not_allowed() -> &'static str {
94    "Method Not Allowed."
95}
96
97#[catch(408)]
98pub async fn request_timeout() -> &'static str {
99    "Request Timeout."
100}
101
102#[catch(409)]
103pub async fn conflict() -> &'static str {
104    "The request could not be completed due to a conflict."
105}
106
107#[catch(413)]
108pub async fn payload_too_large() -> &'static str {
109    "Payload Too Large."
110}
111
112#[catch(415)]
113pub async fn unsupported_media_type() -> &'static str {
114    "Unsupported Media Type."
115}
116
117#[catch(418)]
118pub async fn teapot() -> &'static str {
119    "I'm a teapot."
120}
121
122#[catch(429)]
123pub async fn too_many_requests() -> &'static str {
124    "Too Many Requests."
125}
126
127#[catch(500)]
128pub async fn internal_error() -> &'static str {
129    "Internal Server Error."
130}
131
132#[catch(502)]
133pub async fn bad_gateway() -> &'static str {
134    "Bad Gateway."
135}
136
137#[catch(503)]
138pub async fn service_unavailable() -> &'static str {
139    "Service Unavailable."
140}
141
142#[catch(504)]
143pub async fn gateway_timeout() -> &'static str {
144    "Gateway Timeout."
145}
146"#;
147
148pub const MODELS: &str = r#"use chrono::{DateTime, Utc};
149use schemars::JsonSchema;
150use serde::{Deserialize, Serialize};
151use uuid::Uuid;
152
153/// Database entity struct
154#[derive(Debug, Serialize, Deserialize, Clone)]
155pub struct UserEntity {
156    pub id: Uuid,
157    pub username: String,
158    pub email: String,
159    pub password: String,
160    pub created_at: DateTime<Utc>,
161}
162
163/// DTO with password included
164#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
165pub struct User {
166    pub id: String,
167    pub username: String,
168    pub email: String,
169    pub password: String,
170    pub created_at: String,
171}
172
173/// DTO without password
174#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
175pub struct UserInfo {
176    pub id: String,
177    pub username: String,
178    pub email: String,
179    pub created_at: String,
180}
181
182#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
183pub struct LoginCredentials {
184    pub email: String,
185    pub password: String,
186}
187
188#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
189pub struct RegistrationCredentials {
190    pub username: String,
191    pub email: String,
192    pub password: String,
193}
194
195#[derive(Debug, Deserialize, Serialize)]
196pub struct SuccessResponse {
197    pub status: u16,
198    pub message: String,
199}
200
201#[derive(Debug, Deserialize, Serialize)]
202pub struct ErrorResponse {
203    pub status: u16,
204    pub message: String,
205}
206"#;
207
208pub const OPTIONS: &str = r#"
209#[rocket::options("/<_route_args..>")]
210pub async fn options(_route_args: Option<std::path::PathBuf>) -> rocket::http::Status {
211    rocket::http::Status::Ok
212}
213"#;
214
215pub const ROUTES_MOD: &str = r#"use crate::auth::{authorize_user, hash_password};
216use crate::guards::AuthClaims;
217use crate::models::{ErrorResponse, SuccessResponse, UserInfo};
218use crate::models::{LoginCredentials, RegistrationCredentials, User, UserEntity};
219use crate::repositories::UserRepository;
220
221use rocket::http::Status;
222use rocket::http::{Cookie, CookieJar, SameSite};
223use rocket::serde::json::Json;
224use rocket::{State, delete, get, post, put, routes};
225
226use std::sync::Arc;
227use uuid::Uuid;
228
229/// Registers a new user.
230#[post("/register", data = "<credentials>")]
231pub async fn register(
232    repo: &State<Arc<UserRepository>>,
233    credentials: Json<RegistrationCredentials>,
234) -> Result<Json<SuccessResponse>, Json<ErrorResponse>> {
235    if let Ok(Some(_)) = repo.get_user_by_email(&credentials.email).await {
236        return Err(Json(ErrorResponse {
237            status: Status::Conflict.code,
238            message: "A user with this email already exists".to_string(),
239        }));
240    }
241
242    let hashed_password = match hash_password(credentials.password.clone()) {
243        Ok(hash) => hash,
244        Err(_) => {
245            return Err(Json(ErrorResponse {
246                status: Status::InternalServerError.code,
247                message: "Something went wrong, please try again later".to_string(),
248            }));
249        }
250    };
251
252    let _ = match repo
253        .create_user(&credentials.username, &credentials.email, &hashed_password)
254        .await
255    {
256        Ok(user) => user,
257        Err(_) => {
258            return Err(Json(ErrorResponse {
259                status: Status::InternalServerError.code,
260                message: "Failed to register account".to_string(),
261            }));
262        }
263    };
264
265    Ok(Json(SuccessResponse {
266        status: Status::Ok.code,
267        message: "User registered successfully".to_string(),
268    }))
269}
270
271/// Authenticates a user and sets an authentication cookie.
272#[post("/login", data = "<credentials>")]
273pub async fn login(
274    repo: &State<Arc<UserRepository>>,
275    credentials: Json<LoginCredentials>,
276    cookies: &CookieJar<'_>,
277) -> Result<Json<SuccessResponse>, Json<ErrorResponse>> {
278    let user_entity = match repo.get_user_by_email(&credentials.email).await {
279        Ok(Some(user_entity)) => user_entity,
280        Ok(None) => {
281            return Err(Json(ErrorResponse {
282                status: Status::Unauthorized.code,
283                message: "Invalid email or password".to_string(),
284            }));
285        }
286        Err(_) => {
287            return Err(Json(ErrorResponse {
288                status: Status::InternalServerError.code,
289                message: "Something went wrong, please try again later".to_string(),
290            }));
291        }
292    };
293
294    let user = User {
295        id: user_entity.id.to_string(),
296        username: user_entity.username.clone(),
297        email: user_entity.email.clone(),
298        password: user_entity.password.clone(),
299        created_at: user_entity.created_at.to_rfc3339(),
300    };
301
302    let token = match authorize_user(&user, &credentials).await {
303        Ok(token) => token,
304        Err(_) => {
305            return Err(Json(ErrorResponse {
306                status: Status::Unauthorized.code,
307                message: "Invalid email or password".to_string(),
308            }));
309        }
310    };
311
312    // Set the token cookie (HTTP-only, secure)
313    #[allow(deprecated)]
314    let cookie = Cookie::build(("auth_token", token.clone()))
315        .http_only(true)
316        .secure(false) // Set to true in production with HTTPS
317        .same_site(SameSite::Lax)
318        .path("/")
319        .finish();
320
321    cookies.add(cookie);
322
323    Ok(Json(SuccessResponse {
324        status: Status::Ok.code,
325        message: "Login successful".to_string(),
326    }))
327}
328
329/// Logs out the current user by removing the authentication cookie.
330#[post("/logout")]
331pub fn logout(cookies: &CookieJar<'_>) -> Json<SuccessResponse> {
332    cookies.remove(Cookie::build(("auth_token", "")).path("/").build());
333    Json(SuccessResponse {
334        status: 200,
335        message: "Logged out successfully".to_string(),
336    })
337}
338
339/// Retrieves a single user by ID (requires authentication).
340#[get("/users/<id>")]
341pub async fn get_user(
342    _auth: AuthClaims,
343    repo: &State<Arc<UserRepository>>,
344    id: &str,
345) -> Result<Json<UserEntity>, Json<ErrorResponse>> {
346    let uuid = match Uuid::parse_str(id) {
347        Ok(uuid) => uuid,
348        Err(_) => {
349            return Err(Json(ErrorResponse {
350                status: Status::BadRequest.code,
351                message: "Invalid user ID format".to_string(),
352            }));
353        }
354    };
355
356    let user = match repo.get_user_by_id(uuid).await {
357        Ok(Some(user)) => user,
358        Ok(None) => {
359            return Err(Json(ErrorResponse {
360                status: Status::NotFound.code,
361                message: "User not found".to_string(),
362            }));
363        }
364        Err(_) => {
365            return Err(Json(ErrorResponse {
366                status: Status::InternalServerError.code,
367                message: "Something went wrong, please try again later".to_string(),
368            }));
369        }
370    };
371
372    Ok(Json(user))
373}
374
375/// Retrieves a single user by email (requires authentication).
376#[get("/user/<email>")]
377pub async fn get_user_by_email(
378    _auth: AuthClaims,
379    repo: &State<Arc<UserRepository>>,
380    email: &str,
381) -> Result<Json<UserInfo>, Json<ErrorResponse>> {
382    let user = match repo.get_user_by_email(email).await {
383        Ok(Some(user)) => user,
384        Ok(None) => {
385            return Err(Json(ErrorResponse {
386                status: Status::NotFound.code,
387                message: "User not found".to_string(),
388            }));
389        }
390        Err(_) => {
391            return Err(Json(ErrorResponse {
392                status: Status::InternalServerError.code,
393                message: "Something went wrong, please try again later".to_string(),
394            }));
395        }
396    };
397
398    Ok(Json(UserInfo {
399        id: user.id.to_string(),
400        username: user.username,
401        email: user.email,
402        created_at: user.created_at.to_rfc3339(),
403    }))
404}
405
406/// Updates an existing user's information by ID (requires authentication).
407#[put("/update/<id>", data = "<credentials>")]
408pub async fn update_user(
409    _auth: AuthClaims,
410    repo: &State<Arc<UserRepository>>,
411    id: &str,
412    credentials: Json<RegistrationCredentials>,
413) -> Result<Json<UserEntity>, Json<ErrorResponse>> {
414    let uuid = match Uuid::parse_str(id) {
415        Ok(uuid) => uuid,
416        Err(_) => {
417            return Err(Json(ErrorResponse {
418                status: Status::BadRequest.code,
419                message: "Invalid user ID format".to_string(),
420            }));
421        }
422    };
423
424    // Check if the email is already in use by another user
425    if let Ok(Some(existing_user)) = repo.get_user_by_email(&credentials.email).await {
426        // If the email exists and it's not the user being updated
427        if existing_user.id != uuid {
428            return Err(Json(ErrorResponse {
429                status: Status::Conflict.code,
430                message: "A user with this email already exists".to_string(),
431            }));
432        }
433    }
434
435    let hashed_password = match hash_password(credentials.password.clone()) {
436        Ok(hash) => hash,
437        Err(_) => {
438            return Err(Json(ErrorResponse {
439                status: Status::InternalServerError.code,
440                message: "Something went wrong, please try again later".to_string(),
441            }));
442        }
443    };
444
445    let user = match repo
446        .update_user(
447            uuid,
448            Some(&credentials.username),
449            Some(&credentials.email),
450            Some(&hashed_password),
451        )
452        .await
453    {
454        Ok(Some(user)) => user,
455        Ok(None) => {
456            return Err(Json(ErrorResponse {
457                status: Status::NotFound.code,
458                message: "User not found".to_string(),
459            }));
460        }
461        Err(_) => {
462            return Err(Json(ErrorResponse {
463                status: Status::InternalServerError.code,
464                message: "Something went wrong, please try again later".to_string(),
465            }));
466        }
467    };
468
469    Ok(Json(user))
470}
471
472/// Deletes a user by ID (requires authentication).
473#[delete("/delete/<id>")]
474pub async fn delete_user(
475    _auth: AuthClaims,
476    repo: &State<Arc<UserRepository>>,
477    id: &str,
478) -> Result<Json<SuccessResponse>, Json<ErrorResponse>> {
479    let uuid = match Uuid::parse_str(id) {
480        Ok(uuid) => uuid,
481        Err(_) => {
482            return Err(Json(ErrorResponse {
483                status: Status::BadRequest.code,
484                message: "Invalid user ID format".to_string(),
485            }));
486        }
487    };
488
489    match repo.delete_user(uuid).await {
490        Ok(Some(_)) => Ok(Json(SuccessResponse {
491            status: Status::Ok.code,
492            message: "User deleted successfully".to_string(),
493        })),
494        Ok(None) => {
495            return Err(Json(ErrorResponse {
496                status: Status::NotFound.code,
497                message: "User not found".to_string(),
498            }));
499        }
500        Err(_) => {
501            return Err(Json(ErrorResponse {
502                status: Status::InternalServerError.code,
503                message: "Something went wrong, please try again later".to_string(),
504            }));
505        }
506    }
507}
508
509/// Collects all user-related routes for mounting.
510pub fn user_routes() -> Vec<rocket::Route> {
511    routes![
512        register,
513        login,
514        logout,
515        get_user,
516        get_user_by_email,
517        update_user,
518        delete_user
519    ]
520}
521"#;
522
523pub const DB: &str = r#"use dotenvy::dotenv;
524use rbatis::RBatis;
525use rbdc_pg::driver::PgDriver;
526use rocket::fairing::AdHoc;
527use std::sync::Arc;
528
529use crate::repositories::UserRepository;
530
531pub fn init() -> AdHoc {
532    AdHoc::on_ignite(
533        "Establish connection with PostgreSQL database",
534        |rocket| async {
535            match connect().await {
536                Ok(user_repository) => rocket.manage(user_repository),
537                Err(error) => {
538                    panic!("Cannot connect to database -> {:?}", error)
539                }
540            }
541        },
542    )
543}
544
545async fn connect() -> Result<Arc<UserRepository>, rbatis::Error> {
546    dotenv().ok();
547    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set...");
548
549    let rb = RBatis::new();
550    rb.link(PgDriver {}, &database_url).await?;
551
552    Ok(Arc::new(UserRepository::new(rb)))
553}
554"#;
555
556pub const REPOSITORIES: &str = r#"use crate::models::UserEntity;
557use chrono::Utc;
558use rbatis::{raw_sql, RBatis};
559use uuid::Uuid;
560
561pub struct UserRepository {
562    rb: RBatis,
563}
564
565impl UserRepository {
566    pub fn new(rb: RBatis) -> Self {
567        Self { rb }
568    }
569
570    //----------------------------------
571    // Create a new user
572    //----------------------------------
573    raw_sql!(insert_user(rb: &RBatis, user: &UserEntity) -> rbatis::rbdc::db::ExecResult =>
574        "INSERT INTO users (id, username, email, password, created_at) VALUES (?, ?, ?, ?, ?)"
575    );
576
577    pub async fn create_user(
578        &self,
579        username: &str,
580        email: &str,
581        password: &str,
582    ) -> Result<UserEntity, rbatis::Error> {
583        //-------------------------------------------
584        // Check for existing user
585        //-------------------------------------------
586        if let Some(_) = self.get_user_by_email(email).await? {
587            return Err(rbatis::Error::from("A user with this email already exists"));
588        }
589
590        let user = UserEntity {
591            id: Uuid::new_v4(),
592            username: username.to_string(),
593            email: email.to_string(),
594            password: password.to_string(),
595            created_at: Utc::now(),
596        };
597
598        Self::insert_user(&self.rb, &user).await?;
599        Ok(user)
600    }
601
602    //----------------------------------------------
603    // Get user by id
604    //----------------------------------------------
605    raw_sql!(get_by_id(rb: &RBatis, id: Uuid) -> Option<UserEntity> =>
606        "SELECT id, username, email, password, created_at FROM users WHERE id = ?"
607    );
608
609    pub async fn get_user_by_id(&self, id: Uuid) -> Result<Option<UserEntity>, rbatis::Error> {
610        Self::get_by_id(&self.rb, id).await
611    }
612
613    //-------------------------------------------------
614    // Get user by email
615    //-------------------------------------------------
616    raw_sql!(get_by_email(rb: &RBatis, email: &str) -> Option<UserEntity> =>
617        "SELECT id, username, email, password, created_at FROM users WHERE email = ?"
618    );
619
620    pub async fn get_user_by_email(
621        &self,
622        email: &str,
623    ) -> Result<Option<UserEntity>, rbatis::Error> {
624        Self::get_by_email(&self.rb, email).await
625    }
626
627    //----------------------------------
628    // Update user
629    //----------------------------------
630    raw_sql!(update_user_sql(
631        rb: &RBatis,
632        id: Uuid,
633        username: &str,
634        email: &str,
635        password: &str
636    ) -> rbatis::rbdc::db::ExecResult =>
637        "UPDATE users SET username = ?, email = ?, password = ? WHERE id = ?"
638    );
639
640    pub async fn update_user(
641        &self,
642        id: Uuid,
643        username: Option<&str>,
644        email: Option<&str>,
645        password: Option<&str>,
646    ) -> Result<Option<UserEntity>, rbatis::Error> {
647        let mut user = match self.get_user_by_id(id).await? {
648            Some(u) => u,
649            None => return Ok(None),
650        };
651
652        if let Some(u) = username {
653            user.username = u.to_string();
654        }
655        if let Some(e) = email {
656            user.email = e.to_string();
657        }
658        if let Some(p) = password {
659            user.password = p.to_string();
660        }
661
662        Self::update_user_sql(
663            &self.rb,
664            user.id,
665            &user.username,
666            &user.email,
667            &user.password,
668        )
669        .await?;
670
671        Ok(Some(user))
672    }
673
674    //-------------------------
675    // Delete user
676    //-------------------------
677    raw_sql!(delete_user_sql(rb: &RBatis, id: Uuid) -> rbatis::rbdc::db::ExecResult =>
678        "DELETE FROM users WHERE id = ?"
679    );
680
681    pub async fn delete_user(&self, id: Uuid) -> Result<Option<UserEntity>, rbatis::Error> {
682        if let Some(user) = self.get_user_by_id(id).await? {
683            Self::delete_user_sql(&self.rb, id).await?;
684            Ok(Some(user))
685        } else {
686            Ok(None)
687        }
688    }
689
690    //--------------------------------------
691    // List all users
692    //--------------------------------------
693    raw_sql!(list_users_sql(rb: &RBatis) -> Vec<UserEntity> =>
694        "SELECT id, username, email, password, created_at FROM users"
695    );
696
697    pub async fn list_users(&self) -> Result<Vec<UserEntity>, rbatis::Error> {
698        Self::list_users_sql(&self.rb).await
699    }
700}
701"#;
702
703pub const MIGRATIONS: &str = r#"-- Create users table migration
704-- File: migrations/001_create_users_table.sql
705
706CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
707
708CREATE TABLE users (
709    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
710    username VARCHAR(255) NOT NULL,
711    email VARCHAR(255) NOT NULL UNIQUE,
712    password VARCHAR(255) NOT NULL,
713    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
714);
715
716CREATE INDEX idx_users_email ON users(email);
717CREATE INDEX idx_users_username ON users(username);
718"#;
719
720pub const ENV_TEMPLATE: &str = r#"# Database Configuration
721#--------------------------------------
722# Database Configuration
723#--------------------------------------
724DATABASE_URL=postgresql://postgres:mysecretpassword@localhost/postgres
725
726#--------------------------------------
727# Generate a secure random string
728# openssl rand 32 -base64
729#--------------------------------------
730JWT_SECRET=your-super-secret-jwt-key-here
731
732#--------------------------------------
733# App Configuration
734#--------------------------------------
735ROCKET_PORT=8000
736ROCKET_ADDRESS=0.0.0.0
737"#;