use std::sync::Arc; use backchannel_common::protocol::ServerMessage; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use crate::auth::{password, token}; use crate::db::users; use crate::error::{Result, ServerError}; use crate::state::{AppState, ChannelBroadcast}; use crate::ws::session::Session; pub async fn handle_login( session: &mut Session, state: &Arc, username: String, password_plain: String, identity_pubkey: String, ) -> Result<()> { let user = users::find_by_username(&state.db, &username) .await? .ok_or(ServerError::Unauthorized)?; password::verify(&password_plain, &user.password_hash)?; let user_id = user.uuid()?; // Update identity key on every login to allow transparent key rotation. users::update_identity_key(&state.db, user_id, &identity_pubkey).await?; let jwt = token::issue(&state.db, user_id, &username, &state.jwt_secret).await?; let jti = extract_jti(&jwt, &state.jwt_secret)?; session.user_id = Some(user_id); session.username = Some(username.clone()); session.jti = Some(jti); state.register_session(session.conn_id, user_id, username.clone(), session.tx.clone()).await; let _ = state.channel_broadcast.send(ChannelBroadcast { message: ServerMessage::UserOnline { user_id, username: username.clone() }, }); session.send(ServerMessage::AuthSuccess { user_id, username, session_token: jwt }) } pub async fn handle_register( session: &mut Session, state: &Arc, username: String, password_plain: String, identity_pubkey: String, ) -> Result<()> { if username.len() < 2 || username.len() > 32 { return Err(ServerError::BadRequest("Username must be 2–32 characters".into())); } if password_plain.len() < 8 { return Err(ServerError::BadRequest("Password must be at least 8 characters".into())); } if users::username_exists(&state.db, &username).await? { return Err(ServerError::BadRequest("Username already taken".into())); } let hash = password::hash(&password_plain)?; let user_id = users::create(&state.db, &username, &hash, &identity_pubkey).await?; users::assign_member_role(&state.db, user_id).await?; let jwt = token::issue(&state.db, user_id, &username, &state.jwt_secret).await?; let jti = extract_jti(&jwt, &state.jwt_secret)?; session.user_id = Some(user_id); session.username = Some(username.clone()); session.jti = Some(jti); state.register_session(session.conn_id, user_id, username.clone(), session.tx.clone()).await; let _ = state.channel_broadcast.send(ChannelBroadcast { message: ServerMessage::UserOnline { user_id, username: username.clone() }, }); session.send(ServerMessage::AuthSuccess { user_id, username, session_token: jwt }) } pub async fn handle_resume( session: &mut Session, state: &Arc, token_str: String, ) -> Result<()> { let claims = token::validate(&state.db, &token_str, &state.jwt_secret).await?; let user_id = claims.user_id()?; session.user_id = Some(user_id); session.username = Some(claims.username.clone()); session.jti = Some(claims.jti.clone()); state.register_session(session.conn_id, user_id, claims.username.clone(), session.tx.clone()).await; let _ = state.channel_broadcast.send(ChannelBroadcast { message: ServerMessage::UserOnline { user_id, username: claims.username.clone() }, }); session.send(ServerMessage::AuthSuccess { user_id, username: claims.username, session_token: token_str, }) } pub async fn handle_logout(session: &mut Session, state: &Arc) -> Result<()> { session.require_auth()?; if let Some(jti) = session.jti.take() { token::revoke(&state.db, &jti).await?; } session.user_id = None; session.username = None; Ok(()) } /// Extract the JTI claim from a JWT without re-running expiry validation. /// Used immediately after `token::issue` to store the JTI in the session. fn extract_jti(jwt: &str, secret: &[u8]) -> Result { let mut v = Validation::new(Algorithm::HS256); v.validate_exp = false; let data = decode::(jwt, &DecodingKey::from_secret(secret), &v)?; Ok(data.claims.jti) }