Skip to content

Commit

Permalink
Fix build on stable toolchain
Browse files Browse the repository at this point in the history
  • Loading branch information
asyade committed Nov 2, 2024
1 parent 9702e91 commit 7f8c3a0
Show file tree
Hide file tree
Showing 18 changed files with 176 additions and 126 deletions.
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# All the following variables can be found under your application/tenant settings in Auth0

# Your OIDC client
OIDC_CLIENT_ID=
# Your OIDC client secret
OIDC_CLIENT_SECRET=
# Your OIDC domain
OIDC_DOMAIN=
# Your OIDC audience
OIDC_AUDIENCE=

# Your database url
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ target
target_analyser
.DS_Store
.vscode
**/**.DS_Store
**/**.DS_Store
pgdata
65 changes: 61 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RUST Backend Template - GraphQL API/PostgreSQL database/JWKS authentication
# *WIP* RUST Backend template: GraphQL/PostgreSQL/JWKS
This repository provide a production ready stateless backend application template written in Rust.
Its mainely design to be the backend of a SaaS or mobile application backend featuring a GraphQL API, a PostgreSQL database (that can be embedded in the application or external) and a JWKS authentication compatible with Auth0 or any other provider supporting JWKS and OpenID Connect.

Expand Down Expand Up @@ -26,12 +26,69 @@ But even better it came with everythings you need to easyly build proper develop
- Feature integration tests execution with isolated database and parallel execution
- Multiplatform (Linux/MacOS/Windows)

## How to run the project with minimal setup
In this section we will see how to run the project with a minimal setup, without a persistent database and without Auth0.
## Demo frontend
This template came with an fully featured example of frontend application that uses the `expo` framework to build a mobile/web application that use the API and perform authentication using Auth0.

## How to prepare and run the project
There is various way to run the project depending on your needs.
For development, you can use the `embedded-database` feature to have a fast setup with an embeded database.
For production or regular development, you will want to use an externaly installed database.

### Prerequisites
- Rust nightly toolchain with rustc version >= 1.84
- `sqlx_cli` golbaly installed (i.e `cargo install sqlx-cli`)

### OIDC Provider
You need to setup an OIDC provider that support JWKS and OpenID Connect in order to use the authentication feature.
To do so we will use Auth0 as an example but you can use any other OpenID Connect provider that support JWKS and OpenID Connect.

#### Auth0
You will need to create a new application in your Auth0 tenant and get the following information:
- Client ID
- Client Secret
- Domain
- Audience

Here is a picture of the Auth0 application settings:

![Auth0 Application Settings](./images/auth0-application-settings.png)

The audience is the API identifier, you can find it under the "settings" section of your application:

![Auth0 API Identifier](./images/auth0-api-identifier.png)


### Preparing your environment
You can use the `.env.example` as a template to create your own `.env` file with the correct environment variables.

### Running the project
#### Development
To run the project in development mode, you can use the following command:

```bash
cargo run --features embedded-database
```

or if you want to run the project without the embedded database:
> You also can use the `DATABASE_URL` environment variable to set the database url.
```bash
cargo run --database-url <your-database-url>
```

```bash
cargo run
```

Once the application is running, you can test the API using your browser and directly access the GraphQL playground at `http://<listen_address>/graphql`

Once whithin the playground you can test the API with the following query:

```graphql
query {
getCurrentUserFeed(category: HOME, limit: 0) {
offset
posts {
content
}
}
}
```
45 changes: 27 additions & 18 deletions binaries/rave-app-backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#![feature(exitcode_exit_method)]

use std::process::ExitCode;
use std::process::{abort, ExitCode};

use dotenv::dotenv;
use tracing::{error, info};
use rave_api::prelude::*;
use tracing::{error, info};
mod log;

