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::PasswordNotSet => "password_not_set",
95 AccountErrorCode::GithubNotLinked => "github_not_linked",
96 }
97 }
98
99 fn native_error(error: ErrorResponse) -> PyErr {
100 let ErrorResponse::Error {
101 error_code,
102 message,
103 } = error;
104
105 let error_name = error_code.rb_constant_name();
106 let payload = ErrorPayload {
107 error_code: error_code as i32,
108 error_name,
109 message,
110 };
111
112 let message = serde_json::to_string(&payload).unwrap_or_else(|_| {
113 "{\"error_code\":0,\"error_name\":\"Unknown\",\"message\":\"Unknown error.\"}".to_string()
114 });
115
116 NativeSdkError::new_err(message)
117 }
118
119 #[pyfunction]
120 fn signup_with_client(
121 py: Python<'_>,
122 env: &str,
123 app_id: &str,
124 app_secret: &str,
125 email: &str,
126 password: &str,
127 ) -> PyResult<PyObject> {
128 parse_env(env).and_then(|env| {
129 let client = client_credentials(app_id, app_secret);
130 runtime()
131 .block_on(rust_signup_with_client(
132 env,
133 client,
134 email.to_string(),
135 password.to_string(),
136 ))
137 .map_err(native_error)
138 .and_then(|result| serialize(py, &result))
139 })
140 }
141
142 #[pyfunction]
143 fn login_with_client(
144 py: Python<'_>,
145 env: &str,
146 app_id: &str,
147 app_secret: &str,
148 email: &str,
149 password: &str,
150 ) -> PyResult<PyObject> {
151 parse_env(env).and_then(|env| {
152 let client = client_credentials(app_id, app_secret);
153 runtime()
154 .block_on(rust_login_with_client(
155 env,
156 client,
157 email.to_string(),
158 password.to_string(),
159 ))
160 .map(login_payload)
161 .map_err(native_error)
162 .and_then(|result| serialize(py, &result))
163 })
164 }
165
166 #[pyfunction]
167 fn logout_with_client(
168 env: &str,
169 app_id: &str,
170 app_secret: &str,
171 access_token: &str,
172 ) -> PyResult<()> {
173 parse_env(env).and_then(|env| {
174 let client = client_credentials(app_id, app_secret);
175 runtime()
176 .block_on(rust_logout_with_client(
177 env,
178 client,
179 access_token.to_string(),
180 ))
181 .map_err(native_error)
182 })
183 }
184
185 #[pyfunction]
186 fn me_with_client(
187 py: Python<'_>,
188 env: &str,
189 app_id: &str,
190 app_secret: &str,
191 access_token: &str,
192 ) -> PyResult<PyObject> {
193 parse_env(env).and_then(|env| {
194 let client = client_credentials(app_id, app_secret);
195 runtime()
196 .block_on(rust_me_with_client(env, client, access_token))
197 .map_err(native_error)
198 .and_then(|result| serialize(py, &result))
199 })
200 }
201
202 #[pyfunction]
203 fn remove_with_client(
204 env: &str,
205 app_id: &str,
206 app_secret: &str,
207 access_token: &str,
208 ) -> PyResult<()> {
209 parse_env(env).and_then(|env| {
210 let client = client_credentials(app_id, app_secret);
211 runtime()
212 .block_on(rust_remove_with_client(env, client, access_token))
213 .map_err(native_error)
214 })
215 }
216
217 #[pymodule(name = "_native")]
218 fn python_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
219 module.add("NativeSdkError", py.get_type_bound::<NativeSdkError>())?;
220 module.add_function(wrap_pyfunction!(signup_with_client, module)?)?;
221 module.add_function(wrap_pyfunction!(login_with_client, module)?)?;
222 module.add_function(wrap_pyfunction!(logout_with_client, module)?)?;
223 module.add_function(wrap_pyfunction!(me_with_client, module)?)?;
224 module.add_function(wrap_pyfunction!(remove_with_client, module)?)?;
225 Ok(())
226 }