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