You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-01 20:26:56 +03:00
Setup a repository to track user terms agreements
This commit is contained in:
17
crates/storage-pg/.sqlx/query-037fae6964130343453ef607791c4c3deaa01b5aaa091d3a3487caf3e2634daf.json
generated
Normal file
17
crates/storage-pg/.sqlx/query-037fae6964130343453ef607791c4c3deaa01b5aaa091d3a3487caf3e2634daf.json
generated
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO user_terms (user_terms_id, user_id, terms_url, created_at)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (user_id, terms_url) DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "037fae6964130343453ef607791c4c3deaa01b5aaa091d3a3487caf3e2634daf"
|
||||
}
|
32
crates/storage-pg/migrations/20240207100003_user_terms.sql
Normal file
32
crates/storage-pg/migrations/20240207100003_user_terms.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
-- Track when users have accepted the terms of service, and which version they accepted.
|
||||
CREATE TABLE user_terms (
|
||||
"user_terms_id" UUID NOT NULL
|
||||
PRIMARY KEY,
|
||||
|
||||
-- The user who accepted the terms of service.
|
||||
"user_id" UUID NOT NULL
|
||||
REFERENCES users (user_id) ON DELETE CASCADE,
|
||||
|
||||
-- The URL of the terms of service that the user accepted.
|
||||
"terms_url" TEXT NOT NULL,
|
||||
|
||||
-- When the user accepted the terms of service.
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
|
||||
-- Unique constraint to ensure that a user can only accept a given version of the terms once.
|
||||
UNIQUE ("user_id", "terms_url")
|
||||
);
|
@ -54,7 +54,7 @@ use crate::{
|
||||
},
|
||||
user::{
|
||||
PgBrowserSessionRepository, PgUserEmailRepository, PgUserPasswordRepository,
|
||||
PgUserRepository,
|
||||
PgUserRepository, PgUserTermsRepository,
|
||||
},
|
||||
DatabaseError,
|
||||
};
|
||||
@ -179,6 +179,12 @@ where
|
||||
Box::new(PgUserPasswordRepository::new(self.conn.as_mut()))
|
||||
}
|
||||
|
||||
fn user_terms<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn mas_storage::user::UserTermsRepository<Error = Self::Error> + 'c> {
|
||||
Box::new(PgUserTermsRepository::new(self.conn.as_mut()))
|
||||
}
|
||||
|
||||
fn browser_session<'c>(
|
||||
&'c mut self,
|
||||
) -> Box<dyn BrowserSessionRepository<Error = Self::Error> + 'c> {
|
||||
|
@ -29,13 +29,14 @@ use crate::{tracing::ExecuteExt, DatabaseError};
|
||||
mod email;
|
||||
mod password;
|
||||
mod session;
|
||||
mod terms;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use self::{
|
||||
email::PgUserEmailRepository, password::PgUserPasswordRepository,
|
||||
session::PgBrowserSessionRepository,
|
||||
session::PgBrowserSessionRepository, terms::PgUserTermsRepository,
|
||||
};
|
||||
|
||||
/// An implementation of [`UserRepository`] for a PostgreSQL connection
|
||||
|
82
crates/storage-pg/src/user/terms.rs
Normal file
82
crates/storage-pg/src/user/terms.rs
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::User;
|
||||
use mas_storage::{user::UserTermsRepository, Clock};
|
||||
use rand::RngCore;
|
||||
use sqlx::PgConnection;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{tracing::ExecuteExt, DatabaseError};
|
||||
|
||||
/// An implementation of [`UserTermsRepository`] for a PostgreSQL connection
|
||||
pub struct PgUserTermsRepository<'c> {
|
||||
conn: &'c mut PgConnection,
|
||||
}
|
||||
|
||||
impl<'c> PgUserTermsRepository<'c> {
|
||||
/// Create a new [`PgUserTermsRepository`] from an active PostgreSQL
|
||||
/// connection
|
||||
pub fn new(conn: &'c mut PgConnection) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'c> UserTermsRepository for PgUserTermsRepository<'c> {
|
||||
type Error = DatabaseError;
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_terms.accept_terms",
|
||||
skip_all,
|
||||
fields(
|
||||
db.statement,
|
||||
%user.id,
|
||||
user_terms.id,
|
||||
%user_terms.url = terms_url.as_str(),
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn accept_terms(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
terms_url: Url,
|
||||
) -> Result<(), Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
tracing::Span::current().record("user_terms.id", tracing::field::display(id));
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_terms (user_terms_id, user_id, terms_url, created_at)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, terms_url) DO NOTHING
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
Uuid::from(user.id),
|
||||
terms_url.as_str(),
|
||||
created_at,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -517,3 +517,58 @@ async fn test_user_session(pool: PgPool) {
|
||||
// This time the session is finished
|
||||
assert!(session_lookup.finished_at.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_user_terms(pool: PgPool) {
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap();
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.add(&mut rng, &clock, "john".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Accepting the terms should work
|
||||
repo.user_terms()
|
||||
.accept_terms(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&user,
|
||||
"https://example.com/terms".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Accepting a second time should also work
|
||||
repo.user_terms()
|
||||
.accept_terms(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&user,
|
||||
"https://example.com/terms".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Accepting a different terms should also work
|
||||
repo.user_terms()
|
||||
.accept_terms(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&user,
|
||||
"https://example.com/terms?v=2".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut conn = repo.into_inner();
|
||||
|
||||
// We should have two rows, as the first terms was deduped
|
||||
let res: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM user_terms")
|
||||
.fetch_one(&mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(res, 2);
|
||||
}
|
||||
|
Reference in New Issue
Block a user