Skip to content

Commit 0217f41

Browse files
committed
Clean namespace surfaces and add Docker publish smoke gate
1 parent aa416c0 commit 0217f41

76 files changed

Lines changed: 1590 additions & 1403 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
target
22
.nvimlog
33
.tmp-playwright/
4+
rustc-ice-*.txt
45
artifacts/visual/current
56
artifacts/
67
.playwright-browsers/

crates/app/src/auth.rs

Lines changed: 20 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod authenticate_flow;
22
mod get_user_flow;
3+
pub mod password;
4+
pub mod repository;
35

46
use std::sync::Arc;
57

@@ -8,7 +10,6 @@ use bon::Builder;
810
use nutype::nutype;
911
use secrecy::SecretString;
1012
use snafu::prelude::*;
11-
use strum_macros::Display;
1213

1314
use domain::user;
1415

@@ -19,62 +20,26 @@ type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
1920
#[derive(Debug, Snafu)]
2021
pub enum Error {
2122
#[snafu(display("{source}"))]
22-
Repository { source: RepositoryError },
23+
Repository { source: repository::Error },
2324
#[snafu(display("auth password hashing failed: {source}"))]
24-
HashPassword { source: PasswordHashError },
25+
HashPassword { source: password::HashError },
2526
#[snafu(display("stored password hash parsing failed: {source}"))]
26-
ParseStoredPasswordHash { source: PasswordHashError },
27+
ParseStoredPasswordHash { source: password::HashError },
2728
#[snafu(display("invalid authenticated user id: {source}"))]
2829
InvalidAuthenticatedUserId { source: uuid::Error },
2930
}
3031

31-
#[derive(Clone, Copy, Debug, Display)]
32-
pub enum RepositoryOperation {
33-
#[strum(serialize = "find auth record by email")]
34-
FindByEmail,
35-
#[strum(serialize = "find auth record by id")]
36-
FindById,
37-
}
38-
39-
#[derive(Debug, Snafu)]
40-
pub enum RepositoryError {
41-
#[snafu(display("auth repository query failed while {operation}: {source}"))]
42-
Query {
43-
operation: RepositoryOperation,
44-
source: BoxError,
45-
},
46-
#[snafu(display("failed to decode auth username: {source}"))]
47-
DecodeUsername { source: user::UsernameError },
48-
#[snafu(display("failed to decode auth email: {source}"))]
49-
DecodeEmail { source: user::EmailError },
50-
}
51-
52-
#[derive(Debug)]
53-
pub struct PasswordHashError(BoxError);
54-
55-
impl core::fmt::Display for PasswordHashError {
56-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57-
write!(f, "{}", self.0)
58-
}
59-
}
60-
61-
impl std::error::Error for PasswordHashError {
62-
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63-
Some(&*self.0)
64-
}
65-
}
66-
6732
fn box_error(source: impl std::error::Error + Send + Sync + 'static) -> BoxError {
6833
Box::new(source)
6934
}
7035

