Skip to content

Commit 0d2fa29

Browse files
authored
Merge pull request #82 from Govcraft/feat/issue-71-invite-onboard
feat: user invite & onboard (issue #71) + project_name branding
2 parents ef2f2a0 + 54352a4 commit 0d2fa29

24 files changed

Lines changed: 2349 additions & 23 deletions

File tree

Cargo.lock

Lines changed: 66 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ my-platform/
211211
└── main.rs
212212
```
213213

214+
The generated `config.toml` is seeded with `[schema_forge] project_name = "my-platform"` — the human-facing name shown to users in invitation emails and used as the default email `From` display-name. Edit it to whatever your users should recognize.
215+
214216
### Define a Schema
215217

216218
Create a file at `schemas/crm.schema`:
@@ -930,6 +932,7 @@ SchemaForge is under active development. All seven crates compile and pass 1123
930932
- Axum JSON API with dynamic CRUD routes and schema management
931933
- React site generator (`schemaforge site generate`) producing a Vite + Tailwind + shadcn app against the JSON API
932934
- Token-based authentication (PASETO) with an auth-store-backed login endpoint
935+
- Email-based user invitation and onboarding with single-use PASETO invite links and SMTP delivery (see [`docs/invitations-reference.md`](docs/invitations-reference.md))
933936
- Cedar authorization policy generation
934937
- Schema-level and field-level access control via `@access` and `@field_access` annotations
935938
- Record-level ownership-based access control

config.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ issuer = "schema-forge"
1515
per_user_rpm = 6000
1616
per_client_rpm = 60000
1717

18+
# Human-facing name of this deployment. Shown to onboarding users in
19+
# invitation emails (body + subject) and used as the default email From
20+
# display-name when the `from` below is a bare address. Defaults to
21+
# "SchemaForge" when unset — set it to your application's name.
22+
# [schema_forge]
23+
# project_name = "Bob's Dog Scheduling"
24+
25+
# Outbound email (user invitations, issue #71). Disabled by default.
26+
# The SMTP password is NEVER read from this file — supply it at runtime via
27+
# the SCHEMAFORGE_SMTP_PASSWORD environment variable so the secret stays out
28+
# of git. (acton-service's ACTON_-prefixed env layering can't target the
29+
# `[schema_forge]` section, so this dedicated env var is the supported path.)
30+
# [schema_forge.email]
31+
# enabled = true
32+
# host = "mail.example.gov"
33+
# port = 465 # 465 = implicit TLS (default); 587 = STARTTLS
34+
# tls = "implicit" # or "start_tls"
35+
# from = "Example <noreply@example.gov>"
36+
# username = "noreply@example.gov"
37+
# public_base_url = "https://app.example.gov" # used to build invite-accept links
38+
1839
# Webhook notification settings
1940
# [schema_forge.webhooks]
2041
# enabled = true

crates/schema-forge-acton/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "schema-forge-acton"
3-
version = "0.31.0"
3+
version = "0.32.0"
44
edition = "2021"
55

66
[dependencies]
@@ -43,6 +43,7 @@ toml = "1"
4343
aws-lc-rs = { version = "1", features = ["fips"], optional = true }
4444
rustls = { version = "0.23", default-features = false, features = ["std", "aws_lc_rs", "logging"] }
4545
schema-forge-signing = { version = "0.1.0", path = "../schema-forge-signing" }
46+
lettre = { version = "0.11.22", default-features = false, features = ["tokio1-rustls", "aws-lc-rs", "webpki-roots", "smtp-transport", "builder", "pool", "hostname"] }
4647

4748

4849
[features]

crates/schema-forge-acton/src/config.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ pub struct SchemaForgeConfig {
1919
/// SchemaForge settings.
2020
#[derive(Debug, Clone, Serialize, Deserialize)]
2121
pub struct SchemaForgeSettings {
22+
/// Human-facing name of this deployment, used wherever an end user sees
23+
/// the application by name rather than seeing the SchemaForge engine.
24+
/// Today that is the invitation email — its body and subject — and the
25+
/// default `From` display-name when [`crate::email::EmailConfig::from`] is
26+
/// a bare address. Defaults to `"SchemaForge"` so existing deployments are
27+
/// unchanged; a "Bob's Dog Scheduling" deployment sets this once and every
28+
/// onboarding touchpoint reads correctly.
29+
#[serde(default = "default_project_name")]
30+
pub project_name: String,
31+
2232
/// The URL path prefix for SchemaForge routes (default: "/forge").
2333
#[serde(default = "default_route_prefix")]
2434
pub route_prefix: String,
@@ -39,6 +49,12 @@ pub struct SchemaForgeSettings {
3949
#[serde(default)]
4050
pub storage: crate::storage::StorageConfig,
4151

52+
/// Outbound email (SMTP) settings, used by operational flows that must
53+
/// reach a human out-of-band — currently user invitations. Disabled by
54+
/// default; see [`crate::email::EmailConfig`].
55+
#[serde(default)]
56+
pub email: crate::email::EmailConfig,
57+
4258
/// Authorization configuration. Currently exposes operator-defined
4359
/// PASETO custom-claim → Cedar `Forge::Principal` attribute mappings;
4460
/// see [`crate::authz::principal_claims`].
@@ -123,14 +139,20 @@ fn default_route_prefix() -> String {
123139
"/forge".to_string()
124140
}
125141

142+
fn default_project_name() -> String {
143+
"SchemaForge".to_string()
144+
}
145+
126146
impl Default for SchemaForgeSettings {
127147
fn default() -> Self {
128148
Self {
149+
project_name: default_project_name(),
129150
route_prefix: default_route_prefix(),
130151
auto_generate_cedar_policies: false,
131152
webhooks: crate::webhook::WebhookConfig::default(),
132153
hooks: crate::hooks::HooksConfig::default(),
133154
storage: crate::storage::StorageConfig::default(),
155+
email: crate::email::EmailConfig::default(),
134156
authz: AuthzConfig::default(),
135157
signing: SigningConfig::default(),
136158
client: ClientConfig::default(),
@@ -153,11 +175,13 @@ mod tests {
153175
fn serde_roundtrip_preserves_all_fields() {
154176
let config = SchemaForgeConfig {
155177
schema_forge: SchemaForgeSettings {
178+
project_name: "Bob's Dog Scheduling".to_string(),
156179
route_prefix: "/api/forge".to_string(),
157180
auto_generate_cedar_policies: true,
158181
webhooks: crate::webhook::WebhookConfig::default(),
159182
hooks: crate::hooks::HooksConfig::default(),
160183
storage: crate::storage::StorageConfig::default(),
184+
email: crate::email::EmailConfig::default(),
161185
authz: AuthzConfig::default(),
162186
signing: SigningConfig::default(),
163187
client: ClientConfig::default(),
@@ -166,10 +190,27 @@ mod tests {
166190
let json = serde_json::to_string(&config).unwrap();
167191
let back: SchemaForgeConfig = serde_json::from_str(&json).unwrap();
168192
assert_eq!(back.schema_forge.route_prefix, "/api/forge");
193+
assert_eq!(back.schema_forge.project_name, "Bob's Dog Scheduling");
169194
assert!(back.schema_forge.auto_generate_cedar_policies);
170195
assert!(back.schema_forge.authz.principal_claims.is_empty());
171196
}
172197

198+
#[test]
199+
fn project_name_defaults_to_schemaforge() {
200+
let config: SchemaForgeConfig = serde_json::from_str("{}").unwrap();
201+
assert_eq!(config.schema_forge.project_name, "SchemaForge");
202+
}
203+
204+
#[test]
205+
fn project_name_deserialises_from_toml() {
206+
let toml = r#"
207+
[schema_forge]
208+
project_name = "Bob's Dog Scheduling"
209+
"#;
210+
let config: SchemaForgeConfig = toml::from_str(toml).unwrap();
211+
assert_eq!(config.schema_forge.project_name, "Bob's Dog Scheduling");
212+
}
213+
173214
#[test]
174215
fn principal_claims_section_deserialises() {
175216
let toml = r#"

0 commit comments

Comments
 (0)