headless_lms_utils/tmc/
mod.rs1use anyhow::{Context, Result};
2use reqwest::Client;
3use serde_json::json;
4
5use crate::ApplicationConfiguration;
6
7#[derive(Debug, Clone)]
8pub struct TmcClient {
9 client: Client,
10 access_token: String,
11 ratelimit_api_key: String,
12}
13
14pub struct NewUserInfo {
15 pub first_name: String,
16 pub last_name: String,
17 pub email: String,
18 pub password: String,
19 pub password_confirmation: String,
20 pub language: String,
21}
22
23const TMC_API_URL: &str = "https://tmc.mooc.fi/api/v8/users";
24
25impl TmcClient {
26 pub fn new_from_env() -> Result<Self> {
27 let is_dev =
28 cfg!(debug_assertions) || std::env::var("APP_ENV").map_or(true, |v| v == "development");
29
30 let access_token = std::env::var("TMC_ACCESS_TOKEN").unwrap_or_else(|_| {
31 if is_dev {
32 "mock-access-token".to_string()
33 } else {
34 panic!("TMC_ACCESS_TOKEN must be defined in production")
35 }
36 });
37
38 let ratelimit_api_key =
39 std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY").unwrap_or_else(|_| {
40 if is_dev {
41 "mock-api-key".to_string()
42 } else {
43 panic!("RATELIMIT_PROTECTION_SAFE_API_KEY must be defined in production")
44 }
45 });
46
47 if !is_dev {
48 if access_token.trim().is_empty() {
49 anyhow::bail!("TMC_ACCESS_TOKEN cannot be empty");
50 }
51 if ratelimit_api_key.trim().is_empty() {
52 anyhow::bail!("RATELIMIT_PROTECTION_SAFE_API_KEY cannot be empty");
53 }
54 }
55
56 Ok(Self {
57 client: Client::default(),
58 access_token,
59 ratelimit_api_key,
60 })
61 }
62
63 async fn request_with_headers(
64 &self,
65 method: reqwest::Method,
66 url: &str,
67 use_auth: bool,
68 body: Option<serde_json::Value>,
69 ) -> Result<reqwest::Response> {
70 let mut builder = self
71 .client
72 .request(method, url)
73 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", &self.ratelimit_api_key)
74 .header(reqwest::header::CONTENT_TYPE, "application/json")
75 .header(reqwest::header::ACCEPT, "application/json");
76
77 if use_auth {
78 builder = builder.bearer_auth(&self.access_token);
79 }
80
81 if let Some(json_body) = body {
82 builder = builder.json(&json_body);
83 }
84
85 let res = builder
86 .send()
87 .await
88 .context("Failed to send HTTP request")?;
89
90 if res.status().is_success() {
91 Ok(res)
92 } else {
93 let status = res.status();
94 let error_text = res
95 .text()
96 .await
97 .unwrap_or_else(|e| format!("(Failed to read error body: {e})"));
98
99 warn!(
100 "Request to {} failed with status {}: {}",
101 url, status, error_text
102 );
103
104 Err(anyhow::anyhow!(
105 "Request failed with status {}: {}",
106 status,
107 error_text
108 ))
109 }
110 }
111
112 pub async fn update_user_information(
113 &self,
114 first_name: String,
115 last_name: String,
116 email: Option<String>,
117 user_upstream_id: String,
118 ) -> Result<()> {
119 let mut user_obj = serde_json::Map::new();
120 let mut user_field_obj = serde_json::Map::new();
121
122 if let Some(email) = email {
123 user_obj.insert("email".to_string(), serde_json::Value::String(email));
124 }
125
126 user_field_obj.insert(
127 "first_name".to_string(),
128 serde_json::Value::String(first_name),
129 );
130 user_field_obj.insert(
131 "last_name".to_string(),
132 serde_json::Value::String(last_name),
133 );
134
135 let mut payload = serde_json::Map::new();
136
137 if !user_obj.is_empty() {
138 payload.insert("user".to_string(), serde_json::Value::Object(user_obj));
139 }
140
141 payload.insert(
142 "user_field".to_string(),
143 serde_json::Value::Object(user_field_obj),
144 );
145
146 let payload_value = serde_json::Value::Object(payload);
147
148 let url = format!("{}/{}", TMC_API_URL, user_upstream_id);
149
150 self.request_with_headers(reqwest::Method::PUT, &url, true, Some(payload_value))
151 .await
152 .map(|_| ())
153 }
154
155 pub async fn post_new_user_to_moocfi(
156 &self,
157 user_info: NewUserInfo,
158 app_conf: &ApplicationConfiguration,
159 ) -> Result<()> {
160 let payload = json!({
161 "user": {
162 "email": user_info.email,
163 "first_name": user_info.first_name,
164 "last_name": user_info.last_name,
165 "password": user_info.password,
166 "password_confirmation": user_info.password_confirmation
167 },
168 "user_field": {
169 "first_name": user_info.first_name,
170 "last_name": user_info.last_name
171 },
172 "origin": app_conf.tmc_account_creation_origin,
173 "language": user_info.language
174 });
175
176 self.request_with_headers(reqwest::Method::POST, TMC_API_URL, false, Some(payload))
177 .await
178 .map(|_| ())
179 }
180
181 pub fn mock_for_test() -> Self {
182 Self {
183 client: Client::default(),
184 access_token: "mock-token".to_string(),
185 ratelimit_api_key: "mock-api-key".to_string(),
186 }
187 }
188}