Skip to main content

rustbasic_cli/
auth.rs

1use std::fs;
2use colored::*;
3use regex::Regex;
4use crate::scaffolding::update_controller_mod_rs;
5
6pub async fn make_auth() {
7    println!("\n{}", "🔐 Scaffolding Authentication...".magenta().bold());
8
9    // 1. Create src/routes/auth.rs
10    let auth_route_path = "src/routes/auth.rs";
11    let auth_route_template = r#"use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};
12use crate::app::http::controllers::auth;
13use crate::app::http::middleware::auth::guest_middleware;
14use rustbasic_core::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    // 2. Update src/routes/mod.rs
37    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    // 3. Update src/routes/web.rs
47    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 rustbasic_core::axum::{Router, routing::get};", "use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};");
51            content = content.replace("use rustbasic_core::server::AppState;", "use crate::app::http::controllers::{auth, dashboard_controller};\nuse crate::app::http::middleware::auth::auth_middleware;\nuse rustbasic_core::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("/about", get(welcome_controller::about))
61        .route("/dev", get(welcome_controller::dev_info))
62        .merge(auth_routes::router())
63        .merge(auth_protected_routes)"#;
64
65            // Use regex for more robust replacement (includes leading spaces)
66            let re = Regex::new(r#"(?s)Router::new\(\s*\n\s*\.route\("/", get\(welcome_controller::index\)\)\s*\n\s*\.route\("/about", get\(welcome_controller::about\)\)\s*\n\s*\.route\("/dev", get\(welcome_controller::dev_info\)\)"#).unwrap();
67            if re.is_match(&content) {
68                content = re.replace(&content, merge_logic).to_string();
69            } else {
70                // Fallback for simple replacement
71                content = content.replace("Router::new()\n        .route(\"/\", get(welcome_controller::index))\n        .route(\"/about\", get(welcome_controller::about))\n        .route(\"/dev\", get(welcome_controller::dev_info))", merge_logic);
72            }
73            
74            fs::write(web_route_path, content).ok();
75            println!("   {} {}", "📝 Updated:".blue(), web_route_path.cyan());
76        }
77    }
78
79    // 3.1 Create Password Resets Migration
80    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
81    let migration_name = format!("m{}_create_password_resets_table", timestamp);
82    let migration_path = format!("database/migrations/{}.rs", migration_name);
83    
84    // Check if any password reset migration already exists
85    let mut exists = false;
86    if let Ok(entries) = std::fs::read_dir("database/migrations") {
87        for entry in entries.flatten() {
88            if let Some(name) = entry.file_name().to_str() {
89                if name.ends_with("_create_password_resets_table.rs") {
90                    exists = true;
91                    println!("   {} {}", "⚠️  Exists:".yellow(), name.cyan());
92                    break;
93                }
94            }
95        }
96    }
97
98    if !exists {
99        let migration_template = r#"use sea_orm_migration::prelude::*;
100use async_trait::async_trait;
101
102#[derive(Iden)]
103enum PasswordResets {
104    Table,
105    Email,
106    Token,
107    CreatedAt,
108}
109
110#[derive(DeriveMigrationName)]
111pub struct Migration;
112
113#[async_trait]
114impl MigrationTrait for Migration {
115    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
116        manager
117            .create_table(
118                Table::create()
119                    .table(PasswordResets::Table)
120                    .if_not_exists()
121                    .col(ColumnDef::new(PasswordResets::Email).string().not_null().primary_key())
122                    .col(ColumnDef::new(PasswordResets::Token).string().not_null())
123                    .col(
124                        ColumnDef::new(PasswordResets::CreatedAt)
125                            .timestamp()
126                            .default(Expr::current_timestamp())
127                            .not_null(),
128                    )
129                    .to_owned(),
130            )
131            .await
132    }
133
134    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
135        manager
136            .drop_table(Table::drop().table(PasswordResets::Table).to_owned())
137            .await
138    }
139}
140"#.to_string();
141        fs::write(&migration_path, migration_template).ok();
142        
143        crate::scaffolding::update_migration_mod_rs(&migration_name);
144        println!("   {} {}", "✅ Created:".green(), format!("Migration {}", migration_name).cyan());
145    }
146
147    // 4. Create Controller Folder & mod.rs
148    let auth_controller_dir = "src/app/http/controllers/auth";
149    fs::create_dir_all(auth_controller_dir).ok();
150    let auth_controller_mod = "src/app/http/controllers/auth/mod.rs";
151    if !std::path::Path::new(auth_controller_mod).exists() {
152        fs::write(auth_controller_mod, "pub mod auth_controller;").ok();
153    }
154    update_controller_mod_rs("auth");
155
156    // 4.1 Create Auth Middleware
157    let auth_middleware_dir = "src/app/http/middleware";
158    fs::create_dir_all(auth_middleware_dir).ok();
159    let auth_middleware_path = "src/app/http/middleware/auth.rs";
160    if !std::path::Path::new(auth_middleware_path).exists() {
161        let middleware_template = r#"use rustbasic_core::axum::{
162    middleware::Next,
163    response::{IntoResponse, Redirect},
164    extract::Request,
165};
166use rustbasic_core::session_manager::RustBasicSessionStore;
167use rustbasic_core::axum_session::Session;
168
169pub async fn auth_middleware(req: Request, next: Next) -> impl IntoResponse {
170    let session = req.extensions().get::<Session<RustBasicSessionStore>>().unwrap();
171    if session.get::<i32>("user_id").is_none() {
172        session.set("error", "Silakan login terlebih dahulu");
173        return Redirect::to("/login").into_response();
174    }
175    next.run(req).await
176}
177
178pub async fn guest_middleware(req: Request, next: Next) -> impl IntoResponse {
179    let session = req.extensions().get::<Session<RustBasicSessionStore>>().unwrap();
180    if session.get::<i32>("user_id").is_some() {
181        return Redirect::to("/dashboard").into_response();
182    }
183    next.run(req).await
184}
185"#;
186        fs::write(auth_middleware_path, middleware_template).ok();
187        
188        // Update src/app/http/middleware/mod.rs
189        let middleware_mod_path = "src/app/http/middleware/mod.rs";
190        if let Ok(mut content) = fs::read_to_string(middleware_mod_path) {
191            if !content.contains("pub mod auth;") {
192                content.push_str("pub mod auth;\n");
193                fs::write(middleware_mod_path, content).ok();
194            }
195        }
196        println!("   {} {}", "✅ Created:".green(), auth_middleware_path.cyan());
197    }
198
199    // 4.1 Create Password Resets Model
200    let model_path = "src/app/models/password_resets.rs";
201    if !std::path::Path::new(model_path).exists() {
202        let model_template = r#"use rustbasic_core::sea_orm::entity::prelude::*;
203use serde::{Deserialize, Serialize};
204
205#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
206#[sea_orm(table_name = "password_resets")]
207pub struct Model {
208    #[sea_orm(primary_key, auto_increment = false)]
209    pub email: String,
210    pub token: String,
211    pub created_at: DateTime,
212}
213
214#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
215pub enum Relation {}
216
217impl ActiveModelBehavior for ActiveModel {}
218"#;
219        fs::write(model_path, model_template).ok();
220        
221        // Update src/app/models/mod.rs
222        let models_mod_path = "src/app/models/mod.rs";
223        if let Ok(mut content) = fs::read_to_string(models_mod_path) {
224            if !content.contains("pub mod password_resets;") {
225                content.push_str("pub mod password_resets;\n");
226                fs::write(models_mod_path, content).ok();
227            }
228        }
229        println!("   {} {}", "✅ Created:".green(), "Model password_resets".cyan());
230    }
231
232    // 4.2 Create Auth Controller
233    let auth_controller_path = "src/app/http/controllers/auth/auth_controller.rs";
234    if !std::path::Path::new(auth_controller_path).exists() {
235        let controller_template = r#"/* ---------------------------------------------------------
236 * 📑 LABEL: AUTH CONTROLLER (auth/auth_controller.rs)
237 * Menangani pendaftaran, login, dan logout user.
238 * --------------------------------------------------------- */
239
240use crate::app::inertia::inertia;
241use crate::app::models::users;
242use rustbasic_core::requests::Request;
243use rustbasic_core::server::AppState;
244use rustbasic_core::axum::{response::{IntoResponse, Response, Redirect}, extract::State};
245use rustbasic_core::bcrypt::{hash, verify, DEFAULT_COST};
246use rustbasic_core::uuid::Uuid;
247use serde::Deserialize;
248use validator::Validate;
249use rustbasic_core::mail::MailService;
250use rustbasic_core::sea_orm::{EntityTrait, ColumnTrait, QueryFilter, Set};
251use serde_json::json;
252
253#[derive(Deserialize, Validate)]
254pub struct RegisterRequest {
255    #[validate(length(min = 3, message = "Nama minimal 3 karakter"))]
256    pub name: String,
257    
258    #[validate(email(message = "Format email tidak valid"))]
259    pub email: String,
260    
261    #[validate(length(min = 8, message = "Password minimal 8 karakter"))]
262    pub password: String,
263}
264
265#[derive(Deserialize, Validate)]
266pub struct LoginRequest {
267    #[validate(email(message = "Format email tidak valid"))]
268    pub email: String,
269    pub password: String,
270    pub remember: Option<bool>,
271}
272
273#[derive(Deserialize, Validate)]
274pub struct ForgotPasswordRequest {
275    #[validate(email(message = "Format email tidak valid"))]
276    pub email: String,
277}
278
279#[derive(Deserialize, Validate)]
280pub struct ResetPasswordRequest {
281    pub token: String,
282    #[validate(length(min = 8, message = "Password minimal 8 karakter"))]
283    pub password: String,
284}
285
286pub struct AuthController;
287
288impl AuthController {
289    /// Menampilkan halaman login
290    pub async fn login_page(req: Request) -> Response {
291        inertia(req, "Auth/Login", json!({ "title": "Login" }))
292    }
293
294    /// Menampilkan halaman register
295    pub async fn register_page(req: Request) -> Response {
296        inertia(req, "Auth/Register", json!({ "title": "Daftar Akun" }))
297    }
298
299    /// Proses Pendaftaran
300    pub async fn register(State(state): State<AppState>, req: Request) -> impl IntoResponse {
301        // 1. Validasi Input
302        let data = match req.validate::<RegisterRequest>() {
303            Ok(d) => d,
304            Err(_) => return Redirect::to("/register").into_response(),
305        };
306
307        // 2. Cek apakah email sudah terdaftar
308        let existing = users::Entity::find()
309            .filter(users::Column::Email.eq(&data.email))
310            .one(&state.db)
311            .await
312            .ok()
313            .flatten();
314
315        if existing.is_some() {
316            req.session.set("error", "Email sudah terdaftar");
317            return Redirect::to("/register").into_response();
318        }
319
320        // 3. Hash Password
321        let hashed = hash(data.password, DEFAULT_COST).unwrap();
322
323        // 4. Simpan ke Database
324        let new_user = users::ActiveModel {
325            name: Set(data.name),
326            email: Set(data.email),
327            password: Set(hashed),
328            ..Default::default()
329        };
330
331        if let Err(e) = users::Entity::insert(new_user).exec(&state.db).await {
332            rustbasic_core::tracing::error!("Gagal menyimpan user: {}", e);
333            req.session.set("error", "Gagal mendaftar, coba lagi.");
334            return Redirect::to("/register").into_response();
335        }
336
337        req.session.set("success", "Pendaftaran berhasil! Silakan login.");
338        Redirect::to("/login").into_response()
339    }
340
341    /// Proses Login
342    pub async fn login(State(state): State<AppState>, req: Request) -> impl IntoResponse {
343        // 1. Validasi Input
344        let data = match req.validate::<LoginRequest>() {
345            Ok(d) => d,
346            Err(_) => return Redirect::to("/login").into_response(),
347        };
348
349        // 2. Ambil User dari DB
350        let user = users::Entity::find()
351            .filter(users::Column::Email.eq(&data.email))
352            .one(&state.db)
353            .await
354            .ok()
355            .flatten();
356
357        if let Some(u) = user {
358            // 3. Verifikasi Password
359            if verify(data.password, &u.password).unwrap_or(false) {
360                // 4. Set Session
361                req.session.set("user_id", u.id);
362                req.session.set("success", "Selamat datang kembali!");
363                return Redirect::to("/dashboard").into_response();
364            }
365        }
366
367        req.session.set("error", "Email atau password salah");
368        Redirect::to("/login").into_response()
369    }
370
371    /// Menampilkan halaman lupa password
372    pub async fn forgot_password_page(req: Request) -> Response {
373        inertia(req, "Auth/ForgotPassword", json!({ "title": "Lupa Password" }))
374    }
375
376    /// Kirim link reset password
377    pub async fn send_reset_link(State(state): State<AppState>, req: Request) -> impl IntoResponse {
378        let data = match req.validate::<ForgotPasswordRequest>() {
379            Ok(d) => d,
380            Err(_) => return Redirect::to("/forgot-password").into_response(),
381        };
382
383        // 1. Cek apakah user ada
384        let user = users::Entity::find()
385            .filter(users::Column::Email.eq(&data.email))
386            .one(&state.db)
387            .await
388            .ok()
389            .flatten();
390
391        if let Some(u) = user {
392            // 2. Generate Token
393            let token = Uuid::new_v4().to_string();
394
395            // 3. Simpan Token
396            let reset = crate::app::models::password_resets::ActiveModel {
397                email: Set(u.email.clone()),
398                token: Set(token.clone()),
399                created_at: Set(rustbasic_core::chrono::Utc::now().naive_utc()),
400            };
401
402            let _ = crate::app::models::password_resets::Entity::insert(reset)
403                .on_conflict(
404                    rustbasic_core::sea_orm::sea_query::OnConflict::column(crate::app::models::password_resets::Column::Email)
405                        .update_column(crate::app::models::password_resets::Column::Token)
406                        .update_column(crate::app::models::password_resets::Column::CreatedAt)
407                        .to_owned()
408                )
409                .exec(&state.db)
410                .await;
411
412            // 4. Kirim Email (Gunakan Config::load().mail_*)
413            let config = rustbasic_core::Config::load();
414            let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| "RustBasic".to_string());
415            let reset_url = format!("{}/reset-password?token={}", config.app_url, token);
416
417            let subject = format!("Reset Password - {}", app_name);
418            let body = rustbasic_core::view::render_to_string("emails/reset.rb.html", rustbasic_core::minijinja::context! {
419                app_name => app_name,
420                reset_url => reset_url,
421            });
422
423            if let Err(e) = MailService::send_email(&u.email, &subject, &body).await {
424                rustbasic_core::tracing::error!("Gagal mengirim email reset: {}", e);
425            }
426
427            rustbasic_core::tracing::info!("Reset link for {}: {}", u.email, reset_url);
428        }
429
430        req.session.set("success", "Jika email terdaftar, link reset password akan dikirim.");
431        Redirect::to("/login").into_response()
432    }
433
434    /// Menampilkan halaman reset password
435    pub async fn reset_password_page(req: Request) -> Response {
436        let token = req.input_as_str("token").unwrap_or_default();
437        inertia(req, "Auth/ResetPassword", json!({ "title": "Reset Password", "token": token }))
438    }
439
440    /// Proses update password baru
441    pub async fn update_password(State(state): State<AppState>, req: Request) -> impl IntoResponse {
442        let data = match req.validate::<ResetPasswordRequest>() {
443            Ok(d) => d,
444            Err(_) => return Redirect::to("/login").into_response(),
445        };
446
447        // 1. Cari Token
448        let reset = crate::app::models::password_resets::Entity::find()
449            .filter(crate::app::models::password_resets::Column::Token.eq(&data.token))
450            .one(&state.db)
451            .await
452            .ok()
453            .flatten();
454
455        if let Some(r) = reset {
456            // 2. Cek Kadaluarsa (60 Menit)
457            let now = rustbasic_core::chrono::Utc::now().naive_utc();
458            let duration = now.signed_duration_since(r.created_at);
459            
460            if duration.num_minutes() > 60 {
461                // Hapus token yang sudah kadaluarsa
462                let _ = crate::app::models::password_resets::Entity::delete_by_id(r.email.clone())
463                    .exec(&state.db)
464                    .await;
465                    
466                req.session.set("error", "Tautan reset password sudah kadaluarsa (melebihi 60 menit).");
467                return Redirect::to("/login").into_response();
468            }
469
470            // 3. Hash Password Baru
471            let hashed = rustbasic_core::bcrypt::hash(data.password, rustbasic_core::bcrypt::DEFAULT_COST).unwrap();
472
473            // 4. Update User
474            let _ = users::Entity::update_many()
475                .col_expr(users::Column::Password, rustbasic_core::sea_orm::sea_query::Expr::value(hashed))
476                .filter(users::Column::Email.eq(&r.email))
477                .exec(&state.db)
478                .await;
479
480            // 5. Hapus Token
481            let _ = crate::app::models::password_resets::Entity::delete_by_id(r.email)
482                .exec(&state.db)
483                .await;
484
485            req.session.set("success", "Password berhasil diubah. Silakan login.");
486            return Redirect::to("/login").into_response();
487        }
488
489        req.session.set("error", "Token tidak valid atau sudah kadaluarsa.");
490        Redirect::to("/login").into_response()
491    }
492
493    /// Proses Logout
494    pub async fn logout(req: Request) -> impl IntoResponse {
495        req.session.remove("user_id");
496        req.session.set("success", "Anda telah keluar.");
497        Redirect::to("/").into_response()
498    }
499}
500"#;
501        fs::write(auth_controller_path, controller_template).ok();
502        println!("   {} {}", "✅ Created:".green(), auth_controller_path.cyan());
503    }
504
505    // 5. Views (React Components in resources/js/Pages)
506    let auth_page_dir = "src/resources/js/Pages/Auth";
507    fs::create_dir_all(auth_page_dir).ok();
508    
509    let login_template = r##"import React from 'react';
510import { Link, useForm, usePage } from '@inertiajs/react';
511
512export default function Login() {
513  const { flash } = usePage().props;
514  const { data, setData, post, processing, errors } = useForm({
515    email: '',
516    password: '',
517    remember: false,
518  });
519
520  const handleSubmit = (e) => {
521    e.preventDefault();
522    post('/login');
523  };
524
525  return (
526    <div className="min-h-screen bg-gradient-to-tr from-slate-950 via-slate-900 to-indigo-950 flex items-center justify-center p-6 text-slate-100 font-sans">
527      <div className="w-full max-w-md bg-slate-900/60 backdrop-blur-md border border-slate-800/80 rounded-3xl p-8 shadow-2xl relative overflow-hidden glassmorphism">
528        <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
529        <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
530        
531        <div className="text-center mb-8">
532          <span className="text-xs font-bold tracking-widest text-indigo-400 bg-indigo-500/10 px-3 py-1 rounded-full border border-indigo-500/20 uppercase">
533            RustBasic SPA
534          </span>
535          <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Selamat Datang</h1>
536          <p className="text-slate-400 text-sm mt-2">Silakan masuk ke akun Anda</p>
537        </div>
538
539        {flash?.success && (
540          <div className="mb-6 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-semibold text-center animate-pulse">
541            {flash.success}
542          </div>
543        )}
544
545        {flash?.error && (
546          <div className="mb-6 p-4 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-400 text-sm font-semibold text-center">
547            {flash.error}
548          </div>
549        )}
550
551        <form onSubmit={handleSubmit} className="space-y-5">
552          <div>
553            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Email Address</label>
554            <input
555              type="email"
556              value={data.email}
557              onChange={(e) => setData('email', e.target.value)}
558              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
559              placeholder="nama@email.com"
560              required
561            />
562            {errors.email && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.email}</p>}
563          </div>
564
565          <div>
566            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Password</label>
567            <input
568              type="password"
569              value={data.password}
570              onChange={(e) => setData('password', e.target.value)}
571              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
572              placeholder="••••••••"
573              required
574            />
575            {errors.password && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.password}</p>}
576          </div>
577
578          <div className="flex items-center justify-between text-sm">
579            <label className="flex items-center space-x-2 text-slate-400 cursor-pointer">
580              <input
581                type="checkbox"
582                checked={data.remember}
583                onChange={(e) => setData('remember', e.target.checked)}
584                className="w-4 h-4 rounded border-slate-800 bg-slate-950 text-indigo-600 focus:ring-indigo-500 focus:ring-opacity-25"
585              />
586              <span className="select-none">Ingat Saya</span>
587            </label>
588            <Link href="/forgot-password" className="text-indigo-400 hover:text-indigo-300 font-semibold transition-colors duration-200" style={{ textDecoration: 'none' }}>
589              Lupa Password?
590            </Link>
591          </div>
592
593          <button
594            type="submit"
595            disabled={processing}
596            className="w-full py-3.5 px-4 bg-gradient-to-r from-indigo-600 to-indigo-700 hover:from-indigo-500 hover:to-indigo-600 text-white rounded-xl font-bold tracking-wide shadow-[0_0_20px_rgba(99,102,241,0.25)] hover:shadow-[0_0_25px_rgba(99,102,241,0.4)] disabled:opacity-50 transition-all duration-300 transform active:scale-[0.98]"
597          >
598            {processing ? 'MEMROSES...' : 'MASUK KE DASHBOARD'}
599          </button>
600        </form>
601
602        <p className="text-center text-sm text-slate-500 mt-8">
603          Belum punya akun?{' '}
604          <Link href="/register" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
605            Daftar Sekarang
606          </Link>
607        </p>
608      </div>
609    </div>
610  );
611}
612"##;
613
614    let register_template = r##"import React from 'react';
615import { Link, useForm } from '@inertiajs/react';
616
617export default function Register() {
618  const { data, setData, post, processing, errors } = useForm({
619    name: '',
620    email: '',
621    password: '',
622  });
623
624  const handleSubmit = (e) => {
625    e.preventDefault();
626    post('/register');
627  };
628
629  return (
630    <div className="min-h-screen bg-gradient-to-tr from-slate-950 via-slate-900 to-indigo-950 flex items-center justify-center p-6 text-slate-100 font-sans">
631      <div className="w-full max-w-md bg-slate-900/60 backdrop-blur-md border border-slate-800/80 rounded-3xl p-8 shadow-2xl relative overflow-hidden glassmorphism">
632        <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
633        <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
634        
635        <div className="text-center mb-8">
636          <span className="text-xs font-bold tracking-widest text-indigo-400 bg-indigo-500/10 px-3 py-1 rounded-full border border-indigo-500/20 uppercase">
637            RustBasic SPA
638          </span>
639          <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Daftar Akun</h1>
640          <p className="text-slate-400 text-sm mt-2">Mulai perjalanan Anda bersama kami</p>
641        </div>
642
643        <form onSubmit={handleSubmit} className="space-y-5">
644          <div>
645            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Nama Lengkap</label>
646            <input
647              type="text"
648              value={data.name}
649              onChange={(e) => setData('name', e.target.value)}
650              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
651              placeholder="Nama Lengkap Anda"
652              required
653            />
654            {errors.name && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.name}</p>}
655          </div>
656
657          <div>
658            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Email Address</label>
659            <input
660              type="email"
661              value={data.email}
662              onChange={(e) => setData('email', e.target.value)}
663              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
664              placeholder="nama@email.com"
665              required
666            />
667            {errors.email && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.email}</p>}
668          </div>
669
670          <div>
671            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Password</label>
672            <input
673              type="password"
674              value={data.password}
675              onChange={(e) => setData('password', e.target.value)}
676              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
677              placeholder="Min. 8 karakter"
678              required
679            />
680            {errors.password && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.password}</p>}
681          </div>
682
683          <button
684            type="submit"
685            disabled={processing}
686            className="w-full py-3.5 px-4 bg-gradient-to-r from-indigo-600 to-indigo-700 hover:from-indigo-500 hover:to-indigo-600 text-white rounded-xl font-bold tracking-wide shadow-[0_0_20px_rgba(99,102,241,0.25)] hover:shadow-[0_0_25px_rgba(99,102,241,0.4)] disabled:opacity-50 transition-all duration-300 transform active:scale-[0.98]"
687          >
688            {processing ? 'MENDAFTAR...' : 'BUAT AKUN SEKARANG'}
689          </button>
690        </form>
691
692        <p className="text-center text-sm text-slate-500 mt-8">
693          Sudah punya akun?{' '}
694          <Link href="/login" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
695            Login Disini
696          </Link>
697        </p>
698      </div>
699    </div>
700  );
701}
702"##;
703
704    let forgot_template = r##"import React from 'react';
705import { Link, useForm, usePage } from '@inertiajs/react';
706
707export default function ForgotPassword() {
708  const { flash } = usePage().props;
709  const { data, setData, post, processing, errors } = useForm({
710    email: '',
711  });
712
713  const handleSubmit = (e) => {
714    e.preventDefault();
715    post('/forgot-password');
716  };
717
718  return (
719    <div className="min-h-screen bg-gradient-to-tr from-slate-950 via-slate-900 to-indigo-950 flex items-center justify-center p-6 text-slate-100 font-sans">
720      <div className="w-full max-w-md bg-slate-900/60 backdrop-blur-md border border-slate-800/80 rounded-3xl p-8 shadow-2xl relative overflow-hidden glassmorphism">
721        <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
722        <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
723        
724        <div className="text-center mb-8">
725          <span className="text-xs font-bold tracking-widest text-indigo-400 bg-indigo-500/10 px-3 py-1 rounded-full border border-indigo-500/20 uppercase">
726            Keamanan Akun
727          </span>
728          <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Lupa Password</h1>
729          <p className="text-slate-400 text-sm mt-2">Kami akan mengirimkan instruksi ke email Anda</p>
730        </div>
731
732        {flash?.success && (
733          <div className="mb-6 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-semibold text-center animate-pulse">
734            {flash.success}
735          </div>
736        )}
737
738        <form onSubmit={handleSubmit} className="space-y-5">
739          <div>
740            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Email Address</label>
741            <input
742              type="email"
743              value={data.email}
744              onChange={(e) => setData('email', e.target.value)}
745              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
746              placeholder="nama@email.com"
747              required
748              autoFocus
749            />
750            {errors.email && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.email}</p>}
751          </div>
752
753          <button
754            type="submit"
755            disabled={processing}
756            className="w-full py-3.5 px-4 bg-gradient-to-r from-indigo-600 to-indigo-700 hover:from-indigo-500 hover:to-indigo-600 text-white rounded-xl font-bold tracking-wide shadow-[0_0_20px_rgba(99,102,241,0.25)] hover:shadow-[0_0_25px_rgba(99,102,241,0.4)] disabled:opacity-50 transition-all duration-300 transform active:scale-[0.98]"
757          >
758            {processing ? 'MENGIRIM...' : 'KIRIM LINK RESET PASSWORD'}
759          </button>
760        </form>
761
762        <p className="text-center text-sm text-slate-500 mt-8">
763          Ingat password Anda?{' '}
764          <Link href="/login" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
765            Login Disini
766          </Link>
767        </p>
768      </div>
769    </div>
770  );
771}
772"##;
773
774    let login_view = "src/resources/js/Pages/Auth/Login.jsx";
775    if !std::path::Path::new(login_view).exists() {
776        fs::write(login_view, login_template).ok();
777    }
778    
779    let register_view = "src/resources/js/Pages/Auth/Register.jsx";
780    if !std::path::Path::new(register_view).exists() {
781        fs::write(register_view, register_template).ok();
782    }
783
784    let forgot_view = "src/resources/js/Pages/Auth/ForgotPassword.jsx";
785    if !std::path::Path::new(forgot_view).exists() {
786        fs::write(forgot_view, forgot_template).ok();
787    }
788
789    let reset_view = "src/resources/js/Pages/Auth/ResetPassword.jsx";
790    if !std::path::Path::new(reset_view).exists() {
791        let reset_template = r##"import React from 'react';
792import { useForm } from '@inertiajs/react';
793
794export default function ResetPassword({ token }) {
795  const { data, setData, post, processing, errors } = useForm({
796    token: token || '',
797    password: '',
798  });
799
800  const handleSubmit = (e) => {
801    e.preventDefault();
802    post('/reset-password');
803  };
804
805  return (
806    <div className="min-h-screen bg-gradient-to-tr from-slate-950 via-slate-900 to-indigo-950 flex items-center justify-center p-6 text-slate-100 font-sans">
807      <div className="w-full max-w-md bg-slate-900/60 backdrop-blur-md border border-slate-800/80 rounded-3xl p-8 shadow-2xl relative overflow-hidden glassmorphism">
808        <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
809        <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
810        
811        <div className="text-center mb-8">
812          <span className="text-xs font-bold tracking-widest text-indigo-400 bg-indigo-500/10 px-3 py-1 rounded-full border border-indigo-500/20 uppercase">
813            Akses Akun
814          </span>
815          <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Reset Password</h1>
816          <p className="text-slate-400 text-sm mt-2">Silakan masukkan password baru Anda</p>
817        </div>
818
819        <form onSubmit={handleSubmit} className="space-y-5">
820          <input type="hidden" value={data.token} />
821
822          <div>
823            <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Password Baru</label>
824            <input
825              type="password"
826              value={data.password}
827              onChange={(e) => setData('password', e.target.value)}
828              className="w-full bg-slate-950/80 border border-slate-800 rounded-xl p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-indigo-500/50 transition-all duration-300"
829              placeholder="Minimal 8 karakter"
830              required
831              autoFocus
832            />
833            {errors.password && <p className="text-rose-500 text-xs mt-1 font-semibold">{errors.password}</p>}
834          </div>
835
836          <button
837            type="submit"
838            disabled={processing}
839            className="w-full py-3.5 px-4 bg-gradient-to-r from-indigo-600 to-indigo-700 hover:from-indigo-500 hover:to-indigo-600 text-white rounded-xl font-bold tracking-wide shadow-[0_0_20px_rgba(99,102,241,0.25)] hover:shadow-[0_0_25px_rgba(99,102,241,0.4)] disabled:opacity-50 transition-all duration-300 transform active:scale-[0.98]"
840          >
841            {processing ? 'MENYIMPAN...' : 'SIMPAN PASSWORD BARU'}
842          </button>
843        </form>
844      </div>
845    </div>
846  );
847}
848"##;
849        fs::write(reset_view, reset_template).ok();
850    }
851
852    // 5.1 Create Email Reset Template (Minijinja string processing is still perfect here)
853    let email_reset_view = "src/resources/views/emails/reset.rb.html";
854    if !std::path::Path::new(email_reset_view).exists() {
855        fs::create_dir_all("src/resources/views/emails").ok();
856        let email_reset_template = r##"<!DOCTYPE html>
857<html>
858<head>
859    <meta charset="utf-8">
860    <style>
861        body { font-family: 'Inter', -apple-system, sans-serif; line-height: 1.6; color: #1a1a1a; margin: 0; padding: 0; }
862        .container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
863        .card { background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
864        .header { background: linear-gradient(135deg, #6366f1, #a855f7); padding: 40px; text-align: center; color: white; }
865        .content { padding: 40px; }
866        .button { display: inline-block; padding: 14px 32px; background: #6366f1; color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 24px 0; }
867        .footer { padding: 24px; text-align: center; font-size: 13px; color: #6b7280; }
868        h1 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.025em; }
869        p { margin: 16px 0; color: #4b5563; }
870        .divider { height: 1px; background: #f3f4f6; margin: 24px 0; }
871    </style>
872</head>
873<body>
874    <div class="container">
875        <div class="card">
876            <div class="header">
877                <h1>{{ app_name }}</h1>
878            </div>
879            <div class="content">
880                <h2 style="margin: 0; color: #111827; font-size: 20px;">Halo!</h2>
881                <p>Anda menerima email ini karena kami menerima permintaan reset password untuk akun Anda di <strong>{{ app_name }}</strong>.</p>
882                
883                <div style="text-align: center;">
884                    <a href="{{ reset_url }}" class="button">Reset Password Saya</a>
885                </div>
886
887                <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>
888                
889                <div class="divider"></div>
890                
891                <p style="font-size: 12px; color: #9ca3af;">
892                    Jika Anda kesulitan menekan tombol, salin dan tempel URL berikut ke browser Anda:<br>
893                    <span style="word-break: break-all; color: #6366f1;">{{ reset_url }}</span>
894                </p>
895            </div>
896        </div>
897        <div class="footer">
898            &copy; 2026 {{ app_name }}. All rights reserved.
899        </div>
900    </div>
901</body>
902</html>
903"##;
904        fs::write(email_reset_view, email_reset_template).ok();
905    }
906
907    // 5.2 Create Dashboard Page in React
908    let dashboard_view = "src/resources/js/Pages/Dashboard.jsx";
909    if !std::path::Path::new(dashboard_view).exists() {
910        let dashboard_template = r##"import React from 'react';
911import { Link, router, usePage } from '@inertiajs/react';
912
913export default function Dashboard({ title, userName, userEmail, totalUsers }) {
914  const { flash } = usePage().props;
915
916  const handleLogout = (e) => {
917    e.preventDefault();
918    router.post('/logout');
919  };
920
921  return (
922    <div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col md:flex-row font-sans">
923      {/* Sidebar */}
924      <aside className="w-full md:w-80 bg-slate-900 border-b md:border-b-0 md:border-r border-slate-800/80 p-6 flex flex-col justify-between relative overflow-hidden">
925        <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl pointer-events-none" />
926        
927        <div>
928          {/* Logo */}
929          <div className="flex items-center space-x-3 mb-10">
930            <div className="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center font-extrabold text-white text-lg shadow-lg shadow-indigo-600/30">
931              R
932            </div>
933            <span className="text-xl font-extrabold text-white tracking-tight">RustBasic</span>
934          </div>
935
936          {/* User Profile Info Card */}
937          <div className="bg-slate-950/60 border border-slate-800/50 rounded-2xl p-4 mb-8">
938            <div className="flex items-center space-x-3">
939              <div className="w-12 h-12 bg-gradient-to-tr from-indigo-500 to-purple-500 rounded-full flex items-center justify-center font-extrabold text-white text-lg">
940                {userName ? userName[0].toUpperCase() : 'G'}
941              </div>
942              <div className="overflow-hidden">
943                <h4 className="text-sm font-bold text-white truncate">{userName || 'Administrator'}</h4>
944                <p className="text-xs text-slate-500 truncate">{userEmail || 'admin@rustbasic.dev'}</p>
945              </div>
946            </div>
947          </div>
948
949          {/* Navigation links */}
950          <nav className="space-y-2">
951            <Link
952              href="/dashboard"
953              className="flex items-center space-x-3 w-full px-4 py-3 bg-indigo-600 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-600/10 transition-all duration-300"
954              style={{ textDecoration: 'none' }}
955            >
956              <span>📊</span>
957              <span>Dashboard Overview</span>
958            </Link>
959            <Link
960              href="/"
961              className="flex items-center space-x-3 w-full px-4 py-3 text-slate-400 hover:text-white rounded-xl font-semibold text-sm hover:bg-slate-800/30 transition-all duration-300"
962              style={{ textDecoration: 'none' }}
963            >
964              <span>🏠</span>
965              <span>Main Website</span>
966            </Link>
967          </nav>
968        </div>
969
970        {/* Logout Form / Button */}
971        <div className="mt-8 md:mt-0">
972          <form onSubmit={handleLogout}>
973            <button
974              type="submit"
975              className="w-full py-3 px-4 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/20 text-rose-400 rounded-xl font-bold text-sm transition-all duration-300 flex items-center justify-center space-x-2"
976            >
977              <span>🚪</span>
978              <span>KELUAR SISTEM</span>
979            </button>
980          </form>
981        </div>
982      </aside>
983
984      {/* Main Workspace */}
985      <main className="flex-1 p-6 md:p-12 overflow-y-auto">
986        <div className="max-w-6xl mx-auto">
987          {/* Header */}
988          <header className="flex flex-col md:flex-row md:items-center md:justify-between mb-10 gap-4">
989            <div>
990              <h1 className="text-3xl font-extrabold text-white tracking-tight">{title || 'Overview'}</h1>
991              <p className="text-slate-400 text-sm mt-1">Selamat datang kembali, kendalikan project Anda secara instan.</p>
992            </div>
993            <div>
994              <span className="inline-flex items-center px-4 py-2 bg-slate-900 border border-slate-800 rounded-xl text-xs font-bold text-slate-300 shadow-sm">
995                <span className="w-2.5 h-2.5 bg-emerald-500 rounded-full mr-2 animate-ping" />
996                Server Status: <span className="text-emerald-400 ml-1">Running</span>
997              </span>
998            </div>
999          </header>
1000
1001          {/* Flash Notification */}
1002          {flash?.success && (
1003            <div className="mb-8 p-4 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 rounded-2xl text-sm font-semibold text-center animate-fade-in">
1004              ✨ {flash.success}
1005            </div>
1006          )}
1007
1008          {/* Stats Grid */}
1009          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
1010            {/* Stat 1 */}
1011            <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1012              <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1013                User Terdaftar
1014              </span>
1015              <div className="flex items-baseline space-x-2">
1016                <span className="text-5xl font-black text-white tracking-tight">{totalUsers || 0}</span>
1017                <span className="text-emerald-400 text-sm font-bold">↑ 12%</span>
1018              </div>
1019            </div>
1020
1021            {/* Stat 2 */}
1022            <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1023              <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1024                Response Time
1025              </span>
1026              <div className="flex items-baseline space-x-1">
1027                <span className="text-5xl font-black text-indigo-400 tracking-tight">24</span>
1028                <span className="text-slate-400 text-lg font-bold">ms</span>
1029              </div>
1030            </div>
1031
1032            {/* Stat 3 */}
1033            <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1034              <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1035                Database Status
1036              </span>
1037              <div className="flex items-center space-x-3 mt-2">
1038                <div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_12px_#10b981]" />
1039                <span className="text-xl font-extrabold text-emerald-400 tracking-wide uppercase">HEALTHY</span>
1040              </div>
1041            </div>
1042          </div>
1043
1044          {/* Main Info Panel */}
1045          <div className="bg-slate-900/40 border border-slate-800/60 rounded-3xl p-8 glassmorphism">
1046            <div className="flex items-center justify-between mb-6">
1047              <div>
1048                <h3 className="text-lg font-bold text-white">Informasi Kernel Server</h3>
1049                <p className="text-xs text-slate-400 mt-0.5">Detail lingkungan runtime eksekusi Axum Anda.</p>
1050              </div>
1051              <span className="text-[10px] font-bold text-indigo-400 bg-indigo-500/10 border border-indigo-500/20 px-3 py-1 rounded-full uppercase tracking-wider">
1052                v2026.1
1053              </span>
1054            </div>
1055
1056            <div className="bg-slate-950 border border-slate-800/50 rounded-2xl p-6 font-mono text-xs text-emerald-400 leading-relaxed shadow-inner">
1057              <div className="text-slate-600 mb-2">// RustBasic SPA Kernel Logs</div>
1058              <div>[OK] Compiled with Axum 0.8.2</div>
1059              <div>[OK] Database Pool: Sea-ORM Connection Established</div>
1060              <div>[OK] Modern SPA Routing: Powered by Inertia.js Bridge</div>
1061              <div>[OK] Single-Binary Mode: Compile-time embedding enabled</div>
1062              <div>[OK] Workers: 8 logical threads spawned on CPU cores</div>
1063            </div>
1064          </div>
1065        </div>
1066      </main>
1067    </div>
1068  );
1069}
1070"##;
1071        fs::write(dashboard_view, dashboard_template).ok();
1072    }
1073    
1074    // 6. Create Dashboard Controller in Rust
1075    let dashboard_controller_path = "src/app/http/controllers/dashboard_controller.rs";
1076    if !std::path::Path::new(dashboard_controller_path).exists() {
1077        let dashboard_template = r#"use crate::app::inertia::inertia;
1078use crate::app::models::users;
1079use rustbasic_core::requests::Request;
1080use rustbasic_core::server::AppState;
1081use rustbasic_core::axum::{response::Response, extract::State};
1082use rustbasic_core::sea_orm::{EntityTrait, PaginatorTrait};
1083use serde_json::json;
1084
1085pub struct DashboardController;
1086
1087impl DashboardController {
1088    pub async fn index(State(state): State<AppState>, req: Request) -> Response {
1089        let user_id = req.session.get::<i32>("user_id").unwrap_or(0);
1090        let user = users::Entity::find_by_id(user_id).one(&state.db).await.ok().flatten();
1091        let total_users = users::Entity::find().count(&state.db).await.unwrap_or(0);
1092
1093        inertia(req, "Dashboard", json!({
1094            "title": "Dashboard",
1095            "userName": user.as_ref().map(|u| u.name.clone()).unwrap_or("Guest".to_string()),
1096            "userEmail": user.as_ref().map(|u| u.email.clone()).unwrap_or_default(),
1097            "totalUsers": total_users,
1098        }))
1099    }
1100}
1101"#;
1102        fs::write(dashboard_controller_path, dashboard_template).ok();
1103        println!("   {} {}", "✅ Created:".green(), dashboard_controller_path.cyan());
1104    }
1105    update_controller_mod_rs("dashboard_controller");
1106
1107    println!("   {} Folder src/resources/js/Pages/Auth dan Dashboard siap.", "✅ Views:".green());
1108
1109    // 7. Update Welcome.jsx
1110    let welcome_path = "src/resources/js/Pages/Welcome.jsx";
1111    if let Ok(content) = fs::read_to_string(welcome_path) {
1112        if content.contains("Backend Online") && !content.contains("auth_installed ?") {
1113            let target = r#"          <div className="flex items-center gap-4">
1114            <span className="inline-flex items-center gap-1.5 px-3 h-8 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
1115              <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1116              Backend Online
1117            </span>
1118          </div>"#;
1119
1120            let replacement = r#"          <div className="flex items-center gap-4">
1121            <span className="inline-flex items-center gap-1.5 px-3 h-8 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 mr-2">
1122              <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1123              Backend Online
1124            </span>
1125            {auth_installed ? (
1126              <Link 
1127                href="/dashboard" 
1128                className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1129                style={{ textDecoration: 'none' }}
1130              >
1131                Dashboard
1132              </Link>
1133            ) : (
1134              <div className="flex gap-2">
1135                <Link 
1136                  href="/login" 
1137                  className="px-4 py-2 rounded-lg border border-white/10 text-sm font-bold hover:bg-white/5 transition-all duration-300 text-gray-300 hover:text-white"
1138                  style={{ textDecoration: 'none' }}
1139                >
1140                  Masuk
1141                </Link>
1142                <Link 
1143                  href="/register" 
1144                  className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1145                  style={{ textDecoration: 'none' }}
1146                >
1147                  Daftar
1148                </Link>
1149              </div>
1150            )}
1151          </div>"#;
1152
1153            let updated = content.replace(target, replacement);
1154            fs::write(welcome_path, updated).ok();
1155            println!("   {} {}", "📝 Updated:".blue(), welcome_path.cyan());
1156        }
1157    }
1158
1159    println!("\n{}", "✨ Authentication scaffolded successfully!".green().bold());
1160    println!("{}", "Jalankan 'cargo rustbasic route:list' untuk melihat rute baru.".dimmed());
1161}
1162
1163pub async fn remove_auth() {
1164    println!("\n{}", "🗑️  Removing Authentication Scaffold...".red().bold());
1165
1166    // 1. Delete src/routes/auth.rs
1167    let auth_route_path = "src/routes/auth.rs";
1168    if std::path::Path::new(auth_route_path).exists() {
1169        fs::remove_file(auth_route_path).ok();
1170        println!("   {} {}", "✅ Deleted:".green(), auth_route_path.cyan());
1171    }
1172
1173    // 2. Update src/routes/mod.rs
1174    let routes_mod_path = "src/routes/mod.rs";
1175    if let Ok(mut content) = fs::read_to_string(routes_mod_path) {
1176        if content.contains("pub mod auth;") {
1177            content = content.replace("pub mod auth;\n", "");
1178            fs::write(routes_mod_path, content).ok();
1179            println!("   {} {}", "📝 Updated:".blue(), routes_mod_path.cyan());
1180        }
1181    }
1182
1183    // 3. Update src/routes/web.rs
1184    let web_route_path = "src/routes/web.rs";
1185    if let Ok(mut content) = fs::read_to_string(web_route_path) {
1186        let mut changed = false;
1187        
1188        // Remove imports
1189        if content.contains("use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};") {
1190            content = content.replace("use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};", "use rustbasic_core::axum::{Router, routing::get};");
1191            changed = true;
1192        }
1193        
1194        let imports_to_remove = [
1195            "use crate::app::http::controllers::{auth, dashboard_controller};\n",
1196            "use crate::app::http::middleware::auth::auth_middleware;\n",
1197            "use rustbasic_core::server::AppState;\n",
1198            "use crate::routes::auth as auth_routes;\n",
1199            "use crate::app::http::controllers::{auth, dashboard_controller};",
1200            "use crate::app::http::middleware::auth::auth_middleware;",
1201            "use crate::routes::auth as auth_routes;",
1202        ];
1203        
1204        for imp in imports_to_remove {
1205            if content.contains(imp) {
1206                content = content.replace(imp, "");
1207                changed = true;
1208            }
1209        }
1210        
1211        // Re-add server::AppState if it was removed
1212        if !content.contains("use rustbasic_core::server::AppState;") {
1213            content = content.replace("use rustbasic_core::axum::{Router, routing::get};", "use rustbasic_core::axum::{Router, routing::get};\nuse rustbasic_core::server::AppState;");
1214        }
1215
1216        // Remove auth_protected_routes logic and restore basic Router
1217        if content.contains("let auth_protected_routes = Router::new()") {
1218            let re = Regex::new(r##"(?s)\s*let auth_protected_routes = Router::new\(\).*?\.layer\(from_fn\(auth_middleware\)\);\s*"##).unwrap();
1219            content = re.replace(&content, "\n").to_string();
1220            
1221            content = content.replace(".merge(auth_routes::router())", "");
1222            content = content.replace(".merge(auth_protected_routes)", "");
1223            
1224            // Restore clean Router::new()
1225            let clean_router = r#"    Router::new()
1226        .route("/", get(welcome_controller::index))
1227        .route("/about", get(welcome_controller::about))
1228        .route("/dev", get(welcome_controller::dev_info))"#;
1229            
1230            let router_re = Regex::new(r##"(?s)Router::new\(\).*?\.route\(\s*\"/dev\"\s*,\s*get\(welcome_controller::dev_info\)\s*\)"##).unwrap();
1231            content = router_re.replace(&content, clean_router).to_string();
1232            
1233            // Final cleanup of multiple newlines
1234            let multi_newline_re = Regex::new(r#"\n{3,}"#).unwrap();
1235            content = multi_newline_re.replace_all(&content, "\n\n").to_string();
1236            
1237            changed = true;
1238        }
1239
1240        if changed {
1241            fs::write(web_route_path, content).ok();
1242            println!("   {} {}", "📝 Updated:".blue(), web_route_path.cyan());
1243        }
1244    }
1245
1246    // 4. Delete Controllers
1247    let auth_controller_dir = "src/app/http/controllers/auth";
1248    if std::path::Path::new(auth_controller_dir).exists() {
1249        fs::remove_dir_all(auth_controller_dir).ok();
1250        println!("   {} {}", "✅ Deleted:".green(), auth_controller_dir.cyan());
1251    }
1252
1253    // 4.1 Delete Password Resets Migration & Model
1254    if let Ok(entries) = std::fs::read_dir("database/migrations") {
1255        for entry in entries.flatten() {
1256            if let Some(name) = entry.file_name().to_str() {
1257                if name.ends_with("_create_password_resets_table.rs") {
1258                    let path = entry.path();
1259                    fs::remove_file(&path).ok();
1260                    println!("   {} {}", "✅ Deleted:".green(), path.display().to_string().cyan());
1261                }
1262            }
1263        }
1264    }
1265    
1266    let model_path = "src/app/models/password_resets.rs";
1267    if std::path::Path::new(model_path).exists() {
1268        fs::remove_file(model_path).ok();
1269        println!("   {} {}", "✅ Deleted:".green(), model_path.cyan());
1270    }
1271
1272    // 5. Delete React Pages/Auth & Dashboard.jsx
1273    let auth_page_dir = "src/resources/js/Pages/Auth";
1274    if std::path::Path::new(auth_page_dir).exists() {
1275        fs::remove_dir_all(auth_page_dir).ok();
1276        println!("   {} {}", "✅ Deleted:".green(), auth_page_dir.cyan());
1277    }
1278
1279    let dashboard_page = "src/resources/js/Pages/Dashboard.jsx";
1280    if std::path::Path::new(dashboard_page).exists() {
1281        fs::remove_file(dashboard_page).ok();
1282        println!("   {} {}", "✅ Deleted:".green(), dashboard_page.cyan());
1283    }
1284
1285    // 5.1 Delete Auth Middleware
1286    let auth_middleware_path = "src/app/http/middleware/auth.rs";
1287    if std::path::Path::new(auth_middleware_path).exists() {
1288        fs::remove_file(auth_middleware_path).ok();
1289        println!("   {} {}", "✅ Deleted:".green(), auth_middleware_path.cyan());
1290    }
1291
1292    let middleware_mod_path = "src/app/http/middleware/mod.rs";
1293    if let Ok(mut content) = fs::read_to_string(middleware_mod_path) {
1294        if content.contains("pub mod auth;") {
1295            content = content.replace("pub mod auth;\n", "");
1296            fs::write(middleware_mod_path, content).ok();
1297            println!("   {} {}", "📝 Updated:".blue(), middleware_mod_path.cyan());
1298        }
1299    }
1300
1301    // 6. Delete Dashboard Controller
1302    let dashboard_path = "src/app/http/controllers/dashboard_controller.rs";
1303    if std::path::Path::new(dashboard_path).exists() {
1304        fs::remove_file(dashboard_path).ok();
1305        println!("   {} {}", "✅ Deleted:".green(), dashboard_path.cyan());
1306    }
1307
1308    // 7. Update src/app/http/controllers/mod.rs
1309    let controllers_mod_path = "src/app/http/controllers/mod.rs";
1310    if let Ok(mut content) = fs::read_to_string(controllers_mod_path) {
1311        let mut changed = false;
1312        if content.contains("pub mod auth;") {
1313            content = content.replace("pub mod auth;\n", "");
1314            changed = true;
1315        }
1316        if content.contains("pub mod dashboard_controller;") {
1317            content = content.replace("pub mod dashboard_controller;\n", "");
1318            changed = true;
1319        }
1320        if changed {
1321            fs::write(controllers_mod_path, content).ok();
1322            println!("   {} {}", "📝 Updated:".blue(), controllers_mod_path.cyan());
1323        }
1324    }
1325
1326    // 7.1 Update src/app/models/mod.rs
1327    let models_mod_path = "src/app/models/mod.rs";
1328    if let Ok(mut content) = fs::read_to_string(models_mod_path) {
1329        let mut changed = false;
1330        if content.contains("pub mod password_resets;") {
1331            content = content.replace("pub mod password_resets;\n", "");
1332            content = content.replace("pub mod password_resets;", "");
1333            changed = true;
1334        }
1335        if changed {
1336            fs::write(models_mod_path, content).ok();
1337            println!("   {} {}", "📝 Updated:".blue(), models_mod_path.cyan());
1338        }
1339    }
1340
1341    // 7.2 Update database/migrations/mod.rs
1342    let migration_mod_path = "database/migrations/mod.rs";
1343    if let Ok(content) = fs::read_to_string(migration_mod_path) {
1344        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
1345        let mut changed = false;
1346        
1347        // Remove the mod line
1348        lines.retain(|line| {
1349            if line.contains("_create_password_resets_table;") || (line.contains("Box::new(") && line.contains("_create_password_resets_table::Migration")) {
1350                changed = true;
1351                false
1352            } else {
1353                true
1354            }
1355        });
1356
1357        if changed {
1358            fs::write(migration_mod_path, lines.join("\n")).ok();
1359            println!("   {} {}", "📝 Updated:".blue(), migration_mod_path.cyan());
1360        }
1361    }
1362
1363    // 7.3 Restore Welcome.jsx
1364    let welcome_path = "src/resources/js/Pages/Welcome.jsx";
1365    if let Ok(content) = fs::read_to_string(welcome_path) {
1366        if content.contains("auth_installed ?") {
1367            let target = r#"          <div className="flex items-center gap-4">
1368            <span className="inline-flex items-center gap-1.5 px-3 h-8 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 mr-2">
1369              <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1370              Backend Online
1371            </span>
1372            {auth_installed ? (
1373              <Link 
1374                href="/dashboard" 
1375                className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1376                style={{ textDecoration: 'none' }}
1377              >
1378                Dashboard
1379              </Link>
1380            ) : (
1381              <div className="flex gap-2">
1382                <Link 
1383                  href="/login" 
1384                  className="px-4 py-2 rounded-lg border border-white/10 text-sm font-bold hover:bg-white/5 transition-all duration-300 text-gray-300 hover:text-white"
1385                  style={{ textDecoration: 'none' }}
1386                >
1387                  Masuk
1388                </Link>
1389                <Link 
1390                  href="/register" 
1391                  className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1392                  style={{ textDecoration: 'none' }}
1393                >
1394                  Daftar
1395                </Link>
1396              </div>
1397            )}
1398          </div>"#;
1399
1400            let replacement = r#"          <div className="flex items-center gap-4">
1401            <span className="inline-flex items-center gap-1.5 px-3 h-8 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
1402              <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1403              Backend Online
1404            </span>
1405          </div>"#;
1406
1407            let updated = content.replace(target, replacement);
1408            fs::write(welcome_path, updated).ok();
1409            println!("   {} {}", "📝 Restored:".blue(), welcome_path.cyan());
1410        }
1411    }
1412
1413    // 7.4 Delete Migration Record from Database
1414    println!("   {} {}", "⏳".blue(), "Cleaning up migration records from database...".dimmed());
1415    let cfg = rustbasic_core::Config::load();
1416    let db_url = if cfg.db_connection == "mysql" {
1417        format!(
1418            "mysql://{}:{}@{}:{}/{}",
1419            cfg.db_username, cfg.db_password, cfg.db_host, cfg.db_port, cfg.db_database
1420        )
1421    } else {
1422        format!("sqlite:database/{}.sqlite?mode=rwc", cfg.db_database)
1423    };
1424
1425    if let Ok(db) = sea_orm::Database::connect(db_url).await {
1426        use sea_orm::ConnectionTrait;
1427        let table_name = if cfg.db_connection == "mysql" { "sea_orm_migrations" } else { "seaql_migrations" };
1428        let sql = format!("DELETE FROM {} WHERE version LIKE '%_create_password_resets_table'", table_name);
1429        let _ = db.execute(sea_orm::Statement::from_string(cfg.db_backend(), sql)).await;
1430        println!("   {} {}", "✅ Cleaned:".green(), "Database migration records removed.".cyan());
1431    }
1432
1433    println!("\n{}", "✨ Authentication removed successfully!".green().bold());
1434}