Add authentication to more handlers
This commit is contained in:
parent
eb7706974e
commit
d1c933e1ea
@ -64,6 +64,7 @@ pub fn migration() -> String {
|
||||
|
||||
m.create_table("interior_ref_lists", |t| {
|
||||
t.add_column("id", types::primary().indexed(true));
|
||||
// TODO make shop_id unique, recover unique_violation
|
||||
t.add_column("shop_id", types::foreign("shops", "id").indexed(true));
|
||||
t.add_column("owner_id", types::foreign("owners", "id").indexed(true));
|
||||
t.add_column("ref_list", types::custom("jsonb"));
|
||||
|
@ -48,6 +48,7 @@ pub fn create_shop(
|
||||
warp::path::end()
|
||||
.and(warp::post())
|
||||
.and(json_body::<Shop>())
|
||||
.and(warp::header::optional("api-key"))
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::create_shop)
|
||||
}
|
||||
@ -86,6 +87,7 @@ pub fn create_owner(
|
||||
.and(warp::post())
|
||||
.and(json_body::<Owner>())
|
||||
.and(warp::addr::remote())
|
||||
.and(warp::header::optional("api-key"))
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::create_owner)
|
||||
}
|
||||
@ -125,6 +127,7 @@ pub fn create_interior_ref_list(
|
||||
warp::path::end()
|
||||
.and(warp::post())
|
||||
.and(json_body::<InteriorRefList>())
|
||||
.and(warp::header::optional("api-key"))
|
||||
.and(with_env(env))
|
||||
.and_then(handlers::create_interior_ref_list)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ipnetwork::IpNetwork;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::net::SocketAddr;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
@ -9,26 +8,22 @@ use warp::reply::{json, with_header, with_status};
|
||||
use warp::{Rejection, Reply};
|
||||
|
||||
use super::models::{InteriorRefList, ListParams, Model, Owner, Shop};
|
||||
use super::problem::{forbidden_no_api_key, forbidden_no_owner, reject_anyhow};
|
||||
use super::problem::{reject_anyhow, unauthorized_no_api_key, unauthorized_no_owner};
|
||||
use super::Environment;
|
||||
use crate::caches::Cache;
|
||||
|
||||
#[instrument(level = "debug", skip(db, cache, api_key))]
|
||||
pub async fn authenticate(
|
||||
db: &PgPool,
|
||||
cache: &Cache<Uuid, i32>,
|
||||
api_key: Option<Uuid>,
|
||||
) -> Result<i32> {
|
||||
#[instrument(level = "debug", skip(env, api_key))]
|
||||
pub async fn authenticate(env: &Environment, api_key: Option<Uuid>) -> Result<i32> {
|
||||
if let Some(api_key) = api_key {
|
||||
cache
|
||||
env.caches
|
||||
.owner_ids_by_api_key
|
||||
.get(api_key, || async {
|
||||
Ok(
|
||||
sqlx::query!("SELECT id FROM owners WHERE api_key = $1", api_key)
|
||||
.fetch_one(db)
|
||||
.fetch_one(&env.db)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
if let sqlx::Error::RowNotFound = error {
|
||||
return forbidden_no_owner();
|
||||
return unauthorized_no_owner();
|
||||
}
|
||||
anyhow!(error)
|
||||
})?
|
||||
@ -37,8 +32,7 @@ pub async fn authenticate(
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
// TODO: this should be 401 status instead
|
||||
Err(forbidden_no_api_key())
|
||||
Err(unauthorized_no_api_key())
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,10 +63,20 @@ pub async fn list_shops(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_shop(shop: Shop, env: Environment) -> Result<impl Reply, Rejection> {
|
||||
// TODO: authenticate
|
||||
// TODO: return 400 error with message if unique key is violated
|
||||
let saved_shop = shop.save(&env.db).await.map_err(reject_anyhow)?;
|
||||
pub async fn create_shop(
|
||||
shop: Shop,
|
||||
api_key: Option<Uuid>,
|
||||
env: Environment,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
|
||||
let shop_with_owner_id = Shop {
|
||||
owner_id: Some(owner_id),
|
||||
..shop
|
||||
};
|
||||
let saved_shop = shop_with_owner_id
|
||||
.save(&env.db)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
let url = saved_shop.url(&env.api_url).map_err(reject_anyhow)?;
|
||||
let reply = json(&saved_shop);
|
||||
let reply = with_header(reply, "Location", url.as_str());
|
||||
@ -86,9 +90,7 @@ pub async fn delete_shop(
|
||||
api_key: Option<Uuid>,
|
||||
env: Environment,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let owner_id = authenticate(&env.db, &env.caches.owner_ids_by_api_key, api_key)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
|
||||
Shop::delete(&env.db, owner_id, id)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
@ -131,23 +133,34 @@ pub async fn list_owners(
|
||||
pub async fn create_owner(
|
||||
owner: Owner,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
api_key: Option<Uuid>,
|
||||
env: Environment,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
// TODO: authenticate and get api_key from header
|
||||
let owner_with_ip = match remote_addr {
|
||||
Some(addr) => Owner {
|
||||
ip_address: Some(IpNetwork::from(addr.ip())),
|
||||
..owner
|
||||
},
|
||||
None => owner,
|
||||
};
|
||||
let saved_owner = owner_with_ip.save(&env.db).await.map_err(reject_anyhow)?;
|
||||
let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?;
|
||||
let reply = json(&saved_owner);
|
||||
let reply = with_header(reply, "Location", url.as_str());
|
||||
let reply = with_status(reply, StatusCode::CREATED);
|
||||
env.caches.list_owners.clear().await;
|
||||
Ok(reply)
|
||||
if let Some(api_key) = api_key {
|
||||
let owner_with_ip_and_key = match remote_addr {
|
||||
Some(addr) => Owner {
|
||||
api_key: Some(api_key),
|
||||
ip_address: Some(IpNetwork::from(addr.ip())),
|
||||
..owner
|
||||
},
|
||||
None => Owner {
|
||||
api_key: Some(api_key),
|
||||
..owner
|
||||
},
|
||||
};
|
||||
let saved_owner = owner_with_ip_and_key
|
||||
.save(&env.db)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?;
|
||||
let reply = json(&saved_owner);
|
||||
let reply = with_header(reply, "Location", url.as_str());
|
||||
let reply = with_status(reply, StatusCode::CREATED);
|
||||
env.caches.list_owners.clear().await;
|
||||
Ok(reply)
|
||||
} else {
|
||||
Err(reject_anyhow(unauthorized_no_api_key()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_owner(
|
||||
@ -155,9 +168,7 @@ pub async fn delete_owner(
|
||||
api_key: Option<Uuid>,
|
||||
env: Environment,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let owner_id = authenticate(&env.db, &env.caches.owner_ids_by_api_key, api_key)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
|
||||
Owner::delete(&env.db, owner_id, id)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
@ -204,10 +215,15 @@ pub async fn list_interior_ref_lists(
|
||||
|
||||
pub async fn create_interior_ref_list(
|
||||
interior_ref_list: InteriorRefList,
|
||||
api_key: Option<Uuid>,
|
||||
env: Environment,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
// TODO: authenticate
|
||||
let saved_interior_ref_list = interior_ref_list
|
||||
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
|
||||
let ref_list_with_owner_id = InteriorRefList {
|
||||
owner_id: Some(owner_id),
|
||||
..interior_ref_list
|
||||
};
|
||||
let saved_interior_ref_list = ref_list_with_owner_id
|
||||
.save(&env.db)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
@ -226,9 +242,7 @@ pub async fn delete_interior_ref_list(
|
||||
api_key: Option<Uuid>,
|
||||
env: Environment,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let owner_id = authenticate(&env.db, &env.caches.owner_ids_by_api_key, api_key)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
|
||||
InteriorRefList::delete(&env.db, owner_id, id)
|
||||
.await
|
||||
.map_err(reject_anyhow)?;
|
||||
|
@ -73,7 +73,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
let host = env::var("HOST").expect("`HOST` environment variable not defined");
|
||||
let host_url = Url::parse(&host).expect("Cannot parse URL from `HOST` environment variable");
|
||||
let api_url = host_url.join("/api/v1")?;
|
||||
let api_url = host_url.join("/api/v1/")?;
|
||||
let env = Environment::new(api_url).await?;
|
||||
|
||||
let base = warp::path("api").and(warp::path("v1"));
|
||||
|
@ -32,7 +32,7 @@ pub struct InteriorRef {
|
||||
pub struct InteriorRefList {
|
||||
pub id: Option<i32>,
|
||||
pub shop_id: i32,
|
||||
pub owner_id: i32,
|
||||
pub owner_id: Option<i32>,
|
||||
pub ref_list: Json<Vec<InteriorRef>>,
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
|
@ -14,7 +14,7 @@ where
|
||||
fn pk(&self) -> Option<i32>;
|
||||
fn url(&self, api_url: &Url) -> Result<Url> {
|
||||
if let Some(pk) = self.pk() {
|
||||
Ok(api_url.join(&format!("/{}s/{}", Self::resource_name(), pk))?)
|
||||
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Cannot get URL for {} with no primary key",
|
||||
|
@ -15,7 +15,9 @@ use crate::problem::forbidden_permission;
|
||||
pub struct Owner {
|
||||
pub id: Option<i32>,
|
||||
pub name: String,
|
||||
pub api_key: Uuid,
|
||||
#[serde(skip_serializing)]
|
||||
pub api_key: Option<Uuid>,
|
||||
#[serde(skip_serializing)]
|
||||
pub ip_address: Option<IpNetwork>,
|
||||
pub mod_version: String,
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
@ -40,7 +42,7 @@ impl Model for Owner {
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(db))]
|
||||
#[instrument(level = "debug", skip(self, db))]
|
||||
async fn save(self, db: &PgPool) -> Result<Self> {
|
||||
Ok(sqlx::query_as!(
|
||||
Self,
|
||||
|
@ -13,7 +13,7 @@ use crate::problem::forbidden_permission;
|
||||
pub struct Shop {
|
||||
pub id: Option<i32>,
|
||||
pub name: String,
|
||||
pub owner_id: i32,
|
||||
pub owner_id: Option<i32>,
|
||||
pub description: String,
|
||||
pub is_not_sell_buy: bool,
|
||||
pub sell_buy_list_id: i32,
|
||||
@ -41,7 +41,7 @@ impl Model for Shop {
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(db))]
|
||||
#[instrument(level = "debug", skip(self, db))]
|
||||
async fn save(self, db: &PgPool) -> Result<Self> {
|
||||
Ok(sqlx::query_as!(
|
||||
Self,
|
||||
|
@ -11,16 +11,16 @@ pub fn forbidden_permission() -> Error {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn forbidden_no_owner() -> Error {
|
||||
pub fn unauthorized_no_owner() -> Error {
|
||||
anyhow!(
|
||||
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,)
|
||||
HttpApiProblem::with_title_and_type_from_status(StatusCode::UNAUTHORIZED,)
|
||||
.set_detail("Api-Key not recognized")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn forbidden_no_api_key() -> Error {
|
||||
pub fn unauthorized_no_api_key() -> Error {
|
||||
anyhow!(
|
||||
HttpApiProblem::with_title_and_type_from_status(StatusCode::FORBIDDEN,)
|
||||
HttpApiProblem::with_title_and_type_from_status(StatusCode::UNAUTHORIZED,)
|
||||
.set_detail("Api-Key header not present")
|
||||
)
|
||||
}
|
||||
@ -42,14 +42,37 @@ pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
|
||||
db_error.message(),
|
||||
db_error.details().unwrap_or("")
|
||||
);
|
||||
dbg!(&db_error);
|
||||
if let Some(code) = db_error.code() {
|
||||
dbg!(&code);
|
||||
if let Some(constraint) = db_error.constraint_name() {
|
||||
dbg!(&constraint);
|
||||
if code == "23503" && constraint == "shops_owner_id_fkey" {
|
||||
// foreign_key_violation
|
||||
return HttpApiProblem::with_title_and_type_from_status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.set_detail("Owner does not exist");
|
||||
} else if code == "23505" && constraint == "owners_api_key_key" {
|
||||
// unique_violation
|
||||
return HttpApiProblem::with_title_and_type_from_status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.set_detail("Owner with Api-Key already exists");
|
||||
} else if code == "23505" && constraint == "owners_unique_name_and_api_key"
|
||||
{
|
||||
// unique_violation
|
||||
return HttpApiProblem::with_title_and_type_from_status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.set_detail("Duplicate owner with same name and Api-Key exists");
|
||||
} else if code == "23505" && constraint == "shops_unique_name_and_owner_id"
|
||||
{
|
||||
// unique_violation
|
||||
return HttpApiProblem::with_title_and_type_from_status(
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.set_detail("Owner already has a shop with that name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Test Owner",
|
||||
"api_key": "9972bf37-5154-4e1a-a76e-49c93d7d78df",
|
||||
"mod_version": "0.0.1"
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Test Owner 2",
|
||||
"api_key": "84f03de0-225f-44c1-894d-d5a8c2f74803",
|
||||
"mod_version": "0.0.1"
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "Test Shop",
|
||||
"owner_id": 1,
|
||||
"description": "for testing",
|
||||
"is_not_sell_buy": true,
|
||||
"sell_buy_list_id": 1,
|
||||
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "Test Shop",
|
||||
"owner_id": 2,
|
||||
"name": "Test Shop 2",
|
||||
"description": "for testing",
|
||||
"is_not_sell_buy": true,
|
||||
"sell_buy_list_id": 1,
|
||||
|
Loading…
Reference in New Issue
Block a user