2 Commits
Author SHA1 Message Date
schn33fuchs 654b55a2a4 Put some api calls behind admin access 2026-05-02 19:15:40 +02:00
schn33fuchs 931bd27c65 Sidebar fix
Styles now actually get applied and sidebar doesn't block content
Also the login page doesn't have a sidebar now
2026-05-02 19:09:41 +02:00
8 changed files with 159 additions and 15 deletions
+83
View File
@@ -93,3 +93,86 @@ pub async fn validate_token(
request.extensions_mut().insert(filter_user(&user));
Ok(next.run(request).await)
}
pub async fn validate_admin(
cookies: CookieJar,
State(data): State<Arc<AppState>>,
mut request: Request<Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let token = cookies
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
request
.headers()
.get(header::AUTHORIZATION)
.and_then(|header| header.to_str().ok())
.and_then(|value| {
if value.starts_with("Bearer ") {
Some(value[7..].to_owned())
} else {
None
}
})
});
let token = token.ok_or_else(|| {
let error = json!({
"status": "error",
"message": "Please provide a valid token"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
let claims = decode_token(
token,
&DecodingKey::from_secret(data.env.token_secret.as_ref()),
)
.map_err(|(status, json_err)| {
let error = json!({
"status": json_err.status,
"message": json_err.message
});
(status, Json(error))
})?;
let uuid = (&claims.sub).parse::<i16>().map_err(|_| {
let error = json!({
"status": "error",
"message": "Invalid user id"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
let user = sqlx::query_as::<_, User>(r#"SELECT * FROM users WHERE id = $1"#)
.bind(uuid)
.fetch_optional(&data.db)
.await
.map_err(|e| {
let error = json!({
"status": "error",
"message": format!("Database error: {}", e)
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
})?;
let user = user.ok_or_else(|| {
let error = json!({
"status": "error",
"message": "Invalid user"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
if !user.is_admin {
let error = json!({
"status": "error",
"message": "Admin access required"
});
return Err((StatusCode::FORBIDDEN, Json(error)));
}
request.extensions_mut().insert(filter_user(&user));
Ok(next.run(request).await)
}
+12 -5
View File
@@ -7,7 +7,7 @@ use axum::{
use crate::{
AppState,
cookie::validation::validate_token,
cookie::validation::{validate_admin, validate_token},
handlers::{
auth::{
create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
@@ -18,9 +18,7 @@ use crate::{
};
pub fn create_router(state: Arc<AppState>) -> Router {
let protected_routes = Router::new()
.route("/api/tickets", get(get_tickets))
.route("/api/tickets/create", post(create_ticket))
let admin_routes = Router::new()
.route(
"/api/tickets/{id}",
get(get_ticket_by_id)
@@ -28,13 +26,22 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.patch(edit_ticket),
)
.route("/api/register", post(create_user))
.route("/api/logout", get(logout))
.route("/api/users", get(get_users))
.route("/api/users/current", get(get_current_user))
.route(
"/api/users/{id}",
get(get_user_by_id).delete(delete_user).patch(update_user),
)
.layer(middleware::from_fn_with_state(
state.clone(),
validate_admin,
));
let protected_routes = Router::new()
.merge(admin_routes)
.route("/api/tickets", get(get_tickets))
.route("/api/tickets/create", post(create_ticket))
.route("/api/logout", get(logout))
.layer(middleware::from_fn_with_state(
state.clone(),
validate_token,
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<link data-trunk rel="rust" data-bin="bin" />
<link rel="scss" href="src/styles/main.scss" />
<link data-trunk rel="scss" href="src/styles/main.scss" />
<meta charset="utf-8" />
<title>Yew App</title>
</head>
+41 -9
View File
@@ -32,49 +32,82 @@ enum Route {
NotFound,
}
#[derive(Properties, PartialEq)]
pub struct SidebarShellProps {
pub children: Children,
}
#[component(SidebarShell)]
fn sidebar_shell(props: &SidebarShellProps) -> Html {
html! {
<div class="layout">
<sidebar::Sidebar/>
<main class="content">
{ for props.children.iter() }
</main>
</div>
}
}
fn switch(route: Route) -> Html {
match route {
Route::Home => html! {
<ProtectedRoute admin_page={false}>
<basic_pages::Home/>
<SidebarShell>
<basic_pages::Home/>
</SidebarShell>
</ProtectedRoute>
},
Route::NotFound => html! { <basic_pages::NotFound/> },
Route::Ticket => html! {
<ProtectedRoute admin_page={false}>
<ticket::SubmitTicket/>
<SidebarShell>
<ticket::SubmitTicket/>
</SidebarShell>
</ProtectedRoute>
},
Route::TicketById { id } => html! {
<ProtectedRoute admin_page={true}>
<ticket::TicketByID {id}/>
<SidebarShell>
<ticket::TicketByID {id}/>
</SidebarShell>
</ProtectedRoute>
},
Route::AllTickets => html! {
<ProtectedRoute admin_page={false}>
<ticket::AllTickets/>
<SidebarShell>
<ticket::AllTickets/>
</SidebarShell>
</ProtectedRoute>
},
Route::Register => html! {
<ProtectedRoute admin_page={true}>
<user::Register/>
<SidebarShell>
<user::Register/>
</SidebarShell>
</ProtectedRoute>
},
Route::Login => html! { <user::Login/> },
Route::AllUsers => html! {
<ProtectedRoute admin_page={true}>
<user::AllUsers/>
<SidebarShell>
<user::AllUsers/>
</SidebarShell>
</ProtectedRoute>
},
Route::UserByID { id } => html! {
<ProtectedRoute admin_page={true}>
<user::UserByID {id}/>
<SidebarShell>
<user::UserByID {id}/>
</SidebarShell>
</ProtectedRoute>
},
Route::PermissionDenied => html! { <basic_pages::PermissionDenied/> },
Route::Diagnostics => html! {
<ProtectedRoute admin_page={true}>
<utilities::Diagnostics/>
<SidebarShell>
<utilities::Diagnostics/>
</SidebarShell>
</ProtectedRoute>
},
}
@@ -84,7 +117,6 @@ fn switch(route: Route) -> Html {
pub fn app() -> Html {
html! {
<BrowserRouter>
<sidebar::Sidebar/>
<Switch<Route> render={switch} />
</BrowserRouter>
}
+2
View File
@@ -1,3 +1,5 @@
@use "variables" as *;
@mixin card {
backgroud: #fff;
border-radius: &border-radius;
+2
View File
@@ -1,3 +1,5 @@
@use "variables" as *;
.u-hidden { display: none !important; }
.u-gap-sm { gap: $spacing-sm; }
.text-muted { color: $color-muted; }
@@ -1,3 +1,6 @@
@use "../mixins" as *;
@use "../variables" as *;
.ticket-card {
@include card();
border-left: 4px solid $color-accent;
+15
View File
@@ -14,6 +14,21 @@ body {
.admin { display: flex; }
.content { flex: 1; padding: variables.$spacing-md; }
.layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 260px;
flex-shrink: 0;
}
.content {
flex: 1;
min-width: 0;
}
@media (max-width: 768px) {
.sidebar { position: fixed; left: -100%; transition: left .2s; }
.sidebar.open { left: 0; }