Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

example: manually serving the OpenAPI doc #855

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1,411 changes: 719 additions & 692 deletions examples/demo/Cargo.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion examples/demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ unic-langid = "0.9.4"
tera = "1.19.1"
tower = "0.4.13"
futures-util = "0.3.30"
utoipa = "4.2.3"

utoipa = "5.0.0"
utoipa-axum = "0.1.0"
utoipa-swagger-ui = { version = "8.0", features = ["axum", "vendored"] }
utoipa-redoc = { version = "5.0.0", features = ["axum"] }
utoipa-scalar = { version = "0.2.0", features = ["axum"] }

[[bin]]
name = "demo_app-cli"
Expand Down
13 changes: 12 additions & 1 deletion examples/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,15 @@ This app is a kitchensink for various capabilities and examples of the [Loco](ht

## OpenAPI with `utoipa`

See [src/controllers/responses.rs](src/controllers/responses.rs)
### Implementing OpenAPI path see
- [src/controllers/auth.rs](src/controllers/auth.rs)
- `Album` in [src/controllers/responses.rs](src/controllers/responses.rs)

### How to serve the OpenAPI doc see
- `after_routes` in [src/app.rs](src/app.rs)
- `api_routes` in [src/controllers/auth.rs](src/controllers/auth.rs)

### View the served OpenAPI doc at
- [http://localhost:5150/swagger-ui/](http://localhost:5150/swagger-ui/)
- [http://localhost:5150/redoc](http://localhost:5150/redoc)
- [http://localhost:5150/scalar](http://localhost:5150/scalar)
58 changes: 58 additions & 0 deletions examples/demo/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::Path;

use async_trait::async_trait;
use axum::Router as AxumRouter;
use loco_rs::{
app::{AppContext, Hooks, Initializer},
boot::{create_app, BootResult, StartMode},
Expand All @@ -15,6 +16,14 @@ use loco_rs::{
};
use migration::Migrator;
use sea_orm::DatabaseConnection;
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme, HttpBuilder, HttpAuthScheme},
Modify, OpenApi,
};
use utoipa_axum::router::OpenApiRouter;
use utoipa_redoc::{Redoc, Servable};
use utoipa_scalar::{Scalar, Servable as ScalarServable};
use utoipa_swagger_ui::SwaggerUi;

use crate::{
controllers::{self, middlewares},
Expand Down Expand Up @@ -69,6 +78,55 @@ impl Hooks for App {
.add_route(controllers::cache::routes())
}

async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
// Serving the OpenAPI doc
#[derive(OpenApi)]
#[openapi(modifiers(&SecurityAddon),
info(
title = "Loco Demo",
description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
))]
struct ApiDoc;

// TODO set the jwt token location
// let auth_location = ctx.config.auth.as_ref();

struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_schemes_from_iter([
(
"jwt_token",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
),
(
"api_key",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("apikey"))),
),
]);
}
}
}

let (_, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
.merge(controllers::auth::api_routes())
.merge(controllers::responses::api_routes())
.split_for_parts();

let router = router
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", api.clone()))
.merge(Redoc::with_url("/redoc", api.clone()))
.merge(Scalar::with_url("/scalar", api));

Ok(router)
}

async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment).await
}
Expand Down
26 changes: 23 additions & 3 deletions examples/demo/src/controllers/auth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use loco_rs::{controller::bad_request, prelude::*};
use serde::{Deserialize, Serialize};
use utoipa_axum::{router::OpenApiRouter, routes};

