1use std::fs;
2use colored::*;
3use regex::Regex;
4use super::scaffolding::update_controller_mod_rs;
5
6pub async fn make_auth() {
7 println!("\n{}", "🔐 Scaffolding Authentication...".magenta().bold());
8
9 let auth_route_path = "src/routes/auth.rs";
11 let auth_route_template = r#"use axum::{Router, routing::{get, post}, middleware::from_fn};
12use crate::app::http::controllers::auth;
13use crate::app::http::middleware::auth::guest_middleware;
14use crate::server::AppState;
15
16pub fn router() -> Router<AppState> {
17 Router::new()
18 .route("/login", get(auth::auth_controller::AuthController::login_page))
19 .route("/login", post(auth::auth_controller::AuthController::login))
20 .route("/register", get(auth::auth_controller::AuthController::register_page))
21 .route("/register", post(auth::auth_controller::AuthController::register))
22 .route("/forgot-password", get(auth::auth_controller::AuthController::forgot_password_page))
23 .route("/forgot-password", post(auth::auth_controller::AuthController::send_reset_link))
24 .route("/reset-password", get(auth::auth_controller::AuthController::reset_password_page))
25 .route("/reset-password", post(auth::auth_controller::AuthController::update_password))
26 .layer(from_fn(guest_middleware))
27}
28"#;
29 if !std::path::Path::new(auth_route_path).exists() {
30 fs::write(auth_route_path, auth_route_template).ok();
31 println!(" {} {}", "✅ Created:".green(), auth_route_path.cyan());
32 } else {
33 println!(" {} {}", "⚠️ Exists:".yellow(), auth_route_path.cyan());
34 }
35
36 let routes_mod_path = "src/routes/mod.rs";
38 if let Ok(mut content) = fs::read_to_string(routes_mod_path) {
39 if !content.contains("pub mod auth;") {
40 content.push_str("pub mod auth;\n");
41 fs::write(routes_mod_path, content).ok();
42 println!(" {} {}", "📝 Updated:".blue(), routes_mod_path.cyan());
43 }
44 }
45
46 let web_route_path = "src/routes/web.rs";
48 if let Ok(mut content) = fs::read_to_string(web_route_path) {
49 if !content.contains("use crate::routes::auth as auth_routes;") {
50 content = content.replace("use axum::{Router, routing::get};", "use axum::{Router, routing::{get, post}, middleware::from_fn};");
51 content = content.replace("use crate::server::AppState;", "use crate::app::http::controllers::{auth, dashboard_controller};\nuse crate::app::http::middleware::auth::auth_middleware;\nuse crate::server::AppState;\nuse crate::routes::auth as auth_routes;");
52
53 let merge_logic = r#"let auth_protected_routes = Router::new()
54 .route("/dashboard", get(dashboard_controller::DashboardController::index))
55 .route("/logout", post(auth::auth_controller::AuthController::logout))
56 .layer(from_fn(auth_middleware));
57
58 Router::new()
59 .route("/", get(welcome_controller::index))
60 .route("/dev", get(welcome_controller::dev_info))
61 .merge(auth_routes::router())
62 .merge(auth_protected_routes)"#;
63
64 let re = Regex::new(r#"(?s)Router::new\(\s*\n\s*\.route\("/", get\(welcome_controller::index\)\)\s*\n\s*\.route\("/dev", get\(welcome_controller::dev_info\)\)"#).unwrap();
66 if re.is_match(&content) {
67 content = re.replace(&content, merge_logic).to_string();
68 } else {
69 content = content.replace("Router::new()\n .route(\"/\", get(welcome_controller::index))\n .route(\"/dev\", get(welcome_controller::dev_info))", merge_logic);
71 }
72
73 fs::write(web_route_path, content).ok();
74 println!(" {} {}", "📝 Updated:".blue(), web_route_path.cyan());
75 }
76 }
77
78 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
80 let migration_name = format!("m{}_create_password_resets_table", timestamp);
81 let migration_path = format!("database/migrations/{}.rs", migration_name);
82
83 let mut exists = false;
85 if let Ok(entries) = std::fs::read_dir("database/migrations") {
86 for entry in entries.flatten() {
87 if let Some(name) = entry.file_name().to_str() {
88 if name.ends_with("_create_password_resets_table.rs") {
89 exists = true;
90 println!(" {} {}", "⚠️ Exists:".yellow(), name.cyan());
91 break;
92 }
93 }
94 }
95 }
96
97 if !exists {
98 let migration_template = format!(r#"use sea_orm_migration::prelude::*;
99
100#[derive(Iden)]
101enum PasswordResets {{
102 Table,
103 Email,
104 Token,
105 CreatedAt,
106}}
107
108#[derive(DeriveMigrationName)]
109pub struct Migration;
110
111#[async_trait::async_trait]
112impl MigrationTrait for Migration {{
113 async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
114 manager
115 .create_table(
116 Table::create()
117 .table(PasswordResets::Table)
118 .if_not_exists()
119 .col(ColumnDef::new(PasswordResets::Email).string().not_null().primary_key())
120 .col(ColumnDef::new(PasswordResets::Token).string().not_null())
121 .col(
122 ColumnDef::new(PasswordResets::CreatedAt)
123 .timestamp()
124 .default(Expr::current_timestamp())
125 .not_null(),
126 )
127 .to_owned(),
128 )
129 .await
130 }}
131
132 async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
133 manager
134 .drop_table(Table::drop().table(PasswordResets::Table).to_owned())
135 .await
136 }}
137}}
138"#);
139 fs::write(&migration_path, migration_template).ok();
140
141 super::scaffolding::update_migration_mod_rs(&migration_name);
142 println!(" {} {}", "✅ Created:".green(), format!("Migration {}", migration_name).cyan());
143 }
144
145 let auth_controller_dir = "src/app/http/controllers/auth";
147 fs::create_dir_all(auth_controller_dir).ok();
148 let auth_controller_mod = "src/app/http/controllers/auth/mod.rs";
149 if !std::path::Path::new(auth_controller_mod).exists() {
150 fs::write(auth_controller_mod, "pub mod auth_controller;").ok();
151 }
152 update_controller_mod_rs("auth");
153
154 let model_path = "src/app/models/password_resets.rs";
156 if !std::path::Path::new(model_path).exists() {
157 let model_template = r#"use sea_orm::entity::prelude::*;
158use serde::{Deserialize, Serialize};
159
160#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
161#[sea_orm(table_name = "password_resets")]
162pub struct Model {
163 #[sea_orm(primary_key, auto_increment = false)]
164 pub email: String,
165 pub token: String,
166 pub created_at: DateTime,
167}
168
169#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
170pub enum Relation {}
171
172impl ActiveModelBehavior for ActiveModel {}
173"#;
174 fs::write(model_path, model_template).ok();
175
176 let models_mod_path = "src/app/models/mod.rs";
178 if let Ok(mut content) = fs::read_to_string(models_mod_path) {
179 if !content.contains("pub mod password_resets;") {
180 content.push_str("pub mod password_resets;\n");
181 fs::write(models_mod_path, content).ok();
182 }
183 }
184 println!(" {} {}", "✅ Created:".green(), "Model password_resets".cyan());
185 }
186
187 let auth_controller_path = "src/app/http/controllers/auth/auth_controller.rs";
188 if !std::path::Path::new(auth_controller_path).exists() {
189 let controller_template = r#"/* ---------------------------------------------------------
190 * 📑 LABEL: AUTH CONTROLLER (auth/auth_controller.rs)
191 * Menangani pendaftaran, login, dan logout user.
192 * --------------------------------------------------------- */
193
194use crate::app::view;
195use crate::app::models::users;
196use crate::requests::Request;
197use crate::responses::ResponseHelper;
198use crate::server::AppState;
199use axum::{response::IntoResponse, extract::State};
200use bcrypt::{hash, verify, DEFAULT_COST};
201use uuid::Uuid;
202use serde::Deserialize;
203use validator::Validate;
204use crate::mail::MailService;
205use minijinja::context;
206use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, Set};
207
208#[derive(Deserialize, Validate)]
209pub struct RegisterRequest {
210 #[validate(length(min = 3, message = "Nama minimal 3 karakter"))]
211 pub name: String,
212
213 #[validate(email(message = "Format email tidak valid"))]
214 pub email: String,
215
216 #[validate(length(min = 8, message = "Password minimal 8 karakter"))]
217 pub password: String,
218}
219
220#[derive(Deserialize, Validate)]
221pub struct LoginRequest {
222 #[validate(email(message = "Format email tidak valid"))]
223 pub email: String,
224 pub password: String,
225 pub remember: Option<String>,
226}
227
228#[derive(Deserialize, Validate)]
229pub struct ForgotPasswordRequest {
230 #[validate(email(message = "Format email tidak valid"))]
231 pub email: String,
232}
233
234#[derive(Deserialize, Validate)]
235pub struct ResetPasswordRequest {
236 pub token: String,
237 #[validate(length(min = 8, message = "Password minimal 8 karakter"))]
238 pub password: String,
239}
240
241pub struct AuthController;
242
243impl AuthController {
244 /// Menampilkan halaman login
245 pub async fn login_page(req: Request) -> impl IntoResponse {
246 view(&req, "auth/login.rb.html", context! { title => "Login" })
247 }
248
249 /// Menampilkan halaman register
250 pub async fn register_page(req: Request) -> impl IntoResponse {
251 view(&req, "auth/register.rb.html", context! { title => "Daftar Akun" })
252 }
253
254 /// Proses Pendaftaran
255 pub async fn register(State(state): State<AppState>, req: Request) -> impl IntoResponse {
256 // 1. Validasi Input
257 let data = match req.validate::<RegisterRequest>() {
258 Ok(d) => d,
259 Err(_) => return ResponseHelper::redirect("/register"),
260 };
261
262 // 2. Cek apakah email sudah terdaftar
263 let existing = users::Entity::find()
264 .filter(users::Column::Email.eq(&data.email))
265 .one(&state.db)
266 .await
267 .ok()
268 .flatten();
269
270 if existing.is_some() {
271 return ResponseHelper::redirect_with_error("/register", "Email sudah terdaftar", req.session);
272 }
273
274 // 3. Hash Password
275 let hashed = hash(data.password, DEFAULT_COST).unwrap();
276
277 // 4. Simpan ke Database
278 let new_user = users::ActiveModel {
279 name: Set(data.name),
280 email: Set(data.email),
281 password: Set(hashed),
282 ..Default::default()
283 };
284
285 if let Err(e) = users::Entity::insert(new_user).exec(&state.db).await {
286 tracing::error!("Gagal menyimpan user: {}", e);
287 return ResponseHelper::redirect_with_error("/register", "Gagal mendaftar, coba lagi.", req.session);
288 }
289
290 ResponseHelper::redirect_with_success("/login", "Pendaftaran berhasil! Silakan login.", req.session)
291 }
292
293 /// Proses Login
294 pub async fn login(State(state): State<AppState>, req: Request) -> impl IntoResponse {
295 // 1. Validasi Input
296 let data = match req.validate::<LoginRequest>() {
297 Ok(d) => d,
298 Err(_) => return ResponseHelper::redirect("/login"),
299 };
300
301 // 2. Ambil User dari DB
302 let user = users::Entity::find()
303 .filter(users::Column::Email.eq(&data.email))
304 .one(&state.db)
305 .await
306 .ok()
307 .flatten();
308
309 if let Some(u) = user {
310 // 3. Verifikasi Password
311 if verify(data.password, &u.password).unwrap_or(false) {
312 // 4. Set Session
313 req.session.set("user_id", u.id);
314
315 // Handle "Remember Me"
316 if data.remember.is_some() {
317 // Set session expiration to 30 days if remember is checked
318 // Note: implementation depends on axum_session configuration
319 tracing::info!("Remember me checked for user: {}", u.email);
320 }
321
322 return ResponseHelper::redirect_with_success("/dashboard", "Selamat datang kembali!", req.session);
323 }
324 }
325
326 ResponseHelper::redirect_with_error("/login", "Email atau password salah", req.session)
327 }
328
329 /// Menampilkan halaman lupa password
330 pub async fn forgot_password_page(req: Request) -> impl IntoResponse {
331 view(&req, "auth/forgot.rb.html", context! { title => "Lupa Password" })
332 }
333
334 /// Kirim link reset password
335 pub async fn send_reset_link(State(state): State<AppState>, req: Request) -> impl IntoResponse {
336 let data = match req.validate::<ForgotPasswordRequest>() {
337 Ok(d) => d,
338 Err(_) => return ResponseHelper::redirect("/forgot-password"),
339 };
340
341 // 1. Cek apakah user ada
342 let user = users::Entity::find()
343 .filter(users::Column::Email.eq(&data.email))
344 .one(&state.db)
345 .await
346 .ok()
347 .flatten();
348
349 if let Some(u) = user {
350 // 2. Generate Token
351 let token = Uuid::new_v4().to_string();
352
353 // 3. Simpan Token
354 let reset = crate::app::models::password_resets::ActiveModel {
355 email: Set(u.email.clone()),
356 token: Set(token.clone()),
357 created_at: Set(chrono::Utc::now().naive_utc()),
358 };
359
360 let _ = crate::app::models::password_resets::Entity::insert(reset)
361 .on_conflict(
362 sea_orm::sea_query::OnConflict::column(crate::app::models::password_resets::Column::Email)
363 .update_column(crate::app::models::password_resets::Column::Token)
364 .update_column(crate::app::models::password_resets::Column::CreatedAt)
365 .to_owned()
366 )
367 .exec(&state.db)
368 .await;
369
370 // 4. Kirim Email (Gunakan Config::load().mail_*)
371 let config = crate::Config::load();
372 let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| "RustBasic".to_string());
373 let reset_url = format!("{}/reset-password?token={}", config.app_url, token);
374
375 let subject = format!("Reset Password - {}", app_name);
376 let body = crate::view::render_to_string("emails/reset.rb.html", context! {
377 app_name => app_name,
378 reset_url => reset_url,
379 });
380
381 if let Err(e) = MailService::send_email(&u.email, &subject, &body).await {
382 tracing::error!("Gagal mengirim email reset: {}", e);
383 }
384
385 tracing::info!("Reset link for {}: {}", u.email, reset_url);
386 }
387
388 ResponseHelper::redirect_with_success("/login", "Jika email terdaftar, link reset password akan dikirim.", req.session)
389 }
390
391 /// Menampilkan halaman reset password
392 pub async fn reset_password_page(req: Request) -> impl IntoResponse {
393 let token = req.input_as_str("token").unwrap_or_default();
394 view(&req, "auth/reset.rb.html", context! { title => "Reset Password", token => token })
395 }
396
397 /// Proses update password baru
398 pub async fn update_password(State(state): State<AppState>, req: Request) -> impl IntoResponse {
399 let data = match req.validate::<ResetPasswordRequest>() {
400 Ok(d) => d,
401 Err(_) => return ResponseHelper::redirect("/login"),
402 };
403
404 // 1. Cari Token
405 let reset = crate::app::models::password_resets::Entity::find()
406 .filter(crate::app::models::password_resets::Column::Token.eq(&data.token))
407 .one(&state.db)
408 .await
409 .ok()
410 .flatten();
411
412 if let Some(r) = reset {
413 // 2. Cek Kadaluarsa (60 Menit)
414 let now = chrono::Utc::now().naive_utc();
415 let duration = now.signed_duration_since(r.created_at);
416
417 if duration.num_minutes() > 60 {
418 // Hapus token yang sudah kadaluarsa
419 let _ = crate::app::models::password_resets::Entity::delete_by_id(r.email)
420 .exec(&state.db)
421 .await;
422
423 return ResponseHelper::redirect_with_error("/login", "Tautan reset password sudah kadaluarsa (melebihi 60 menit).", req.session);
424 }
425
426 // 3. Hash Password Baru
427 let hashed = bcrypt::hash(data.password, bcrypt::DEFAULT_COST).unwrap();
428
429 // 4. Update User
430 let _ = users::Entity::update_many()
431 .col_expr(users::Column::Password, sea_orm::sea_query::Expr::value(hashed))
432 .filter(users::Column::Email.eq(&r.email))
433 .exec(&state.db)
434 .await;
435
436 // 5. Hapus Token
437 let _ = crate::app::models::password_resets::Entity::delete_by_id(r.email)
438 .exec(&state.db)
439 .await;
440
441 return ResponseHelper::redirect_with_success("/login", "Password berhasil diubah. Silakan login.", req.session);
442 }
443
444 ResponseHelper::redirect_with_error("/login", "Token tidak valid atau sudah kadaluarsa.", req.session)
445 }
446
447 /// Proses Logout
448 pub async fn logout(req: Request) -> impl IntoResponse {
449 req.session.remove("user_id");
450 ResponseHelper::redirect_with_success("/", "Anda telah keluar.", req.session)
451 }
452}
453"#;
454 fs::write(auth_controller_path, controller_template).ok();
455 println!(" {} {}", "✅ Created:".green(), auth_controller_path.cyan());
456 }
457
458 let auth_view_dir = "src/resources/views/auth";
460 fs::create_dir_all(auth_view_dir).ok();
461
462 let login_template = r##"{% extends "layouts/app.rb.html" %}
463
464{% block title %}Login - RustBasic{% endblock %}
465
466{% block content %}
467<div class="split-screen">
468 <!-- Sisi Visual -->
469 <div class="split-side-visual">
470 <div class="visual-inner" style="max-width: 600px;">
471 <div style="margin-bottom: 2rem;">
472 <span class="badge" style="background: rgba(255,255,255,0.2); color: #fff; border: none;">RUSTBASIC FRAMEWORK</span>
473 </div>
474 <h1 style="font-size: 3.5rem; font-weight: 900; line-height: 1.1; margin-bottom: 1.5rem; text-shadow: 0 10px 20px rgba(0,0,0,0.1);">
475 Selamat Datang <br> <span style="color: rgba(255,255,255,0.8);">Kembali</span>
476 </h1>
477 <p style="font-size: 1.2rem; opacity: 0.9; margin-bottom: 2.5rem; font-weight: 500;">
478 Masuk untuk melanjutkan pengembangan aplikasi modern Anda dengan kecepatan dan keamanan Rust.
479 </p>
480 <div class="tech-stack" style="justify-content: center; margin-top: 1rem;">
481 <span class="badge">Axum</span>
482 <span class="badge">Sea-ORM</span>
483 <span class="badge">Minijinja</span>
484 </div>
485 </div>
486 </div>
487
488 <!-- Sisi Form -->
489 <div class="split-side-content">
490 <div class="content-container">
491 <div style="margin-bottom: 3rem;">
492 <h2 class="title" style="font-size: 2.8rem; margin-bottom: 0.5rem;">Login</h2>
493 <p class="text-muted" style="font-weight: 500;">Silakan masukkan akun Anda untuk melanjutkan.</p>
494 </div>
495
496 <form hx-post="/login" hx-target="body" hx-push-url="true" hx-indicator="#indicator" style="display: flex; flex-direction: column; gap: 1.5rem;">
497 <div>
498 <label class="form-label">Email Address</label>
499 <input type="email" name="email" class="form-control" placeholder="nama@email.com" value="{{ old.email }}" required autofocus>
500 {% if errors.email %}
501 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.email }}</div>
502 {% endif %}
503 </div>
504
505 <div>
506 <label class="form-label">Password</label>
507 <input type="password" name="password" class="form-control" placeholder="••••••••" required>
508 {% if errors.password %}
509 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.password }}</div>
510 {% endif %}
511 </div>
512
513 <div style="display: flex; justify-content: space-between; align-items: center;">
514 <label style="display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; cursor: pointer; color: var(--text-muted); font-weight: 500;">
515 <input type="checkbox" name="remember" value="1" style="width: 18px; height: 18px; accent-color: var(--primary);">
516 Ingat Saya
517 </label>
518 <a href="/forgot-password" style="font-size: 0.9rem; font-weight: 700; color: var(--primary); text-decoration: none;">Lupa Password?</a>
519 </div>
520
521 <div style="margin-top: 1rem;">
522 <button type="submit" class="btn btn-primary w-100" style="padding: 1.25rem;">
523 MASUK KE DASHBOARD
524 </button>
525 </div>
526
527 <p class="text-center" style="font-size: 0.95rem; color: var(--text-muted); margin-top: 1rem;">
528 Belum punya akun? <a href="/register" style="font-weight: 800; color: var(--accent); text-decoration: none;">Daftar Sekarang</a>
529 </p>
530 </form>
531 </div>
532 </div>
533</div>
534{% endblock %}
535"##;
536
537 let register_template = r##"{% extends "layouts/app.rb.html" %}
538
539{% block title %}Daftar - RustBasic{% endblock %}
540
541{% block content %}
542<div class="split-screen">
543 <!-- Sisi Visual -->
544 <div class="split-side-visual" style="background: linear-gradient(135deg, var(--secondary), var(--accent), var(--primary));">
545 <div class="visual-inner" style="max-width: 600px;">
546 <div style="margin-bottom: 2rem;">
547 <span class="badge" style="background: rgba(255,255,255,0.2); color: #fff; border: none;">JOIN REVOLUTION</span>
548 </div>
549 <h1 style="font-size: 3.5rem; font-weight: 900; line-height: 1.1; margin-bottom: 1.5rem; text-shadow: 0 10px 20px rgba(0,0,0,0.1);">
550 Mulai Perjalanan <br> <span style="color: rgba(255,255,255,0.8);">Anda</span>
551 </h1>
552 <p style="font-size: 1.2rem; opacity: 0.9; margin-bottom: 2.5rem; font-weight: 500;">
553 Bangun infrastruktur digital yang kokoh dengan framework yang mengutamakan keamanan dan performa maksimal.
554 </p>
555 <div style="display: flex; gap: 1rem; justify-content: center;">
556 <div style="text-align: center;">
557 <div style="font-size: 1.5rem; font-weight: 800;">100%</div>
558 <div style="font-size: 0.75rem; font-weight: 700; opacity: 0.8;">TYPE SAFE</div>
559 </div>
560 <div style="height: 40px; width: 1px; background: rgba(255,255,255,0.3);"></div>
561 <div style="text-align: center;">
562 <div style="font-size: 1.5rem; font-weight: 800;">BLAZING</div>
563 <div style="font-size: 0.75rem; font-weight: 700; opacity: 0.8;">FAST</div>
564 </div>
565 </div>
566 </div>
567 </div>
568
569 <!-- Sisi Form -->
570 <div class="split-side-content">
571 <div class="content-container">
572 <div style="margin-bottom: 3rem;">
573 <h2 class="title" style="font-size: 2.8rem; margin-bottom: 0.5rem;">Daftar</h2>
574 <p class="text-muted" style="font-weight: 500;">Lengkapi formulir di bawah untuk bergabung.</p>
575 </div>
576
577 <form hx-post="/register" hx-target="body" hx-push-url="true" hx-indicator="#indicator" style="display: flex; flex-direction: column; gap: 1.5rem;">
578 <div>
579 <label class="form-label">Nama Lengkap</label>
580 <input type="text" name="name" class="form-control" placeholder="Nama Anda" value="{{ old.name }}" required autofocus>
581 {% if errors.name %}
582 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.name }}</div>
583 {% endif %}
584 </div>
585
586 <div>
587 <label class="form-label">Email Address</label>
588 <input type="email" name="email" class="form-control" placeholder="nama@email.com" value="{{ old.email }}" required>
589 {% if errors.email %}
590 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.email }}</div>
591 {% endif %}
592 </div>
593
594 <div>
595 <label class="form-label">Password</label>
596 <input type="password" name="password" class="form-control" placeholder="Min. 8 karakter" required>
597 {% if errors.password %}
598 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.password }}</div>
599 {% endif %}
600 </div>
601
602 <div style="margin-top: 1rem;">
603 <button type="submit" class="btn btn-primary w-100" style="padding: 1.25rem;">
604 BUAT AKUN SEKARANG
605 </button>
606 </div>
607
608 <p class="text-center" style="font-size: 0.95rem; color: var(--text-muted); margin-top: 1rem;">
609 Sudah punya akun? <a href="/login" style="font-weight: 800; color: var(--accent); text-decoration: none;">Login Disini</a>
610 </p>
611 </form>
612 </div>
613 </div>
614</div>
615{% endblock %}
616"##;
617
618 let forgot_template = r##"{% extends "layouts/app.rb.html" %}
619
620{% block title %}Lupa Password - RustBasic{% endblock %}
621
622{% block content %}
623<div class="split-screen">
624 <!-- Sisi Visual -->
625 <div class="split-side-visual" style="background: linear-gradient(135deg, var(--primary), var(--secondary));">
626 <div class="visual-inner" style="max-width: 600px;">
627 <div style="margin-bottom: 2rem;">
628 <span class="badge" style="background: rgba(255,255,255,0.2); color: #fff; border: none;">SECURITY ASSIST</span>
629 </div>
630 <h1 style="font-size: 3.5rem; font-weight: 900; line-height: 1.1; margin-bottom: 1.5rem; text-shadow: 0 10px 20px rgba(0,0,0,0.1);">
631 Lupa <br> <span style="color: rgba(255,255,255,0.8);">Password?</span>
632 </h1>
633 <p style="font-size: 1.2rem; opacity: 0.9; margin-bottom: 2.5rem; font-weight: 500;">
634 Jangan khawatir, hal ini biasa terjadi. Kami akan membantu Anda mendapatkan akses kembali dengan aman.
635 </p>
636 </div>
637 </div>
638
639 <!-- Sisi Form -->
640 <div class="split-side-content">
641 <div class="content-container">
642 <div style="margin-bottom: 3rem;">
643 <h2 class="title" style="font-size: 2.8rem; margin-bottom: 0.5rem;">Reset</h2>
644 <p class="text-muted" style="font-weight: 500;">Masukkan email Anda untuk menerima link reset.</p>
645 </div>
646
647 <form hx-post="/forgot-password" hx-target="body" hx-push-url="true" hx-indicator="#indicator" style="display: flex; flex-direction: column; gap: 1.5rem;">
648 <div>
649 <label class="form-label">Email Address</label>
650 <input type="email" name="email" class="form-control" placeholder="nama@email.com" value="{{ old.email }}" required autofocus>
651 {% if errors.email %}
652 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.email }}</div>
653 {% endif %}
654 </div>
655
656 <div style="margin-top: 1rem;">
657 <button type="submit" class="btn btn-primary w-100" style="padding: 1.25rem;">
658 KIRIM LINK RESET PASSWORD
659 </button>
660 </div>
661
662 <p class="text-center" style="font-size: 0.95rem; color: var(--text-muted); margin-top: 1rem;">
663 Ingat password Anda? <a href="/login" style="font-weight: 800; color: var(--accent); text-decoration: none;">Login Disini</a>
664 </p>
665 </form>
666 </div>
667 </div>
668</div>
669{% endblock %}
670"##;
671
672 let login_view = "src/resources/views/auth/login.rb.html";
673 if !std::path::Path::new(login_view).exists() {
674 fs::write(login_view, login_template).ok();
675 }
676
677 let register_view = "src/resources/views/auth/register.rb.html";
678 if !std::path::Path::new(register_view).exists() {
679 fs::write(register_view, register_template).ok();
680 }
681
682 let forgot_view = "src/resources/views/auth/forgot.rb.html";
683 if !std::path::Path::new(forgot_view).exists() {
684 fs::write(forgot_view, forgot_template).ok();
685 }
686
687 let reset_view = "src/resources/views/auth/reset.rb.html";
688 if !std::path::Path::new(reset_view).exists() {
689 let reset_template = r##"{% extends "layouts/app.rb.html" %}
690
691{% block title %}Reset Password - RustBasic{% endblock %}
692
693{% block content %}
694<div class="split-screen">
695 <!-- Sisi Visual -->
696 <div class="split-side-visual" style="background: linear-gradient(135deg, var(--accent), var(--primary));">
697 <div class="visual-inner" style="max-width: 600px;">
698 <div style="margin-bottom: 2rem;">
699 <span class="badge" style="background: rgba(255,255,255,0.2); color: #fff; border: none;">RECOVER ACCESS</span>
700 </div>
701 <h1 style="font-size: 3.5rem; font-weight: 900; line-height: 1.1; margin-bottom: 1.5rem; text-shadow: 0 10px 20px rgba(0,0,0,0.1);">
702 Buat Password <br> <span style="color: rgba(255,255,255,0.8);">Baru</span>
703 </h1>
704 <p style="font-size: 1.2rem; opacity: 0.9; margin-bottom: 2.5rem; font-weight: 500;">
705 Hampir selesai! Gunakan kombinasi password yang kuat untuk menjaga keamanan akun Anda di masa depan.
706 </p>
707 </div>
708 </div>
709
710 <!-- Sisi Form -->
711 <div class="split-side-content">
712 <div class="content-container">
713 <div style="margin-bottom: 3rem;">
714 <h2 class="title" style="font-size: 2.8rem; margin-bottom: 0.5rem;">Update</h2>
715 <p class="text-muted" style="font-weight: 500;">Silakan masukkan password baru Anda.</p>
716 </div>
717
718 <form hx-post="/reset-password" hx-target="body" hx-push-url="true" hx-indicator="#indicator" style="display: flex; flex-direction: column; gap: 1.5rem;">
719 <input type="hidden" name="token" value="{{ token }}">
720
721 <div>
722 <label class="form-label">Password Baru</label>
723 <input type="password" name="password" class="form-control" placeholder="Min. 8 karakter" required autofocus>
724 {% if errors.password %}
725 <div style="color: var(--secondary); font-size: 0.85rem; margin-top: 0.5rem; font-weight: 600;">{{ errors.password }}</div>
726 {% endif %}
727 </div>
728
729 <div style="margin-top: 1rem;">
730 <button type="submit" class="btn btn-primary w-100" style="padding: 1.25rem;">
731 SIMPAN PASSWORD BARU
732 </button>
733 </div>
734 </form>
735 </div>
736 </div>
737</div>
738{% endblock %}
739"##;
740 fs::write(reset_view, reset_template).ok();
741 }
742
743 let email_reset_view = "src/resources/views/emails/reset.rb.html";
744 if !std::path::Path::new(email_reset_view).exists() {
745 fs::create_dir_all("src/resources/views/emails").ok();
746 let email_reset_template = r##"<!DOCTYPE html>
747<html>
748<head>
749 <meta charset="utf-8">
750 <style>
751 body { font-family: 'Inter', -apple-system, sans-serif; line-height: 1.6; color: #1a1a1a; margin: 0; padding: 0; }
752 .container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
753 .card { background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
754 .header { background: linear-gradient(135deg, #6366f1, #a855f7); padding: 40px; text-align: center; color: white; }
755 .content { padding: 40px; }
756 .button { display: inline-block; padding: 14px 32px; background: #6366f1; color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 24px 0; }
757 .footer { padding: 24px; text-align: center; font-size: 13px; color: #6b7280; }
758 h1 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.025em; }
759 p { margin: 16px 0; color: #4b5563; }
760 .divider { height: 1px; background: #f3f4f6; margin: 24px 0; }
761 </style>
762</head>
763<body>
764 <div class="container">
765 <div class="card">
766 <div class="header">
767 <h1>{{ app_name }}</h1>
768 </div>
769 <div class="content">
770 <h2 style="margin: 0; color: #111827; font-size: 20px;">Halo!</h2>
771 <p>Anda menerima email ini karena kami menerima permintaan reset password untuk akun Anda di <strong>{{ app_name }}</strong>.</p>
772
773 <div style="text-align: center;">
774 <a href="{{ reset_url }}" class="button">Reset Password Saya</a>
775 </div>
776
777 <p style="font-size: 14px; color: #9ca3af;">Link ini akan kadaluarsa dalam 60 menit. Jika Anda tidak merasa meminta reset password, abaikan saja email ini.</p>
778
779 <div class="divider"></div>
780
781 <p style="font-size: 12px; color: #9ca3af;">
782 Jika Anda kesulitan menekan tombol, salin dan tempel URL berikut ke browser Anda:<br>
783 <span style="word-break: break-all; color: #6366f1;">{{ reset_url }}</span>
784 </p>
785 </div>
786 </div>
787 <div class="footer">
788 © 2026 {{ app_name }}. All rights reserved.
789 </div>
790 </div>
791</body>
792</html>
793"##;
794 fs::write(email_reset_view, email_reset_template).ok();
795 }
796
797 let dashboard_view = "src/resources/views/dashboard.rb.html";
798 if !std::path::Path::new(dashboard_view).exists() {
799 let dashboard_template = r##"{% extends "layouts/app.rb.html" %}
800
801{% block title %}{{ title }} - RustBasic{% endblock %}
802
803{% block content %}
804<div class="split-screen" style="background: #f8faff;">
805 <!-- Sidebar / Navigation (Kiri) -->
806 <div class="split-side-visual" style="flex: 0.35; align-items: flex-start; text-align: left; padding: 3rem; background: linear-gradient(180deg, var(--text-main), #2d3436);">
807 <div style="width: 100%;">
808 <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 3rem;">
809 <div style="width: 50px; height: 50px; background: var(--primary); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-weight: 900; color: white; font-size: 1.5rem;">
810 R
811 </div>
812 <h2 style="font-size: 1.5rem; font-weight: 800; color: white;">RustBasic</h2>
813 </div>
814
815 <div style="background: rgba(255,255,255,0.05); padding: 1.5rem; border-radius: 1.5rem; border: 1px solid rgba(255,255,255,0.1); margin-bottom: 3rem;">
816 <div style="display: flex; align-items: center; gap: 1rem;">
817 <div style="width: 45px; height: 45px; background: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; color: white; font-size: 1.2rem;">
818 {{ user_name[0] | upper }}
819 </div>
820 <div>
821 <div style="font-weight: 700; color: white; font-size: 0.95rem;">{{ user_name }}</div>
822 <div style="font-size: 0.8rem; color: rgba(255,255,255,0.5);">Administrator</div>
823 </div>
824 </div>
825 </div>
826
827 <nav style="display: flex; flex-direction: column; gap: 0.5rem;">
828 <a href="/dashboard" class="btn" style="background: var(--primary); color: white; justify-content: flex-start; text-transform: none; letter-spacing: normal; padding: 1rem 1.5rem; border-radius: 12px;">
829 📊 Dashboard Overview
830 </a>
831 <a href="/" class="btn" style="color: rgba(255,255,255,0.6); justify-content: flex-start; text-transform: none; letter-spacing: normal; padding: 1rem 1.5rem;">
832 🏠 Main Website
833 </a>
834 </nav>
835
836 <div style="margin-top: 5rem;">
837 <form hx-post="/logout" hx-target="body" style="margin:0;">
838 <button type="submit" class="btn w-100" style="background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); border-radius: 12px; font-weight: 700; padding: 1rem;">
839 🚪 KELUAR SISTEM
840 </button>
841 </form>
842 </div>
843 </div>
844 </div>
845
846 <!-- Main Workspace (Kanan) -->
847 <div class="split-side-content" style="flex: 1.2; align-items: flex-start; justify-content: flex-start; padding: 0;">
848 <div style="width: 100%; padding: 4rem;">
849 <header style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4rem;">
850 <div>
851 <h1 class="title" style="font-size: 2.5rem; text-align: left; margin-bottom: 0.25rem;">Overview</h1>
852 <p class="text-muted" style="font-weight: 500;">Selamat datang kembali, kendalikan project Anda.</p>
853 </div>
854 <div style="display: flex; gap: 1rem;">
855 <div class="badge" style="background: white; padding: 0.8rem 1.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.02);">
856 Server: <span style="color: var(--primary);">Running</span>
857 </div>
858 </div>
859 </header>
860
861 <!-- Stats Grid -->
862 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 2rem; margin-bottom: 4rem;">
863 <div style="background: white; border-radius: 24px; padding: 2rem; box-shadow: 0 10px 20px rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.03);">
864 <div style="color: var(--text-muted); font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1.5rem;">
865 User Terdaftar
866 </div>
867 <div style="display: flex; align-items: baseline; gap: 0.5rem;">
868 <div style="font-size: 3rem; font-weight: 900; color: var(--text-main);">{{ total_users }}</div>
869 <div style="color: #10b981; font-weight: 700; font-size: 0.9rem;">↑ 12%</div>
870 </div>
871 </div>
872
873 <div style="background: white; border-radius: 24px; padding: 2rem; box-shadow: 0 10px 20px rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.03);">
874 <div style="color: var(--text-muted); font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1.5rem;">
875 Response Time
876 </div>
877 <div style="display: flex; align-items: baseline; gap: 0.5rem;">
878 <div style="font-size: 3rem; font-weight: 900; color: var(--accent);">24</div>
879 <div style="color: var(--accent); font-weight: 700; font-size: 0.9rem;">ms</div>
880 </div>
881 </div>
882
883 <div style="background: white; border-radius: 24px; padding: 2rem; box-shadow: 0 10px 20px rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.03);">
884 <div style="color: var(--text-muted); font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 1.5rem;">
885 Database Status
886 </div>
887 <div style="display: flex; align-items: center; gap: 0.8rem; padding: 0.5rem 0;">
888 <div style="width: 12px; height: 12px; background: #10b981; border-radius: 50%; box-shadow: 0 0 10px #10b981;"></div>
889 <div style="font-size: 1.5rem; font-weight: 800; color: #10b981;">HEALTHY</div>
890 </div>
891 </div>
892 </div>
893
894 <!-- Main Panel -->
895 <div class="glass-panel" style="max-width: none; padding: 3rem; margin: 0; border-radius: 32px; background: linear-gradient(135deg, white, #f1f3f5);">
896 <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem;">
897 <div>
898 <h3 style="font-size: 1.8rem; font-weight: 800; margin-bottom: 0.5rem;">Informasi Server</h3>
899 <p class="text-muted">Detail lingkungan eksekusi RustBasic Anda.</p>
900 </div>
901 <span class="badge" style="background: var(--primary); color: white;">v2026.1</span>
902 </div>
903
904 <div style="background: var(--text-main); color: #00ff00; padding: 2rem; border-radius: 16px; font-family: monospace; font-size: 0.9rem; line-height: 1.6; box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);">
905 <div style="color: #636e72;">// RustBasic Kernel System</div>
906 <div>[OK] Compiled with Axum 0.8.2</div>
907 <div>[OK] Database Pool: Sea-ORM Connection Established</div>
908 <div>[OK] Live Reload: Active on port 4000</div>
909 <div>[OK] Workers: 8 logical threads spawned</div>
910 </div>
911 </div>
912 </div>
913 </div>
914</div>
915{% endblock %}
916"##;
917 fs::write(dashboard_view, dashboard_template).ok();
918 }
919
920 let dashboard_controller_path = "src/app/http/controllers/dashboard_controller.rs";
922 if !std::path::Path::new(dashboard_controller_path).exists() {
923 let dashboard_template = r#"use crate::app::view;
924use crate::app::models::users;
925use crate::requests::Request;
926use crate::server::AppState;
927use axum::{response::IntoResponse, extract::State};
928use minijinja::context;
929use sea_orm::{EntityTrait, PaginatorTrait};
930
931pub struct DashboardController;
932
933impl DashboardController {
934 pub async fn index(State(state): State<AppState>, req: Request) -> impl IntoResponse {
935 let user_id = req.session.get::<i32>("user_id").unwrap_or(0);
936 let user = users::Entity::find_by_id(user_id).one(&state.db).await.ok().flatten();
937 let total_users = users::Entity::find().count(&state.db).await.unwrap_or(0);
938
939 view(&req, "dashboard.rb.html", context! {
940 title => "Dashboard",
941 user_name => user.as_ref().map(|u| u.name.clone()).unwrap_or("Guest".to_string()),
942 user_email => user.as_ref().map(|u| u.email.clone()).unwrap_or_default(),
943 total_users => total_users,
944 })
945 }
946}
947"#;
948 fs::write(dashboard_controller_path, dashboard_template).ok();
949 println!(" {} {}", "✅ Created:".green(), dashboard_controller_path.cyan());
950 }
951 update_controller_mod_rs("dashboard_controller");
952
953 println!(" {} Folder src/resources/views/auth dan dashboard siap.", "✅ Views:".green());
954
955 let welcome_path = "src/resources/views/welcome.rb.html";
957 if let Ok(content) = fs::read_to_string(welcome_path) {
958 if !content.contains("{% if auth %}") {
959 println!(" {} {}", "⚠️ Manual:".yellow(), "Pastikan welcome.rb.html memiliki tombol login/register.".dimmed());
960 } else {
961 println!(" {} {}", "✅ OK:".green(), "welcome.rb.html sudah memiliki logika auth.".dimmed());
962 }
963 }
964
965 println!("\n{}", "✨ Authentication scaffolded successfully!".green().bold());
966 println!("{}", "Jalankan 'cargo rustbasic route:list' untuk melihat route baru.".dimmed());
967}
968
969pub async fn remove_auth() {
970 println!("\n{}", "🗑️ Removing Authentication Scaffold...".red().bold());
971
972 let auth_route_path = "src/routes/auth.rs";
974 if std::path::Path::new(auth_route_path).exists() {
975 fs::remove_file(auth_route_path).ok();
976 println!(" {} {}", "✅ Deleted:".green(), auth_route_path.cyan());
977 }
978
979 let routes_mod_path = "src/routes/mod.rs";
981 if let Ok(mut content) = fs::read_to_string(routes_mod_path) {
982 if content.contains("pub mod auth;") {
983 content = content.replace("pub mod auth;\n", "");
984 fs::write(routes_mod_path, content).ok();
985 println!(" {} {}", "📝 Updated:".blue(), routes_mod_path.cyan());
986 }
987 }
988
989 let web_route_path = "src/routes/web.rs";
991 if let Ok(mut content) = fs::read_to_string(web_route_path) {
992 let mut changed = false;
993
994 if content.contains("use axum::{Router, routing::{get, post}, middleware::from_fn};") {
996 content = content.replace("use axum::{Router, routing::{get, post}, middleware::from_fn};", "use axum::{Router, routing::get};");
997 changed = true;
998 }
999
1000 let imports_to_remove = [
1001 "use crate::app::http::controllers::{auth, dashboard_controller};\n",
1002 "use crate::app::http::middleware::auth::auth_middleware;\n",
1003 "use crate::routes::auth as auth_routes;\n",
1004 "use crate::app::http::controllers::{auth, dashboard_controller};",
1005 "use crate::app::http::middleware::auth::auth_middleware;",
1006 "use crate::routes::auth as auth_routes;",
1007 ];
1008
1009 for imp in imports_to_remove {
1010 if content.contains(imp) {
1011 content = content.replace(imp, "");
1012 changed = true;
1013 }
1014 }
1015
1016 if content.contains("let auth_protected_routes = Router::new()") {
1018 let re = Regex::new(r##"(?s)\s*let auth_protected_routes = Router::new\(\).*?\.layer\(from_fn\(auth_middleware\)\);\s*"##).unwrap();
1019 content = re.replace(&content, "\n").to_string();
1020
1021 content = content.replace(".merge(auth_routes::router())", "");
1022 content = content.replace(".merge(auth_protected_routes)", "");
1023
1024 let clean_router = r#" Router::new()
1026 .route("/", get(welcome_controller::index))
1027 .route("/dev", get(welcome_controller::dev_info))"#;
1028
1029 let router_re = Regex::new(r##"(?s)Router::new\(\).*?\.route\(\s*\"/dev\"\s*,\s*get\(welcome_controller::dev_info\)\s*\)"##).unwrap();
1030 content = router_re.replace(&content, clean_router).to_string();
1031
1032 let multi_newline_re = Regex::new(r#"\n{3,}"#).unwrap();
1034 content = multi_newline_re.replace_all(&content, "\n\n").to_string();
1035
1036 changed = true;
1037 }
1038
1039 if changed {
1040 fs::write(web_route_path, content).ok();
1041 println!(" {} {}", "📝 Updated:".blue(), web_route_path.cyan());
1042 }
1043 }
1044
1045 let auth_controller_dir = "src/app/http/controllers/auth";
1047 if std::path::Path::new(auth_controller_dir).exists() {
1048 fs::remove_dir_all(auth_controller_dir).ok();
1049 println!(" {} {}", "✅ Deleted:".green(), auth_controller_dir.cyan());
1050 }
1051
1052 if let Ok(entries) = std::fs::read_dir("database/migrations") {
1054 for entry in entries.flatten() {
1055 if let Some(name) = entry.file_name().to_str() {
1056 if name.ends_with("_create_password_resets_table.rs") {
1057 let path = entry.path();
1058 fs::remove_file(&path).ok();
1059 println!(" {} {}", "✅ Deleted:".green(), path.display().to_string().cyan());
1060 }
1061 }
1062 }
1063 }
1064
1065 let model_path = "src/app/models/password_resets.rs";
1066 if std::path::Path::new(model_path).exists() {
1067 fs::remove_file(model_path).ok();
1068 println!(" {} {}", "✅ Deleted:".green(), model_path.cyan());
1069 }
1070
1071 let auth_view_dir = "src/resources/views/auth";
1073 if std::path::Path::new(auth_view_dir).exists() {
1074 fs::remove_dir_all(auth_view_dir).ok();
1075 println!(" {} {}", "✅ Deleted:".green(), auth_view_dir.cyan());
1076 }
1077
1078 let dashboard_path = "src/app/http/controllers/dashboard_controller.rs";
1080 if std::path::Path::new(dashboard_path).exists() {
1081 fs::remove_file(dashboard_path).ok();
1082 println!(" {} {}", "✅ Deleted:".green(), dashboard_path.cyan());
1083 }
1084
1085 let controllers_mod_path = "src/app/http/controllers/mod.rs";
1087 if let Ok(mut content) = fs::read_to_string(controllers_mod_path) {
1088 let mut changed = false;
1089 if content.contains("pub mod auth;") {
1090 content = content.replace("pub mod auth;\n", "");
1091 changed = true;
1092 }
1093 if content.contains("pub mod dashboard_controller;") {
1094 content = content.replace("pub mod dashboard_controller;\n", "");
1095 changed = true;
1096 }
1097 if changed {
1098 fs::write(controllers_mod_path, content).ok();
1099 println!(" {} {}", "📝 Updated:".blue(), controllers_mod_path.cyan());
1100 }
1101 }
1102
1103 let models_mod_path = "src/app/models/mod.rs";
1105 if let Ok(mut content) = fs::read_to_string(models_mod_path) {
1106 if content.contains("pub mod password_resets;") {
1107 content = content.replace("pub mod password_resets;\n", "");
1108 content = content.replace("pub mod password_resets;", "");
1109 fs::write(models_mod_path, content).ok();
1110 println!(" {} {}", "📝 Updated:".blue(), models_mod_path.cyan());
1111 }
1112 }
1113
1114 let migration_mod_path = "database/migrations/mod.rs";
1116 if let Ok(content) = fs::read_to_string(migration_mod_path) {
1117 let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
1118 let mut changed = false;
1119
1120 lines.retain(|line| {
1122 if line.contains("_create_password_resets_table;") {
1123 changed = true;
1124 false
1125 } else if line.contains("Box::new(") && line.contains("_create_password_resets_table::Migration") {
1126 changed = true;
1127 false
1128 } else {
1129 true
1130 }
1131 });
1132
1133 if changed {
1134 fs::write(migration_mod_path, lines.join("\n")).ok();
1135 println!(" {} {}", "📝 Updated:".blue(), migration_mod_path.cyan());
1136 }
1137 }
1138
1139 println!("\n{}", "✨ Authentication removed successfully!".green().bold());
1140}