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