use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;
pub struct NewsletterScaffold;
impl Scaffold for NewsletterScaffold {
fn name(&self) -> &'static str {
"newsletter"
}
fn description(&self) -> &'static str {
"Newsletter: subscriber model, double opt-in, campaigns, send job, unsubscribe"
}
fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
let mut r = ScaffoldResult::default();
let d = args.dry_run;
write_file(
&mut r,
"src/app/controllers/newsletter_controller.rs",
CONTROLLER,
d,
)?;
write_file(&mut r, "src/app/jobs/send_campaign_job.rs", JOB, d)?;
write_file(
&mut r,
"migrations/create_newsletter_tables.sql",
MIGRATION,
d,
)?;
r.warnings.push("Register /newsletter routes".into());
r.warnings
.push("Configure MAIL_* env vars for sending".into());
Ok(r)
}
}
const CONTROLLER: &str = r#"use axum::{extract::{Path, Query}, response::IntoResponse, Json};
use rok_auth::axum::Response;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct SubscribeRequest { pub email: String, pub name: Option<String> }
pub async fn subscribe(Json(req): Json<SubscribeRequest>) -> impl IntoResponse {
// TODO: create subscriber with confirmed_at = NULL, send confirmation email
Response::json(serde_json::json!({ "message": "Check your email to confirm" }))
}
pub async fn confirm(Path(token): Path<String>) -> impl IntoResponse {
// TODO: set confirmed_at = now() WHERE confirm_token = token
Response::json(serde_json::json!({ "message": "Subscription confirmed" }))
}
pub async fn unsubscribe(Path(token): Path<String>) -> impl IntoResponse {
// TODO: set unsubscribed_at = now() WHERE unsubscribe_token = token
Response::json(serde_json::json!({ "message": "Unsubscribed successfully" }))
}
"#;
const JOB: &str = r#"use rok_queue::Job;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct SendCampaignJob {
pub campaign_id: i64,
pub batch_size: usize,
pub offset: usize,
}
#[async_trait::async_trait]
impl Job for SendCampaignJob {
const QUEUE: &'static str = "newsletters";
async fn handle(&self) -> anyhow::Result<()> {
// TODO: fetch subscribers batch, render template, send via rok-mail
Ok(())
}
}
"#;
const MIGRATION: &str = r#"CREATE TABLE newsletter_subscribers (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
confirmed_at TIMESTAMPTZ,
unsubscribed_at TIMESTAMPTZ,
confirm_token TEXT,
unsubscribe_token TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE newsletter_campaigns (
id BIGSERIAL PRIMARY KEY,
subject TEXT NOT NULL,
body_html TEXT NOT NULL,
body_text TEXT,
status TEXT NOT NULL DEFAULT 'draft',
scheduled_at TIMESTAMPTZ,
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;