125 lines
4.3 KiB
Rust
125 lines
4.3 KiB
Rust
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<AppState>,
|
||
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<AppState>,
|
||
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<AppState>,
|
||
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<AppState>) -> 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<String> {
|
||
let mut v = Validation::new(Algorithm::HS256);
|
||
v.validate_exp = false;
|
||
let data = decode::<token::Claims>(jwt, &DecodingKey::from_secret(secret), &v)?;
|
||
Ok(data.claims.jti)
|
||
}
|