1 #![allow(unexpected_cfgs, unsafe_op_in_unsafe_fn)]
2 // The #[pyfunction] macro generates FFI wrapper code that calls From<PyErr>::from(err)
3 // on the error path of every wrapped function. Since PyErr: From<PyErr> is the identity
4 // impl, clippy flags it as useless_conversion. We cannot change macro-generated code,
5 // so suppress the lint at crate level.
6 #![allow(clippy::useless_conversion)]
7
8 use pyo3::{
9 create_exception,
10 exceptions::{PyException, PyValueError},
11 prelude::*,
12 };
13 use pythonize::pythonize;
14 use serde::Serialize;
15 use smbcloud_auth_sdk::{
16 client_credentials::ClientCredentials, login::login_with_client as rust_login_with_client,
17 logout::logout_with_client as rust_logout_with_client,
18 me::me_with_client as rust_me_with_client,
19 remove::remove_with_client as rust_remove_with_client,
20 signup::signup_with_client as rust_signup_with_client,
21 };
22 use smbcloud_model::{
23 account::ErrorCode as AccountErrorCode, error_codes::ErrorResponse, login::AccountStatus,
24 };
25 use smbcloud_network::environment::Environment;
26 use std::sync::OnceLock;
27 use tokio::runtime::{Builder, Runtime};
28
29 create_exception!(_native, NativeSdkError, PyException);
30
31 #[derive(Serialize)]
32 struct ErrorPayload {
33 error_code: i32,
34 error_name: String,
35 message: String,
36 }
37
38 #[derive(Serialize)]
39 #[serde(tag = "kind", rename_all = "snake_case")]
40 enum LoginPayload {
41 NotFound,
42 Ready {
43 access_token: String,
44 },
45 Incomplete {
46 status: String,
47 status_code: u32,
48 message: String,
49 },
50 }
51
52 fn runtime() -> &'static Runtime {
53 static RUNTIME: OnceLock<Runtime> = OnceLock::new();
54 RUNTIME.get_or_init(|| {
55 Builder::new_multi_thread()
56 .enable_all()
57 .build()
58 .expect("failed to initialize tokio runtime for smbcloud auth python sdk")
59 })
60 }
61
62 fn parse_env(env: &str) -> PyResult<Environment> {
63 env.parse()
64 .map_err(|_| PyValueError::new_err("env must be 'dev' or 'production'"))
65 }
66
67 fn client_credentials<'a>(app_id: &'a str, app_secret: &'a str) -> ClientCredentials<'a> {
68 ClientCredentials { app_id, app_secret }
69 }
70
71 fn serialize<'py, T>(py: Python<'py>, value: &T) -> PyResult<PyObject>
72 where
73 T: Serialize,
74 {
75 Ok(pythonize(py, value)?.unbind())
76 }
77
78 fn login_payload(status: AccountStatus) -> LoginPayload {
79 match status {
80 AccountStatus::NotFound => LoginPayload::NotFound,
81 AccountStatus::Ready { access_token } => LoginPayload::Ready { access_token },
82 AccountStatus::Incomplete { status } => LoginPayload::Incomplete {
83 status: account_error_name(status).to_string(),
84 status_code: status as u32,
85 message: status.to_string(),
86 },
87 }
88 }
89
90 fn account_error_name(code: AccountErrorCode) -> &'static str {
91 match code {
92 AccountErrorCode::EmailNotFound => "email_not_found",
93 AccountErrorCode::EmailUnverified => "email_unverified",
94 AccountErrorCode::EmailConfirmationFailed => "email_confirmation_failed",
95 AccountErrorCode::PasswordNotSet => "password_not_set",
96 AccountErrorCode::GithubNotLinked => "github_not_linked",
97 AccountErrorCode::EmailAlreadyExist => "email_already_exist",
98 AccountErrorCode::InvalidPassword => "invalid_password",
99 AccountErrorCode::HostedMailAccountUnverified => "hosted_mail_account_unverified",
100 }
101 }
102
103 fn native_error(error: ErrorResponse) -> PyErr {
104 let ErrorResponse::Error {
105 error_code,
106 message,
107 } = error;
108
109 let error_name = error_code.rb_constant_name();
110 let payload = ErrorPayload {
111 error_code: error_code as i32,
112 error_name,
113 message,
114 };
115
116 let message = serde_json::to_string(&payload).unwrap_or_else(|_| {
117 "{\"error_code\":0,\"error_name\":\"Unknown\",\"message\":\"Unknown error.\"}".to_string()
118 });
119
120 NativeSdkError::new_err(message)
121 }
122
123 #[pyfunction]
124 fn signup_with_client(
125 py: Python<'_>,
126 env: &str,
127 app_id: &str,
128 app_secret: &str,
129 email: &str,
130 password: &str,
131 ) -> PyResult<PyObject> {
132 parse_env(env).and_then(|env| {
133 let client = client_credentials(app_id, app_secret);
134 runtime()
135 .block_on(rust_signup_with_client(
136 env,
137 client,
138 email.to_string(),
139 password.to_string(),
140 ))
141 .map_err(native_error)
142 .and_then(|result| serialize(py, &result))
143 })
144 }
145
146 #[pyfunction]
147 fn login_with_client(
148 py: Python<'_>,
149 env: &str,
150 app_id: &str,
151 app_secret: &str,
152 email: &str,
153 password: &str,
154 ) -> PyResult<PyObject> {
155 parse_env(env).and_then(|env| {
156 let client = client_credentials(app_id, app_secret);
157 runtime()
158 .block_on(rust_login_with_client(
159 env,
160 client,
161 email.to_string(),
162 password.to_string(),
163 ))
164 .map(login_payload)
165 .map_err(native_error)
166 .and_then(|result| serialize(py, &result))
167 })
168 }
169
170 #[pyfunction]
171 fn logout_with_client(
172 env: &str,
173 app_id: &str,
174 app_secret: &str,
175 access_token: &str,
176 ) -> PyResult<()> {
177 parse_env(env).and_then(|env| {
178 let client = client_credentials(app_id, app_secret);
179 runtime()
180 .block_on(rust_logout_with_client(
181 env,
182 client,
183 access_token.to_string(),
184 ))
185 .map_err(native_error)
186 })
187 }
188
189 #[pyfunction]
190 fn me_with_client(
191 py: Python<'_>,
192 env: &str,
193 app_id: &str,
194 app_secret: &str,
195 access_token: &str,
196 ) -> PyResult<PyObject> {
197 parse_env(env).and_then(|env| {
198 let client = client_credentials(app_id, app_secret);
199 runtime()
200 .block_on(rust_me_with_client(env, client, access_token))
201 .map_err(native_error)
202 .and_then(|result| serialize(py, &result))
203 })
204 }
205
206 #[pyfunction]
207 fn remove_with_client(
208 env: &str,
209 app_id: &str,
210 app_secret: &str,
211 access_token: &str,
212 ) -> PyResult<()> {
213 parse_env(env).and_then(|env| {
214 let client = client_credentials(app_id, app_secret);
215 runtime()
216 .block_on(rust_remove_with_client(env, client, access_token))
217 .map_err(native_error)
218 })
219 }
220
221 #[pymodule(name = "_native")]
222 fn python_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
223 module.add("NativeSdkError", py.get_type_bound::<NativeSdkError>())?;
224 module.add_function(wrap_pyfunction!(signup_with_client, module)?)?;
225 module.add_function(wrap_pyfunction!(login_with_client, module)?)?;
226 module.add_function(wrap_pyfunction!(logout_with_client, module)?)?;
227 module.add_function(wrap_pyfunction!(me_with_client, module)?)?;
228 module.add_function(wrap_pyfunction!(remove_with_client, module)?)?;
229 Ok(())
230 }