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"#;