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 && !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 let web_route_path = "src/routes/web.rs";
101 if let Ok(mut content) = fs::read_to_string(web_route_path)
102 && !content.contains("use crate::routes::auth as auth_routes;") {
103 content = content.replace("use rustbasic_core::axum::{Router, routing::get};", "use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};");
104 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;");
105
106 let merge_logic = r#"let auth_protected_routes = Router::new()
107 .route("/dashboard", get(dashboard_controller::DashboardController::index))
108 .route("/logout", post(auth::auth_controller::AuthController::logout))
109 .layer(from_fn(auth_middleware));
110
111 Router::new()
112 .route("/", get(welcome_controller::index))
113 .route("/about", get(welcome_controller::about))
114 .route("/dev", get(welcome_controller::dev_info))
115 .merge(auth_routes::router())
116 .merge(auth_protected_routes)"#;
117
118 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();
120 if re.is_match(&content) {
121 content = re.replace(&content, merge_logic).to_string();
122 } else {
123 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);
125 }
126
127 fs::write(web_route_path, content).ok();
128 println!(" {} {}", "📝 Updated:".blue(), web_route_path.cyan());
129 }
130
131 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
133 let migration_name = format!("m{}_create_password_resets_table", timestamp);
134 let migration_path = format!("database/migrations/{}.rs", migration_name);
135
136 let mut exists = false;
138 if let Ok(entries) = std::fs::read_dir("database/migrations") {
139 for entry in entries.flatten() {
140 if let Some(name) = entry.file_name().to_str()
141 && name.ends_with("_create_password_resets_table.rs") {
142 exists = true;
143 println!(" {} {}", "⚠️ Exists:".yellow(), name.cyan());
144 break;
145 }
146 }
147 }
148
149 if !exists {
150 let migration_template = r#"use sea_orm_migration::prelude::*;
151use async_trait::async_trait;
152
153#[derive(Iden)]
154enum PasswordResets {
155 Table,
156 Email,
157 Token,
158 CreatedAt,
159}
160
161#[derive(DeriveMigrationName)]
162pub struct Migration;
163
164#[async_trait]
165impl MigrationTrait for Migration {
166 async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
167 manager
168 .create_table(
169 Table::create()
170 .table(PasswordResets::Table)
171 .if_not_exists()
172 .col(ColumnDef::new(PasswordResets::Email).string().not_null().primary_key())
173 .col(ColumnDef::new(PasswordResets::Token).string().not_null())
174 .col(
175 ColumnDef::new(PasswordResets::CreatedAt)
176 .timestamp()
177 .default(Expr::current_timestamp())
178 .not_null(),
179 )
180 .to_owned(),
181 )
182 .await
183 }
184
185 async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
186 manager
187 .drop_table(Table::drop().table(PasswordResets::Table).to_owned())
188 .await
189 }
190}
191"#.to_string();
192 fs::write(&migration_path, migration_template).ok();
193
194 update_migration_mod_rs(&migration_name);
195 println!(" {} {}", "✅ Created:".green(), format!("Migration {}", migration_name).cyan());
196 }
197
198 let auth_controller_dir = "src/app/http/controllers/auth";
200 fs::create_dir_all(auth_controller_dir).ok();
201 let auth_controller_mod = "src/app/http/controllers/auth/mod.rs";
202 if !std::path::Path::new(auth_controller_mod).exists() {
203 fs::write(auth_controller_mod, "pub mod auth_controller;").ok();
204 }
205 update_controller_mod_rs("auth");
206
207 let auth_middleware_dir = "src/app/http/middleware";
209 fs::create_dir_all(auth_middleware_dir).ok();
210 let auth_middleware_path = "src/app/http/middleware/auth.rs";
211 if !std::path::Path::new(auth_middleware_path).exists() {
212 let middleware_template = r#"use rustbasic_core::axum::{
213 middleware::Next,
214 response::{IntoResponse, Redirect},
215 extract::Request,
216};
217use rustbasic_core::session_manager::RustBasicSessionStore;
218use rustbasic_core::axum_session::Session;
219
220pub async fn auth_middleware(req: Request, next: Next) -> impl IntoResponse {
221 let session = req.extensions().get::<Session<RustBasicSessionStore>>().unwrap();
222 if session.get::<i32>("user_id").is_none() {
223 session.set("error", "Silakan login terlebih dahulu");
224 return Redirect::to("/login").into_response();
225 }
226 next.run(req).await
227}
228
229pub async fn guest_middleware(req: Request, next: Next) -> impl IntoResponse {
230 let session = req.extensions().get::<Session<RustBasicSessionStore>>().unwrap();
231 if session.get::<i32>("user_id").is_some() {
232 return Redirect::to("/dashboard").into_response();
233 }
234 next.run(req).await
235}
236"#;
237 fs::write(auth_middleware_path, middleware_template).ok();
238
239 let middleware_mod_path = "src/app/http/middleware/mod.rs";
241 if let Ok(mut content) = fs::read_to_string(middleware_mod_path)
242 && !content.contains("pub mod auth;") {
243 content.push_str("pub mod auth;\n");
244 fs::write(middleware_mod_path, content).ok();
245 }
246 println!(" {} {}", "✅ Created:".green(), auth_middleware_path.cyan());
247 }
248
249 let model_path = "src/app/models/password_resets.rs";
251 if !std::path::Path::new(model_path).exists() {
252 let model_template = r#"use rustbasic_core::sea_orm::entity::prelude::*;
253use serde::{Deserialize, Serialize};
254
255#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
256#[sea_orm(table_name = "password_resets")]
257pub struct Model {
258 #[sea_orm(primary_key, auto_increment = false)]
259 pub email: String,
260 pub token: String,
261 pub created_at: DateTime,
262}
263
264#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
265pub enum Relation {}
266
267impl ActiveModelBehavior for ActiveModel {}
268"#;
269 fs::write(model_path, model_template).ok();
270
271 let models_mod_path = "src/app/models/mod.rs";
273 if let Ok(mut content) = fs::read_to_string(models_mod_path)
274 && !content.contains("pub mod password_resets;") {
275 content.push_str("pub mod password_resets;\n");
276 fs::write(models_mod_path, content).ok();
277 }
278 println!(" {} {}", "✅ Created:".green(), "Model password_resets".cyan());
279 }
280
281 let auth_controller_path = "src/app/http/controllers/auth/auth_controller.rs";
283 if !std::path::Path::new(auth_controller_path).exists() {
284 let controller_template = r#"/* ---------------------------------------------------------
285 * 📑 LABEL: AUTH CONTROLLER (auth/auth_controller.rs)
286 * Menangani pendaftaran, login, dan logout user.
287 * --------------------------------------------------------- */
288
289use crate::app::inertia::inertia;
290use crate::app::models::users;
291use rustbasic_core::requests::Request;
292use rustbasic_core::server::AppState;
293use rustbasic_core::axum::{response::{IntoResponse, Response, Redirect}, extract::State};
294use rustbasic_core::bcrypt::{hash, verify, DEFAULT_COST};
295use rustbasic_core::uuid::Uuid;
296use serde::Deserialize;
297use validator::Validate;
298use rustbasic_core::mail::MailService;
299use rustbasic_core::sea_orm::{EntityTrait, ColumnTrait, QueryFilter, Set};
300use serde_json::json;
301
302#[derive(Deserialize, Validate)]
303pub struct RegisterRequest {
304 #[validate(length(min = 3, message = "Nama minimal 3 karakter"))]
305 pub name: String,
306
307 #[validate(email(message = "Format email tidak valid"))]
308 pub email: String,
309
310 #[validate(length(min = 8, message = "Password minimal 8 karakter"))]
311 pub password: String,
312}
313
314#[derive(Deserialize, Validate)]
315pub struct LoginRequest {
316 #[validate(email(message = "Format email tidak valid"))]
317 pub email: String,
318 pub password: String,
319 pub remember: Option<bool>,
320}
321
322#[derive(Deserialize, Validate)]
323pub struct ForgotPasswordRequest {
324 #[validate(email(message = "Format email tidak valid"))]
325 pub email: String,
326}
327
328#[derive(Deserialize, Validate)]
329pub struct ResetPasswordRequest {
330 pub token: String,
331 #[validate(length(min = 8, message = "Password minimal 8 karakter"))]
332 pub password: String,
333}
334
335pub struct AuthController;
336
337impl AuthController {
338 /// Menampilkan halaman login
339 pub async fn login_page(req: Request) -> Response {
340 inertia(&req, "Auth/Login", json!({ "title": "Login" }))
341 }
342
343 /// Menampilkan halaman register
344 pub async fn register_page(req: Request) -> Response {
345 inertia(&req, "Auth/Register", json!({ "title": "Daftar Akun" }))
346 }
347
348 /// Proses Pendaftaran
349 pub async fn register(State(state): State<AppState>, req: Request) -> impl IntoResponse {
350 // 1. Validasi Input
351 let data = match req.validate::<RegisterRequest>() {
352 Ok(d) => d,
353 Err(_) => return Redirect::to("/register").into_response(),
354 };
355
356 // 2. Cek apakah email sudah terdaftar
357 let existing = users::Entity::find()
358 .filter(users::Column::Email.eq(&data.email))
359 .one(&state.db)
360 .await
361 .ok()
362 .flatten();
363
364 if existing.is_some() {
365 req.session.set("error", "Email sudah terdaftar");
366 return Redirect::to("/register").into_response();
367 }
368
369 // 3. Hash Password
370 let hashed = hash(data.password, DEFAULT_COST).unwrap();
371
372 // 4. Simpan ke Database
373 let new_user = users::ActiveModel {
374 name: Set(data.name),
375 email: Set(data.email),
376 password: Set(hashed),
377 ..Default::default()
378 };
379
380 if let Err(e) = users::Entity::insert(new_user).exec(&state.db).await {
381 rustbasic_core::tracing::error!("Gagal menyimpan user: {}", e);
382 req.session.set("error", "Gagal mendaftar, coba lagi.");
383 return Redirect::to("/register").into_response();
384 }
385
386 req.session.set("success", "Pendaftaran berhasil! Silakan login.");
387 Redirect::to("/login").into_response()
388 }
389
390 /// Proses Login
391 pub async fn login(State(state): State<AppState>, req: Request) -> impl IntoResponse {
392 // 1. Validasi Input
393 let data = match req.validate::<LoginRequest>() {
394 Ok(d) => d,
395 Err(_) => return Redirect::to("/login").into_response(),
396 };
397
398 // 2. Ambil User dari DB
399 let user = users::Entity::find()
400 .filter(users::Column::Email.eq(&data.email))
401 .one(&state.db)
402 .await
403 .ok()
404 .flatten();
405
406 if let Some(u) = user {
407 // 3. Verifikasi Password
408 if verify(data.password, &u.password).unwrap_or(false) {
409 // 4. Set Session
410 req.session.set("user_id", u.id);
411 req.session.set("success", "Selamat datang kembali!");
412 return Redirect::to("/dashboard").into_response();
413 }
414 }
415
416 req.session.set("error", "Email atau password salah");
417 Redirect::to("/login").into_response()
418 }
419
420 /// Menampilkan halaman lupa password
421 pub async fn forgot_password_page(req: Request) -> Response {
422 inertia(&req, "Auth/ForgotPassword", json!({ "title": "Lupa Password" }))
423 }
424
425 /// Kirim link reset password
426 pub async fn send_reset_link(State(state): State<AppState>, req: Request) -> impl IntoResponse {
427 let data = match req.validate::<ForgotPasswordRequest>() {
428 Ok(d) => d,
429 Err(_) => return Redirect::to("/forgot-password").into_response(),
430 };
431
432 // 1. Cek apakah user ada
433 let user = users::Entity::find()
434 .filter(users::Column::Email.eq(&data.email))
435 .one(&state.db)
436 .await
437 .ok()
438 .flatten();
439
440 if let Some(u) = user {
441 // 2. Generate Token
442 let token = Uuid::new_v4().to_string();
443
444 // 3. Simpan Token
445 let reset = crate::app::models::password_resets::ActiveModel {
446 email: Set(u.email.clone()),
447 token: Set(token.clone()),
448 created_at: Set(rustbasic_core::chrono::Utc::now().naive_utc()),
449 };
450
451 let _ = crate::app::models::password_resets::Entity::insert(reset)
452 .on_conflict(
453 rustbasic_core::sea_orm::sea_query::OnConflict::column(crate::app::models::password_resets::Column::Email)
454 .update_column(crate::app::models::password_resets::Column::Token)
455 .update_column(crate::app::models::password_resets::Column::CreatedAt)
456 .to_owned()
457 )
458 .exec(&state.db)
459 .await;
460
461 // 4. Kirim Email (Gunakan Config::load().mail_*)
462 let config = rustbasic_core::Config::load();
463 let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| "RustBasic".to_string());
464 let reset_url = format!("{}/reset-password?token={}", config.app_url, token);
465
466 let subject = format!("Reset Password - {}", app_name);
467 let body = rustbasic_core::view::render_to_string("emails/reset.rb.html", rustbasic_core::minijinja::context! {
468 app_name => app_name,
469 reset_url => reset_url,
470 });
471
472 if let Err(e) = MailService::send_email(&u.email, &subject, &body).await {
473 rustbasic_core::tracing::error!("Gagal mengirim email reset: {}", e);
474 }
475
476 rustbasic_core::tracing::info!("Reset link for {}: {}", u.email, reset_url);
477 }
478
479 req.session.set("success", "Jika email terdaftar, link reset password akan dikirim.");
480 Redirect::to("/login").into_response()
481 }
482
483 /// Menampilkan halaman reset password
484 pub async fn reset_password_page(req: Request) -> Response {
485 let token = req.input_as_str("token").unwrap_or_default();
486 inertia(&req, "Auth/ResetPassword", json!({ "title": "Reset Password", "token": token }))
487 }
488
489 /// Proses update password baru
490 pub async fn update_password(State(state): State<AppState>, req: Request) -> impl IntoResponse {
491 let data = match req.validate::<ResetPasswordRequest>() {
492 Ok(d) => d,
493 Err(_) => return Redirect::to("/login").into_response(),
494 };
495
496 // 1. Cari Token
497 let reset = crate::app::models::password_resets::Entity::find()
498 .filter(crate::app::models::password_resets::Column::Token.eq(&data.token))
499 .one(&state.db)
500 .await
501 .ok()
502 .flatten();
503
504 if let Some(r) = reset {
505 // 2. Cek Kadaluarsa (60 Menit)
506 let now = rustbasic_core::chrono::Utc::now().naive_utc();
507 let duration = now.signed_duration_since(r.created_at);
508
509 if duration.num_minutes() > 60 {
510 // Hapus token yang sudah kadaluarsa
511 let _ = crate::app::models::password_resets::Entity::delete_by_id(r.email.clone())
512 .exec(&state.db)
513 .await;
514
515 req.session.set("error", "Tautan reset password sudah kadaluarsa (melebihi 60 menit).");
516 return Redirect::to("/login").into_response();
517 }
518
519 // 3. Hash Password Baru
520 let hashed = rustbasic_core::bcrypt::hash(data.password, rustbasic_core::bcrypt::DEFAULT_COST).unwrap();
521
522 // 4. Update User
523 let _ = users::Entity::update_many()
524 .col_expr(users::Column::Password, rustbasic_core::sea_orm::sea_query::Expr::value(hashed))
525 .filter(users::Column::Email.eq(&r.email))
526 .exec(&state.db)
527 .await;
528
529 // 5. Hapus Token
530 let _ = crate::app::models::password_resets::Entity::delete_by_id(r.email)
531 .exec(&state.db)
532 .await;
533
534 req.session.set("success", "Password berhasil diubah. Silakan login.");
535 return Redirect::to("/login").into_response();
536 }
537
538 req.session.set("error", "Token tidak valid atau sudah kadaluarsa.");
539 Redirect::to("/login").into_response()
540 }
541
542 /// Proses Logout
543 pub async fn logout(req: Request) -> impl IntoResponse {
544 req.session.remove("user_id");
545 req.session.set("success", "Anda telah keluar.");
546 Redirect::to("/").into_response()
547 }
548}
549"#;
550 fs::write(auth_controller_path, controller_template).ok();
551 println!(" {} {}", "✅ Created:".green(), auth_controller_path.cyan());
552 }
553
554 let auth_page_dir = "src/resources/js/Pages/Auth";
556 fs::create_dir_all(auth_page_dir).ok();
557
558 let components_dir = "src/resources/js/Components";
560 fs::create_dir_all(components_dir).ok();
561
562 let toast_template = r##"import React, { useState, useEffect, useCallback } from 'react';
563
564const ICONS = {
565 success: (
566 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
567 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
568 <path d="M6 10.5L8.5 13L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
569 </svg>
570 ),
571 error: (
572 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
573 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
574 <path d="M7 7L13 13M13 7L7 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
575 </svg>
576 ),
577 warning: (
578 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
579 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
580 <path d="M10 6V11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
581 <circle cx="10" cy="14" r="1" fill="currentColor" />
582 </svg>
583 ),
584 info: (
585 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
586 <circle cx="10" cy="10" r="10" fill="currentColor" opacity="0.15" />
587 <path d="M10 9V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
588 <circle cx="10" cy="6.5" r="1" fill="currentColor" />
589 </svg>
590 ),
591};
592
593const STYLES = {
594 success: {
595 bg: 'rgba(16, 185, 129, 0.08)',
596 border: 'rgba(16, 185, 129, 0.25)',
597 color: '#34d399',
598 progress: '#10b981',
599 shadow: '0 8px 32px rgba(16, 185, 129, 0.15)',
600 },
601 error: {
602 bg: 'rgba(244, 63, 94, 0.08)',
603 border: 'rgba(244, 63, 94, 0.25)',
604 color: '#fb7185',
605 progress: '#f43f5e',
606 shadow: '0 8px 32px rgba(244, 63, 94, 0.15)',
607 },
608 warning: {
609 bg: 'rgba(245, 158, 11, 0.08)',
610 border: 'rgba(245, 158, 11, 0.25)',
611 color: '#fbbf24',
612 progress: '#f59e0b',
613 shadow: '0 8px 32px rgba(245, 158, 11, 0.15)',
614 },
615 info: {
616 bg: 'rgba(99, 102, 241, 0.08)',
617 border: 'rgba(99, 102, 241, 0.25)',
618 color: '#818cf8',
619 progress: '#6366f1',
620 shadow: '0 8px 32px rgba(99, 102, 241, 0.15)',
621 },
622};
623
624function SingleToast({ id, type, message, duration = 5000, onDismiss }) {
625 const [isVisible, setIsVisible] = useState(false);
626 const [isLeaving, setIsLeaving] = useState(false);
627 const [progress, setProgress] = useState(100);
628
629 const style = STYLES[type] || STYLES.info;
630
631 const dismiss = useCallback(() => {
632 setIsLeaving(true);
633 setTimeout(() => onDismiss(id), 350);
634 }, [id, onDismiss]);
635
636 useEffect(() => {
637 const enterTimer = setTimeout(() => setIsVisible(true), 10);
638 const dismissTimer = setTimeout(() => dismiss(), duration);
639 const startTime = Date.now();
640 const progressInterval = setInterval(() => {
641 const elapsed = Date.now() - startTime;
642 const remaining = Math.max(0, 100 - (elapsed / duration) * 100);
643 setProgress(remaining);
644 if (remaining <= 0) clearInterval(progressInterval);
645 }, 30);
646
647 return () => {
648 clearTimeout(enterTimer);
649 clearTimeout(dismissTimer);
650 clearInterval(progressInterval);
651 };
652 }, [duration, dismiss]);
653
654 return (
655 <div
656 style={{
657 background: style.bg,
658 border: `1px solid ${style.border}`,
659 borderRadius: '16px',
660 padding: '14px 18px',
661 marginBottom: '10px',
662 display: 'flex',
663 alignItems: 'flex-start',
664 gap: '12px',
665 color: style.color,
666 backdropFilter: 'blur(20px)',
667 boxShadow: style.shadow,
668 transform: isVisible && !isLeaving
669 ? 'translateX(0) scale(1)'
670 : isLeaving
671 ? 'translateX(120%) scale(0.9)'
672 : 'translateX(120%) scale(0.9)',
673 opacity: isVisible && !isLeaving ? 1 : 0,
674 transition: 'all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
675 position: 'relative',
676 overflow: 'hidden',
677 minWidth: '320px',
678 maxWidth: '420px',
679 cursor: 'pointer',
680 }}
681 onClick={dismiss}
682 role="alert"
683 >
684 <div style={{ flexShrink: 0, marginTop: '1px' }}>{ICONS[type] || ICONS.info}</div>
685 <div style={{ flex: 1, minWidth: 0 }}>
686 <div style={{
687 fontSize: '11px',
688 fontWeight: 700,
689 textTransform: 'uppercase',
690 letterSpacing: '0.08em',
691 opacity: 0.7,
692 marginBottom: '2px',
693 }}>
694 {type === 'success' && 'Berhasil'}
695 {type === 'error' && 'Kesalahan'}
696 {type === 'warning' && 'Peringatan'}
697 {type === 'info' && 'Informasi'}
698 </div>
699 <div style={{
700 fontSize: '13px',
701 fontWeight: 500,
702 color: '#e2e8f0',
703 lineHeight: 1.5,
704 wordBreak: 'break-word',
705 }}>{message}</div>
706 </div>
707 <button
708 onClick={(e) => { e.stopPropagation(); dismiss(); }}
709 style={{
710 background: 'none',
711 border: 'none',
712 color: style.color,
713 cursor: 'pointer',
714 padding: '2px',
715 opacity: 0.5,
716 transition: 'opacity 0.2s',
717 flexShrink: 0,
718 marginTop: '1px',
719 }}
720 >
721 ✕
722 </button>
723 <div style={{
724 position: 'absolute',
725 bottom: 0,
726 left: 0,
727 right: 0,
728 height: '3px',
729 background: `${style.progress}15`,
730 borderRadius: '0 0 16px 16px',
731 overflow: 'hidden',
732 }}>
733 <div style={{
734 width: `${progress}%`,
735 height: '100%',
736 background: `linear-gradient(90deg, ${style.progress}, ${style.progress}aa)`,
737 transition: 'width 0.1s linear',
738 borderRadius: '0 0 16px 16px',
739 }} />
740 </div>
741 </div>
742 );
743}
744
745export default function Toast({ flash, duration = 5000, position = 'top-right' }) {
746 const [toasts, setToasts] = useState([]);
747
748 useEffect(() => {
749 if (!flash) return;
750 const newToasts = [];
751 if (flash.success) newToasts.push({ id: Date.now() + '_s', type: 'success', message: flash.success });
752 if (flash.error) newToasts.push({ id: Date.now() + '_e', type: 'error', message: flash.error });
753 if (flash.warning) newToasts.push({ id: Date.now() + '_w', type: 'warning', message: flash.warning });
754 if (flash.info) newToasts.push({ id: Date.now() + '_i', type: 'info', message: flash.info });
755
756 if (newToasts.length > 0) {
757 setToasts(prev => [...prev, ...newToasts]);
758 }
759 }, [flash?.success, flash?.error, flash?.warning, flash?.info]);
760
761 const handleDismiss = useCallback((id) => {
762 setToasts(prev => prev.filter(t => t.id !== id));
763 }, []);
764
765 const positionStyle = {
766 'top-right': { top: '24px', right: '24px' },
767 'top-left': { top: '24px', left: '24px' },
768 'bottom-right': { bottom: '24px', right: '24px' },
769 'bottom-left': { bottom: '24px', left: '24px' },
770 };
771
772 if (toasts.length === 0) return null;
773
774 return (
775 <div style={{ position: 'fixed', zIndex: 99999, pointerEvents: 'none', ...positionStyle[position] }}>
776 <div style={{ pointerEvents: 'auto' }}>
777 {toasts.map(toast => (
778 <SingleToast
779 key={toast.id}
780 id={toast.id}
781 type={toast.type}
782 message={toast.message}
783 duration={duration}
784 onDismiss={handleDismiss}
785 />
786 ))}
787 </div>
788 </div>
789 );
790}
791"##;
792
793 let alert_banner_template = r##"import React from 'react';
794
795const ALERT_STYLES = {
796 success: {
797 bg: 'rgba(16, 185, 129, 0.08)',
798 border: 'rgba(16, 185, 129, 0.2)',
799 color: '#34d399',
800 icon: '✅',
801 },
802 error: {
803 bg: 'rgba(244, 63, 94, 0.08)',
804 border: 'rgba(244, 63, 94, 0.2)',
805 color: '#fb7185',
806 icon: '❌',
807 },
808 warning: {
809 bg: 'rgba(245, 158, 11, 0.08)',
810 border: 'rgba(245, 158, 11, 0.2)',
811 color: '#fbbf24',
812 icon: '⚠️',
813 },
814 info: {
815 bg: 'rgba(99, 102, 241, 0.08)',
816 border: 'rgba(99, 102, 241, 0.2)',
817 color: '#818cf8',
818 icon: 'ℹ️',
819 },
820};
821
822export default function AlertBanner({ type = 'info', message, onDismiss }) {
823 if (!message) return null;
824 const style = ALERT_STYLES[type] || ALERT_STYLES.info;
825
826 return (
827 <div
828 role="alert"
829 style={{
830 background: style.bg,
831 border: `1px solid ${style.border}`,
832 borderRadius: '14px',
833 padding: '14px 18px',
834 marginBottom: '20px',
835 display: 'flex',
836 alignItems: 'center',
837 gap: '10px',
838 animation: 'alertSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
839 }}
840 >
841 <span style={{ fontSize: '16px', flexShrink: 0 }}>{style.icon}</span>
842 <span style={{ flex: 1, fontSize: '13px', fontWeight: 600, color: style.color, lineHeight: 1.5 }}>{message}</span>
843 {onDismiss && (
844 <button
845 onClick={onDismiss}
846 style={{
847 background: 'none',
848 border: 'none',
849 color: style.color,
850 cursor: 'pointer',
851 padding: '2px 4px',
852 opacity: 0.6,
853 transition: 'opacity 0.2s',
854 fontSize: '14px',
855 }}
856 >
857 ✕
858 </button>
859 )}
860 <style>{`
861 @keyframes alertSlideIn {
862 from { opacity: 0; transform: translateY(-8px) scale(0.97); }
863 to { opacity: 1; transform: translateY(0) scale(1); }
864 }
865 `}</style>
866 </div>
867 );
868}
869"##;
870
871 let form_input_template = r##"import React from 'react';
872
873export default function FormInput({
874 label,
875 type = 'text',
876 value,
877 onChange,
878 error,
879 placeholder,
880 required = false,
881 autoFocus = false,
882 disabled = false,
883}) {
884 const hasError = !!error;
885
886 return (
887 <div>
888 {label && (
889 <label style={{
890 display: 'block',
891 fontSize: '11px',
892 fontWeight: 700,
893 textTransform: 'uppercase',
894 letterSpacing: '0.08em',
895 marginBottom: '8px',
896 color: hasError ? '#fb7185' : '#94a3b8',
897 transition: 'color 0.3s ease',
898 }}>{label}</label>
899 )}
900 <input
901 type={type}
902 value={value}
903 onChange={onChange}
904 placeholder={placeholder}
905 required={required}
906 autoFocus={autoFocus}
907 disabled={disabled}
908 style={{
909 width: '100%',
910 boxSizing: 'border-box',
911 background: 'rgba(2, 6, 23, 0.8)',
912 border: `1px solid ${hasError ? 'rgba(244, 63, 94, 0.5)' : 'rgba(30, 41, 59, 1)'}`,
913 borderRadius: '12px',
914 padding: '12px 14px',
915 fontSize: '14px',
916 color: '#ffffff',
917 outline: 'none',
918 transition: 'all 0.3s ease',
919 opacity: disabled ? 0.5 : 1,
920 }}
921 onFocus={(e) => {
922 e.target.style.borderColor = hasError ? 'rgba(244, 63, 94, 0.7)' : 'rgba(99, 102, 241, 0.5)';
923 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)';
924 }}
925 onBlur={(e) => {
926 e.target.style.borderColor = hasError ? 'rgba(244, 63, 94, 0.5)' : 'rgba(30, 41, 59, 1)';
927 e.target.style.boxShadow = 'none';
928 }}
929 />
930 {hasError && (
931 <div style={{
932 display: 'flex',
933 alignItems: 'center',
934 gap: '5px',
935 marginTop: '6px',
936 animation: 'errorShake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97)',
937 }}>
938 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={{ flexShrink: 0 }}>
939 <circle cx="6" cy="6" r="6" fill="rgba(244, 63, 94, 0.15)" />
940 <path d="M6 3.5V6.5" stroke="#fb7185" strokeWidth="1.2" strokeLinecap="round" />
941 <circle cx="6" cy="8.2" r="0.6" fill="#fb7185" />
942 </svg>
943 <span style={{ fontSize: '12px', fontWeight: 600, color: '#fb7185', lineHeight: 1.3 }}>{error}</span>
944 </div>
945 )}
946 <style>{`
947 @keyframes errorShake {
948 0%, 100% { transform: translateX(0); }
949 20% { transform: translateX(-4px); }
950 40% { transform: translateX(4px); }
951 60% { transform: translateX(-2px); }
952 80% { transform: translateX(2px); }
953 }
954 `}</style>
955 </div>
956 );
957}
958"##;
959
960 let login_template = r##"import React from 'react';
961import { Link, useForm, usePage } from '@inertiajs/react';
962import Toast from '../../Components/Toast';
963import AlertBanner from '../../Components/AlertBanner';
964import FormInput from '../../Components/FormInput';
965
966export default function Login() {
967 const { flash } = usePage().props;
968 const { data, setData, post, processing, errors } = useForm({
969 email: '',
970 password: '',
971 remember: false,
972 });
973
974 const handleSubmit = (e) => {
975 e.preventDefault();
976 post('/login');
977 };
978
979 return (
980 <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">
981 <Toast flash={flash} />
982
983 <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">
984 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
985 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
986
987 <div className="text-center mb-8">
988 <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">
989 RustBasic SPA
990 </span>
991 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Selamat Datang</h1>
992 <p className="text-slate-400 text-sm mt-2">Silakan masuk ke akun Anda</p>
993 </div>
994
995 {flash?.success && <AlertBanner type="success" message={flash.success} />}
996 {flash?.error && <AlertBanner type="error" message={flash.error} />}
997
998 <form onSubmit={handleSubmit} className="space-y-5">
999 <FormInput
1000 label="Email Address"
1001 type="email"
1002 value={data.email}
1003 onChange={(e) => setData('email', e.target.value)}
1004 error={errors.email}
1005 placeholder="nama@email.com"
1006 required
1007 />
1008
1009 <FormInput
1010 label="Password"
1011 type="password"
1012 value={data.password}
1013 onChange={(e) => setData('password', e.target.value)}
1014 error={errors.password}
1015 placeholder="••••••••"
1016 required
1017 />
1018
1019 <div className="flex items-center justify-between text-sm">
1020 <label className="flex items-center space-x-2 text-slate-400 cursor-pointer">
1021 <input
1022 type="checkbox"
1023 checked={data.remember}
1024 onChange={(e) => setData('remember', e.target.checked)}
1025 className="w-4 h-4 rounded border-slate-800 bg-slate-950 text-indigo-600 focus:ring-indigo-500 focus:ring-opacity-25"
1026 />
1027 <span className="select-none">Ingat Saya</span>
1028 </label>
1029 <Link href="/forgot-password" className="text-indigo-400 hover:text-indigo-300 font-semibold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1030 Lupa Password?
1031 </Link>
1032 </div>
1033
1034 <button
1035 type="submit"
1036 disabled={processing}
1037 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]"
1038 >
1039 {processing ? 'MEMROSES...' : 'MASUK KE DASHBOARD'}
1040 </button>
1041 </form>
1042
1043 <p className="text-center text-sm text-slate-500 mt-8">
1044 Belum punya akun?{' '}
1045 <Link href="/register" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1046 Daftar Sekarang
1047 </Link>
1048 </p>
1049 </div>
1050 </div>
1051 );
1052}
1053"##;
1054
1055 let register_template = r##"import React from 'react';
1056import { Link, useForm, usePage } from '@inertiajs/react';
1057import Toast from '../../Components/Toast';
1058import AlertBanner from '../../Components/AlertBanner';
1059import FormInput from '../../Components/FormInput';
1060
1061export default function Register() {
1062 const { flash } = usePage().props;
1063 const { data, setData, post, processing, errors } = useForm({
1064 name: '',
1065 email: '',
1066 password: '',
1067 });
1068
1069 const handleSubmit = (e) => {
1070 e.preventDefault();
1071 post('/register');
1072 };
1073
1074 return (
1075 <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">
1076 <Toast flash={flash} />
1077
1078 <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">
1079 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
1080 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
1081
1082 <div className="text-center mb-8">
1083 <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">
1084 RustBasic SPA
1085 </span>
1086 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Daftar Akun</h1>
1087 <p className="text-slate-400 text-sm mt-2">Mulai perjalanan Anda bersama kami</p>
1088 </div>
1089
1090 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1091 {flash?.success && <AlertBanner type="success" message={flash.success} />}
1092
1093 <form onSubmit={handleSubmit} className="space-y-5">
1094 <FormInput
1095 label="Nama Lengkap"
1096 type="text"
1097 value={data.name}
1098 onChange={(e) => setData('name', e.target.value)}
1099 error={errors.name}
1100 placeholder="Nama Lengkap Anda"
1101 required
1102 />
1103
1104 <FormInput
1105 label="Email Address"
1106 type="email"
1107 value={data.email}
1108 onChange={(e) => setData('email', e.target.value)}
1109 error={errors.email}
1110 placeholder="nama@email.com"
1111 required
1112 />
1113
1114 <FormInput
1115 label="Password"
1116 type="password"
1117 value={data.password}
1118 onChange={(e) => setData('password', e.target.value)}
1119 error={errors.password}
1120 placeholder="Min. 8 karakter"
1121 required
1122 />
1123
1124 <button
1125 type="submit"
1126 disabled={processing}
1127 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]"
1128 >
1129 {processing ? 'MENDAFTAR...' : 'BUAT AKUN SEKARANG'}
1130 </button>
1131 </form>
1132
1133 <p className="text-center text-sm text-slate-500 mt-8">
1134 Sudah punya akun?{' '}
1135 <Link href="/login" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1136 Login Disini
1137 </Link>
1138 </p>
1139 </div>
1140 </div>
1141 );
1142}
1143"##;
1144
1145 let forgot_template = r##"import React from 'react';
1146import { Link, useForm, usePage } from '@inertiajs/react';
1147import Toast from '../../Components/Toast';
1148import AlertBanner from '../../Components/AlertBanner';
1149import FormInput from '../../Components/FormInput';
1150
1151export default function ForgotPassword() {
1152 const { flash } = usePage().props;
1153 const { data, setData, post, processing, errors } = useForm({
1154 email: '',
1155 });
1156
1157 const handleSubmit = (e) => {
1158 e.preventDefault();
1159 post('/forgot-password');
1160 };
1161
1162 return (
1163 <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">
1164 <Toast flash={flash} />
1165
1166 <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">
1167 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
1168 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
1169
1170 <div className="text-center mb-8">
1171 <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">
1172 Keamanan Akun
1173 </span>
1174 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Lupa Password</h1>
1175 <p className="text-slate-400 text-sm mt-2">Kami akan mengirimkan instruksi ke email Anda</p>
1176 </div>
1177
1178 {flash?.success && <AlertBanner type="success" message={flash.success} />}
1179 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1180
1181 <form onSubmit={handleSubmit} className="space-y-5">
1182 <FormInput
1183 label="Email Address"
1184 type="email"
1185 value={data.email}
1186 onChange={(e) => setData('email', e.target.value)}
1187 error={errors.email}
1188 placeholder="nama@email.com"
1189 required
1190 autoFocus
1191 />
1192
1193 <button
1194 type="submit"
1195 disabled={processing}
1196 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]"
1197 >
1198 {processing ? 'MENGIRIM...' : 'KIRIM LINK RESET PASSWORD'}
1199 </button>
1200 </form>
1201
1202 <p className="text-center text-sm text-slate-500 mt-8">
1203 Ingat password Anda?{' '}
1204 <Link href="/login" className="text-indigo-400 hover:underline font-bold transition-colors duration-200" style={{ textDecoration: 'none' }}>
1205 Login Disini
1206 </Link>
1207 </p>
1208 </div>
1209 </div>
1210 );
1211}
1212"##;
1213
1214 let toast_view = "src/resources/js/Components/Toast.jsx";
1215 if !std::path::Path::new(toast_view).exists() {
1216 fs::write(toast_view, toast_template).ok();
1217 }
1218
1219 let alert_banner_view = "src/resources/js/Components/AlertBanner.jsx";
1220 if !std::path::Path::new(alert_banner_view).exists() {
1221 fs::write(alert_banner_view, alert_banner_template).ok();
1222 }
1223
1224 let form_input_view = "src/resources/js/Components/FormInput.jsx";
1225 if !std::path::Path::new(form_input_view).exists() {
1226 fs::write(form_input_view, form_input_template).ok();
1227 }
1228
1229 let login_view = "src/resources/js/Pages/Auth/Login.jsx";
1230 if !std::path::Path::new(login_view).exists() {
1231 fs::write(login_view, login_template).ok();
1232 }
1233
1234 let register_view = "src/resources/js/Pages/Auth/Register.jsx";
1235 if !std::path::Path::new(register_view).exists() {
1236 fs::write(register_view, register_template).ok();
1237 }
1238
1239 let forgot_view = "src/resources/js/Pages/Auth/ForgotPassword.jsx";
1240 if !std::path::Path::new(forgot_view).exists() {
1241 fs::write(forgot_view, forgot_template).ok();
1242 }
1243
1244 let reset_view = "src/resources/js/Pages/Auth/ResetPassword.jsx";
1245 if !std::path::Path::new(reset_view).exists() {
1246 let reset_template = r##"import React from 'react';
1247import { useForm, usePage } from '@inertiajs/react';
1248import Toast from '../../Components/Toast';
1249import AlertBanner from '../../Components/AlertBanner';
1250import FormInput from '../../Components/FormInput';
1251
1252export default function ResetPassword({ token }) {
1253 const { flash } = usePage().props;
1254 const { data, setData, post, processing, errors } = useForm({
1255 token: token || '',
1256 password: '',
1257 });
1258
1259 const handleSubmit = (e) => {
1260 e.preventDefault();
1261 post('/reset-password');
1262 };
1263
1264 return (
1265 <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">
1266 <Toast flash={flash} />
1267
1268 <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">
1269 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
1270 <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
1271
1272 <div className="text-center mb-8">
1273 <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">
1274 Akses Akun
1275 </span>
1276 <h1 className="text-3xl font-extrabold text-white mt-4 tracking-tight">Reset Password</h1>
1277 <p className="text-slate-400 text-sm mt-2">Silakan masukkan password baru Anda</p>
1278 </div>
1279
1280 {flash?.error && <AlertBanner type="error" message={flash.error} />}
1281
1282 <form onSubmit={handleSubmit} className="space-y-5">
1283 <input type="hidden" value={data.token} />
1284
1285 <FormInput
1286 label="Password Baru"
1287 type="password"
1288 value={data.password}
1289 onChange={(e) => setData('password', e.target.value)}
1290 error={errors.password}
1291 placeholder="Minimal 8 karakter"
1292 required
1293 autoFocus
1294 />
1295
1296 <button
1297 type="submit"
1298 disabled={processing}
1299 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]"
1300 >
1301 {processing ? 'MENYIMPAN...' : 'SIMPAN PASSWORD BARU'}
1302 </button>
1303 </form>
1304 </div>
1305 </div>
1306 );
1307}
1308"##;
1309 fs::write(reset_view, reset_template).ok();
1310 }
1311
1312 let email_reset_view = "src/resources/views/emails/reset.rb.html";
1314 if !std::path::Path::new(email_reset_view).exists() {
1315 fs::create_dir_all("src/resources/views/emails").ok();
1316 let email_reset_template = r##"<!DOCTYPE html>
1317<html>
1318<head>
1319 <meta charset="utf-8">
1320 <style>
1321 body { font-family: 'Inter', -apple-system, sans-serif; line-height: 1.6; color: #1a1a1a; margin: 0; padding: 0; }
1322 .container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
1323 .card { background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
1324 .header { background: linear-gradient(135deg, #6366f1, #a855f7); padding: 40px; text-align: center; color: white; }
1325 .content { padding: 40px; }
1326 .button { display: inline-block; padding: 14px 32px; background: #6366f1; color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 24px 0; }
1327 .footer { padding: 24px; text-align: center; font-size: 13px; color: #6b7280; }
1328 h1 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.025em; }
1329 p { margin: 16px 0; color: #4b5563; }
1330 .divider { height: 1px; background: #f3f4f6; margin: 24px 0; }
1331 </style>
1332</head>
1333<body>
1334 <div class="container">
1335 <div class="card">
1336 <div class="header">
1337 <h1>{{ app_name }}</h1>
1338 </div>
1339 <div class="content">
1340 <h2 style="margin: 0; color: #111827; font-size: 20px;">Halo!</h2>
1341 <p>Anda menerima email ini karena kami menerima permintaan reset password untuk akun Anda di <strong>{{ app_name }}</strong>.</p>
1342
1343 <div style="text-align: center;">
1344 <a href="{{ reset_url }}" class="button">Reset Password Saya</a>
1345 </div>
1346
1347 <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>
1348
1349 <div class="divider"></div>
1350
1351 <p style="font-size: 12px; color: #9ca3af;">
1352 Jika Anda kesulitan menekan tombol, salin dan tempel URL berikut ke browser Anda:<br>
1353 <span style="word-break: break-all; color: #6366f1;">{{ reset_url }}</span>
1354 </p>
1355 </div>
1356 </div>
1357 <div class="footer">
1358 © 2026 {{ app_name }}. All rights reserved.
1359 </div>
1360 </div>
1361</body>
1362</html>
1363"##;
1364 fs::write(email_reset_view, email_reset_template).ok();
1365 }
1366
1367 let dashboard_view = "src/resources/js/Pages/Dashboard.jsx";
1369 if !std::path::Path::new(dashboard_view).exists() {
1370 let dashboard_template = r##"import React from 'react';
1371import { Link, router, usePage } from '@inertiajs/react';
1372import Toast from '../Components/Toast';
1373
1374export default function Dashboard({ title, userName, userEmail, totalUsers }) {
1375 const { flash } = usePage().props;
1376
1377 const handleLogout = (e) => {
1378 e.preventDefault();
1379 router.post('/logout');
1380 };
1381
1382 return (
1383 <div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col md:flex-row font-sans">
1384 {/* Sidebar */}
1385 <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">
1386 <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl pointer-events-none" />
1387
1388 <div>
1389 {/* Logo */}
1390 <div className="flex items-center space-x-3 mb-10">
1391 <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">
1392 R
1393 </div>
1394 <span className="text-xl font-extrabold text-white tracking-tight">RustBasic</span>
1395 </div>
1396
1397 {/* User Profile Info Card */}
1398 <div className="bg-slate-950/60 border border-slate-800/50 rounded-2xl p-4 mb-8">
1399 <div className="flex items-center space-x-3">
1400 <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">
1401 {userName ? userName[0].toUpperCase() : 'G'}
1402 </div>
1403 <div className="overflow-hidden">
1404 <h4 className="text-sm font-bold text-white truncate">{userName || 'Administrator'}</h4>
1405 <p className="text-xs text-slate-500 truncate">{userEmail || 'admin@rustbasic.dev'}</p>
1406 </div>
1407 </div>
1408 </div>
1409
1410 {/* Navigation links */}
1411 <nav className="space-y-2">
1412 <Link
1413 href="/dashboard"
1414 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"
1415 style={{ textDecoration: 'none' }}
1416 >
1417 <span>📊</span>
1418 <span>Dashboard Overview</span>
1419 </Link>
1420 <Link
1421 href="/"
1422 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"
1423 style={{ textDecoration: 'none' }}
1424 >
1425 <span>🏠</span>
1426 <span>Main Website</span>
1427 </Link>
1428 </nav>
1429 </div>
1430
1431 {/* Logout Form / Button */}
1432 <div className="mt-8 md:mt-0">
1433 <form onSubmit={handleLogout}>
1434 <button
1435 type="submit"
1436 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"
1437 >
1438 <span>🚪</span>
1439 <span>KELUAR SISTEM</span>
1440 </button>
1441 </form>
1442 </div>
1443 </aside>
1444
1445 {/* Main Workspace */}
1446 <main className="flex-1 p-6 md:p-12 overflow-y-auto">
1447 <div className="max-w-6xl mx-auto">
1448 {/* Header */}
1449 <header className="flex flex-col md:flex-row md:items-center md:justify-between mb-10 gap-4">
1450 <div>
1451 <h1 className="text-3xl font-extrabold text-white tracking-tight">{title || 'Overview'}</h1>
1452 <p className="text-slate-400 text-sm mt-1">Selamat datang kembali, kendalikan project Anda secara instan.</p>
1453 </div>
1454 <div>
1455 <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">
1456 <span className="w-2.5 h-2.5 bg-emerald-500 rounded-full mr-2 animate-ping" />
1457 Server Status: <span className="text-emerald-400 ml-1">Running</span>
1458 </span>
1459 </div>
1460 </header>
1461
1462 {/* Toast Notifications */}
1463 <Toast flash={flash} />
1464
1465 {/* Stats Grid */}
1466 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
1467 {/* Stat 1 */}
1468 <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1469 <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1470 User Terdaftar
1471 </span>
1472 <div className="flex items-baseline space-x-2">
1473 <span className="text-5xl font-black text-white tracking-tight">{totalUsers || 0}</span>
1474 <span className="text-emerald-400 text-sm font-bold">↑ 12%</span>
1475 </div>
1476 </div>
1477
1478 {/* Stat 2 */}
1479 <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1480 <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1481 Response Time
1482 </span>
1483 <div className="flex items-baseline space-x-1">
1484 <span className="text-5xl font-black text-indigo-400 tracking-tight">24</span>
1485 <span className="text-slate-400 text-lg font-bold">ms</span>
1486 </div>
1487 </div>
1488
1489 {/* Stat 3 */}
1490 <div className="bg-slate-900/60 border border-slate-800/80 rounded-3xl p-6 relative overflow-hidden glassmorphism">
1491 <span className="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-4">
1492 Database Status
1493 </span>
1494 <div className="flex items-center space-x-3 mt-2">
1495 <div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_12px_#10b981]" />
1496 <span className="text-xl font-extrabold text-emerald-400 tracking-wide uppercase">HEALTHY</span>
1497 </div>
1498 </div>
1499 </div>
1500
1501 {/* Main Info Panel */}
1502 <div className="bg-slate-900/40 border border-slate-800/60 rounded-3xl p-8 glassmorphism">
1503 <div className="flex items-center justify-between mb-6">
1504 <div>
1505 <h3 className="text-lg font-bold text-white">Informasi Kernel Server</h3>
1506 <p className="text-xs text-slate-400 mt-0.5">Detail lingkungan runtime eksekusi Axum Anda.</p>
1507 </div>
1508 <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">
1509 v2026.1
1510 </span>
1511 </div>
1512
1513 <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">
1514 <div className="text-slate-600 mb-2">// RustBasic SPA Kernel Logs</div>
1515 <div>[OK] Compiled with Axum 0.8.2</div>
1516 <div>[OK] Database Pool: Sea-ORM Connection Established</div>
1517 <div>[OK] Modern SPA Routing: Powered by Inertia.js Bridge</div>
1518 <div>[OK] Single-Binary Mode: Compile-time embedding enabled</div>
1519 <div>[OK] Workers: 8 logical threads spawned on CPU cores</div>
1520 </div>
1521 </div>
1522 </div>
1523 </main>
1524 </div>
1525 );
1526}
1527"##;
1528 fs::write(dashboard_view, dashboard_template).ok();
1529 }
1530
1531 let dashboard_controller_path = "src/app/http/controllers/dashboard_controller.rs";
1533 if !std::path::Path::new(dashboard_controller_path).exists() {
1534 let dashboard_template = r#"use crate::app::inertia::inertia;
1535use crate::app::models::users;
1536use rustbasic_core::requests::Request;
1537use rustbasic_core::server::AppState;
1538use rustbasic_core::axum::{response::Response, extract::State};
1539use rustbasic_core::sea_orm::{EntityTrait, PaginatorTrait};
1540use serde_json::json;
1541
1542pub struct DashboardController;
1543
1544impl DashboardController {
1545 pub async fn index(State(state): State<AppState>, req: Request) -> Response {
1546 let user_id = req.session.get::<i32>("user_id").unwrap_or(0);
1547 let user = users::Entity::find_by_id(user_id).one(&state.db).await.ok().flatten();
1548 let total_users = users::Entity::find().count(&state.db).await.unwrap_or(0);
1549
1550 inertia(&req, "Dashboard", json!({
1551 "title": "Dashboard",
1552 "userName": user.as_ref().map(|u| u.name.clone()).unwrap_or("Guest".to_string()),
1553 "userEmail": user.as_ref().map(|u| u.email.clone()).unwrap_or_default(),
1554 "totalUsers": total_users,
1555 }))
1556 }
1557}
1558"#;
1559 fs::write(dashboard_controller_path, dashboard_template).ok();
1560 println!(" {} {}", "✅ Created:".green(), dashboard_controller_path.cyan());
1561 }
1562 update_controller_mod_rs("dashboard_controller");
1563
1564 println!(" {} Folder src/resources/js/Pages/Auth dan Dashboard siap.", "✅ Views:".green());
1565
1566 let welcome_path = "src/resources/js/Pages/Welcome.jsx";
1568 if let Ok(content) = fs::read_to_string(welcome_path)
1569 && content.contains("Backend Online") && !content.contains("auth_installed ?") {
1570 let target = r#" <div className="flex items-center gap-4">
1571 <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">
1572 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1573 Backend Online
1574 </span>
1575 </div>"#;
1576
1577 let replacement = r#" <div className="flex items-center gap-4">
1578 <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">
1579 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1580 Backend Online
1581 </span>
1582 {auth_installed ? (
1583 <Link
1584 href="/dashboard"
1585 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1586 style={{ textDecoration: 'none' }}
1587 >
1588 Dashboard
1589 </Link>
1590 ) : (
1591 <div className="flex gap-2">
1592 <Link
1593 href="/login"
1594 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"
1595 style={{ textDecoration: 'none' }}
1596 >
1597 Masuk
1598 </Link>
1599 <Link
1600 href="/register"
1601 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1602 style={{ textDecoration: 'none' }}
1603 >
1604 Daftar
1605 </Link>
1606 </div>
1607 )}
1608 </div>"#;
1609
1610 let updated = content.replace(target, replacement);
1611 fs::write(welcome_path, updated).ok();
1612 println!(" {} {}", "📝 Updated:".blue(), welcome_path.cyan());
1613 }
1614
1615 println!("\n{}", "✨ Authentication scaffolded successfully!".green().bold());
1616 println!("{}", "Jalankan 'cargo rustbasic route:list' untuk melihat rute baru.".dimmed());
1617}
1618
1619pub async fn remove_auth() {
1620 println!("\n{}", "🗑️ Removing Authentication Scaffold...".red().bold());
1621
1622 let auth_route_path = "src/routes/auth.rs";
1624 if std::path::Path::new(auth_route_path).exists() {
1625 fs::remove_file(auth_route_path).ok();
1626 println!(" {} {}", "✅ Deleted:".green(), auth_route_path.cyan());
1627 }
1628
1629 let routes_mod_path = "src/routes/mod.rs";
1631 if let Ok(mut content) = fs::read_to_string(routes_mod_path)
1632 && content.contains("pub mod auth;") {
1633 content = content.replace("pub mod auth;\n", "");
1634 fs::write(routes_mod_path, content).ok();
1635 println!(" {} {}", "📝 Updated:".blue(), routes_mod_path.cyan());
1636 }
1637
1638 let web_route_path = "src/routes/web.rs";
1640 if let Ok(mut content) = fs::read_to_string(web_route_path) {
1641 let mut changed = false;
1642
1643 if content.contains("use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};") {
1645 content = content.replace("use rustbasic_core::axum::{Router, routing::{get, post}, middleware::from_fn};", "use rustbasic_core::axum::{Router, routing::get};");
1646 changed = true;
1647 }
1648
1649 let imports_to_remove = [
1650 "use crate::app::http::controllers::{auth, dashboard_controller};\n",
1651 "use crate::app::http::middleware::auth::auth_middleware;\n",
1652 "use rustbasic_core::server::AppState;\n",
1653 "use crate::routes::auth as auth_routes;\n",
1654 "use crate::app::http::controllers::{auth, dashboard_controller};",
1655 "use crate::app::http::middleware::auth::auth_middleware;",
1656 "use crate::routes::auth as auth_routes;",
1657 ];
1658
1659 for imp in imports_to_remove {
1660 if content.contains(imp) {
1661 content = content.replace(imp, "");
1662 changed = true;
1663 }
1664 }
1665
1666 if !content.contains("use rustbasic_core::server::AppState;") {
1668 content = content.replace("use rustbasic_core::axum::{Router, routing::get};", "use rustbasic_core::axum::{Router, routing::get};\nuse rustbasic_core::server::AppState;");
1669 }
1670
1671 if content.contains("let auth_protected_routes = Router::new()") {
1673 let re = Regex::new(r##"(?s)\s*let auth_protected_routes = Router::new\(\).*?\.layer\(from_fn\(auth_middleware\)\);\s*"##).unwrap();
1674 content = re.replace(&content, "\n").to_string();
1675
1676 content = content.replace(".merge(auth_routes::router())", "");
1677 content = content.replace(".merge(auth_protected_routes)", "");
1678
1679 let clean_router = r#" Router::new()
1681 .route("/", get(welcome_controller::index))
1682 .route("/about", get(welcome_controller::about))
1683 .route("/dev", get(welcome_controller::dev_info))"#;
1684
1685 let router_re = Regex::new(r##"(?s)Router::new\(\).*?\.route\(\s*\"/dev\"\s*,\s*get\(welcome_controller::dev_info\)\s*\)"##).unwrap();
1686 content = router_re.replace(&content, clean_router).to_string();
1687
1688 let multi_newline_re = Regex::new(r#"\n{3,}"#).unwrap();
1690 content = multi_newline_re.replace_all(&content, "\n\n").to_string();
1691
1692 changed = true;
1693 }
1694
1695 if changed {
1696 fs::write(web_route_path, content).ok();
1697 println!(" {} {}", "📝 Updated:".blue(), web_route_path.cyan());
1698 }
1699 }
1700
1701 let auth_controller_dir = "src/app/http/controllers/auth";
1703 if std::path::Path::new(auth_controller_dir).exists() {
1704 fs::remove_dir_all(auth_controller_dir).ok();
1705 println!(" {} {}", "✅ Deleted:".green(), auth_controller_dir.cyan());
1706 }
1707
1708 if let Ok(entries) = std::fs::read_dir("database/migrations") {
1710 for entry in entries.flatten() {
1711 if let Some(name) = entry.file_name().to_str()
1712 && name.ends_with("_create_password_resets_table.rs") {
1713 let path = entry.path();
1714 fs::remove_file(&path).ok();
1715 println!(" {} {}", "✅ Deleted:".green(), path.display().to_string().cyan());
1716 }
1717 }
1718 }
1719
1720 let model_path = "src/app/models/password_resets.rs";
1721 if std::path::Path::new(model_path).exists() {
1722 fs::remove_file(model_path).ok();
1723 println!(" {} {}", "✅ Deleted:".green(), model_path.cyan());
1724 }
1725
1726 let components_dir = "src/resources/js/Components";
1728 let toast_view = "src/resources/js/Components/Toast.jsx";
1729 if std::path::Path::new(toast_view).exists() {
1730 fs::remove_file(toast_view).ok();
1731 }
1732 let alert_banner_view = "src/resources/js/Components/AlertBanner.jsx";
1733 if std::path::Path::new(alert_banner_view).exists() {
1734 fs::remove_file(alert_banner_view).ok();
1735 }
1736 let form_input_view = "src/resources/js/Components/FormInput.jsx";
1737 if std::path::Path::new(form_input_view).exists() {
1738 fs::remove_file(form_input_view).ok();
1739 }
1740 if std::path::Path::new(components_dir).exists()
1741 && let Ok(entries) = std::fs::read_dir(components_dir)
1742 && entries.count() == 0 {
1743 fs::remove_dir(components_dir).ok();
1744 }
1745
1746 let auth_page_dir = "src/resources/js/Pages/Auth";
1747 if std::path::Path::new(auth_page_dir).exists() {
1748 fs::remove_dir_all(auth_page_dir).ok();
1749 println!(" {} {}", "✅ Deleted:".green(), auth_page_dir.cyan());
1750 }
1751
1752 let dashboard_page = "src/resources/js/Pages/Dashboard.jsx";
1753 if std::path::Path::new(dashboard_page).exists() {
1754 fs::remove_file(dashboard_page).ok();
1755 println!(" {} {}", "✅ Deleted:".green(), dashboard_page.cyan());
1756 }
1757
1758 let auth_middleware_path = "src/app/http/middleware/auth.rs";
1760 if std::path::Path::new(auth_middleware_path).exists() {
1761 fs::remove_file(auth_middleware_path).ok();
1762 println!(" {} {}", "✅ Deleted:".green(), auth_middleware_path.cyan());
1763 }
1764
1765 let middleware_mod_path = "src/app/http/middleware/mod.rs";
1766 if let Ok(mut content) = fs::read_to_string(middleware_mod_path)
1767 && content.contains("pub mod auth;") {
1768 content = content.replace("pub mod auth;\n", "");
1769 fs::write(middleware_mod_path, content).ok();
1770 println!(" {} {}", "📝 Updated:".blue(), middleware_mod_path.cyan());
1771 }
1772
1773 let dashboard_path = "src/app/http/controllers/dashboard_controller.rs";
1775 if std::path::Path::new(dashboard_path).exists() {
1776 fs::remove_file(dashboard_path).ok();
1777 println!(" {} {}", "✅ Deleted:".green(), dashboard_path.cyan());
1778 }
1779
1780 let controllers_mod_path = "src/app/http/controllers/mod.rs";
1782 if let Ok(mut content) = fs::read_to_string(controllers_mod_path) {
1783 let mut changed = false;
1784 if content.contains("pub mod auth;") {
1785 content = content.replace("pub mod auth;\n", "");
1786 changed = true;
1787 }
1788 if content.contains("pub mod dashboard_controller;") {
1789 content = content.replace("pub mod dashboard_controller;\n", "");
1790 changed = true;
1791 }
1792 if changed {
1793 fs::write(controllers_mod_path, content).ok();
1794 println!(" {} {}", "📝 Updated:".blue(), controllers_mod_path.cyan());
1795 }
1796 }
1797
1798 let models_mod_path = "src/app/models/mod.rs";
1800 if let Ok(mut content) = fs::read_to_string(models_mod_path) {
1801 let mut changed = false;
1802 if content.contains("pub mod password_resets;") {
1803 content = content.replace("pub mod password_resets;\n", "");
1804 content = content.replace("pub mod password_resets;", "");
1805 changed = true;
1806 }
1807 if changed {
1808 fs::write(models_mod_path, content).ok();
1809 println!(" {} {}", "📝 Updated:".blue(), models_mod_path.cyan());
1810 }
1811 }
1812
1813 let migration_mod_path = "database/migrations/mod.rs";
1815 if let Ok(content) = fs::read_to_string(migration_mod_path) {
1816 let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
1817 let mut changed = false;
1818
1819 lines.retain(|line| {
1821 if line.contains("_create_password_resets_table;") || (line.contains("Box::new(") && line.contains("_create_password_resets_table::Migration")) {
1822 changed = true;
1823 false
1824 } else {
1825 true
1826 }
1827 });
1828
1829 if changed {
1830 fs::write(migration_mod_path, lines.join("\n")).ok();
1831 println!(" {} {}", "📝 Updated:".blue(), migration_mod_path.cyan());
1832 }
1833 }
1834
1835 let welcome_path = "src/resources/js/Pages/Welcome.jsx";
1837 if let Ok(content) = fs::read_to_string(welcome_path)
1838 && content.contains("auth_installed ?") {
1839 let target = r#" <div className="flex items-center gap-4">
1840 <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">
1841 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1842 Backend Online
1843 </span>
1844 {auth_installed ? (
1845 <Link
1846 href="/dashboard"
1847 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1848 style={{ textDecoration: 'none' }}
1849 >
1850 Dashboard
1851 </Link>
1852 ) : (
1853 <div className="flex gap-2">
1854 <Link
1855 href="/login"
1856 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"
1857 style={{ textDecoration: 'none' }}
1858 >
1859 Masuk
1860 </Link>
1861 <Link
1862 href="/register"
1863 className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-bold text-white transition-all duration-300"
1864 style={{ textDecoration: 'none' }}
1865 >
1866 Daftar
1867 </Link>
1868 </div>
1869 )}
1870 </div>"#;
1871
1872 let replacement = r#" <div className="flex items-center gap-4">
1873 <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">
1874 <span className="w-2 h-2 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 10px #34d399" }} />
1875 Backend Online
1876 </span>
1877 </div>"#;
1878
1879 let updated = content.replace(target, replacement);
1880 fs::write(welcome_path, updated).ok();
1881 println!(" {} {}", "📝 Restored:".blue(), welcome_path.cyan());
1882 }
1883
1884 println!(" {} {}", "⏳".blue(), "Cleaning up migration records from database...".dimmed());
1886 let db_connection = std::env::var("DB_CONNECTION").unwrap_or_else(|_| "sqlite".to_string());
1887 let db_host = std::env::var("DB_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
1888 let db_port = std::env::var("DB_PORT").unwrap_or_else(|_| "3306".to_string());
1889 let db_database = std::env::var("DB_DATABASE").unwrap_or_else(|_| "rustbasic".to_string());
1890 let db_username = std::env::var("DB_USERNAME").unwrap_or_else(|_| "root".to_string());
1891 let db_password = std::env::var("DB_PASSWORD").unwrap_or_default();
1892
1893 let db_url = if db_connection == "mysql" {
1894 format!(
1895 "mysql://{}:{}@{}:{}/{}",
1896 db_username, db_password, db_host, db_port, db_database
1897 )
1898 } else {
1899 format!("sqlite:database/{}.sqlite?mode=rwc", db_database)
1900 };
1901
1902 if let Ok(db) = sea_orm::Database::connect(db_url).await {
1903 use sea_orm::ConnectionTrait;
1904 let table_name = if db_connection == "mysql" { "sea_orm_migrations" } else { "seaql_migrations" };
1905 let sql = format!("DELETE FROM {} WHERE version LIKE '%_create_password_resets_table'", table_name);
1906 let db_backend = if db_connection == "mysql" { sea_orm::DbBackend::MySql } else { sea_orm::DbBackend::Sqlite };
1907 let _ = db.execute(sea_orm::Statement::from_string(db_backend, sql)).await;
1908 println!(" {} {}", "✅ Cleaned:".green(), "Database migration records removed.".cyan());
1909 }
1910
1911 println!("\n{}", "✨ Authentication removed successfully!".green().bold());
1912}