feat: refresh token
This commit is contained in:
7
crates/application/src/auth/logout.rs
Normal file
7
crates/application/src/auth/logout.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use domain::errors::DomainResult;
|
||||
|
||||
use crate::context::AppContext;
|
||||
|
||||
pub async fn execute(ctx: &AppContext, refresh_token: &str) -> DomainResult<()> {
|
||||
ctx.repos.refresh_session.revoke(refresh_token).await
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod commands;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod refresh;
|
||||
pub mod register;
|
||||
|
||||
43
crates/application/src/auth/refresh.rs
Normal file
43
crates/application/src/auth/refresh.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use domain::{
|
||||
auth::RefreshSession,
|
||||
errors::{DomainError, DomainResult},
|
||||
user::entity::User,
|
||||
};
|
||||
|
||||
use crate::context::AppContext;
|
||||
|
||||
pub struct RefreshResult {
|
||||
pub user: User,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
pub async fn execute(ctx: &AppContext, old_refresh_token: &str) -> DomainResult<RefreshResult> {
|
||||
let session = ctx
|
||||
.repos
|
||||
.refresh_session
|
||||
.find_by_token(old_refresh_token)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::Unauthorized("invalid refresh token".into()))?;
|
||||
|
||||
ctx.repos.refresh_session.revoke(old_refresh_token).await?;
|
||||
|
||||
if session.is_expired() {
|
||||
return Err(DomainError::Unauthorized("refresh token expired".into()));
|
||||
}
|
||||
|
||||
let user = ctx
|
||||
.repos
|
||||
.user
|
||||
.find_by_id(&session.user_id())
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("user no longer exists".into()))?;
|
||||
|
||||
let new_session = RefreshSession::new(session.user_id(), ctx.config.refresh_token_ttl_seconds);
|
||||
let refresh_token = new_session.token().to_string();
|
||||
ctx.repos.refresh_session.create(&new_session).await?;
|
||||
|
||||
Ok(RefreshResult {
|
||||
user,
|
||||
refresh_token,
|
||||
})
|
||||
}
|
||||
@@ -5,6 +5,8 @@ pub struct AppConfig {
|
||||
pub smart: SmartConfig,
|
||||
/// When false the `/auth/register` endpoint returns 403.
|
||||
pub allow_registration: bool,
|
||||
/// Refresh token time-to-live in seconds. Default: 30 days.
|
||||
pub refresh_token_ttl_seconds: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -28,6 +30,7 @@ impl AppConfig {
|
||||
base_url: std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:3000".into()),
|
||||
smart: SmartConfig::default(),
|
||||
allow_registration: true,
|
||||
refresh_token_ttl_seconds: 30 * 24 * 3600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
auth::ports::RefreshSessionRepository,
|
||||
events::{EventConsumer, EventPublisher},
|
||||
note::ports::{LinkRepository, NoteRepository},
|
||||
smart::ports::{EmbeddingGenerator, VectorStore},
|
||||
@@ -16,6 +17,7 @@ pub struct Repositories {
|
||||
pub tag: Arc<dyn TagRepository>,
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub link: Arc<dyn LinkRepository>,
|
||||
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -253,6 +253,54 @@ impl LinkRepository for MemoryLinkRepo {
|
||||
}
|
||||
}
|
||||
|
||||
// ── RefreshSession ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemoryRefreshSessionRepo {
|
||||
sessions: Mutex<Vec<domain::auth::RefreshSession>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl domain::auth::ports::RefreshSessionRepository for MemoryRefreshSessionRepo {
|
||||
async fn create(&self, session: &domain::auth::RefreshSession) -> DomainResult<()> {
|
||||
self.sessions.lock().unwrap().push(session.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_by_token(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> DomainResult<Option<domain::auth::RefreshSession>> {
|
||||
Ok(self
|
||||
.sessions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|s| s.token() == token)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn revoke(&self, token: &str) -> DomainResult<()> {
|
||||
self.sessions.lock().unwrap().retain(|s| s.token() != token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn revoke_all_for_user(&self, user_id: &UserId) -> DomainResult<()> {
|
||||
self.sessions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.retain(|s| s.user_id() != *user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_expired(&self) -> DomainResult<u64> {
|
||||
let before = self.sessions.lock().unwrap().len();
|
||||
self.sessions.lock().unwrap().retain(|s| !s.is_expired());
|
||||
let after = self.sessions.lock().unwrap().len();
|
||||
Ok((before - after) as u64)
|
||||
}
|
||||
}
|
||||
|
||||
// ── PasswordHasher ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct PlaintextHasher;
|
||||
@@ -310,6 +358,7 @@ impl TestContext {
|
||||
tag: Arc::new(MemoryTagRepo::default()),
|
||||
user: Arc::new(MemoryUserRepo::default()),
|
||||
link: Arc::new(MemoryLinkRepo::default()),
|
||||
refresh_session: Arc::new(MemoryRefreshSessionRepo::default()),
|
||||
},
|
||||
services: Services {
|
||||
password_hasher: Arc::new(PlaintextHasher),
|
||||
@@ -322,6 +371,7 @@ impl TestContext {
|
||||
base_url: "http://localhost:3000".into(),
|
||||
smart: SmartConfig::default(),
|
||||
allow_registration: true,
|
||||
refresh_token_ttl_seconds: 2_592_000,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user