forked from
smokesignal.events/smokesignal
The smokesignal.events web application
1use anyhow::Result;
2use axum::{
3 Form,
4 extract::Query,
5 response::{IntoResponse, Redirect},
6};
7use axum_template::RenderHtml;
8use minijinja::context as template_context;
9use serde::Deserialize;
10
11use crate::{
12 contextual_error,
13 http::{
14 context::{AdminRequestContext, admin_template_context},
15 errors::WebError,
16 pagination::{Pagination, PaginationView},
17 },
18 profile_index::ProfileIndexManager,
19 select_template,
20};
21
22#[derive(Debug, Deserialize)]
23pub(crate) struct SearchIndexQuery {
24 #[serde(default)]
25 query: Option<String>,
26}
27
28#[derive(Debug, Deserialize)]
29pub(crate) struct DeleteIndexedProfileForm {
30 aturi: String,
31}
32
33pub(crate) async fn handle_admin_profile_index(
34 admin_ctx: AdminRequestContext,
35 pagination: Query<Pagination>,
36 search_query: Query<SearchIndexQuery>,
37) -> Result<impl IntoResponse, WebError> {
38 let canonical_url = format!(
39 "https://{}/admin/search-index/profiles",
40 admin_ctx.web_context.config.external_base
41 );
42 let default_context =
43 admin_template_context(&admin_ctx, &canonical_url, "search-index-profiles");
44
45 let render_template = select_template!("admin_profile_index", false, false, admin_ctx.language);
46 let error_template = select_template!(false, false, admin_ctx.language);
47
48 // Check if OpenSearch is enabled
49 let opensearch_endpoint = match admin_ctx.web_context.config.opensearch_endpoint.as_ref() {
50 Some(endpoint) => endpoint,
51 None => {
52 return contextual_error!(
53 admin_ctx.web_context,
54 admin_ctx.language,
55 error_template,
56 default_context,
57 anyhow::anyhow!("OpenSearch is not enabled")
58 );
59 }
60 };
61
62 // Create profile index manager
63 let manager = match ProfileIndexManager::new(opensearch_endpoint) {
64 Ok(m) => m,
65 Err(err) => {
66 return contextual_error!(
67 admin_ctx.web_context,
68 admin_ctx.language,
69 error_template,
70 default_context,
71 err
72 );
73 }
74 };
75
76 // Get index stats
77 let stats = match manager.get_stats().await {
78 Ok(s) => s,
79 Err(err) => {
80 return contextual_error!(
81 admin_ctx.web_context,
82 admin_ctx.language,
83 error_template,
84 default_context,
85 err
86 );
87 }
88 };
89
90 let (page, page_size) = pagination.admin_clamped();
91
92 // Get profiles (either search or list)
93 let (total_count, mut profiles) = if let Some(ref q) = search_query.query {
94 if !q.is_empty() {
95 match manager.search_indexed_profiles(q, page, page_size).await {
96 Ok(result) => result,
97 Err(err) => {
98 return contextual_error!(
99 admin_ctx.web_context,
100 admin_ctx.language,
101 error_template,
102 default_context,
103 err
104 );
105 }
106 }
107 } else {
108 match manager.list_indexed_profiles(page, page_size).await {
109 Ok(result) => result,
110 Err(err) => {
111 return contextual_error!(
112 admin_ctx.web_context,
113 admin_ctx.language,
114 error_template,
115 default_context,
116 err
117 );
118 }
119 }
120 }
121 } else {
122 match manager.list_indexed_profiles(page, page_size).await {
123 Ok(result) => result,
124 Err(err) => {
125 return contextual_error!(
126 admin_ctx.web_context,
127 admin_ctx.language,
128 error_template,
129 default_context,
130 err
131 );
132 }
133 }
134 };
135
136 // Build query params for pagination
137 let mut params: Vec<(&str, String)> = vec![];
138 if let Some(ref q) = search_query.query
139 && !q.is_empty()
140 {
141 params.push(("query", q.clone()));
142 }
143
144 let params_refs: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
145
146 let pagination_view = PaginationView::new(page_size, profiles.len() as i64, page, params_refs);
147
148 if profiles.len() > page_size as usize {
149 profiles.truncate(page_size as usize);
150 }
151
152 Ok(RenderHtml(
153 &render_template,
154 admin_ctx.web_context.engine.clone(),
155 template_context! { ..default_context, ..template_context! {
156 profiles,
157 total_count,
158 index_stats => stats,
159 search_query => search_query.query.clone().unwrap_or_default(),
160 pagination => pagination_view,
161 }},
162 )
163 .into_response())
164}
165
166pub(crate) async fn handle_admin_profile_index_delete(
167 admin_ctx: AdminRequestContext,
168 Form(form): Form<DeleteIndexedProfileForm>,
169) -> Result<impl IntoResponse, WebError> {
170 let error_template = select_template!(false, false, admin_ctx.language);
171
172 // Check if OpenSearch is enabled
173 let opensearch_endpoint = match admin_ctx.web_context.config.opensearch_endpoint.as_ref() {
174 Some(endpoint) => endpoint,
175 None => {
176 return contextual_error!(
177 admin_ctx.web_context,
178 admin_ctx.language,
179 error_template,
180 template_context! {},
181 anyhow::anyhow!("OpenSearch is not enabled")
182 );
183 }
184 };
185
186 // Create profile index manager
187 let manager = match ProfileIndexManager::new(opensearch_endpoint) {
188 Ok(m) => m,
189 Err(err) => {
190 return contextual_error!(
191 admin_ctx.web_context,
192 admin_ctx.language,
193 error_template,
194 template_context! {},
195 err
196 );
197 }
198 };
199
200 // Delete the indexed profile
201 if let Err(err) = manager.delete_indexed_profile(&form.aturi).await {
202 return contextual_error!(
203 admin_ctx.web_context,
204 admin_ctx.language,
205 error_template,
206 template_context! {},
207 err
208 );
209 }
210
211 Ok(Redirect::to("/admin/search-index/profiles").into_response())
212}
213
214pub(crate) async fn handle_admin_profile_index_rebuild(
215 admin_ctx: AdminRequestContext,
216) -> Result<impl IntoResponse, WebError> {
217 let error_template = select_template!(false, false, admin_ctx.language);
218
219 // Check if OpenSearch is enabled
220 let opensearch_endpoint = match admin_ctx.web_context.config.opensearch_endpoint.as_ref() {
221 Some(endpoint) => endpoint,
222 None => {
223 return contextual_error!(
224 admin_ctx.web_context,
225 admin_ctx.language,
226 error_template,
227 template_context! {},
228 anyhow::anyhow!("OpenSearch is not enabled")
229 );
230 }
231 };
232
233 // Create profile index manager
234 let manager = match ProfileIndexManager::new(opensearch_endpoint) {
235 Ok(m) => m,
236 Err(err) => {
237 return contextual_error!(
238 admin_ctx.web_context,
239 admin_ctx.language,
240 error_template,
241 template_context! {},
242 err
243 );
244 }
245 };
246
247 // Rebuild the index
248 let indexed_count = match manager
249 .rebuild_index(
250 &admin_ctx.web_context.pool,
251 admin_ctx.web_context.identity_resolver.clone(),
252 )
253 .await
254 {
255 Ok(count) => count,
256 Err(err) => {
257 return contextual_error!(
258 admin_ctx.web_context,
259 admin_ctx.language,
260 error_template,
261 template_context! {},
262 err
263 );
264 }
265 };
266
267 tracing::info!(
268 "Profile search index rebuilt: {} profiles indexed",
269 indexed_count
270 );
271
272 Ok(Redirect::to("/admin/search-index/profiles").into_response())
273}