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