Skip to main content

rustbasic_breeze/
lib.rs

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