Compare commits

...

2 Commits

Author SHA1 Message Date
07493b83a5
Migrated to bitflags 2025-02-07 14:24:21 +01:00
cf379a1288
Beginning of permissions system 2025-02-07 12:04:51 +01:00
13 changed files with 267 additions and 67 deletions

65
server/Cargo.lock generated
View File

@ -138,6 +138,7 @@ dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"headers",
"http",
@ -284,6 +285,17 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -406,6 +418,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -1132,6 +1153,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
@ -1277,6 +1304,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -1926,6 +1959,37 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
@ -2473,6 +2537,7 @@ dependencies = [
"argon2",
"axum",
"axum-extra",
"bitflags",
"chrono",
"csv",
"dotenvy",

View File

@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
# Primary crates
axum = { version = "0.8", features = [ "macros", "json" ] }
axum-extra = { version = "0.10.0", features = [ "typed-header" ] }
axum-extra = { version = "0.10.0", features = [ "typed-header", "cookie" ] }
tokio = { version = "1.43", features = [ "rt-multi-thread", "macros" ] }
sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres", "uuid", "chrono" ] }
@ -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

@ -4,8 +4,13 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2, PasswordHash, PasswordVerifier,
};
use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt};
use axum::{
extract::FromRequestParts,
http::{request::Parts, StatusCode},
RequestPartsExt,
};
use axum_extra::{
extract::cookie::{Cookie, CookieJar},
headers::{authorization::Bearer, Authorization},
typed_header::TypedHeaderRejectionReason,
TypedHeader,
@ -17,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>);
@ -51,6 +59,16 @@ where
},
};
match parts.extract::<CookieJar>().await {
Ok(jar) => {
if let Some(session_token) = jar.get("session_token") {
// TODO: Implement function to retrieve user permissions
tracing::info!("{session_token:?}")
}
}
Err(_) => (),
}
Err(AuthError::Unauthorized.into())
}
}

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,5 +1,5 @@
use chrono::{DateTime, Utc};
use sqlx::Postgres;
use sqlx::{PgPool, Postgres};
pub struct Session {
pub session_id: uuid::Uuid,
@ -34,4 +34,12 @@ impl Session {
Ok(())
}
pub async fn from_token(transaction: &PgPool, token: &str) -> Result<Self, sqlx::Error> {
let session = sqlx::query_as!(Self, "SELECT * FROM sessions WHERE token = $1;", token)
.fetch_one(transaction)
.await?;
Ok(session)
}
}

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,13 +54,9 @@ 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),
},
)
.await?;
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"),
},