Added swagger ui support

This commit is contained in:
2026-05-21 16:22:22 +03:00
parent 16abe754af
commit d5de34470e
9 changed files with 136 additions and 19 deletions
+9 -1
View File
@@ -86,6 +86,7 @@ fn config_display_entries(config: &AppConfig, sources: &ConfigSources) -> Vec<Co
entry!(auth_sso_enabled, config.auth_sso_enabled.to_string(), defaults.auth_sso_enabled.to_string()),
entry!(oidc_button_text, config.oidc_button_text.clone(), defaults.oidc_button_text.clone()),
entry!(oidc_admin_groups, config.oidc_admin_groups.clone(), defaults.oidc_admin_groups.clone()),
entry!(swagger_enabled, config.swagger_enabled.to_string(), defaults.swagger_enabled.to_string()),
]
}
@@ -155,6 +156,8 @@ struct SettingsTemplate {
oidc_client_secret_source: &'static str,
oidc_admin_groups: String,
oidc_admin_groups_source: &'static str,
swagger_enabled: bool,
swagger_enabled_source: &'static str,
}
pub async fn settings_handler(
@@ -185,6 +188,8 @@ pub async fn settings_handler(
oidc_client_secret_source: sources.oidc_client_secret.code(),
oidc_admin_groups: config.oidc_admin_groups,
oidc_admin_groups_source: sources.oidc_admin_groups.code(),
swagger_enabled: config.swagger_enabled,
swagger_enabled_source: sources.swagger_enabled.code(),
};
Ok(Html::new(template.render()?))
}
@@ -198,6 +203,7 @@ pub struct OidcSettingsForm {
oidc_client_id: String,
oidc_client_secret: String,
oidc_admin_groups: String,
swagger_enabled: Option<String>,
}
pub async fn settings_submit(
@@ -212,7 +218,8 @@ pub async fn settings_submit(
FormResult::Ok(data) => {
let pw_enabled = if data.auth_password_enabled.is_some() { "true" } else { "false" };
let sso_enabled = if data.auth_sso_enabled.is_some() { "true" } else { "false" };
let fields: [(&str, &str); 7] = [
let swagger = if data.swagger_enabled.is_some() { "true" } else { "false" };
let fields: [(&str, &str); 8] = [
("auth_password_enabled", pw_enabled),
("auth_sso_enabled", sso_enabled),
("oidc_button_text", &data.oidc_button_text),
@@ -220,6 +227,7 @@ pub async fn settings_submit(
("oidc_client_id", &data.oidc_client_id),
("oidc_client_secret", &data.oidc_client_secret),
("oidc_admin_groups", &data.oidc_admin_groups),
("swagger_enabled", swagger),
];
for (key, value) in fields {
let mut entry = ConfigEntry::new(key.to_owned(), value.to_owned());
+20 -16
View File
@@ -1,23 +1,19 @@
use cot::db::Database;
use cot::router::method::get;
use cot::json::Json;
use cot::response::IntoResponse;
use cot::router::method::openapi::api_get;
use cot::router::{Route, Router};
use cot::session::Session;
use cot::{App, Body};
use schemars::JsonSchema;
use serde::Serialize;
use crate::auth;
// ---------------------------------------------------------------------------
// JSON response helpers
// JSON error helper
// ---------------------------------------------------------------------------
fn json_ok(value: &serde_json::Value) -> cot::response::Response {
cot::http::Response::builder()
.status(cot::http::StatusCode::OK)
.header(cot::http::header::CONTENT_TYPE, "application/json")
.body(Body::fixed(value.to_string()))
.expect("valid response")
}
fn json_error(status: cot::http::StatusCode, message: &str) -> cot::response::Response {
let body = serde_json::json!({ "error": message });
cot::http::Response::builder()
@@ -31,6 +27,13 @@ fn json_error(status: cot::http::StatusCode, message: &str) -> cot::response::Re
// GET /api/me
// ---------------------------------------------------------------------------
#[derive(Debug, Serialize, JsonSchema)]
struct MeResponse {
id: i64,
name: String,
role: String,
}
async fn me_handler(
session: Session,
db: Database,
@@ -42,11 +45,12 @@ async fn me_handler(
));
};
Ok(json_ok(&serde_json::json!({
"id": user.id,
"name": user.name,
"role": user.role.code(),
})))
Json(MeResponse {
id: user.id,
name: user.name,
role: user.role.code().to_owned(),
})
.into_response()
}
// ---------------------------------------------------------------------------
@@ -62,7 +66,7 @@ impl App for ApiApp {
fn router(&self) -> Router {
Router::with_urls([
Route::with_handler_and_name("/me", get(me_handler), "api_me"),
Route::with_api_handler_and_name("/me", api_get(me_handler), "api_me"),
])
}
}
+7
View File
@@ -127,6 +127,7 @@ pub struct ConfigSources {
pub auth_sso_enabled: ConfigSource,
pub oidc_button_text: ConfigSource,
pub oidc_admin_groups: ConfigSource,
pub swagger_enabled: ConfigSource,
}
impl Default for ConfigSources {
@@ -141,6 +142,7 @@ impl Default for ConfigSources {
auth_sso_enabled: ConfigSource::Default,
oidc_button_text: ConfigSource::Default,
oidc_admin_groups: ConfigSource::Default,
swagger_enabled: ConfigSource::Default,
}
}
}
@@ -223,6 +225,8 @@ pub struct AppConfig {
pub oidc_button_text: String,
/// Comma-separated list of OIDC group names that grant admin role.
pub oidc_admin_groups: String,
/// Whether the Swagger UI is served at /swagger/.
pub swagger_enabled: bool,
}
impl Default for AppConfig {
@@ -237,6 +241,7 @@ impl Default for AppConfig {
auth_sso_enabled: false,
oidc_button_text: "Sign in with SSO".into(),
oidc_admin_groups: String::new(),
swagger_enabled: false,
}
}
}
@@ -252,6 +257,7 @@ impl_env_overrides!(
auth_sso_enabled,
oidc_button_text,
oidc_admin_groups,
swagger_enabled,
);
impl AppConfig {
@@ -317,6 +323,7 @@ impl AppConfig {
apply_db_field!(auth_sso_enabled);
apply_db_field!(oidc_button_text);
apply_db_field!(oidc_admin_groups);
apply_db_field!(swagger_enabled);
}
}
+5
View File
@@ -89,6 +89,11 @@ translations! {
users_password_hint: "Leave blank to keep current" , "Оставьте пустым, чтобы не менять";
users_saved: "User saved." , "Пользователь сохранён.";
// API settings
settings_api: "API" , "API";
settings_swagger: "Swagger UI" , "Swagger UI";
settings_swagger_help: "Serves interactive API docs at /swagger/ (requires restart)" , "Интерактивная документация API на /swagger/ (требуется перезапуск)";
// OIDC login errors
login_oidc_error: "SSO login failed. Please try again." , "Ошибка входа через SSO. Попробуйте ещё раз.";
login_sso_disabled: "SSO login is not configured." , "Вход через SSO не настроен.";
+16
View File
@@ -19,6 +19,7 @@ use cot::db::Database;
use cot::form::{Form, FormResult};
use cot::html::Html;
use cot::middleware::SessionMiddleware;
use cot::static_files::StaticFilesMiddleware;
use cot::project::RegisterAppsContext;
use cot::request::extractors::{RequestForm, UrlQuery};
use cot::response::IntoResponse;
@@ -148,6 +149,11 @@ impl App for FuruApp {
get(|| async { Ok::<_, cot::Error>(auth::redirect("/admin/")) }),
"admin_redirect",
),
Route::with_handler_and_name(
"/swagger",
get(|| async { Ok::<_, cot::Error>(auth::redirect("/swagger/")) }),
"swagger_redirect",
),
Route::with_handler_and_name("/", index, "index"),
Route::with_handler_and_name(
"/login",
@@ -258,6 +264,9 @@ impl Project for FuruProject {
" FURU_OIDC_BUTTON_TEXT SSO button label (default: Sign in with SSO)\n",
" FURU_OIDC_ADMIN_GROUPS OIDC groups that grant admin role\n",
"\n",
" API:\n",
" FURU_SWAGGER_ENABLED Enable Swagger UI at /swagger/ (default: false)\n",
"\n",
"QUICK START\n",
" export FURU_DATABASE_URL=postgres://user:pass@localhost/furumusic\n",
" furumusic run",
@@ -300,6 +309,7 @@ impl Project for FuruProject {
context: &cot::project::MiddlewareContext,
) -> cot::project::RootHandler {
handler
.middleware(StaticFilesMiddleware::from_context(context))
.middleware(
SessionMiddleware::from_context(context)
.same_site(cot::config::SameSite::Lax),
@@ -320,6 +330,12 @@ impl Project for FuruProject {
"/admin",
);
apps.register_with_views(api::ApiApp, "/api");
if self.app_config.swagger_enabled {
apps.register_with_views(
cot::openapi::swagger_ui::SwaggerUi::new(),
"/swagger",
);
}
}
}