use musli_web04::api::{EncodeBody, Id, MessageId};
use musli_web04::ws::{Incoming, Outgoing};
use super::{
AuthUpdate, DeleteUserRequest, IssueLinkRequest, MeRequest, Request, RevokeAccessRequest,
RevokeLinkRequest, SignOutUserRequest, UserUpdate, UsersRequest,
};
use crate::Auth;
use crate::axum08::SignedIn;
use crate::core::{AuthEvent, Error, Result};
use crate::store::{Session, User};
use crate::types::{
Access, AuthError, CreatedUser, Invalid, NewUser, PasswordChange, UserChange, UserId,
};
#[derive(Clone)]
pub struct Connection {
auth: Auth,
session: String,
user: UserId,
}
impl Connection {
pub fn new(auth: Auth, signed_in: &SignedIn) -> Self {
Self {
auth,
session: signed_in.session.id.clone(),
user: signed_in.user.id,
}
}
pub async fn update(&self, event: &AuthEvent) -> Result<Option<AuthUpdate>> {
let update = match event {
AuthEvent::SessionEnded(_) | AuthEvent::UserSessionsEnded { .. } => return Ok(None),
AuthEvent::UserChanged(info) if info.id == self.user => {
let Some((user, session)) = self.auth.session(&self.session).await? else {
return Ok(None);
};
let info = self.auth.session_info(&user, &session).await?;
return Ok(Some(AuthUpdate::Me(info)));
}
AuthEvent::UserChanged(info) => AuthUpdate::UserChanged(info.clone()),
AuthEvent::UserCreated(info) => AuthUpdate::UserCreated(info.clone()),
AuthEvent::UserDeleted(id) => AuthUpdate::UserDeleted(*id),
};
let Some((user, _)) = self.auth.session(&self.session).await? else {
return Ok(None);
};
if user.access != Access::Active || !user.is_admin() {
return Ok(None);
}
Ok(Some(update))
}
pub async fn lagged(&self) -> Result<Option<AuthUpdate>> {
let Some((user, session)) = self.auth.session(&self.session).await? else {
return Ok(None);
};
let info = self.auth.session_info(&user, &session).await?;
if user.access == Access::Active && user.is_admin() {
Ok(Some(AuthUpdate::Resync(info)))
} else {
Ok(Some(AuthUpdate::Me(info)))
}
}
pub fn handles(id: MessageId) -> bool {
!matches!(Request::from_id(id), Request::Unknown(_))
}
pub async fn handle(
&self,
id: MessageId,
incoming: &mut Incoming<'_>,
outgoing: &mut Outgoing<'_>,
) -> Result<bool> {
let result = self
.dispatch(Request::from_id(id), incoming, outgoing)
.await;
if let Err(error) = &result {
tracing::error!(error = ?error, "auth: socket request {id} failed: {error}");
}
result
}
async fn dispatch(
&self,
request: Request,
incoming: &mut Incoming<'_>,
outgoing: &mut Outgoing<'_>,
) -> Result<bool> {
match request {
Request::Me => {
let Some(MeRequest) = incoming.read() else {
return Ok(false);
};
let response = async {
let (user, session) = self.signed_in().await?;
self.auth.session_info(&user, &session).await
};
respond(outgoing, response.await)
}
Request::UpdateAccount => {
let Some(change) = incoming.read::<UserChange>() else {
return Ok(false);
};
let response = async {
let (user, session) = self.signed_in().await?;
if change.role.is_some() {
return Err(AuthError::Invalid(Invalid::RoleNotChangeable).into());
}
let user = self.auth.update_user(&user, user.id, &change).await?;
self.auth.session_info(&user, &session).await
};
respond(outgoing, response.await)
}
Request::ChangePassword => {
let Some(change) = incoming.read::<PasswordChange>() else {
return Ok(false);
};
let response = async {
let (user, session) = self.signed_in().await?;
let user = self.auth.change_password(&user, &session, &change).await?;
self.auth.session_info(&user, &session).await
};
respond(outgoing, response.await)
}
Request::Users => {
let Some(UsersRequest) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
self.auth.users(&admin).await
};
respond(outgoing, response.await)
}
Request::CreateUser => {
let Some(new) = incoming.read::<NewUser>() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
let (entry, link) = self.auth.create_user(&admin, &new).await?;
Ok(CreatedUser { entry, link })
};
respond(outgoing, response.await)
}
Request::UpdateUser => {
let Some(UserUpdate { id, change }) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
let user = self.auth.update_user(&admin, id, &change).await?;
Ok(user.info())
};
respond(outgoing, response.await)
}
Request::DeleteUser => {
let Some(DeleteUserRequest { id }) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
self.auth.delete_user(&admin, id).await
};
respond(outgoing, response.await)
}
Request::IssueLink => {
let Some(IssueLinkRequest { id }) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
self.auth.issue_link(&admin, id).await
};
respond(outgoing, response.await)
}
Request::RevokeLink => {
let Some(RevokeLinkRequest { id }) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
self.auth.revoke_link(&admin, id).await
};
respond(outgoing, response.await)
}
Request::SignOutUser => {
let Some(SignOutUserRequest { id }) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
self.auth.sign_out_everywhere(&admin, id).await
};
respond(outgoing, response.await)
}
Request::RevokeAccess => {
let Some(RevokeAccessRequest { id }) = incoming.read() else {
return Ok(false);
};
let response = async {
let admin = self.admin().await?;
let user = self.auth.revoke_access(&admin, id).await?;
Ok(user.info())
};
respond(outgoing, response.await)
}
Request::Unknown(_) => Ok(false),
}
}
async fn signed_in(&self) -> Result<(User, Session)> {
match self.auth.session(&self.session).await? {
Some(signed_in) => Ok(signed_in),
None => Err(AuthError::Unauthorized.into()),
}
}
async fn admin(&self) -> Result<User> {
let (user, _) = self.signed_in().await?;
if !user.is_admin() {
return Err(AuthError::Forbidden.into());
}
Ok(user)
}
}
fn respond<T>(outgoing: &mut Outgoing<'_>, result: Result<T>) -> Result<bool>
where
Result<T, AuthError>: EncodeBody,
{
let response = match result {
Ok(value) => Ok(value),
Err(Error::Auth(error)) => Err(error),
Err(error) => return Err(error),
};
outgoing.write(response);
Ok(true)
}