update added jwt tokens

This commit is contained in:
wt committed 2025-11-11 14:26:16 +07:00
1 parent a5e28c5533
commit b0064a9b81
25 files changed
+2004 -31

No files matched your search

+22 -1
View File
@@ -7,6 +7,8 @@ struct FileConfig {
host: Option<String>,
port: Option<u16>,
log_level: Option<String>,
jwt_secret: Option<String>,
web: Option<String>,
}
#[derive(Parser, Debug)]
@@ -23,6 +25,12 @@ struct Cli {
#[arg(short, long)]
log_level: Option<String>,
#[arg(short, long)]
jwt_secret: Option<String>,
#[arg(short, long)]
web: Option<String>,
}
#[derive(Debug, Clone)]
@@ -30,6 +38,8 @@ pub struct Config {
pub host: String,
pub port: u16,
pub log_level: String,
pub jwt_secret: String,
pub web: String,
}
impl Config {
@@ -43,6 +53,8 @@ impl Config {
host: None,
port: None,
log_level: None,
jwt_secret: None,
web: None,
}
};
@@ -55,11 +67,20 @@ impl Config {
.log_level
.or(file_config.log_level)
.unwrap_or_else(|| "info".to_string());
let jwt_secret: String = cli
.jwt_secret
.or(file_config.jwt_secret)
.unwrap_or_else(|| "secret".to_string());
let web: String = cli
.web
.or(file_config.web)
.unwrap_or_else(|| "./web".to_string());
Ok(Config {
host,
port,
log_level,
jwt_secret,
web,
})
}
+207 -22
View File
@@ -3,14 +3,23 @@ use crate::model;
use crate::state;
use axum::Json;
use axum::extract::State;
use chrono::Utc;
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use std::time::Instant;
use uuid::Uuid;
pub async fn get_all_users_handler(
State(state): State<state::AppState>,
) -> Json<Vec<model::GetUserResponse>> {
Json(payload): Json<model::GetAllUsersRequest>,
) -> Result<Json<Vec<model::GetUserResponse>>, Json<model::ErrorResponse>> {
if !(verify_jwt_token(&state, payload.token)) {
return Err(Json(model::ErrorResponse {
error: "Invalid token".to_string(),
}));
}
let key = "all_users".to_string();
if let Some(cached_data) = state.cache.get(&key) {
return Json(cached_data.0);
return Ok(Json(cached_data.0));
}
let storage_guard = state.storage.lock().await;
let users: Vec<model::User> = storage_guard.values().cloned().collect();
@@ -27,16 +36,22 @@ pub async fn get_all_users_handler(
state
.cache
.insert(key, (mapped_users.clone(), Instant::now()));
Json(mapped_users)
Ok(Json(mapped_users))
}
pub async fn get_user_by_id_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::GetUserByIdRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
if !verify_jwt_token(&state, payload.token.clone()) {
return Err(Json(model::ErrorResponse {
error: "Invalid token".to_string(),
}));
}
let claims = get_jwt_claims(&state, payload.token.clone()).map_err(|err| err)?;
let storage_guard = state.storage.lock().await;
storage_guard
.get(&payload.id)
.get(&claims.user_id)
.cloned()
.map(|user| {
Json(model::GetUserResponse {
@@ -58,32 +73,48 @@ pub async fn get_user_by_username_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::GetUserByUsernameRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
if !(verify_jwt_token(&state, payload.token)) {
return Err(Json(model::ErrorResponse {
error: "Invalid token".to_string(),
}));
}
let storage_guard = state.storage.lock().await;
storage_guard
.get(&payload.username)
.cloned()
.map(|user| {
Json(model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
})
})
.ok_or_else(|| {
Json(model::ErrorResponse {
error: "User not found".to_string(),
})
})
let users: Vec<model::User> = storage_guard.values().cloned().collect();
let user = users
.into_iter()
.find(|user| user.username == payload.username);
if user.is_none() {
return Err(Json(model::ErrorResponse {
error: "User not found".to_string(),
}));
}
let user = user.unwrap();
Ok(Json(model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
}))
}
pub async fn add_user_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::AddUserRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
if !verify_jwt_token(&state, payload.token.clone()) {
return Err(Json(model::ErrorResponse {
error: "Invalid token".to_string(),
}));
}
let claims = get_jwt_claims(&state, payload.token).map_err(|err| err)?;
if !claims.is_superuser {
return Err(Json(model::ErrorResponse {
error: "Permission denied".to_string(),
}));
}
let user = model::User {
id: payload.id.clone(),
id: Uuid::new_v4().to_string(),
username: payload.username.clone(),
email: payload.email.clone(),
password: payload.password.clone(),
@@ -116,7 +147,18 @@ pub async fn delete_user_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::DeleteUserRequest>,
) -> Result<Json<model::DeleteUserResponse>, Json<model::ErrorResponse>> {
if !verify_jwt_token(&state, payload.token.clone()) {
return Err(Json(model::ErrorResponse {
error: "Invalid token".to_string(),
}));
}
let claims = get_jwt_claims(&state, payload.token.clone()).map_err(|error| error)?;
let mut storage_guard = state.storage.lock().await;
if claims.user_id != payload.id && !claims.is_superuser {
return Err(Json(model::ErrorResponse {
error: "Unauthorized".to_string(),
}));
}
if storage_guard.remove(&payload.id).is_none() {
return Err(Json(model::ErrorResponse {
error: "User not found".to_string(),
@@ -135,6 +177,17 @@ pub async fn update_user_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::UpdateUserRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
if !verify_jwt_token(&state, payload.token.clone()) {
return Err(Json(model::ErrorResponse {
error: "Unauthorized".to_string(),
}));
}
let claims = get_jwt_claims(&state, payload.token.clone()).map_err(|e| e)?;
if claims.user_id != payload.id && !claims.is_superuser {
return Err(Json(model::ErrorResponse {
error: "Unauthorized".to_string(),
}));
}
let mut storage_guard = state.storage.lock().await;
if !storage_guard.contains_key(&payload.id) {
return Err(Json(model::ErrorResponse {
@@ -168,3 +221,135 @@ pub async fn update_user_handler(
is_superuser: payload.is_superuser,
}))
}
pub async fn get_token_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::GetTokenRequest>,
) -> Result<Json<model::GetTokenResponse>, Json<model::ErrorResponse>> {
let storage_guard = state.storage.lock().await;
let users: Vec<model::User> = storage_guard.values().cloned().collect();
let user = users
.iter()
.find(|user| user.username == payload.username && user.password == payload.password);
if user.is_none() {
return Err(Json(model::ErrorResponse {
error: "Invalid username or password".to_string(),
}));
}
let now = Utc::now();
let expiration = (now + chrono::Duration::hours(24)).timestamp() as usize;
let claims = model::JwtClaims {
sub: "1234".to_owned(),
exp: expiration,
iat: now.timestamp() as usize,
user_id: user.unwrap().id.clone(),
username: user.unwrap().username.clone(),
email: user.unwrap().email.clone(),
group: user.unwrap().group.clone(),
is_superuser: user.unwrap().is_superuser,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(state.config.jwt_secret.as_ref()),
)
.map_err(|err| {
Json(model::ErrorResponse {
error: format!("Failed to encode JWT token: {}", err),
})
})?;
Ok(Json(model::GetTokenResponse { token }))
}
pub async fn verify_token_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::VerifyTokenRequest>,
) -> Result<Json<model::VerifyTokenResponse>, Json<model::ErrorResponse>> {
let token = payload.token;
if verify_jwt_token(&state, token) {
Ok(Json(model::VerifyTokenResponse { success: true }))
} else {
Err(Json(model::ErrorResponse {
error: "Invalid token".to_string(),
}))
}
}
fn verify_jwt_token(state: &state::AppState, token: String) -> bool {
match decode::<model::JwtClaims>(
token,
&DecodingKey::from_secret(state.config.jwt_secret.as_ref()),
&Validation::new(jsonwebtoken::Algorithm::HS256),
) {
Ok(_) => true,
Err(_) => false,
}
}
fn get_jwt_claims(
state: &state::AppState,
token: String,
) -> Result<model::JwtClaims, Json<model::ErrorResponse>> {
match decode::<model::JwtClaims>(
token,
&DecodingKey::from_secret(state.config.jwt_secret.as_ref()),
&Validation::new(jsonwebtoken::Algorithm::HS256),
) {
Ok(claims) => Ok(claims.claims),
Err(err) => Err(Json(model::ErrorResponse {
error: format!("Failed to decode JWT token: {}", err),
})),
}
}
pub async fn update_user_password_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::UpdateUserPasswordRequest>,
) -> Result<Json<model::UpdateUserPasswordResponse>, Json<model::ErrorResponse>> {
if !verify_jwt_token(&state, payload.token.clone()) {
return Err(Json(model::ErrorResponse {
error: "Unauthorized".to_string(),
}));
}
let claims = get_jwt_claims(&state, payload.token.clone()).map_err(|err| err)?;
if payload.old_password.is_none() && !claims.is_superuser {
return Err(Json(model::ErrorResponse {
error: "Unauthorized".to_string(),
}));
}
let mut storage_guard = state.storage.lock().await;
let user = storage_guard.get(&payload.id).cloned();
if user.is_none() {
return Err(Json(model::ErrorResponse {
error: "User not found".to_string(),
}));
}
let user = user.unwrap();
if !payload.old_password.is_none() {
if payload.old_password.unwrap() != user.password {
return Err(Json(model::ErrorResponse {
error: "Incorrect old password".to_string(),
}));
}
}
storage_guard.insert(
payload.id.clone(),
model::User {
id: payload.id.clone(),
username: user.username.clone(),
email: user.email.clone(),
password: payload.new_password.clone(),
group: user.group.clone(),
is_superuser: user.is_superuser,
},
);
let storage_clone = storage_guard.clone(); // write to file in background
tokio::spawn(async move {
if let Err(e) = db::write_users_to_file(&storage_clone).await {
eprintln!("Failed to write users to file: {}", e);
}
});
Ok(Json(model::UpdateUserPasswordResponse { success: true }))
}
+57 -3
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct User {
pub id: String,
pub username: String,
@@ -12,17 +12,18 @@ pub struct User {
#[derive(Debug, Deserialize)]
pub struct GetUserByIdRequest {
pub id: String,
pub token: String,
}
#[derive(Debug, Deserialize)]
pub struct GetUserByUsernameRequest {
pub token: String,
pub username: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AddUserRequest {
pub id: String,
pub token: String,
pub username: String,
pub email: String,
pub password: String,
@@ -41,6 +42,7 @@ pub struct GetUserResponse {
#[derive(Serialize, Deserialize, Debug)]
pub struct DeleteUserRequest {
pub token: String,
pub id: String,
}
@@ -51,6 +53,7 @@ pub struct DeleteUserResponse {
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateUserRequest {
pub token: String,
pub id: String,
pub username: String,
pub email: String,
@@ -62,3 +65,54 @@ pub struct UpdateUserRequest {
pub struct ErrorResponse {
pub error: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: String,
pub exp: usize,
pub iat: usize,
pub user_id: String,
pub username: String,
pub email: String,
pub group: String,
pub is_superuser: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetTokenRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetTokenResponse {
pub token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyTokenRequest {
pub token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyTokenResponse {
pub success: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetAllUsersRequest {
pub token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateUserPasswordRequest {
pub token: String,
pub id: String,
pub new_password: String,
pub old_password: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateUserPasswordResponse {
pub success: bool,
}
+8
View File
@@ -2,6 +2,7 @@ use crate::handlers;
use crate::state;
use axum::{Router, routing::delete, routing::post, routing::put};
use std::net::SocketAddr;
use tower_http::services::ServeDir;
pub async fn start_server() -> anyhow::Result<()> {
let state = state::AppState::new().await?;
@@ -30,6 +31,13 @@ pub fn create_app(state: state::AppState) -> Router {
.route("/api/users/add", post(handlers::add_user_handler))
.route("/api/users", delete(handlers::delete_user_handler))
.route("/api/users", put(handlers::update_user_handler))
.route("/api/token", post(handlers::get_token_handler))
.route("/api/token/verify", post(handlers::verify_token_handler))
.route(
"/api/users/password",
post(handlers::update_user_password_handler),
)
.nest_service("/", ServeDir::new(state.config.web.clone()))
.with_state(state)
}
+1 -1
View File
@@ -18,7 +18,7 @@ impl AppState {
let user_storage = db::init_user_storage().await;
let cache = Cache::builder()
.max_capacity(10_000) // максимальное количество записей
.time_to_live(Duration::from_secs(300)) // TTL 5 минут
.time_to_live(Duration::from_secs(2)) // TTL 5 минут
.build();
let state = AppState {
storage: user_storage,