use std::fs;
use std::path::PathBuf;
use anyhow::{Context, Result};
use super::project_root;
pub fn scaffold() -> Result<()> {
let root = project_root();
let files = [
("src/app/controllers_auth.rs", AUTH_CONTROLLER),
("src/app/requests_auth.rs", AUTH_REQUESTS),
("src/routes/auth.rs", AUTH_ROUTES),
("resources/views/auth/login.forge.html", LOGIN_VIEW),
("resources/views/auth/register.forge.html", REGISTER_VIEW),
(
"database/migrations/2026_01_01_000099_add_auth_columns_to_users.rs",
AUTH_MIGRATION,
),
];
let mut written = Vec::new();
let mut skipped = Vec::new();
for (rel, contents) in files {
let path = root.join(rel);
if path.exists() {
skipped.push(rel);
continue;
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).ok();
}
fs::write(&path, contents).with_context(|| format!("writing {}", path.display()))?;
written.push(rel);
}
println!();
println!(" ✓ scaffolded auth");
println!();
for w in &written {
println!(" + {w}");
}
if !skipped.is_empty() {
println!();
println!(" skipped (already exist):");
for s in &skipped {
println!(" - {s}");
}
}
println!();
println!(" manual steps:");
println!(" 1. Add `mod controllers_auth;` and `mod requests_auth;` to src/app/mod.rs");
println!(" 2. Add `pub mod auth;` to src/routes/mod.rs");
println!(" 3. Wire `routes::auth::register` into bootstrap/app.rs");
println!(" 4. Run `smith migrate` to add auth columns to your users table");
println!();
Ok(())
}
const AUTH_CONTROLLER: &str = r##"//! Authentication controllers — login, register, logout.
use anvilforge::prelude::*;
use anvilforge::auth;
use anvilforge::session::Session;
use crate::app::models::User;
use crate::app::requests_auth::{LoginRequest, RegisterRequest};
pub struct AuthController;
impl AuthController {
/// GET /login — show the login form.
pub async fn show_login() -> Result<ViewResponse> {
Ok(ViewResponse::new(include_str!("../../resources/views/auth/login.forge.html").to_string()))
}
/// POST /login — verify credentials, persist user in session.
pub async fn login(
State(c): State<Container>,
session: Session,
payload: LoginRequest,
) -> Result<Redirect> {
let user = auth::attempt::<User>(&c, &payload.email, &payload.password)
.await?
.ok_or_else(|| Error::Unauthenticated)?;
auth::login(&session, &user).await?;
Ok(Redirect::to("/"))
}
/// GET /register — show the registration form.
pub async fn show_register() -> Result<ViewResponse> {
Ok(ViewResponse::new(include_str!("../../resources/views/auth/register.forge.html").to_string()))
}
/// POST /register — create a user and log them in.
pub async fn register(
State(c): State<Container>,
session: Session,
payload: RegisterRequest,
) -> Result<Redirect> {
let password_hash = auth::hash_password(&payload.password)?;
let row: (i64,) = sqlx::query_as(
"INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3) RETURNING id",
)
.bind(&payload.name)
.bind(&payload.email)
.bind(&password_hash)
.fetch_one(c.pool())
.await
.map_err(Error::Database)?;
let user = User::find(c.pool(), row.0)
.await?
.ok_or(Error::NotFound)?;
auth::login(&session, &user).await?;
Ok(Redirect::to("/"))
}
/// POST /logout — clear the session.
pub async fn logout(session: Session) -> Result<Redirect> {
auth::logout(&session).await?;
Ok(Redirect::to("/"))
}
}
"##;
const AUTH_REQUESTS: &str = r#"//! Auth-related form requests.
use anvilforge::prelude::*;
use garde::Validate;
#[derive(Debug, Deserialize, Validate, FormRequest)]
pub struct LoginRequest {
#[garde(email)]
pub email: String,
#[garde(length(min = 1))]
pub password: String,
}
#[derive(Debug, Deserialize, Validate, FormRequest)]
pub struct RegisterRequest {
#[garde(length(min = 1, max = 80))]
pub name: String,
#[garde(email)]
pub email: String,
#[garde(length(min = 8))]
pub password: String,
}
"#;
const AUTH_ROUTES: &str = r#"//! Auth routes.
use anvilforge::prelude::*;
use crate::app::controllers_auth::AuthController;
pub fn register(r: Router) -> Router {
r.get("/login", AuthController::show_login)
.post("/login", AuthController::login)
.get("/register", AuthController::show_register)
.post("/register", AuthController::register)
.post("/logout", AuthController::logout)
}
"#;
const LOGIN_VIEW: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Log in</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 24rem; margin: 4rem auto; padding: 0 1rem; }
h1 { color: #c2410c; }
form { display: grid; gap: 0.75rem; }
label { display: grid; gap: 0.25rem; }
input { padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 0.25rem; }
button { padding: 0.5rem; font-size: 1rem; background: #c2410c; color: white; border: 0; border-radius: 0.25rem; cursor: pointer; }
</style>
</head>
<body>
<h1>Log in</h1>
<form method="POST" action="/login">
@csrf
<label>Email <input type="email" name="email" required></label>
<label>Password <input type="password" name="password" required></label>
<button type="submit">Log in</button>
</form>
<p>No account? <a href="/register">Register</a></p>
</body>
</html>
"#;
const REGISTER_VIEW: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 24rem; margin: 4rem auto; padding: 0 1rem; }
h1 { color: #c2410c; }
form { display: grid; gap: 0.75rem; }
label { display: grid; gap: 0.25rem; }
input { padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 0.25rem; }
button { padding: 0.5rem; font-size: 1rem; background: #c2410c; color: white; border: 0; border-radius: 0.25rem; cursor: pointer; }
</style>
</head>
<body>
<h1>Register</h1>
<form method="POST" action="/register">
@csrf
<label>Name <input type="text" name="name" required></label>
<label>Email <input type="email" name="email" required></label>
<label>Password <input type="password" name="password" required minlength="8"></label>
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="/login">Log in</a></p>
</body>
</html>
"#;
const AUTH_MIGRATION: &str = r#"//! Add auth columns to the users table.
//! Adjust as needed — if your `users` table already has `password_hash`, this migration is a no-op.
use anvilforge::prelude::*;
use anvilforge::cast::Schema;
pub struct AddAuthColumnsToUsersTable;
impl CastMigration for AddAuthColumnsToUsersTable {
fn name(&self) -> &'static str {
"2026_01_01_000099_add_auth_columns_to_users"
}
fn up(&self, s: &mut Schema) {
// sea-query's alter API is limited; emit raw SQL for compatibility.
s.raw("ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255) NOT NULL DEFAULT ''");
s.raw("ALTER TABLE users ADD COLUMN IF NOT EXISTS remember_token VARCHAR(100)");
}
fn down(&self, s: &mut Schema) {
s.raw("ALTER TABLE users DROP COLUMN IF EXISTS password_hash");
s.raw("ALTER TABLE users DROP COLUMN IF EXISTS remember_token");
}
}
"#;
#[allow(dead_code)]
fn _link(_: PathBuf) {}