summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs31
-rw-r--r--src/error.rs48
-rw-r--r--src/lib.rs3
-rw-r--r--src/main.rs52
-rw-r--r--src/model.rs46
-rw-r--r--src/routes.rs119
-rw-r--r--src/state.rs19
7 files changed, 243 insertions, 75 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..dc132b8
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,31 @@
+#[derive(Debug, Default, Clone)]
+pub struct Config {
+ pub database_url: String,
+ pub jwt_secret: String,
+ pub jwt_expires_in: String,
+ pub jwt_maxage: i32,
+}
+
+impl Config {
+ pub fn init() -> Config {
+ let mut config = Config::default();
+
+ if let Ok(database_url) = std::env::var("DATABASE_URL") {
+ config.database_url = database_url;
+ };
+
+ if let Ok(jwt_secret) = std::env::var("JWT_SECRET") {
+ config.jwt_secret = jwt_secret;
+ };
+
+ if let Ok(jwt_expires_in) = std::env::var("JWT_EXPIRED_IN") {
+ config.jwt_expires_in = jwt_expires_in;
+ };
+
+ if let Ok(jwt_maxage) = std::env::var("JWT_MAXAGE") {
+ config.jwt_maxage = jwt_maxage.parse::<i32>().unwrap();
+ };
+
+ config
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index 1f4b354..a5b48ff 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,26 +1,46 @@
-use axum::{
- http::StatusCode,
- response::{IntoResponse, Response},
- Json,
-};
-
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
- #[error("IO error: {0}")]
+ #[error(transparent)]
IO(#[from] std::io::Error),
- #[error("Axum Error: {0:?}")]
+ #[error(transparent)]
+ TaskJoin(#[from] tokio::task::JoinError),
+
+ #[error(transparent)]
Axum(#[from] axum::Error),
+
+ #[error(transparent)]
+ Sqlx(#[from] sqlx::Error),
+
+ #[error(transparent)]
+ Migration(#[from] sqlx::migrate::MigrateError),
+
+ #[error("User not found: {0}")]
+ UserNotFound(uuid::Uuid),
}
-impl IntoResponse for Error {
- fn into_response(self) -> Response {
- let body = Json(serde_json::json!({
- "error": self.to_string(),
- }));
+impl axum::response::IntoResponse for Error {
+ fn into_response(self) -> axum::response::Response {
+ use axum::{http::StatusCode, Json};
+ use serde_json::json;
- (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
+ match self {
+ Error::UserNotFound(uuid) => (
+ StatusCode::BAD_REQUEST,
+ Json(json!({
+ "status": "fail",
+ "message": uuid,
+ })),
+ ),
+ err => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({
+ "error": err.to_string(),
+ })),
+ ),
+ }
+ .into_response()
}
}
diff --git a/src/lib.rs b/src/lib.rs
index d899368..ad634d0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,7 @@
pub use error::{Error, Result};
-pub use routes::serve;
+pub use routes::router;
pub mod error;
pub mod routes;
pub mod state;
+pub mod model;
diff --git a/src/main.rs b/src/main.rs
index 7b5d3c2..f2b81ae 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,18 +1,60 @@
+use tokio::net::TcpListener;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
+use unnamed_server::state::AppState;
+
+use crate::config::Config;
+
+mod config;
#[tokio::main]
#[tracing::instrument]
async fn main() -> Result<(), unnamed_server::Error> {
tracing_subscriber::registry()
.with(
- tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
- "example_tracing_aka_logging=debug,tower_http=debug,axum::rejection=trace".into()
- }),
+ tracing_subscriber::EnvFilter::try_from_default_env()
+ .unwrap_or_else(|_| "unnamed_server=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
- let socket_addr = std::net::SocketAddr::from(([127, 0, 0, 1], 30000));
+ let _ = dotenvy::dotenv();
+
+ let config = Config::init();
+
+ let state = AppState::new(config.database_url).await?;
+
+ let app = unnamed_server::router(state);
+
+ let listener = TcpListener::bind("127.0.0.1:30000").await?;
+
+ tracing::info!("Server listening on http://{}", listener.local_addr()?);
+
+ axum::serve(listener, app)
+ .with_graceful_shutdown(shutdown_signal())
+ .await
+ .map_err(From::from)
+}
+
+async fn shutdown_signal() {
+ let ctrl_c = async {
+ tokio::signal::ctrl_c()
+ .await
+ .expect("failed to install Ctrl+C handler");
+ };
+
+ #[cfg(unix)]
+ let terminate = async {
+ tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
+ .expect("failed to install signal handler")
+ .recv()
+ .await;
+ };
+
+ #[cfg(not(unix))]
+ let terminate = std::future::pending::<()>();
- unnamed_server::serve(socket_addr).await
+ tokio::select! {
+ _ = ctrl_c => {},
+ _ = terminate => {},
+ }
}
diff --git a/src/model.rs b/src/model.rs
new file mode 100644
index 0000000..9c1bfe6
--- /dev/null
+++ b/src/model.rs
@@ -0,0 +1,46 @@
+use chrono::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[allow(non_snake_case)]
+#[derive(Debug, Deserialize, sqlx::FromRow, Serialize, Clone)]
+pub struct User {
+ pub id: uuid::Uuid,
+ pub name: String,
+ pub email: String,
+ pub password: String,
+ #[serde(rename = "createdAt")]
+ pub created_at: Option<DateTime<Utc>>,
+ #[serde(rename = "updatedAt")]
+ pub updated_at: Option<DateTime<Utc>>,
+}
+
+impl User {
+ pub fn into_query_response(self) -> axum::Json<serde_json::Value> {
+ axum::Json(serde_json::json!({
+ "status": "success",
+ "data": serde_json::json!({
+ "user": self
+ })
+ }))
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenClaims {
+ pub sub: String,
+ pub iat: usize,
+ pub exp: usize,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RegisterUserSchema {
+ pub name: String,
+ pub email: String,
+ pub password: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct LoginUserSchema {
+ pub email: String,
+ pub password: String,
+}
diff --git a/src/routes.rs b/src/routes.rs
index 3625eff..0a81317 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -1,69 +1,80 @@
-use axum::Router;
-use axum_extra::routing::{RouterExt, TypedPath};
-use tokio::{
- net::{TcpListener, ToSocketAddrs},
- signal,
+use std::sync::Arc;
+
+use axum::{
+ extract::State,
+ http::{StatusCode, Uri},
+ Json,
};
+use axum_extra::routing::{RouterExt, TypedPath};
+use serde::Deserialize;
+
+use crate::{model::User, state::AppState, Error};
+
+pub fn router(state: AppState) -> axum::Router {
+ axum::Router::new()
+ // .route("/api/user", get(get_user))
+ .typed_get(HealthCheck::get)
+ .typed_get(UserId::get)
+ .fallback(fallback)
+ .with_state(Arc::new(state))
+}
+
+#[derive(Debug, Deserialize, TypedPath)]
+#[typed_path("/api/healthcheck")]
+pub struct HealthCheck;
-use crate::Error;
+impl HealthCheck {
+ pub async fn get(self) -> Json<serde_json::Value> {
+ const MESSAGE: &str = "Unnamed server";
-#[derive(TypedPath)]
-#[typed_path("/ping")]
-pub struct Ping;
+ let json_response = serde_json::json!({
+ "status": "success",
+ "message": MESSAGE
+ });
-/// # Test endpoint
-///
-/// Returns "pong"
-#[tracing::instrument(ret)]
-pub async fn ping(_: Ping) -> &'static str {
- "pong"
+ Json(json_response)
+ }
}
-#[derive(TypedPath)]
-#[typed_path("/")]
-pub struct Root;
+#[derive(Debug, Deserialize, TypedPath)]
+#[typed_path("/api/user/:uuid")]
+pub struct UserId {
+ pub uuid: uuid::Uuid,
+}
-#[tracing::instrument(ret)]
-pub async fn root(_: Root) -> &'static str {
- "Hello, World!"
+impl UserId {
+ /// Get a user via their `id`
+ #[tracing::instrument(ret, skip(data))]
+ pub async fn get(
+ self,
+ State(data): State<Arc<AppState>>,
+ ) -> Result<Json<serde_json::Value>, Error> {
+ sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", self.uuid)
+ .fetch_optional(&data.db_pool)
+ .await?
+ .ok_or_else(|| Error::UserNotFound(self.uuid))
+ .map(User::into_query_response)
+ }
}
-pub async fn serve<A>(addr: A) -> Result<(), Error>
-where
- A: ToSocketAddrs,
-{
- let app = Router::new().typed_get(root).typed_get(ping);
+pub async fn fallback(uri: Uri) -> (StatusCode, String) {
+ (StatusCode::NOT_FOUND, format!("Route not found: {uri}"))
+}
- let listener = TcpListener::bind(addr).await?;
+#[cfg(test)]
+mod tests {
+ use super::*;
- tracing::info!("Server listening on http://{}", listener.local_addr()?);
+ use axum_test::TestServer;
- axum::serve(listener, app)
- .with_graceful_shutdown(shutdown_signal())
- .await
- .map_err(From::from)
-}
+ #[tokio::test]
+ async fn test_fallback() -> Result<(), Box<dyn std::error::Error>> {
+ let server = TestServer::new(axum::Router::new().fallback(fallback))?;
+
+ let response = server.get("/fallback").await;
+
+ assert_eq!(StatusCode::NOT_FOUND, response.status_code());
-async fn shutdown_signal() {
- let ctrl_c = async {
- signal::ctrl_c()
- .await
- .expect("failed to install Ctrl+C handler");
- };
-
- #[cfg(unix)]
- let terminate = async {
- signal::unix::signal(signal::unix::SignalKind::terminate())
- .expect("failed to install signal handler")
- .recv()
- .await;
- };
-
- #[cfg(not(unix))]
- let terminate = std::future::pending::<()>();
-
- tokio::select! {
- _ = ctrl_c => {},
- _ = terminate => {},
+ Ok(())
}
}
diff --git a/src/state.rs b/src/state.rs
index 78a6654..efe2192 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1 +1,18 @@
-pub struct AppState;
+use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
+
+use crate::Error;
+
+pub struct AppState {
+ pub db_pool: Pool<Postgres>,
+}
+
+impl AppState {
+ pub async fn new<S: AsRef<str>>(db_url: S) -> Result<Self, Error> {
+ let db_pool = PgPoolOptions::new()
+ .max_connections(10)
+ .connect(db_url.as_ref())
+ .await?;
+
+ Ok(Self { db_pool })
+ }
+}