First commit in rust
This commit is contained in:
124
backchannel-server/src/handlers/auth.rs
Normal file
124
backchannel-server/src/handlers/auth.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
||||
107
backchannel-server/src/handlers/channels.rs
Normal file
107
backchannel-server/src/handlers/channels.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use backchannel_common::protocol::ServerMessage;
|
||||
use backchannel_common::types::{has_permission, PermissionFlags};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{channels, messages, roles};
|
||||
use crate::error::{Result, ServerError};
|
||||
use crate::state::{AppState, ChannelBroadcast};
|
||||
use crate::ws::session::Session;
|
||||
|
||||
pub async fn handle_send(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
channel_id: Uuid,
|
||||
content: String,
|
||||
) -> Result<()> {
|
||||
let user_id = session.require_auth()?;
|
||||
let username = session.require_username()?.to_string();
|
||||
|
||||
channels::find_by_id(&state.db, channel_id)
|
||||
.await?
|
||||
.ok_or_else(|| ServerError::NotFound("Channel not found".into()))?;
|
||||
|
||||
let perms = roles::get_user_permissions(&state.db, user_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::SEND_MESSAGES) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
|
||||
let (message_id, timestamp) =
|
||||
messages::insert_channel_message(&state.db, channel_id, user_id, &content).await?;
|
||||
|
||||
let _ = state.channel_broadcast.send(ChannelBroadcast {
|
||||
message: ServerMessage::ChannelMessage {
|
||||
message_id,
|
||||
channel_id,
|
||||
author_id: user_id,
|
||||
author_username: username,
|
||||
content,
|
||||
timestamp,
|
||||
},
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_history(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
channel_id: Uuid,
|
||||
before_message_id: Option<Uuid>,
|
||||
limit: u32,
|
||||
) -> Result<()> {
|
||||
let user_id = session.require_auth()?;
|
||||
let perms = roles::get_user_permissions(&state.db, user_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::READ_MESSAGES) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
|
||||
let msgs = messages::fetch_channel_history(&state.db, channel_id, before_message_id, limit).await?;
|
||||
session.send(ServerMessage::ChannelHistory { channel_id, messages: msgs })
|
||||
}
|
||||
|
||||
pub async fn handle_create(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
name: String,
|
||||
topic: Option<String>,
|
||||
) -> Result<()> {
|
||||
let user_id = session.require_auth()?;
|
||||
let perms = roles::get_user_permissions(&state.db, user_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::MANAGE_CHANNELS) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
if channels::name_exists(&state.db, &name).await? {
|
||||
return Err(ServerError::BadRequest("Channel name already exists".into()));
|
||||
}
|
||||
|
||||
let channel_id = channels::create(&state.db, &name, topic.as_deref(), user_id).await?;
|
||||
|
||||
let _ = state.channel_broadcast.send(ChannelBroadcast {
|
||||
message: ServerMessage::ChannelCreated { channel_id, name, topic },
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_delete(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
channel_id: Uuid,
|
||||
) -> Result<()> {
|
||||
let user_id = session.require_auth()?;
|
||||
let perms = roles::get_user_permissions(&state.db, user_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::MANAGE_CHANNELS) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
if !channels::delete(&state.db, channel_id).await? {
|
||||
return Err(ServerError::NotFound("Channel not found".into()));
|
||||
}
|
||||
|
||||
let _ = state.channel_broadcast.send(ChannelBroadcast {
|
||||
message: ServerMessage::ChannelDeleted { channel_id },
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
115
backchannel-server/src/handlers/dms.rs
Normal file
115
backchannel-server/src/handlers/dms.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use backchannel_common::protocol::ServerMessage;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::messages;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
use crate::ws::session::Session;
|
||||
|
||||
pub async fn handle_init_key_exchange(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
recipient_id: Uuid,
|
||||
sender_ephemeral_pubkey: String,
|
||||
) -> Result<()> {
|
||||
let initiator_id = session.require_auth()?;
|
||||
let initiator_username = session.require_username()?.to_string();
|
||||
|
||||
// Store pending exchange in AppState (cleared once recipient accepts).
|
||||
state
|
||||
.pending_key_exchanges
|
||||
.lock()
|
||||
.await
|
||||
.insert((initiator_id, recipient_id), sender_ephemeral_pubkey.clone());
|
||||
|
||||
// Relay to recipient if they're currently connected.
|
||||
state
|
||||
.send_to_user(
|
||||
recipient_id,
|
||||
&ServerMessage::DmKeyExchangeRequest {
|
||||
initiator_id,
|
||||
initiator_username,
|
||||
sender_ephemeral_pubkey,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_accept_key_exchange(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
initiator_id: Uuid,
|
||||
recipient_ephemeral_pubkey: String,
|
||||
) -> Result<()> {
|
||||
let recipient_id = session.require_auth()?;
|
||||
let recipient_username = session.require_username()?.to_string();
|
||||
|
||||
// Clear the pending exchange.
|
||||
state
|
||||
.pending_key_exchanges
|
||||
.lock()
|
||||
.await
|
||||
.remove(&(initiator_id, recipient_id));
|
||||
|
||||
// Relay response back to initiator.
|
||||
state
|
||||
.send_to_user(
|
||||
initiator_id,
|
||||
&ServerMessage::DmKeyExchangeResponse {
|
||||
recipient_id,
|
||||
recipient_username,
|
||||
recipient_ephemeral_pubkey,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_send_dm(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
recipient_id: Uuid,
|
||||
ciphertext: String,
|
||||
nonce: String,
|
||||
) -> Result<()> {
|
||||
let sender_id = session.require_auth()?;
|
||||
let sender_username = session.require_username()?.to_string();
|
||||
|
||||
let (message_id, timestamp) =
|
||||
messages::insert_dm(&state.db, sender_id, recipient_id, &ciphertext, &nonce).await?;
|
||||
|
||||
let msg = ServerMessage::DirectMessage {
|
||||
message_id,
|
||||
sender_id,
|
||||
sender_username,
|
||||
ciphertext,
|
||||
nonce,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
// Deliver to recipient and to sender's other sessions (multi-client sync).
|
||||
state.send_to_user(recipient_id, &msg).await;
|
||||
state.send_to_user(sender_id, &msg).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_dm_history(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
peer_id: Uuid,
|
||||
before_message_id: Option<Uuid>,
|
||||
limit: u32,
|
||||
) -> Result<()> {
|
||||
let user_id = session.require_auth()?;
|
||||
|
||||
let history =
|
||||
messages::fetch_dm_history(&state.db, user_id, peer_id, before_message_id, limit).await?;
|
||||
|
||||
session.send(ServerMessage::DmHistory { peer_id, messages: history })
|
||||
}
|
||||
4
backchannel-server/src/handlers/mod.rs
Normal file
4
backchannel-server/src/handlers/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod channels;
|
||||
pub mod dms;
|
||||
pub mod roles;
|
||||
87
backchannel-server/src/handlers/roles.rs
Normal file
87
backchannel-server/src/handlers/roles.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use backchannel_common::protocol::ServerMessage;
|
||||
use backchannel_common::types::{has_permission, PermissionFlags};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{keys, roles};
|
||||
use crate::error::{Result, ServerError};
|
||||
use crate::state::{AppState, ChannelBroadcast};
|
||||
use crate::ws::session::Session;
|
||||
|
||||
pub async fn handle_create(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
name: String,
|
||||
permissions: u64,
|
||||
) -> Result<()> {
|
||||
let user_id = session.require_auth()?;
|
||||
let perms = roles::get_user_permissions(&state.db, user_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::MANAGE_ROLES) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
|
||||
let role_id = roles::create(&state.db, &name, permissions).await?;
|
||||
|
||||
let _ = state.channel_broadcast.send(ChannelBroadcast {
|
||||
message: ServerMessage::RoleCreated { role_id, name, permissions },
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_assign(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
) -> Result<()> {
|
||||
let caller_id = session.require_auth()?;
|
||||
let perms = roles::get_user_permissions(&state.db, caller_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::MANAGE_ROLES) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
|
||||
roles::find_by_id(&state.db, role_id)
|
||||
.await?
|
||||
.ok_or_else(|| ServerError::NotFound("Role not found".into()))?;
|
||||
|
||||
roles::assign(&state.db, user_id, role_id).await?;
|
||||
|
||||
let _ = state.channel_broadcast.send(ChannelBroadcast {
|
||||
message: ServerMessage::RoleAssigned { user_id, role_id },
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_revoke(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
) -> Result<()> {
|
||||
let caller_id = session.require_auth()?;
|
||||
let perms = roles::get_user_permissions(&state.db, caller_id).await?;
|
||||
if !has_permission(perms, PermissionFlags::MANAGE_ROLES) {
|
||||
return Err(ServerError::Forbidden);
|
||||
}
|
||||
|
||||
roles::revoke(&state.db, user_id, role_id).await?;
|
||||
|
||||
let _ = state.channel_broadcast.send(ChannelBroadcast {
|
||||
message: ServerMessage::RoleRevoked { user_id, role_id },
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_query_key(
|
||||
session: &Session,
|
||||
state: &Arc<AppState>,
|
||||
user_id: Uuid,
|
||||
) -> Result<()> {
|
||||
session.require_auth()?;
|
||||
let pubkey = keys::get_identity_key(&state.db, user_id).await?;
|
||||
session.send(ServerMessage::IdentityKeyResponse { user_id, identity_pubkey: pubkey })
|
||||
}
|
||||
Reference in New Issue
Block a user