Skip to main content

ferro_cli/templates/
auth.rs

1// ============================================================================
2// Auth scaffolding templates
3// ============================================================================
4
5/// Migration template that adds auth fields to an existing users table.
6///
7/// Uses ALTER TABLE to add name, email (unique), password, and remember_token.
8pub fn auth_migration_template() -> String {
9    r#"use sea_orm_migration::prelude::*;
10
11#[derive(DeriveMigrationName)]
12pub struct Migration;
13
14#[async_trait::async_trait]
15impl MigrationTrait for Migration {
16    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
17        // Add auth fields to existing users table
18        manager
19            .alter_table(
20                Table::alter()
21                    .table(Users::Table)
22                    .add_column(ColumnDef::new(Users::Name).string().not_null().default(""))
23                    .add_column(ColumnDef::new(Users::Email).string().not_null().default(""))
24                    .add_column(ColumnDef::new(Users::Password).string().not_null().default(""))
25                    .add_column(ColumnDef::new(Users::RememberToken).string().null())
26                    .to_owned(),
27            )
28            .await?;
29
30        // Add unique index on email
31        manager
32            .create_index(
33                Index::create()
34                    .name("idx_users_email_unique")
35                    .table(Users::Table)
36                    .col(Users::Email)
37                    .unique()
38                    .to_owned(),
39            )
40            .await?;
41
42        Ok(())
43    }
44
45    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
46        manager
47            .drop_index(
48                Index::drop()
49                    .name("idx_users_email_unique")
50                    .table(Users::Table)
51                    .to_owned(),
52            )
53            .await?;
54
55        manager
56            .alter_table(
57                Table::alter()
58                    .table(Users::Table)
59                    .drop_column(Users::Name)
60                    .drop_column(Users::Email)
61                    .drop_column(Users::Password)
62                    .drop_column(Users::RememberToken)
63                    .to_owned(),
64            )
65            .await?;
66
67        Ok(())
68    }
69}
70
71#[derive(DeriveIden)]
72enum Users {
73    Table,
74    Name,
75    Email,
76    Password,
77    RememberToken,
78}
79"#
80    .to_string()
81}
82
83/// Auth controller template with register, login, and logout handlers.
84pub fn auth_controller_template() -> String {
85    r#"//! Authentication controller
86//!
87//! Handles user registration, login, and logout.
88//!
89//! Tip: Use AuthUser<user::Model> to auto-extract the authenticated user:
90//!
91//!   use ferro::AuthUser;
92//!
93//!   #[handler]
94//!   pub async fn profile(user: AuthUser<user::Model>) -> Response {
95//!       Ok(HttpResponse::json(serde_json::json!({"user": user.name})))
96//!   }
97
98use ferro::database::ModelMut;
99use ferro::http::{HttpResponse, Request, Response};
100use ferro::{handler, hash, json_response, rules, verify};
101use ferro::{Auth, Validator, required, string, email, min};
102use ferro::ActiveValue;
103use serde::Deserialize;
104
105use crate::models::user;
106
107#[derive(Deserialize)]
108struct RegisterInput {
109    name: String,
110    email: String,
111    password: String,
112    password_confirmation: String,
113}
114
115#[derive(Deserialize)]
116struct LoginInput {
117    email: String,
118    password: String,
119}
120
121/// Register a new user
122#[handler]
123pub async fn register(req: Request) -> Response {
124    let input: RegisterInput = req.input().await.map_err(|_| {
125        HttpResponse::json(serde_json::json!({
126            "message": "Invalid request body."
127        }))
128        .status(422)
129    })?;
130
131    // Validate input
132    let data = serde_json::json!({
133        "name": input.name,
134        "email": input.email,
135        "password": input.password,
136        "password_confirmation": input.password_confirmation,
137    });
138
139    let mut validator = Validator::new(&data)
140        .rules("name", rules![required(), string()])
141        .rules("email", rules![required(), email()])
142        .rules("password", rules![required(), string(), min(8)]);
143
144    // Check password confirmation
145    if input.password != input.password_confirmation {
146        validator = validator.with_error("password_confirmation", "Passwords do not match.");
147    }
148
149    // Check email uniqueness
150    if let Some(_existing) = user::Model::find_by_email(&input.email).await.map_err(|e| {
151        HttpResponse::json(serde_json::json!({
152            "message": format!("Database error: {}", e)
153        }))
154        .status(500)
155    })? {
156        validator = validator.with_error("email", "This email is already registered.");
157    }
158
159    if let Err(errors) = validator.validate() {
160        return Err(HttpResponse::json(serde_json::json!({
161            "message": "Validation failed.",
162            "errors": errors,
163        }))
164        .status(422));
165    }
166
167    // Hash password
168    let password_hash = hash(&input.password).map_err(|e| {
169        HttpResponse::json(serde_json::json!({
170            "message": format!("Failed to hash password: {}", e)
171        }))
172        .status(500)
173    })?;
174
175    // Create user
176    let user = user::ActiveModel {
177        name: ActiveValue::Set(input.name.clone()),
178        email: ActiveValue::Set(input.email.clone()),
179        password: ActiveValue::Set(password_hash),
180        remember_token: ActiveValue::Set(None),
181        ..Default::default()
182    };
183
184    let user = user::Entity::insert_one(user)
185        .await
186        .map_err(|e| {
187            HttpResponse::json(serde_json::json!({
188                "message": format!("Failed to create user: {}", e)
189            }))
190            .status(500)
191        })?;
192
193    // Log in the new user
194    Auth::login(user.id as i64);
195
196    Ok(HttpResponse::json(serde_json::json!({
197        "user": {
198            "id": user.id,
199            "name": user.name,
200            "email": user.email,
201        }
202    }))
203    .status(201))
204}
205
206/// Log in an existing user
207#[handler]
208pub async fn login(req: Request) -> Response {
209    let input: LoginInput = req.input().await.map_err(|_| {
210        HttpResponse::json(serde_json::json!({
211            "message": "Invalid request body."
212        }))
213        .status(422)
214    })?;
215
216    // Validate input
217    let data = serde_json::json!({
218        "email": input.email,
219        "password": input.password,
220    });
221
222    if let Err(errors) = Validator::new(&data)
223        .rules("email", rules![required(), email()])
224        .rules("password", rules![required()])
225        .validate()
226    {
227        return Err(HttpResponse::json(serde_json::json!({
228            "message": "Validation failed.",
229            "errors": errors,
230        }))
231        .status(422));
232    }
233
234    // Attempt authentication
235    let email = input.email.clone();
236    let password = input.password.clone();
237
238    let result = Auth::attempt(|| async {
239        let user = user::Model::find_by_email(&email).await?;
240        match user {
241            Some(user) => {
242                if verify(&password, &user.password)? {
243                    Ok(Some(user.id as i64))
244                } else {
245                    Ok(None)
246                }
247            }
248            None => Ok(None),
249        }
250    })
251    .await;
252
253    match result {
254        Ok(Some(_id)) => {
255            // Re-fetch user for response
256            let user = user::Model::find_by_email(&input.email)
257                .await
258                .map_err(|e| {
259                    HttpResponse::json(serde_json::json!({
260                        "message": format!("Database error: {}", e)
261                    }))
262                    .status(500)
263                })?;
264
265            match user {
266                Some(user) => json_response!({
267                    "user": {
268                        "id": user.id,
269                        "name": user.name,
270                        "email": user.email,
271                    }
272                }),
273                None => Err(HttpResponse::json(serde_json::json!({
274                    "email": ["These credentials do not match our records."]
275                }))
276                .status(422)),
277            }
278        }
279        Ok(None) => Err(HttpResponse::json(serde_json::json!({
280            "email": ["These credentials do not match our records."]
281        }))
282        .status(422)),
283        Err(e) => Err(HttpResponse::json(serde_json::json!({
284            "message": format!("Authentication error: {}", e)
285        }))
286        .status(500)),
287    }
288}
289
290/// Log out the current user
291#[handler]
292pub async fn logout(_req: Request) -> Response {
293    Auth::logout();
294    json_response!({
295        "message": "Logged out successfully."
296    })
297}
298"#
299    .to_string()
300}