1use std::collections::HashMap;
9use std::sync::Arc;
10
11use adminx_core::auth;
12use adminx_core::registry::all_resources;
13use adminx_core::request::ReqCtx;
14use adminx_core::resource::Resource;
15use adminx_core::response::{ApiBody, ApiResponse};
16use adminx_core::storage::storage;
17use adminx_core::ui;
18
19use actix_web::http::header::{HeaderName, HeaderValue};
20use actix_web::http::StatusCode;
21use actix_web::{web, HttpRequest, HttpResponse, Scope};
22use serde::Deserialize;
23use serde_json::{json, Value};
24
25const MOUNT: &str = "/adminx";
27
28#[derive(Deserialize)]
29struct LoginForm {
30 email: String,
31 password: String,
32}
33
34#[derive(Deserialize)]
35struct CodeForm {
36 code: String,
37}
38
39fn into_response(api: ApiResponse) -> HttpResponse {
41 let status = StatusCode::from_u16(api.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
42 let mut builder = HttpResponse::build(status);
43 for (k, v) in &api.headers {
44 if let (Ok(name), Ok(val)) = (
45 HeaderName::from_bytes(k.as_bytes()),
46 HeaderValue::from_str(v),
47 ) {
48 builder.append_header((name, val));
49 }
50 }
51 match api.body {
52 ApiBody::Json(v) => builder.json(v),
53 ApiBody::Bytes { content_type, data } => builder.content_type(content_type).body(data),
54 ApiBody::Empty => builder.finish(),
55 }
56}
57
58fn make_ctx(req: &HttpRequest) -> ReqCtx {
60 let token = req.cookie(auth::COOKIE_NAME).map(|c| c.value().to_string());
61 auth::build_ctx(MOUNT, req.query_string(), token.as_deref())
62}
63
64fn resource_routes(resource: Box<dyn Resource>) -> Scope {
66 let r: Arc<dyn Resource> = Arc::from(resource);
67 let mut scope = web::scope("");
68
69 scope = scope.route("/api", {
71 let r = r.clone();
72 web::get().to(move |req: HttpRequest| {
73 let r = r.clone();
74 async move { into_response(r.list(&make_ctx(&req)).await) }
75 })
76 });
77 scope = scope.route("/api", {
78 let r = r.clone();
79 web::post().to(move |req: HttpRequest, body: web::Json<Value>| {
80 let r = r.clone();
81 async move { into_response(r.create(&make_ctx(&req), body.into_inner()).await) }
82 })
83 });
84 scope = scope.route("/api/{id}", {
85 let r = r.clone();
86 web::get().to(move |req: HttpRequest, path: web::Path<String>| {
87 let r = r.clone();
88 async move { into_response(r.get(&make_ctx(&req), &path.into_inner()).await) }
89 })
90 });
91 scope = scope.route("/api/{id}", {
92 let r = r.clone();
93 web::put().to(
94 move |req: HttpRequest, path: web::Path<String>, body: web::Json<Value>| {
95 let r = r.clone();
96 async move {
97 into_response(
98 r.update(&make_ctx(&req), &path.into_inner(), body.into_inner())
99 .await,
100 )
101 }
102 },
103 )
104 });
105 scope = scope.route("/api/{id}", {
106 let r = r.clone();
107 web::delete().to(move |req: HttpRequest, path: web::Path<String>| {
108 let r = r.clone();
109 async move { into_response(r.delete(&make_ctx(&req), &path.into_inner()).await) }
110 })
111 });
112
113 scope = scope.route("/list", {
115 let r = r.clone();
116 web::get().to(move |req: HttpRequest| {
117 let r = r.clone();
118 async move { into_response(r.list_page(&make_ctx(&req)).await) }
119 })
120 });
121 scope = scope.route("/new", {
122 let r = r.clone();
123 web::get().to(move |req: HttpRequest| {
124 let r = r.clone();
125 async move { into_response(r.new_page(&make_ctx(&req)).await) }
126 })
127 });
128 scope = scope.route("/view/{id}", {
129 let r = r.clone();
130 web::get().to(move |req: HttpRequest, path: web::Path<String>| {
131 let r = r.clone();
132 async move { into_response(r.view_page(&make_ctx(&req), &path.into_inner()).await) }
133 })
134 });
135 scope = scope.route("/edit/{id}", {
136 let r = r.clone();
137 web::get().to(move |req: HttpRequest, path: web::Path<String>| {
138 let r = r.clone();
139 async move { into_response(r.edit_page(&make_ctx(&req), &path.into_inner()).await) }
140 })
141 });
142 scope = scope.route("/create", {
143 let r = r.clone();
144 web::post().to(
145 move |req: HttpRequest, form: web::Form<HashMap<String, String>>| {
146 let r = r.clone();
147 async move {
148 into_response(r.create_form(&make_ctx(&req), form.into_inner()).await)
149 }
150 },
151 )
152 });
153 scope = scope.route("/update/{id}", {
154 let r = r.clone();
155 web::post().to(
156 move |req: HttpRequest,
157 path: web::Path<String>,
158 form: web::Form<HashMap<String, String>>| {
159 let r = r.clone();
160 async move {
161 into_response(
162 r.update_form(&make_ctx(&req), &path.into_inner(), form.into_inner())
163 .await,
164 )
165 }
166 },
167 )
168 });
169 scope = scope.route("/{id}/delete", {
170 let r = r.clone();
171 web::post().to(move |req: HttpRequest, path: web::Path<String>| {
172 let r = r.clone();
173 async move { into_response(r.delete_form(&make_ctx(&req), &path.into_inner()).await) }
174 })
175 });
176
177 scope = scope.route("/{id}/action/{name}", {
179 let r = r.clone();
180 web::post().to(
181 move |req: HttpRequest, path: web::Path<(String, String)>, body: web::Bytes| {
182 let r = r.clone();
183 async move {
184 let (id, name) = path.into_inner();
185 let payload: Value = serde_json::from_slice(&body).unwrap_or_else(|_| json!({}));
186 into_response(r.run_action(&make_ctx(&req), &name, id, payload).await)
187 }
188 },
189 )
190 });
191
192 scope
193}
194
195async fn dashboard(req: HttpRequest) -> HttpResponse {
196 let ctx = make_ctx(&req);
197 if let Some(deny) = auth::guard_ui(&ctx) {
198 return into_response(deny);
199 }
200 into_response(ui::dashboard(&ctx))
201}
202
203async fn login_form(req: HttpRequest) -> HttpResponse {
204 into_response(auth::login_page(&make_ctx(&req), None))
205}
206
207async fn login_submit(req: HttpRequest, form: web::Form<LoginForm>) -> HttpResponse {
208 let ctx = make_ctx(&req);
209 into_response(auth::handle_login(&ctx, &form.email, &form.password).await)
210}
211
212async fn logout(req: HttpRequest) -> HttpResponse {
213 into_response(auth::handle_logout(&make_ctx(&req)))
214}
215
216async fn mfa_setup_get(req: HttpRequest) -> HttpResponse {
217 into_response(auth::mfa_setup_page(&make_ctx(&req), None).await)
218}
219
220async fn mfa_enable_post(req: HttpRequest, form: web::Form<CodeForm>) -> HttpResponse {
221 into_response(auth::handle_mfa_enable(&make_ctx(&req), &form.code).await)
222}
223
224async fn mfa_verify_get(req: HttpRequest) -> HttpResponse {
225 into_response(auth::mfa_verify_page(&make_ctx(&req), None).await)
226}
227
228async fn mfa_verify_post(req: HttpRequest, form: web::Form<CodeForm>) -> HttpResponse {
229 into_response(auth::handle_mfa_verify(&make_ctx(&req), &form.code).await)
230}
231
232async fn health() -> HttpResponse {
233 let ok = storage().health().await;
234 let body = json!({ "status": if ok { "healthy" } else { "unhealthy" } });
235 into_response(ApiResponse::json(if ok { 200 } else { 503 }, body))
236}
237
238pub fn scope() -> Scope {
241 let mut scope = web::scope(MOUNT)
242 .route("", web::get().to(dashboard))
243 .route("/", web::get().to(dashboard))
244 .route("/health", web::get().to(health))
245 .route("/login", web::get().to(login_form))
246 .route("/login", web::post().to(login_submit))
247 .route("/logout", web::get().to(logout))
248 .route("/mfa/setup", web::get().to(mfa_setup_get))
249 .route("/mfa/enable", web::post().to(mfa_enable_post))
250 .route("/mfa/verify", web::get().to(mfa_verify_get))
251 .route("/mfa/verify", web::post().to(mfa_verify_post));
252
253 let resources = all_resources();
254 tracing::info!("adminx-actix: mounting {} resource(s)", resources.len());
255
256 for resource in resources {
257 let base = format!("/{}", resource.base_path());
258 tracing::info!(
259 "adminx-actix: mounting '{}' at '{}{}'",
260 resource.resource_name(),
261 MOUNT,
262 base
263 );
264 scope = scope.service(web::scope(&base).service(resource_routes(resource)));
265 }
266
267 scope
268}