rok-cli 0.3.2

Developer CLI for rok-based Axum applications
use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;

pub struct AuthScaffold;

impl Scaffold for AuthScaffold {
    fn name(&self) -> &'static str {
        "auth"
    }
    fn description(&self) -> &'static str {
        "Full auth system: login/register/logout, password reset, email verify, TOTP"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;

        write_file(
            &mut r,
            "src/app/controllers/auth_controller.rs",
            AUTH_CONTROLLER,
            d,
        )?;
        write_file(
            &mut r,
            "src/app/controllers/password_reset_controller.rs",
            PASSWORD_RESET,
            d,
        )?;
        write_file(
            &mut r,
            "src/app/controllers/email_verify_controller.rs",
            EMAIL_VERIFY,
            d,
        )?;
        write_file(
            &mut r,
            "src/app/controllers/totp_controller.rs",
            TOTP_CONTROLLER,
            d,
        )?;
        write_file(
            &mut r,
            "src/app/middleware/auth_rate_limit.rs",
            AUTH_RATE_LIMIT,
            d,
        )?;
        write_file(&mut r, "tests/auth_flow_test.rs", AUTH_TEST, d)?;

        r.warnings
            .push("Register auth routes in src/routes/mod.rs".into());
        r.warnings
            .push("Add JWT_SECRET, MAIL_* vars to .env".into());
        Ok(r)
    }
}

const AUTH_CONTROLLER: &str = r#"use axum::{extract::State, response::IntoResponse, Json};
use rok_auth::axum::{Ctx, Response};
use rok_validate::Valid;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct LoginRequest {
    pub email: String,
    pub password: String,
}

#[derive(Deserialize)]
pub struct RegisterRequest {
    pub email: String,
    pub password: String,
    pub name: String,
}

pub async fn login(Valid(Json(req)): Valid<Json<LoginRequest>>) -> impl IntoResponse {
    // TODO: verify credentials, issue JWT
    Response::json(serde_json::json!({"message": "Login not yet implemented"}))
}

pub async fn register(Valid(Json(req)): Valid<Json<RegisterRequest>>) -> impl IntoResponse {
    // TODO: create user, send verification email
    Response::json(serde_json::json!({"message": "Register not yet implemented"}))
}

pub async fn logout(ctx: Ctx) -> impl IntoResponse {
    // TODO: blacklist token
    Response::json(serde_json::json!({"message": "logged out"}))
}

pub async fn refresh(ctx: Ctx) -> impl IntoResponse {
    // TODO: issue new access token
    Response::json(serde_json::json!({"message": "Refresh not yet implemented"}))
}
"#;

const PASSWORD_RESET: &str = r#"use axum::{response::IntoResponse, Json};
use rok_auth::axum::Response;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct ForgotRequest { pub email: String }

#[derive(Deserialize)]
pub struct ResetRequest { pub token: String, pub password: String }

pub async fn forgot(Json(req): Json<ForgotRequest>) -> impl IntoResponse {
    // TODO: send reset email via rok-mail
    Response::json(serde_json::json!({"message": "Reset email sent"}))
}

pub async fn reset(Json(req): Json<ResetRequest>) -> impl IntoResponse {
    // TODO: verify token and update password
    Response::json(serde_json::json!({"message": "Password updated"}))
}
"#;

const EMAIL_VERIFY: &str = r#"use axum::{extract::Query, response::IntoResponse};
use rok_auth::axum::Response;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct VerifyQuery { pub token: String }

pub async fn verify(Query(q): Query<VerifyQuery>) -> impl IntoResponse {
    // TODO: verify email token and mark user as verified
    Response::json(serde_json::json!({"message": "Email verified"}))
}

pub async fn resend(ctx: rok_auth::axum::Ctx) -> impl IntoResponse {
    // TODO: resend verification email
    Response::json(serde_json::json!({"message": "Verification email resent"}))
}
"#;

const TOTP_CONTROLLER: &str = r#"use axum::{response::IntoResponse, Json};
use rok_auth::axum::{Ctx, Response};
use serde::Deserialize;

#[derive(Deserialize)]
pub struct TotpVerifyRequest { pub code: String }

pub async fn setup(ctx: Ctx) -> impl IntoResponse {
    // TODO: generate TOTP secret, return QR code URI
    Response::json(serde_json::json!({"qr_uri": "otpauth://totp/..."}))
}

pub async fn enable(ctx: Ctx, Json(req): Json<TotpVerifyRequest>) -> impl IntoResponse {
    // TODO: verify code and enable TOTP
    Response::json(serde_json::json!({"message": "TOTP enabled"}))
}

pub async fn disable(ctx: Ctx, Json(req): Json<TotpVerifyRequest>) -> impl IntoResponse {
    // TODO: verify code and disable TOTP
    Response::json(serde_json::json!({"message": "TOTP disabled"}))
}
"#;

const AUTH_RATE_LIMIT: &str = r#"use axum::{extract::Request, middleware::Next, response::Response};
use rok_problem::Problem;

pub async fn auth_rate_limit(req: Request, next: Next) -> Result<Response, Problem> {
    // TODO: integrate with rok-cache for sliding window rate limiting
    // Example: 10 requests per minute per IP for auth endpoints
    Ok(next.run(req).await)
}
"#;

const AUTH_TEST: &str = r#"#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn test_login_invalid_credentials() {
        // TODO: set up test app, POST /auth/login with wrong password, expect 401
    }

    #[tokio::test]
    async fn test_register_creates_user() {
        // TODO: POST /auth/register, verify user in DB, expect 201
    }

    #[tokio::test]
    async fn test_logout_blacklists_token() {
        // TODO: login, logout, retry with old token, expect 401
    }
}
"#;