Added admin field to prevent a normal user fetching admin data

This commit is contained in:
xeovalyte 2024-10-09 11:11:05 +02:00
parent e7edea5c85
commit 1aa7a0565d
Signed by: xeovalyte
SSH Key Fingerprint: SHA256:kSQDrQDmKzljJzfGYcd3m9RqHi4h8rSwkZ3sQ9kBURo
10 changed files with 94 additions and 28 deletions

View File

@ -2,7 +2,7 @@ pub mod migration;
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::util::model::member::Member; use crate::util::model::{member::Member, session::Session};
pub fn Members() -> Element { pub fn Members() -> Element {
let members = use_resource(fetch_members); let members = use_resource(fetch_members);
@ -58,13 +58,27 @@ fn MemberRow(props: MemberRowProps) -> Element {
class: "hover hover:cursor-pointer", class: "hover hover:cursor-pointer",
th { "{props.member.id}" } th { "{props.member.id}" }
td { "{props.member.name.full}" } td { "{props.member.name.full}" }
td { "{registration_token} {props.member.hours:?}" } td { "{registration_token}" }
} }
} }
} }
#[server] #[server]
async fn fetch_members() -> Result<Vec<Member>, ServerFnError> { async fn fetch_members() -> Result<Vec<Member>, ServerFnError> {
let user = Session::fetch_current_user().await?;
if !user.admin {
return Err(crate::Error::NoPermissions.into());
}
let members = Member::fetch_all().await?; let members = Member::fetch_all().await?;
Ok(members) Ok(members)
} }
fn MemberModal(props: MemberRowProps) -> Element {
rsx! {
div {
"BOe"
}
}
}

View File

