Migrated to bitflags

This commit is contained in:
xeovalyte 2025-02-07 14:24:21 +01:00
parent cf379a1288
commit 07493b83a5
Signed by: xeovalyte
SSH Key Fingerprint: SHA256:GWI1hq+MNKR2UOcvk7n9tekASXT8vyazK7vDF9Xyciw
12 changed files with 177 additions and 64 deletions

1
server/Cargo.lock generated
View File

@ -2537,6 +2537,7 @@ dependencies = [
"argon2",
"axum",
"axum-extra",
"bitflags",
"chrono",
"csv",
"dotenvy",

View File

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

View File

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

View File

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

19
server/src/auth/scopes.rs Normal file
View File

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

View File

@ -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<String>,
pub diploma: Option<String>,
pub hours: Vec<String>,
pub groups: Vec<String>,
}
pub struct SearchMember {
pub registration_tokens: Option<Vec<String>>,
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<String>,
) -> Result<Vec<Self>, 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<Self> = 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);",
&registration_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(())

View File

@ -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<String>,
pub diploma: Option<String>,
pub hours: Vec<String>,
pub groups: Vec<String>,
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<i64> for SwimGroups {
fn from(value: i64) -> Self {
Self::from_bits(value as u64).unwrap_or(SwimGroups::NONE)
}
}
impl From<i64> 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<DbMember> 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<Member> for DbMember {
full_name: value.name.full,
registration_token: None,
diploma: value.diploma,
hours: value.hours,
swim_groups: value.swim_groups,
groups: value.groups,
}
}

View File

@ -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,12 +54,8 @@ pub async fn register<'a>(
Json(auth_request): Json<RegisterRequest>,
) -> Result<String, crate::Error> {
// Get all the members to link with the user
let members = DbMember::search(
&state.pool,
SearchMember {
registration_tokens: Some(auth_request.registration_tokens),
},
)
let members =
DbMember::get_many_from_registration_tokens(&state.pool, auth_request.registration_tokens)
.await?;
let member_ids: Vec<String> = members.into_iter().map(|m| m.member_id).collect();

View File

@ -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<String>,
}
@ -161,22 +164,22 @@ impl Row {
members
}
fn hours_parsed(&self) -> Vec<String> {
let mut hours: Vec<String> = Vec::new();
fn swim_groups_parsed(&self) -> SwimGroups {
let mut swim_groups: Vec<String> = 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<Member> 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<Member>, members_old: Vec<Member>) -> 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 {

View File

@ -1,3 +1,4 @@
mod bitflags;
pub mod error;
mod helpers;

View File

@ -0,0 +1,18 @@
#[macro_export]
macro_rules! bitflags_serde_impl {
($type:ident, $int_type:ident) => {
impl serde::Serialize for $type {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_i64(self.bits() as i64)
}
}
impl<'de> serde::Deserialize<'de> for $type {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v: i64 = Deserialize::deserialize(deserializer)?;
Ok($type::from_bits_truncate(v as $int_type))
}
}
};
}

View File

@ -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"),
},