refactor(session): extract share layer into lib/session
ZhenYi committed
Added lib/session/src/share/guard.rs lib/session/src/share/guard.rs +60 −0 View lib/session/src/share/guard.rs
@@ -0,0 +1,60 @@
1use async_trait::async_trait;
2use axum::extract::{Request, State};
3use axum::middleware::Next;
4use axum::response::{IntoResponse, Response};
5use time::OffsetDateTime;
6use uuid::Uuid;
7
8use crate::error::SessionError;
9use crate::share::session::{pwd_version_matches, session_token_key};
10use crate::{Session, SessionUser, SessionUserMut};
11
12pub struct SessionWithUser {
13 pub user_id: Uuid,
14 pub is_revoked: bool,
15 pub expires_at: OffsetDateTime,
16 pub is_active: bool,
17 pub prohibit_login: bool,
18 pub is_restricted: bool,
19 pub passwd_version: Option<i64>,
20}
21
22#[async_trait]
23pub trait SessionGuard: Send + Sync {
24 async fn lookup_session_with_user(
25 &self,
26 token_hash: &str,
27 ) -> Result<Option<SessionWithUser>, SessionError>;
28}
29
30pub async fn session_guard<S>(State(service): State<S>, session: Session, request: Request, next: Next) -> Response
31where
32 S: SessionGuard,
33{
34 let Some(uid) = session.uid() else {
35 return SessionError::Unauthorized.into_response();
36 };
37 let Some(token_hash) = session.get::<String>(&session_token_key(uid)) else {
38 return SessionError::Unauthorized.into_response();
39 };
40 let lookup = match service.lookup_session_with_user(&token_hash).await {
41 Ok(value) => value,
42 Err(error) => return error.into_response(),
43 };
44 let Some(row) = lookup else {
45 session.logout_user(uid);
46 return SessionError::Unauthorized.into_response();
47 };
48 let authorized = !row.is_revoked
49 && row.user_id == uid
50 && row.expires_at > OffsetDateTime::now_utc()
51 && row.is_active
52 && !row.prohibit_login
53 && !row.is_restricted
54 && pwd_version_matches(&session, row.passwd_version);
55 if !authorized {
56 session.logout_user(uid);
57 return SessionError::Unauthorized.into_response();
58 }
59 next.run(request).await
60}
No newline at end of file
Added lib/session/src/share/mod.rs lib/session/src/share/mod.rs +2 −0 View lib/session/src/share/mod.rs
@@ -0,0 +1,2 @@
1pub mod guard;
2pub mod session;
No newline at end of file
Added lib/session/src/share/session.rs lib/session/src/share/session.rs +42 −0 View lib/session/src/share/session.rs
@@ -0,0 +1,42 @@
1use crate::user::{AuthUser, SessionUser};
2use crate::{Session, SessionUserMut};
3use uuid::Uuid;
4
5use crate::error::SessionError;
6
7pub const PWD_VERSION_KEY: &str = "auth:pwd_version";
8
9pub fn session_token_key(uid: Uuid) -> String {
10 format!("auth:token_hash:{uid}")
11}
12
13pub fn require_uid(session: &Session) -> Result<Uuid, SessionError> {
14 session.uid().ok_or(SessionError::Unauthorized)
15}
16
17pub fn apply_login(session: &Session, user: AuthUser, passwd_version: Option<i64>) {
18 session.login_user(user);
19 session.set(PWD_VERSION_KEY, passwd_version.unwrap_or(0));
20}
21
22pub fn pwd_version_matches(session: &Session, current: Option<i64>) -> bool {
23 let stored = session.get::<i64>(PWD_VERSION_KEY);
24 match (stored, current) {
25 (Some(stored_ver), Some(curr_ver)) => stored_ver == curr_ver,
26 (None, None) => true,
27 _ => false,
28 }
29}
30
31pub fn validate_session_pwd_version(
32 session: &Session,
33 current_passwd_version: Option<i64>,
34) -> Result<(), SessionError> {
35 if !pwd_version_matches(session, current_passwd_version) {
36 if let Some(uid) = session.uid() {
37 session.logout_user(uid);
38 }
39 return Err(SessionError::Unauthorized);
40 }
41 Ok(())
42}
No newline at end of file
Added lib/session/src/error.rs lib/session/src/error.rs +44 −0 View lib/session/src/error.rs
@@ -0,0 +1,44 @@
1use axum::response::{IntoResponse, Response};
2use export::rest::{AppResponseData, NoData};
3use sea_orm::DbErr;
4
5#[derive(Debug)]
6pub enum SessionError {
7 Unauthorized,
8 Database(DbErr),
9}
10
11impl std::fmt::Display for SessionError {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 match self {
14 Self::Unauthorized => write!(f, "unauthorized"),
15 Self::Database(err) => write!(f, "database error: {err}"),
16 }
17 }
18}
19
20impl std::error::Error for SessionError {
21 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
22 match self {
23 Self::Database(err) => Some(err),
24 _ => None,
25 }
26 }
27}
28
29impl From<DbErr> for SessionError {
30 fn from(err: DbErr) -> Self {
31 Self::Database(err)
32 }
33}
34
35impl IntoResponse for SessionError {
36 fn into_response(self) -> Response {
37 match self {
38 Self::Unauthorized => AppResponseData::<NoData>::err(401, "unauthorized")
39 .into_response(),
40 Self::Database(_) => AppResponseData::<NoData>::err(500, "internal error")
41 .into_response(),
42 }
43 }
44}
No newline at end of file
Modified lib/session/src/lib.rs lib/session/src/lib.rs +8 −0 View lib/session/src/lib.rs
@@ -1,7 +1,15 @@
11pub mod config;
2pub mod error;
3pub mod share;
24pub mod store;
35pub mod user;
46
57pub use config::build_session_config;
8pub use error::SessionError;
9pub use share::guard::{session_guard, SessionGuard, SessionWithUser};
10pub use share::session::{
11 apply_login, pwd_version_matches, require_uid, session_token_key, validate_session_pwd_version,
12 PWD_VERSION_KEY,
13};
614pub use store::{Session, SessionLayer, SessionRedisPool, SessionStore, ReadOnlySession};
715pub use user::{AuthUser, SessionUser, SessionUserMut, UID_KEY, USERS_KEY};
No newline at end of file
Modified lib/session/Cargo.toml lib/session/Cargo.toml +7 −4 View lib/session/Cargo.toml
@@ -4,13 +4,16 @@
44edition.workspace = true
55
66[dependencies]
7async-trait = { workspace = true }
8axum = { workspace = true }
9base64 = { workspace = true }
10chrono = { workspace = true }
711config = { workspace = true }
812export = { workspace = true }
913axum_session = { workspace = true }
1014redis = { workspace = true }
11async-trait = { workspace = true }
12base64 = { workspace = true }
13chrono = { workspace = true }
14tracing = { workspace = true }
15sea-orm = { workspace = true }
1516serde = { workspace = true, features = ["derive"] }
17time = { workspace = true }
18tracing = { workspace = true }
1619uuid = { workspace = true }
No newline at end of file