Skip to content

Commit

Permalink
feat: 第十章代码(一)
Browse files Browse the repository at this point in the history
  • Loading branch information
fan-tastic-z committed Nov 7, 2024
1 parent 4af2177 commit 601c0d6
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 30 deletions.
82 changes: 82 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ path = "src/main.rs"
name = "zero2prod"

[dependencies]
argon2 = { version = "0.5.3", features = ["std"] }
axum = { version = "0.7.7", features = ["macros", "tracing"] }
axum-extra = { version = "0.9.4", features = ["typed-header"] }
backtrace_printer = "1.3.0"
base64 = "0.22.1"
chrono = { version = "0.4.38", features = ["serde"] }
colored = "2.1.0"
config = "0.14.1"
Expand Down
6 changes: 6 additions & 0 deletions migrations/20241106063704_create_users_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Add migration script here
CREATE TABLE users(
user_id uuid PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)
3 changes: 3 additions & 0 deletions migrations/20241106084255_rename_password_column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE
users RENAME COLUMN password TO password_hash
5 changes: 5 additions & 0 deletions migrations/20241106095320_add_salt_to_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Add migration script here
ALTER TABLE
users
ADD
COLUMN salt TEXT
3 changes: 3 additions & 0 deletions migrations/20241107002001_remove_salt_from_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE
users DROP COLUMN salt;
125 changes: 120 additions & 5 deletions src/controller/newsletters.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
use axum::{debug_handler, extract::State, response::Response, Json};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use axum::{
async_trait, debug_handler,
extract::{FromRequestParts, State},
http::request::Parts,
response::Response,
Json, RequestPartsExt,
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use base64::prelude::*;
use secrecy::{ExposeSecret, Secret};
use serde::Deserialize;
use sqlx::PgPool;
use uuid::Uuid;

use crate::{domain::SubscriberEmail, startup::AppState, Result};
use crate::{domain::SubscriberEmail, errors::Error, startup::AppState, Result as zero2prodResult};

use super::format;

Expand All @@ -20,9 +34,12 @@ pub struct Content {

#[debug_handler]
pub async fn publish_newsletter(
credentials: Credentials,
State(state): State<AppState>,
Json(params): Json<BodyData>,
) -> Result<Response> {
) -> zero2prodResult<Response> {
let _user_id = validate_credentials(credentials, &state.db_pool).await?;

let subscribers = get_confirmed_subscribers(&state.db_pool).await?;
for subscriber in subscribers {
match subscriber {
Expand Down Expand Up @@ -53,7 +70,9 @@ struct ConfirmedSubscriber {
email: SubscriberEmail,
}

async fn get_confirmed_subscribers(pool: &PgPool) -> Result<Vec<Result<ConfirmedSubscriber>>> {
async fn get_confirmed_subscribers(
pool: &PgPool,
) -> zero2prodResult<Vec<zero2prodResult<ConfirmedSubscriber>>> {
#[derive(sqlx::FromRow, Debug)]
struct Row {
email: String,
Expand All @@ -65,7 +84,7 @@ async fn get_confirmed_subscribers(pool: &PgPool) -> Result<Vec<Result<Confirmed
)
.fetch_all(pool)
.await?;
let confirmed_subscribers: Vec<Result<ConfirmedSubscriber>> = rows
let confirmed_subscribers: Vec<zero2prodResult<ConfirmedSubscriber>> = rows
.into_iter()
.map(|r| match SubscriberEmail::parse(r.email) {
Ok(email) => Ok(ConfirmedSubscriber { email }),
Expand All @@ -74,3 +93,99 @@ async fn get_confirmed_subscribers(pool: &PgPool) -> Result<Vec<Result<Confirmed
.collect();
Ok(confirmed_subscribers)
}

#[derive(Debug)]
pub struct Credentials {
username: String,
password: Secret<String>,
}

#[async_trait]
impl<S> FromRequestParts<S> for Credentials
where
S: Send + Sync,
{
type Rejection = Error;

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) =
match parts.extract::<TypedHeader<Authorization<Bearer>>>().await {
Ok(header) => header,
Err(_) => {
return Err(Error::Unauthorized(
"header credentials token error".to_string(),
))
}
};

let decoded_credentials =
// TODO: bearer.token error need to be handled
match String::from_utf8(BASE64_STANDARD_NO_PAD.decode(bearer.token())?) {
Ok(credentials) => credentials,
Err(_) => {
return Err(Error::Unauthorized(
"header credentials token error".to_string()
))
}
};

let mut credentials = decoded_credentials.splitn(2, ':');
let username = match credentials.next() {
Some(username) => username.to_string(),
None => {
return Err(Error::Unauthorized(
"header credentials token error".to_string(),
))
}
};

let password = match credentials.next() {
Some(password) => password.to_string(),
None => {
return Err(Error::Unauthorized(
"header credentials token error".to_string(),
))
}
};

Ok(Credentials {
username,
password: password.into(),
})
}
}

async fn validate_credentials(credentials: Credentials, pool: &PgPool) -> zero2prodResult<Uuid> {
let row: Option<User> = sqlx::query_as(
r#"
SELECT user_id, password_hash
FROM users
WHERE username = $1
"#,
)
.bind(credentials.username)
.fetch_optional(pool)
.await?;

let (expected_password_hash, user_id) = match row {
Some(row) => (row.password_hash, row.user_id),
None => {
return Err(Error::Unauthorized(
"Failed to query to retrieve stored credentials.".to_string(),
))
}
};
let expected_password_hash = PasswordHash::new(&expected_password_hash)?;
Argon2::default().verify_password(
credentials.password.expose_secret().as_bytes(),
&expected_password_hash,
)?;

Ok(user_id)
}

#[derive(sqlx::FromRow)]
struct User {
user_id: Uuid,
password_hash: String,
}
1 change: 0 additions & 1 deletion src/controller/subscriptions_confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ pub async fn confirm(
Ok(id) => id,
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};
dbg!(id);
match id {
Some(subscriber_id) => {
if confirm_subscriber(&state.db_pool, subscriber_id)
Expand Down
22 changes: 22 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::string;

use crate::{backtrace, Result};
use axum::{extract::FromRequest, http::StatusCode, response::IntoResponse};
use colored::Colorize;
Expand All @@ -24,12 +26,22 @@ pub enum Error {
Sqlx(#[from] sqlx::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Base64Decode(#[from] base64::DecodeError),
#[error(transparent)]
FromUtf8(#[from] string::FromUtf8Error),
#[error(transparent)]
Argon2(#[from] argon2::Error),
#[error(transparent)]
Argon2PasswordHashError(#[from] argon2::password_hash::Error),

// API
#[error("not found")]
NotFound,
#[error("{0}")]
BadRequest(String),
#[error("{0}")]
Unauthorized(String),
#[error("internal server error")]
InternalServerError,
#[error("")]
Expand Down Expand Up @@ -137,6 +149,16 @@ impl IntoResponse for Error {
StatusCode::NOT_FOUND,
ErrorDetail::new("not_found", "Resource was not found"),
),
Self::Unauthorized(err) => {
tracing::warn!(err);
(
StatusCode::UNAUTHORIZED,
ErrorDetail::new(
"unauthorized",
"You do not have permission to access this resource",
),
)
}
Self::InternalServerError => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorDetail::new("internal_server_error", "Internal Server Error"),
Expand Down
Loading

0 comments on commit 601c0d6

Please sign in to comment.