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