1use std::sync::Arc;
2
3use axum::{
4 extract::State,
5 response::{IntoResponse, Response},
6 Json,
7};
8use chrono::Utc;
9use serde::{Deserialize, Serialize};
10use tracing::{info, warn};
11
12#[cfg(feature = "openapi")]
13use utoipa::ToSchema;
14
15use crate::errors::{LicenseError, LicenseResult};
16use crate::server::api_error::ApiError;
17use crate::server::database::{Database, License};
18
19#[cfg(feature = "jwt-auth")]
20use crate::server::auth::AuthState;
21
22#[derive(Clone)]
28pub struct AppState {
29 pub db: Arc<Database>,
30 #[cfg(feature = "jwt-auth")]
31 pub auth: AuthState,
32}
33
34impl IntoResponse for LicenseError {
42 fn into_response(self) -> Response {
43 let api_error: ApiError = self.into();
44 api_error.into_response()
45 }
46}
47
48#[derive(Debug, Deserialize, Serialize)]
50#[cfg_attr(feature = "openapi", derive(ToSchema))]
51pub struct LicenseRequest {
52 pub license_id: String,
53 pub client_id: String,
54}
55
56#[derive(Debug, Deserialize, Serialize)]
58#[cfg_attr(feature = "openapi", derive(ToSchema))]
59pub struct LicenseResponse {
60 pub success: bool,
61}
62
63#[derive(Debug, Deserialize, Serialize)]
67#[cfg_attr(feature = "openapi", derive(ToSchema))]
68pub struct HeartbeatRequest {
69 pub license_id: String,
70 pub client_id: String,
71}
72
73#[derive(Debug, Deserialize, Serialize)]
75#[cfg_attr(feature = "openapi", derive(ToSchema))]
76pub struct HeartbeatResponse {
77 pub success: bool,
78}
79
80#[cfg_attr(feature = "openapi", utoipa::path(
87 post,
88 path = "/activate",
89 tag = "legacy",
90 request_body = LicenseRequest,
91 responses(
92 (status = 200, description = "License activated", body = LicenseResponse),
93 (status = 500, description = "Server error"),
94 )
95))]
96pub async fn activate_license_handler(
97 State(state): State<AppState>,
98 Json(payload): Json<LicenseRequest>,
99) -> LicenseResult<Json<LicenseResponse>> {
100 info!(
101 "Activating license_id={} for client_id={}",
102 payload.license_id, payload.client_id
103 );
104
105 let now = Utc::now().naive_utc();
106
107 let license = License {
108 license_id: payload.license_id.clone(),
109 client_id: Some(payload.client_id.clone()),
110 status: "active".to_string(),
111 features: None,
112 issued_at: now,
113 expires_at: None,
114 hardware_id: None,
115 signature: None,
116 last_heartbeat: Some(now),
117 org_id: None,
119 org_name: None,
120 license_key: None,
121 tier: None,
122 device_name: None,
123 device_info: None,
124 bound_at: None,
125 last_seen_at: None,
126 suspended_at: None,
127 revoked_at: None,
128 revoke_reason: None,
129 grace_period_ends_at: None,
130 suspension_message: None,
131 is_blacklisted: None,
132 blacklisted_at: None,
133 blacklist_reason: None,
134 metadata: None,
135 bandwidth_used_bytes: None,
136 bandwidth_limit_bytes: None,
137 quota_exceeded: None,
138 };
139
140 state.db.insert_license(license).await?;
141
142 Ok(Json(LicenseResponse { success: true }))
143}
144
145#[cfg_attr(feature = "openapi", utoipa::path(
154 post,
155 path = "/validate",
156 tag = "legacy",
157 request_body = LicenseRequest,
158 responses(
159 (status = 200, description = "Validation result", body = LicenseResponse),
160 (status = 500, description = "Server error"),
161 )
162))]
163pub async fn validate_license_handler(
164 State(state): State<AppState>,
165 Json(payload): Json<LicenseRequest>,
166) -> LicenseResult<Json<LicenseResponse>> {
167 info!(
168 "Validating license_id={} for client_id={}",
169 payload.license_id, payload.client_id
170 );
171
172 let license_opt = state.db.get_license(&payload.license_id).await?;
173
174 let success = match license_opt {
175 Some(license) => {
176 if license.client_id.as_deref() != Some(payload.client_id.as_str()) {
177 warn!(
178 "Client ID mismatch for license_id={} (expected={:?}, got={})",
179 payload.license_id, license.client_id, payload.client_id
180 );
181 false
182 } else if license.status != "active" {
183 warn!(
184 "License is not active for license_id={} (status={})",
185 payload.license_id, license.status
186 );
187 false
188 } else {
189 true
190 }
191 }
192 None => {
193 warn!("License not found for license_id={}", payload.license_id);
194 false
195 }
196 };
197
198 Ok(Json(LicenseResponse { success }))
199}
200
201#[cfg_attr(feature = "openapi", utoipa::path(
210 post,
211 path = "/deactivate",
212 tag = "legacy",
213 request_body = LicenseRequest,
214 responses(
215 (status = 200, description = "Deactivation result", body = LicenseResponse),
216 (status = 500, description = "Server error"),
217 )
218))]
219pub async fn deactivate_license_handler(
220 State(state): State<AppState>,
221 Json(payload): Json<LicenseRequest>,
222) -> LicenseResult<Json<LicenseResponse>> {
223 info!(
224 "Deactivating license_id={} for client_id={}",
225 payload.license_id, payload.client_id
226 );
227
228 let license_opt = state.db.get_license(&payload.license_id).await?;
229
230 let success = if let Some(mut license) = license_opt {
231 if license.client_id.as_deref() == Some(payload.client_id.as_str()) {
232 license.status = "inactive".to_string();
233 state.db.insert_license(license).await?;
234 info!(
235 "License deactivated for license_id={} client_id={}",
236 payload.license_id, payload.client_id
237 );
238 true
239 } else {
240 warn!(
241 "Client ID mismatch during deactivation for license_id={} (expected={:?}, got={})",
242 payload.license_id, license.client_id, payload.client_id
243 );
244 false
245 }
246 } else {
247 warn!(
248 "Deactivation requested for non-existent license_id={}",
249 payload.license_id
250 );
251 false
252 };
253
254 Ok(Json(LicenseResponse { success }))
255}
256
257#[cfg_attr(feature = "openapi", utoipa::path(
266 post,
267 path = "/heartbeat",
268 tag = "legacy",
269 request_body = HeartbeatRequest,
270 responses(
271 (status = 200, description = "Heartbeat result", body = HeartbeatResponse),
272 (status = 500, description = "Server error"),
273 )
274))]
275pub async fn heartbeat_handler(
276 State(state): State<AppState>,
277 Json(payload): Json<HeartbeatRequest>,
278) -> LicenseResult<Json<HeartbeatResponse>> {
279 info!(
280 "Received heartbeat for license_id={} client_id={}",
281 payload.license_id, payload.client_id
282 );
283
284 let updated = state
285 .db
286 .update_last_heartbeat(&payload.license_id, &payload.client_id)
287 .await?;
288
289 if !updated {
290 warn!(
291 "Failed to update heartbeat: no matching license for license_id={} client_id={}",
292 payload.license_id, payload.client_id
293 );
294 }
295
296 Ok(Json(HeartbeatResponse { success: updated }))
297}
298
299#[cfg_attr(feature = "openapi", utoipa::path(
304 get,
305 path = "/health",
306 tag = "system",
307 responses(
308 (status = 200, description = "Service health status", body = crate::server::logging::HealthResponse),
309 )
310))]
311pub async fn health_handler(
312 State(state): State<AppState>,
313) -> Json<crate::server::logging::HealthResponse> {
314 let db_connected = state.db.health_check().await;
316 let db_type = state.db.db_type();
317
318 Json(crate::server::logging::HealthResponse::healthy(
319 db_connected,
320 db_type,
321 ))
322}