update added jwt tokens
This commit is contained in:
1 parent
a5e28c5533
commit
b0064a9b81
25 files changed
+2004
-31
No files matched your search
+22
-1
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user