1
0
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:
Quentin Gliech
2024-02-07 11:17:05 +01:00
parent c0afe98507
commit 90c386847a
9 changed files with 270 additions and 3 deletions

View 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"
}

View 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")
);

View File

@ -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> {

View File

@ -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

View 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(())
}
}

View File

@ -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);
}