Compare commits
2
Commits
99bcbaf3b0
...
8ddfe2ba14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ddfe2ba14 | ||
|
|
26ac32db21 |
@@ -304,6 +304,84 @@ pub async fn update_user(
|
|||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_admin_exists(
|
||||||
|
State(data): State<Arc<AppState>>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
let admin_count = sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM users WHERE is_admin = true"#)
|
||||||
|
.fetch_one(&data.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({"status": "error", "message": format!("{:?}", e)})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let has_admin = admin_count > 0;
|
||||||
|
Ok(Json(json!({"has_admin": has_admin})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_initial_admin(
|
||||||
|
State(data): State<Arc<AppState>>,
|
||||||
|
Json(request): Json<UserCreateScheme>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
// Check if any admin already exists
|
||||||
|
let admin_count = sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM users WHERE is_admin = true"#)
|
||||||
|
.fetch_one(&data.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({"status": "error", "message": format!("{:?}", e)})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if admin_count > 0 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(json!({"status": "error", "message": "Admin user already exists"})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.username.is_empty() || request.pwd.is_empty() {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(json!({"status": "error", "message": "Missing credential"})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let argon = Argon2::default();
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let hashed_pwd = match argon.hash_password(request.pwd.clone().as_bytes(), &salt) {
|
||||||
|
Ok(h) => h.to_string(),
|
||||||
|
Err(e) => panic!("Error hashing {:}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = sqlx::query("INSERT INTO users (username, pwd, first_name, last_name, is_admin) VALUES ($1, $2, $3, $4, $5)")
|
||||||
|
.bind(request.username)
|
||||||
|
.bind(&hashed_pwd)
|
||||||
|
.bind(request.first_name)
|
||||||
|
.bind(request.last_name)
|
||||||
|
.bind(true)
|
||||||
|
.execute(&data.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({"status": "error", "message": format!("{}", e)})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if user.rows_affected() < 1 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({"status": "error", "message": "Error creating admin user"})),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
Ok(Json(json!({"status": "success", "result": "Admin user created"})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn filter_user(user: &User) -> FilteredUser {
|
pub fn filter_user(user: &User) -> FilteredUser {
|
||||||
FilteredUser {
|
FilteredUser {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ use crate::{
|
|||||||
cookie::validation::{validate_admin, 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,
|
check_admin_exists, create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
|
||||||
update_user,
|
setup_initial_admin, update_user,
|
||||||
},
|
},
|
||||||
ticket::{create_ticket, delete_ticket, edit_ticket, get_ticket_by_id, get_tickets},
|
ticket::{create_ticket, delete_ticket, edit_ticket, get_ticket_by_id, get_tickets},
|
||||||
},
|
},
|
||||||
@@ -50,5 +50,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.merge(protected_routes)
|
.merge(protected_routes)
|
||||||
.route("/api/login", post(login))
|
.route("/api/login", post(login))
|
||||||
|
.route("/api/check-admin", get(check_admin_exists))
|
||||||
|
.route("/api/setup-admin", post(setup_initial_admin))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-1
@@ -2,6 +2,8 @@ mod auth;
|
|||||||
mod pages;
|
mod pages;
|
||||||
use crate::auth::ProtectedRoute;
|
use crate::auth::ProtectedRoute;
|
||||||
use crate::pages::*;
|
use crate::pages::*;
|
||||||
|
use gloo_net::http::Request;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
@@ -19,6 +21,8 @@ enum Route {
|
|||||||
Register,
|
Register,
|
||||||
#[at("/login")]
|
#[at("/login")]
|
||||||
Login,
|
Login,
|
||||||
|
#[at("/setup")]
|
||||||
|
Setup,
|
||||||
#[at("/users")]
|
#[at("/users")]
|
||||||
AllUsers,
|
AllUsers,
|
||||||
#[at("/users/:id")]
|
#[at("/users/:id")]
|
||||||
@@ -49,6 +53,47 @@ fn sidebar_shell(props: &SidebarShellProps) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct AdminCheckWrapperProps {
|
||||||
|
pub children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(AdminCheckWrapper)]
|
||||||
|
fn admin_check_wrapper(props: &AdminCheckWrapperProps) -> Html {
|
||||||
|
let admin_exists = use_state(|| None::<bool>);
|
||||||
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let admin_exists = admin_exists.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let admin_exists = admin_exists.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match Request::get("/api/check-admin").send().await {
|
||||||
|
Ok(resp) if resp.status() == 200 => {
|
||||||
|
if let Ok(data) = resp.json::<serde_json::Value>().await {
|
||||||
|
let has_admin = data["has_admin"].as_bool().unwrap_or(false);
|
||||||
|
admin_exists.set(Some(has_admin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
admin_exists.set(Some(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|| ()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match *admin_exists {
|
||||||
|
None => html! { <div>{ "Loading..." }</div> },
|
||||||
|
Some(false) => {
|
||||||
|
navigator.push(&Route::Setup);
|
||||||
|
html! { <div>{ "Redirecting to setup..." }</div> }
|
||||||
|
}
|
||||||
|
Some(true) => props.children.clone().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn switch(route: Route) -> Html {
|
fn switch(route: Route) -> Html {
|
||||||
match route {
|
match route {
|
||||||
Route::Home => html! {
|
Route::Home => html! {
|
||||||
@@ -87,7 +132,14 @@ fn switch(route: Route) -> Html {
|
|||||||
</SidebarShell>
|
</SidebarShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
},
|
},
|
||||||
Route::Login => html! { <user::Login/> },
|
Route::Login => html! {
|
||||||
|
<AdminCheckWrapper>
|
||||||
|
<user::Login/>
|
||||||
|
</AdminCheckWrapper>
|
||||||
|
},
|
||||||
|
Route::Setup => html! {
|
||||||
|
<setup::InitialAdminSetup/>
|
||||||
|
},
|
||||||
Route::AllUsers => html! {
|
Route::AllUsers => html! {
|
||||||
<ProtectedRoute admin_page={true}>
|
<ProtectedRoute admin_page={true}>
|
||||||
<SidebarShell>
|
<SidebarShell>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod basic_pages;
|
pub mod basic_pages;
|
||||||
|
pub mod setup;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
use gloo_net::http::Request;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AdminSetupScheme {
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub username: String,
|
||||||
|
pub pwd: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(InitialAdminSetup)]
|
||||||
|
pub fn initial_admin_setup() -> Html {
|
||||||
|
let first_name = use_state(|| "".to_string());
|
||||||
|
let last_name = use_state(|| "".to_string());
|
||||||
|
let username = use_state(|| "".to_string());
|
||||||
|
let pwd = use_state(|| "".to_string());
|
||||||
|
let pwd_confirm = use_state(|| "".to_string());
|
||||||
|
let error = use_state(|| String::new());
|
||||||
|
let success = use_state(|| false);
|
||||||
|
let loading = use_state(|| false);
|
||||||
|
let admin_check_done = use_state(|| false);
|
||||||
|
let navigator = use_navigator().unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let admin_check_done = admin_check_done.clone();
|
||||||
|
let navigator = navigator.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let admin_check_done = admin_check_done.clone();
|
||||||
|
let navigator = navigator.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
match Request::get("/api/check-admin").send().await {
|
||||||
|
Ok(resp) if resp.status() == 200 => {
|
||||||
|
if let Ok(data) = resp.json::<serde_json::Value>().await {
|
||||||
|
let has_admin = data["has_admin"].as_bool().unwrap_or(false);
|
||||||
|
if has_admin {
|
||||||
|
navigator.push(&crate::Route::Login);
|
||||||
|
} else {
|
||||||
|
admin_check_done.set(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
admin_check_done.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
admin_check_done.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|| ()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*admin_check_done {
|
||||||
|
return html! { <div>{ "Checking..." }</div> };
|
||||||
|
}
|
||||||
|
|
||||||
|
let onsubmit = {
|
||||||
|
let first_name = first_name.clone();
|
||||||
|
let last_name = last_name.clone();
|
||||||
|
let username = username.clone();
|
||||||
|
let pwd = pwd.clone();
|
||||||
|
let pwd_confirm = pwd_confirm.clone();
|
||||||
|
let error = error.clone();
|
||||||
|
let success = success.clone();
|
||||||
|
let loading = loading.clone();
|
||||||
|
let navigator = navigator.clone();
|
||||||
|
|
||||||
|
Callback::from(move |e: SubmitEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
|
||||||
|
if (*pwd).is_empty() || (*pwd_confirm).is_empty() {
|
||||||
|
error.set("Password fields cannot be empty".to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if *pwd != *pwd_confirm {
|
||||||
|
error.set("Passwords do not match".to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*username).is_empty() {
|
||||||
|
error.set("Username cannot be empty".to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_name_val = (*first_name).clone();
|
||||||
|
let last_name_val = (*last_name).clone();
|
||||||
|
let username_val = (*username).clone();
|
||||||
|
let pwd_val = (*pwd).clone();
|
||||||
|
|
||||||
|
loading.set(true);
|
||||||
|
error.set(String::new());
|
||||||
|
success.set(false);
|
||||||
|
|
||||||
|
let error = error.clone();
|
||||||
|
let success = success.clone();
|
||||||
|
let loading = loading.clone();
|
||||||
|
let navigator = navigator.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
let payload = AdminSetupScheme {
|
||||||
|
first_name: first_name_val,
|
||||||
|
last_name: last_name_val,
|
||||||
|
username: username_val,
|
||||||
|
pwd: pwd_val,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = Request::post("/api/setup-admin")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&payload)
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
loading.set(false);
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(r) if r.status() == 200 => {
|
||||||
|
success.set(true);
|
||||||
|
navigator.push(&crate::Route::Login);
|
||||||
|
}
|
||||||
|
Ok(r) => {
|
||||||
|
let text = r.text().await.unwrap_or_else(|_| "unknown".into());
|
||||||
|
error.set(format!("HTTP {}: {}", r.status(), text));
|
||||||
|
}
|
||||||
|
Err(err) => error.set(format!("Network error: {}", err)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="setup-container">
|
||||||
|
<div class="setup-box">
|
||||||
|
<h1>{ "Initial Admin Setup" }</h1>
|
||||||
|
<p>{ "Create your first administrator account" }</p>
|
||||||
|
|
||||||
|
<form {onsubmit} class="setup-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="first_name">{ "First Name:" }
|
||||||
|
<input
|
||||||
|
id="first_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="First name"
|
||||||
|
value={(*first_name).clone()}
|
||||||
|
oninput={Callback::from(move |e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
|
first_name.set(input.value());
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="last_name">{ "Last Name:" }
|
||||||
|
<input
|
||||||
|
id="last_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Last name"
|
||||||
|
value={(*last_name).clone()}
|
||||||
|
oninput={Callback::from(move |e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
|
last_name.set(input.value());
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">{ "Username:" }
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
value={(*username).clone()}
|
||||||
|
oninput={Callback::from(move |e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
|
username.set(input.value());
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">{ "Password:" }
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={(*pwd).clone()}
|
||||||
|
oninput={Callback::from(move |e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
|
pwd.set(input.value());
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwd_confirm">{ "Confirm Password:" }
|
||||||
|
<input
|
||||||
|
id="pwd_confirm"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
value={(*pwd_confirm).clone()}
|
||||||
|
oninput={Callback::from(move |e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
|
pwd_confirm.set(input.value());
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={*loading} class="submit-btn">
|
||||||
|
{ if *loading { "Creating..." } else { "Create Admin Account" } }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
if !error.is_empty() {
|
||||||
|
<p class="error-message" style="color:red">{ (*error).clone() }</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
if *success {
|
||||||
|
<p class="success-message" style="color:green">{ "Admin account created successfully! Redirecting to login..." }</p>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user