diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index 920726ef8..5221f4867 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -21,6 +21,7 @@ pub struct User { pub locked_at: Option>, pub deactivated_at: Option>, pub can_request_admin: bool, + pub is_guest: bool, } impl User { @@ -43,6 +44,7 @@ impl User { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: false, }] } } diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index 62065dfe4..2f6648402 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -52,6 +52,9 @@ pub struct User { /// Whether the user can request admin privileges. admin: bool, + + /// Whether the user was a guest before migrating to MAS, + legacy_guest: bool, } impl User { @@ -65,6 +68,7 @@ impl User { locked_at: None, deactivated_at: None, admin: false, + legacy_guest: false, }, Self { id: Ulid::from_bytes([0x02; 16]), @@ -73,6 +77,7 @@ impl User { locked_at: None, deactivated_at: None, admin: true, + legacy_guest: false, }, Self { id: Ulid::from_bytes([0x03; 16]), @@ -81,6 +86,7 @@ impl User { locked_at: Some(DateTime::default()), deactivated_at: None, admin: false, + legacy_guest: true, }, ] } @@ -95,6 +101,7 @@ impl From for User { locked_at: user.locked_at, deactivated_at: user.deactivated_at, admin: user.can_request_admin, + legacy_guest: user.is_guest, } } } diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index 8a135fadc..b963b73d5 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -209,7 +209,8 @@ mod tests { "created_at": "2022-01-16T14:40:00Z", "locked_at": null, "deactivated_at": "2022-01-16T14:40:00Z", - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" @@ -289,7 +290,8 @@ mod tests { "created_at": "2022-01-16T14:40:00Z", "locked_at": "2022-01-16T14:40:00Z", "deactivated_at": "2022-01-16T14:41:00Z", - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" diff --git a/crates/handlers/src/admin/v1/users/list.rs b/crates/handlers/src/admin/v1/users/list.rs index 021e39f37..17b652c82 100644 --- a/crates/handlers/src/admin/v1/users/list.rs +++ b/crates/handlers/src/admin/v1/users/list.rs @@ -54,6 +54,10 @@ pub struct FilterParams { #[serde(rename = "filter[admin]")] admin: Option, + /// Retrieve users with (or without) the `legacy_guest` flag set + #[serde(rename = "filter[legacy-guest]")] + legacy_guest: Option, + /// Retrieve the items with the given status /// /// Defaults to retrieve all users, including locked ones. @@ -75,6 +79,10 @@ impl std::fmt::Display for FilterParams { write!(f, "{sep}filter[admin]={admin}")?; sep = '&'; } + if let Some(legacy_guest) = self.legacy_guest { + write!(f, "{sep}filter[legacy-guest]={legacy_guest}")?; + sep = '&'; + } if let Some(status) = self.status { write!(f, "{sep}filter[status]={status}")?; sep = '&'; @@ -143,6 +151,12 @@ pub async fn handler( None => filter, }; + let filter = match params.legacy_guest { + Some(true) => filter.guest_only(), + Some(false) => filter.non_guest_only(), + None => filter, + }; + let filter = match params.status { Some(UserStatus::Active) => filter.active_only(), Some(UserStatus::Locked) => filter.locked_only(), diff --git a/crates/handlers/src/rate_limit.rs b/crates/handlers/src/rate_limit.rs index 8b2485941..0471e6351 100644 --- a/crates/handlers/src/rate_limit.rs +++ b/crates/handlers/src/rate_limit.rs @@ -328,6 +328,7 @@ mod tests { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: true, }; let bob = User { @@ -338,6 +339,7 @@ mod tests { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: true, }; // Three times the same IP address should be allowed diff --git a/crates/storage-pg/.sqlx/query-d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716.json b/crates/storage-pg/.sqlx/query-23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08.json similarity index 74% rename from crates/storage-pg/.sqlx/query-d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716.json rename to crates/storage-pg/.sqlx/query-23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08.json index 171a83623..28391d844 100644 --- a/crates/storage-pg/.sqlx/query-d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716.json +++ b/crates/storage-pg/.sqlx/query-23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE LOWER(username) = LOWER($1)\n ", + "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n , is_guest\n FROM users\n WHERE LOWER(username) = LOWER($1)\n ", "describe": { "columns": [ { @@ -32,6 +32,11 @@ "ordinal": 5, "name": "can_request_admin", "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "is_guest", + "type_info": "Bool" } ], "parameters": { @@ -45,8 +50,9 @@ false, true, true, + false, false ] }, - "hash": "d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716" + "hash": "23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08" } diff --git a/crates/storage-pg/.sqlx/query-cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d.json b/crates/storage-pg/.sqlx/query-4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce.json similarity index 75% rename from crates/storage-pg/.sqlx/query-cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d.json rename to crates/storage-pg/.sqlx/query-4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce.json index 6603fa37d..6c8cdbe88 100644 --- a/crates/storage-pg/.sqlx/query-cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d.json +++ b/crates/storage-pg/.sqlx/query-4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE user_id = $1\n ", + "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n , is_guest\n FROM users\n WHERE user_id = $1\n ", "describe": { "columns": [ { @@ -32,6 +32,11 @@ "ordinal": 5, "name": "can_request_admin", "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "is_guest", + "type_info": "Bool" } ], "parameters": { @@ -45,8 +50,9 @@ false, true, true, + false, false ] }, - "hash": "cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d" + "hash": "4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce" } diff --git a/crates/storage-pg/.sqlx/query-f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8.json b/crates/storage-pg/.sqlx/query-572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40.json similarity index 84% rename from crates/storage-pg/.sqlx/query-f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8.json rename to crates/storage-pg/.sqlx/query-572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40.json index 1ddb0acc8..155277181 100644 --- a/crates/storage-pg/.sqlx/query-f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8.json +++ b/crates/storage-pg/.sqlx/query-572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , s.last_active_at AS \"user_session_last_active_at\"\n , s.last_active_ip AS \"user_session_last_active_ip: IpAddr\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n , u.deactivated_at AS \"user_deactivated_at\"\n , u.can_request_admin AS \"user_can_request_admin\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", + "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , s.last_active_at AS \"user_session_last_active_at\"\n , s.last_active_ip AS \"user_session_last_active_ip: IpAddr\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n , u.deactivated_at AS \"user_deactivated_at\"\n , u.can_request_admin AS \"user_can_request_admin\"\n , u.is_guest AS \"user_is_guest\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", "describe": { "columns": [ { @@ -62,6 +62,11 @@ "ordinal": 11, "name": "user_can_request_admin", "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "user_is_guest", + "type_info": "Bool" } ], "parameters": { @@ -81,8 +86,9 @@ false, true, true, + false, false ] }, - "hash": "f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8" + "hash": "572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40" } diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index e6c03acc4..a861f59c7 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -39,6 +39,7 @@ pub enum Users { LockedAt, DeactivatedAt, CanRequestAdmin, + IsGuest, } #[derive(sea_query::Iden)] diff --git a/crates/storage-pg/src/user/mod.rs b/crates/storage-pg/src/user/mod.rs index 8af61b61c..1af09a455 100644 --- a/crates/storage-pg/src/user/mod.rs +++ b/crates/storage-pg/src/user/mod.rs @@ -73,6 +73,7 @@ mod priv_ { pub(super) locked_at: Option>, pub(super) deactivated_at: Option>, pub(super) can_request_admin: bool, + pub(super) is_guest: bool, } } @@ -89,6 +90,7 @@ impl From for User { locked_at: value.locked_at, deactivated_at: value.deactivated_at, can_request_admin: value.can_request_admin, + is_guest: value.is_guest, } } } @@ -114,6 +116,10 @@ impl Filter for UserFilter<'_> { .add_option(self.can_request_admin().map(|can_request_admin| { Expr::col((Users::Table, Users::CanRequestAdmin)).eq(can_request_admin) })) + .add_option( + self.is_guest() + .map(|is_guest| Expr::col((Users::Table, Users::IsGuest)).eq(is_guest)), + ) } } @@ -140,6 +146,7 @@ impl UserRepository for PgUserRepository<'_> { , locked_at , deactivated_at , can_request_admin + , is_guest FROM users WHERE user_id = $1 "#, @@ -176,6 +183,7 @@ impl UserRepository for PgUserRepository<'_> { , locked_at , deactivated_at , can_request_admin + , is_guest FROM users WHERE LOWER(username) = LOWER($1) "#, @@ -249,6 +257,7 @@ impl UserRepository for PgUserRepository<'_> { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: false, }) } @@ -487,6 +496,10 @@ impl UserRepository for PgUserRepository<'_> { Expr::col((Users::Table, Users::CanRequestAdmin)), UserLookupIden::CanRequestAdmin, ) + .expr_as( + Expr::col((Users::Table, Users::IsGuest)), + UserLookupIden::IsGuest, + ) .from(Users::Table) .apply_filter(filter) .generate_pagination((Users::Table, Users::UserId), pagination) diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index a8c5ab458..54645b3cb 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -61,6 +61,7 @@ struct SessionLookup { user_locked_at: Option>, user_deactivated_at: Option>, user_can_request_admin: bool, + user_is_guest: bool, } impl TryFrom for BrowserSession { @@ -76,6 +77,7 @@ impl TryFrom for BrowserSession { locked_at: value.user_locked_at, deactivated_at: value.user_deactivated_at, can_request_admin: value.user_can_request_admin, + is_guest: value.user_is_guest, }; Ok(BrowserSession { @@ -201,6 +203,7 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> { , u.locked_at AS "user_locked_at" , u.deactivated_at AS "user_deactivated_at" , u.can_request_admin AS "user_can_request_admin" + , u.is_guest AS "user_is_guest" FROM user_sessions s INNER JOIN users u USING (user_id) @@ -391,6 +394,10 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> { Expr::col((Users::Table, Users::CanRequestAdmin)), SessionLookupIden::UserCanRequestAdmin, ) + .expr_as( + Expr::col((Users::Table, Users::IsGuest)), + SessionLookupIden::UserIsGuest, + ) .from(UserSessions::Table) .inner_join( Users::Table, diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index 0294d83b4..8a5949e81 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -75,6 +75,7 @@ impl UserState { pub struct UserFilter<'a> { state: Option, can_request_admin: Option, + is_guest: Option, _phantom: std::marker::PhantomData<&'a ()>, } @@ -120,6 +121,20 @@ impl UserFilter<'_> { self } + /// Filter for guest users + #[must_use] + pub fn guest_only(mut self) -> Self { + self.is_guest = Some(true); + self + } + + /// Filter for non-guest users + #[must_use] + pub fn non_guest_only(mut self) -> Self { + self.is_guest = Some(false); + self + } + /// Get the state filter /// /// Returns [`None`] if no state filter was set @@ -135,6 +150,14 @@ impl UserFilter<'_> { pub fn can_request_admin(&self) -> Option { self.can_request_admin } + + /// Get the is guest filter + /// + /// Returns [`None`] if no is guest filter was set + #[must_use] + pub fn is_guest(&self) -> Option { + self.is_guest + } } /// A [`UserRepository`] helps interacting with [`User`] saved in the storage diff --git a/docs/api/spec.json b/docs/api/spec.json index ab8e7f2ec..0e8acdded 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -904,6 +904,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "filter[legacy-guest]", + "description": "Retrieve users with (or without) the `legacy_guest` flag set", + "schema": { + "description": "Retrieve users with (or without) the `legacy_guest` flag set", + "type": "boolean", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[status]", @@ -937,7 +948,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -951,7 +963,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": true + "admin": true, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/02081040G2081040G2081040G2" @@ -965,7 +978,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": "1970-01-01T00:00:00Z", "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": true }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" @@ -1017,7 +1031,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1103,7 +1118,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1260,7 +1276,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1340,7 +1357,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": true + "admin": true, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/02081040G2081040G2081040G2" @@ -1419,7 +1437,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": "1970-01-01T00:00:00Z", "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": true }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" @@ -1489,7 +1508,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1559,7 +1579,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": "1970-01-01T00:00:00Z", "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": true }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" @@ -1629,7 +1650,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -3866,6 +3888,11 @@ "type": "boolean", "nullable": true }, + "filter[legacy-guest]": { + "description": "Retrieve users with (or without) the `legacy_guest` flag set", + "type": "boolean", + "nullable": true + }, "filter[status]": { "description": "Retrieve the items with the given status\n\nDefaults to retrieve all users, including locked ones.\n\n* `active`: Only retrieve active users\n\n* `locked`: Only retrieve locked users (includes deactivated users)\n\n* `deactivated`: Only retrieve deactivated users", "$ref": "#/components/schemas/UserStatus", @@ -3941,6 +3968,7 @@ "required": [ "admin", "created_at", + "legacy_guest", "username" ], "properties": { @@ -3968,6 +3996,10 @@ "admin": { "description": "Whether the user can request admin privileges.", "type": "boolean" + }, + "legacy_guest": { + "description": "Whether the user was a guest before migrating to MAS,", + "type": "boolean" } } },