Skip to content

Commit 1d5f6cf

Browse files
committed
feat: login, logout
1 parent 17776d4 commit 1d5f6cf

21 files changed

Lines changed: 300 additions & 7 deletions

File tree

‎rust/rust-web-app-book/Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use shared::error::{AppError, AppResult};
2+
use std::str::FromStr;
3+
4+
use kernel::model::{
5+
auth::{AccessToken, event::CreateToken},
6+
id::UserId,
7+
};
8+
9+
use crate::redis::model::{RedisKey, RedisValue};
10+
11+
pub struct UserItem {
12+
pub user_id: UserId,
13+
pub password_hash: String,
14+
}
15+
16+
pub struct AuthorizationKey(String);
17+
pub struct AuthorizedUserId(UserId);
18+
19+
pub fn from(event: CreateToken) -> (AuthorizationKey, AuthorizedUserId) {
20+
(
21+
AuthorizationKey(event.access_token),
22+
AuthorizedUserId(event.user_id),
23+
)
24+
}
25+
26+
impl From<AuthorizationKey> for AccessToken {
27+
fn from(key: AuthorizationKey) -> Self {
28+
Self(key.0)
29+
}
30+
}
31+
32+
impl From<AccessToken> for AuthorizationKey {
33+
fn from(token: AccessToken) -> Self {
34+
Self(token.0)
35+
}
36+
}
37+
38+
impl From<&AccessToken> for AuthorizationKey {
39+
fn from(token: &AccessToken) -> Self {
40+
Self(token.0.to_string())
41+
}
42+
}
43+
44+
impl RedisKey for AuthorizationKey {
45+
type Value = AuthorizedUserId;
46+
47+
fn inner(&self) -> String {
48+
self.0.clone()
49+
}
50+
}
51+
52+
impl RedisValue for AuthorizedUserId {
53+
fn inner(&self) -> String {
54+
self.0.to_string()
55+
}
56+
}
57+
58+
impl TryFrom<String> for AuthorizedUserId {
59+
type Error = AppError;
60+
61+
fn try_from(s: String) -> AppResult<Self> {
62+
Ok(Self(UserId::from_str(&s).map_err(|e| {
63+
AppError::ConversionEntityError(e.to_string())
64+
})?))
65+
}
66+
}
67+
68+
impl AuthorizedUserId {
69+
pub fn into_inner(self) -> UserId {
70+
self.0
71+
}
72+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod auth;
12
pub mod book;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use std::sync::Arc;
2+
3+
use async_trait::async_trait;
4+
use derive_new::new;
5+
use kernel::{
6+
model::{
7+
auth::{AccessToken, event::CreateToken},
8+
id::UserId,
9+
},
10+
repository::auth::AuthRepository,
11+
};
12+
use shared::error::{AppError, AppResult};
13+
14+
use crate::{
15+
database::{
16+
ConnectionPool,
17+
model::auth::{AuthorizationKey, AuthorizedUserId, UserItem, from},
18+
},
19+
redis::RedisClient,
20+
};
21+
22+
#[derive(new)]
23+
pub struct AuthRepositoryImpl {
24+
db: ConnectionPool,
25+
kv: Arc<RedisClient>,
26+
ttl: u64,
27+
}
28+
29+
#[async_trait]
30+
impl AuthRepository for AuthRepositoryImpl {
31+
async fn fetch_user_id_from_token(
32+
&self,
33+
access_token: &AccessToken,
34+
) -> AppResult<Option<UserId>> {
35+
let key: AuthorizationKey = access_token.into();
36+
self.kv
37+
.get(&key)
38+
.await
39+
.map(|x| x.map(AuthorizedUserId::into_inner))
40+
}
41+
42+
async fn verify_user(&self, email: &str, password: &str) -> AppResult<UserId> {
43+
let user_item = sqlx::query_as!(
44+
UserItem,
45+
r#"
46+
SELECT user_id, password_hash FROM users
47+
WHERE email = $1;
48+
"#,
49+
email
50+
)
51+
.fetch_one(self.db.inner_ref())
52+
.await
53+
.map_err(AppError::SpecificOperationError)?;
54+
55+
let valid = bcrypt::verify(password, &user_item.password_hash)?;
56+
57+
if !valid {
58+
return Err(AppError::UnauthenticatedError);
59+
}
60+
61+
Ok(user_item.user_id)
62+
}
63+
64+
async fn create_token(&self, event: CreateToken) -> AppResult<AccessToken> {
65+
let (key, value) = from(event);
66+
self.kv.set_ex(&key, &value, self.ttl).await?;
67+
Ok(key.into())
68+
}
69+
70+
async fn delete_token(&self, access_token: AccessToken) -> AppResult<()> {
71+
let key: AuthorizationKey = access_token.into();
72+
self.kv.delete(&key).await
73+
}
74+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod auth;
12
pub mod book;
23
pub mod health;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use axum::{Json, extract::State, http::StatusCode};
2+
use kernel::model::auth::event::CreateToken;
3+
use registry::AppRegistry;
4+
use shared::error::AppResult;
5+
6+
use crate::model::auth::{AccessTokenResponse, LoginRequest};
7+
8+
pub async fn login(
9+
State(registry): State<AppRegistry>,
10+
Json(req): Json<LoginRequest>,
11+
) -> AppResult<Json<AccessTokenResponse>> {
12+
let user_id = registry
13+
.auth_repository()
14+
.verify_user(&req.email, &req.password)
15+
.await?;
16+
let access_token = registry
17+
.auth_repository()
18+
.create_token(CreateToken::new(user_id))
19+
.await?;
20+
21+
Ok(Json(AccessTokenResponse {
22+
user_id,
23+
access_token: access_token.0,
24+
}))
25+
}
26+
27+
pub async fn logout(State(registry): State<AppRegistry>) -> AppResult<StatusCode> {
28+
todo!() // ここはいまは実装しない。
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod auth;
12
pub mod book;
23
pub mod health;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use kernel::model::id::UserId;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Deserialize)]
5+
#[serde(rename_all = "camelCase")]
6+
pub struct LoginRequest {
7+
pub email: String,
8+
pub password: String,
9+
}
10+
11+
#[derive(Serialize)]
12+
#[serde(rename_all = "camelCase")]
13+
pub struct AccessTokenResponse {
14+
pub user_id: UserId,
15+
pub access_token: String,
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod auth;
12
pub mod book;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use axum::{Router, routing::post};
2+
use registry::AppRegistry;
3+
4+
use crate::handler::auth::{login, logout};
5+
6+
pub fn routes() -> Router<AppRegistry> {
7+
let auth_router = Router::new()
8+
.route("/login", post(login))
9+
.route("/logout", post(logout));
10+
Router::new().nest("/auth", auth_router)
11+
}

0 commit comments

Comments
 (0)