Skip to content

Commit f44e9c8

Browse files
committed
Address old TODOs addressing site_url and default email sender.
1 parent 74afa15 commit f44e9c8

File tree

9 files changed

+62
-30
lines changed

9 files changed

+62
-30
lines changed

trailbase-core/src/admin/user/list_users.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ pub async fn list_users_handler(
6161
) -> Result<Json<ListUsersResponse>, Error> {
6262
let conn = state.user_conn();
6363

64-
// TODO: we should probably return an error if the query parsing fails rather than quietly
65-
// falling back to defaults.
6664
let QueryParseResult {
6765
params: filter_params,
6866
cursor,

trailbase-core/src/app_state.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use crate::value_notifier::{Computed, ValueNotifier};
2222
struct InternalState {
2323
data_dir: DataDir,
2424
public_dir: Option<PathBuf>,
25-
address: String,
25+
site_url: Computed<url::Url, Config>,
2626
dev: bool,
2727
demo: bool,
2828

@@ -74,6 +74,8 @@ impl AppState {
7474
pub(crate) fn new(args: AppStateArgs) -> Self {
7575
let config = ValueNotifier::new(args.config);
7676

77+
let site_url = Computed::new(&config, move |c| build_site_url(c, &args.address));
78+
7779
let record_apis = {
7880
let schema_metadata_clone = args.schema_metadata.clone();
7981
let conn_clone = args.conn.clone();
@@ -109,7 +111,7 @@ impl AppState {
109111
state: Arc::new(InternalState {
110112
data_dir: args.data_dir,
111113
public_dir: args.public_dir,
112-
address: args.address,
114+
site_url,
113115
dev: args.dev,
114116
demo: args.demo,
115117
oauth: Computed::new(&config, |c| {
@@ -233,11 +235,8 @@ impl AppState {
233235
.collect();
234236
}
235237

236-
// TODO: Turn this into a parsed url::Url.
237-
pub fn site_url(&self) -> String {
238-
self
239-
.access_config(|c| c.server.site_url.clone())
240-
.unwrap_or_else(|| format!("http://{}", self.state.address))
238+
pub fn site_url(&self) -> Arc<url::Url> {
239+
return self.state.site_url.load().clone();
241240
}
242241

243242
pub(crate) fn mailer(&self) -> Arc<Mailer> {
@@ -344,6 +343,7 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
344343
// Construct a fabricated config for tests and make sure it's valid.
345344
let mut config = Config::new_with_custom_defaults();
346345

346+
config.server.site_url = Some("https://test.org".to_string());
347347
config.email.smtp_host = Some("smtp.test.org".to_string());
348348
config.email.smtp_port = Some(587);
349349
config.email.smtp_username = Some("user".to_string());
@@ -429,11 +429,12 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
429429
});
430430
}
431431

432+
let address = "localhost:1234";
432433
return Ok(AppState {
433434
state: Arc::new(InternalState {
434435
data_dir,
435436
public_dir: None,
436-
address: "localhost:1234".to_string(),
437+
site_url: Computed::new(&config, move |c| build_site_url(c, address)),
437438
dev: true,
438439
demo: false,
439440
oauth: Computed::new(&config, |c| {
@@ -537,3 +538,20 @@ pub(crate) fn build_objectstore(
537538
object_store::local::LocalFileSystem::new_with_prefix(data_dir.uploads_path())?,
538539
));
539540
}
541+
542+
fn build_site_url(c: &Config, address: &str) -> url::Url {
543+
let fallback = url::Url::parse(&format!("http://{address}")).expect("startup");
544+
545+
if let Some(ref site_url) = c.server.site_url {
546+
match url::Url::parse(site_url) {
547+
Ok(url) => {
548+
return url;
549+
}
550+
Err(err) => {
551+
error!("Failed to parse site_url '{site_url}' from config: {err}");
552+
}
553+
};
554+
}
555+
556+
return fallback;
557+
}

trailbase-core/src/auth/api/avatar.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::auth::util::user_by_id;
1414
use crate::constants::{AVATAR_TABLE, RECORD_API_PATH};
1515
use crate::util::{assert_uuidv7_version, id_to_b64};
1616

17-
async fn get_avatar_url(state: &AppState, user: &DbUser) -> Option<String> {
17+
async fn get_avatar_url(state: &AppState, user: &DbUser) -> Option<url::Url> {
1818
lazy_static! {
1919
static ref QUERY: String =
2020
format!(r#"SELECT EXISTS(SELECT user FROM "{AVATAR_TABLE}" WHERE user = $1)"#);
@@ -32,12 +32,14 @@ async fn get_avatar_url(state: &AppState, user: &DbUser) -> Option<String> {
3232
.unwrap_or(false);
3333

3434
if has_avatar {
35-
let site = state.site_url();
3635
let record_user_id = id_to_b64(&user.id);
3736
let col_name = "file";
38-
return Some(format!(
39-
"{site}/{RECORD_API_PATH}/{AVATAR_TABLE}/{record_user_id}/file/{col_name}"
40-
));
37+
return state
38+
.site_url()
39+
.join(&format!(
40+
"/{RECORD_API_PATH}/{AVATAR_TABLE}/{record_user_id}/file/{col_name}"
41+
))
42+
.ok();
4143
}
4244

4345
return None;
@@ -68,7 +70,7 @@ pub async fn get_avatar_url_handler(
6870
// TODO: Allow a configurable fallback url.
6971
let avatar_url = get_avatar_url(&state, &db_user)
7072
.await
71-
.or(db_user.provider_avatar_url);
73+
.map_or_else(|| db_user.provider_avatar_url, |url| Some(url.to_string()));
7274

7375
// TODO: Maybe return a JSON response with url if content-type is JSON.
7476
return match avatar_url {
@@ -288,11 +290,11 @@ mod tests {
288290
.to_str()
289291
.unwrap();
290292

293+
let mut _url = url::Url::parse(location).unwrap();
291294
assert_eq!(
292295
location,
293296
format!(
294-
"{site}/{RECORD_API_PATH}/{AVATAR_COLLECTION_NAME}/{record_id_b64}/file/{COL_NAME}",
295-
site = state.site_url(),
297+
"https://test.org/{RECORD_API_PATH}/{AVATAR_COLLECTION_NAME}/{record_id_b64}/file/{COL_NAME}",
296298
record_id_b64 = uuid_to_b64(&record_id),
297299
)
298300
);

trailbase-core/src/auth/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub(super) fn router() -> Router<crate::AppState> {
7474
// * refresh-token (no CSRF, safe side-effect)
7575
// * logout (no CSRF, safe side-effect)
7676
// * change-password (no CSRF: requires old pass),
77-
// * change-email (TODO: CSRF: requires old email so only targeted),
77+
// * change-email (CSRF: requires old email so only targeted),
7878
// * delete-user (technically CSRF: however, currently DELETE method)
7979
//
8080
// Avatar life-cycle: read+update are handled as record APIs.

trailbase-core/src/auth/oauth/oauth_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ async fn test_oauth() {
159159
assert_eq!(auth_query.state, oauth_state.csrf_secret);
160160
assert_eq!(
161161
auth_query.redirect_uri,
162-
format!("{}/{AUTH_API_PATH}/oauth/{name}/callback", state.site_url())
162+
format!("http://localhost:1234/{AUTH_API_PATH}/oauth/{name}/callback")
163163
);
164164
assert_eq!(
165165
auth_query.code_challenge,

trailbase-core/src/auth/oauth/provider.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,13 @@ pub trait OAuthProvider {
6565
fn settings(&self) -> Result<OAuthClientSettings, AuthError>;
6666

6767
fn oauth_client(&self, state: &AppState) -> Result<OAuthClient, AuthError> {
68-
let redirect_url: Url = Url::parse(&format!(
69-
"{site}/{AUTH_API_PATH}/oauth/{name}/callback",
70-
site = state.site_url(),
71-
name = self.name()
72-
))
73-
.map_err(|err| AuthError::FailedDependency(err.into()))?;
68+
let redirect_url: Url = state
69+
.site_url()
70+
.join(&format!(
71+
"/{AUTH_API_PATH}/oauth/{name}/callback",
72+
name = self.name()
73+
))
74+
.map_err(|err| AuthError::FailedDependency(err.into()))?;
7475

7576
let settings = self.settings()?;
7677
if settings.client_id.is_empty() {

trailbase-core/src/email.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,21 @@ impl Email {
223223
fn get_sender(state: &AppState) -> Result<Mailbox, EmailError> {
224224
let (sender_address, sender_name) =
225225
state.access_config(|c| (c.email.sender_address.clone(), c.email.sender_name.clone()));
226-
// TODO: Have a better default sender, e.g. derive from SITE_URL.
227-
let address = sender_address.unwrap_or_else(|| "admin@localhost".to_string());
226+
let address = sender_address.unwrap_or_else(|| fallback_sender(&state.site_url()));
228227

229228
if let Some(ref name) = sender_name {
230229
return Ok(format!("{} <{}>", name, address).parse::<Mailbox>()?);
231230
}
232231
return Ok(address.parse::<Mailbox>()?);
233232
}
234233

234+
fn fallback_sender(site_url: &url::Url) -> String {
235+
if let Some(host) = site_url.host() {
236+
return format!("noreply@{}", host);
237+
}
238+
return "noreply@localhost".to_string();
239+
}
240+
235241
#[derive(Clone)]
236242
pub(crate) enum Mailer {
237243
Smtp(Arc<dyn AsyncTransport<Ok = smtp::response::Response, Error = smtp::Error> + Send + Sync>),
@@ -435,4 +441,11 @@ pub mod testing {
435441
assert!(email.body.contains(code));
436442
}
437443
}
444+
445+
#[test]
446+
fn test_fallback_sender() {
447+
let url = url::Url::parse("https://test.org").unwrap();
448+
let sender = fallback_sender(&url);
449+
assert_eq!("[email protected]", sender);
450+
}
438451
}

trailbase-core/tests/admin_permissions_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fn test_admin_permissions() {
2020
tls,
2121
} = Server::init(ServerOptions {
2222
data_dir: DataDir(data_dir.path().to_path_buf()),
23-
address: "".to_string(),
23+
address: "localhost:4040".to_string(),
2424
admin_address: None,
2525
public_dir: None,
2626
dev: false,

trailbase-core/tests/integration_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async fn test_record_apis() {
4242
tls,
4343
} = Server::init(ServerOptions {
4444
data_dir: DataDir(data_dir.path().to_path_buf()),
45-
address: "".to_string(),
45+
address: "localhost:4041".to_string(),
4646
admin_address: None,
4747
public_dir: None,
4848
dev: false,

0 commit comments

Comments
 (0)