Skip to content

Commit f21673c

Browse files
0xPoeTurbo87
authored andcommitted
feat: find tokens expiring with couple days
1 parent d73353f commit f21673c

7 files changed

+64
-9
lines changed

src/models/token.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub struct ApiToken {
3030
pub endpoint_scopes: Option<Vec<EndpointScope>>,
3131
#[serde(with = "rfc3339::option")]
3232
pub expired_at: Option<NaiveDateTime>,
33+
#[serde(with = "rfc3339::option")]
34+
pub expiry_notification_at: Option<NaiveDateTime>,
3335
}
3436

3537
impl ApiToken {
@@ -95,6 +97,25 @@ impl ApiToken {
9597
.or_else(|_| tokens.select(ApiToken::as_select()).first(conn))
9698
.map_err(Into::into)
9799
}
100+
101+
/// Find all tokens that are not revoked and will expire within the specified number of days.
102+
pub fn find_tokens_expiring_within_days(
103+
conn: &mut PgConnection,
104+
days_until_expiry: i64,
105+
) -> QueryResult<Vec<ApiToken>> {
106+
use diesel::dsl::{now, IntervalDsl};
107+
108+
api_tokens::table
109+
.filter(api_tokens::revoked.eq(false))
110+
.filter(
111+
api_tokens::expired_at
112+
.is_not_null()
113+
.and(api_tokens::expired_at.lt(now.nullable() + days_until_expiry.days())),
114+
)
115+
.filter(api_tokens::expiry_notification_at.is_null())
116+
.select(ApiToken::as_select())
117+
.get_results(conn)
118+
}
98119
}
99120

100121
#[derive(Debug)]
@@ -125,6 +146,7 @@ mod tests {
125146
crate_scopes: None,
126147
endpoint_scopes: None,
127148
expired_at: None,
149+
expiry_notification_at: None,
128150
};
129151
let json = serde_json::to_string(&tok).unwrap();
130152
assert_some!(json

src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__create__create_token_success.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
source: src/tests/routes/me/tokens/create.rs
3-
expression: response.into_json()
3+
expression: response.json()
44
---
55
{
66
"api_token": {
77
"crate_scopes": null,
88
"created_at": "[datetime]",
99
"endpoint_scopes": null,
1010
"expired_at": null,
11+
"expiry_notification_at": null,
1112
"id": "[id]",
1213
"last_used_at": "[datetime]",
1314
"name": "bar",

src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__create__create_token_with_expiry_date.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
source: src/tests/routes/me/tokens/create.rs
3-
expression: response.into_json()
3+
expression: response.json()
44
---
55
{
66
"api_token": {
77
"crate_scopes": null,
88
"created_at": "[datetime]",
99
"endpoint_scopes": null,
1010
"expired_at": "2024-12-24T07:34:56+00:00",
11+
"expiry_notification_at": null,
1112
"id": "[id]",
1213
"last_used_at": "[datetime]",
1314
"name": "bar",

src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__create__create_token_with_null_scopes.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
source: src/tests/routes/me/tokens/create.rs
3-
expression: response.into_json()
3+
expression: response.json()
44
---
55
{
66
"api_token": {
77
"crate_scopes": null,
88
"created_at": "[datetime]",
99
"endpoint_scopes": null,
1010
"expired_at": null,
11+
"expiry_notification_at": null,
1112
"id": "[id]",
1213
"last_used_at": "[datetime]",
1314
"name": "bar",

src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__create__create_token_with_scopes.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: src/tests/routes/me/tokens/create.rs
3-
expression: response.into_json()
3+
expression: response.json()
44
---
55
{
66
"api_token": {
@@ -13,6 +13,7 @@ expression: response.into_json()
1313
"publish-update"
1414
],
1515
"expired_at": null,
16+
"expiry_notification_at": null,
1617
"id": "[id]",
1718
"last_used_at": "[datetime]",
1819
"name": "bar",

src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__list__list_tokens.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: src/tests/routes/me/tokens/list.rs
3-
expression: response.into_json()
3+
expression: response.json()
44
---
55
{
66
"api_tokens": [
@@ -14,6 +14,7 @@ expression: response.into_json()
1414
"publish-update"
1515
],
1616
"expired_at": null,
17+
"expiry_notification_at": null,
1718
"id": "[id]",
1819
"last_used_at": "[datetime]",
1920
"name": "baz"
@@ -23,6 +24,7 @@ expression: response.into_json()
2324
"created_at": "[datetime]",
2425
"endpoint_scopes": null,
2526
"expired_at": null,
27+
"expiry_notification_at": null,
2628
"id": "[id]",
2729
"last_used_at": "[datetime]",
2830
"name": "bar"

src/worker/jobs/expiry_notification.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use std::sync::Arc;
2+
use diesel::{Connection, PgConnection};
23

34
use crates_io_worker::BackgroundJob;
4-
5+
use crate::Emails;
56
use crate::worker::Environment;
67

8+
/// The threshold in days for the expiry notification.
9+
const EXPIRY_THRESHOLD: i64 = 3;
10+
711
/// A job responsible for monitoring the status of a token.
812
/// It checks if the token is about to reach its expiry date.
913
/// If the token is about to expire, the job triggers a notification.
@@ -17,8 +21,31 @@ impl BackgroundJob for CheckAboutToExpireToken {
1721

1822
#[instrument(skip(env), err)]
1923
async fn run(&self, env: Self::Context) -> anyhow::Result<()> {
20-
// Check if the token is about to expire
21-
// If the token is about to expire, trigger a notification.
22-
Ok(())
24+
let conn = env.deadpool.get().await?;
25+
conn.interact(move |conn| {
26+
// Check if the token is about to expire
27+
// If the token is about to expire, trigger a notification.
28+
check(&env.emails, conn)
29+
})
30+
.await
31+
.map_err(|err| anyhow!(err.to_string()))?
2332
}
2433
}
34+
// Check if the token is about to expire and send a notification if it is.
35+
fn check(emails: &Emails, conn: &mut PgConnection) -> anyhow::Result<()> {
36+
info!("Checking if tokens are about to expire");
37+
let expired_tokens =
38+
crate::models::token::ApiToken::find_tokens_expiring_within_days(conn, EXPIRY_THRESHOLD)?;
39+
// Batch send notifications in transactions.
40+
const BATCH_SIZE: usize = 100;
41+
for chunk in expired_tokens.chunks(BATCH_SIZE) {
42+
conn.transaction(|conn| {
43+
for token in chunk {
44+
// Send notification.
45+
}
46+
Ok::<_, anyhow::Error>(())
47+
})?;
48+
}
49+
50+
Ok(())
51+
}

0 commit comments

Comments
 (0)