use crate::{
mailers::auth::AuthMailer,
Expand All @@ -9,24 +10,27 @@ use crate::{
},
views::auth::UserSession,
};
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct VerifyParams {
pub token: String,
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct ForgotParams {
pub email: String,
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct ResetParams {
pub token: String,
pub password: String,
}

/// Register new user
///
/// Register function creates a new user with the given parameters and sends a
/// welcome email to the user
#[utoipa::path(post, tag = "auth", request_body = RegisterParams, path = "/api/auth/register", responses((status = 200, body = UserSession)))]
async fn register(
State(ctx): State<AppContext>,
Json(params): Json<RegisterParams>,
Expand Down Expand Up @@ -58,8 +62,11 @@ async fn register(
format::json(UserSession::new(&user, &token))
}

/// Verify registered user
///
/// Verify register user. if the user not verified his email, he can't login to
/// the system.
#[utoipa::path(post, tag = "auth", request_body = VerifyParams, path = "/api/auth/verify", responses((status = 200)))]
async fn verify(
State(ctx): State<AppContext>,
Json(params): Json<VerifyParams>,
Expand All @@ -77,10 +84,13 @@ async fn verify(
format::empty_json()
}

/// Forgot password
///
/// In case the user forgot his password this endpoints generate a forgot token
/// and send email to the user. In case the email not found in our DB, we are
/// returning a valid request for for security reasons (not exposing users DB
/// list).
#[utoipa::path(post, tag = "auth", request_body = ForgotParams, path = "/api/auth/forgot", responses((status = 200)))]
async fn forgot(
State(ctx): State<AppContext>,
Json(params): Json<ForgotParams>,
Expand All @@ -101,7 +111,10 @@ async fn forgot(
format::empty_json()
}

/// Reset password
///
/// reset user password by the given parameters
#[utoipa::path(post, tag = "auth", request_body = ResetParams, path = "/api/auth/reset", responses((status = 200)))]
async fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -> Result<Response> {
let Ok(user) = users::Model::find_by_reset_token(&ctx.db, &params.token).await else {
// we don't want to expose our users email. if the email is invalid we still
Expand All @@ -117,7 +130,10 @@ async fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -
format::empty_json()
}

/// Login
///
/// Creates a user login and returns a token
#[utoipa::path(post, tag = "auth", request_body = LoginParams, path = "/api/auth/login", responses((status = 200, body = UserSession)))]
async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {
let user = users::Model::find_by_email(&ctx.db, &params.email).await?;

Expand Down Expand Up @@ -145,3 +161,7 @@ pub fn routes() -> Routes {
.add("/forgot", post(forgot))
.add("/reset", post(reset))
}

pub fn api_routes() -> OpenApiRouter<AppContext> {
OpenApiRouter::new().routes(routes!(register, verify, login, forgot, reset))
}
7 changes: 6 additions & 1 deletion examples/demo/src/controllers/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
use axum_extra::extract::cookie::Cookie;
use loco_rs::prelude::*;
use serde::Serialize;
use utoipa::{openapi, OpenApi, ToSchema};
use utoipa::{OpenApi, ToSchema};
use utoipa_axum::{router::OpenApiRouter, routes};

#[derive(Serialize)]
pub struct Health {
Expand Down Expand Up @@ -145,3 +146,7 @@ pub fn routes() -> Routes {
.add("/album", get(album))
.add("/set_cookie", get(set_cookie))
}

pub fn api_routes() -> OpenApiRouter<AppContext> {
OpenApiRouter::new().routes(routes!(album))
}
1 change: 0 additions & 1 deletion examples/demo/src/models/roles.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use loco_rs::prelude::*;
use sea_orm::entity::prelude::*;

pub use super::_entities::roles::{self, ActiveModel, Entity, Model};
use crate::models::{_entities::sea_orm_active_enums::RolesName, users, users_roles};
Expand Down
4 changes: 2 additions & 2 deletions examples/demo/src/models/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ use uuid::Uuid;

pub use super::_entities::users::{self, ActiveModel, Entity, Model};

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct LoginParams {
pub email: String,
pub password: String,
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct RegisterParams {
pub email: String,
pub password: String,
Expand Down
4 changes: 2 additions & 2 deletions examples/demo/src/views/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use serde::{Deserialize, Serialize};

use crate::models::_entities::users;

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct UserSession {
pub token: String,
pub user: UserDetail,
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
pub struct UserDetail {
pub pid: String,
pub email: String,
Expand Down
3 changes: 2 additions & 1 deletion src/mailer/email_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ impl EmailSender {
///
/// # Errors
///
/// When email doesn't send successfully or has an error to build the message
/// When email doesn't send successfully or has an error to build the
/// message
pub async fn mail(&self, email: &Email) -> Result<()> {
let content = MultiPart::alternative_plain_html(email.text.clone(), email.html.clone());
let mut builder = Message::builder()
Expand Down