1use crate::api::v1;
2use crate::{AdminPanel, RenderError, RenderedPage};
3use async_trait::async_trait;
4use axum::extract::{Path, State};
5use axum::http::{HeaderMap, StatusCode};
6use axum::response::{IntoResponse, Redirect, Response};
7use axum::{routing, Form, Json, Router};
8use feattle_core::{Feattles, UpdateError};
9use serde::Deserialize;
10use std::sync::Arc;
11
12#[async_trait]
36pub trait ExtractModifiedBy: Send + Sync + 'static {
37 async fn extract_modified_by(&self, headers: &HeaderMap) -> Result<String, Response>;
38}
39
40pub fn axum_router<F>(
78 admin_panel: Arc<AdminPanel<F>>,
79 extract_modified_by: impl ExtractModifiedBy,
80) -> Router<()>
81where
82 F: Feattles + Sync + Send + 'static,
83{
84 async fn list_feattles<F: Feattles + Sync>(
85 State(state): State<RouterState<F>>,
86 ) -> impl IntoResponse {
87 state.admin_panel.list_feattles().await
88 }
89
90 async fn list_feattles_api_v1<F: Feattles + Sync>(
91 State(state): State<RouterState<F>>,
92 ) -> impl IntoResponse {
93 state.admin_panel.list_feattles_api_v1().await.map(Json)
94 }
95
96 async fn show_feattle<F: Feattles + Sync>(
97 State(state): State<RouterState<F>>,
98 Path(key): Path<String>,
99 ) -> impl IntoResponse {
100 state.admin_panel.show_feattle(&key).await
101 }
102
103 async fn show_feattle_api_v1<F: Feattles + Sync>(
104 State(state): State<RouterState<F>>,
105 Path(key): Path<String>,
106 ) -> impl IntoResponse {
107 state.admin_panel.show_feattle_api_v1(&key).await.map(Json)
108 }
109
110 async fn edit_feattle<F: Feattles + Sync>(
111 State(state): State<RouterState<F>>,
112 Path(key): Path<String>,
113 headers: HeaderMap,
114 Form(form): Form<EditFeattleForm>,
115 ) -> Response {
116 let modified_by = match state
117 .extract_modified_by
118 .extract_modified_by(&headers)
119 .await
120 {
121 Ok(modified_by) => modified_by,
122 Err(response) => return response,
123 };
124
125 state
126 .admin_panel
127 .edit_feattle(&key, &form.value_json, modified_by)
128 .await
129 .map(|_| Redirect::to("/"))
130 .into_response()
131 }
132
133 async fn edit_feattle_api_v1<F: Feattles + Sync>(
134 State(state): State<RouterState<F>>,
135 Path(key): Path<String>,
136 Json(request): Json<v1::EditFeattleRequest>,
137 ) -> impl IntoResponse {
138 state
139 .admin_panel
140 .edit_feattle_api_v1(&key, request)
141 .await
142 .map(Json)
143 }
144
145 async fn render_public_file<F: Feattles + Sync>(
146 State(state): State<RouterState<F>>,
147 Path(file_name): Path<String>,
148 ) -> impl IntoResponse {
149 state.admin_panel.render_public_file(&file_name)
150 }
151
152 let state = RouterState {
153 admin_panel,
154 extract_modified_by: Arc::new(extract_modified_by),
155 };
156
157 Router::new()
158 .route("/", routing::get(list_feattles))
159 .route("/api/v1/feattles", routing::get(list_feattles_api_v1))
160 .route("/feattle/{key}", routing::get(show_feattle))
161 .route("/api/v1/feattle/{key}", routing::get(show_feattle_api_v1))
162 .route("/feattle/{key}/edit", routing::post(edit_feattle))
163 .route("/api/v1/feattle/{key}", routing::post(edit_feattle_api_v1))
164 .route("/public/{file_name}", routing::get(render_public_file))
165 .with_state(state)
166}
167
168#[derive(Debug, Deserialize)]
169struct EditFeattleForm {
170 value_json: String,
171}
172
173struct RouterState<F> {
174 admin_panel: Arc<AdminPanel<F>>,
175 extract_modified_by: Arc<dyn ExtractModifiedBy>,
176}
177
178impl IntoResponse for RenderedPage {
179 fn into_response(self) -> Response {
180 ([("Content-Type", self.content_type)], self.content).into_response()
181 }
182}
183
184impl IntoResponse for RenderError {
185 fn into_response(self) -> Response {
186 match self {
187 RenderError::NotFound | RenderError::Update(UpdateError::UnknownKey(_)) => {
188 StatusCode::NOT_FOUND.into_response()
189 }
190 RenderError::Update(UpdateError::Parsing(err)) => (
191 StatusCode::BAD_REQUEST,
192 format!("Failed to parse: {:?}", err),
193 )
194 .into_response(),
195 err => {
196 log::error!("request failed with {:?}", err);
197 (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", err)).into_response()
198 }
199 }
200 }
201}
202
203impl<F> Clone for RouterState<F> {
204 fn clone(&self) -> Self {
205 RouterState {
206 admin_panel: self.admin_panel.clone(),
207 extract_modified_by: self.extract_modified_by.clone(),
208 }
209 }
210}
211
212#[async_trait]
213impl ExtractModifiedBy for String {
214 async fn extract_modified_by(&self, _headers: &HeaderMap) -> Result<String, Response> {
215 Ok(self.clone())
216 }
217}
218
219#[async_trait]
220impl ExtractModifiedBy for &'static str {
221 async fn extract_modified_by(&self, _headers: &HeaderMap) -> Result<String, Response> {
222 Ok(self.to_string())
223 }
224}
225
226#[async_trait]
227impl<F, R> ExtractModifiedBy for F
228where
229 F: Fn(&HeaderMap) -> Result<String, R> + Send + Sync + 'static,
230 R: IntoResponse,
231{
232 async fn extract_modified_by(&self, headers: &HeaderMap) -> Result<String, Response> {
233 self(headers).map_err(|response| response.into_response())
234 }
235}