rok-cli 0.1.1

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

pub struct SocialAuthScaffold;

impl Scaffold for SocialAuthScaffold {
    fn name(&self) -> &'static str {
        "social-auth"
    }
    fn description(&self) -> &'static str {
        "OAuth login: GitHub/Google/Discord/Twitter, account linking, state verification"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;
        write_file(
            &mut r,
            "src/app/controllers/oauth_controller.rs",
            CONTROLLER,
            d,
        )?;
        write_file(
            &mut r,
            "migrations/create_social_accounts_table.sql",
            MIGRATION,
            d,
        )?;
        r.warnings
            .push("Set GITHUB_CLIENT_ID/SECRET, GOOGLE_CLIENT_ID/SECRET in .env".into());
        r.warnings.push(
            "Register GET /auth/{provider}/redirect and /auth/{provider}/callback routes".into(),
        );
        Ok(r)
    }
}

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

#[derive(Deserialize)]
pub struct OAuthCallback {
    pub code: String,
    pub state: Option<String>,
}

pub async fn redirect(Path(provider): Path<String>) -> impl IntoResponse {
    // TODO: generate state param, build provider auth URL, redirect
    match provider.as_str() {
        "github" | "google" | "discord" | "twitter" => {
            Redirect::to("https://provider-auth-url.example.com/authorize").into_response()
        }
        _ => Response::bad_request("Unknown provider").into_response(),
    }
}

pub async fn callback(
    Path(provider): Path<String>,
    Query(q): Query<OAuthCallback>,
    State(pool): State<sqlx::PgPool>,
) -> impl IntoResponse {
    // TODO: verify state, exchange code for token, fetch profile, upsert social_account, issue JWT
    Response::json(serde_json::json!({ "message": "OAuth not fully implemented" }))
}
"#;

const MIGRATION: &str = r#"CREATE TABLE social_accounts (
    id            BIGSERIAL PRIMARY KEY,
    user_id       BIGINT NOT NULL,
    provider      TEXT NOT NULL,
    provider_id   TEXT NOT NULL,
    access_token  TEXT,
    refresh_token TEXT,
    profile       JSONB,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (provider, provider_id)
);
"#;