remove OIDC/Postgres, replace ApiError w/ AppError, move params to api-types
This commit is contained in:
@@ -15,7 +15,6 @@ rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -29,6 +29,12 @@ pub enum DomainError {
|
||||
#[error("Forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
|
||||
#[error("Repository error: {0}")]
|
||||
RepositoryError(String),
|
||||
|
||||
@@ -52,12 +58,12 @@ impl DomainError {
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DomainError::UserNotFound(_) | DomainError::ChannelNotFound(_)
|
||||
DomainError::UserNotFound(_) | DomainError::ChannelNotFound(_) | DomainError::NotFound(_)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_conflict(&self) -> bool {
|
||||
matches!(self, DomainError::UserAlreadyExists(_))
|
||||
matches!(self, DomainError::UserAlreadyExists(_) | DomainError::Conflict(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
pub mod auth;
|
||||
pub mod channel;
|
||||
pub mod ids;
|
||||
pub mod oidc;
|
||||
pub mod scheduling;
|
||||
pub mod search;
|
||||
|
||||
pub use auth::*;
|
||||
pub use channel::*;
|
||||
pub use ids::*;
|
||||
pub use oidc::*;
|
||||
pub use scheduling::*;
|
||||
pub use search::*;
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::fmt;
|
||||
use url::Url;
|
||||
|
||||
use super::auth::ValidationError;
|
||||
|
||||
// Stores original string to preserve exact formatting — OIDC providers expect issuer URLs to match exactly
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct IssuerUrl(String);
|
||||
|
||||
impl IssuerUrl {
|
||||
pub fn new(value: impl AsRef<str>) -> Result<Self, ValidationError> {
|
||||
let value = value.as_ref().trim().to_string();
|
||||
Url::parse(&value).map_err(|e| ValidationError::InvalidUrl(e.to_string()))?;
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for IssuerUrl {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IssuerUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for IssuerUrl {
|
||||
type Error = ValidationError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IssuerUrl> for String {
|
||||
fn from(val: IssuerUrl) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ClientId(String);
|
||||
|
||||
impl ClientId {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
|
||||
let value = value.into().trim().to_string();
|
||||
if value.is_empty() {
|
||||
return Err(ValidationError::Empty("client_id".to_string()));
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ClientId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ClientId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ClientId {
|
||||
type Error = ValidationError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientId> for String {
|
||||
fn from(val: ClientId) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden in Debug for security
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ClientSecret(String);
|
||||
|
||||
impl ClientSecret {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ClientSecret {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden in Debug for security
|
||||
impl fmt::Debug for ClientSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ClientSecret(***)")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ClientSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "***")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ClientSecret {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(Self::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
// ClientSecret must NOT implement Serialize
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct RedirectUrl(Url);
|
||||
|
||||
impl RedirectUrl {
|
||||
pub fn new(value: impl AsRef<str>) -> Result<Self, ValidationError> {
|
||||
let value = value.as_ref().trim();
|
||||
let url = Url::parse(value).map_err(|e| ValidationError::InvalidUrl(e.to_string()))?;
|
||||
Ok(Self(url))
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> &Url {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RedirectUrl {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RedirectUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RedirectUrl {
|
||||
type Error = ValidationError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RedirectUrl> for String {
|
||||
fn from(val: RedirectUrl) -> Self {
|
||||
val.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ResourceId(String);
|
||||
|
||||
impl ResourceId {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
|
||||
let value = value.into().trim().to_string();
|
||||
if value.is_empty() {
|
||||
return Err(ValidationError::Empty("resource_id".to_string()));
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ResourceId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ResourceId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ResourceId {
|
||||
type Error = ValidationError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResourceId> for String {
|
||||
fn from(val: ResourceId) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CsrfToken(String);
|
||||
|
||||
impl CsrfToken {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for CsrfToken {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CsrfToken {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OidcNonce(String);
|
||||
|
||||
impl OidcNonce {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for OidcNonce {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for OidcNonce {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden in Debug for security
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PkceVerifier(String);
|
||||
|
||||
impl PkceVerifier {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PkceVerifier {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden in Debug for security
|
||||
impl fmt::Debug for PkceVerifier {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "PkceVerifier(***)")
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden in Debug for security
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct AuthorizationCode(String);
|
||||
|
||||
impl AuthorizationCode {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AuthorizationCode {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden in Debug for security
|
||||
impl fmt::Debug for AuthorizationCode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "AuthorizationCode(***)")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AuthorizationCode {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(Self::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthorizationUrlData {
|
||||
pub url: Url,
|
||||
pub csrf_token: CsrfToken,
|
||||
pub nonce: OidcNonce,
|
||||
pub pkce_verifier: PkceVerifier,
|
||||
}
|
||||
|
||||
pub const MIN_JWT_SECRET_LENGTH: usize = 32;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct JwtSecret(String);
|
||||
|
||||
impl JwtSecret {
|
||||
pub fn new(value: impl Into<String>, is_production: bool) -> Result<Self, ValidationError> {
|
||||
let value = value.into();
|
||||
if is_production && value.len() < MIN_JWT_SECRET_LENGTH {
|
||||
return Err(ValidationError::SecretTooShort {
|
||||
min: MIN_JWT_SECRET_LENGTH,
|
||||
actual: value.len(),
|
||||
});
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
pub fn new_unchecked(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for JwtSecret {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for JwtSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "JwtSecret(***)")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/oidc.rs"]
|
||||
mod tests;
|
||||
@@ -1,51 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
mod oidc_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_issuer_url_valid() {
|
||||
assert!(IssuerUrl::new("https://auth.example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issuer_url_invalid() {
|
||||
assert!(IssuerUrl::new("not-a-url").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_id_non_empty() {
|
||||
assert!(ClientId::new("my-client").is_ok());
|
||||
assert!(ClientId::new("").is_err());
|
||||
assert!(ClientId::new(" ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_secret_hides_in_debug() {
|
||||
let secret = ClientSecret::new("super-secret");
|
||||
let debug = format!("{:?}", secret);
|
||||
assert!(!debug.contains("super-secret"));
|
||||
assert!(debug.contains("***"));
|
||||
}
|
||||
}
|
||||
|
||||
mod secret_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_jwt_secret_production_check() {
|
||||
let short = "short";
|
||||
let long = "a".repeat(32);
|
||||
|
||||
assert!(JwtSecret::new(short, true).is_err());
|
||||
assert!(JwtSecret::new(&long, true).is_ok());
|
||||
|
||||
assert!(JwtSecret::new(short, false).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_secrets_hide_in_debug() {
|
||||
let jwt = JwtSecret::new_unchecked("secret");
|
||||
assert!(!format!("{:?}", jwt).contains("secret"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user