Files
psg-backchannel/backchannel-server/src/handlers/auth.rs
2026-02-19 13:51:07 +08:00

125 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 232 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)
}