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