Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
654b55a2a4 | ||
|
|
931bd27c65 |
@@ -93,3 +93,86 @@ pub async fn validate_token(
|
|||||||
request.extensions_mut().insert(filter_user(&user));
|
request.extensions_mut().insert(filter_user(&user));
|
||||||
Ok(next.run(request).await)
|
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
@@ -7,7 +7,7 @@ use axum::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
cookie::validation::validate_token,
|
cookie::validation::{validate_admin, validate_token},
|
||||||
handlers::{
|
handlers::{
|
||||||
auth::{
|
auth::{
|
||||||
create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
|
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 {
|
pub fn create_router(state: Arc<AppState>) -> Router {
|
||||||
let protected_routes = Router::new()
|
let admin_routes = Router::new()
|
||||||
.route("/api/tickets", get(get_tickets))
|
|
||||||
.route("/api/tickets/create", post(create_ticket))
|
|
||||||
.route(
|
.route(
|
||||||
"/api/tickets/{id}",
|
"/api/tickets/{id}",
|
||||||
get(get_ticket_by_id)
|
get(get_ticket_by_id)
|
||||||
@@ -28,13 +26,22 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|||||||
.patch(edit_ticket),
|
.patch(edit_ticket),
|
||||||
)
|
)
|
||||||
.route("/api/register", post(create_user))
|
.route("/api/register", post(create_user))
|
||||||
.route("/api/logout", get(logout))
|
|
||||||
.route("/api/users", get(get_users))
|
.route("/api/users", get(get_users))
|
||||||
.route("/api/users/current", get(get_current_user))
|
.route("/api/users/current", get(get_current_user))
|
||||||
.route(
|
.route(
|
||||||
"/api/users/{id}",
|
"/api/users/{id}",
|
||||||
get(get_user_by_id).delete(delete_user).patch(update_user),
|
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(
|
.layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
validate_token,
|
validate_token,
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<link data-trunk rel="rust" data-bin="bin" />
|
<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" />
|
<meta charset="utf-8" />
|
||||||
<title>Yew App</title>
|
<title>Yew App</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+33
-1
@@ -32,49 +32,82 @@ enum Route {
|
|||||||
NotFound,
|
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 {
|
fn switch(route: Route) -> Html {
|
||||||
match route {
|
match route {
|
||||||
Route::Home => html! {
|
Route::Home => html! {
|
||||||
<ProtectedRoute admin_page={false}>
|
<ProtectedRoute admin_page={false}>
|
||||||
|
<SidebarShell>
|
||||||
<basic_pages::Home/>
|
<basic_pages::Home/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::NotFound => html! { <basic_pages::NotFound/> },
|
Route::NotFound => html! { <basic_pages::NotFound/> },
|
||||||
Route::Ticket => html! {
|
Route::Ticket => html! {
|
||||||
<ProtectedRoute admin_page={false}>
|
<ProtectedRoute admin_page={false}>
|
||||||
|
<SidebarShell>
|
||||||
<ticket::SubmitTicket/>
|
<ticket::SubmitTicket/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::TicketById { id } => html! {
|
Route::TicketById { id } => html! {
|
||||||
<ProtectedRoute admin_page={true}>
|
<ProtectedRoute admin_page={true}>
|
||||||
|
<SidebarShell>
|
||||||
<ticket::TicketByID {id}/>
|
<ticket::TicketByID {id}/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::AllTickets => html! {
|
Route::AllTickets => html! {
|
||||||
<ProtectedRoute admin_page={false}>
|
<ProtectedRoute admin_page={false}>
|
||||||
|
<SidebarShell>
|
||||||
<ticket::AllTickets/>
|
<ticket::AllTickets/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::Register => html! {
|
Route::Register => html! {
|
||||||
<ProtectedRoute admin_page={true}>
|
<ProtectedRoute admin_page={true}>
|
||||||
|
<SidebarShell>
|
||||||
<user::Register/>
|
<user::Register/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::Login => html! { <user::Login/> },
|
Route::Login => html! { <user::Login/> },
|
||||||
Route::AllUsers => html! {
|
Route::AllUsers => html! {
|
||||||
<ProtectedRoute admin_page={true}>
|
<ProtectedRoute admin_page={true}>
|
||||||
|
<SidebarShell>
|
||||||
<user::AllUsers/>
|
<user::AllUsers/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::UserByID { id } => html! {
|
Route::UserByID { id } => html! {
|
||||||
<ProtectedRoute admin_page={true}>
|
<ProtectedRoute admin_page={true}>
|
||||||
|
<SidebarShell>
|
||||||
<user::UserByID {id}/>
|
<user::UserByID {id}/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::PermissionDenied => html! { <basic_pages::PermissionDenied/> },
|
Route::PermissionDenied => html! { <basic_pages::PermissionDenied/> },
|
||||||
Route::Diagnostics => html! {
|
Route::Diagnostics => html! {
|
||||||
<ProtectedRoute admin_page={true}>
|
<ProtectedRoute admin_page={true}>
|
||||||
|
<SidebarShell>
|
||||||
<utilities::Diagnostics/>
|
<utilities::Diagnostics/>
|
||||||
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -84,7 +117,6 @@ fn switch(route: Route) -> Html {
|
|||||||
pub fn app() -> Html {
|
pub fn app() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<sidebar::Sidebar/>
|
|
||||||
<Switch<Route> render={switch} />
|
<Switch<Route> render={switch} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "variables" as *;
|
||||||
|
|
||||||
@mixin card {
|
@mixin card {
|
||||||
backgroud: #fff;
|
backgroud: #fff;
|
||||||
border-radius: &border-radius;
|
border-radius: &border-radius;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "variables" as *;
|
||||||
|
|
||||||
.u-hidden { display: none !important; }
|
.u-hidden { display: none !important; }
|
||||||
.u-gap-sm { gap: $spacing-sm; }
|
.u-gap-sm { gap: $spacing-sm; }
|
||||||
.text-muted { color: $color-muted; }
|
.text-muted { color: $color-muted; }
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
@use "../mixins" as *;
|
||||||
|
@use "../variables" as *;
|
||||||
|
|
||||||
.ticket-card {
|
.ticket-card {
|
||||||
@include card();
|
@include card();
|
||||||
border-left: 4px solid $color-accent;
|
border-left: 4px solid $color-accent;
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ body {
|
|||||||
.admin { display: flex; }
|
.admin { display: flex; }
|
||||||
.content { flex: 1; padding: variables.$spacing-md; }
|
.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) {
|
@media (max-width: 768px) {
|
||||||
.sidebar { position: fixed; left: -100%; transition: left .2s; }
|
.sidebar { position: fixed; left: -100%; transition: left .2s; }
|
||||||
.sidebar.open { left: 0; }
|
.sidebar.open { left: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user