chore: publish clean community baseline
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
use axum::{
|
||||
extract::{OriginalUri, Request, State},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
use crank_community_auth::{
|
||||
cleared_session_cookie as build_cleared_session_cookie,
|
||||
create_session_cookie as build_session_cookie, hash_password as community_hash_password,
|
||||
session_cookie as build_session_cookie_header,
|
||||
};
|
||||
use crank_core::{User, UserSessionId, WorkspaceId};
|
||||
use crank_registry::WorkspaceMembershipRecord;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
pub use crank_community_auth::{
|
||||
SESSION_COOKIE_NAME, SessionCookie, extract_session_token, hash_session_secret, verify_password,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BootstrapAdminConfig {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthSettings {
|
||||
pub session_secret: String,
|
||||
pub password_pepper: String,
|
||||
pub session_ttl_hours: i64,
|
||||
pub cookie_secure: bool,
|
||||
pub bootstrap_admin: BootstrapAdminConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AuthenticatedSession {
|
||||
pub session_id: UserSessionId,
|
||||
pub user: User,
|
||||
pub memberships: Vec<WorkspaceMembershipRecord>,
|
||||
pub current_workspace_id: Option<WorkspaceId>,
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &str, pepper: &str) -> Result<String, ApiError> {
|
||||
community_hash_password(password, pepper)
|
||||
.map_err(|error| ApiError::internal(format!("failed to hash password: {error}")))
|
||||
}
|
||||
|
||||
pub fn create_session_cookie(settings: &AuthSettings) -> Result<SessionCookie, ApiError> {
|
||||
build_session_cookie(settings.session_ttl_hours)
|
||||
.map_err(|error| ApiError::internal(format!("failed to create session cookie: {error}")))
|
||||
}
|
||||
|
||||
pub fn session_cookie(settings: &AuthSettings, token: &str) -> Cookie<'static> {
|
||||
build_session_cookie_header(token, settings.cookie_secure, settings.session_ttl_hours)
|
||||
}
|
||||
|
||||
pub fn cleared_session_cookie(settings: &AuthSettings) -> Cookie<'static> {
|
||||
build_cleared_session_cookie(settings.cookie_secure)
|
||||
}
|
||||
|
||||
pub async fn require_session(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, ApiError> {
|
||||
let session = resolve_authenticated_session(state.clone(), &jar).await?;
|
||||
request.extensions_mut().insert(session);
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
pub async fn require_workspace_session(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
original_uri: OriginalUri,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, ApiError> {
|
||||
let session = resolve_authenticated_session(state.clone(), &jar).await?;
|
||||
let workspace_id = workspace_id_from_path(original_uri.path())
|
||||
.ok_or_else(|| ApiError::internal("workspace middleware was applied to an invalid path"))?;
|
||||
|
||||
let has_access = state
|
||||
.service
|
||||
.user_has_workspace_access(&session.user.id, &workspace_id)
|
||||
.await?;
|
||||
if !has_access {
|
||||
return Err(ApiError::forbidden("workspace access denied"));
|
||||
}
|
||||
|
||||
request.extensions_mut().insert(session);
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
async fn resolve_authenticated_session(
|
||||
state: AppState,
|
||||
jar: &CookieJar,
|
||||
) -> Result<AuthenticatedSession, ApiError> {
|
||||
let (session_id, session_value) = extract_session_token(jar)
|
||||
.ok_or_else(|| ApiError::unauthorized("authentication required"))?;
|
||||
let session = state
|
||||
.service
|
||||
.get_session(&session_id, &session_value)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::unauthorized("session is invalid or expired"))?;
|
||||
|
||||
state.service.touch_session(&session_id).await?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn workspace_id_from_path(path: &str) -> Option<WorkspaceId> {
|
||||
let marker = "/api/admin/workspaces/";
|
||||
let (_, suffix) = path.split_once(marker)?;
|
||||
let workspace_id = suffix.split('/').next()?;
|
||||
if workspace_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(WorkspaceId::new(workspace_id))
|
||||
}
|
||||
Reference in New Issue
Block a user