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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let auth_page_dir = "src/resources/js/Pages/Auth";
561 fs::create_dir_all(auth_page_dir).ok();
562
563 let components_dir = "src/resources/js/Components";
565 fs::create_dir_all(components_dir).ok();
566
567 let toast_template = r##"import React, { useState, useEffect, useCallback } from 'react';
568
569const ICONS = {
570 success: (
571 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
572 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
573 <path d="M6 10.5L8.5 13L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
574 </svg>
575 ),
576 error: (
577 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
578 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
579 <path d="M7 7L13 13M13 7L7 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
580 </svg>
581 ),
582 warning: (
583 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
584 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
585 <path d="M10 6V11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
586 <circle cx="10" cy="14" r="1" fill="currentColor" />
587 </svg>
588 ),
589 info: (
590 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
591 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
592 <path d="M10 9V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
593 <circle cx="10" cy="6.5" r="1" fill="currentColor" />
594 </svg>
595 ),
596};
597
598const STYLES = {
599 success: {
600 bg: 'rgba(16, 185, 129, 0.08)',
601 border: 'rgba(16, 185, 129, 0.25)',
602 color: '#34d399',
603 progress: '#10b981',
604 shadow: '0 8px 32px rgba(16, 185, 129, 0.15)',
605 },
606 error: {
607 bg: 'rgba(244, 63, 94, 0.08)',
608 border: 'rgba(244, 63, 94, 0.25)',
609 color: '#fb7185',
610 progress: '#f43f5e',
611 shadow: '0 8px 32px rgba(244, 63, 94, 0.15)',
612 },
613 warning: {
614 bg: 'rgba(245, 158, 11, 0.08)',
615 border: 'rgba(245, 158, 11, 0.25)',
616 color: '#fbbf24',
617 progress: '#f59e0b',
618 shadow: '0 8px 32px rgba(245, 158, 11, 0.15)',
619 },
620 info: {
621 bg: 'rgba(99, 102, 241, 0.08)',
622 border: 'rgba(99, 102, 241, 0.25)',
623 color: '#818cf8',
624 progress: '#6366f1',
625 shadow: '0 8px 32px rgba(99, 102, 241, 0.15)',
626 },
627};
628
629function SingleToast({ id, type, message, duration = 5000, onDismiss }) {
630 const [isVisible, setIsVisible] = useState(false);
631 const [isLeaving, setIsLeaving] = useState(false);
632 const [progress, setProgress] = useState(100);
633
634 const style = STYLES[type] || STYLES.info;
635
636 const dismiss = useCallback(() => {
637 setIsLeaving(true);
638 setTimeout(() => onDismiss(id), 350);
639 }, [id, onDismiss]);
640
641 useEffect(() => {
642 const enterTimer = setTimeout(() => setIsVisible(true), 10);
643 const dismissTimer = setTimeout(() => dismiss(), duration);
644 const startTime = Date.now();
645 const progressInterval = setInterval(() => {
646 const elapsed = Date.now() - startTime;
647 const remaining = Math.max(0, 100 - (elapsed / duration) * 100);
648 setProgress(remaining);
649 if (remaining <= 0) clearInterval(progressInterval);
650 }, 30);
651
652 return () => {
653 clearTimeout(enterTimer);
654 clearTimeout(dismissTimer);
655 clearInterval(progressInterval);
656 };
657 }, [duration, dismiss]);
658
659 return (
660 <div
661 style={{
662 background: style.bg,
663 border: `1px solid ${style.border}`,
664 borderRadius: '16px',
665 padding: '14px 18px',
666 marginBottom: '10px',
667 display: 'flex',
668 alignItems: 'flex-start',
669 gap: '12px',
670 color: style.color,
671 backdropFilter: 'blur(20px)',
672 boxShadow: style.shadow,
673 transform: isVisible && !isLeaving
674 ? 'translateX(0) scale(1)'
675 : isLeaving
676 ? 'translateX(120%) scale(0.9)'
677 : 'translateX(120%) scale(0.9)',
678 opacity: isVisible && !isLeaving ? 1 : 0,
679 transition: 'all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
680 position: 'relative',
681 overflow: 'hidden',
682 minWidth: '320px',
683 maxWidth: '420px',
684 cursor: 'pointer',
685 }}
686 onClick={dismiss}
687 role="alert"
688 >
689 <div style={{ flexShrink: 0, marginTop: '1px' }}>{ICONS[type] || ICONS.info}</div>
690 <div style={{ flex: 1, minWidth: 0 }}>
691 <div style={{
692 fontSize: '11px',
693 fontWeight: 700,
694 textTransform: 'uppercase',
695 letterSpacing: '0.08em',
696 opacity: 0.7,
697 marginBottom: '2px',
698 }}>
699 {type === 'success' && 'Berhasil'}
700 {type === 'error' && 'Kesalahan'}
701 {type === 'warning' && 'Peringatan'}
702 {type === 'info' && 'Informasi'}
703 </div>
704 <div style={{
705 fontSize: '13px',
706 fontWeight: 500,
707 color: '#e2e8f0',
708 lineHeight: 1.5,
709 wordBreak: 'break-word',
710 }}>{message}</div>
711 </div>
712 <button
713 onClick={(e) => { e.stopPropagation(); dismiss(); }}
714 style={{
715 background: 'none',
716 border: 'none',
717 color: style.color,
718 cursor: 'pointer',
719 padding: '2px',
720 opacity: 0.5,
721 transition: 'opacity 0.2s',
722 flexShrink: 0,
723 marginTop: '1px',
724 }}
725 >
726 ✕
727 </button>
728 <div style={{
729 position: 'absolute',
730 bottom: 0,
731 left: 0,
732 right: 0,
733 height: '3px',
734 background: `${style.progress}15`,
735 borderRadius: '0 0 16px 16px',
736 overflow: 'hidden',
737 }}>
738 <div style={{
739 width: `${progress}%`,
740 height: '100%',
741 background: `linear-gradient(90deg, ${style.progress}, ${style.progress}aa)`,
742 transition: 'width 0.1s linear',
743 borderRadius: '0 0 16px 16px',
744 }} />
745 </div>
746 </div>
747 );
748}
749
750export default function Toast({ flash, duration = 5000, position = 'top-right' }) {
751 const [toasts, setToasts] = useState([]);
752
753 useEffect(() => {
754 if (!flash) return;
755 const newToasts = [];
756 if (flash.success) newToasts.push({ id: Date.now() + '_s', type: 'success', message: flash.success });
757 if (flash.error) newToasts.push({ id: Date.now() + '_e', type: 'error', message: flash.error });
758 if (flash.warning) newToasts.push({ id: Date.now() + '_w', type: 'warning', message: flash.warning });
759 if (flash.info) newToasts.push({ id: Date.now() + '_i', type: 'info', message: flash.info });
760
761 if (newToasts.length > 0) {
762 setToasts(prev => [...prev, ...newToasts]);
763 }
764 }, [flash?.success, flash?.error, flash?.warning, flash?.info]);
765
766 const handleDismiss = useCallback((id) => {
767 setToasts(prev => prev.filter(t => t.id !== id));
768 }, []);
769
770 const positionStyle = {
771 'top-right': { top: '24px', right: '24px' },
772 'top-left': { top: '24px', left: '24px' },
773 'bottom-right': { bottom: '24px', right: '24px' },
774 'bottom-left': { bottom: '24px', left: '24px' },
775 };
776
777 if (toasts.length === 0) return null;
778
779 return (
780 <div style={{ position: 'fixed', zIndex: 99999, pointerEvents: 'none', ...positionStyle[position] }}>
781 <div style={{ pointerEvents: 'auto' }}>
782 {toasts.map(toast => (
783 <SingleToast
784 key={toast.id}
785 id={toast.id}
786 type={toast.type}
787 message={toast.message}
788 duration={duration}
789 onDismiss={handleDismiss}
790 />
791 ))}
792 </div>
793 </div>
794 );
795}
796"##;
797
798 let alert_banner_template = r##"import React from 'react';
799
800const ALERT_STYLES = {
801 success: {
802 bg: 'rgba(16, 185, 129, 0.08)',
803 border: 'rgba(16, 185, 129, 0.2)',
804 color: '#34d399',
805 icon: '✅',
806 },
807 error: {
808 bg: 'rgba(244, 63, 94, 0.08)',
809 border: 'rgba(244, 63, 94, 0.2)',
810 color: '#fb7185',
811 icon: '❌',
812 },
813 warning: {
814 bg: 'rgba(245, 158, 11, 0.08)',
815 border: 'rgba(245, 158, 11, 0.2)',
816 color: '#fbbf24',
817 icon: '⚠️',
818 },
819 info: {
820 bg: 'rgba(99, 102, 241, 0.08)',
821 border: 'rgba(99, 102, 241, 0.2)',
822 color: '#818cf8',
823 icon: 'ℹ️',
824 },
825};
826
827export default function AlertBanner({ type = 'info', message, onDismiss }) {
828 if (!message) return null;
829 const style = ALERT_STYLES[type] || ALERT_STYLES.info;
830
831 return (
832 <div
833 role="alert"
834 style={{
835 background: style.bg,
836 border: `1px solid ${style.border}`,
837 borderRadius: '14px',
838 padding: '14px 18px',
839 marginBottom: '20px',
840 display: 'flex',
841 alignItems: 'center',
842 gap: '10px',
843 animation: 'alertSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
844 }}
845 >
846 <span style={{ fontSize: '16px', flexShrink: 0 }}>{style.icon}</span>
847 <span style={{ flex: 1, fontSize: '13px', fontWeight: 600, color: style.color, lineHeight: 1.5 }}>{message}</span>
848 {onDismiss && (
849 <button
850 onClick={onDismiss}
851 style={{
852 background: 'none',
853 border: 'none',
854 color: style.color,
855 cursor: 'pointer',
856 padding: '2px 4px',
857 opacity: 0.6,
858 transition: 'opacity 0.2s',
859 fontSize: '14px',
860 }}
861 >
862 ✕
863 </button>
864 )}
865 <style>{`
866 @keyframes alertSlideIn {
867 from { opacity: 0; transform: translateY(-8px) scale(0.97); }
868 to { opacity: 1; transform: translateY(0) scale(1); }
869 }
870 `}</style>
871 </div>
872 );
873}
874"##;
875
876 let form_input_template = r##"import React from 'react';
877
878export default function FormInput({
879 label,
880 type = 'text',
881 value,
882 onChange,
883 error,
884 placeholder,
885 required = false,
886 autoFocus = false,
887 disabled = false,
888}) {
889 const hasError = !!error;
890
891 return (
892 <div>
893 {label && (
894 <label style={{
895 display: 'block',
896 fontSize: '11px',
897 fontWeight: 700,
898 textTransform: 'uppercase',
899 letterSpacing: '0.08em',
900 marginBottom: '8px',
901 color: hasError ? '#fb7185' : '#94a3b8',
902 transition: 'color 0.3s ease',
903 }}>{label}</label>
904 )}
905 <input
906 type={type}
907 value={value}
908 onChange={onChange}
909 placeholder={placeholder}
910 required={required}
911 autoFocus={autoFocus}
912 disabled={disabled}
913 style={{
914 width: '100%',
915 boxSizing: 'border-box',
916 background: 'rgba(2, 6, 23, 0.8)',
917 border: `1px solid ${hasError ? 'rgba(244, 63, 94, 0.5)' : 'rgba(30, 41, 59, 1)'}`,
918 borderRadius: '12px',
919 padding: '12px 14px',
920 fontSize: '14px',
921 color: '#ffffff',
922 outline: 'none',
923 transition: 'all 0.3s ease',
924 opacity: disabled ? 0.5 : 1,
925 }}
926 onFocus={(e) => {
927 e.target.style.borderColor = hasError ? 'rgba(244, 63, 94, 0.7)' : 'rgba(99, 102, 241, 0.5)';
928 e.target.style.boxShadow = hasError ? '0 0 0 3px rgba(244, 63, 94, 0.1)' : '0 0 0 3px rgba(99, 102, 241, 0.1)';
929 }}
930 onBlur={(e) => {
931 e.target.style.borderColor = hasError ? 'rgba(244, 63, 94, 0.5)' : 'rgba(30, 41, 59, 1)';
932 e.target.style.boxShadow = 'none';
933 }}
934 />
935 {hasError && (
936 <div style={{
937 display: 'flex',
938 alignItems: 'center',
939 gap: '5px',
940 marginTop: '6px',
941 animation: 'errorShake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97)',
942 }}>
943 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={{ flexShrink: 0 }}>
944 <circle cx="6" cy="6" r="6" fill="rgba(244, 63, 94, 0.15)" />
945 <path d="M6 3.5V6.5" stroke="#fb7185" strokeWidth="1.2" strokeLinecap="round" />
946 <circle cx="6" cy="8.2" r="0.6" fill="#fb7185" />
947 </svg>
948 <span style={{ fontSize: '12px', fontWeight: 600, color: '#fb7185', lineHeight: 1.3 }}>{error}</span>
949 </div>
950 )}
951 <style>{`
952 @keyframes errorShake {
953 0%, 100% { transform: translateX(0); }
954 20% { transform: translateX(-4px); }
955 40% { transform: translateX(4px); }
956 60% { transform: translateX(-2px); }
957 80% { transform: translateX(2px); }
958 }
959 `}</style>
960 </div>
961 );
962}
963"##;
964
965 let login_template = r##"import React from 'react';
966import { Link, useForm, usePage } from '@inertiajs/react';
967import Toast from '../../Components/Toast';
968import AlertBanner from '../../Components/AlertBanner';
969import FormInput from '../../Components/FormInput';
970
971export default function Login() {
972 const { flash } = usePage().props;
973 const { data, setData, post, processing, errors } = useForm({
974 email: '',
975 password: '',
976 remember: false,
977 });
978
979 const handleSubmit = (e) => {
980 e.preventDefault();
981 post('/login');
982 };
983
984 return (
985 <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">
986 <Toast flash={flash} />
987
988 <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">
989 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
990 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
991
992 <div className="text-center mb-8">
993 <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">
994 RustBasic SPA
995 </span>
996 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Selamat Datang</h1>
997 <p className="text-slate-400 text-sm mt-2">Silakan masuk ke akun Anda</p>
998 </div>
999
1000 {flash?.success && <AlertBanner type="success" message={flash.success} />}
1001 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1002
1003 <form onSubmit={handleSubmit} className="space-y-5">
1004 <FormInput
1005 label="Email Address"
1006 type="email"
1007 value={data.email}
1008 onChange={(e) => setData('email', e.target.value)}
1009 error={errors.email}
1010 placeholder="nama@email.com"
1011 required
1012 />
1013
1014 <FormInput
1015 label="Password"
1016 type="password"
1017 value={data.password}
1018 onChange={(e) => setData('password', e.target.value)}
1019 error={errors.password}
1020 placeholder="••••••••"
1021 required
1022 />
1023
1024 <div className="flex items-center justify-between text-sm">
1025 <label className="flex items-center space-x-2 text-slate-400 cursor-pointer">
1026 <input
1027 type="checkbox"
1028 checked={data.remember}
1029 onChange={(e) => setData('remember', e.target.checked)}
1030 className="w-4 h-4 rounded border-slate-800 bg-slate-950 text-indigo-600 focus:ring-indigo-500 focus:ring-opacity-25"
1031 />
1032 <span className="select-none">Ingat Saya</span>
1033 </label>
1034 <Link href="/forgot-password" className="text-indigo-400 hover:text-indigo-300 font-semibold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1035 Lupa Password?
1036 </Link>
1037 </div>
1038
1039 <button
1040 type="submit"
1041 disabled={processing}
1042 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]"
1043 >
1044 {processing ? 'MEMROSES...' : 'MASUK KE DASHBOARD'}
1045 </button>
1046 </form>
1047
1048 <p className="text-center text-sm text-slate-500 mt-8">
1049 Belum punya akun?{' '}
1050 <Link href="/register" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1051 Daftar Sekarang
1052 </Link>
1053 </p>
1054 </div>
1055 </div>
1056 );
1057}
1058"##;
1059
1060 let register_template = r##"import React from 'react';
1061import { Link, useForm, usePage } from '@inertiajs/react';
1062import Toast from '../../Components/Toast';
1063import AlertBanner from '../../Components/AlertBanner';
1064import FormInput from '../../Components/FormInput';
1065
1066export default function Register() {
1067 const { flash } = usePage().props;
1068 const { data, setData, post, processing, errors } = useForm({
1069 name: '',
1070 email: '',
1071 password: '',
1072 });
1073
1074 const handleSubmit = (e) => {
1075 e.preventDefault();
1076 post('/register');
1077 };
1078
1079 return (
1080 <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">
1081 <Toast flash={flash} />
1082
1083 <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">
1084 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
1085 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
1086
1087 <div className="text-center mb-8">
1088 <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">
1089 RustBasic SPA
1090 </span>
1091 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Daftar Akun</h1>
1092 <p className="text-slate-400 text-sm mt-2">Mulai perjalanan Anda bersama kami</p>
1093 </div>
1094
1095 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1096 {flash?.success && <AlertBanner type="success" message={flash.success} />}
1097
1098 <form onSubmit={handleSubmit} className="space-y-5">
1099 <FormInput
1100 label="Nama Lengkap"
1101 type="text"
1102 value={data.name}
1103 onChange={(e) => setData('name', e.target.value)}
1104 error={errors.name}
1105 placeholder="Nama Lengkap Anda"
1106 required
1107 />
1108
1109 <FormInput
1110 label="Email Address"
1111 type="email"
1112 value={data.email}
1113 onChange={(e) => setData('email', e.target.value)}
1114 error={errors.email}
1115 placeholder="nama@email.com"
1116 required
1117 />
1118
1119 <FormInput
1120 label="Password"
1121 type="password"
1122 value={data.password}
1123 onChange={(e) => setData('password', e.target.value)}
1124 error={errors.password}
1125 placeholder="Min. 8 karakter"
1126 required
1127 />
1128
1129 <button
1130 type="submit"
1131 disabled={processing}
1132 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]"
1133 >
1134 {processing ? 'MENDAFTAR...' : 'BUAT AKUN SEKARANG'}
1135 </button>
1136 </form>
1137
1138 <p className="text-center text-sm text-slate-500 mt-8">
1139 Sudah punya akun?{' '}
1140 <Link href="/login" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1141 Login Disini
1142 </Link>
1143 </p>
1144 </div>
1145 </div>
1146 );
1147}
1148"##;
1149
1150 let forgot_template = r##"import React from 'react';
1151import { Link, useForm, usePage } from '@inertiajs/react';
1152import Toast from '../../Components/Toast';
1153import AlertBanner from '../../Components/AlertBanner';
1154import FormInput from '../../Components/FormInput';
1155
1156export default function ForgotPassword() {
1157 const { flash } = usePage().props;
1158 const { data, setData, post, processing, errors } = useForm({
1159 email: '',
1160 });
1161
1162 const handleSubmit = (e) => {
1163 e.preventDefault();
1164 post('/forgot-password');
1165 };
1166
1167 return (
1168 <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">
1169 <Toast flash={flash} />
1170
1171 <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">
1172 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
1173 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
1174
1175 <div className="text-center mb-8">
1176 <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">
1177 Keamanan Akun
1178 </span>
1179 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Lupa Password</h1>
1180 <p className="text-slate-400 text-sm mt-2">Kami akan mengirimkan instruksi ke email Anda</p>
1181 </div>
1182
1183 {flash?.success && <AlertBanner type="success" message={flash.success} />}
1184 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1185
1186 <form onSubmit={handleSubmit} className="space-y-5">
1187 <FormInput
1188 label="Email Address"
1189 type="email"
1190 value={data.email}
1191 onChange={(e) => setData('email', e.target.value)}
1192 error={errors.email}
1193 placeholder="nama@email.com"
1194 required
1195 autoFocus
1196 />
1197
1198 <button
1199 type="submit"
1200 disabled={processing}
1201 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]"
1202 >
1203 {processing ? 'MENGIRIM...' : 'KIRIM LINK RESET PASSWORD'}
1204 </button>
1205 </form>
1206
1207 <p className="text-center text-sm text-slate-500 mt-8">
1208 Ingat password Anda?{' '}
1209 <Link href="/login" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1210 Login Disini
1211 </Link>
1212 </p>
1213 </div>
1214 </div>
1215 );
1216}
1217"##;
1218
1219 let toast_view = "src/resources/js/Components/Toast.jsx";
1220 if !std::path::Path::new(toast_view).exists() {
1221 fs::write(toast_view, toast_template).ok();
1222 }
1223
1224 let alert_banner_view = "src/resources/js/Components/AlertBanner.jsx";
1225 if !std::path::Path::new(alert_banner_view).exists() {
1226 fs::write(alert_banner_view, alert_banner_template).ok();
1227 }
1228
1229 let form_input_view = "src/resources/js/Components/FormInput.jsx";
1230 if !std::path::Path::new(form_input_view).exists() {
1231 fs::write(form_input_view, form_input_template).ok();
1232 }
1233
1234 let login_view = "src/resources/js/Pages/Auth/Login.jsx";
1235 if !std::path::Path::new(login_view).exists() {
1236 fs::write(login_view, login_template).ok();
1237 }
1238
1239 let register_view = "src/resources/js/Pages/Auth/Register.jsx";
1240 if !std::path::Path::new(register_view).exists() {
1241 fs::write(register_view, register_template).ok();
1242 }
1243
1244 let forgot_view = "src/resources/js/Pages/Auth/ForgotPassword.jsx";
1245 if !std::path::Path::new(forgot_view).exists() {
1246 fs::write(forgot_view, forgot_template).ok();
1247 }
1248
1249 let reset_view = "src/resources/js/Pages/Auth/ResetPassword.jsx";
1250 if !std::path::Path::new(reset_view).exists() {
1251 let reset_template = r##"import React from 'react';
1252import { useForm, usePage } from '@inertiajs/react';
1253import Toast from '../../Components/Toast';
1254import AlertBanner from '../../Components/AlertBanner';
1255import FormInput from '../../Components/FormInput';
1256
1257export default function ResetPassword({ token }) {
1258 const { flash } = usePage().props;
1259 const { data, setData, post, processing, errors } = useForm({
1260 token: token || '',
1261 password: '',
1262 });
1263
1264 const handleSubmit = (e) => {
1265 e.preventDefault();
1266 post('/reset-password');
1267 };
1268
1269 return (
1270 <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">
1271 <Toast flash={flash} />
1272
1273 <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">
1274 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
1275 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
1276
1277 <div className="text-center mb-8">
1278 <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">
1279 Akses Akun
1280 </span>
1281 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Reset Password</h1>
1282 <p className="text-slate-400 text-sm mt-2">Silakan masukkan password baru Anda</p>
1283 </div>
1284
1285 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1286
1287 <form onSubmit={handleSubmit} className="space-y-5">
1288 <input type="hidden" value={data.token} />
1289
1290 <FormInput
1291 label="Password Baru"
1292 type="password"
1293 value={data.password}
1294 onChange={(e) => setData('password', e.target.value)}
1295 error={errors.password}
1296 placeholder="Minimal 8 karakter"
1297 required
1298 autoFocus
1299 />
1300
1301 <button
1302 type="submit"
1303 disabled={processing}
1304 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]"
1305 >
1306 {processing ? 'MENYIMPAN...' : 'SIMPAN PASSWORD BARU'}
1307 </button>
1308 </form>
1309 </div>
1310 </div>
1311 );
1312}
1313"##;
1314 fs::write(reset_view, reset_template).ok();
1315 }
1316
1317 let email_reset_view = "src/resources/views/emails/reset.rb.html";
1319 if !std::path::Path::new(email_reset_view).exists() {
1320 fs::create_dir_all("src/resources/views/emails").ok();
1321 let email_reset_template = r##"<!DOCTYPE html>
1322<html>
1323<head>
1324 <meta charset="utf-8">
1325 <style>
1326 body { font-family: 'Inter', -apple-system, sans-serif; line-height: 1.6; color: #1a1a1a; margin: 0; padding: 0; }
1327 .container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
1328 .card { background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
1329 .header { background: linear-gradient(135deg, #6366f1, #a855f7); padding: 40px; text-align: center; color: white; }
1330 .content { padding: 40px; }
1331 .button { display: inline-block; padding: 14px 32px; background: #6366f1; color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 24px 0; }
1332 .footer { padding: 24px; text-align: center; font-size: 13px; color: #6b7280; }
1333 h1 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.025em; }
1334 p { margin: 16px 0; color: #4b5563; }
1335 .divider { height: 1px; background: #f3f4f6; margin: 24px 0; }
1336 </style>
1337</head>
1338<body>
1339 <div class="container">
1340 <div class="card">
1341 <div class="header">
1342 <h1>{{ app_name }}</h1>
1343 </div>
1344 <div class="content">
1345 <h2 style="margin: 0; color: #111827; font-size: 20px;">Halo!</h2>
1346 <p>Anda menerima email ini karena kami menerima permintaan reset password untuk akun Anda di <strong>{{ app_name }}</strong>.</p>
1347
1348 <div style="text-align: center;">
1349 <a href="{{ reset_url }}" class="button">Reset Password Saya</a>
1350 </div>
1351
1352 <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>
1353
1354 <div class="divider"></div>
1355
1356 <p style="font-size: 12px; color: #9ca3af;">
1357 Jika Anda kesulitan menekan tombol, salin dan tempel URL berikut ke browser Anda:<br>
1358 <span style="word-break: break-all; color: #6366f1;">{{ reset_url }}</span>
1359 </p>
1360 </div>
1361 </div>
1362 <div class="footer">
1363 © 2026 {{ app_name }}. All rights reserved.
1364 </div>
1365 </div>
1366</body>
1367</html>
1368"##;
1369 fs::write(email_reset_view, email_reset_template).ok();
1370 }
1371
1372 let dashboard_view = "src/resources/js/Pages/Dashboard.jsx";
1374 if !std::path::Path::new(dashboard_view).exists() {
1375 let dashboard_template = r##"import React from 'react';
1376import { Link, router, usePage } from '@inertiajs/react';
1377import Toast from '../Components/Toast';
1378
1379export default function Dashboard({ title, userName, userEmail, totalUsers }) {
1380 const { flash } = usePage().props;
1381
1382 const handleLogout = (e) => {
1383 e.preventDefault();
1384 router.post('/logout');
1385 };
1386
1387 return (
1388 <div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col md:flex-row font-sans">
1389 {/* Sidebar */}
1390 <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">
1391 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl pointer-events-none" />
1392
1393 <div>
1394 {/* Logo */}
1395 <div className="flex items-center space-x-3 mb-10">
1396 <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">
1397 R
1398 </div>
1399 <span className="text-xl font-extrabold text-white tracking-tight">RustBasic</span>
1400 </div>
1401
1402 {/* User Profile Info Card */}
1403 <div className="bg-slate-950/60 border border-slate-800/50 rounded-2xl p-4 mb-8">
1404 <div className="flex items-center space-x-3">
1405 <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">
1406 {userName ? userName[0].toUpperCase() : 'G'}
1407 </div>
1408 <div className="overflow-hidden">
1409 <h4 className="text-sm font-bold text-white truncate">{userName || 'Administrator'}</h4>
1410 <p className="text-xs text-slate-500 truncate">{userEmail || 'admin@rustbasic.dev'}</p>
1411 </div>
1412 </div>
1413 </div>
1414
1415 {/* Navigation links */}
1416 <nav className="space-y-2">
1417 <Link
1418 href="/dashboard"
1419 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"
1420 style={{ textDecoration: 'none' }}
1421 >
1422 <span>📊</span>
1423 <span>Dashboard Overview</span>
1424 </Link>
1425 <Link
1426 href="/"
1427 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"
1428 style={{ textDecoration: 'none' }}
1429 >
1430 <span>🏠</span>
1431 <span>Main Website</span>
1432 </Link>
1433 </nav>
1434 </div>
1435
1436 {/* Logout Form / Button */}
1437 <div className="mt-8 md:mt-0">
1438 <form onSubmit={handleLogout}>
1439 <button
1440 type="submit"
1441 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"
1442 >
1443 <span>🚪</span>
1444 <span>KELUAR SISTEM</span>
1445 </button>
1446 </form>
1447 </div>
1448 </aside>
1449
1450 {/* Main Workspace */}
1451 <main className="flex-1 p-6 md:p-12 overflow-y-auto">
1452 <div className="max-w-6xl mx-auto">
1453 {/* Header */}
1454 <header className="flex flex-col md:flex-row md:items-center md:justify-between mb-10 gap-4">
1455 <div>
1456 <h1 className="text-3xl font-extrabold text-white tracking-tight">{title || 'Overview'}</h1>
1457 <p className="text-slate-400 text-sm mt-1">Selamat datang kembali, kendalikan project Anda secara instan.</p>
1458 </div>
1459 <div>
1460 <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">
1461 <span className="w-2.5 h-2.5 bg-emerald-500 rounded-full mr-2 animate-ping" />
1462 Server Status: <span className="text-emerald-400 ml-1">Running</span>
1463 </span>
1464 </div>
1465 </header>
1466
1467 {/* Toast Notifications */}
1468 <Toast flash={flash} />
1469
1470 {/* Stats Grid */}
1471 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
1472 {/* Stat 1 */}
1473 <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1474 <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1475 User Terdaftar
1476 </span>
1477 <div className="flex items-baseline space-x-2">
1478 <span className="text-5xl font-black text-white tracking-tight">{totalUsers || 0}</span>
1479 <span className="text-emerald-400 text-sm font-bold">↑ 12%</span>
1480 </div>
1481 </div>
1482
1483 {/* Stat 2 */}
1484 <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1485 <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1486 Response Time
1487 </span>
1488 <div className="flex items-baseline space-x-1">
1489 <span className="text-5xl font-black text-indigo-400 tracking-tight">24</span>
1490 <span className="text-slate-400 text-lg font-bold">ms</span>
1491 </div>
1492 </div>
1493
1494 {/* Stat 3 */}
1495 <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1496 <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1497 Database Status
1498 </span>
1499 <div className="flex items-center space-x-3 mt-2">
1500 <div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_12px_#10b981]" />
1501 <span className="text-xl font-extrabold text-emerald-400 tracking-wide uppercase">HEALTHY</span>
1502 </div>
1503 </div>
1504 </div>
1505
1506 {/* Main Info Panel */}
1507 <div className="bg-slate-900/40 border border-slate-800/60 rounded-3xl p-8 glassmorphism">
1508 <div className="flex items-center justify-between mb-6">
1509 <div>
1510 <h3 className="text-lg font-bold text-white">Informasi Kernel Server</h3>
1511 <p className="text-xs text-slate-400 mt-0.5">Detail lingkungan runtime eksekusi Axum Anda.</p>
1512 </div>
1513 <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">
1514 v2026.1
1515 </span>
1516 </div>
1517
1518 <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">
1519 <div className="text-slate-600 mb-2">// RustBasic SPA Kernel Logs</div>
1520 <div>[OK] Compiled with Axum 0.8.2</div>
1521 <div>[OK] Database Pool: Sea-ORM Connection Established</div>
1522 <div>[OK] Modern SPA Routing: Powered by Inertia.js Bridge</div>
1523 <div>[OK] Single-Binary Mode: Compile-time embedding enabled</div>
1524 <div>[OK] Workers: 8 logical threads spawned on CPU cores</div>
1525 </div>
1526 </div>
1527 </div>
1528 </main>
1529 </div>
1530 );
1531}
1532"##;
1533 fs::write(dashboard_view, dashboard_template).ok();
1534 }
1535
1536 let dashboard_controller_path = "src/app/http/controllers/dashboard_controller.rs";
1538 if !std::path::Path::new(dashboard_controller_path).exists() {
1539 let dashboard_template = r#"use crate::app::inertia::inertia;
1540use crate::app::models::users;
1541use rustbasic_core::requests::Request;
1542use rustbasic_core::server::AppState;
1543use rustbasic_core::axum::{response::Response, extract::State};
1544use rustbasic_core::sea_orm::{EntityTrait, PaginatorTrait};
1545use serde_json::json;
1546
1547pub struct DashboardController;
1548
1549impl DashboardController {
1550 pub async fn index(State(state): State<AppState>, req: Request) -> Response {
1551 let user_id = req.session.get::<i32>("user_id").unwrap_or(0);
1552 let user = users::Entity::find_by_id(user_id).one(&state.db).await.ok().flatten();
1553 let total_users = users::Entity::find().count(&state.db).await.unwrap_or(0);
1554
1555 inertia(&req, "Dashboard", json!({
1556 "title": "Dashboard",
1557 "userName": user.as_ref().map(|u| u.name.clone()).unwrap_or("Guest".to_string()),
1558 "userEmail": user.as_ref().map(|u| u.email.clone()).unwrap_or_default(),
1559 "totalUsers": total_users,
1560 }))
1561 }
1562}
1563"#;
1564 fs::write(dashboard_controller_path, dashboard_template).ok();
1565 println!(" {} {}", "✅ Created:".green(), dashboard_controller_path.cyan());
1566 }
1567 update_controller_mod_rs("dashboard_controller");
1568
1569 println!(" {} Folder src/resources/js/Pages/Auth dan Dashboard siap.", "✅ Views:".green());
1570
1571 let welcome_path = "src/resources/js/Pages/Welcome.jsx";
1573 if let Ok(content) = fs::read_to_string(welcome_path) {
1574 if content.contains("Backend Online") && !content.contains("auth_installed ?") {
1575 let target = r#" <div className="flex items-center gap-4">
1576 <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">
1577 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1578 Backend Online
1579 </span>
1580 </div>"#;
1581
1582 let replacement = r#" <div className="flex items-center gap-4">
1583 <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">
1584 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1585 Backend Online
1586 </span>
1587 {auth_installed ? (
1588 <Link
1589 href="/dashboard"
1590 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1591 style={{ textDecoration: 'none' }}
1592 >
1593 Dashboard
1594 </Link>
1595 ) : (
1596 <div className="flex gap-2">
1597 <Link
1598 href="/login"
1599 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"
1600 style={{ textDecoration: 'none' }}
1601 >
1602 Masuk
1603 </Link>
1604 <Link
1605 href="/register"
1606 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1607 style={{ textDecoration: 'none' }}
1608 >
1609 Daftar
1610 </Link>
1611 </div>
1612 )}
1613 </div>"#;
1614
1615 let updated = content.replace(target, replacement);
1616 fs::write(welcome_path, updated).ok();
1617 println!(" {} {}", "📝 Updated:".blue(), welcome_path.cyan());
1618 }
1619 }
1620
1621 println!("\n{}", "✨ Authentication scaffolded successfully!".green().bold());
1622 println!("{}", "Jalankan 'cargo rustbasic route:list' untuk melihat rute baru.".dimmed());
1623}
1624
1625pub async fn remove_auth() {
1626 println!("\n{}", "🗑️ Removing Authentication Scaffold...".red().bold());
1627
1628 let auth_route_path = "src/routes/auth.rs";
1630 if std::path::Path::new(auth_route_path).exists() {
1631 fs::remove_file(auth_route_path).ok();
1632 println!(" {} {}", "✅ Deleted:".green(), auth_route_path.cyan());
1633 }
1634
1635 let routes_mod_path = "src/routes/mod.rs";
1637 if let Ok(mut content) = fs::read_to_string(routes_mod_path) {
1638 if content.contains("pub mod auth;") {
1639 content = content.replace("pub mod auth;\n", "");
1640 fs::write(routes_mod_path, content).ok();
1641 println!(" {} {}", "📝 Updated:".blue(), routes_mod_path.cyan());
1642 }
1643 }
1644
1645 let web_route_path = "src/routes/web.rs";
1647 if let Ok(mut content) = fs::read_to_string(web_route_path) {
1648 let mut changed = false;
1649
1650 if content.contains("use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};") {
1652 content = content.replace("use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};", "use rustbasic_core::axum::{Router, routing::get};");
1653 changed = true;
1654 }
1655
1656 let imports_to_remove = [
1657 "use crate::app::http::controllers::{auth, dashboard_controller};\n",
1658 "use crate::app::http::middleware::auth::auth_middleware;\n",
1659 "use rustbasic_core::server::AppState;\n",
1660 "use crate::routes::auth as auth_routes;\n",
1661 "use crate::app::http::controllers::{auth, dashboard_controller};",
1662 "use crate::app::http::middleware::auth::auth_middleware;",
1663 "use crate::routes::auth as auth_routes;",
1664 ];
1665
1666 for imp in imports_to_remove {
1667 if content.contains(imp) {
1668 content = content.replace(imp, "");
1669 changed = true;
1670 }
1671 }
1672
1673 if !content.contains("use rustbasic_core::server::AppState;") {
1675 content = content.replace("use rustbasic_core::axum::{Router, routing::get};", "use rustbasic_core::axum::{Router, routing::get};\nuse rustbasic_core::server::AppState;");
1676 }
1677
1678 if content.contains("let auth_protected_routes = Router::new()") {
1680 let re = Regex::new(r##"(?s)\s*let auth_protected_routes = Router::new\(\).*?\.layer\(from_fn\(auth_middleware\)\);\s*"##).unwrap();
1681 content = re.replace(&content, "\n").to_string();
1682
1683 content = content.replace(".merge(auth_routes::router())", "");
1684 content = content.replace(".merge(auth_protected_routes)", "");
1685
1686 let clean_router = r#" Router::new()
1688 .route("/", get(welcome_controller::index))
1689 .route("/about", get(welcome_controller::about))
1690 .route("/dev", get(welcome_controller::dev_info))"#;
1691
1692 let router_re = Regex::new(r##"(?s)Router::new\(\).*?\.route\(\s*\"/dev\"\s*,\s*get\(welcome_controller::dev_info\)\s*\)"##).unwrap();
1693 content = router_re.replace(&content, clean_router).to_string();
1694
1695 let multi_newline_re = Regex::new(r#"\n{3,}"#).unwrap();
1697 content = multi_newline_re.replace_all(&content, "\n\n").to_string();
1698
1699 changed = true;
1700 }
1701
1702 if changed {
1703 fs::write(web_route_path, content).ok();
1704 println!(" {} {}", "📝 Updated:".blue(), web_route_path.cyan());
1705 }
1706 }
1707
1708 let auth_controller_dir = "src/app/http/controllers/auth";
1710 if std::path::Path::new(auth_controller_dir).exists() {
1711 fs::remove_dir_all(auth_controller_dir).ok();
1712 println!(" {} {}", "✅ Deleted:".green(), auth_controller_dir.cyan());
1713 }
1714
1715 if let Ok(entries) = std::fs::read_dir("database/migrations") {
1717 for entry in entries.flatten() {
1718 if let Some(name) = entry.file_name().to_str() {
1719 if name.ends_with("_create_password_resets_table.rs") {
1720 let path = entry.path();
1721 fs::remove_file(&path).ok();
1722 println!(" {} {}", "✅ Deleted:".green(), path.display().to_string().cyan());
1723 }
1724 }
1725 }
1726 }
1727
1728 let model_path = "src/app/models/password_resets.rs";
1729 if std::path::Path::new(model_path).exists() {
1730 fs::remove_file(model_path).ok();
1731 println!(" {} {}", "✅ Deleted:".green(), model_path.cyan());
1732 }
1733
1734 let components_dir = "src/resources/js/Components";
1736 let toast_view = "src/resources/js/Components/Toast.jsx";
1737 if std::path::Path::new(toast_view).exists() {
1738 fs::remove_file(toast_view).ok();
1739 }
1740 let alert_banner_view = "src/resources/js/Components/AlertBanner.jsx";
1741 if std::path::Path::new(alert_banner_view).exists() {
1742 fs::remove_file(alert_banner_view).ok();
1743 }
1744 let form_input_view = "src/resources/js/Components/FormInput.jsx";
1745 if std::path::Path::new(form_input_view).exists() {
1746 fs::remove_file(form_input_view).ok();
1747 }
1748 if std::path::Path::new(components_dir).exists() {
1749 if let Ok(entries) = std::fs::read_dir(components_dir) {
1750 if entries.count() == 0 {
1751 fs::remove_dir(components_dir).ok();
1752 }
1753 }
1754 }
1755
1756 let auth_page_dir = "src/resources/js/Pages/Auth";
1757 if std::path::Path::new(auth_page_dir).exists() {
1758 fs::remove_dir_all(auth_page_dir).ok();
1759 println!(" {} {}", "✅ Deleted:".green(), auth_page_dir.cyan());
1760 }
1761
1762 let dashboard_page = "src/resources/js/Pages/Dashboard.jsx";
1763 if std::path::Path::new(dashboard_page).exists() {
1764 fs::remove_file(dashboard_page).ok();
1765 println!(" {} {}", "✅ Deleted:".green(), dashboard_page.cyan());
1766 }
1767
1768 let auth_middleware_path = "src/app/http/middleware/auth.rs";
1770 if std::path::Path::new(auth_middleware_path).exists() {
1771 fs::remove_file(auth_middleware_path).ok();
1772 println!(" {} {}", "✅ Deleted:".green(), auth_middleware_path.cyan());
1773 }
1774
1775 let middleware_mod_path = "src/app/http/middleware/mod.rs";
1776 if let Ok(mut content) = fs::read_to_string(middleware_mod_path) {
1777 if content.contains("pub mod auth;") {
1778 content = content.replace("pub mod auth;\n", "");
1779 fs::write(middleware_mod_path, content).ok();
1780 println!(" {} {}", "📝 Updated:".blue(), middleware_mod_path.cyan());
1781 }
1782 }
1783
1784 let dashboard_path = "src/app/http/controllers/dashboard_controller.rs";
1786 if std::path::Path::new(dashboard_path).exists() {
1787 fs::remove_file(dashboard_path).ok();
1788 println!(" {} {}", "✅ Deleted:".green(), dashboard_path.cyan());
1789 }
1790
1791 let controllers_mod_path = "src/app/http/controllers/mod.rs";
1793 if let Ok(mut content) = fs::read_to_string(controllers_mod_path) {
1794 let mut changed = false;
1795 if content.contains("pub mod auth;") {
1796 content = content.replace("pub mod auth;\n", "");
1797 changed = true;
1798 }
1799 if content.contains("pub mod dashboard_controller;") {
1800 content = content.replace("pub mod dashboard_controller;\n", "");
1801 changed = true;
1802 }
1803 if changed {
1804 fs::write(controllers_mod_path, content).ok();
1805 println!(" {} {}", "📝 Updated:".blue(), controllers_mod_path.cyan());
1806 }
1807 }
1808
1809 let models_mod_path = "src/app/models/mod.rs";
1811 if let Ok(mut content) = fs::read_to_string(models_mod_path) {
1812 let mut changed = false;
1813 if content.contains("pub mod password_resets;") {
1814 content = content.replace("pub mod password_resets;\n", "");
1815 content = content.replace("pub mod password_resets;", "");
1816 changed = true;
1817 }
1818 if changed {
1819 fs::write(models_mod_path, content).ok();
1820 println!(" {} {}", "📝 Updated:".blue(), models_mod_path.cyan());
1821 }
1822 }
1823
1824 let migration_mod_path = "database/migrations/mod.rs";
1826 if let Ok(content) = fs::read_to_string(migration_mod_path) {
1827 let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
1828 let mut changed = false;
1829
1830 lines.retain(|line| {
1832 if line.contains("_create_password_resets_table;") || (line.contains("Box::new(") && line.contains("_create_password_resets_table::Migration")) {
1833 changed = true;
1834 false
1835 } else {
1836 true
1837 }
1838 });
1839
1840 if changed {
1841 fs::write(migration_mod_path, lines.join("\n")).ok();
1842 println!(" {} {}", "📝 Updated:".blue(), migration_mod_path.cyan());
1843 }
1844 }
1845
1846 let welcome_path = "src/resources/js/Pages/Welcome.jsx";
1848 if let Ok(content) = fs::read_to_string(welcome_path) {
1849 if content.contains("auth_installed ?") {
1850 let target = r#" <div className="flex items-center gap-4">
1851 <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">
1852 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1853 Backend Online
1854 </span>
1855 {auth_installed ? (
1856 <Link
1857 href="/dashboard"
1858 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1859 style={{ textDecoration: 'none' }}
1860 >
1861 Dashboard
1862 </Link>
1863 ) : (
1864 <div className="flex gap-2">
1865 <Link
1866 href="/login"
1867 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"
1868 style={{ textDecoration: 'none' }}
1869 >
1870 Masuk
1871 </Link>
1872 <Link
1873 href="/register"
1874 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1875 style={{ textDecoration: 'none' }}
1876 >
1877 Daftar
1878 </Link>
1879 </div>
1880 )}
1881 </div>"#;
1882
1883 let replacement = r#" <div className="flex items-center gap-4">
1884 <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">
1885 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1886 Backend Online
1887 </span>
1888 </div>"#;
1889
1890 let updated = content.replace(target, replacement);
1891 fs::write(welcome_path, updated).ok();
1892 println!(" {} {}", "📝 Restored:".blue(), welcome_path.cyan());
1893 }
1894 }
1895
1896 println!(" {} {}", "⏳".blue(), "Cleaning up migration records from database...".dimmed());
1898 let cfg = rustbasic_core::Config::load();
1899 let db_url = if cfg.db_connection == "mysql" {
1900 format!(
1901 "mysql://{}:{}@{}:{}/{}",
1902 cfg.db_username, cfg.db_password, cfg.db_host, cfg.db_port, cfg.db_database
1903 )
1904 } else {
1905 format!("sqlite:database/{}.sqlite?mode=rwc", cfg.db_database)
1906 };
1907
1908 if let Ok(db) = sea_orm::Database::connect(db_url).await {
1909 use sea_orm::ConnectionTrait;
1910 let table_name = if cfg.db_connection == "mysql" { "sea_orm_migrations" } else { "seaql_migrations" };
1911 let sql = format!("DELETE FROM {} WHERE version LIKE '%_create_password_resets_table'", table_name);
1912 let _ = db.execute(sea_orm::Statement::from_string(cfg.db_backend(), sql)).await;
1913 println!(" {} {}", "✅ Cleaned:".green(), "Database migration records removed.".cyan());
1914 }
1915
1916 println!("\n{}", "✨ Authentication removed successfully!".green().bold());
1917}