Skip to main content

chasm/api/
auth.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! Authentication and Authorization Module
4//!
5//! Provides JWT-based authentication, user management, and subscription handling
6//! for the CSM ecosystem (csm-rust, csm-web, csm-app).
7
8use actix_web::{dev::Payload, web, FromRequest, HttpMessage, HttpRequest, HttpResponse};
9use chrono::{Duration, Utc};
10use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
11use rusqlite::OptionalExtension;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14use std::future::{ready, Ready};
15use uuid::Uuid;
16
17// =============================================================================
18// Configuration
19// =============================================================================
20
21/// JWT secret key - in production, this should come from environment variables
22const JWT_SECRET: &[u8] = b"csm_jwt_secret_key_change_in_production_2024";
23const JWT_EXPIRY_HOURS: i64 = 24;
24const REFRESH_TOKEN_EXPIRY_DAYS: i64 = 30;
25
26// =============================================================================
27// Subscription Tiers
28// =============================================================================
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32#[derive(Default)]
33pub enum SubscriptionTier {
34    /// Free tier - basic sync, limited features
35    #[default]
36    Free,
37    /// Pro tier - full sync, all features, priority support
38    Pro,
39    /// Enterprise tier - team features, admin controls, SLA
40    Enterprise,
41}
42
43impl SubscriptionTier {
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            Self::Free => "free",
47            Self::Pro => "pro",
48            Self::Enterprise => "enterprise",
49        }
50    }
51
52    pub fn from_str(s: &str) -> Option<Self> {
53        match s.to_lowercase().as_str() {
54            "free" => Some(Self::Free),
55            "pro" => Some(Self::Pro),
56            "enterprise" => Some(Self::Enterprise),
57            _ => None,
58        }
59    }
60}
61
62// =============================================================================
63// Subscription Features
64// =============================================================================
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SubscriptionFeatures {
68    /// Maximum number of workspaces
69    pub max_workspaces: Option<u32>,
70    /// Maximum number of sessions
71    pub max_sessions: Option<u32>,
72    /// Maximum number of agents
73    pub max_agents: Option<u32>,
74    /// Maximum number of swarms
75    pub max_swarms: Option<u32>,
76    /// Real-time sync enabled
77    pub realtime_sync: bool,
78    /// Cross-device sync enabled
79    pub cross_device_sync: bool,
80    /// Priority support
81    pub priority_support: bool,
82    /// Team collaboration features
83    pub team_collaboration: bool,
84    /// Advanced analytics
85    pub analytics: bool,
86    /// API access
87    pub api_access: bool,
88    /// Custom integrations
89    pub custom_integrations: bool,
90}
91
92impl SubscriptionFeatures {
93    pub fn for_tier(tier: SubscriptionTier) -> Self {
94        match tier {
95            SubscriptionTier::Free => Self {
96                max_workspaces: Some(10),
97                max_sessions: Some(100),
98                max_agents: Some(5),
99                max_swarms: Some(1),
100                realtime_sync: true,
101                cross_device_sync: true,
102                priority_support: false,
103                team_collaboration: false,
104                analytics: false,
105                api_access: false,
106                custom_integrations: false,
107            },
108            SubscriptionTier::Pro => Self {
109                max_workspaces: Some(100),
110                max_sessions: None, // Unlimited
111                max_agents: Some(100),
112                max_swarms: Some(20),
113                realtime_sync: true,
114                cross_device_sync: true,
115                priority_support: true,
116                team_collaboration: false,
117                analytics: true,
118                api_access: true,
119                custom_integrations: false,
120            },
121            SubscriptionTier::Enterprise => Self {
122                max_workspaces: None, // Unlimited
123                max_sessions: None,
124                max_agents: None,
125                max_swarms: None,
126                realtime_sync: true,
127                cross_device_sync: true,
128                priority_support: true,
129                team_collaboration: true,
130                analytics: true,
131                api_access: true,
132                custom_integrations: true,
133            },
134        }
135    }
136}
137
138// =============================================================================
139// User Model
140// =============================================================================
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct User {
144    pub id: String,
145    pub email: String,
146    pub display_name: String,
147    #[serde(skip_serializing)]
148    #[allow(dead_code)]
149    pub password_hash: String,
150    pub subscription_tier: SubscriptionTier,
151    pub subscription_expires_at: Option<i64>,
152    pub created_at: i64,
153    pub updated_at: i64,
154    pub last_login_at: Option<i64>,
155    pub email_verified: bool,
156    pub avatar_url: Option<String>,
157    pub metadata: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct PublicUser {
162    pub id: String,
163    pub email: String,
164    pub display_name: String,
165    pub subscription_tier: SubscriptionTier,
166    pub subscription_expires_at: Option<i64>,
167    pub created_at: i64,
168    pub email_verified: bool,
169    pub avatar_url: Option<String>,
170    pub features: SubscriptionFeatures,
171}
172
173impl From<User> for PublicUser {
174    fn from(user: User) -> Self {
175        Self {
176            id: user.id,
177            email: user.email,
178            display_name: user.display_name,
179            subscription_tier: user.subscription_tier,
180            subscription_expires_at: user.subscription_expires_at,
181            created_at: user.created_at,
182            email_verified: user.email_verified,
183            avatar_url: user.avatar_url,
184            features: SubscriptionFeatures::for_tier(user.subscription_tier),
185        }
186    }
187}
188
189// =============================================================================
190// JWT Claims
191// =============================================================================
192
193#[derive(Debug, Serialize, Deserialize)]
194pub struct Claims {
195    /// Subject (user ID)
196    pub sub: String,
197    /// Email
198    pub email: String,
199    /// Subscription tier
200    pub tier: String,
201    /// Issued at
202    pub iat: i64,
203    /// Expiration
204    pub exp: i64,
205    /// Token type (access or refresh)
206    pub token_type: String,
207}
208
209// =============================================================================
210// Auth State (for middleware)
211// =============================================================================
212
213#[derive(Debug, Clone)]
214#[allow(dead_code)]
215pub struct AuthenticatedUser {
216    pub user_id: String,
217    pub email: String,
218    pub tier: SubscriptionTier,
219}
220
221impl FromRequest for AuthenticatedUser {
222    type Error = actix_web::Error;
223    type Future = Ready<Result<Self, Self::Error>>;
224
225    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
226        // Try to get user from request extensions (set by middleware)
227        if let Some(user) = req.extensions().get::<AuthenticatedUser>() {
228            return ready(Ok(user.clone()));
229        }
230
231        // Try to extract from Authorization header directly
232        if let Some(auth_header) = req.headers().get("Authorization") {
233            if let Ok(auth_str) = auth_header.to_str() {
234                if let Some(token) = auth_str.strip_prefix("Bearer ") {
235                    if let Some(user) = validate_token(token) {
236                        return ready(Ok(user));
237                    }
238                }
239            }
240        }
241
242        ready(Ok(AuthenticatedUser {
243            user_id: String::new(),
244            email: String::new(),
245            tier: SubscriptionTier::Free,
246        }))
247    }
248}
249
250// =============================================================================
251// Request/Response Types
252// =============================================================================
253
254#[derive(Debug, Deserialize)]
255pub struct RegisterRequest {
256    pub email: String,
257    pub password: String,
258    pub display_name: String,
259}
260
261#[derive(Debug, Deserialize)]
262pub struct LoginRequest {
263    pub email: String,
264    pub password: String,
265}
266
267#[derive(Debug, Deserialize)]
268pub struct RefreshTokenRequest {
269    pub refresh_token: String,
270}
271
272#[derive(Debug, Serialize)]
273pub struct AuthResponse {
274    pub user: PublicUser,
275    pub access_token: String,
276    pub refresh_token: String,
277    pub expires_in: i64,
278}
279
280#[derive(Debug, Deserialize)]
281pub struct UpdateProfileRequest {
282    pub display_name: Option<String>,
283    pub avatar_url: Option<String>,
284}
285
286#[derive(Debug, Deserialize)]
287pub struct ChangePasswordRequest {
288    pub current_password: String,
289    pub new_password: String,
290}
291
292#[derive(Debug, Deserialize)]
293pub struct UpgradeSubscriptionRequest {
294    pub tier: String,
295    /// Payment token from payment provider (Stripe, etc.)
296    #[allow(dead_code)]
297    pub payment_token: Option<String>,
298}
299
300// =============================================================================
301// Helper Functions
302// =============================================================================
303
304/// Hash a password using SHA-256 with salt
305pub fn hash_password(password: &str, salt: &str) -> String {
306    let mut hasher = Sha256::new();
307    hasher.update(password.as_bytes());
308    hasher.update(salt.as_bytes());
309    format!("{:x}", hasher.finalize())
310}
311
312/// Generate a JWT access token
313pub fn generate_access_token(user: &User) -> Option<String> {
314    let now = Utc::now();
315    let exp = now + Duration::hours(JWT_EXPIRY_HOURS);
316
317    let claims = Claims {
318        sub: user.id.clone(),
319        email: user.email.clone(),
320        tier: user.subscription_tier.as_str().to_string(),
321        iat: now.timestamp(),
322        exp: exp.timestamp(),
323        token_type: "access".to_string(),
324    };
325
326    encode(
327        &Header::default(),
328        &claims,
329        &EncodingKey::from_secret(JWT_SECRET),
330    )
331    .ok()
332}
333
334/// Generate a refresh token
335pub fn generate_refresh_token(user: &User) -> Option<String> {
336    let now = Utc::now();
337    let exp = now + Duration::days(REFRESH_TOKEN_EXPIRY_DAYS);
338
339    let claims = Claims {
340        sub: user.id.clone(),
341        email: user.email.clone(),
342        tier: user.subscription_tier.as_str().to_string(),
343        iat: now.timestamp(),
344        exp: exp.timestamp(),
345        token_type: "refresh".to_string(),
346    };
347
348    encode(
349        &Header::default(),
350        &claims,
351        &EncodingKey::from_secret(JWT_SECRET),
352    )
353    .ok()
354}
355
356/// Validate a JWT token and return the authenticated user
357pub fn validate_token(token: &str) -> Option<AuthenticatedUser> {
358    let validation = Validation::new(Algorithm::HS256);
359
360    let token_data =
361        decode::<Claims>(token, &DecodingKey::from_secret(JWT_SECRET), &validation).ok()?;
362
363    let claims = token_data.claims;
364
365    // Check if token is expired
366    if claims.exp < Utc::now().timestamp() {
367        return None;
368    }
369
370    Some(AuthenticatedUser {
371        user_id: claims.sub,
372        email: claims.email,
373        tier: SubscriptionTier::from_str(&claims.tier).unwrap_or_default(),
374    })
375}
376
377/// Validate refresh token specifically
378pub fn validate_refresh_token(token: &str) -> Option<AuthenticatedUser> {
379    let validation = Validation::new(Algorithm::HS256);
380
381    let token_data =
382        decode::<Claims>(token, &DecodingKey::from_secret(JWT_SECRET), &validation).ok()?;
383
384    let claims = token_data.claims;
385
386    // Must be a refresh token
387    if claims.token_type != "refresh" {
388        return None;
389    }
390
391    // Check if token is expired
392    if claims.exp < Utc::now().timestamp() {
393        return None;
394    }
395
396    Some(AuthenticatedUser {
397        user_id: claims.sub,
398        email: claims.email,
399        tier: SubscriptionTier::from_str(&claims.tier).unwrap_or_default(),
400    })
401}
402
403// =============================================================================
404// Database Operations
405// =============================================================================
406
407/// Initialize auth tables in the database
408pub fn init_auth_tables(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
409    conn.execute(
410        "CREATE TABLE IF NOT EXISTS users (
411            id TEXT PRIMARY KEY,
412            email TEXT UNIQUE NOT NULL,
413            display_name TEXT NOT NULL,
414            password_hash TEXT NOT NULL,
415            password_salt TEXT NOT NULL,
416            subscription_tier TEXT NOT NULL DEFAULT 'free',
417            subscription_expires_at INTEGER,
418            created_at INTEGER NOT NULL,
419            updated_at INTEGER NOT NULL,
420            last_login_at INTEGER,
421            email_verified INTEGER DEFAULT 0,
422            avatar_url TEXT,
423            metadata TEXT
424        )",
425        [],
426    )?;
427
428    conn.execute(
429        "CREATE TABLE IF NOT EXISTS refresh_tokens (
430            id TEXT PRIMARY KEY,
431            user_id TEXT NOT NULL,
432            token_hash TEXT NOT NULL,
433            expires_at INTEGER NOT NULL,
434            created_at INTEGER NOT NULL,
435            revoked INTEGER DEFAULT 0,
436            device_info TEXT,
437            FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
438        )",
439        [],
440    )?;
441
442    conn.execute(
443        "CREATE TABLE IF NOT EXISTS user_sessions (
444            id TEXT PRIMARY KEY,
445            user_id TEXT NOT NULL,
446            device_id TEXT,
447            device_name TEXT,
448            platform TEXT,
449            ip_address TEXT,
450            last_active_at INTEGER NOT NULL,
451            created_at INTEGER NOT NULL,
452            FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
453        )",
454        [],
455    )?;
456
457    // Create indexes
458    conn.execute(
459        "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
460        [],
461    )?;
462    conn.execute(
463        "CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id)",
464        [],
465    )?;
466    conn.execute(
467        "CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id)",
468        [],
469    )?;
470
471    Ok(())
472}
473
474// =============================================================================
475// HTTP Handlers
476// =============================================================================
477
478/// Register a new user
479pub async fn register(
480    app_state: web::Data<crate::api::state::AppState>,
481    body: web::Json<RegisterRequest>,
482) -> HttpResponse {
483    let db = app_state.db.lock().unwrap();
484
485    // Initialize tables if needed
486    if let Err(e) = init_auth_tables(&db.conn) {
487        return HttpResponse::InternalServerError().json(serde_json::json!({
488            "success": false,
489            "error": format!("Database error: {}", e)
490        }));
491    }
492
493    // Validate input
494    if body.email.is_empty() || !body.email.contains('@') {
495        return HttpResponse::BadRequest().json(serde_json::json!({
496            "success": false,
497            "error": "Invalid email address"
498        }));
499    }
500
501    if body.password.len() < 8 {
502        return HttpResponse::BadRequest().json(serde_json::json!({
503            "success": false,
504            "error": "Password must be at least 8 characters"
505        }));
506    }
507
508    if body.display_name.is_empty() {
509        return HttpResponse::BadRequest().json(serde_json::json!({
510            "success": false,
511            "error": "Display name is required"
512        }));
513    }
514
515    // Check if email already exists
516    let existing: rusqlite::Result<Option<String>> = db
517        .conn
518        .query_row(
519            "SELECT id FROM users WHERE email = ?1",
520            rusqlite::params![body.email.to_lowercase()],
521            |row| row.get(0),
522        )
523        .optional();
524
525    if let Ok(Some(_)) = existing {
526        return HttpResponse::Conflict().json(serde_json::json!({
527            "success": false,
528            "error": "Email already registered"
529        }));
530    }
531
532    // Create user
533    let user_id = Uuid::new_v4().to_string();
534    let salt = Uuid::new_v4().to_string();
535    let password_hash = hash_password(&body.password, &salt);
536    let now = Utc::now().timestamp();
537
538    let result = db.conn.execute(
539        "INSERT INTO users (id, email, display_name, password_hash, password_salt, 
540                           subscription_tier, created_at, updated_at)
541         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
542        rusqlite::params![
543            user_id,
544            body.email.to_lowercase(),
545            body.display_name,
546            password_hash,
547            salt,
548            "free",
549            now,
550            now,
551        ],
552    );
553
554    if let Err(e) = result {
555        return HttpResponse::InternalServerError().json(serde_json::json!({
556            "success": false,
557            "error": format!("Failed to create user: {}", e)
558        }));
559    }
560
561    // Create user object for token generation
562    let user = User {
563        id: user_id,
564        email: body.email.to_lowercase(),
565        display_name: body.display_name.clone(),
566        password_hash,
567        subscription_tier: SubscriptionTier::Free,
568        subscription_expires_at: None,
569        created_at: now,
570        updated_at: now,
571        last_login_at: None,
572        email_verified: false,
573        avatar_url: None,
574        metadata: None,
575    };
576
577    // Generate tokens
578    let access_token = match generate_access_token(&user) {
579        Some(t) => t,
580        None => {
581            return HttpResponse::InternalServerError().json(serde_json::json!({
582                "success": false,
583                "error": "Failed to generate access token"
584            }))
585        }
586    };
587
588    let refresh_token = match generate_refresh_token(&user) {
589        Some(t) => t,
590        None => {
591            return HttpResponse::InternalServerError().json(serde_json::json!({
592                "success": false,
593                "error": "Failed to generate refresh token"
594            }))
595        }
596    };
597
598    HttpResponse::Created().json(serde_json::json!({
599        "success": true,
600        "data": AuthResponse {
601            user: PublicUser::from(user),
602            access_token,
603            refresh_token,
604            expires_in: JWT_EXPIRY_HOURS * 3600,
605        }
606    }))
607}
608
609/// Login with email and password
610pub async fn login(
611    app_state: web::Data<crate::api::state::AppState>,
612    body: web::Json<LoginRequest>,
613) -> HttpResponse {
614    let db = app_state.db.lock().unwrap();
615
616    // Initialize tables if needed
617    if let Err(e) = init_auth_tables(&db.conn) {
618        return HttpResponse::InternalServerError().json(serde_json::json!({
619            "success": false,
620            "error": format!("Database error: {}", e)
621        }));
622    }
623
624    // Find user by email
625    let user_result: rusqlite::Result<(
626        String,
627        String,
628        String,
629        String,
630        String,
631        String,
632        Option<i64>,
633        i64,
634        i64,
635        Option<i64>,
636        i32,
637        Option<String>,
638    )> = db.conn.query_row(
639        "SELECT id, email, display_name, password_hash, password_salt, 
640                    subscription_tier, subscription_expires_at, created_at, updated_at,
641                    last_login_at, email_verified, avatar_url
642             FROM users WHERE email = ?1",
643        rusqlite::params![body.email.to_lowercase()],
644        |row| {
645            Ok((
646                row.get(0)?,
647                row.get(1)?,
648                row.get(2)?,
649                row.get(3)?,
650                row.get(4)?,
651                row.get(5)?,
652                row.get(6)?,
653                row.get(7)?,
654                row.get(8)?,
655                row.get(9)?,
656                row.get(10)?,
657                row.get(11)?,
658            ))
659        },
660    );
661
662    let (
663        id,
664        email,
665        display_name,
666        stored_hash,
667        salt,
668        tier_str,
669        sub_expires,
670        created_at,
671        updated_at,
672        _last_login,
673        verified,
674        avatar,
675    ) = match user_result {
676        Ok(data) => data,
677        Err(_) => {
678            return HttpResponse::Unauthorized().json(serde_json::json!({
679                "success": false,
680                "error": "Invalid email or password"
681            }))
682        }
683    };
684
685    // Verify password
686    let provided_hash = hash_password(&body.password, &salt);
687    if provided_hash != stored_hash {
688        return HttpResponse::Unauthorized().json(serde_json::json!({
689            "success": false,
690            "error": "Invalid email or password"
691        }));
692    }
693
694    // Update last login time
695    let now = Utc::now().timestamp();
696    let _ = db.conn.execute(
697        "UPDATE users SET last_login_at = ?1 WHERE id = ?2",
698        rusqlite::params![now, id],
699    );
700
701    let user = User {
702        id,
703        email,
704        display_name,
705        password_hash: stored_hash,
706        subscription_tier: SubscriptionTier::from_str(&tier_str).unwrap_or_default(),
707        subscription_expires_at: sub_expires,
708        created_at,
709        updated_at,
710        last_login_at: Some(now),
711        email_verified: verified == 1,
712        avatar_url: avatar,
713        metadata: None,
714    };
715
716    // Generate tokens
717    let access_token = match generate_access_token(&user) {
718        Some(t) => t,
719        None => {
720            return HttpResponse::InternalServerError().json(serde_json::json!({
721                "success": false,
722                "error": "Failed to generate access token"
723            }))
724        }
725    };
726
727    let refresh_token = match generate_refresh_token(&user) {
728        Some(t) => t,
729        None => {
730            return HttpResponse::InternalServerError().json(serde_json::json!({
731                "success": false,
732                "error": "Failed to generate refresh token"
733            }))
734        }
735    };
736
737    HttpResponse::Ok().json(serde_json::json!({
738        "success": true,
739        "data": AuthResponse {
740            user: PublicUser::from(user),
741            access_token,
742            refresh_token,
743            expires_in: JWT_EXPIRY_HOURS * 3600,
744        }
745    }))
746}
747
748/// Refresh access token using refresh token
749pub async fn refresh_token(
750    app_state: web::Data<crate::api::state::AppState>,
751    body: web::Json<RefreshTokenRequest>,
752) -> HttpResponse {
753    // Validate refresh token
754    let auth_user = match validate_refresh_token(&body.refresh_token) {
755        Some(u) => u,
756        None => {
757            return HttpResponse::Unauthorized().json(serde_json::json!({
758                "success": false,
759                "error": "Invalid or expired refresh token"
760            }))
761        }
762    };
763
764    let db = app_state.db.lock().unwrap();
765
766    // Get current user data
767    let user_result: rusqlite::Result<(
768        String,
769        String,
770        String,
771        String,
772        Option<i64>,
773        i64,
774        i64,
775        i32,
776        Option<String>,
777    )> = db.conn.query_row(
778        "SELECT id, email, display_name, subscription_tier, subscription_expires_at,
779                    created_at, updated_at, email_verified, avatar_url
780             FROM users WHERE id = ?1",
781        rusqlite::params![auth_user.user_id],
782        |row| {
783            Ok((
784                row.get(0)?,
785                row.get(1)?,
786                row.get(2)?,
787                row.get(3)?,
788                row.get(4)?,
789                row.get(5)?,
790                row.get(6)?,
791                row.get(7)?,
792                row.get(8)?,
793            ))
794        },
795    );
796
797    let (id, email, display_name, tier_str, sub_expires, created_at, updated_at, verified, avatar) =
798        match user_result {
799            Ok(data) => data,
800            Err(_) => {
801                return HttpResponse::NotFound().json(serde_json::json!({
802                    "success": false,
803                    "error": "User not found"
804                }))
805            }
806        };
807
808    let user = User {
809        id,
810        email,
811        display_name,
812        password_hash: String::new(), // Not needed for token generation
813        subscription_tier: SubscriptionTier::from_str(&tier_str).unwrap_or_default(),
814        subscription_expires_at: sub_expires,
815        created_at,
816        updated_at,
817        last_login_at: None,
818        email_verified: verified == 1,
819        avatar_url: avatar,
820        metadata: None,
821    };
822
823    // Generate new tokens
824    let access_token = match generate_access_token(&user) {
825        Some(t) => t,
826        None => {
827            return HttpResponse::InternalServerError().json(serde_json::json!({
828                "success": false,
829                "error": "Failed to generate access token"
830            }))
831        }
832    };
833
834    let new_refresh_token = match generate_refresh_token(&user) {
835        Some(t) => t,
836        None => {
837            return HttpResponse::InternalServerError().json(serde_json::json!({
838                "success": false,
839                "error": "Failed to generate refresh token"
840            }))
841        }
842    };
843
844    HttpResponse::Ok().json(serde_json::json!({
845        "success": true,
846        "data": AuthResponse {
847            user: PublicUser::from(user),
848            access_token,
849            refresh_token: new_refresh_token,
850            expires_in: JWT_EXPIRY_HOURS * 3600,
851        }
852    }))
853}
854
855/// Get current user profile
856pub async fn get_profile(
857    app_state: web::Data<crate::api::state::AppState>,
858    auth_user: AuthenticatedUser,
859) -> HttpResponse {
860    if auth_user.user_id.is_empty() {
861        return HttpResponse::Unauthorized().json(serde_json::json!({
862            "success": false,
863            "error": "Not authenticated"
864        }));
865    }
866
867    let db = app_state.db.lock().unwrap();
868
869    let user_result: rusqlite::Result<(
870        String,
871        String,
872        String,
873        String,
874        Option<i64>,
875        i64,
876        i64,
877        i32,
878        Option<String>,
879    )> = db.conn.query_row(
880        "SELECT id, email, display_name, subscription_tier, subscription_expires_at,
881                    created_at, updated_at, email_verified, avatar_url
882             FROM users WHERE id = ?1",
883        rusqlite::params![auth_user.user_id],
884        |row| {
885            Ok((
886                row.get(0)?,
887                row.get(1)?,
888                row.get(2)?,
889                row.get(3)?,
890                row.get(4)?,
891                row.get(5)?,
892                row.get(6)?,
893                row.get(7)?,
894                row.get(8)?,
895            ))
896        },
897    );
898
899    match user_result {
900        Ok((
901            id,
902            email,
903            display_name,
904            tier_str,
905            sub_expires,
906            created_at,
907            updated_at,
908            verified,
909            avatar,
910        )) => {
911            let user = User {
912                id,
913                email,
914                display_name,
915                password_hash: String::new(),
916                subscription_tier: SubscriptionTier::from_str(&tier_str).unwrap_or_default(),
917                subscription_expires_at: sub_expires,
918                created_at,
919                updated_at,
920                last_login_at: None,
921                email_verified: verified == 1,
922                avatar_url: avatar,
923                metadata: None,
924            };
925
926            HttpResponse::Ok().json(serde_json::json!({
927                "success": true,
928                "data": PublicUser::from(user)
929            }))
930        }
931        Err(_) => HttpResponse::NotFound().json(serde_json::json!({
932            "success": false,
933            "error": "User not found"
934        })),
935    }
936}
937
938/// Update user profile
939pub async fn update_profile(
940    app_state: web::Data<crate::api::state::AppState>,
941    auth_user: AuthenticatedUser,
942    body: web::Json<UpdateProfileRequest>,
943) -> HttpResponse {
944    if auth_user.user_id.is_empty() {
945        return HttpResponse::Unauthorized().json(serde_json::json!({
946            "success": false,
947            "error": "Not authenticated"
948        }));
949    }
950
951    // Scope the db lock so it's released before calling get_profile
952    {
953        let db = app_state.db.lock().unwrap();
954        let now = Utc::now().timestamp();
955
956        // Build update query
957        let mut updates = Vec::new();
958        let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
959
960        if let Some(ref name) = body.display_name {
961            updates.push("display_name = ?");
962            params.push(Box::new(name.clone()));
963        }
964
965        if let Some(ref avatar) = body.avatar_url {
966            updates.push("avatar_url = ?");
967            params.push(Box::new(avatar.clone()));
968        }
969
970        if updates.is_empty() {
971            return HttpResponse::BadRequest().json(serde_json::json!({
972                "success": false,
973                "error": "No fields to update"
974            }));
975        }
976
977        updates.push("updated_at = ?");
978        params.push(Box::new(now));
979
980        let query = format!("UPDATE users SET {} WHERE id = ?", updates.join(", "));
981        params.push(Box::new(auth_user.user_id.clone()));
982
983        let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
984
985        if let Err(e) = db.conn.execute(&query, params_refs.as_slice()) {
986            return HttpResponse::InternalServerError().json(serde_json::json!({
987                "success": false,
988                "error": format!("Failed to update profile: {}", e)
989            }));
990        }
991    } // db lock released here
992
993    // Return updated profile
994    get_profile(app_state, auth_user).await
995}
996
997/// Change password
998pub async fn change_password(
999    app_state: web::Data<crate::api::state::AppState>,
1000    auth_user: AuthenticatedUser,
1001    body: web::Json<ChangePasswordRequest>,
1002) -> HttpResponse {
1003    if auth_user.user_id.is_empty() {
1004        return HttpResponse::Unauthorized().json(serde_json::json!({
1005            "success": false,
1006            "error": "Not authenticated"
1007        }));
1008    }
1009
1010    if body.new_password.len() < 8 {
1011        return HttpResponse::BadRequest().json(serde_json::json!({
1012            "success": false,
1013            "error": "New password must be at least 8 characters"
1014        }));
1015    }
1016
1017    let db = app_state.db.lock().unwrap();
1018
1019    // Get current password hash and salt
1020    let creds: rusqlite::Result<(String, String)> = db.conn.query_row(
1021        "SELECT password_hash, password_salt FROM users WHERE id = ?1",
1022        rusqlite::params![auth_user.user_id],
1023        |row| Ok((row.get(0)?, row.get(1)?)),
1024    );
1025
1026    let (stored_hash, salt) = match creds {
1027        Ok(c) => c,
1028        Err(_) => {
1029            return HttpResponse::NotFound().json(serde_json::json!({
1030                "success": false,
1031                "error": "User not found"
1032            }))
1033        }
1034    };
1035
1036    // Verify current password
1037    let current_hash = hash_password(&body.current_password, &salt);
1038    if current_hash != stored_hash {
1039        return HttpResponse::Unauthorized().json(serde_json::json!({
1040            "success": false,
1041            "error": "Current password is incorrect"
1042        }));
1043    }
1044
1045    // Update password
1046    let new_salt = Uuid::new_v4().to_string();
1047    let new_hash = hash_password(&body.new_password, &new_salt);
1048    let now = Utc::now().timestamp();
1049
1050    if let Err(e) = db.conn.execute(
1051        "UPDATE users SET password_hash = ?1, password_salt = ?2, updated_at = ?3 WHERE id = ?4",
1052        rusqlite::params![new_hash, new_salt, now, auth_user.user_id],
1053    ) {
1054        return HttpResponse::InternalServerError().json(serde_json::json!({
1055            "success": false,
1056            "error": format!("Failed to update password: {}", e)
1057        }));
1058    }
1059
1060    HttpResponse::Ok().json(serde_json::json!({
1061        "success": true,
1062        "message": "Password updated successfully"
1063    }))
1064}
1065
1066/// Get subscription details
1067pub async fn get_subscription(
1068    app_state: web::Data<crate::api::state::AppState>,
1069    auth_user: AuthenticatedUser,
1070) -> HttpResponse {
1071    if auth_user.user_id.is_empty() {
1072        return HttpResponse::Unauthorized().json(serde_json::json!({
1073            "success": false,
1074            "error": "Not authenticated"
1075        }));
1076    }
1077
1078    let db = app_state.db.lock().unwrap();
1079
1080    let result: rusqlite::Result<(String, Option<i64>)> = db.conn.query_row(
1081        "SELECT subscription_tier, subscription_expires_at FROM users WHERE id = ?1",
1082        rusqlite::params![auth_user.user_id],
1083        |row| Ok((row.get(0)?, row.get(1)?)),
1084    );
1085
1086    match result {
1087        Ok((tier_str, expires_at)) => {
1088            let tier = SubscriptionTier::from_str(&tier_str).unwrap_or_default();
1089            let features = SubscriptionFeatures::for_tier(tier);
1090
1091            HttpResponse::Ok().json(serde_json::json!({
1092                "success": true,
1093                "data": {
1094                    "tier": tier_str,
1095                    "expiresAt": expires_at,
1096                    "features": features,
1097                    "isActive": expires_at.map(|exp| exp > Utc::now().timestamp()).unwrap_or(true),
1098                }
1099            }))
1100        }
1101        Err(_) => HttpResponse::NotFound().json(serde_json::json!({
1102            "success": false,
1103            "error": "User not found"
1104        })),
1105    }
1106}
1107
1108/// Upgrade subscription (simulated - in production would integrate with payment provider)
1109pub async fn upgrade_subscription(
1110    app_state: web::Data<crate::api::state::AppState>,
1111    auth_user: AuthenticatedUser,
1112    body: web::Json<UpgradeSubscriptionRequest>,
1113) -> HttpResponse {
1114    if auth_user.user_id.is_empty() {
1115        return HttpResponse::Unauthorized().json(serde_json::json!({
1116            "success": false,
1117            "error": "Not authenticated"
1118        }));
1119    }
1120
1121    let new_tier = match SubscriptionTier::from_str(&body.tier) {
1122        Some(t) => t,
1123        None => {
1124            return HttpResponse::BadRequest().json(serde_json::json!({
1125                "success": false,
1126                "error": "Invalid subscription tier"
1127            }))
1128        }
1129    };
1130
1131    // In production, this would:
1132    // 1. Validate the payment token with Stripe/PayPal
1133    // 2. Create a subscription in the payment provider
1134    // 3. Set up webhooks for subscription events
1135
1136    let db = app_state.db.lock().unwrap();
1137    let now = Utc::now().timestamp();
1138    let expires_at = if new_tier == SubscriptionTier::Free {
1139        None
1140    } else {
1141        // 1 year subscription
1142        Some(now + (365 * 24 * 3600))
1143    };
1144
1145    if let Err(e) = db.conn.execute(
1146        "UPDATE users SET subscription_tier = ?1, subscription_expires_at = ?2, updated_at = ?3 WHERE id = ?4",
1147        rusqlite::params![new_tier.as_str(), expires_at, now, auth_user.user_id],
1148    ) {
1149        return HttpResponse::InternalServerError().json(serde_json::json!({
1150            "success": false,
1151            "error": format!("Failed to upgrade subscription: {}", e)
1152        }));
1153    }
1154
1155    let features = SubscriptionFeatures::for_tier(new_tier);
1156
1157    HttpResponse::Ok().json(serde_json::json!({
1158        "success": true,
1159        "data": {
1160            "tier": new_tier.as_str(),
1161            "expiresAt": expires_at,
1162            "features": features,
1163            "message": "Subscription upgraded successfully"
1164        }
1165    }))
1166}
1167
1168/// Logout (invalidate session)
1169pub async fn logout(_auth_user: AuthenticatedUser) -> HttpResponse {
1170    // In a production system, you would:
1171    // 1. Add the token to a blacklist
1172    // 2. Remove the refresh token from the database
1173    // 3. Clear any server-side session data
1174
1175    HttpResponse::Ok().json(serde_json::json!({
1176        "success": true,
1177        "message": "Logged out successfully"
1178    }))
1179}
1180
1181// =============================================================================
1182// Route Configuration
1183// =============================================================================
1184
1185pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) {
1186    cfg.service(
1187        web::scope("/auth")
1188            .route("/register", web::post().to(register))
1189            .route("/login", web::post().to(login))
1190            .route("/refresh", web::post().to(refresh_token))
1191            .route("/logout", web::post().to(logout))
1192            .route("/profile", web::get().to(get_profile))
1193            .route("/profile", web::put().to(update_profile))
1194            .route("/password", web::put().to(change_password))
1195            .route("/subscription", web::get().to(get_subscription))
1196            .route(
1197                "/subscription/upgrade",
1198                web::post().to(upgrade_subscription),
1199            ),
1200    );
1201}