7136
impl Error {
7237
pub fn query_repository(
73-
operation: RepositoryOperation,
38+
operation: repository::Operation,
7439
source: impl std::error::Error + Send + Sync + 'static,
7540
) -> Self {
7641
Self::Repository {
77-
source: RepositoryError::Query {
42+
source: repository::Error::Query {
7843
operation,
7944
source: box_error(source),
8045
},
@@ -83,27 +48,27 @@ impl Error {
8348

8449
pub fn decode_username(source: user::UsernameError) -> Self {
8550
Self::Repository {
86-
source: RepositoryError::DecodeUsername { source },
51+
source: repository::Error::DecodeUsername { source },
8752
}
8853
}
8954

9055
pub fn decode_email(source: user::EmailError) -> Self {
9156
Self::Repository {
92-
source: RepositoryError::DecodeEmail { source },
57+
source: repository::Error::DecodeEmail { source },
9358
}
9459
}
9560

9661
pub fn hash_password(source: impl std::error::Error + Send + Sync + 'static) -> Self {
9762
Self::HashPassword {
98-
source: PasswordHashError(box_error(source)),
63+
source: password::HashError(box_error(source)),
9964
}
10065
}
10166

10267
pub fn parse_stored_password_hash(
10368
source: impl std::error::Error + Send + Sync + 'static,
10469
) -> Self {
10570
Self::ParseStoredPasswordHash {
106-
source: PasswordHashError(box_error(source)),
71+
source: password::HashError(box_error(source)),
10772
}
10873
}
10974
}
@@ -127,7 +92,7 @@ pub struct Record {
12792
pub id: user::Id,
12893
pub username: user::Username,
12994
pub email: user::Email,
130-
pub password_hash: PasswordHash,
95+
pub password_hash: password::Hash,
13196
pub session_hash: SessionHash,
13297
}
13398

@@ -190,19 +155,14 @@ pub trait Repository: Send + Sync {
190155
async fn find_by_id(&self, user_id: &user::Id) -> Result<Option<Record>>;
191156
}
192157

193-
pub trait PasswordHasher: Send + Sync {
194-
fn hash(&self, password: &str) -> Result<PasswordHash>;
195-
fn verify(&self, password: &str, password_hash: &PasswordHash) -> Result<bool>;
196-
}
197-
198158
#[derive(Clone)]
199159
pub struct ProviderImpl {
200160
repo: Arc<dyn Repository>,
201-
hasher: Arc<dyn PasswordHasher>,
161+
hasher: Arc<dyn password::Hasher>,
202162
}
203163

204164
impl ProviderImpl {
205-
pub fn new(repo: Arc<dyn Repository>, hasher: Arc<dyn PasswordHasher>) -> Self {
165+
pub fn new(repo: Arc<dyn Repository>, hasher: Arc<dyn password::Hasher>) -> Self {
206166
Self { repo, hasher }
207167
}
208168
}
@@ -227,9 +187,6 @@ impl Provider for ProviderImpl {
227187
}
228188
}
229189

230-
#[nutype(sanitize(trim), derive(Clone, Debug, PartialEq, Display))]
231-
pub struct PasswordHash(String);
232-
233190
#[nutype(sanitize(trim), derive(Clone, Debug, PartialEq, Display))]
234191
pub struct SessionHash(String);
235192

@@ -249,7 +206,7 @@ mod tests {
249206
#[test]
250207
fn repository_error_preserves_source() {
251208
let error = Error::query_repository(
252-
RepositoryOperation::FindByEmail,
209+
repository::Operation::FindByEmail,
253210
std::io::Error::other("db unavailable"),
254211
);
255212

@@ -311,8 +268,8 @@ mod tests {
311268
user::Id::from_uuid(uuid::Uuid::new_v4())
312269
}
313270

314-
fn test_password_hash() -> PasswordHash {
315-
PasswordHash::new("hash")
271+
fn test_password_hash() -> password::Hash {
272+
password::Hash::new("hash")
316273
}
317274

318275
fn test_session_hash() -> SessionHash {
@@ -338,12 +295,12 @@ mod tests {
338295
ok: bool,
339296
}
340297

341-
impl PasswordHasher for TestHasher {
342-
fn hash(&self, _password: &str) -> Result<PasswordHash> {
298+
impl password::Hasher for TestHasher {
299+
fn hash(&self, _password: &str) -> Result<password::Hash> {
343300
Ok(test_password_hash())
344301
}
345302

346-
fn verify(&self, _password: &str, _password_hash: &PasswordHash) -> Result<bool> {
303+
fn verify(&self, _password: &str, _password_hash: &password::Hash) -> Result<bool> {
347304
Ok(self.ok)
348305
}
349306
}

crates/app/src/auth/authenticate_flow.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use secrecy::{ExposeSecret, SecretString};
22
use statum::{machine, state, transition};
33

4-
use super::{AuthenticatedUser, Credentials, PasswordHash, PasswordHasher, Record, Result};
4+
use super::{AuthenticatedUser, Credentials, Record, Result, password};
55
use domain::user;
66

77
#[derive(Clone, Debug)]
@@ -69,13 +69,13 @@ impl AuthenticateFlow<Incoming> {
6969
}
7070

7171
impl AuthenticateFlow<RecordFound> {
72-
pub(super) fn password_hash(&self) -> &PasswordHash {
72+
pub(super) fn password_hash(&self) -> &password::Hash {
7373
&self.state_data.record.password_hash
7474
}
7575

7676
pub(super) fn verify_password(
7777
self,
78-
hasher: &dyn PasswordHasher,
78+
hasher: &dyn password::Hasher,
7979
) -> Result<PasswordCheckOutcome> {
8080
let verified =
8181
hasher.verify(self.password().expose_secret(), self.password_hash())?;
@@ -137,7 +137,7 @@ pub(super) enum PasswordCheckOutcome {
137137
impl LookupOutcome {
138138
pub(super) fn authenticate(
139139
self,
140-
hasher: &dyn PasswordHasher,
140+
hasher: &dyn password::Hasher,
141141
) -> Result<Option<AuthenticatedUser>> {
142142
match self {
143143
Self::Found(found) => Ok(found.verify_password(hasher)?.into_user_option()),
@@ -174,7 +174,7 @@ mod tests {
174174
.id(user::Id::new_v4())
175175
.username(test_username())
176176
.email(test_email())
177-
.password_hash(PasswordHash::new("hash"))
177+
.password_hash(password::Hash::new("hash"))
178178
.session_hash(super::super::SessionHash::new("session"))
179179
.build()
180180
}
@@ -190,12 +190,12 @@ mod tests {
190190
ok: bool,
191191
}
192192

193-
impl PasswordHasher for TestHasher {
194-
fn hash(&self, _password: &str) -> Result<PasswordHash> {
195-
Ok(PasswordHash::new("unused"))
193+
impl password::Hasher for TestHasher {
194+
fn hash(&self, _password: &str) -> Result<password::Hash> {
195+
Ok(password::Hash::new("unused"))
196196
}
197197

198-
fn verify(&self, _password: &str, _password_hash: &PasswordHash) -> Result<bool> {
198+
fn verify(&self, _password: &str, _password_hash: &password::Hash) -> Result<bool> {
199199
Ok(self.ok)
200200
}
201201
}

crates/app/src/auth/get_user_flow.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ mod tests {
9696
.id(user::Id::new_v4())
9797
.username(user::Username::try_new("person").expect("valid username"))
9898
.email(user::Email::try_new("person@example.com").expect("valid email"))
99-
.password_hash(super::super::PasswordHash::new("hash"))
99+
.password_hash(super::super::password::Hash::new("hash"))
100100
.session_hash(super::super::SessionHash::new("session"))
101101
.build()
102102
}

crates/app/src/auth/password.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use nutype::nutype;
2+
3+
use super::{BoxError, Result};
4+
5+
#[derive(Debug)]
6+
pub struct HashError(pub(super) BoxError);
7+
8+
impl core::fmt::Display for HashError {
9+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
10+
write!(f, "{}", self.0)
11+
}
12+
}
13+
14+
impl std::error::Error for HashError {
15+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
16+
Some(&*self.0)
17+
}
18+
}
19+
20+
pub trait Hasher: Send + Sync {
21+
fn hash(&self, password: &str) -> Result<Hash>;
22+
fn verify(&self, password: &str, password_hash: &Hash) -> Result<bool>;
23+
}
24+
25+
#[nutype(sanitize(trim), derive(Clone, Debug, PartialEq, Display))]
26+
pub struct Hash(String);

crates/app/src/auth/repository.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use domain::user;
2+
use snafu::prelude::*;
3+
use strum_macros::Display;
4+
5+
use super::BoxError;
6+
7+
#[derive(Clone, Copy, Debug, Display)]
8+
pub enum Operation {
9+
#[strum(serialize = "find auth record by email")]
10+
FindByEmail,
11+
#[strum(serialize = "find auth record by id")]
12+
FindById,
13+
}
14+
15+
#[derive(Debug, Snafu)]
16+
pub enum Error {
17+
#[snafu(display("auth repository query failed while {operation}: {source}"))]
18+
Query {
19+
operation: Operation,
20+
source: BoxError,
21+
},
22+
#[snafu(display("failed to decode auth username: {source}"))]
23+
DecodeUsername { source: user::UsernameError },
24+
#[snafu(display("failed to decode auth email: {source}"))]
25+
DecodeEmail { source: user::EmailError },
26+
}

crates/app/src/chat/audit.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use async_trait::async_trait;
2+
use bon::Builder;
3+
use nutype::nutype;
4+
use strum_macros::{Display, EnumString};
5+
6+
use super::{Result, chat};
7+
8+
#[derive(Clone, Debug, Builder)]
9+
pub struct Entry {
10+
pub room_id: chat::room::Id,
11+
pub actor_id: chat::UserId,
12+
pub action: Action,
13+
pub metadata: Vec<(Key, Value)>,
14+
}
15+
16+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Display, EnumString)]
17+
pub enum Action {
18+
#[strum(serialize = "chat.room.create")]
19+
RoomCreate,
20+
#[strum(serialize = "chat.room.join")]
21+
RoomJoin,
22+
#[strum(serialize = "chat.message.post")]
23+
MessagePost,
24+
#[strum(serialize = "chat.message.moderate")]
25+
MessageModerate,
26+
}
27+
28+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Display, EnumString)]
29+
pub enum Key {
30+
#[strum(serialize = "room_id")]
31+
RoomId,
32+
#[strum(serialize = "message_id")]
33+
MessageId,
34+
#[strum(serialize = "status")]
35+
Status,
36+
#[strum(serialize = "decision")]
37+
Decision,
38+
#[strum(serialize = "reason")]
39+
Reason,
40+
#[strum(serialize = "timestamp_ms")]
41+
TimestampMs,
42+
#[strum(serialize = "role")]
43+
Role,
44+
}
45+
46+
#[nutype(sanitize(trim), derive(Clone, Debug, PartialEq, Display))]
47+
pub struct Value(String);
48+
49+
#[async_trait]
50+
pub trait Log: Send + Sync {
51+
async fn record(&self, entry: Entry) -> Result<()>;
52+
}

crates/app/src/chat/create_room_flow.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use statum::{machine, state, transition};
22

3-
use super::{AuditAction, AuditKey, AuditValue, CreateRoom, RoomRole};
3+
use super::{CreateRoom, RoomRole, audit};
44
use domain::chat;
55

66
#[derive(Clone, Debug, PartialEq)]
@@ -123,10 +123,10 @@ impl CreateRoomFlow<OwnerMembershipAdded> {
123123
.record(service.audit_entry(
124124
room_id,
125125
created_by,
126-
AuditAction::RoomCreate,
126+
audit::Action::RoomCreate,
127127
vec![(
128-
AuditKey::RoomId,
129-
AuditValue::new(room_id.as_uuid().to_string()),
128+
audit::Key::RoomId,
129+
audit::Value::new(room_id.as_uuid().to_string()),
130130
)],
131131
))
132132
.await?;

0 commit comments

Comments
 (0)