Skip to main content

aetheris_client_wasm/
auth.rs

1// Proto message types come from this crate's own build.rs (prost-only, no service stubs).
2#![allow(clippy::missing_errors_doc)]
3use crate::auth_proto::{
4    ClientMetadata, LoginRequest, LoginResponse, LogoutRequest, LogoutResponse, OtpLoginRequest,
5    OtpRequest, OtpRequestAck, login_request,
6};
7use tonic::codegen::StdError;
8use tonic_web_wasm_client::Client;
9use tracing::{error, info};
10
11/// Phase 1 WASM-compatible gRPC client for `AuthService`.
12///
13/// This is a hand-written replacement for the tonic-generated client stub.
14pub struct AuthServiceClient<T> {
15    inner: tonic::client::Grpc<T>,
16}
17
18impl<T> AuthServiceClient<T>
19where
20    T: tonic::client::GrpcService<tonic::body::Body>,
21    T::Error: Into<StdError>,
22    T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
23    <T::ResponseBody as tonic::codegen::Body>::Error: Into<StdError> + Send,
24{
25    pub fn new(inner: T) -> Self {
26        Self {
27            inner: tonic::client::Grpc::new(inner),
28        }
29    }
30
31    pub async fn request_otp(
32        &mut self,
33        request: impl tonic::IntoRequest<OtpRequest>,
34    ) -> Result<tonic::Response<OtpRequestAck>, tonic::Status> {
35        self.inner.ready().await.map_err(|e| {
36            tonic::Status::unknown(format!(
37                "Service was not ready: {}",
38                Into::<StdError>::into(e)
39            ))
40        })?;
41        let codec = tonic_prost::ProstCodec::<OtpRequest, OtpRequestAck>::default();
42        let path = tonic::codegen::http::uri::PathAndQuery::from_static(
43            "/aetheris.auth.v1.AuthService/RequestOtp",
44        );
45        self.inner.unary(request.into_request(), path, codec).await
46    }
47
48    pub async fn login(
49        &mut self,
50        request: impl tonic::IntoRequest<LoginRequest>,
51    ) -> Result<tonic::Response<LoginResponse>, tonic::Status> {
52        self.inner.ready().await.map_err(|e| {
53            tonic::Status::unknown(format!(
54                "Service was not ready: {}",
55                Into::<StdError>::into(e)
56            ))
57        })?;
58        let codec = tonic_prost::ProstCodec::<LoginRequest, LoginResponse>::default();
59        let path = tonic::codegen::http::uri::PathAndQuery::from_static(
60            "/aetheris.auth.v1.AuthService/Login",
61        );
62        self.inner.unary(request.into_request(), path, codec).await
63    }
64    pub async fn logout(
65        &mut self,
66        request: impl tonic::IntoRequest<LogoutRequest>,
67    ) -> Result<tonic::Response<LogoutResponse>, tonic::Status> {
68        self.inner.ready().await.map_err(|e| {
69            tonic::Status::unknown(format!(
70                "Service was not ready: {}",
71                Into::<StdError>::into(e)
72            ))
73        })?;
74        let codec = tonic_prost::ProstCodec::<LogoutRequest, LogoutResponse>::default();
75        let path = tonic::codegen::http::uri::PathAndQuery::from_static(
76            "/aetheris.auth.v1.AuthService/Logout",
77        );
78        self.inner.unary(request.into_request(), path, codec).await
79    }
80}
81
82#[allow(clippy::future_not_send)]
83pub async fn request_otp(base_url: String, email: String) -> Result<String, String> {
84    let redacted = email.split('@').nth(1).map_or("[redacted]", |d| d);
85    info!(email_domain = %redacted, "Attempting gRPC OTP request");
86
87    info!(base_url = %base_url, "Initialising gRPC OTP request");
88    let web_client = Client::new(base_url.clone());
89    let mut client = AuthServiceClient::new(web_client);
90
91    let request = tonic::Request::new(OtpRequest { email });
92
93    match client.request_otp(request).await {
94        Ok(response) => {
95            let inner = response.into_inner();
96            info!(request_id = %inner.request_id, "OTP request successful");
97            Ok(inner.request_id)
98        }
99        Err(e) => {
100            let msg = format!("OTP request failed: {}", e.message());
101            error!(error = %msg, code = ?e.code(), "gRPC request rejected");
102            Err(msg)
103        }
104    }
105}
106
107/// Helper for OTP-based login.
108#[allow(clippy::future_not_send)]
109pub async fn login_with_otp(
110    base_url: String,
111    request_id: String,
112    code: String,
113) -> Result<String, String> {
114    info!("Attempting gRPC OTP login");
115
116    info!(base_url = %base_url, "Initialising gRPC OTP login");
117    let web_client = Client::new(base_url.clone());
118    let mut client = AuthServiceClient::new(web_client);
119
120    let request = tonic::Request::new(LoginRequest {
121        method: Some(login_request::Method::Otp(OtpLoginRequest {
122            request_id,
123            code,
124        })),
125        metadata: Some(ClientMetadata {
126            client_version: env!("CARGO_PKG_VERSION").to_string(),
127            platform: "wasm".to_string(),
128        }),
129    });
130
131    match client.login(request).await {
132        Ok(response) => {
133            let inner = response.into_inner();
134            info!("Login successful!");
135            Ok(inner.session_token)
136        }
137        Err(e) => {
138            let msg = format!("Login failed: {}", e.message());
139            error!(error = %msg, code = ?e.code(), "gRPC login rejected");
140            Err(msg)
141        }
142    }
143}
144
145#[allow(clippy::future_not_send)]
146pub async fn logout(base_url: String, session_token: String) -> Result<(), String> {
147    info!("Attempting gRPC logout");
148
149    let web_client = Client::new(base_url);
150    let mut client = AuthServiceClient::new(web_client);
151
152    let request = tonic::Request::new(LogoutRequest { session_token });
153
154    match client.logout(request).await {
155        Ok(response) => {
156            let inner = response.into_inner();
157            if inner.revoked {
158                info!("Logout successful!");
159                Ok(())
160            } else {
161                let msg = "Logout failed: token not revoked by server".to_string();
162                error!(error = %msg, "gRPC logout did not revoke token");
163                Err(msg)
164            }
165        }
166        Err(e) => {
167            let msg = format!("Logout failed: {}", e.message());
168            error!(error = %msg, code = ?e.code(), "gRPC logout rejected");
169            Err(msg)
170        }
171    }
172}