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 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 let migration_created = generate_migration(migrations_dir, force);
42
43 let controller_created = generate_auth_controller(controllers_dir, force);
45
46 let mod_updated = update_controllers_mod(controllers_dir);
48
49 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 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 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(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 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 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 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}