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<users::Model> to auto-extract the authenticated user:
90//!
91//!   use ferro::AuthUser;
92//!
93//!   #[handler]
94//!   pub async fn profile(user: AuthUser<users::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 sea_orm::ActiveValue;
103use serde::Deserialize;
104
105use crate::models::users;
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) = users::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 = users::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 = users::Entity::insert(user)
185        .exec_with_returning(&ferro::database::connection().await)
186        .await
187        .map_err(|e| {
188            HttpResponse::json(serde_json::json!({
189                "message": format!("Failed to create user: {}", e)
190            }))
191            .status(500)
192        })?;
193
194    // Log in the new user
195    Auth::login(user.id as i64);
196
197    Ok(HttpResponse::json(serde_json::json!({
198        "user": {
199            "id": user.id,
200            "name": user.name,
201            "email": user.email,
202        }
203    }))
204    .status(201))
205}
206
207/// Log in an existing user
208#[handler]
209pub async fn login(req: Request) -> Response {
210    let input: LoginInput = req.input().await.map_err(|_| {
211        HttpResponse::json(serde_json::json!({
212            "message": "Invalid request body."
213        }))
214        .status(422)
215    })?;
216
217    // Validate input
218    let data = serde_json::json!({
219        "email": input.email,
220        "password": input.password,
221    });
222
223    if let Err(errors) = Validator::new(&data)
224        .rules("email", rules![required(), email()])
225        .rules("password", rules![required()])
226        .validate()
227    {
228        return Err(HttpResponse::json(serde_json::json!({
229            "message": "Validation failed.",
230            "errors": errors,
231        }))
232        .status(422));
233    }
234
235    // Attempt authentication
236    let email = input.email.clone();
237    let password = input.password.clone();
238
239    let result = Auth::attempt(|| async {
240        let user = users::Model::find_by_email(&email).await?;
241        match user {
242            Some(user) => {
243                if verify(&password, &user.password)? {
244                    Ok(Some(user.id as i64))
245                } else {
246                    Ok(None)
247                }
248            }
249            None => Ok(None),
250        }
251    })
252    .await;
253
254    match result {
255        Ok(Some(_id)) => {
256            // Re-fetch user for response
257            let user = users::Model::find_by_email(&input.email)
258                .await
259                .map_err(|e| {
260                    HttpResponse::json(serde_json::json!({
261                        "message": format!("Database error: {}", e)
262                    }))
263                    .status(500)
264                })?;
265
266            match user {
267                Some(user) => json_response!({
268                    "user": {
269                        "id": user.id,
270                        "name": user.name,
271                        "email": user.email,
272                    }
273                }),
274                None => Err(HttpResponse::json(serde_json::json!({
275                    "email": ["These credentials do not match our records."]
276                }))
277                .status(422)),
278            }
279        }
280        Ok(None) => Err(HttpResponse::json(serde_json::json!({
281            "email": ["These credentials do not match our records."]
282        }))
283        .status(422)),
284        Err(e) => Err(HttpResponse::json(serde_json::json!({
285            "message": format!("Authentication error: {}", e)
286        }))
287        .status(500)),
288    }
289}
290
291/// Log out the current user
292#[handler]
293pub async fn logout(_req: Request) -> Response {
294    Auth::logout();
295    json_response!({
296        "message": "Logged out successfully."
297    })
298}
299"#
300    .to_string()
301}