From 819caae9147e545b194ae12f43e62c7ca84e65c0 Mon Sep 17 00:00:00 2001
From: Brent Schroeter
Date: Wed, 23 Apr 2025 12:57:10 -0700
Subject: [PATCH] implement watchdog timers
---
Cargo.lock | 32 +++
Cargo.toml | 1 +
.../2025-04-22-053211_watchdogs/down.sql | 1 +
migrations/2025-04-22-053211_watchdogs/up.sql | 10 +
src/api_keys.rs | 48 ++--
src/channel_selections.rs | 4 +-
src/channels.rs | 23 +-
src/channels_router.rs | 140 +++++++----
src/csrf.rs | 26 +-
src/governors.rs | 208 +++++++++++-----
src/main.rs | 1 +
src/messages.rs | 77 +++++-
src/projects.rs | 110 ++++++---
src/projects_router.rs | 87 ++++---
src/schema.rs | 13 +
src/team_invitations.rs | 14 +-
src/team_memberships.rs | 8 +-
src/teams.rs | 26 +-
src/teams_router.rs | 96 +++++---
src/users.rs | 10 +-
src/v0_router.rs | 230 +++++++++---------
src/watchdogs.rs | 67 +++++
src/worker.rs | 78 +++++-
templates/project.html | 84 +++++--
templates/projects.html | 4 +-
25 files changed, 964 insertions(+), 434 deletions(-)
create mode 100644 migrations/2025-04-22-053211_watchdogs/down.sql
create mode 100644 migrations/2025-04-22-053211_watchdogs/up.sql
create mode 100644 src/watchdogs.rs
diff --git a/Cargo.lock b/Cargo.lock
index 7a1062b..3f004bc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -764,6 +764,37 @@ dependencies = [
"powerfmt",
]
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn",
+]
+
[[package]]
name = "diesel"
version = "2.2.8"
@@ -2644,6 +2675,7 @@ dependencies = [
"clap",
"config",
"deadpool-diesel",
+ "derive_builder",
"diesel",
"diesel_migrations",
"dotenvy",
diff --git a/Cargo.toml b/Cargo.toml
index 0d28dc9..57e4aed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.31", features = ["derive"] }
config = "0.14.1"
deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] }
+derive_builder = "0.20.2"
diesel = { version = "2.2.6", features = ["postgres", "chrono", "uuid", "serde_json"] }
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
dotenvy = "0.15.7"
diff --git a/migrations/2025-04-22-053211_watchdogs/down.sql b/migrations/2025-04-22-053211_watchdogs/down.sql
new file mode 100644
index 0000000..2214cff
--- /dev/null
+++ b/migrations/2025-04-22-053211_watchdogs/down.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS watchdogs;
diff --git a/migrations/2025-04-22-053211_watchdogs/up.sql b/migrations/2025-04-22-053211_watchdogs/up.sql
new file mode 100644
index 0000000..ddaac01
--- /dev/null
+++ b/migrations/2025-04-22-053211_watchdogs/up.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS watchdogs (
+ id UUID PRIMARY KEY NOT NULL,
+ project_id UUID UNIQUE NOT NULL REFERENCES projects (id) ON DELETE RESTRICT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ last_set_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ expiration TIMESTAMPTZ NOT NULL,
+ notified BOOLEAN NOT NULL DEFAULT FALSE
+);
+CREATE INDEX ON watchdogs (project_id);
+CREATE INDEX ON watchdogs (expiration);
diff --git a/src/api_keys.rs b/src/api_keys.rs
index 5e6bfeb..185059d 100644
--- a/src/api_keys.rs
+++ b/src/api_keys.rs
@@ -1,21 +1,21 @@
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use chrono::{DateTime, Utc};
-use deadpool_diesel::postgres::Connection;
use diesel::{
- dsl::{auto_type, AsSelect},
+ dsl::{auto_type, update, AsSelect},
pg::Pg,
prelude::*,
};
use uuid::Uuid;
-use crate::{app_error::AppError, schema::api_keys, teams::Team};
+use crate::{app_error::AppError, schema::api_keys};
+
+pub use crate::schema::api_keys::{dsl, table};
/// A team-scoped application key for authenticating API calls to /say, etc.
/// Does not authorize any administrative functions besides creating projects.
-#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
+#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
#[diesel(table_name = api_keys)]
-#[diesel(belongs_to(Team))]
pub struct ApiKey {
pub id: Uuid,
pub team_id: Uuid,
@@ -23,38 +23,28 @@ pub struct ApiKey {
}
impl ApiKey {
- pub async fn generate_for_team(db_conn: &Connection, team_id: Uuid) -> Result {
- let api_key = Self {
+ pub fn new_from_team_id(team_id: Uuid) -> Self {
+ Self {
team_id,
id: Uuid::new_v4(),
last_used_at: None,
- };
- let api_key_copy = api_key.clone();
- db_conn
- .interact(move |conn| {
- diesel::insert_into(api_keys::table)
- .values(api_key_copy)
- .execute(conn)
- })
- .await
- .unwrap()?;
- Ok(api_key)
+ }
}
#[auto_type(no_type_alias)]
pub fn all() -> _ {
- let select: AsSelect = ApiKey::as_select();
- api_keys::table.select(select)
+ let select: AsSelect = Self::as_select();
+ table.select(select)
}
#[auto_type(no_type_alias)]
pub fn with_id<'a>(id: &'a Uuid) -> _ {
- api_keys::id.eq(id)
+ dsl::id.eq(id)
}
#[auto_type(no_type_alias)]
pub fn with_team<'a>(team_id: &'a Uuid) -> _ {
- api_keys::team_id.eq(team_id)
+ dsl::team_id.eq(team_id)
}
}
@@ -78,3 +68,15 @@ pub fn try_parse_as_uuid(value: &str) -> Result {
Uuid::try_parse(value).or(Err(anyhow::anyhow!("failed to parse")))
}
}
+
+pub fn use_api_key(key_id: &str, db_conn: &mut PgConnection) -> Result {
+ let normalized_id =
+ try_parse_as_uuid(key_id).or(Err(AppError::Forbidden("Key not accepted.".to_string())))?;
+ update(table.filter(ApiKey::with_id(&normalized_id)))
+ .set(dsl::last_used_at.eq(diesel::dsl::now))
+ .returning(ApiKey::as_returning())
+ .get_result(db_conn)
+ .optional()
+ .context("failed to load api key")?
+ .ok_or(AppError::Forbidden("Key not accepted.".to_owned()))
+}
diff --git a/src/channel_selections.rs b/src/channel_selections.rs
index c88a1f6..35b8303 100644
--- a/src/channel_selections.rs
+++ b/src/channel_selections.rs
@@ -7,7 +7,9 @@ use uuid::Uuid;
use crate::schema::channel_selections;
-#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
+pub use crate::schema::channel_selections::{dsl, table};
+
+#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
#[diesel(belongs_to(crate::channels::Channel))]
#[diesel(belongs_to(crate::projects::Project))]
#[diesel(primary_key(channel_id, project_id))]
diff --git a/src/channels.rs b/src/channels.rs
index 5bae8cc..7dcc31f 100644
--- a/src/channels.rs
+++ b/src/channels.rs
@@ -1,5 +1,6 @@
use std::fmt::Debug;
+use derive_builder::Builder;
use diesel::{
backend::Backend,
deserialize::{self, FromSql, FromSqlRow},
@@ -14,7 +15,7 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use uuid::Uuid;
-use crate::{schema::channels, teams::Team};
+use crate::schema::channels;
pub const CHANNEL_BACKEND_EMAIL: &str = "email";
pub const CHANNEL_BACKEND_SLACK: &str = "slack";
@@ -25,8 +26,7 @@ pub use crate::schema::channels::{dsl, table};
/// defined in the backend_config field. A single channel may be attached to
/// (in other words, "enabled" or "selected" for) any number of projects within
/// the same team.
-#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
-#[diesel(belongs_to(Team))]
+#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(check_for_backend(Pg))]
pub struct Channel {
pub id: Uuid,
@@ -57,6 +57,23 @@ impl Channel {
pub fn where_enabled_by_default() -> _ {
channels::enable_by_default.eq(true)
}
+
+ pub fn insertable_builder() -> InsertableChannelBuilder {
+ InsertableChannelBuilder::default()
+ }
+}
+
+#[derive(Builder, Clone, Debug, Insertable)]
+#[diesel(table_name = channels)]
+#[builder(pattern = "owned", setter(prefix = "with"))]
+pub struct InsertableChannel {
+ #[builder(setter(skip), default = "uuid::Uuid::now_v7()")]
+ id: Uuid,
+ team_id: Uuid,
+ name: String,
+ #[builder(setter(strip_option), default)]
+ enable_by_default: Option,
+ backend_config: BackendConfig,
}
// Note: In a previous implementation, channel configuration was handled by
diff --git a/src/channels_router.rs b/src/channels_router.rs
index 2ea4138..2f4df07 100644
--- a/src/channels_router.rs
+++ b/src/channels_router.rs
@@ -1,8 +1,8 @@
-use anyhow::Context as _;
+use anyhow::{Context as _, Result};
use askama::Template;
use axum::{
- extract::{Path, State},
- response::{Html, IntoResponse, Redirect},
+ extract::{OriginalUri, Path, State},
+ response::{Html, IntoResponse as _, Redirect, Response},
routing::{get, post},
Router,
};
@@ -16,17 +16,16 @@ use crate::{
app_error::AppError,
app_state::{AppState, DbConn, ReqwestClient},
channels::{
- BackendConfig, Channel, EmailBackendConfig, SlackBackendConfig, CHANNEL_BACKEND_EMAIL,
- CHANNEL_BACKEND_SLACK,
+ self, BackendConfig, Channel, EmailBackendConfig, SlackBackendConfig,
+ CHANNEL_BACKEND_EMAIL, CHANNEL_BACKEND_SLACK,
},
csrf::generate_csrf_token,
email::{is_permissible_email, MailSender as _, Mailer},
guards,
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_CHANNELS},
- schema::channels,
settings::{Settings, SlackSettings},
slack_auth,
- slack_utils::{self, ConversationType, SlackClient},
+ slack_utils::{self, ConversationType, SlackClient, SlackError},
users::CurrentUser,
};
@@ -82,7 +81,7 @@ async fn channels_page(
DbConn(db_conn): DbConn,
Path(team_id): Path,
CurrentUser(current_user): CurrentUser,
-) -> Result {
+) -> Result {
let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
let channels = {
@@ -139,41 +138,42 @@ async fn post_new_channel(
Path(team_id): Path,
CurrentUser(current_user): CurrentUser,
Form(form_body): Form,
-) -> Result {
+) -> Result {
let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?;
- let channel_id = Uuid::now_v7();
let channel = match form_body.channel_type.as_str() {
CHANNEL_BACKEND_EMAIL => db_conn
- .interact::<_, Result>(move |conn| {
- Ok(diesel::insert_into(channels::table)
- .values((
- channels::id.eq(channel_id),
- channels::team_id.eq(team_id),
- channels::name.eq("Untitled Email Channel"),
- channels::backend_config
- .eq(Into::::into(EmailBackendConfig::default())),
- ))
+ .interact(move |conn| -> Result {
+ diesel::insert_into(channels::table)
+ .values(
+ Channel::insertable_builder()
+ .with_team_id(team_id)
+ .with_name("Untitled Email Channel".to_owned())
+ .with_backend_config(EmailBackendConfig::default().into())
+ .build()
+ .context("failed to build insertable channel")?,
+ )
.returning(Channel::as_returning())
.get_result(conn)
- .context("Failed to insert new EmailChannel.")?)
+ .context("Failed to insert new EmailChannel.")
})
.await
.unwrap()?,
CHANNEL_BACKEND_SLACK => db_conn
- .interact::<_, Result>(move |conn| {
- Ok(diesel::insert_into(channels::table)
- .values((
- channels::id.eq(channel_id),
- channels::team_id.eq(team_id),
- channels::name.eq("Untitled Slack Channel"),
- channels::backend_config
- .eq(Into::::into(SlackBackendConfig::default())),
- ))
+ .interact(move |conn| -> Result {
+ diesel::insert_into(channels::table)
+ .values(
+ Channel::insertable_builder()
+ .with_team_id(team_id)
+ .with_name("Untitled Slack Channel".to_owned())
+ .with_backend_config(SlackBackendConfig::default().into())
+ .build()
+ .context("failed to build insertable channel")?,
+ )
.returning(Channel::as_returning())
.get_result(conn)
- .context("Failed to insert new SlackChannel.")?)
+ .context("Failed to insert new SlackChannel.")
})
.await
.unwrap()?,
@@ -189,7 +189,8 @@ async fn post_new_channel(
base_path,
team.id.simple(),
channel.id.simple()
- )))
+ ))
+ .into_response())
}
async fn channel_page(
@@ -206,7 +207,8 @@ async fn channel_page(
DbConn(db_conn): DbConn,
Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
-) -> Result {
+ OriginalUri(original_uri): OriginalUri,
+) -> Result {
let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
let channel = {
@@ -260,21 +262,53 @@ async fn channel_page(
.build(),
}
.render()?,
- ))
+ )
+ .into_response())
}
BackendConfig::Slack(slack_data) => {
- let slack_client = slack_data.oauth_tokens.map(|tokens| {
+ let slack_client = slack_data.oauth_tokens.clone().map(|tokens| {
SlackClient::new(&tokens.access_token)
.with_reqwest_client(reqwest_client)
.with_api_root(&slack_api_root)
});
let slack_channels = if let Some(client) = slack_client {
- client
+ match client
.list_conversations()
.with_types([ConversationType::PublicChannel])
.with_exclude_archived(true)
.load_all()
- .await?
+ .await
+ {
+ Err(SlackError::Api(slack_utils::ApiError {
+ error: slack_utils::ErrorCode::AccountInactive,
+ })) => {
+ // Access needs to be reauthorized.
+ tracing::info!("encountered account_inactive error for slack backend of channel {}; resetting oauth tokens", channel.id);
+ let new_slack_data = SlackBackendConfig {
+ oauth_tokens: None,
+ ..slack_data
+ };
+ db_conn
+ .interact(move |conn| -> Result<()> {
+ diesel::update(
+ channels::table.filter(Channel::with_id(&channel.id)),
+ )
+ .set(
+ channels::dsl::backend_config
+ .eq(BackendConfig::from(new_slack_data)),
+ )
+ .execute(conn)
+ .context("failed to clear oauth tokens on slack backend config")
+ .and(Ok(()))
+ })
+ .await
+ .unwrap()?;
+ // Have the HTTP client refresh now that the old OAuth
+ // tokens have been cleared.
+ return Ok(Redirect::to(&original_uri.to_string()).into_response());
+ }
+ other => other,
+ }?
} else {
Vec::new()
};
@@ -306,7 +340,8 @@ async fn channel_page(
slack_channels,
}
.render()?,
- ))
+ )
+ .into_response())
}
}
}
@@ -324,7 +359,7 @@ async fn update_channel(
Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
Form(form_body): Form,
-) -> Result {
+) -> Result {
guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?;
guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
@@ -337,8 +372,8 @@ async fn update_channel(
.filter(Channel::with_team(&team_id)),
)
.set((
- channels::name.eq(form_body.name),
- channels::enable_by_default
+ channels::dsl::name.eq(form_body.name),
+ channels::dsl::enable_by_default
.eq(form_body.enable_by_default.unwrap_or("false".to_string()) == "true"),
))
.execute(conn)
@@ -379,7 +414,7 @@ async fn update_channel_email_recipient(
Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
Form(form_body): Form,
-) -> Result {
+) -> Result {
guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?;
guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
@@ -402,14 +437,14 @@ async fn update_channel_email_recipient(
// TODO: transaction retries
conn.transaction::<_, AppError, _>(move |conn| {
let channel = get_channel_by_params(conn, &team_id, &channel_id)?;
- let new_config = BackendConfig::Email(EmailBackendConfig {
+ let new_config = EmailBackendConfig {
recipient,
verification_code,
verification_code_guesses: 0,
..channel.backend_config.try_into()?
- });
+ };
let num_rows = diesel::update(channels::table.filter(Channel::with_id(&channel.id)))
- .set(channels::backend_config.eq(new_config))
+ .set(channels::dsl::backend_config.eq(BackendConfig::from(new_config)))
.execute(conn)?;
if num_rows != 1 {
return Err(anyhow::anyhow!(
@@ -447,7 +482,8 @@ async fn update_channel_email_recipient(
base_path,
team_id.simple(),
channel_id.simple()
- )))
+ ))
+ .into_response())
}
#[derive(Deserialize)]
@@ -462,7 +498,7 @@ async fn update_channel_slack_conversation(
Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
Form(form): Form,
-) -> Result {
+) -> Result {
guards::require_valid_csrf_token(&form.csrf_token, ¤t_user, &db_conn).await?;
guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
@@ -491,7 +527,7 @@ async fn update_channel_slack_conversation(
// TODO: Ensure this holds true with private channels and groups.
slack_data.conversation_id = Some(form.conversation_id);
let num_rows = diesel::update(channels::table.filter(Channel::with_id(&channel.id)))
- .set(channels::backend_config.eq(BackendConfig::from(slack_data)))
+ .set(channels::dsl::backend_config.eq(BackendConfig::from(slack_data)))
.execute(conn)?;
tracing::debug!("updated {} rows", num_rows);
// If the channel is deleted while this db interaction is running, 0
@@ -509,7 +545,8 @@ async fn update_channel_slack_conversation(
base_path,
team_id.simple(),
channel_id.simple()
- )))
+ ))
+ .into_response())
}
#[derive(Deserialize)]
@@ -524,7 +561,7 @@ async fn verify_email(
Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
Form(form_body): Form,
-) -> Result {
+) -> Result {
guards::require_valid_csrf_token(&form_body.csrf_token, ¤t_user, &db_conn).await?;
guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
@@ -565,7 +602,7 @@ async fn verify_email(
}
};
diesel::update(channels::table.filter(Channel::with_id(&channel_id)))
- .set(channels::backend_config.eq(Into::::into(new_config)))
+ .set(channels::dsl::backend_config.eq(BackendConfig::from(new_config)))
.execute(conn)?;
Ok(())
})
@@ -579,5 +616,6 @@ async fn verify_email(
base_path,
team_id.simple(),
channel_id.simple()
- )))
+ ))
+ .into_response())
}
diff --git a/src/csrf.rs b/src/csrf.rs
index d5270be..12c305d 100644
--- a/src/csrf.rs
+++ b/src/csrf.rs
@@ -7,12 +7,14 @@ use diesel::{
};
use uuid::Uuid;
-use crate::{app_error::AppError, schema::csrf_tokens::dsl::*};
+use crate::{app_error::AppError, schema::csrf_tokens};
+
+pub use crate::schema::csrf_tokens::{dsl, table};
const TOKEN_PREFIX: &str = "csrf-";
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
-#[diesel(table_name = crate::schema::csrf_tokens)]
+#[diesel(table_name = csrf_tokens)]
#[diesel(check_for_backend(Pg))]
pub struct CsrfToken {
pub id: Uuid,
@@ -21,24 +23,24 @@ pub struct CsrfToken {
}
impl CsrfToken {
- fn all() -> Select> {
- csrf_tokens.select(Self::as_select())
+ fn all() -> Select> {
+ table.select(Self::as_select())
}
- pub fn is_not_expired() -> Gt> {
+ pub fn is_not_expired() -> Gt> {
let ttl = TimeDelta::hours(24);
let min_created_at: DateTime = Utc::now() - ttl;
- created_at.gt(min_created_at)
+ dsl::created_at.gt(min_created_at)
}
#[auto_type(no_type_alias)]
pub fn with_user_id<'a>(token_user_id: &'a Option) -> _ {
- user_id.is_not_distinct_from(token_user_id)
+ dsl::user_id.is_not_distinct_from(token_user_id)
}
#[auto_type(no_type_alias)]
pub fn with_token_id<'a>(token_id: &'a Uuid) -> _ {
- id.eq(token_id)
+ dsl::id.eq(token_id)
}
}
@@ -50,11 +52,11 @@ pub async fn generate_csrf_token(
let token_id = Uuid::new_v4();
db_conn
.interact(move |conn| {
- diesel::insert_into(csrf_tokens)
+ diesel::insert_into(table)
.values((
- id.eq(token_id),
- user_id.eq(with_user_id),
- created_at.eq(diesel::dsl::now),
+ dsl::id.eq(token_id),
+ dsl::user_id.eq(with_user_id),
+ dsl::created_at.eq(diesel::dsl::now),
))
.execute(conn)
})
diff --git a/src/governors.rs b/src/governors.rs
index e4a2808..ccacb69 100644
--- a/src/governors.rs
+++ b/src/governors.rs
@@ -1,9 +1,10 @@
// Fault tolerant rate limiting backed by Postgres.
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use chrono::{DateTime, TimeDelta, Utc};
+use derive_builder::Builder;
use diesel::{
- dsl::{auto_type, insert_into, AsSelect},
+ dsl::{auto_type, AsSelect},
pg::Pg,
prelude::*,
sql_types::Timestamptz,
@@ -12,12 +13,14 @@ use uuid::Uuid;
use crate::schema::{governor_entries, governors};
+pub use crate::schema::governors::{dsl, table};
+
// Expose built-in Postgres GREATEST() function to Diesel
define_sql_function! {
fn greatest(a: diesel::sql_types::Integer, b: diesel::sql_types::Integer) -> Integer
}
-#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
+#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = governors)]
pub struct Governor {
pub id: Uuid,
@@ -29,75 +32,86 @@ pub struct Governor {
}
impl Governor {
- pub fn insert_new<'a>(
- db_conn: &mut diesel::PgConnection,
- team_id: &'a Uuid,
- project_id: Option<&'a Uuid>,
- window_size: &'a TimeDelta,
- max_count: i32,
- ) -> Result {
- let id: Uuid = Uuid::now_v7();
- Ok(insert_into(governors::table)
- .values((
- governors::team_id.eq(team_id),
- governors::id.eq(id),
- governors::project_id.eq(project_id),
- governors::window_size.eq(window_size),
- governors::max_count.eq(max_count),
- ))
- .get_result(db_conn)?)
+ pub fn insertable_builder() -> InsertableGovernorBuilder {
+ InsertableGovernorBuilder::default()
+ }
+
+ pub fn lazy_getter() -> LazyGetterBuilder {
+ LazyGetterBuilder::default()
}
#[auto_type(no_type_alias)]
pub fn all() -> _ {
let select: AsSelect = Governor::as_select();
- governors::table.select(select)
+ table.select(select)
}
#[auto_type(no_type_alias)]
pub fn with_id<'a>(governor_id: &'a Uuid) -> _ {
- governors::id.eq(governor_id)
+ dsl::id.eq(governor_id)
}
#[auto_type(no_type_alias)]
pub fn with_team<'a>(team_id: &'a Uuid) -> _ {
- governors::team_id.eq(team_id)
+ dsl::team_id.eq(team_id)
}
#[auto_type(no_type_alias)]
pub fn with_project<'a>(project_id: &'a Option) -> _ {
- governors::project_id.is_not_distinct_from(project_id)
+ dsl::project_id.is_not_distinct_from(project_id)
}
- // TODO: return a custom result enum instead of a Result
diff --git a/templates/projects.html b/templates/projects.html
index df54acf..ecfad0f 100644
--- a/templates/projects.html
+++ b/templates/projects.html
@@ -17,12 +17,12 @@
- https://shout.dev{{ base_path }}/say?project=my-first-project&key=***&message=Hello,%20World
+ {{ frontend_host }}{{ base_path }}/say?project=my-first-project&key=***&message=Hello,%20World
- https://shout.dev{{ base_path }}/watchdog?project=my-first-project&key=***&seconds=300
+ {{ frontend_host }}{{ base_path }}/watchdog?project=my-first-project&key=***&timeout_minutes=15