| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
use axum::{ |
| 9 |
extract::State, |
| 10 |
response::{IntoResponse, Response}, |
| 11 |
Form, |
| 12 |
}; |
| 13 |
use serde::Deserialize; |
| 14 |
|
| 15 |
use crate::{ |
| 16 |
auth::AdminUser, |
| 17 |
db, |
| 18 |
error::{AppError, Result}, |
| 19 |
helpers::get_csrf_token, |
| 20 |
templates::*, |
| 21 |
types::*, |
| 22 |
AppState, |
| 23 |
}; |
| 24 |
|
| 25 |
|
| 26 |
#[tracing::instrument(skip_all, name = "admin::admin_comp_codes")] |
| 27 |
pub(super) async fn admin_comp_codes( |
| 28 |
State(state): State<AppState>, |
| 29 |
session: tower_sessions::Session, |
| 30 |
AdminUser(user): AdminUser, |
| 31 |
) -> Result<impl IntoResponse> { |
| 32 |
let csrf_token = get_csrf_token(&session).await; |
| 33 |
let comp_codes = load_comp_code_rows(&state).await?; |
| 34 |
Ok(AdminCompCodesTemplate { |
| 35 |
csrf_token, |
| 36 |
session_user: Some(user), |
| 37 |
admin_active_page: "comp-codes", |
| 38 |
comp_codes, |
| 39 |
}) |
| 40 |
} |
| 41 |
|
| 42 |
|
| 43 |
#[derive(Debug, Deserialize)] |
| 44 |
pub(super) struct CompCodeForm { |
| 45 |
code: String, |
| 46 |
trial_days: i32, |
| 47 |
|
| 48 |
#[serde(default, deserialize_with = "empty_string_as_none")] |
| 49 |
max_uses: Option<i32>, |
| 50 |
|
| 51 |
#[serde(default, deserialize_with = "empty_string_as_none")] |
| 52 |
expires_in_days: Option<i64>, |
| 53 |
} |
| 54 |
|
| 55 |
|
| 56 |
fn empty_string_as_none<'de, D, T>(de: D) -> std::result::Result<Option<T>, D::Error> |
| 57 |
where |
| 58 |
D: serde::Deserializer<'de>, |
| 59 |
T: std::str::FromStr, |
| 60 |
T::Err: std::fmt::Display, |
| 61 |
{ |
| 62 |
let opt = Option::<String>::deserialize(de)?; |
| 63 |
match opt.as_deref().map(str::trim) { |
| 64 |
None | Some("") => Ok(None), |
| 65 |
Some(s) => s.parse::<T>().map(Some).map_err(serde::de::Error::custom), |
| 66 |
} |
| 67 |
} |
| 68 |
|
| 69 |
|
| 70 |
|
| 71 |
|
| 72 |
|
| 73 |
|
| 74 |
|
| 75 |
|
| 76 |
|
| 77 |
#[tracing::instrument(skip_all, name = "admin::admin_create_comp_code")] |
| 78 |
pub(super) async fn admin_create_comp_code( |
| 79 |
State(state): State<AppState>, |
| 80 |
AdminUser(admin): AdminUser, |
| 81 |
Form(form): Form<CompCodeForm>, |
| 82 |
) -> Result<Response> { |
| 83 |
let code = form.code.trim().to_uppercase(); |
| 84 |
if code.is_empty() { |
| 85 |
return Err(AppError::BadRequest("Code is required".to_string())); |
| 86 |
} |
| 87 |
if form.trial_days <= 0 { |
| 88 |
return Err(AppError::BadRequest("Trial days must be positive".to_string())); |
| 89 |
} |
| 90 |
let expires_at = form |
| 91 |
.expires_in_days |
| 92 |
.map(|d| chrono::Utc::now() + chrono::Duration::days(d)); |
| 93 |
|
| 94 |
db::promo_codes::create_platform_promo_code( |
| 95 |
&state.db, |
| 96 |
admin.id, |
| 97 |
&code, |
| 98 |
db::CodePurpose::FreeTrial, |
| 99 |
None, |
| 100 |
None, |
| 101 |
0, |
| 102 |
Some(form.trial_days), |
| 103 |
form.max_uses, |
| 104 |
expires_at, |
| 105 |
) |
| 106 |
.await?; |
| 107 |
|
| 108 |
tracing::info!(code = %code, trial_days = form.trial_days, "minted creator-tier comp code"); |
| 109 |
|
| 110 |
let comp_codes = load_comp_code_rows(&state).await?; |
| 111 |
Ok(AdminCompCodesEntriesTemplate { comp_codes }.into_response()) |
| 112 |
} |
| 113 |
|
| 114 |
|
| 115 |
async fn load_comp_code_rows(state: &AppState) -> Result<Vec<AdminCompCodeRow>> { |
| 116 |
let codes = db::promo_codes::get_platform_trial_codes(&state.db).await?; |
| 117 |
Ok(codes.iter().map(AdminCompCodeRow::from_db).collect()) |
| 118 |
} |
| 119 |
|