summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json22
-rw-r--r--.sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json (renamed from .sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json)15
-rw-r--r--.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json22
-rw-r--r--.sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json (renamed from .sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json)14
-rw-r--r--.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json22
-rw-r--r--Cargo.toml2
-rw-r--r--fixtures/users.sql4
-rw-r--r--migrations/20240321225523_init.down.sql1
-rw-r--r--migrations/20240321225523_init.up.sql15
-rw-r--r--migrations/20240412153145_create_credential.down.sql2
-rw-r--r--migrations/20240412153145_create_credential.up.sql8
-rw-r--r--migrations/20240412153150_create_user_.down.sql1
-rw-r--r--migrations/20240412153150_create_user_.up.sql7
-rw-r--r--src/api.rs8
-rw-r--r--src/api/error.rs5
-rw-r--r--src/api/users.rs123
-rw-r--r--src/auth.rs84
-rw-r--r--src/lib.rs9
-rw-r--r--src/main.rs4
-rw-r--r--src/state.rs18
20 files changed, 196 insertions, 190 deletions
diff --git a/.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json b/.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json
new file mode 100644
index 0000000..35f05d9
--- /dev/null
+++ b/.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT password_hash FROM credential WHERE id = $1",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "password_hash",
+ "type_info": "Varchar"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Uuid"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c"
+}
diff --git a/.sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json b/.sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json
index 28227a3..ec353da 100644
--- a/.sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json
+++ b/.sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
- "query": "INSERT INTO users (name,email) VALUES ($1, $2) RETURNING *",
+ "query": "INSERT INTO user_ (id,name,email) VALUES ($1, $2, $3) RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
- "name": "uuid",
+ "name": "id",
"type_info": "Uuid"
},
{
@@ -20,22 +20,18 @@
},
{
"ordinal": 3,
- "name": "session_epoch",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
- "ordinal": 5,
+ "ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
+ "Uuid",
"Varchar",
"Varchar"
]
@@ -45,9 +41,8 @@
false,
false,
false,
- false,
false
]
},
- "hash": "9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1"
+ "hash": "6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df"
}
diff --git a/.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json b/.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json
new file mode 100644
index 0000000..ea84914
--- /dev/null
+++ b/.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT EXISTS(SELECT 1 FROM user_ WHERE email = $1 LIMIT 1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ },
+ "hash": "9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06"
+}
diff --git a/.sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json b/.sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json
index 97b0e16..a857287 100644
--- a/.sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json
+++ b/.sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT * FROM users WHERE uuid = $1 LIMIT 1",
+ "query": "SELECT * FROM user_ WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
- "name": "uuid",
+ "name": "id",
"type_info": "Uuid"
},
{
@@ -20,16 +20,11 @@
},
{
"ordinal": 3,
- "name": "session_epoch",
- "type_info": "Timestamptz"
- },
- {
- "ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
- "ordinal": 5,
+ "ordinal": 4,
"name": "updated_at",
"type_info": "Timestamptz"
}
@@ -44,9 +39,8 @@
false,
false,
false,
- false,
false
]
},
- "hash": "5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d"
+ "hash": "d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827"
}
diff --git a/.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json b/.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json
new file mode 100644
index 0000000..7287db6
--- /dev/null
+++ b/.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO credential (password_hash) VALUES ($1) RETURNING id",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Varchar"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d"
+}
diff --git a/Cargo.toml b/Cargo.toml
index 8696c1a..9ee0b70 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ thiserror = "1.0.58"
time = { version = "0.3.34", features = ["serde", "serde-human-readable"] }
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread", "signal"] }
toml = "0.8.12"
+tower = { version = "0.4.13", features = ["util"] }
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
@@ -29,7 +30,6 @@ uuid = { version = "1.8.0", features = ["serde", "v4"] }
[dev-dependencies]
pgtemp = "0.2.1"
-tower = { version = "0.4.13", features = ["util"] }
mime = "0.3.17"
http-body-util = "0.1.1"
test-log = { version = "0.2.15", features = ["trace"] }
diff --git a/fixtures/users.sql b/fixtures/users.sql
index 5904b1a..96c34a9 100644
--- a/fixtures/users.sql
+++ b/fixtures/users.sql
@@ -1,10 +1,10 @@
-INSERT INTO users (uuid, name, email) VALUES(
+INSERT INTO user_ (id, name, email) VALUES(
'4c14f795-86f0-4361-a02f-0edb966fb145',
'Arthur Dent',
'adent@earth.sol'
);
-INSERT INTO credentials (uuid, password_hash) VALUES(
+INSERT INTO credential (id, password_hash) VALUES(
'4c14f795-86f0-4361-a02f-0edb966fb145',
'$argon2id$v=19$m=19456,t=2,p=1$31LeWXQsq0wwHT0MgAliVA$V6pQ0nKpgcq+nOWT6p4AuyVM0zy/09Ct9XpSPHq3wSo'
);
diff --git a/migrations/20240321225523_init.down.sql b/migrations/20240321225523_init.down.sql
deleted file mode 100644
index e3470ec..0000000
--- a/migrations/20240321225523_init.down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE IF EXISTS users, credentials;
diff --git a/migrations/20240321225523_init.up.sql b/migrations/20240321225523_init.up.sql
deleted file mode 100644
index ca86893..0000000
--- a/migrations/20240321225523_init.up.sql
+++ /dev/null
@@ -1,15 +0,0 @@
-CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-
-CREATE TABLE users (
- uuid UUID NOT NULL PRIMARY KEY DEFAULT (uuid_generate_v4()),
- name VARCHAR(100) NOT NULL,
- email VARCHAR(255) NOT NULL UNIQUE,
- session_epoch TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
-);
-
-CREATE TABLE credentials (
- uuid UUID NOT NULL PRIMARY KEY,
- password_hash VARCHAR(100) NOT NULL
-);
diff --git a/migrations/20240412153145_create_credential.down.sql b/migrations/20240412153145_create_credential.down.sql
new file mode 100644
index 0000000..d2cb87f
--- /dev/null
+++ b/migrations/20240412153145_create_credential.down.sql
@@ -0,0 +1,2 @@
+DROP TABLE credential;
+DROP EXTENSION "uuid-ossp";
diff --git a/migrations/20240412153145_create_credential.up.sql b/migrations/20240412153145_create_credential.up.sql
new file mode 100644
index 0000000..a2f0920
--- /dev/null
+++ b/migrations/20240412153145_create_credential.up.sql
@@ -0,0 +1,8 @@
+CREATE EXTENSION "uuid-ossp";
+
+CREATE TABLE credential (
+ id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
+ password_hash VARCHAR(100) NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
+);
diff --git a/migrations/20240412153150_create_user_.down.sql b/migrations/20240412153150_create_user_.down.sql
new file mode 100644
index 0000000..2ed90ec
--- /dev/null
+++ b/migrations/20240412153150_create_user_.down.sql
@@ -0,0 +1 @@
+DROP TABLE user_;
diff --git a/migrations/20240412153150_create_user_.up.sql b/migrations/20240412153150_create_user_.up.sql
new file mode 100644
index 0000000..e13fd90
--- /dev/null
+++ b/migrations/20240412153150_create_user_.up.sql
@@ -0,0 +1,7 @@
+CREATE TABLE user_ (
+ id UUID NOT NULL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
+);
diff --git a/src/api.rs b/src/api.rs
index f74e33a..17cbd03 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -2,14 +2,13 @@ use axum::{response::IntoResponse, routing::get};
use crate::state::AppState;
-mod users;
pub mod error;
+mod users;
-pub fn router(state: AppState) -> axum::Router {
+pub fn router() -> axum::Router<AppState> {
axum::Router::new()
.merge(users::router())
.route("/healthcheck", get(healthcheck))
- .with_state(state)
}
pub async fn healthcheck() -> impl IntoResponse {
@@ -25,6 +24,7 @@ mod tests {
use axum::{
body::Body,
http::{Request, StatusCode},
+ Router,
};
use sqlx::PgPool;
use tower::ServiceExt;
@@ -33,7 +33,7 @@ mod tests {
async fn test_healthcheck_ok(pool: PgPool) -> TestResult {
setup_test_env();
- let router = router(AppState { pool });
+ let router = Router::new().merge(router()).with_state(AppState { pool });
let request = Request::builder().uri("/healthcheck").body(Body::empty())?;
diff --git a/src/api/error.rs b/src/api/error.rs
index 4088b9b..f5d4291 100644
--- a/src/api/error.rs
+++ b/src/api/error.rs
@@ -15,6 +15,9 @@ pub enum Error {
#[error("Invalid email: {0}")]
EmailInvalid(#[from] email_address::Error),
+ #[error("Failed to reach authentication server: {0}")]
+ AuthRequest(#[from] axum::http::Error),
+
#[error("Authentication error: {0}")]
Auth(#[from] crate::auth::error::Error),
}
@@ -28,7 +31,7 @@ impl axum::response::IntoResponse for Error {
Self::EmailExists => StatusCode::CONFLICT,
Self::EmailInvalid(_) => StatusCode::UNPROCESSABLE_ENTITY,
Self::InvalidToken => StatusCode::UNAUTHORIZED,
- Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR,
+ Self::AuthRequest(_) | Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Auth(err) => return err.into_response(),
};
diff --git a/src/api/users.rs b/src/api/users.rs
index 7a9bb6e..2440e6e 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -22,28 +22,13 @@ pub fn router() -> Resource<AppState> {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)]
#[serde(rename_all = "camelCase")]
pub struct UserSchema {
- pub uuid: Uuid,
+ pub id: Uuid,
pub name: String,
pub email: String,
- pub session_epoch: OffsetDateTime,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
-impl Default for UserSchema {
- fn default() -> Self {
- let now = time::OffsetDateTime::now_utc();
- Self {
- uuid: Default::default(),
- name: Default::default(),
- email: Default::default(),
- session_epoch: now,
- created_at: now,
- updated_at: now,
- }
- }
-}
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegisterSchema {
pub name: String,
@@ -61,37 +46,35 @@ pub async fn create(
) -> impl IntoResponse {
email_address::EmailAddress::from_str(&email)?;
- let exists: Option<bool> =
- sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1 LIMIT 1)")
- .bind(email.to_ascii_lowercase())
- .fetch_one(&state.pool)
- .await?;
+ let exists: Option<bool> = sqlx::query_scalar!(
+ "SELECT EXISTS(SELECT 1 FROM user_ WHERE email = $1 LIMIT 1)",
+ email.to_ascii_lowercase()
+ )
+ .fetch_one(&state.pool)
+ .await?;
if exists.is_some_and(|b| b) {
return Err(Error::EmailExists);
}
- let mut transaction = state.pool.begin().await?;
+ // TODO: Move this into a tower service
+ let (status, (access, refresh)) = crate::auth::create(
+ State(state.clone()),
+ TypedHeader(Authorization::basic(&email, &password)),
+ )
+ .await?;
let user = sqlx::query_as!(
UserSchema,
- "INSERT INTO users (name,email) VALUES ($1, $2) RETURNING *",
+ "INSERT INTO user_ (id,name,email) VALUES ($1, $2, $3) RETURNING *",
+ refresh.sub,
name,
email.to_ascii_lowercase(),
)
- .fetch_one(&mut *transaction)
+ .fetch_one(&state.pool)
.await?;
- let (parts, _) = crate::auth::create(
- State(state),
- TypedHeader(Authorization::basic(&user.uuid.to_string(), &password)),
- )
- .await?
- .into_response()
- .into_parts();
-
- transaction.commit().await?;
- Ok((parts, Json(user)))
+ Ok((status, access, refresh, Json(user)))
}
pub async fn show(
@@ -103,15 +86,11 @@ pub async fn show(
return Err(Error::InvalidToken);
}
- sqlx::query_as!(
- UserSchema,
- "SELECT * FROM users WHERE uuid = $1 LIMIT 1",
- sub
- )
- .fetch_optional(&state.pool)
- .await?
- .ok_or_else(|| Error::UserNotFound)
- .map(Json)
+ sqlx::query_as!(UserSchema, "SELECT * FROM user_ WHERE id = $1 LIMIT 1", sub)
+ .fetch_optional(&state.pool)
+ .await?
+ .ok_or_else(|| Error::UserNotFound)
+ .map(Json)
}
#[cfg(test)]
@@ -136,7 +115,10 @@ mod tests {
tests::{setup_test_env, TestResult},
};
- const UUID: uuid::Uuid = uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145");
+ const USER_ID: Uuid = uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145");
+ const USER_NAME: &str = "Arthur Dent";
+ const USER_EMAIL: &str = "adent@earth.sol";
+ const USER_PASSWORD: &str = "solongandthanksforallthefish";
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn test_uuid_ok(pool: PgPool) -> TestResult {
@@ -144,16 +126,9 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
- let user = UserSchema {
- uuid: UUID,
- name: "Arthur Dent".to_string(),
- email: "adent@earth.sol".to_string(),
- ..Default::default()
- };
-
let request = Request::builder()
- .uri(format!("/users/{UUID}"))
- .header(COOKIE, HeaderValue::try_from(AccessClaims::new(UUID))?)
+ .uri(format!("/users/{}", USER_ID))
+ .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?)
.body(Body::empty())?;
let response = router.oneshot(request).await?;
@@ -162,12 +137,12 @@ mod tests {
let body_bytes = response.into_body().collect().await?.to_bytes();
let UserSchema {
- uuid, name, email, ..
+ id, name, email, ..
} = serde_json::from_slice(&body_bytes)?;
- assert_eq!(user.uuid, uuid);
- assert_eq!(user.name, name);
- assert_eq!(user.email, email);
+ assert_eq!(USER_ID, id);
+ assert_eq!(USER_NAME, name);
+ assert_eq!(USER_EMAIL, email);
Ok(())
}
@@ -179,8 +154,8 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
let request = Request::builder()
- .uri(format!("/users/{UUID}"))
- .header(COOKIE, HeaderValue::try_from(AccessClaims::new(UUID))?)
+ .uri(format!("/users/{}", USER_ID))
+ .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?)
.body(Body::empty())?;
let response = router.oneshot(request).await?;
@@ -197,7 +172,7 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
let request = Request::builder()
- .uri(format!("/users/{UUID}"))
+ .uri(format!("/users/{}", USER_ID))
.header(
COOKIE,
HeaderValue::try_from(AccessClaims::new(uuid::Uuid::new_v4()))?,
@@ -218,7 +193,7 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
let request = Request::builder()
- .uri(format!("/users/{UUID}"))
+ .uri(format!("/users/{}", USER_ID))
.header(COOKIE, "token=sadfasdfsdfs")
.body(Body::empty())?;
@@ -236,7 +211,7 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
let request = Request::builder()
- .uri(format!("/users/{UUID}"))
+ .uri(format!("/users/{}", USER_ID))
.body(Body::empty())?;
let response = router.oneshot(request).await?;
@@ -252,11 +227,11 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
- let user = RegisterSchema {
- name: "Arthur Dent".to_string(),
- email: "adent@earth.sol".to_string(),
- password: "solongandthanksforallthefish".to_string(),
- };
+ let user = serde_json::json!( {
+ "name": USER_NAME,
+ "email": USER_EMAIL,
+ "password": USER_PASSWORD,
+ });
let request = Request::builder()
.uri("/users")
@@ -271,8 +246,8 @@ mod tests {
let body_bytes = response.into_body().collect().await?.to_bytes();
let UserSchema { name, email, .. } = serde_json::from_slice(&body_bytes)?;
- assert_eq!(user.name, name);
- assert_eq!(user.email, email);
+ assert_eq!(USER_NAME, name);
+ assert_eq!(USER_EMAIL, email);
Ok(())
}
@@ -283,11 +258,11 @@ mod tests {
let router = Router::new().merge(router()).with_state(AppState { pool });
- let user = RegisterSchema {
- name: "Arthur Dent".to_string(),
- email: "adent@earth.sol".to_string(),
- password: "solongandthanksforallthefish".to_string(),
- };
+ let user = serde_json::json!( {
+ "name": USER_NAME,
+ "email": USER_EMAIL,
+ "password": USER_PASSWORD,
+ });
let request = Request::builder()
.uri("/users")
diff --git a/src/auth.rs b/src/auth.rs
index 3e5443f..8756291 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -2,14 +2,10 @@ use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
};
-use axum::{
- extract::State,
- http::StatusCode,
- response::IntoResponse,
- routing::{get, post},
-};
+use axum::{extract::State, http::StatusCode};
use axum_extra::{
headers::{authorization::Basic, Authorization},
+ routing::Resource,
TypedHeader,
};
use uuid::Uuid;
@@ -24,60 +20,51 @@ pub mod claims;
pub mod error;
pub mod jwt;
-pub fn router(state: AppState) -> axum::Router {
- axum::Router::new()
- .route("/create", post(create))
- .route("/issue", get(issue))
- .route("/refresh", get(refresh))
- .with_state(state)
-}
-
-pub async fn create(
- State(state): State<AppState>,
- TypedHeader(Authorization(basic)): TypedHeader<Authorization<Basic>>,
-) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> {
- let uuid = Uuid::try_parse(basic.username())?;
-
- let salt = SaltString::generate(&mut OsRng);
- let password_hash = Argon2::default().hash_password(basic.password().as_bytes(), &salt)?;
-
- let rows_affected = sqlx::query("INSERT INTO credentials (uuid,password_hash) VALUES ($1, $2)")
- .bind(uuid)
- .bind(password_hash.to_string())
- .execute(&state.pool)
- .await?
- .rows_affected();
-
- if rows_affected == 0 {
- Err(Error::Registration)
- } else {
- Ok((
- StatusCode::CREATED,
- issue(State(state), TypedHeader(Authorization(basic))).await?,
- ))
- }
+pub fn router() -> Resource<AppState> {
+ Resource::named("auth").index(issue).create(create)
}
pub async fn issue(
State(state): State<AppState>,
TypedHeader(Authorization(basic)): TypedHeader<Authorization<Basic>>,
) -> Result<(AccessClaims, RefreshClaims), Error> {
- let uuid = basic.username().try_into()?;
+ let uuid = Uuid::try_parse(basic.username())?;
- let p: String = sqlx::query_scalar("SELECT password_hash FROM credentials WHERE uuid = $1")
- .bind(uuid)
+ let p: String = sqlx::query_scalar!("SELECT password_hash FROM credential WHERE id = $1", uuid)
.fetch_optional(&state.pool)
.await?
.ok_or(Error::LoginInvalid)?;
Argon2::default().verify_password(basic.password().as_bytes(), &PasswordHash::new(&p)?)?;
- let claims = RefreshClaims::new(uuid);
+ let refresh = RefreshClaims::new(uuid);
+ let access = refresh.refresh();
+ Ok((access, refresh))
+}
+
+pub async fn create(
+ State(state): State<AppState>,
+ TypedHeader(Authorization(basic)): TypedHeader<Authorization<Basic>>,
+) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> {
+ let salt = SaltString::generate(&mut OsRng);
+ let password_hash = Argon2::default().hash_password(basic.password().as_bytes(), &salt)?;
+
+ let uuid = sqlx::query!(
+ "INSERT INTO credential (password_hash) VALUES ($1) RETURNING id",
+ password_hash.to_string()
+ )
+ .fetch_optional(&state.pool)
+ .await?
+ .ok_or(Error::Registration)?
+ .id;
+
+ let refresh = RefreshClaims::new(uuid);
+ let access = refresh.refresh();
- Ok((claims.refresh(), claims))
+ Ok((StatusCode::CREATED, (access, refresh)))
}
-pub async fn refresh(claims: RefreshClaims) -> impl IntoResponse {
+pub async fn refresh(claims: RefreshClaims) -> AccessClaims {
claims.refresh()
}
@@ -88,6 +75,7 @@ mod tests {
use axum::{
body::Body,
http::{header::AUTHORIZATION, Request, StatusCode},
+ Router,
};
use axum_extra::headers::authorization::Credentials;
use sqlx::PgPool;
@@ -110,7 +98,7 @@ mod tests {
async fn test_issue_ok(pool: PgPool) -> TestResult {
setup_test_env();
- let router = router(AppState { pool });
+ let router = Router::new().merge(router()).with_state(AppState { pool });
let auth = Authorization::basic(
"4c14f795-86f0-4361-a02f-0edb966fb145",
@@ -118,7 +106,7 @@ mod tests {
);
let request = Request::builder()
- .uri("/issue")
+ .uri("/auth")
.method("GET")
.header(AUTHORIZATION, auth.0.encode())
.body(Body::empty())?;
@@ -135,12 +123,12 @@ mod tests {
async fn test_issue_unauthorized(pool: PgPool) -> TestResult {
setup_test_env();
- let router = router(AppState { pool });
+ let router = Router::new().merge(router()).with_state(AppState { pool });
let auth = Authorization::basic("4c14f795-86f0-4361-a02f-0edb966fb145", "hunter2");
let request = Request::builder()
- .uri("/issue")
+ .uri("/auth")
.method("GET")
.header(AUTHORIZATION, auth.0.encode())
.body(Body::empty())?;
diff --git a/src/lib.rs b/src/lib.rs
index 8be6698..2afed5e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,10 +9,10 @@ pub mod error;
pub mod state;
pub mod utils;
-pub fn router(state: state::AppState) -> axum::Router {
+pub fn router() -> axum::Router<state::AppState> {
axum::Router::new()
- .nest("/api", api::router(state.clone()))
- .nest("/auth", auth::router(state.clone()))
+ .nest("/api", api::router())
+ .merge(auth::router())
.fallback(fallback)
// TODO: do this correctly!
.layer(CorsLayer::permissive())
@@ -34,6 +34,7 @@ pub(crate) mod tests {
use axum::{
body::Body,
http::{Request, StatusCode},
+ Router,
};
use sqlx::PgPool;
use tower::ServiceExt;
@@ -60,7 +61,7 @@ pub(crate) mod tests {
async fn test_fallback_not_found(pool: PgPool) -> TestResult {
setup_test_env();
- let router = router(AppState { pool });
+ let router = Router::new().merge(router()).with_state(AppState { pool });
let request = Request::builder()
.uri("/does-not-exist")
diff --git a/src/main.rs b/src/main.rs
index ae1df98..71e68f9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,8 +21,8 @@ async fn main() -> Result<(), main_error::MainError> {
let config = Config::builder().file()?.env().build()?;
let listener = TcpListener::bind(config.listen_addr).await?;
- let app_state = AppState::new(config.database_url).await?;
- let router = unnamed_server::router(app_state);
+ let state = AppState::new(config.database_url).await?;
+ let router = unnamed_server::router().with_state(state);
tracing::info!("Listening on http://{}", listener.local_addr()?);
diff --git a/src/state.rs b/src/state.rs
index 4f365b9..75c1e11 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1,10 +1,5 @@
use std::fmt::Debug;
-use axum::{
- async_trait,
- extract::{FromRef, FromRequestParts},
- http::request::Parts,
-};
use sqlx::{Pool, Postgres};
use crate::Error;
@@ -30,16 +25,3 @@ impl AppState {
Ok(Self { pool })
}
}
-
-#[async_trait]
-impl<S> FromRequestParts<S> for AppState
-where
- Self: FromRef<S>,
- S: Send + Sync + Debug,
-{
- type Rejection = Error;
-
- async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
- Ok(Self::from_ref(state))
- }
-}