From 07493b83a5aa883b28463016688d3c3391f8e800 Mon Sep 17 00:00:00 2001 From: xeovalyte Date: Fri, 7 Feb 2025 14:24:21 +0100 Subject: [PATCH] Migrated to bitflags --- server/Cargo.lock | 1 + server/Cargo.toml | 1 + server/migrations/001_create_members.sql | 4 +- server/src/auth.rs | 3 + server/src/auth/scopes.rs | 19 ++++++ server/src/database/model/member.rs | 66 +++++++++--------- server/src/model/member.rs | 86 ++++++++++++++++++++++-- server/src/routes/auth.rs | 11 +-- server/src/routes/member/migrate.rs | 29 ++++---- server/src/util.rs | 1 + server/src/util/bitflags.rs | 18 +++++ server/src/util/error.rs | 2 +- 12 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 server/src/auth/scopes.rs create mode 100644 server/src/util/bitflags.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index f8708da..5703e1e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2537,6 +2537,7 @@ dependencies = [ "argon2", "axum", "axum-extra", + "bitflags", "chrono", "csv", "dotenvy", diff --git a/server/Cargo.toml b/server/Cargo.toml index 3045a6d..875ffe4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,6 +16,7 @@ serde = "1.0" dotenvy = "0.15.7" validator = { version = "0.19.0", features = [ "derive" ] } argon2 = "0.5" +bitflags = { version = "2.8", features = [ "serde" ] } # Tertiary crates diff --git a/server/migrations/001_create_members.sql b/server/migrations/001_create_members.sql index 2ed1846..7899046 100644 --- a/server/migrations/001_create_members.sql +++ b/server/migrations/001_create_members.sql @@ -4,7 +4,7 @@ CREATE TABLE "members" ( full_name text NOT NULL, registration_token text NOT NULL UNIQUE, diploma text, - hours text[] NOT NULL, - groups text[] NOT NULL + swim_groups bigint NOT NULL, + groups bigint NOT NULL ); diff --git a/server/src/auth.rs b/server/src/auth.rs index 7aec995..b26803d 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -22,8 +22,11 @@ use rand::prelude::*; use rand_chacha::ChaCha20Rng; use tokio::task; +use crate::database::model::User; + mod bearer; mod error; +mod scopes; #[derive(Debug)] pub struct Permissions<'a>(pub HashSet<&'a str>); diff --git a/server/src/auth/scopes.rs b/server/src/auth/scopes.rs new file mode 100644 index 0000000..df25ba0 --- /dev/null +++ b/server/src/auth/scopes.rs @@ -0,0 +1,19 @@ +use crate::bitflags_serde_impl; +use bitflags::bitflags; +use serde::Deserialize; + +bitflags! { + #[derive(Clone, Copy, Debug)] + pub struct Scopes: u64 { + const USER_READ = 1 << 0; + const USER_WRITE = 1 << 1; + const USER_DELETE = 1 << 2; + + const MEMBER_CREATE = 1 << 3; + const MEMBER_READ = 1 << 4; + const MEMBER_WRITE = 1 << 5; + const MEMBER_DELETE = 1 << 6; + } +} + +bitflags_serde_impl!(Scopes, u64); diff --git a/server/src/database/model/member.rs b/server/src/database/model/member.rs index ae3d397..bfc1355 100644 --- a/server/src/database/model/member.rs +++ b/server/src/database/model/member.rs @@ -2,6 +2,8 @@ use rand::distr::{Alphanumeric, SampleString}; use sqlx::{PgPool, Postgres, QueryBuilder}; use validator::Validate; +use crate::model::member::{Groups, SwimGroups}; + #[derive(Debug, Validate, sqlx::FromRow)] pub struct Member { #[validate(length(equal = 7))] @@ -10,40 +12,22 @@ pub struct Member { pub full_name: String, pub registration_token: Option, pub diploma: Option, - pub hours: Vec, - pub groups: Vec, -} - -pub struct SearchMember { - pub registration_tokens: Option>, + pub swim_groups: SwimGroups, + pub groups: Groups, } impl Member { - pub async fn search( - transaction: &PgPool, - search: SearchMember, + pub async fn get_many_from_registration_tokens( + pool: &PgPool, + registration_tokens: Vec, ) -> Result, sqlx::Error> { - if let None = search.registration_tokens { - return Err(sqlx::Error::RowNotFound); - } - - let mut query = QueryBuilder::new("SELECT * from members WHERE 1=1"); - - if let Some(registration_tokens) = search.registration_tokens { - if registration_tokens.len() == 0 { - return Err(sqlx::Error::RowNotFound); - } - - query.push(" AND registration_token = ANY("); - query.push_bind(registration_tokens); - query.push(")"); - } - - let members: Vec = query.build_query_as().fetch_all(transaction).await?; - - if members.len() == 0 { - return Err(sqlx::Error::RowNotFound); - } + let members = sqlx::query_as!( + Member, + "SELECT * FROM members WHERE registration_token = ANY($1);", + ®istration_tokens + ) + .fetch_all(pool) + .await?; Ok(members) } @@ -69,7 +53,7 @@ impl Member { } let mut query_builder = QueryBuilder::new( - "INSERT INTO members(member_id, first_name, full_name, registration_token, diploma, hours, groups) " + "INSERT INTO members(member_id, first_name, full_name, registration_token, diploma, swim_groups, groups) " ); query_builder.push_values(members.into_iter(), |mut b, member| { @@ -80,8 +64,8 @@ impl Member { b.push_bind(member.full_name); b.push_bind(registration_token); b.push_bind(member.diploma); - b.push_bind(member.hours); - b.push_bind(member.groups); + b.push_bind(member.swim_groups.bits() as i64); + b.push_bind(member.groups.bits() as i64); }); let query = query_builder.build(); @@ -99,7 +83,21 @@ impl Member { } for member in members { - sqlx::query!("UPDATE ONLY members SET first_name = $1, full_name = $2, diploma = $3, hours = $4, groups = $5 WHERE member_id = $6", member.first_name, member.full_name, member.diploma, &member.hours, &member.groups, member.member_id).execute(&mut **transaction).await?; + sqlx::query!( + " + UPDATE ONLY members + SET first_name = $1, full_name = $2, diploma = $3, swim_groups = $4, groups = $5 + WHERE member_id = $6 + ", + member.first_name, + member.full_name, + member.diploma, + member.swim_groups.bits() as i64, + member.groups.bits() as i64, + member.member_id + ) + .execute(&mut **transaction) + .await?; } Ok(()) diff --git a/server/src/model/member.rs b/server/src/model/member.rs index 3d398a1..f039d52 100644 --- a/server/src/model/member.rs +++ b/server/src/model/member.rs @@ -1,17 +1,91 @@ -#[derive(Debug, Clone, serde::Serialize)] +use bitflags::bitflags; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize)] pub struct Name { pub first: String, pub full: String, } -#[derive(Debug, Clone, serde::Serialize)] +#[derive(Debug, Clone, Serialize)] pub struct Member { pub id: String, pub name: Name, pub registration_token: Option, pub diploma: Option, - pub hours: Vec, - pub groups: Vec, + pub swim_groups: SwimGroups, + pub groups: Groups, +} + +bitflags! { + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] + pub struct Groups: u64 { + const NONE = 1 << 0; + const KADER = 1 << 1; + const ZWEMZAKEN = 1 << 2; + const WEDSTRIJDEN = 1 << 3; + } + + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] + pub struct SwimGroups: u64 { + const NONE = 1 << 0; + + const A1 = 1 << 1; + const A2 = 1 << 2; + const A3 = 1 << 3; + const A4 = 1 << 4; + const A5 = 1 << 5; + const A6 = 1 << 6; + + const B1 = 1 << 7; + const B2 = 1 << 8; + const B3 = 1 << 9; + const B4 = 1 << 10; + const B5 = 1 << 11; + const B6 = 1 << 12; + + const C1 = 1 << 13; + const C2 = 1 << 14; + const C3 = 1 << 15; + const C4 = 1 << 16; + const C5 = 1 << 17; + const C6 = 1 << 18; + + const D1 = 1 << 19; + const D2 = 1 << 20; + const D3 = 1 << 21; + const D4 = 1 << 22; + const D5 = 1 << 23; + const D6 = 1 << 24; + + const E1 = 1 << 25; + const E2 = 1 << 26; + const E3 = 1 << 27; + const E4 = 1 << 28; + const E5 = 1 << 29; + const E6 = 1 << 30; + + const Z1 = 1 << 31; + const Z2 = 1 << 32; + const Z3 = 1 << 33; + const Z4 = 1 << 34; + const Z5 = 1 << 35; + const Z6 = 1 << 36; + + const WEDSTRIJD = 1 << 37; + } +} + +impl From for SwimGroups { + fn from(value: i64) -> Self { + Self::from_bits(value as u64).unwrap_or(SwimGroups::NONE) + } +} + +impl From for Groups { + fn from(value: i64) -> Self { + Self::from_bits(value as u64).unwrap_or(Groups::NONE) + } } use crate::database::model::Member as DbMember; @@ -25,7 +99,7 @@ impl From for Member { }, registration_token: value.registration_token, diploma: value.diploma, - hours: value.hours, + swim_groups: value.swim_groups, groups: value.groups, } } @@ -39,7 +113,7 @@ impl From for DbMember { full_name: value.name.full, registration_token: None, diploma: value.diploma, - hours: value.hours, + swim_groups: value.swim_groups, groups: value.groups, } } diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs index 816c34b..a0a4919 100644 --- a/server/src/routes/auth.rs +++ b/server/src/routes/auth.rs @@ -1,7 +1,6 @@ use axum::{extract::State, routing::post, Json, Router}; use crate::auth::verify_password_hash; -use crate::database::model::member::SearchMember; use crate::database::model::Member as DbMember; use crate::database::model::Session as DbSession; use crate::database::model::User as DbUser; @@ -55,13 +54,9 @@ pub async fn register<'a>( Json(auth_request): Json, ) -> Result { // Get all the members to link with the user - let members = DbMember::search( - &state.pool, - SearchMember { - registration_tokens: Some(auth_request.registration_tokens), - }, - ) - .await?; + let members = + DbMember::get_many_from_registration_tokens(&state.pool, auth_request.registration_tokens) + .await?; let member_ids: Vec = members.into_iter().map(|m| m.member_id).collect(); diff --git a/server/src/routes/member/migrate.rs b/server/src/routes/member/migrate.rs index 277cfd8..f6ad732 100644 --- a/server/src/routes/member/migrate.rs +++ b/server/src/routes/member/migrate.rs @@ -10,7 +10,10 @@ use sqlx::PgPool; use crate::{ auth::{AuthError, Permissions}, database::model::Member as DbMember, - model::{member::Name, Member}, + model::{ + member::{Groups, Name, SwimGroups}, + Member, + }, util::convert_vec, AppState, }; @@ -116,7 +119,7 @@ struct Row { #[serde(rename = "E-mail")] email: String, #[serde(rename = "Verenigingssporten")] - hours: String, + swim_groups: String, #[serde(rename = "Diploma dropdown 1")] diploma: Option, } @@ -161,22 +164,22 @@ impl Row { members } - fn hours_parsed(&self) -> Vec { - let mut hours: Vec = Vec::new(); + fn swim_groups_parsed(&self) -> SwimGroups { + let mut swim_groups: Vec = Vec::new(); - let group_parts: Vec<&str> = self.hours.split(", ").collect(); + let group_parts: Vec<&str> = self.swim_groups.split(", ").collect(); for group in group_parts { let hour_parts: Vec<&str> = group.split(" - ").collect(); - for part in hour_parts { - if &*part != "Groep" { - hours.push(part.to_string()); - } + if let Some(group) = hour_parts.get(1) { + swim_groups.push(group.to_uppercase()) } } - hours.into_iter().unique().collect() + let swim_groups_string = swim_groups.join("|"); + + bitflags::parser::from_str(&swim_groups_string).unwrap_or(SwimGroups::empty()) } } @@ -198,8 +201,8 @@ impl Into for Row { name, registration_token: None, diploma: self.diploma.clone(), - hours: self.hours_parsed(), - groups: Vec::new(), + swim_groups: self.swim_groups_parsed(), + groups: Groups::empty(), } } } @@ -261,7 +264,7 @@ fn generate_diff(members_new: Vec, members_old: Vec) -> MembersD name: new_member.name.clone(), registration_token: old_member.registration_token, diploma: new_member.diploma.clone(), - hours: new_member.hours.clone(), + swim_groups: new_member.swim_groups.clone(), groups: old_member.groups, }) } else { diff --git a/server/src/util.rs b/server/src/util.rs index fc84182..d277126 100644 --- a/server/src/util.rs +++ b/server/src/util.rs @@ -1,3 +1,4 @@ +mod bitflags; pub mod error; mod helpers; diff --git a/server/src/util/bitflags.rs b/server/src/util/bitflags.rs new file mode 100644 index 0000000..08647ab --- /dev/null +++ b/server/src/util/bitflags.rs @@ -0,0 +1,18 @@ +#[macro_export] +macro_rules! bitflags_serde_impl { + ($type:ident, $int_type:ident) => { + impl serde::Serialize for $type { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_i64(self.bits() as i64) + } + } + + impl<'de> serde::Deserialize<'de> for $type { + fn deserialize>(deserializer: D) -> Result { + let v: i64 = Deserialize::deserialize(deserializer)?; + + Ok($type::from_bits_truncate(v as $int_type)) + } + } + }; +} diff --git a/server/src/util/error.rs b/server/src/util/error.rs index 357c61e..d164baa 100644 --- a/server/src/util/error.rs +++ b/server/src/util/error.rs @@ -39,7 +39,7 @@ impl IntoResponse for Error { fn into_response(self) -> Response { let (status_code, code) = match self { Self::Sqlx(ref err_kind) => match err_kind { - sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "DATABSE_ROW_NOT_FOUND"), + sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "DATABASE_ROW_NOT_FOUND"), _ => (StatusCode::INTERNAL_SERVER_ERROR, "DATABASE_ERROR"), },