@ -1,4 +1,7 @@
use crate::util::model::member::{Member, MembersMigration}; use crate::util::model::{
member::{Member, MembersMigration},
session::Session,
};
use dioxus::prelude::{dioxus_elements::FileEngine, *}; use dioxus::prelude::{dioxus_elements::FileEngine, *};
use std::sync::Arc; use std::sync::Arc;
@ -209,6 +212,12 @@ fn MembersTable(members: Vec<Member>) -> Element {
#[server] #[server]
async fn upload_members_list(input: String) -> Result<(u16, MembersMigration), ServerFnError> { async fn upload_members_list(input: String) -> Result<(u16, MembersMigration), ServerFnError> {
let user = Session::fetch_current_user().await?;
if !user.admin {
return Err(crate::Error::NoPermissions.into());
}
match MembersMigration::migrate_proposal(input).await { match MembersMigration::migrate_proposal(input).await {
Ok(r) => Ok(r), Ok(r) => Ok(r),
Err(err) => Err(ServerFnError::new(err.to_string())), Err(err) => Err(ServerFnError::new(err.to_string())),
@ -217,6 +226,12 @@ async fn upload_members_list(input: String) -> Result<(u16, MembersMigration), S
#[server] #[server]
async fn migration_response(correct: bool, id: u16) -> Result<(), ServerFnError> { async fn migration_response(correct: bool, id: u16) -> Result<(), ServerFnError> {
let user = Session::fetch_current_user().await?;
if !user.admin {
return Err(crate::Error::NoPermissions.into());
}
if correct { if correct {
match MembersMigration::migrate(id).await { match MembersMigration::migrate(id).await {
Err(err) => Err(ServerFnError::new(err.to_string())), Err(err) => Err(ServerFnError::new(err.to_string())),

View File

@ -1,6 +1,6 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::util::model::user::User; use crate::util::model::{session::Session, user::User};
pub fn Users() -> Element { pub fn Users() -> Element {
let users = use_resource(fetch_users); let users = use_resource(fetch_users);
@ -56,6 +56,13 @@ fn UserRow(props: UserRowProps) -> Element {
#[server] #[server]
async fn fetch_users() -> Result<Vec<User>, ServerFnError> { async fn fetch_users() -> Result<Vec<User>, ServerFnError> {
let user = Session::fetch_current_user().await?;
if !user.admin {
return Err(crate::Error::NoPermissions.into());
}
let users = User::fetch_all().await?; let users = User::fetch_all().await?;
Ok(users) Ok(users)
} }

View File

@ -51,3 +51,17 @@ async fn get_user_from_cookie() -> Result<User, ServerFnError> {
Ok(user) Ok(user)
} }
#[component]
pub fn AdminLayout() -> Element {
let user_state = use_context::<Signal<Option<User>>>();
let user = user_state().unwrap();
rsx! {
if user.admin {
Outlet::<Route> {}
} else {
div { "No Permissions" }
}
}
}

View File

@ -2,9 +2,13 @@ use crate::Route;
use dioxus::prelude::*; use dioxus::prelude::*;
use super::icons; use super::icons;
use crate::util::model::user::User;
#[component] #[component]
pub fn Topbar() -> Element { pub fn Topbar() -> Element {
let user_state = use_context::<Signal<Option<User>>>();
let user = user_state().unwrap();
rsx! { rsx! {
div { div {
class: "navbar bg-base-200 px-3", class: "navbar bg-base-200 px-3",
@ -14,10 +18,12 @@ pub fn Topbar() -> Element {
} }
div { div {
class: "flex-none gap-2", class: "flex-none gap-2",
Link { if user.admin {
class: "btn btn-ghost font-normal", Link {
to: Route::Admin {}, class: "btn btn-ghost font-normal",
"Administration" to: Route::Admin {},
"Administration"
}
} }
Link { Link {
class: "btn btn-square btn-ghost", class: "btn btn-square btn-ghost",

View File

@ -20,4 +20,7 @@ pub enum Error {
#[error("Could not get cookie jar")] #[error("Could not get cookie jar")]
CookieJar(String), CookieJar(String),
#[error("No permissions")]
NoPermissions,
} }

View File

@ -17,29 +17,33 @@ use components::admin::users::Users;
use components::admin::Admin; use components::admin::Admin;
use components::agenda::Agenda; use components::agenda::Agenda;
use components::home::Home; use components::home::Home;
use components::layout::AdminLayout;
use components::layout::Global; use components::layout::Global;
use components::news::News; use components::news::News;
use components::settings::Settings; use components::settings::Settings;
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[rustfmt::skip]
pub enum Route { pub enum Route {
#[layout(Global)] #[layout(Global)]
#[route("/")] #[route("/")]
Home {}, Home {},
#[route("/agenda")] #[route("/agenda")]
Agenda {}, Agenda {},
#[route("/news")] #[route("/news")]
News {}, News {},
#[route("/settings")] #[route("/settings")]
Settings {}, Settings {},
#[route("/admin")]
Admin {}, #[layout(AdminLayout)]
#[route("/admin/users")] #[route("/admin")]
Users {}, Admin {},
#[route("/admin/members")] #[route("/admin/users")]
Members {}, Users {},
#[route("/admin/members/migration")] #[route("/admin/members")]
Migration {}, Members {},
#[route("/admin/members/migration")]
Migration {},
} }
const _TAILWIND_URL: &str = manganis::mg!(file("assets/tailwind.css")); const _TAILWIND_URL: &str = manganis::mg!(file("assets/tailwind.css"));

View File

@ -64,7 +64,7 @@ impl Session {
pub async fn fetch_user_from_token(session_token: String) -> Result<User, crate::Error> { pub async fn fetch_user_from_token(session_token: String) -> Result<User, crate::Error> {
let mut res = DB let mut res = DB
.query( .query(
"SELECT user as id, user.email as email FROM session WHERE token = $session_token", "SELECT user as id, user.email as email, user.admin as admin FROM session WHERE token = $session_token",
) )
.bind(("session_token", session_token)) .bind(("session_token", session_token))
.await?; .await?;

View File

@ -13,13 +13,14 @@ pub struct User {
#[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))] #[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))]
pub id: String, pub id: String,
pub email: String, pub email: String,
pub admin: bool,
password: Option<String>, password: Option<String>,
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
impl User { impl User {
pub async fn fetch_all() -> Result<Vec<Self>, surrealdb::Error> { pub async fn fetch_all() -> Result<Vec<Self>, surrealdb::Error> {
let mut res = DB.query("SELECT id, email FROM user").await?; let mut res = DB.query("SELECT id, email, admin FROM user").await?;
res = res.check()?; res = res.check()?;
@ -35,7 +36,7 @@ impl User {
) -> Result<Self, crate::Error> { ) -> Result<Self, crate::Error> {
// Create user record // Create user record
let mut transaction = DB.query(BeginStatement::default()) let mut transaction = DB.query(BeginStatement::default())
.query("let $user = CREATE ONLY user SET email = $email, password = crypto::argon2::generate($password) RETURN id, email;") .query("let $user = CREATE ONLY user SET email = $email, password = crypto::argon2::generate($password) RETURN id, email, admin;")
.bind(("email", email)) .bind(("email", email))
.bind(("password", password)); .bind(("password", password));
@ -103,7 +104,7 @@ impl User {
password: String, password: String,
) -> Result<String, crate::Error> { ) -> Result<String, crate::Error> {
let mut res = DB let mut res = DB
.query("SELECT id, email FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)") .query("SELECT id, email, admin FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)")
.bind(("email", email)) .bind(("email", email))
.bind(("password", password)) .bind(("password", password))
.await?; .await?;

View File

@ -41,6 +41,8 @@ async fn apply_queries() -> surrealdb::Result<()> {
VALUE string::lowercase($value) VALUE string::lowercase($value)
ASSERT string::is::email($value); ASSERT string::is::email($value);
DEFINE FIELD password ON TABLE user TYPE string; DEFINE FIELD password ON TABLE user TYPE string;
DEFINE FIELD admin ON TABLE user TYPE bool
DEFAULT false;
DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE; DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE;
", ",