Skip to main content

ferro_cli/commands/
make_auth.rs

1use chrono::Local;
2use console::style;
3use std::fs;
4use std::path::Path;
5
6use crate::templates;
7
8pub fn run(force: bool) {
9    let controllers_dir = Path::new("src/controllers");
10    let migrations_dir = find_migrations_dir();
11
12    // Check we're in a Ferro project
13    if !controllers_dir.exists() {
14        eprintln!(
15            "{} Controllers directory not found at src/controllers",
16            style("Error:").red().bold()
17        );
18        eprintln!(
19            "{}",
20            style("Make sure you're in a Ferro project root directory.").dim()
21        );
22        std::process::exit(1);
23    }
24
25    if migrations_dir.is_none() {
26        eprintln!(
27            "{} Migrations directory not found",
28            style("Error:").red().bold()
29        );
30        eprintln!(
31            "{}",
32            style("Expected src/migrations or src/database/migrations").dim()
33        );
34        std::process::exit(1);
35    }
36
37    let migrations_dir = migrations_dir.unwrap();
38    println!("Scaffolding authentication system...\n");
39
40    // 1. Generate migration
41    let migration_created = generate_migration(migrations_dir, force);
42
43    // 2. Generate auth controller
44    let controller_created = generate_auth_controller(controllers_dir, force);
45
46    // 3. Update controllers/mod.rs
47    let mod_updated = update_controllers_mod(controllers_dir);
48
49    // Print summary
50    println!();
51    if migration_created {
52        println!(
53            "{} Created migration in {}",
54            style("Created:").green().bold(),
55            migrations_dir.display()
56        );
57    }
58    if controller_created {
59        println!(
60            "{} src/controllers/auth_controller.rs",
61            style("Created:").green().bold()
62        );
63    }
64    if mod_updated {
65        println!(
66            "{} src/controllers/mod.rs",
67            style("Updated:").green().bold()
68        );
69    }
70
71    // 4. Print next steps
72    print_next_steps();
73}
74
75fn find_migrations_dir() -> Option<&'static Path> {
76    if Path::new("src/migrations").exists() {
77        Some(Path::new("src/migrations"))
78    } else if Path::new("src/database/migrations").exists() {
79        Some(Path::new("src/database/migrations"))
80    } else {
81        None
82    }
83}
84
85fn generate_migration(migrations_dir: &Path, force: bool) -> bool {
86    // Check if an auth migration already exists
87    if !force {
88        if let Ok(entries) = fs::read_dir(migrations_dir) {
89            for entry in entries.flatten() {
90                let name = entry.file_name().to_string_lossy().to_string();
91                if name.contains("add_auth_fields") || name.contains("auth_fields") {
92                    println!(
93                        "{} Auth migration already exists: {}",
94                        style("Skip:").yellow().bold(),
95                        name
96                    );
97                    return false;
98                }
99            }
100        }
101    }
102
103    let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
104    let migration_name = format!("m{timestamp}_add_auth_fields_to_users");
105    let file_path = migrations_dir.join(format!("{migration_name}.rs"));
106
107    let content = templates::auth_migration_template();
108
109    if let Err(e) = fs::write(&file_path, content) {
110        eprintln!(
111            "{} Failed to write migration: {}",
112            style("Error:").red().bold(),
113            e
114        );
115        return false;
116    }
117
118    println!("{} {}", style("✓").green(), file_path.display());
119
120    // Register migration in mod.rs
121    register_migration(migrations_dir, &migration_name);
122
123    true
124}
125
126fn register_migration(migrations_dir: &Path, migration_name: &str) {
127    let mod_path = migrations_dir.join("mod.rs");
128
129    if !mod_path.exists() {
130        eprintln!(
131            "{} migrations/mod.rs not found, skipping registration",
132            style("Warning:").yellow().bold()
133        );
134        return;
135    }
136
137    let content = match fs::read_to_string(&mod_path) {
138        Ok(c) => c,
139        Err(e) => {
140            eprintln!(
141                "{} Failed to read mod.rs: {}",
142                style("Warning:").yellow().bold(),
143                e
144            );
145            return;
146        }
147    };
148
149    let mod_decl = format!("mod {migration_name};");
150    if content.contains(&mod_decl) {
151        return;
152    }
153
154    let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
155
156    // Find position to insert mod declaration (after other mod declarations)
157    let mut last_mod_idx = None;
158    for (i, line) in lines.iter().enumerate() {
159        if line.trim().starts_with("mod ") && !line.contains("mod tests") {
160            last_mod_idx = Some(i);
161        }
162        if line.trim().starts_with("pub mod m") {
163            last_mod_idx = Some(i);
164        }
165    }
166
167    let insert_idx = match last_mod_idx {
168        Some(idx) => idx + 1,
169        None => {
170            let mut idx = 0;
171            for (i, line) in lines.iter().enumerate() {
172                if line.contains("sea_orm_migration") || line.is_empty() {
173                    idx = i + 1;
174                } else if line.starts_with("mod ") || line.starts_with("pub struct") {
175                    break;
176                }
177            }
178            idx
179        }
180    };
181    lines.insert(insert_idx, mod_decl);
182
183    // Add to migrations() vec
184    let box_new_line = format!("            Box::new({migration_name}::Migration),");
185    let mut insert_vec_idx = None;
186
187    for (i, line) in lines.iter().enumerate() {
188        if line.contains("vec![]") {
189            lines[i] = line.replace("vec![]", &format!("vec![\n{box_new_line}\n        ]"));
190            if let Err(e) = fs::write(&mod_path, lines.join("\n")) {
191                eprintln!(
192                    "{} Failed to update mod.rs: {}",
193                    style("Warning:").yellow().bold(),
194                    e
195                );
196            }
197            return;
198        }
199        if line.contains("vec![") && !line.contains("vec![]") {
200            for (j, inner_line) in lines.iter().enumerate().skip(i + 1) {
201                if inner_line.trim() == "]" || inner_line.trim().starts_with(']') {
202                    insert_vec_idx = Some(j);
203                    break;
204                }
205            }
206            break;
207        }
208    }
209
210    if let Some(idx) = insert_vec_idx {
211        lines.insert(idx, box_new_line);
212    }
213
214    if let Err(e) = fs::write(&mod_path, lines.join("\n")) {
215        eprintln!(
216            "{} Failed to update mod.rs: {}",
217            style("Warning:").yellow().bold(),
218            e
219        );
220    }
221}
222
223fn generate_auth_controller(controllers_dir: &Path, force: bool) -> bool {
224    let file_path = controllers_dir.join("auth_controller.rs");
225
226    if file_path.exists() && !force {
227        println!(
228            "{} Auth controller already exists at {}",
229            style("Skip:").yellow().bold(),
230            file_path.display()
231        );
232        return false;
233    }
234
235    let content = templates::auth_controller_template();
236
237    if let Err(e) = fs::write(&file_path, content) {
238        eprintln!(
239            "{} Failed to write auth controller: {}",
240            style("Error:").red().bold(),
241            e
242        );
243        return false;
244    }
245
246    println!("{} src/controllers/auth_controller.rs", style("✓").green());
247    true
248}
249
250fn update_controllers_mod(controllers_dir: &Path) -> bool {
251    let mod_path = controllers_dir.join("mod.rs");
252
253    if !mod_path.exists() {
254        let content = "pub mod auth_controller;\n";
255        if let Err(e) = fs::write(&mod_path, content) {
256            eprintln!(
257                "{} Failed to create mod.rs: {}",
258                style("Error:").red().bold(),
259                e
260            );
261            return false;
262        }
263        return true;
264    }
265
266    let content = match fs::read_to_string(&mod_path) {
267        Ok(c) => c,
268        Err(e) => {
269            eprintln!(
270                "{} Failed to read mod.rs: {}",
271                style("Error:").red().bold(),
272                e
273            );
274            return false;
275        }
276    };
277
278    let mod_decl = "pub mod auth_controller;";
279    if content.contains(mod_decl) {
280        return false;
281    }
282
283    // Find position to insert (after other pub mod declarations)
284    let mut lines: Vec<&str> = content.lines().collect();
285    let mut last_pub_mod_idx = None;
286    for (i, line) in lines.iter().enumerate() {
287        if line.trim().starts_with("pub mod ") {
288            last_pub_mod_idx = Some(i);
289        }
290    }
291
292    let insert_idx = match last_pub_mod_idx {
293        Some(idx) => idx + 1,
294        None => 0,
295    };
296    lines.insert(insert_idx, mod_decl);
297
298    let new_content = lines.join("\n");
299    if let Err(e) = fs::write(&mod_path, new_content) {
300        eprintln!(
301            "{} Failed to update mod.rs: {}",
302            style("Error:").red().bold(),
303            e
304        );
305        return false;
306    }
307
308    true
309}
310
311fn print_next_steps() {
312    println!("\n{}", style("Next steps:").bold());
313    println!(
314        "\n  {} Update your auth provider (src/providers/auth_provider.rs):",
315        style("1.").dim()
316    );
317    println!();
318    println!(
319        "{}",
320        style("     use ferro::{{Auth, Authenticatable, UserProvider, FrameworkError, verify}};")
321            .cyan()
322    );
323    println!(
324        "{}",
325        style("     use crate::models::users::{{self, Entity, Column, Model as User}};").cyan()
326    );
327    println!("{}", style("     use sea_orm::prelude::*;").cyan());
328    println!("{}", style("     use std::sync::Arc;").cyan());
329    println!();
330    println!("{}", style("     pub struct DatabaseUserProvider;").cyan());
331    println!();
332    println!("{}", style("     #[async_trait::async_trait]").cyan());
333    println!(
334        "{}",
335        style("     impl UserProvider for DatabaseUserProvider {").cyan()
336    );
337    println!(
338        "{}",
339        style("         async fn retrieve_by_id(&self, id: i64)").cyan()
340    );
341    println!(
342        "{}",
343        style("             -> Result<Option<Arc<dyn Authenticatable>>, FrameworkError> {").cyan()
344    );
345    println!("{}", style("             let user = User::query()").cyan());
346    println!(
347        "{}",
348        style("                 .filter(Column::Id.eq(id as i32))").cyan()
349    );
350    println!("{}", style("                 .first().await?;").cyan());
351    println!(
352        "{}",
353        style("             Ok(user.map(|u| Arc::new(u) as Arc<dyn Authenticatable>))").cyan()
354    );
355    println!("{}", style("         }").cyan());
356    println!();
357    println!(
358        "{}",
359        style("         async fn retrieve_by_credentials(&self, credentials: &serde_json::Value)")
360            .cyan()
361    );
362    println!(
363        "{}",
364        style("             -> Result<Option<Arc<dyn Authenticatable>>, FrameworkError> {").cyan()
365    );
366    println!(
367        "{}",
368        style("             let email = credentials[\"email\"].as_str().unwrap_or_default();")
369            .cyan()
370    );
371    println!(
372        "{}",
373        style("             let user = User::find_by_email(email).await?;").cyan()
374    );
375    println!(
376        "{}",
377        style("             Ok(user.map(|u| Arc::new(u) as Arc<dyn Authenticatable>))").cyan()
378    );
379    println!("{}", style("         }").cyan());
380    println!();
381    println!(
382        "{}",
383        style("         async fn validate_credentials(&self, user: &dyn Authenticatable,").cyan()
384    );
385    println!(
386        "{}",
387        style("             credentials: &serde_json::Value) -> Result<bool, FrameworkError> {")
388            .cyan()
389    );
390    println!(
391        "{}",
392        style(
393            "             let password = credentials[\"password\"].as_str().unwrap_or_default();"
394        )
395        .cyan()
396    );
397    println!(
398        "{}",
399        style("             let user = user.as_any().downcast_ref::<User>()").cyan()
400    );
401    println!("{}", style("                 .ok_or_else(|| FrameworkError::internal(\"Invalid user type\".into()))?;").cyan());
402    println!(
403        "{}",
404        style("             verify(password, &user.password)").cyan()
405    );
406    println!("{}", style("         }").cyan());
407    println!("{}", style("     }").cyan());
408
409    println!(
410        "\n  {} Add auth routes to src/routes.rs:",
411        style("2.").dim()
412    );
413    println!();
414    println!(
415        "{}",
416        style("     use crate::controllers::auth_controller;").cyan()
417    );
418    println!(
419        "{}",
420        style("     use ferro::{{AuthMiddleware, GuestMiddleware, group, post}};").cyan()
421    );
422    println!();
423    println!(
424        "{}",
425        style("     // Guest-only routes (login/register)").cyan()
426    );
427    println!("{}", style("     group!(\"/auth\")").cyan());
428    println!(
429        "{}",
430        style("         .middleware(GuestMiddleware::redirect_to(\"/\"))").cyan()
431    );
432    println!("{}", style("         .routes([").cyan());
433    println!(
434        "{}",
435        style("             post!(\"/register\", auth_controller::register),").cyan()
436    );
437    println!(
438        "{}",
439        style("             post!(\"/login\", auth_controller::login),").cyan()
440    );
441    println!("{}", style("         ])").cyan());
442    println!();
443    println!("{}", style("     // Authenticated routes").cyan());
444    println!(
445        "{}",
446        style("     post!(\"/auth/logout\", auth_controller::logout)").cyan()
447    );
448    println!(
449        "{}",
450        style("         .middleware(AuthMiddleware::new())").cyan()
451    );
452
453    println!("\n  {} Run the migration:", style("3.").dim());
454    println!("     {}", style("ferro db:migrate").cyan());
455    println!();
456}