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