Split admin auth service module
This commit is contained in:
@@ -7,12 +7,12 @@ use crank_core::{
|
|||||||
AgentId, AgentStatus, AuditSink, AuthConfig, AuthKind, AuthProfile, AuthProfileId,
|
AgentId, AgentStatus, AuditSink, AuthConfig, AuthKind, AuthProfile, AuthProfileId,
|
||||||
CapabilityProfile, CommunityCapabilityProfile, EditionCapabilities, ExecutionMode, ExportMode,
|
CapabilityProfile, CommunityCapabilityProfile, EditionCapabilities, ExecutionMode, ExportMode,
|
||||||
GeneratedDraft, GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel,
|
GeneratedDraft, GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel,
|
||||||
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome,
|
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, MembershipRole,
|
||||||
MembershipRole, NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus,
|
NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine,
|
||||||
OwnerOnlyPolicyEngine, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine,
|
||||||
PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol, ResponseCachePolicy, SampleId,
|
ProductEdition, Protocol, ResponseCachePolicy, SampleId, SecretKind, Target,
|
||||||
SecretKind, Target, ToolQualityMappingRule, ToolQualityMappingSet, ToolQualitySchemaKind,
|
ToolQualityMappingRule, ToolQualityMappingSet, ToolQualitySchemaKind, ToolQualitySchemaNode,
|
||||||
ToolQualitySchemaNode, UsagePeriod, UserSessionId, WizardState, WorkspaceId, WorkspaceStatus,
|
UsagePeriod, WizardState, WorkspaceId, WorkspaceStatus,
|
||||||
};
|
};
|
||||||
use crank_mapping::{JsonPathRoot, MappingRule, MappingSet, infer_mapping_from_samples};
|
use crank_mapping::{JsonPathRoot, MappingRule, MappingSet, infer_mapping_from_samples};
|
||||||
use crank_registry::{
|
use crank_registry::{
|
||||||
@@ -33,20 +33,14 @@ use tracing::{info, instrument};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod agents;
|
mod agents;
|
||||||
|
mod auth;
|
||||||
mod import_export;
|
mod import_export;
|
||||||
mod observability;
|
mod observability;
|
||||||
mod operations;
|
mod operations;
|
||||||
mod secrets;
|
mod secrets;
|
||||||
mod workspaces;
|
mod workspaces;
|
||||||
|
|
||||||
use crate::{
|
use crate::{auth::AuthSettings, error::ApiError, storage::LocalArtifactStorage};
|
||||||
auth::{
|
|
||||||
AuthSettings, AuthenticatedSession, SessionCookie, create_session_cookie, hash_password,
|
|
||||||
hash_session_secret, verify_password,
|
|
||||||
},
|
|
||||||
error::ApiError,
|
|
||||||
storage::LocalArtifactStorage,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AdminService {
|
pub struct AdminService {
|
||||||
@@ -628,248 +622,6 @@ impl AdminServiceBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AdminService {
|
impl AdminService {
|
||||||
pub async fn bootstrap_admin_user(&self) -> Result<(), ApiError> {
|
|
||||||
let password_hash = hash_password(
|
|
||||||
&self.auth_settings.bootstrap_admin.password,
|
|
||||||
&self.auth_settings.password_pepper,
|
|
||||||
)?;
|
|
||||||
let user_id = self
|
|
||||||
.registry
|
|
||||||
.upsert_bootstrap_user(
|
|
||||||
&self.auth_settings.bootstrap_admin.email,
|
|
||||||
&self.auth_settings.bootstrap_admin.display_name,
|
|
||||||
&password_hash,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
self.registry
|
|
||||||
.ensure_membership(
|
|
||||||
&WorkspaceId::new("ws_default"),
|
|
||||||
&user_id,
|
|
||||||
MembershipRole::Owner,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn seed_demo_assets(&self) -> Result<(), ApiError> {
|
|
||||||
let admin_user = self
|
|
||||||
.registry
|
|
||||||
.get_auth_user_by_email(&self.auth_settings.bootstrap_admin.email)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| ApiError::internal("bootstrap admin user was not found"))?;
|
|
||||||
let admin_user_id = admin_user.user.id.clone();
|
|
||||||
let default_workspace_id = WorkspaceId::new("ws_default");
|
|
||||||
|
|
||||||
self.seed_default_workspace_demo(&admin_user_id, &default_workspace_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_session(
|
|
||||||
&self,
|
|
||||||
session_id: &UserSessionId,
|
|
||||||
session_value: &str,
|
|
||||||
) -> Result<Option<AuthenticatedSession>, ApiError> {
|
|
||||||
let secret_hash = hash_session_secret(
|
|
||||||
session_id,
|
|
||||||
session_value,
|
|
||||||
&self.auth_settings.session_secret,
|
|
||||||
);
|
|
||||||
let session = self
|
|
||||||
.registry
|
|
||||||
.get_user_session(session_id, &secret_hash)
|
|
||||||
.await?
|
|
||||||
.map(|record| AuthenticatedSession {
|
|
||||||
session_id: record.session_id,
|
|
||||||
user: record.user,
|
|
||||||
memberships: record.memberships,
|
|
||||||
current_workspace_id: record.current_workspace_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn touch_session(&self, session_id: &UserSessionId) -> Result<(), ApiError> {
|
|
||||||
self.registry.touch_user_session(session_id).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(
|
|
||||||
&self,
|
|
||||||
payload: LoginPayload,
|
|
||||||
) -> Result<(SessionCookie, SessionResponse), ApiError> {
|
|
||||||
let authenticated = self.authenticate_login(&payload).await?;
|
|
||||||
|
|
||||||
let session_cookie = create_session_cookie(&self.auth_settings)?;
|
|
||||||
let secret_hash = hash_session_secret(
|
|
||||||
&session_cookie.session_id,
|
|
||||||
&session_cookie.value,
|
|
||||||
&self.auth_settings.session_secret,
|
|
||||||
);
|
|
||||||
let memberships = self
|
|
||||||
.registry
|
|
||||||
.list_workspaces_for_user(&authenticated.user.id)
|
|
||||||
.await?;
|
|
||||||
let current_workspace_id = authenticated
|
|
||||||
.current_workspace_id
|
|
||||||
.as_ref()
|
|
||||||
.map(|workspace_id| workspace_id.as_str().to_owned())
|
|
||||||
.or_else(|| {
|
|
||||||
memberships
|
|
||||||
.first()
|
|
||||||
.map(|membership| membership.workspace.id.as_str().to_owned())
|
|
||||||
});
|
|
||||||
let current_workspace_ref = current_workspace_id
|
|
||||||
.as_ref()
|
|
||||||
.map(|workspace_id| WorkspaceId::new(workspace_id.clone()));
|
|
||||||
self.registry
|
|
||||||
.create_user_session(
|
|
||||||
&session_cookie.session_id,
|
|
||||||
&authenticated.user.id,
|
|
||||||
current_workspace_ref.as_ref(),
|
|
||||||
&secret_hash,
|
|
||||||
&session_cookie.expires_at,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
session_cookie,
|
|
||||||
SessionResponse {
|
|
||||||
user: authenticated.user,
|
|
||||||
memberships,
|
|
||||||
current_workspace_id,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn authenticate_login(
|
|
||||||
&self,
|
|
||||||
payload: &LoginPayload,
|
|
||||||
) -> Result<crank_core::AuthenticatedIdentity, ApiError> {
|
|
||||||
if let Some(identity_provider) = &self.identity_provider {
|
|
||||||
return match identity_provider
|
|
||||||
.login_password(crank_core::LoginPayload {
|
|
||||||
email: payload.email.clone(),
|
|
||||||
password: payload.password.clone(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(LoginOutcome::Authenticated(identity)) => Ok(identity),
|
|
||||||
Err(error) => Err(map_identity_error(error)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = self
|
|
||||||
.registry
|
|
||||||
.get_auth_user_by_email(&payload.email)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| ApiError::unauthorized("invalid email or password"))?;
|
|
||||||
|
|
||||||
if !verify_password(
|
|
||||||
&payload.password,
|
|
||||||
&self.auth_settings.password_pepper,
|
|
||||||
&user.password_hash,
|
|
||||||
) {
|
|
||||||
return Err(ApiError::unauthorized("invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(crank_core::AuthenticatedIdentity {
|
|
||||||
user: user.user,
|
|
||||||
memberships: vec![],
|
|
||||||
current_workspace_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn logout(
|
|
||||||
&self,
|
|
||||||
session_id: &UserSessionId,
|
|
||||||
_session_value: &str,
|
|
||||||
) -> Result<(), ApiError> {
|
|
||||||
self.registry.revoke_user_session(session_id).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn session_response(
|
|
||||||
&self,
|
|
||||||
session_id: &UserSessionId,
|
|
||||||
session_value: &str,
|
|
||||||
) -> Result<Option<SessionResponse>, ApiError> {
|
|
||||||
Ok(self
|
|
||||||
.get_session(session_id, session_value)
|
|
||||||
.await?
|
|
||||||
.map(|session| SessionResponse {
|
|
||||||
user: session.user,
|
|
||||||
memberships: session.memberships,
|
|
||||||
current_workspace_id: session
|
|
||||||
.current_workspace_id
|
|
||||||
.map(|id| id.as_str().to_owned()),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_profile(
|
|
||||||
&self,
|
|
||||||
user_id: &crank_core::UserId,
|
|
||||||
current_workspace_id: Option<&WorkspaceId>,
|
|
||||||
payload: UpdateProfilePayload,
|
|
||||||
) -> Result<SessionResponse, ApiError> {
|
|
||||||
let display_name = validate_profile_display_name(&payload.display_name)?;
|
|
||||||
let email = validate_profile_email(&payload.email)?;
|
|
||||||
|
|
||||||
let user = self
|
|
||||||
.registry
|
|
||||||
.update_user_profile(user_id, &email, &display_name)
|
|
||||||
.await?;
|
|
||||||
let memberships = self.registry.list_workspaces_for_user(user_id).await?;
|
|
||||||
|
|
||||||
Ok(SessionResponse {
|
|
||||||
user,
|
|
||||||
memberships,
|
|
||||||
current_workspace_id: current_workspace_id.map(|id| id.as_str().to_owned()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn change_password(
|
|
||||||
&self,
|
|
||||||
user_id: &crank_core::UserId,
|
|
||||||
payload: ChangePasswordPayload,
|
|
||||||
) -> Result<(), ApiError> {
|
|
||||||
if payload.new_password.len() < 12 {
|
|
||||||
return Err(ApiError::validation(
|
|
||||||
"new password must be at least 12 characters long",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = self
|
|
||||||
.registry
|
|
||||||
.get_auth_user_by_id(user_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ApiError::not_found_with_context(
|
|
||||||
format!("user {} was not found", user_id.as_str()),
|
|
||||||
json!({ "user_id": user_id.as_str() }),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !verify_password(
|
|
||||||
&payload.current_password,
|
|
||||||
&self.auth_settings.password_pepper,
|
|
||||||
&user.password_hash,
|
|
||||||
) {
|
|
||||||
return Err(ApiError::unauthorized("current password is invalid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let password_hash =
|
|
||||||
hash_password(&payload.new_password, &self.auth_settings.password_pepper)?;
|
|
||||||
self.registry
|
|
||||||
.update_user_password(user_id, &password_hash)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn export_workspace(
|
pub async fn export_workspace(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &WorkspaceId,
|
workspace_id: &WorkspaceId,
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
use crank_core::{LoginOutcome, MembershipRole, UserSessionId, WorkspaceId};
|
||||||
|
use serde_json::json;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::{
|
||||||
|
AuthenticatedSession, SessionCookie, create_session_cookie, hash_password,
|
||||||
|
hash_session_secret, verify_password,
|
||||||
|
},
|
||||||
|
error::ApiError,
|
||||||
|
service::{
|
||||||
|
AdminService, ChangePasswordPayload, LoginPayload, SessionResponse, UpdateProfilePayload,
|
||||||
|
map_identity_error, validate_profile_display_name, validate_profile_email,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl AdminService {
|
||||||
|
pub async fn bootstrap_admin_user(&self) -> Result<(), ApiError> {
|
||||||
|
let password_hash = hash_password(
|
||||||
|
&self.auth_settings.bootstrap_admin.password,
|
||||||
|
&self.auth_settings.password_pepper,
|
||||||
|
)?;
|
||||||
|
let user_id = self
|
||||||
|
.registry
|
||||||
|
.upsert_bootstrap_user(
|
||||||
|
&self.auth_settings.bootstrap_admin.email,
|
||||||
|
&self.auth_settings.bootstrap_admin.display_name,
|
||||||
|
&password_hash,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.registry
|
||||||
|
.ensure_membership(
|
||||||
|
&WorkspaceId::new("ws_default"),
|
||||||
|
&user_id,
|
||||||
|
MembershipRole::Owner,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn seed_demo_assets(&self) -> Result<(), ApiError> {
|
||||||
|
let admin_user = self
|
||||||
|
.registry
|
||||||
|
.get_auth_user_by_email(&self.auth_settings.bootstrap_admin.email)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| ApiError::internal("bootstrap admin user was not found"))?;
|
||||||
|
let admin_user_id = admin_user.user.id.clone();
|
||||||
|
let default_workspace_id = WorkspaceId::new("ws_default");
|
||||||
|
|
||||||
|
self.seed_default_workspace_demo(&admin_user_id, &default_workspace_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_session(
|
||||||
|
&self,
|
||||||
|
session_id: &UserSessionId,
|
||||||
|
session_value: &str,
|
||||||
|
) -> Result<Option<AuthenticatedSession>, ApiError> {
|
||||||
|
let secret_hash = hash_session_secret(
|
||||||
|
session_id,
|
||||||
|
session_value,
|
||||||
|
&self.auth_settings.session_secret,
|
||||||
|
);
|
||||||
|
let session = self
|
||||||
|
.registry
|
||||||
|
.get_user_session(session_id, &secret_hash)
|
||||||
|
.await?
|
||||||
|
.map(|record| AuthenticatedSession {
|
||||||
|
session_id: record.session_id,
|
||||||
|
user: record.user,
|
||||||
|
memberships: record.memberships,
|
||||||
|
current_workspace_id: record.current_workspace_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn touch_session(&self, session_id: &UserSessionId) -> Result<(), ApiError> {
|
||||||
|
self.registry.touch_user_session(session_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
&self,
|
||||||
|
payload: LoginPayload,
|
||||||
|
) -> Result<(SessionCookie, SessionResponse), ApiError> {
|
||||||
|
let authenticated = self.authenticate_login(&payload).await?;
|
||||||
|
|
||||||
|
let session_cookie = create_session_cookie(&self.auth_settings)?;
|
||||||
|
let secret_hash = hash_session_secret(
|
||||||
|
&session_cookie.session_id,
|
||||||
|
&session_cookie.value,
|
||||||
|
&self.auth_settings.session_secret,
|
||||||
|
);
|
||||||
|
let memberships = self
|
||||||
|
.registry
|
||||||
|
.list_workspaces_for_user(&authenticated.user.id)
|
||||||
|
.await?;
|
||||||
|
let current_workspace_id = authenticated
|
||||||
|
.current_workspace_id
|
||||||
|
.as_ref()
|
||||||
|
.map(|workspace_id| workspace_id.as_str().to_owned())
|
||||||
|
.or_else(|| {
|
||||||
|
memberships
|
||||||
|
.first()
|
||||||
|
.map(|membership| membership.workspace.id.as_str().to_owned())
|
||||||
|
});
|
||||||
|
let current_workspace_ref = current_workspace_id
|
||||||
|
.as_ref()
|
||||||
|
.map(|workspace_id| WorkspaceId::new(workspace_id.clone()));
|
||||||
|
self.registry
|
||||||
|
.create_user_session(
|
||||||
|
&session_cookie.session_id,
|
||||||
|
&authenticated.user.id,
|
||||||
|
current_workspace_ref.as_ref(),
|
||||||
|
&secret_hash,
|
||||||
|
&session_cookie.expires_at,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
session_cookie,
|
||||||
|
SessionResponse {
|
||||||
|
user: authenticated.user,
|
||||||
|
memberships,
|
||||||
|
current_workspace_id,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticate_login(
|
||||||
|
&self,
|
||||||
|
payload: &LoginPayload,
|
||||||
|
) -> Result<crank_core::AuthenticatedIdentity, ApiError> {
|
||||||
|
if let Some(identity_provider) = &self.identity_provider {
|
||||||
|
return match identity_provider
|
||||||
|
.login_password(crank_core::LoginPayload {
|
||||||
|
email: payload.email.clone(),
|
||||||
|
password: payload.password.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(LoginOutcome::Authenticated(identity)) => Ok(identity),
|
||||||
|
Err(error) => Err(map_identity_error(error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = self
|
||||||
|
.registry
|
||||||
|
.get_auth_user_by_email(&payload.email)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| ApiError::unauthorized("invalid email or password"))?;
|
||||||
|
|
||||||
|
if !verify_password(
|
||||||
|
&payload.password,
|
||||||
|
&self.auth_settings.password_pepper,
|
||||||
|
&user.password_hash,
|
||||||
|
) {
|
||||||
|
return Err(ApiError::unauthorized("invalid email or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(crank_core::AuthenticatedIdentity {
|
||||||
|
user: user.user,
|
||||||
|
memberships: vec![],
|
||||||
|
current_workspace_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(
|
||||||
|
&self,
|
||||||
|
session_id: &UserSessionId,
|
||||||
|
_session_value: &str,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
self.registry.revoke_user_session(session_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn session_response(
|
||||||
|
&self,
|
||||||
|
session_id: &UserSessionId,
|
||||||
|
session_value: &str,
|
||||||
|
) -> Result<Option<SessionResponse>, ApiError> {
|
||||||
|
Ok(self
|
||||||
|
.get_session(session_id, session_value)
|
||||||
|
.await?
|
||||||
|
.map(|session| SessionResponse {
|
||||||
|
user: session.user,
|
||||||
|
memberships: session.memberships,
|
||||||
|
current_workspace_id: session
|
||||||
|
.current_workspace_id
|
||||||
|
.map(|id| id.as_str().to_owned()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_profile(
|
||||||
|
&self,
|
||||||
|
user_id: &crank_core::UserId,
|
||||||
|
current_workspace_id: Option<&WorkspaceId>,
|
||||||
|
payload: UpdateProfilePayload,
|
||||||
|
) -> Result<SessionResponse, ApiError> {
|
||||||
|
let display_name = validate_profile_display_name(&payload.display_name)?;
|
||||||
|
let email = validate_profile_email(&payload.email)?;
|
||||||
|
|
||||||
|
let user = self
|
||||||
|
.registry
|
||||||
|
.update_user_profile(user_id, &email, &display_name)
|
||||||
|
.await?;
|
||||||
|
let memberships = self.registry.list_workspaces_for_user(user_id).await?;
|
||||||
|
|
||||||
|
Ok(SessionResponse {
|
||||||
|
user,
|
||||||
|
memberships,
|
||||||
|
current_workspace_id: current_workspace_id.map(|id| id.as_str().to_owned()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn change_password(
|
||||||
|
&self,
|
||||||
|
user_id: &crank_core::UserId,
|
||||||
|
payload: ChangePasswordPayload,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
if payload.new_password.len() < 12 {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"new password must be at least 12 characters long",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = self
|
||||||
|
.registry
|
||||||
|
.get_auth_user_by_id(user_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::not_found_with_context(
|
||||||
|
format!("user {} was not found", user_id.as_str()),
|
||||||
|
json!({ "user_id": user_id.as_str() }),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !verify_password(
|
||||||
|
&payload.current_password,
|
||||||
|
&self.auth_settings.password_pepper,
|
||||||
|
&user.password_hash,
|
||||||
|
) {
|
||||||
|
return Err(ApiError::unauthorized("current password is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let password_hash =
|
||||||
|
hash_password(&payload.new_password, &self.auth_settings.password_pepper)?;
|
||||||
|
self.registry
|
||||||
|
.update_user_password(user_id, &password_hash)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user