server: Add a Uuid attribute to every user and group
This commit is contained in:
committed by
nitnelave
parent
cbde363fde
commit
c72c1fdf2c
@@ -3,9 +3,57 @@ use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||
#[derive(
|
||||
PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize, sqlx::FromRow, sqlx::Type,
|
||||
)]
|
||||
#[serde(try_from = "&str")]
|
||||
#[sqlx(transparent)]
|
||||
pub struct Uuid(String);
|
||||
|
||||
impl Uuid {
|
||||
pub fn from_name_and_date(name: &str, creation_date: &chrono::DateTime<chrono::Utc>) -> Self {
|
||||
Uuid(
|
||||
uuid::Uuid::new_v3(
|
||||
&uuid::Uuid::NAMESPACE_X500,
|
||||
&[name.as_bytes(), creation_date.to_rfc3339().as_bytes()].concat(),
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::convert::TryFrom<&'a str> for Uuid {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(s: &'a str) -> anyhow::Result<Self> {
|
||||
Ok(Uuid(uuid::Uuid::parse_str(s)?.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::string::ToString for Uuid {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! uuid {
|
||||
($s:literal) => {
|
||||
crate::domain::handler::Uuid::try_from($s).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize, sqlx::Type)]
|
||||
#[serde(from = "String")]
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserId(String);
|
||||
|
||||
impl UserId {
|
||||
@@ -34,8 +82,7 @@ impl From<String> for UserId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub user_id: UserId,
|
||||
pub email: String,
|
||||
@@ -44,18 +91,22 @@ pub struct User {
|
||||
pub last_name: String,
|
||||
// pub avatar: ?,
|
||||
pub creation_date: chrono::DateTime<chrono::Utc>,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
use chrono::TimeZone;
|
||||
let epoch = chrono::Utc.timestamp(0, 0);
|
||||
User {
|
||||
user_id: UserId::default(),
|
||||
email: String::new(),
|
||||
display_name: String::new(),
|
||||
first_name: String::new(),
|
||||
last_name: String::new(),
|
||||
creation_date: chrono::Utc.timestamp(0, 0),
|
||||
creation_date: epoch,
|
||||
uuid: Uuid::from_name_and_date("", &epoch),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +115,8 @@ impl Default for User {
|
||||
pub struct Group {
|
||||
pub id: GroupId,
|
||||
pub display_name: String,
|
||||
pub creation_date: chrono::DateTime<chrono::Utc>,
|
||||
pub uuid: Uuid,
|
||||
pub users: Vec<UserId>,
|
||||
}
|
||||
|
||||
@@ -92,6 +145,7 @@ pub enum GroupRequestFilter {
|
||||
Or(Vec<GroupRequestFilter>),
|
||||
Not(Box<GroupRequestFilter>),
|
||||
DisplayName(String),
|
||||
Uuid(Uuid),
|
||||
GroupId(GroupId),
|
||||
// Check if the group contains a user identified by uid.
|
||||
Member(UserId),
|
||||
@@ -128,16 +182,22 @@ pub trait LoginHandler: Clone + Send {
|
||||
async fn bind(&self, request: BindRequest) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct GroupId(pub i32);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct GroupIdAndName(pub GroupId, pub String);
|
||||
pub struct GroupDetails {
|
||||
pub group_id: GroupId,
|
||||
pub display_name: String,
|
||||
pub creation_date: chrono::DateTime<chrono::Utc>,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct UserAndGroups {
|
||||
pub user: User,
|
||||
pub groups: Option<Vec<GroupIdAndName>>,
|
||||
pub groups: Option<Vec<GroupDetails>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -149,7 +209,7 @@ pub trait BackendHandler: Clone + Send {
|
||||
) -> Result<Vec<UserAndGroups>>;
|
||||
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
|
||||
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>;
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
|
||||
async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
|
||||
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
|
||||
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
|
||||
@@ -158,7 +218,7 @@ pub trait BackendHandler: Clone + Send {
|
||||
async fn delete_group(&self, group_id: GroupId) -> Result<()>;
|
||||
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
|
||||
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupIdAndName>>;
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -172,14 +232,14 @@ mockall::mock! {
|
||||
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
|
||||
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
|
||||
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName>;
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
|
||||
async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
|
||||
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
|
||||
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
|
||||
async fn delete_user(&self, user_id: &UserId) -> Result<()>;
|
||||
async fn create_group(&self, group_name: &str) -> Result<GroupId>;
|
||||
async fn delete_group(&self, group_id: GroupId) -> Result<()>;
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupIdAndName>>;
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
|
||||
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
|
||||
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
|
||||
}
|
||||
@@ -188,3 +248,19 @@ mockall::mock! {
|
||||
async fn bind(&self, request: BindRequest) -> Result<()>;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_uuid_time() {
|
||||
use chrono::prelude::*;
|
||||
let user_id = "bob";
|
||||
let date1 = Utc.ymd(2014, 7, 8).and_hms(9, 10, 11);
|
||||
let date2 = Utc.ymd(2014, 7, 8).and_hms(9, 10, 12);
|
||||
assert_ne!(
|
||||
Uuid::from_name_and_date(user_id, &date1),
|
||||
Uuid::from_name_and_date(user_id, &date2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> SimpleExpr {
|
||||
Not(f) => Expr::not(Expr::expr(get_group_filter_expr(*f))),
|
||||
DisplayName(name) => Expr::col((Groups::Table, Groups::DisplayName)).eq(name),
|
||||
GroupId(id) => Expr::col((Groups::Table, Groups::GroupId)).eq(id.0),
|
||||
Uuid(uuid) => Expr::col((Groups::Table, Groups::Uuid)).eq(uuid.to_string()),
|
||||
// WHERE (group_id in (SELECT group_id FROM memberships WHERE user_id = user))
|
||||
Member(user) => Expr::col((Memberships::Table, Memberships::GroupId)).in_subquery(
|
||||
Query::select()
|
||||
@@ -126,7 +127,8 @@ impl BackendHandler for SqlBackendHandler {
|
||||
.column(Users::FirstName)
|
||||
.column(Users::LastName)
|
||||
.column(Users::Avatar)
|
||||
.column(Users::CreationDate)
|
||||
.column((Users::Table, Users::CreationDate))
|
||||
.column((Users::Table, Users::Uuid))
|
||||
.from(Users::Table)
|
||||
.order_by((Users::Table, Users::UserId), Order::Asc)
|
||||
.to_owned();
|
||||
@@ -151,6 +153,14 @@ impl BackendHandler for SqlBackendHandler {
|
||||
Expr::col((Groups::Table, Groups::DisplayName)),
|
||||
Alias::new("group_display_name"),
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((Groups::Table, Groups::CreationDate)),
|
||||
sea_query::Alias::new("group_creation_date"),
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((Groups::Table, Groups::Uuid)),
|
||||
sea_query::Alias::new("group_uuid"),
|
||||
)
|
||||
.order_by(Alias::new("group_display_name"), Order::Asc);
|
||||
}
|
||||
if let Some(filter) = filters {
|
||||
@@ -189,13 +199,14 @@ impl BackendHandler for SqlBackendHandler {
|
||||
user: User::from_row(rows.peek().unwrap()).unwrap(),
|
||||
groups: if get_groups {
|
||||
Some(
|
||||
rows.map(|row| {
|
||||
GroupIdAndName(
|
||||
GroupId(row.get::<i32, _>(&*Groups::GroupId.to_string())),
|
||||
row.get::<String, _>("group_display_name"),
|
||||
)
|
||||
rows.map(|row| GroupDetails {
|
||||
group_id: row.get::<GroupId, _>(&*Groups::GroupId.to_string()),
|
||||
display_name: row.get::<String, _>("group_display_name"),
|
||||
creation_date: row
|
||||
.get::<chrono::DateTime<chrono::Utc>, _>("group_creation_date"),
|
||||
uuid: row.get::<Uuid, _>("group_uuid"),
|
||||
})
|
||||
.filter(|g| !g.1.is_empty())
|
||||
.filter(|g| !g.display_name.is_empty())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
@@ -213,6 +224,8 @@ impl BackendHandler for SqlBackendHandler {
|
||||
let mut query_builder = Query::select()
|
||||
.column((Groups::Table, Groups::GroupId))
|
||||
.column(Groups::DisplayName)
|
||||
.column(Groups::CreationDate)
|
||||
.column(Groups::Uuid)
|
||||
.column(Memberships::UserId)
|
||||
.from(Groups::Table)
|
||||
.left_join(
|
||||
@@ -245,20 +258,17 @@ impl BackendHandler for SqlBackendHandler {
|
||||
let mut groups = Vec::new();
|
||||
// The rows are returned sorted by display_name, equivalent to group_id. We group them by
|
||||
// this key which gives us one element (`rows`) per group.
|
||||
for ((group_id, display_name), rows) in &query_with(query.as_str(), values)
|
||||
for (group_details, rows) in &query_with(&query, values)
|
||||
.fetch_all(&self.sql_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.group_by(|row| {
|
||||
(
|
||||
GroupId(row.get::<i32, _>(&*Groups::GroupId.to_string())),
|
||||
row.get::<String, _>(&*Groups::DisplayName.to_string()),
|
||||
)
|
||||
})
|
||||
.group_by(|row| GroupDetails::from_row(row).unwrap())
|
||||
{
|
||||
groups.push(Group {
|
||||
id: group_id,
|
||||
display_name,
|
||||
id: group_details.group_id,
|
||||
display_name: group_details.display_name,
|
||||
creation_date: group_details.creation_date,
|
||||
uuid: group_details.uuid,
|
||||
users: rows
|
||||
.map(|row| row.get::<UserId, _>(&*Memberships::UserId.to_string()))
|
||||
// If a group has no users, an empty string is returned because of the left
|
||||
@@ -281,6 +291,7 @@ impl BackendHandler for SqlBackendHandler {
|
||||
.column(Users::LastName)
|
||||
.column(Users::Avatar)
|
||||
.column(Users::CreationDate)
|
||||
.column(Users::Uuid)
|
||||
.from(Users::Table)
|
||||
.cond_where(Expr::col(Users::UserId).eq(user_id))
|
||||
.build_sqlx(DbQueryBuilder {});
|
||||
@@ -292,29 +303,31 @@ impl BackendHandler for SqlBackendHandler {
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "debug", ret, err)]
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupIdAndName> {
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
|
||||
debug!(?group_id);
|
||||
let (query, values) = Query::select()
|
||||
.column(Groups::GroupId)
|
||||
.column(Groups::DisplayName)
|
||||
.column(Groups::CreationDate)
|
||||
.column(Groups::Uuid)
|
||||
.from(Groups::Table)
|
||||
.cond_where(Expr::col(Groups::GroupId).eq(group_id))
|
||||
.build_sqlx(DbQueryBuilder {});
|
||||
debug!(%query);
|
||||
|
||||
Ok(
|
||||
query_as_with::<_, GroupIdAndName, _>(query.as_str(), values)
|
||||
.fetch_one(&self.sql_pool)
|
||||
.await?,
|
||||
)
|
||||
Ok(query_as_with::<_, GroupDetails, _>(&query, values)
|
||||
.fetch_one(&self.sql_pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "debug", ret, err)]
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupIdAndName>> {
|
||||
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> {
|
||||
debug!(?user_id);
|
||||
let (query, values) = Query::select()
|
||||
.column((Groups::Table, Groups::GroupId))
|
||||
.column(Groups::DisplayName)
|
||||
.column(Groups::CreationDate)
|
||||
.column(Groups::Uuid)
|
||||
.from(Groups::Table)
|
||||
.inner_join(
|
||||
Memberships::Table,
|
||||
@@ -325,17 +338,10 @@ impl BackendHandler for SqlBackendHandler {
|
||||
.build_sqlx(DbQueryBuilder {});
|
||||
debug!(%query);
|
||||
|
||||
query_with(query.as_str(), values)
|
||||
// Extract the group id from the row.
|
||||
.map(|row: DbRow| {
|
||||
GroupIdAndName(
|
||||
row.get::<GroupId, _>(&*Groups::GroupId.to_string()),
|
||||
row.get::<String, _>(&*Groups::DisplayName.to_string()),
|
||||
)
|
||||
})
|
||||
query_as_with::<_, GroupDetails, _>(&query, values)
|
||||
.fetch(&self.sql_pool)
|
||||
// Collect the vector of rows, each potentially an error.
|
||||
.collect::<Vec<sqlx::Result<GroupIdAndName>>>()
|
||||
.collect::<Vec<sqlx::Result<GroupDetails>>>()
|
||||
.await
|
||||
.into_iter()
|
||||
// Transform it into a single result (the first error if any), and group the group_ids
|
||||
@@ -355,18 +361,23 @@ impl BackendHandler for SqlBackendHandler {
|
||||
Users::FirstName,
|
||||
Users::LastName,
|
||||
Users::CreationDate,
|
||||
Users::Uuid,
|
||||
];
|
||||
let now = chrono::Utc::now();
|
||||
let uuid = Uuid::from_name_and_date(request.user_id.as_str(), &now);
|
||||
let values = vec![
|
||||
request.user_id.into(),
|
||||
request.email.into(),
|
||||
request.display_name.unwrap_or_default().into(),
|
||||
request.first_name.unwrap_or_default().into(),
|
||||
request.last_name.unwrap_or_default().into(),
|
||||
now.naive_utc().into(),
|
||||
uuid.into(),
|
||||
];
|
||||
let (query, values) = Query::insert()
|
||||
.into_table(Users::Table)
|
||||
.columns(columns)
|
||||
.values_panic(vec![
|
||||
request.user_id.into(),
|
||||
request.email.into(),
|
||||
request.display_name.unwrap_or_default().into(),
|
||||
request.first_name.unwrap_or_default().into(),
|
||||
request.last_name.unwrap_or_default().into(),
|
||||
chrono::Utc::now().naive_utc().into(),
|
||||
])
|
||||
.values_panic(values)
|
||||
.build_sqlx(DbQueryBuilder {});
|
||||
debug!(%query);
|
||||
query_with(query.as_str(), values)
|
||||
@@ -445,10 +456,19 @@ impl BackendHandler for SqlBackendHandler {
|
||||
#[instrument(skip_all, level = "debug", ret, err)]
|
||||
async fn create_group(&self, group_name: &str) -> Result<GroupId> {
|
||||
debug!(?group_name);
|
||||
let now = chrono::Utc::now();
|
||||
let (query, values) = Query::insert()
|
||||
.into_table(Groups::Table)
|
||||
.columns(vec![Groups::DisplayName])
|
||||
.values_panic(vec![group_name.into()])
|
||||
.columns(vec![
|
||||
Groups::DisplayName,
|
||||
Groups::CreationDate,
|
||||
Groups::Uuid,
|
||||
])
|
||||
.values_panic(vec![
|
||||
group_name.into(),
|
||||
now.naive_utc().into(),
|
||||
Uuid::from_name_and_date(group_name, &now).into(),
|
||||
])
|
||||
.build_sqlx(DbQueryBuilder {});
|
||||
debug!(%query);
|
||||
query_with(query.as_str(), values)
|
||||
@@ -707,7 +727,7 @@ mod tests {
|
||||
u.groups
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|g| g.0)
|
||||
.map(|g| g.group_id)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
@@ -721,6 +741,29 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
{
|
||||
let users = handler
|
||||
.list_users(None, true)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|u| {
|
||||
(
|
||||
u.user.creation_date,
|
||||
u.groups
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|g| g.creation_date)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (user_date, groups) in users {
|
||||
for group_date in groups {
|
||||
assert_ne!(user_date, group_date);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -738,62 +781,33 @@ mod tests {
|
||||
insert_membership(&handler, group_1, "patrick").await;
|
||||
insert_membership(&handler, group_2, "patrick").await;
|
||||
insert_membership(&handler, group_2, "John").await;
|
||||
let get_group_ids = |filter| async {
|
||||
handler
|
||||
.list_groups(filter)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|g| g.id)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(get_group_ids(None).await, vec![group_1, group_3, group_2]);
|
||||
assert_eq!(
|
||||
handler.list_groups(None).await.unwrap(),
|
||||
vec![
|
||||
Group {
|
||||
id: group_1,
|
||||
display_name: "Best Group".to_string(),
|
||||
users: vec![UserId::new("bob"), UserId::new("patrick")]
|
||||
},
|
||||
Group {
|
||||
id: group_3,
|
||||
display_name: "Empty Group".to_string(),
|
||||
users: vec![]
|
||||
},
|
||||
Group {
|
||||
id: group_2,
|
||||
display_name: "Worst Group".to_string(),
|
||||
users: vec![UserId::new("john"), UserId::new("patrick")]
|
||||
},
|
||||
]
|
||||
get_group_ids(Some(GroupRequestFilter::Or(vec![
|
||||
GroupRequestFilter::DisplayName("Empty Group".to_string()),
|
||||
GroupRequestFilter::Member(UserId::new("bob")),
|
||||
])))
|
||||
.await,
|
||||
vec![group_1, group_3]
|
||||
);
|
||||
assert_eq!(
|
||||
handler
|
||||
.list_groups(Some(GroupRequestFilter::Or(vec![
|
||||
GroupRequestFilter::DisplayName("Empty Group".to_string()),
|
||||
GroupRequestFilter::Member(UserId::new("bob")),
|
||||
])))
|
||||
.await
|
||||
.unwrap(),
|
||||
vec![
|
||||
Group {
|
||||
id: group_1,
|
||||
display_name: "Best Group".to_string(),
|
||||
users: vec![UserId::new("bob"), UserId::new("patrick")]
|
||||
},
|
||||
Group {
|
||||
id: group_3,
|
||||
display_name: "Empty Group".to_string(),
|
||||
users: vec![]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
handler
|
||||
.list_groups(Some(GroupRequestFilter::And(vec![
|
||||
GroupRequestFilter::Not(Box::new(GroupRequestFilter::DisplayName(
|
||||
"value".to_string()
|
||||
))),
|
||||
GroupRequestFilter::GroupId(group_1),
|
||||
])))
|
||||
.await
|
||||
.unwrap(),
|
||||
vec![Group {
|
||||
id: group_1,
|
||||
display_name: "Best Group".to_string(),
|
||||
users: vec![UserId::new("bob"), UserId::new("patrick")]
|
||||
}]
|
||||
get_group_ids(Some(GroupRequestFilter::And(vec![
|
||||
GroupRequestFilter::Not(Box::new(GroupRequestFilter::DisplayName(
|
||||
"value".to_string()
|
||||
))),
|
||||
GroupRequestFilter::GroupId(group_1),
|
||||
])))
|
||||
.await,
|
||||
vec![group_1]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -846,26 +860,20 @@ mod tests {
|
||||
insert_membership(&handler, group_1, "bob").await;
|
||||
insert_membership(&handler, group_1, "patrick").await;
|
||||
insert_membership(&handler, group_2, "patrick").await;
|
||||
let mut bob_groups = HashSet::new();
|
||||
bob_groups.insert(GroupIdAndName(group_1, "Group1".to_string()));
|
||||
let mut patrick_groups = HashSet::new();
|
||||
patrick_groups.insert(GroupIdAndName(group_1, "Group1".to_string()));
|
||||
patrick_groups.insert(GroupIdAndName(group_2, "Group2".to_string()));
|
||||
assert_eq!(
|
||||
handler.get_user_groups(&UserId::new("bob")).await.unwrap(),
|
||||
bob_groups
|
||||
);
|
||||
assert_eq!(
|
||||
handler
|
||||
.get_user_groups(&UserId::new("patrick"))
|
||||
let get_group_ids = |user: &'static str| async {
|
||||
let mut groups = handler
|
||||
.get_user_groups(&UserId::new(user))
|
||||
.await
|
||||
.unwrap(),
|
||||
patrick_groups
|
||||
);
|
||||
assert_eq!(
|
||||
handler.get_user_groups(&UserId::new("John")).await.unwrap(),
|
||||
HashSet::new()
|
||||
);
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|g| g.group_id)
|
||||
.collect::<Vec<_>>();
|
||||
groups.sort_by(|g1, g2| g1.0.cmp(&g2.0));
|
||||
groups
|
||||
};
|
||||
assert_eq!(get_group_ids("bob").await, vec![group_1]);
|
||||
assert_eq!(get_group_ids("patrick").await, vec![group_1, group_2]);
|
||||
assert_eq!(get_group_ids("John").await, vec![]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::handler::{GroupId, UserId};
|
||||
use super::handler::{GroupId, UserId, Uuid};
|
||||
use sea_query::*;
|
||||
use sqlx::Row;
|
||||
use tracing::warn;
|
||||
|
||||
pub type Pool = sqlx::sqlite::SqlitePool;
|
||||
pub type PoolOptions = sqlx::sqlite::SqlitePoolOptions;
|
||||
@@ -12,56 +14,6 @@ impl From<GroupId> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB> sqlx::Type<DB> for GroupId
|
||||
where
|
||||
DB: sqlx::Database,
|
||||
i32: sqlx::Type<DB>,
|
||||
{
|
||||
fn type_info() -> <DB as sqlx::Database>::TypeInfo {
|
||||
<i32 as sqlx::Type<DB>>::type_info()
|
||||
}
|
||||
fn compatible(ty: &<DB as sqlx::Database>::TypeInfo) -> bool {
|
||||
<i32 as sqlx::Type<DB>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, DB> sqlx::Decode<'r, DB> for GroupId
|
||||
where
|
||||
DB: sqlx::Database,
|
||||
i32: sqlx::Decode<'r, DB>,
|
||||
{
|
||||
fn decode(
|
||||
value: <DB as sqlx::database::HasValueRef<'r>>::ValueRef,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Sync + Send + 'static>> {
|
||||
<i32 as sqlx::Decode<'r, DB>>::decode(value).map(GroupId)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB> sqlx::Type<DB> for UserId
|
||||
where
|
||||
DB: sqlx::Database,
|
||||
String: sqlx::Type<DB>,
|
||||
{
|
||||
fn type_info() -> <DB as sqlx::Database>::TypeInfo {
|
||||
<String as sqlx::Type<DB>>::type_info()
|
||||
}
|
||||
fn compatible(ty: &<DB as sqlx::Database>::TypeInfo) -> bool {
|
||||
<String as sqlx::Type<DB>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, DB> sqlx::Decode<'r, DB> for UserId
|
||||
where
|
||||
DB: sqlx::Database,
|
||||
String: sqlx::Decode<'r, DB>,
|
||||
{
|
||||
fn decode(
|
||||
value: <DB as sqlx::database::HasValueRef<'r>>::ValueRef,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Sync + Send + 'static>> {
|
||||
<String as sqlx::Decode<'r, DB>>::decode(value).map(|s| UserId::new(&s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserId> for sea_query::Value {
|
||||
fn from(user_id: UserId) -> Self {
|
||||
user_id.into_string().into()
|
||||
@@ -74,6 +26,18 @@ impl From<&UserId> for sea_query::Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for sea_query::Value {
|
||||
fn from(uuid: Uuid) -> Self {
|
||||
uuid.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Uuid> for sea_query::Value {
|
||||
fn from(uuid: &Uuid) -> Self {
|
||||
uuid.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
pub enum Users {
|
||||
Table,
|
||||
@@ -87,6 +51,7 @@ pub enum Users {
|
||||
PasswordHash,
|
||||
TotpSecret,
|
||||
MfaType,
|
||||
Uuid,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
@@ -94,6 +59,8 @@ pub enum Groups {
|
||||
Table,
|
||||
GroupId,
|
||||
DisplayName,
|
||||
CreationDate,
|
||||
Uuid,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
@@ -103,6 +70,19 @@ pub enum Memberships {
|
||||
GroupId,
|
||||
}
|
||||
|
||||
async fn column_exists(pool: &Pool, table_name: &str, column_name: &str) -> sqlx::Result<bool> {
|
||||
// Sqlite specific
|
||||
let query = format!(
|
||||
"SELECT COUNT(*) AS col_count FROM pragma_table_info('{}') WHERE name = '{}'",
|
||||
table_name, column_name
|
||||
);
|
||||
Ok(sqlx::query(&query)
|
||||
.fetch_one(pool)
|
||||
.await?
|
||||
.get::<i32, _>("col_count")
|
||||
> 0)
|
||||
}
|
||||
|
||||
pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
|
||||
// SQLite needs this pragma to be turned on. Other DB might not understand this, so ignore the
|
||||
// error.
|
||||
@@ -130,6 +110,7 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
|
||||
.col(ColumnDef::new(Users::PasswordHash).binary())
|
||||
.col(ColumnDef::new(Users::TotpSecret).string_len(64))
|
||||
.col(ColumnDef::new(Users::MfaType).string_len(64))
|
||||
.col(ColumnDef::new(Users::Uuid).string_len(36).not_null())
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
@@ -151,11 +132,141 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
|
||||
.unique_key()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Users::CreationDate).date_time().not_null())
|
||||
.col(ColumnDef::new(Users::Uuid).string_len(36).not_null())
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
// If the creation_date column doesn't exist, add it.
|
||||
if !column_exists(
|
||||
pool,
|
||||
&*Groups::Table.to_string(),
|
||||
&*Groups::CreationDate.to_string(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
warn!("`creation_date` column not found in `groups`, creating it");
|
||||
sqlx::query(
|
||||
&Table::alter()
|
||||
.table(Groups::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Groups::CreationDate)
|
||||
.date_time()
|
||||
.not_null()
|
||||
.default(chrono::Utc::now().naive_utc()),
|
||||
)
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If the uuid column doesn't exist, add it.
|
||||
if !column_exists(
|
||||
pool,
|
||||
&*Groups::Table.to_string(),
|
||||
&*Groups::Uuid.to_string(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
warn!("`uuid` column not found in `groups`, creating it");
|
||||
sqlx::query(
|
||||
&Table::alter()
|
||||
.table(Groups::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Groups::Uuid)
|
||||
.string_len(36)
|
||||
.not_null()
|
||||
.default(""),
|
||||
)
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
for row in sqlx::query(
|
||||
&Query::select()
|
||||
.from(Groups::Table)
|
||||
.column(Groups::GroupId)
|
||||
.column(Groups::DisplayName)
|
||||
.column(Groups::CreationDate)
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
{
|
||||
sqlx::query(
|
||||
&Query::update()
|
||||
.table(Groups::Table)
|
||||
.value(
|
||||
Groups::Uuid,
|
||||
Uuid::from_name_and_date(
|
||||
&row.get::<String, _>(&*Groups::DisplayName.to_string()),
|
||||
&row.get::<chrono::DateTime<chrono::Utc>, _>(
|
||||
&*Groups::CreationDate.to_string(),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.and_where(
|
||||
Expr::col(Groups::GroupId)
|
||||
.eq(row.get::<GroupId, _>(&*Groups::GroupId.to_string())),
|
||||
)
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !column_exists(pool, &*Users::Table.to_string(), &*Users::Uuid.to_string()).await? {
|
||||
warn!("`uuid` column not found in `users`, creating it");
|
||||
sqlx::query(
|
||||
&Table::alter()
|
||||
.table(Users::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Users::Uuid)
|
||||
.string_len(36)
|
||||
.not_null()
|
||||
.default(""),
|
||||
)
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
for row in sqlx::query(
|
||||
&Query::select()
|
||||
.from(Users::Table)
|
||||
.column(Users::UserId)
|
||||
.column(Users::CreationDate)
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
{
|
||||
let user_id = row.get::<UserId, _>(&*Users::UserId.to_string());
|
||||
sqlx::query(
|
||||
&Query::update()
|
||||
.table(Users::Table)
|
||||
.value(
|
||||
Users::Uuid,
|
||||
Uuid::from_name_and_date(
|
||||
user_id.as_str(),
|
||||
&row.get::<chrono::DateTime<chrono::Utc>, _>(
|
||||
&*Users::CreationDate.to_string(),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.and_where(Expr::col(Users::UserId).eq(user_id))
|
||||
.to_string(DbQueryBuilder {}),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
&Table::create()
|
||||
.table(Memberships::Table)
|
||||
@@ -196,13 +307,13 @@ mod tests {
|
||||
use chrono::prelude::*;
|
||||
use sqlx::{Column, Row};
|
||||
|
||||
#[actix_rt::test]
|
||||
#[tokio::test]
|
||||
async fn test_init_table() {
|
||||
let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap();
|
||||
init_table(&sql_pool).await.unwrap();
|
||||
sqlx::query(r#"INSERT INTO users
|
||||
(user_id, email, display_name, first_name, last_name, creation_date, password_hash)
|
||||
VALUES ("bôb", "böb@bob.bob", "Bob Bobbersön", "Bob", "Bobberson", "1970-01-01 00:00:00", "bob00")"#).execute(&sql_pool).await.unwrap();
|
||||
(user_id, email, display_name, first_name, last_name, creation_date, password_hash, uuid)
|
||||
VALUES ("bôb", "böb@bob.bob", "Bob Bobbersön", "Bob", "Bobberson", "1970-01-01 00:00:00", "bob00", "abc")"#).execute(&sql_pool).await.unwrap();
|
||||
let row =
|
||||
sqlx::query(r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#)
|
||||
.fetch_one(&sql_pool)
|
||||
@@ -216,10 +327,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
#[tokio::test]
|
||||
async fn test_already_init_table() {
|
||||
let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap();
|
||||
init_table(&sql_pool).await.unwrap();
|
||||
init_table(&sql_pool).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_migrate_tables() {
|
||||
// Test that we add the column creation_date to groups and uuid to users and groups.
|
||||
let sql_pool = PoolOptions::new().connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(r#"CREATE TABLE users ( user_id TEXT , creation_date TEXT);"#)
|
||||
.execute(&sql_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(
|
||||
r#"INSERT INTO users (user_id, creation_date)
|
||||
VALUES ("bôb", "1970-01-01 00:00:00")"#,
|
||||
)
|
||||
.execute(&sql_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(r#"CREATE TABLE groups ( group_id int, display_name TEXT );"#)
|
||||
.execute(&sql_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
init_table(&sql_pool).await.unwrap();
|
||||
sqlx::query(
|
||||
r#"INSERT INTO groups (group_id, display_name, creation_date, uuid)
|
||||
VALUES (3, "test", "1970-01-01 00:00:00", "abc")"#,
|
||||
)
|
||||
.execute(&sql_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
sqlx::query(r#"SELECT uuid FROM users"#)
|
||||
.fetch_all(&sql_pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| row.get::<Uuid, _>("uuid"))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![crate::uuid!("a02eaf13-48a7-30f6-a3d4-040ff7c52b04")]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user