headless_lms_server/controllers/cms/
email_templates.rs1use headless_lms_utils::email_processor::{self, EmailRenderInput};
4use models::email_layouts::{self, ResolvedLayout};
5use models::email_templates::{EmailTemplate, EmailTemplateUpdate};
6use utoipa::{OpenApi, ToSchema};
7
8use crate::controllers::helpers::email_content::parse_email_content;
9use crate::domain::authorization::AuthorizationToken;
10use crate::prelude::*;
11
12const MAX_TEST_SENDS_PER_MINUTE: i64 = 5;
14
15#[derive(OpenApi)]
16#[openapi(paths(
17 get_email_template,
18 update_email_template,
19 delete_email_template,
20 preview_email_template,
21 send_test_email
22))]
23pub(crate) struct CmsEmailTemplatesApiDoc;
24
25#[derive(Debug, Serialize, Deserialize, ToSchema)]
27pub struct EmailTemplatePreviewRequest {
28 pub subject: String,
29 pub content: serde_json::Value,
30}
31
32#[derive(Debug, Serialize, Deserialize, ToSchema)]
34pub struct EmailTemplatePreview {
35 pub subject: String,
36 pub html: String,
38 pub plain_text: String,
39}
40
41async fn authorize_template_edit(
43 conn: &mut PgConnection,
44 user: &AuthUser,
45 template: &EmailTemplate,
46) -> Result<AuthorizationToken, ControllerError> {
47 let token = match template.course_id {
48 Some(course_id) => {
49 authorize(conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?
50 }
51 None => {
52 authorize(
53 conn,
54 Act::Administrate,
55 Some(user.id),
56 Res::GlobalPermissions,
57 )
58 .await?
59 }
60 };
61 Ok(token)
62}
63
64#[instrument(skip(pool))]
68#[utoipa::path(
69 get,
70 path = "/{email_template_id}",
71 operation_id = "getCmsEmailTemplate",
72 tag = "cms_email_templates",
73 params(
74 ("email_template_id" = Uuid, Path, description = "Email template id")
75 ),
76 responses(
77 (status = 200, description = "Email template", body = EmailTemplate)
78 )
79)]
80async fn get_email_template(
81 email_template_id: web::Path<Uuid>,
82 pool: web::Data<PgPool>,
83 user: AuthUser,
84) -> ControllerResult<web::Json<EmailTemplate>> {
85 let mut conn = pool.acquire().await?;
86 let email_templates =
87 models::email_templates::get_email_template(&mut conn, *email_template_id).await?;
88 let token = authorize_template_edit(&mut conn, &user, &email_templates).await?;
89 token.authorized_ok(web::Json(email_templates))
90}
91
92#[instrument(skip(pool))]
96#[utoipa::path(
97 put,
98 path = "/{email_template_id}",
99 operation_id = "updateCmsEmailTemplate",
100 tag = "cms_email_templates",
101 params(
102 ("email_template_id" = Uuid, Path, description = "Email template id")
103 ),
104 request_body = EmailTemplateUpdate,
105 responses(
106 (status = 200, description = "Updated email template", body = EmailTemplate)
107 )
108)]
109async fn update_email_template(
110 email_template_id: web::Path<Uuid>,
111 payload: web::Json<EmailTemplateUpdate>,
112 pool: web::Data<PgPool>,
113 user: AuthUser,
114) -> ControllerResult<web::Json<EmailTemplate>> {
115 let mut conn = pool.acquire().await?;
116 let template =
117 models::email_templates::get_email_template(&mut conn, *email_template_id).await?;
118 let token = authorize_template_edit(&mut conn, &user, &template).await?;
119 let request_update_template = payload.0;
120 parse_email_content(&request_update_template.content)?;
121 let updated_template = models::email_templates::update_email_template(
122 &mut conn,
123 *email_template_id,
124 request_update_template,
125 )
126 .await?;
127 token.authorized_ok(web::Json(updated_template))
128}
129
130#[instrument(skip(pool))]
134#[utoipa::path(
135 delete,
136 path = "/{email_template_id}",
137 operation_id = "deleteCmsEmailTemplate",
138 tag = "cms_email_templates",
139 params(
140 ("email_template_id" = Uuid, Path, description = "Email template id")
141 ),
142 responses(
143 (status = 200, description = "Deleted email template", body = EmailTemplate)
144 )
145)]
146async fn delete_email_template(
147 email_template_id: web::Path<Uuid>,
148 pool: web::Data<PgPool>,
149 user: AuthUser,
150) -> ControllerResult<web::Json<EmailTemplate>> {
151 let mut conn = pool.acquire().await?;
152 let template =
153 models::email_templates::get_email_template(&mut conn, *email_template_id).await?;
154 let token = authorize_template_edit(&mut conn, &user, &template).await?;
155 let deleted =
156 models::email_templates::delete_email_template(&mut conn, *email_template_id).await?;
157 token.authorized_ok(web::Json(deleted))
158}
159
160#[instrument(skip(pool, app_conf, payload))]
166#[utoipa::path(
167 post,
168 path = "/{email_template_id}/preview",
169 operation_id = "previewCmsEmailTemplate",
170 tag = "cms_email_templates",
171 params(
172 ("email_template_id" = Uuid, Path, description = "Email template id")
173 ),
174 request_body = EmailTemplatePreviewRequest,
175 responses(
176 (status = 200, description = "Rendered email", body = EmailTemplatePreview)
177 )
178)]
179async fn preview_email_template(
180 email_template_id: web::Path<Uuid>,
181 payload: web::Json<EmailTemplatePreviewRequest>,
182 pool: web::Data<PgPool>,
183 app_conf: web::Data<ApplicationConfiguration>,
184 user: AuthUser,
185) -> ControllerResult<web::Json<EmailTemplatePreview>> {
186 let mut conn = pool.acquire().await?;
187 let template =
188 models::email_templates::get_email_template(&mut conn, *email_template_id).await?;
189 let token = authorize_template_edit(&mut conn, &user, &template).await?;
190 let body = parse_email_content(&payload.content)?;
191 let language = models::email_templates::get_language(&mut conn, template.id).await?;
192 let layouts = email_layouts::get_all_live(&mut conn).await?;
193 let layout = ResolvedLayout::for_language(&layouts, language.as_deref());
194 let replacements = template
195 .email_template_type
196 .sample_placeholders(&app_conf.base_url);
197 let rendered = email_processor::render_email(EmailRenderInput {
198 layout_html: layout.html,
199 theme: layout.theme,
200 subject: &payload.subject,
201 body,
202 language: language.as_deref().unwrap_or_default(),
203 replacements: &replacements,
204 });
205 token.authorized_ok(web::Json(EmailTemplatePreview {
206 subject: rendered.subject,
207 html: rendered.html,
208 plain_text: rendered.plain_text,
209 }))
210}
211
212#[instrument(skip(pool, app_conf, payload))]
219#[utoipa::path(
220 post,
221 path = "/{email_template_id}/send-test",
222 operation_id = "sendCmsEmailTemplateTest",
223 tag = "cms_email_templates",
224 params(
225 ("email_template_id" = Uuid, Path, description = "Email template id")
226 ),
227 request_body = EmailTemplatePreviewRequest,
228 responses(
229 (status = 200, description = "Id of the queued test delivery", body = Uuid)
230 )
231)]
232async fn send_test_email(
233 email_template_id: web::Path<Uuid>,
234 payload: web::Json<EmailTemplatePreviewRequest>,
235 pool: web::Data<PgPool>,
236 app_conf: web::Data<ApplicationConfiguration>,
237 user: AuthUser,
238) -> ControllerResult<web::Json<Uuid>> {
239 let mut conn = pool.acquire().await?;
240 let template =
241 models::email_templates::get_email_template(&mut conn, *email_template_id).await?;
242 let token = authorize_template_edit(&mut conn, &user, &template).await?;
243 parse_email_content(&payload.content)?;
244 let placeholders = serde_json::to_value(
245 template
246 .email_template_type
247 .sample_placeholders(&app_conf.base_url),
248 )?;
249 let delivery_id = models::email_deliveries::insert_test_email_delivery_within_limit(
250 &mut conn,
251 user.id,
252 template.id,
253 &payload.subject,
254 &payload.content,
255 &placeholders,
256 MAX_TEST_SENDS_PER_MINUTE,
257 )
258 .await?
259 .ok_or_else(|| {
260 ControllerError::new(
261 ControllerErrorType::TooManyRequests,
262 "Too many test emails sent in the last minute. Try again shortly.".to_string(),
263 None,
264 )
265 })?;
266 token.authorized_ok(web::Json(delivery_id))
267}
268
269pub fn _add_routes(cfg: &mut ServiceConfig) {
277 cfg.route("/{email_template_id}", web::get().to(get_email_template))
278 .route("/{email_template_id}", web::put().to(update_email_template))
279 .route(
280 "/{email_template_id}",
281 web::delete().to(delete_email_template),
282 )
283 .route(
284 "/{email_template_id}/preview",
285 web::post().to(preview_email_template),
286 )
287 .route(
288 "/{email_template_id}/send-test",
289 web::post().to(send_test_email),
290 );
291}