use clap::Parser;
Expand All @@ -16,41 +14,54 @@ struct Args {
short,
long,
conflicts_with = "database_url",
help = "Name of the embedded database (will be created if not exists). When using embedded database, the DATABASE_URL environment variable is ignored.")]
help = "Name of the embedded database (will be created if not exists). When using embedded database, the DATABASE_URL environment variable is ignored. The database is not persisted by default but setting the `PG_DATA_DIR` environment variable will make it so."
)]
#[cfg(feature = "embedded-database")]
embeded_database: Option<String>,

#[arg(short, long, help = "Listen address in format IP:PORT (use environment variable LISTEN_ADDRESS if not set)")]
#[arg(
short,
long,
help = "Listen address in format IP:PORT (use environment variable LISTEN_ADDRESS if not set)"
)]
address: Option<String>,

#[arg(short, long, help = "Database URL (use environment variable DATABASE_URL if not set)")]
#[arg(
short,
long,
help = "Database URL (use environment variable DATABASE_URL if not set)"
)]
database_url: Option<String>,
}


#[tokio::main]
async fn main() {
dotenv().ok();
log::init();

let args = Args::parse();
let database_url;

#[cfg(feature="embedded-database")] {
let database_url;
// When not using the `embedded_database` feature, just use `args.database_url` as is
#[cfg(not(feature = "embedded-database"))]
{
database_url = args.database_url
}
// When using the `embedded-database` feature, handle embedded database creation
#[cfg(feature = "embedded-database")]
{
use rave_embedded_database::prelude::*;

if let Some(embedded) = args.embeded_database {
// Handle embedded database creation
let database_pool = EmbeddedDatabasePool::new().await.unwrap();
let database = database_pool.create_database(Some(embedded)).await.unwrap();
database_url = Some(database.connection_string());
} else {
// Use regular database_url or env variable if not set
database_url = args.database_url;
}
}

#[cfg(not(feature="embedded-database"))] {
database_url = args.database_url
}

let options = match RaveApiOptions::try_from_env(args.address, database_url) {
Ok(options) => options,
Err(e) => return error!("{}", e),
Expand All @@ -60,9 +71,7 @@ async fn main() {
Ok(_) => info!("Server stopped gracefully"),
Err(e) => {
error!("Server stopped with error: {}", e);

// Exit the process with platform specific failure exit status
ExitCode::FAILURE.exit_process()
abort()
}
};
}
10 changes: 4 additions & 6 deletions crates/rave-api/src/graphql/query/feed.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use crate::{
prelude::*,
services::{
feed_provider::{FeedCategory, FeedChunk, FeedOffset, FeedProvider},
},
services::feed_provider::{FeedCategory, FeedChunk, FeedOffset, FeedProvider},
};

use async_graphql::{Context, Object, Result};
use rave_entity::{async_graphql};
use rave_entity::async_graphql;

#[derive(Default)]
pub struct FeedQuery;
Expand All @@ -20,10 +18,10 @@ impl FeedQuery {
category: FeedCategory,
limit: usize,
) -> Result<FeedChunk> {
// let api_user = ctx.data::<ApiUser>()?;
let api_user = ctx.data::<crate::AnyApiUser>()?;
let feed_provider = ctx.data::<FeedProvider>()?;
let chunk = feed_provider
.get(None, Uuid::new_v4(), category, limit, None)
.get(None, api_user, category, limit, None)
.await?;
tracing::info!(
offset = chunk.offset,
Expand Down
11 changes: 3 additions & 8 deletions crates/rave-api/src/graphql/query/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
use rave_entity::async_graphql;

pub mod user;
pub mod feed;

pub use user::UserQuery;

use self::feed::FeedQuery;

// Add your other ones here to create a unified Query object
// e.x. Query(NoteQuery, OtherQuery, OtherOtherQuery)
// You can add other queries here to create a unified Query object
// e.x. Query(FeedQuery, OtherQuery)
#[derive(async_graphql::MergedObject, Default)]
pub struct Query(UserQuery, FeedQuery);
pub struct Query(FeedQuery);
23 changes: 0 additions & 23 deletions crates/rave-api/src/graphql/query/user.rs

This file was deleted.

57 changes: 33 additions & 24 deletions crates/rave-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,50 +34,59 @@ pub mod options;

pub type ApiSchema = Schema<Query, Mutation, EmptySubscription>;

/// The API state contains the GraphQL schema and may contains other state in the future
/// > It is important to keep in mind that the state must be optional or shared using a cache system to keep the ability to scale horizontally the api
#[derive(Clone)]
pub struct ApiState {
pub schema: ApiSchema,
}

#[instrument(skip(options))]
pub async fn serve(options: RaveApiOptions) -> RaveApiResult<()> {
let db = Database::new().await?;
// Initialize the database pool
let db = Database::new(&options.database_url).await?;

let schema: ApiSchema = build_schema(db.clone())
.await?;
let state = ApiState { schema };
let iam = Iam::init(db, options.auth0.clone())
.await?;
// Initialize the GraphQL schema and wrap it in the `ApiState``
let state = ApiState {
schema: build_schema(db.clone()).await?,
};

// Initialize the authentication layer
let authentication_layer = Extension(Iam::init(db, options.auth0.clone()).await?);

// Initialize the tracing layer
let tracing_layer = TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
info_span!(
"http_request",
method = ?request.method(),
matched_path,
some_other_field = tracing::field::Empty,
)
});

let app = Router::new()
// .layer(Extension(Arc::new(iam)))
.route("/", get(graphiql).post(graphql_handler))
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
info_span!(
"http_request",
method = ?request.method(),
matched_path,
some_other_field = tracing::field::Empty,
)
}),
)
.layer(Extension(iam))
.layer(tracing_layer)
.layer(authentication_layer)
.with_state(state);

tracing::info!("starting server on {}", options.listen_address);

axum::Server::bind(&options.listen_address)
.serve(app.into_make_service())
.await
.map_err(|e| RaveApiError::Http(e.to_string()))?;
Ok(())
}

/// The GraphQL handler is the entry point for all GraphQL requests that came from the webserver
///
/// # Arguments
/// * `state` - The global state of the app
/// * `user` - The user that made the request (extracted from the JWT)
/// * `req` - The GraphQL request
#[instrument(skip(req, user, schema), fields(api_user = %user))]
async fn graphql_handler(
State(ApiState { schema, .. }): State<ApiState>,
Expand Down
18 changes: 9 additions & 9 deletions crates/rave-api/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ macro_rules! opt_from_env {
pub struct RaveApiOptions {
pub listen_address: SocketAddr,
pub database_url: String,
pub auth0: Auth0Options,
pub auth0: AuthOptions,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Auth0Options {
pub struct AuthOptions {
pub client_id: String,
pub client_secret: String,
pub domain: String,
Expand All @@ -43,22 +43,22 @@ impl RaveApiOptions {
.parse()
.map_err(|e| RaveApiError::Config(format!("invalid listen address: {}", e)))?,
database_url: opt_from_env!("DATABASE_URL", database_url)?,
auth0: Auth0Options::try_from_env()?,
auth0: AuthOptions::try_from_env()?,
})
}
}

impl Auth0Options {
impl AuthOptions {
pub fn oidc_url(&self) -> String {
format!("https://{}/.well-known/openid-configuration", self.domain)
}

pub fn try_from_env() -> RaveApiResult<Self> {
Ok(Auth0Options {
client_id: opt_from_env!("AUTH0_CLIENT_ID")?,
client_secret: opt_from_env!("AUTH0_CLIENT_SECRET")?,
domain: opt_from_env!("AUTH0_DOMAIN")?,
audience: opt_from_env!("AUTH0_AUDIENCE")?,
Ok(AuthOptions {
client_id: opt_from_env!("OIDC_CLIENT_ID")?,
client_secret: opt_from_env!("OIDC_CLIENT_SECRET")?,
domain: opt_from_env!("OIDC_DOMAIN")?,
audience: opt_from_env!("OIDC_AUDIENCE")?,
})
}
}
Loading

0 comments on commit 7f8c3a0

Please sign in to comment.