diff --git a/Cargo.lock b/Cargo.lock index 5721db4..da8295e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,21 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456348ed9dcd72a13a1f4a660449fafdecee9ac8205552e286809eb5b0b29bd3" +dependencies = [ + "actix-utils", + "actix-web", + "base64", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -404,6 +419,7 @@ dependencies = [ "actix-cors", "actix-rt", "actix-web", + "actix-web-httpauth", "argon2", "base64", "bcrypt", diff --git a/apps/auth/Cargo.toml b/apps/auth/Cargo.toml index 03b4e3f..50707e1 100644 --- a/apps/auth/Cargo.toml +++ b/apps/auth/Cargo.toml @@ -36,5 +36,6 @@ env_logger = "0.11.8" thiserror = "2.0.12" fernet = "0.2.2" base64 = "0.22.1" +actix-web-httpauth = "0.8.2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/apps/auth/src/entities/mod.rs b/apps/auth/src/entities/mod.rs index cc4a92c..c1e27f7 100644 --- a/apps/auth/src/entities/mod.rs +++ b/apps/auth/src/entities/mod.rs @@ -1,3 +1,6 @@ pub mod keys; +pub mod permission; pub mod revoked_token; +pub mod role; +pub mod role_permission; pub mod user; diff --git a/apps/auth/src/entities/permission.rs b/apps/auth/src/entities/permission.rs new file mode 100644 index 0000000..2b86966 --- /dev/null +++ b/apps/auth/src/entities/permission.rs @@ -0,0 +1,33 @@ +use sea_orm::entity::prelude::*; +use utoipa::ToSchema; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, ToSchema)] +#[sea_orm(table_name = "permissions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = true)] + pub id: i64, + pub name: String, + pub description: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::role_permission::Entity", + from = "Column::Id", + to = "super::role_permission::Column::PermissionId" + )] + RolePermission, +} + +impl Related for Entity { + fn to() -> RelationDef { + super::role_permission::Relation::Role.def() + } + + fn via() -> Option { + Some(super::role_permission::Relation::Permission.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/auth/src/entities/role.rs b/apps/auth/src/entities/role.rs new file mode 100644 index 0000000..6d4018b --- /dev/null +++ b/apps/auth/src/entities/role.rs @@ -0,0 +1,33 @@ +use sea_orm::entity::prelude::*; +use utoipa::ToSchema; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, ToSchema)] +#[sea_orm(table_name = "roles")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = true)] + pub id: i64, + pub name: String, + pub description: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::role_permission::Entity", + from = "Column::Id", + to = "super::role_permission::Column::RoleId" + )] + RolePermission, +} + +impl Related for Entity { + fn to() -> RelationDef { + super::role_permission::Relation::Permission.def() + } + + fn via() -> Option { + Some(super::role_permission::Relation::Role.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/auth/src/entities/role_permission.rs b/apps/auth/src/entities/role_permission.rs new file mode 100644 index 0000000..c7a49e8 --- /dev/null +++ b/apps/auth/src/entities/role_permission.rs @@ -0,0 +1,34 @@ +use sea_orm::entity::prelude::*; +use utoipa::ToSchema; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, ToSchema)] +#[sea_orm(table_name = "role_permissions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub role_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub permission_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Role, + Permission, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Role => Entity::belongs_to(super::role::Entity) + .from(Column::RoleId) + .to(super::role::Column::Id) + .into(), + Self::Permission => Entity::belongs_to(super::permission::Entity) + .from(Column::PermissionId) + .to(super::permission::Column::Id) + .into(), + } + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/auth/src/entities/user.rs b/apps/auth/src/entities/user.rs index e593c03..6cd4d15 100644 --- a/apps/auth/src/entities/user.rs +++ b/apps/auth/src/entities/user.rs @@ -12,7 +12,7 @@ pub struct Model { pub password: String, pub version_terms_agreement: String, pub disabled_since: Option, - pub permission_id: i64, + pub role_id: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/apps/auth/src/infra/server.rs b/apps/auth/src/infra/server.rs index 79a5a85..c5ac8f6 100755 --- a/apps/auth/src/infra/server.rs +++ b/apps/auth/src/infra/server.rs @@ -1,6 +1,12 @@ use crate::routes; use actix_cors::Cors; -use actix_web::{dev::Server, web, App, HttpServer}; +use actix_web::{ + dev::Server, + web, + App, + HttpServer, // +}; + use sea_orm::DatabaseConnection; use utoipa_swagger_ui::SwaggerUi; diff --git a/apps/auth/src/models/auth.rs b/apps/auth/src/models/auth.rs index 9713cbb..400302a 100644 --- a/apps/auth/src/models/auth.rs +++ b/apps/auth/src/models/auth.rs @@ -9,7 +9,7 @@ pub struct RegisterRequest { pub email: String, pub password: String, pub version_terms: String, - pub permission_id: i64, + pub role_id: i64, } #[derive(Deserialize, ToSchema)] diff --git a/apps/auth/src/models/user.rs b/apps/auth/src/models/user.rs index 80300b4..81b3bea 100644 --- a/apps/auth/src/models/user.rs +++ b/apps/auth/src/models/user.rs @@ -10,7 +10,7 @@ pub struct UserPublic { pub login: String, pub email: String, pub version_terms: String, - pub permission_id: i64, + pub role_id: i64, pub disabled_since: Option, } @@ -22,6 +22,6 @@ pub struct UserUpdate { pub email: Option, pub password: Option, pub version_terms: Option, - pub permission_id: Option, + pub role_id: Option, pub disabled_since: Option>, } diff --git a/apps/auth/src/routes/auth.rs b/apps/auth/src/routes/auth.rs index 961916d..a4c1b7c 100644 --- a/apps/auth/src/routes/auth.rs +++ b/apps/auth/src/routes/auth.rs @@ -4,6 +4,7 @@ use actix_web::{ HttpResponse, Responder, }; +use actix_web_httpauth::middleware::HttpAuthentication; use bcrypt::{hash, verify, DEFAULT_COST}; use fernet::Fernet; use sea_orm::{ @@ -14,9 +15,15 @@ use sea_orm::{ QueryFilter, Set, }; +use sea_query::Query; use crate::{ - entities::{keys as keys_entity, user as user_entity}, + entities::{ + keys as keys_entity, + permission as permission_entity, + role_permission as role_permission_entity, + user as user_entity, // + }, infra::server::{ DatabaseClientKeys, DatabaseClientPostgres, // @@ -24,19 +31,26 @@ use crate::{ models::{ auth::*, jwt::Claims, // - }, // - service::{fernet::*, jwt::*}, + }, + service::{ + fernet::*, + jwt::*, // + }, }; use super::common::{handle_server_error_body, handle_server_error_string, CustomError}; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("") - .route("/", web::post().to(login)) - .route("/register", web::post().to(register)) + web::scope("/auth") + .route("/login", web::post().to(login)) .route("/validate", web::post().to(validate_token)) .route("/logout", web::post().to(logout)), + ) + .service( + web::scope("") + .wrap(HttpAuthentication::bearer(validator)) + .route("/register", web::post().to(register)), ); } @@ -73,7 +87,7 @@ pub async fn register( email: Set(encrypt_field(&fernet, &data.email)), password: Set(encrypt_field(&fernet, &hashed)), version_terms_agreement: Set(encrypt_field(&fernet, &data.version_terms)), - permission_id: Set(data.permission_id), + role_id: Set(data.role_id), disabled_since: NotSet, }; @@ -104,7 +118,7 @@ pub async fn register( #[utoipa::path( post, - path = "/", + path = "/auth/login", request_body = LoginRequest, responses( (status = 200, description = "Login successful", body = LoginResponse), @@ -125,51 +139,68 @@ pub async fn login( .one(&postgres_client.client) .await; - match user_result { - Ok(Some(user)) => { - if user.disabled_since.is_some() { - return HttpResponse::Unauthorized().body("Inactive user"); - } - - let user_decryption_key = match get_user_key(user.id, &keys_client, &config).await { - GetUserKeyResult::Ok(key) => key, - GetUserKeyResult::Err(err) => return err, - }; - - let decrypted_password = match decrypt_field( - &fernet::Fernet::new(&user_decryption_key).unwrap(), - &user.password, - ) { - Ok(p) => p, - Err(err) => { - return handle_server_error_body( - "Decryption error", - CustomError::UnsuccessfulDecryption("password".to_string(), err), - &config, - None, - ); - } - }; - - if verify(&data.password, &decrypted_password).unwrap_or(false) { - let token = create_jwt(&user.id.to_string(), &config.jwt_secret); - HttpResponse::Ok().json(LoginResponse { - token, - id: user.id, - permissions: vec![], - }) - } else { - HttpResponse::Unauthorized().body("Invalid credentials") - } + let user = match user_result { + Ok(Some(user)) => user, + Ok(None) => return HttpResponse::Unauthorized().body("User not found"), + Err(err) => return handle_server_error_body("Database Error", err, &config, None), + }; + + if user.disabled_since.is_some() { + return HttpResponse::Unauthorized().body("Inactive user"); + } + + let user_decryption_key = match get_user_key(user.id, &keys_client, &config).await { + GetUserKeyResult::Ok(key) => key, + GetUserKeyResult::Err(err) => return err, + }; + + let decrypted_password = match decrypt_field( + &fernet::Fernet::new(&user_decryption_key).unwrap(), + &user.password, + ) { + Ok(p) => p, + Err(err) => { + return handle_server_error_body( + "Decryption error", + CustomError::UnsuccessfulDecryption("password".to_string(), err), + &config, + None, + ); } - Ok(None) => HttpResponse::Unauthorized().body("User not found"), - Err(err) => handle_server_error_body("Database Error", err, &config, None), + }; + + let permissions = match permission_entity::Entity::find() + .filter( + permission_entity::Column::Id.in_subquery( + Query::select() + .column(role_permission_entity::Column::PermissionId) + .from(role_permission_entity::Entity) + .and_where(role_permission_entity::Column::RoleId.eq(user.role_id)) + .to_owned(), + ), + ) + .all(&postgres_client.client) + .await + { + Ok(permissions) => permissions.into_iter().map(|p| p.name).collect(), + Err(err) => return handle_server_error_body("Database Error", err, &config, None), + }; + + if verify(&data.password, &decrypted_password).unwrap_or(false) { + let token = create_jwt(&user.id.to_string(), &config.jwt_secret); + HttpResponse::Ok().json(LoginResponse { + token, + id: user.id, + permissions, + }) + } else { + HttpResponse::Unauthorized().body("Invalid credentials") } } #[utoipa::path( post, - path = "/validate", + path = "/auth/validate", request_body = ValidateRequest, responses( (status = 200, description = "Token is valid", body = Claims), @@ -191,7 +222,7 @@ pub async fn validate_token( #[utoipa::path( post, - path = "/logout", + path = "/auth/logout", request_body = ValidateRequest, responses( (status = 200, description = "Token invalidated") diff --git a/apps/auth/src/routes/common/error.rs b/apps/auth/src/routes/common/error.rs index eee6fa3..cb7892b 100644 --- a/apps/auth/src/routes/common/error.rs +++ b/apps/auth/src/routes/common/error.rs @@ -5,7 +5,6 @@ use crate::service::fernet::DecryptionError; #[derive(Debug, Error)] pub enum CustomError { - /// Missing decryption key for a given user ID #[error("Decryption key not found in `user_key` table for user ID {0}")] UserKeyNotFound(i64), #[error("Unsuccessful decryption of field `{0}`: {1}")] diff --git a/apps/auth/src/routes/user.rs b/apps/auth/src/routes/user.rs index cf72e42..f08eb5a 100644 --- a/apps/auth/src/routes/user.rs +++ b/apps/auth/src/routes/user.rs @@ -1,4 +1,5 @@ use actix_web::{web, HttpResponse, Responder}; +use actix_web_httpauth::middleware::HttpAuthentication; use bcrypt::{hash, DEFAULT_COST}; use fernet::Fernet; use sea_orm::prelude::Date; @@ -11,16 +12,19 @@ use crate::models::{PaginatedRequest, PaginatedResponse, UserPublic, UserUpdate} use crate::service::fernet::{ decrypt_database_user, encrypt_field, get_user_key, GetUserKeyResult, }; +use crate::service::jwt::validator; use super::common::{handle_server_error_body, ServerErrorType}; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( - // - web::scope("/users").route("/", web::post().to(get_users)), + web::scope("/users") + .wrap(HttpAuthentication::bearer(validator)) + .route("/", web::post().to(get_users)), // ) .service( web::scope("/user") + .wrap(HttpAuthentication::bearer(validator)) .route("/{id}", web::get().to(get_user)) .route("/{id}", web::put().to(update_user)) .route("/{id}", web::delete().to(delete_user)), @@ -219,8 +223,8 @@ async fn update_user( Some(ref version_terms) => Set(encrypt_field(&fernet, &version_terms)), None => NotSet, }; - user_update_model.permission_id = match user_update.permission_id { - Some(permission_id) => Set(permission_id), + user_update_model.role_id = match user_update.role_id { + Some(role_id) => Set(role_id), None => NotSet, }; user_update_model.password = match user_update.password { diff --git a/apps/auth/src/service/fernet.rs b/apps/auth/src/service/fernet.rs index 7343c26..600c86b 100644 --- a/apps/auth/src/service/fernet.rs +++ b/apps/auth/src/service/fernet.rs @@ -100,7 +100,7 @@ pub fn decrypt_database_user( login: user.login, email, version_terms, - permission_id: user.permission_id, + role_id: user.role_id, disabled_since: match user.disabled_since { Some(dt) => Some(dt.format("%Y-%m-%d").to_string()), None => None, diff --git a/apps/auth/src/service/jwt.rs b/apps/auth/src/service/jwt.rs index 6b72814..a9a8265 100644 --- a/apps/auth/src/service/jwt.rs +++ b/apps/auth/src/service/jwt.rs @@ -1,43 +1,51 @@ +use crate::models::jwt::*; +use actix_web::dev::ServiceRequest; use actix_web::{http::header, HttpRequest}; -use jsonwebtoken::{ - decode, - encode, - DecodingKey, - EncodingKey, - Header, - TokenData, - Validation, -}; +use actix_web::{web, HttpMessage}; +use actix_web_httpauth::extractors::bearer::BearerAuth; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use sea_orm::EntityTrait; -use sea_orm::{ - ActiveModelTrait, - ActiveValue::Set, - DatabaseConnection, -}; +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection}; use sqlx::types::chrono::Utc; use time::{Duration, OffsetDateTime}; use uuid::Uuid; -use crate::models::jwt::*; use crate::entities::revoked_token; -pub fn create_jwt(user_id: &str, jwt_secret: &str) -> String { - let now = OffsetDateTime::now_utc(); - let exp = (now + Duration::hours(24)).unix_timestamp() as usize; +pub async fn validator( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result { + let jwt_secret = match req.app_data::>() { + Some(data) => data.get_ref().clone(), + None => { + return Err(( + actix_web::error::ErrorInternalServerError("JWT secret not configured"), + req, + )) + } + }; - let claims = Claims { - sub: user_id.to_owned(), - iat: now.unix_timestamp() as usize, - exp, - jti: Uuid::new_v4().to_string(), + let db = match req.app_data::>() { + Some(data) => data.get_ref().clone(), + None => { + return Err(( + actix_web::error::ErrorInternalServerError("Database connection not configured"), + req, + )) + } }; - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(jwt_secret.as_bytes()), - ) - .expect("Failed to create token") + match verify_jwt(credentials.token(), &jwt_secret, &db).await { + Ok(token_data) => { + req.extensions_mut().insert(token_data.claims); + Ok(req) + } + Err(_) => Err(( + actix_web::error::ErrorUnauthorized("Invalid or revoked token"), + req, + )), + } } pub async fn verify_jwt( @@ -63,6 +71,25 @@ pub async fn verify_jwt( Ok(token_data) } +pub fn create_jwt(user_id: &str, jwt_secret: &str) -> String { + let now = OffsetDateTime::now_utc(); + let exp = (now + Duration::hours(24)).unix_timestamp() as usize; + + let claims = Claims { + sub: user_id.to_owned(), + iat: now.unix_timestamp() as usize, + exp, + jti: Uuid::new_v4().to_string(), + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(jwt_secret.as_bytes()), + ) + .expect("Failed to create token") +} + pub fn extract_bearer(req: &HttpRequest) -> Result<&str, &'static str> { let hdr = req .headers() diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index f358324..10fddc3 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -11,15 +11,14 @@ import '../styles.css'; import YieldRegister from '../pages/YieldRegister'; import ProjectionPage from '../pages/ProjectionPage'; import UserManagementPage from '../pages/UserManagementPage'; -import UserInformation from '../pages/PersonalData'; -import { getUserIdFromLocalStorage } from '../store/storage'; import Login from '../pages/Login'; import ProjectionCustomPage from '../pages/ProjectionCustomPage'; import { useEffect, useState } from 'react'; import ReportPage from '../pages/ReportPage'; -import { isUserLoggedIn } from '../store/storage'; +import { getLocalStorageData, isUserLoggedIn } from '../store/storage'; import TermsPage from '../pages/Terms'; import TermsModal from '../pages/TermsModal'; +import EditUserPage from '../pages/PersonalData'; function App() { const [isAuthenticated, setIsAuthenticated] = useState(true); @@ -28,11 +27,7 @@ function App() { useEffect(() => { isUserLoggedIn().then((result) => setIsAuthenticated(result)); - const localPermissions = localStorage.getItem('khali_api6:permissions'); - if (localPermissions) { - const parsed = JSON.parse(localPermissions).map((p: string) => p.trim()); - setPermissions(parsed); - } + setPermissions(getLocalStorageData()?.permissions || []); }, []); return ( @@ -76,7 +71,7 @@ function App() { } + element={} /> { /* handle accept */ }} setIsAuthenticated={setIsAuthenticated} />} /> diff --git a/apps/web/src/components/NavBar.tsx b/apps/web/src/components/NavBar.tsx index cfa518f..2f78c8e 100644 --- a/apps/web/src/components/NavBar.tsx +++ b/apps/web/src/components/NavBar.tsx @@ -2,7 +2,6 @@ import { logout } from '../service/AuthService'; import { useNavigate } from 'react-router-dom'; import { clearLocalStorageData, - clearUserIdFromLocalStorage, } from '../store/storage'; import { useEffect, useState } from 'react'; @@ -35,7 +34,6 @@ const Navbar = ({ setIsAuthenticated }: NavbarProps) => { try { await logout(); setIsAuthenticated(false); - clearUserIdFromLocalStorage(); clearLocalStorageData(); navigate('/login'); } catch (err) { diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index ce55f49..8748755 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -4,11 +4,7 @@ import '../styles.css'; import backgroundImage from '../assets/background-login.jpg'; import kersysLogo from '../assets/kersys-logo.png'; import { login } from '../service/AuthService'; -import { - setTokenToLocalStorage, - setPermissionsToLocalStorage, - getUserIdFromLocalStorage, -} from '../store/storage'; +import { setLocalStorageData, getLocalStorageData } from '../store/storage'; import { TermsService } from '../service/TermsService'; const Login = ({ @@ -32,20 +28,18 @@ const Login = ({ console.log('Login attempt:', { username, password }); if (username === 'admin' && password === 'admin123') { - setTokenToLocalStorage('token'); + setLocalStorageData({ + id: 1, + token: 'token', + permissions: ['dashboard ', 'register ', 'analitic', 'terms'], + }); setIsAuthenticated(true); navigate('/', { replace: true }); - setPermissionsToLocalStorage([ - 'dashboard ', - 'register ', - 'analitic', - 'terms', - ]); } else if (await login({ login: username, password })) { setIsAuthenticated(true); if ( await TermsService.hasUserAcceptedTerms( - String(getUserIdFromLocalStorage()) + String(getLocalStorageData()?.id) ) ) { setIsAuthenticated(true); diff --git a/apps/web/src/pages/PersonalData.tsx b/apps/web/src/pages/PersonalData.tsx index 2289f08..3464d51 100644 --- a/apps/web/src/pages/PersonalData.tsx +++ b/apps/web/src/pages/PersonalData.tsx @@ -4,6 +4,7 @@ import { FieldSchema } from '../schemas/FormsSchema'; import { getUser, updateUser } from '../service/UserService'; import { NewUser } from '../schemas/UserSchema'; import { useNavigate } from 'react-router-dom'; +import { getLocalStorageData } from '../store/storage'; const editFormSchema: FieldSchema[] = [ { name: 'name', label: 'Nome Completo', type: 'text' }, @@ -13,14 +14,15 @@ const editFormSchema: FieldSchema[] = [ { name: 'confirmPassword', label: 'Confirmar Senha', type: 'password' }, ]; -const EditUserPage = ({ userId }: { userId: number }) => { +const EditUserPage = () => { + const [userId, setUserId] = useState(null); const [currentUser, setCurrentUser] = useState>({}); const [isLoading, setIsLoading] = useState(true); const navigate = useNavigate(); const loadUser = async () => { - setIsLoading(true); - try { + if (userId) { + setIsLoading(true); const user = await getUser(userId); setCurrentUser({ id: user.id, @@ -28,9 +30,6 @@ const EditUserPage = ({ userId }: { userId: number }) => { login: user.login, email: user.email, }); - } catch (error) { - console.error('Erro ao carregar usuário:', error); - } finally { setIsLoading(false); } }; @@ -39,8 +38,18 @@ const EditUserPage = ({ userId }: { userId: number }) => { loadUser(); }, [userId]); + useEffect(() => { + const storedUserId = getLocalStorageData()?.id || null; + if (storedUserId) { + setUserId(storedUserId); + } else { + console.error('ID do usuário não encontrado no localStorage.'); + navigate('/login'); + } + }, []); + const handleEditSubmit = async (formData: Record) => { - try { + if (userId) { const userData: Partial = { name: formData.name, login: formData.login, @@ -55,8 +64,6 @@ const EditUserPage = ({ userId }: { userId: number }) => { } await updateUser(userId, userData); alert('Usuário atualizado com sucesso!'); - } catch (error) { - console.error('Erro ao atualizar usuário:', error); } }; diff --git a/apps/web/src/pages/TermsModal.tsx b/apps/web/src/pages/TermsModal.tsx index caf9982..9843646 100644 --- a/apps/web/src/pages/TermsModal.tsx +++ b/apps/web/src/pages/TermsModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { TermsService } from '../service/TermsService'; @@ -7,11 +7,7 @@ import { TermStatus, UserAcceptanceRequest, } from '../schemas/TermsSchema'; -import { - clearLocalStorageData, - clearUserIdFromLocalStorage, - getUserIdFromLocalStorage, -} from '../store/storage'; +import { getLocalStorageData, clearLocalStorageData } from '../store/storage'; import { deleteUser } from '../service/UserService'; import { logout } from '../service/AuthService'; @@ -26,12 +22,12 @@ interface NavbarProps { setIsAuthenticated: (auth: boolean) => void; } -const TermsModal: React.FC = ({ +const TermsModal = ({ onAccept, initialNewsletterOptIn = false, showBackOption = false, setIsAuthenticated, -}) => { +}: TermsModalProps) => { const [isChecked, setIsChecked] = useState(false); const [newsletterOptIn, setNewsletterOptIn] = useState( initialNewsletterOptIn @@ -46,7 +42,7 @@ const TermsModal: React.FC = ({ useEffect(() => { const fetchTermsAndUserAcceptance = async () => { - const userId = String(getUserIdFromLocalStorage()); + const userId = getLocalStorageData()?.id || null; const term = await TermsService.getActiveTerm(); setTermsText(term.text); @@ -85,14 +81,14 @@ const TermsModal: React.FC = ({ }, []); const handleAccept = async () => { - const userId = getUserIdFromLocalStorage(); + const userId = getLocalStorageData()?.id; if (!userId) { console.error('ID do usuário não encontrado no localStorage.'); return; } const handleDelete = async () => { - const currentUser = getUserIdFromLocalStorage(); + const currentUser = getLocalStorageData()?.id; if ( currentUser && window.confirm('Tem certeza que deseja excluir este usuário?') @@ -124,7 +120,6 @@ const TermsModal: React.FC = ({ await logout(); setIsAuthenticated(false); - clearUserIdFromLocalStorage(); clearLocalStorageData(); navigate('/login'); return; diff --git a/apps/web/src/schemas/ProjectionCostumSchema.ts b/apps/web/src/schemas/ProjectionCustomSchema.ts similarity index 100% rename from apps/web/src/schemas/ProjectionCostumSchema.ts rename to apps/web/src/schemas/ProjectionCustomSchema.ts diff --git a/apps/web/src/schemas/auth.ts b/apps/web/src/schemas/auth.ts index 3183f6d..7846e89 100644 --- a/apps/web/src/schemas/auth.ts +++ b/apps/web/src/schemas/auth.ts @@ -4,9 +4,11 @@ export type LoginRequest = { }; export type LoginResponse = { - token?: string; - userId: number; - permissions: string[]; + user?: { + id: number; + token: string; + permissions: string[]; + }; }; export type ValidateRequest = { diff --git a/apps/web/src/service/AuthService.ts b/apps/web/src/service/AuthService.ts index a509db9..0336c85 100644 --- a/apps/web/src/service/AuthService.ts +++ b/apps/web/src/service/AuthService.ts @@ -5,44 +5,27 @@ import { ValidateResponse, } from '../schemas/auth'; -import { - getTokenFromLocalStorage, - setPermissionsToLocalStorage, - setTokenToLocalStorage, - setUserIdToLocalStorage -} from '../store/storage'; +import { getLocalStorageData, setLocalStorageData } from '../store/storage'; import { AUTH_BASE_URL, processPOST } from './service'; -import { jwtDecode } from "jwt-decode"; export const login = async (params: LoginRequest): Promise => { const result = await processPOST({ - path: '/', + path: '/auth/login', body: params, overrideURL: AUTH_BASE_URL, }); - if (!result.token) { + if (!result.user) { return false; } - setPermissionsToLocalStorage(result.permissions); - setTokenToLocalStorage(result.token); - try{ - const decoded = jwtDecode(result.token); - const userId = decoded.sub; - if (userId) { - setUserIdToLocalStorage(userId); - } - } catch (error) { - console.error('Error decoding token:', error); - return false; - } + setLocalStorageData(result.user); return true; }; export const validate = async (token: string) => { return await processPOST({ - path: '/validate', + path: '/auth/validate', body: { token }, overrideURL: AUTH_BASE_URL, }).catch(() => false); @@ -50,9 +33,9 @@ export const validate = async (token: string) => { export const logout = async (): Promise => { const result = await processPOST({ - path: '/logout', + path: '/auth/logout', body: { - token: getTokenFromLocalStorage(), + token: getLocalStorageData()?.token, }, overrideURL: AUTH_BASE_URL, }); diff --git a/apps/web/src/service/ProjectionCostumService.ts b/apps/web/src/service/ProjectionCustomService.ts similarity index 100% rename from apps/web/src/service/ProjectionCostumService.ts rename to apps/web/src/service/ProjectionCustomService.ts diff --git a/apps/web/src/service/TermsService.ts b/apps/web/src/service/TermsService.ts index 8ec9cbb..55b0131 100644 --- a/apps/web/src/service/TermsService.ts +++ b/apps/web/src/service/TermsService.ts @@ -13,8 +13,8 @@ import { processGET, processPOST, processRequest, - API_BASE_URL } from './service'; - +} from './service'; + const TERMS_BASE_PATH = '/terms'; export class TermsService { @@ -31,7 +31,9 @@ export class TermsService { }); } - static async acceptTerms(data: UserAcceptanceRequest): Promise<{ success: boolean }> { + static async acceptTerms( + data: UserAcceptanceRequest + ): Promise<{ success: boolean }> { return await processPOST({ path: `${TERMS_BASE_PATH}/user/accept`, body: data, @@ -44,20 +46,15 @@ export class TermsService { }); } - static async createNewVersion(data: NewTermVersionRequest): Promise { + static async createNewVersion( + data: NewTermVersionRequest + ): Promise { return await processPOST({ path: `${TERMS_BASE_PATH}/new-version`, body: data, }); } - // static async updateUserAcceptance(userId: string, data: UpdateAcceptanceRequest): Promise<{ success: boolean }> { - // return await processRequest<'PUT', { success: boolean }>('PUT', { - // path: `${TERMS_BASE_PATH}/user/update/${userId}`, - // body: data, - // }); - // } - static async checkCompliance(userId: string): Promise { return await processGET({ path: `${TERMS_BASE_PATH}/user/compliance/${userId}`, @@ -66,10 +63,10 @@ export class TermsService { static async hasUserAcceptedTerms(userId: string): Promise { try { - const response = await processGET<{ - _id: string; - topics: { accepted: boolean; description: string; status: string }[]; - user_id: string; + const response = await processGET<{ + _id: string; + topics: { accepted: boolean; description: string; status: string }[]; + user_id: string; }>({ path: `${TERMS_BASE_PATH}/user/${userId}`, }); @@ -90,25 +87,31 @@ export class TermsService { // Caso contrário, todos os termos ativos estão aceitos, retorna true return true; - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { // Caso de erro na requisição, considera que usuário não aceitou return false; } } - static async getUserAcceptance(userId: string): Promise<{ _id: string; topics: AcceptedTopic[]; user_id: string }> { - return await processGET({ - path: `${TERMS_BASE_PATH}/user/${userId}` - }); - } - - static async updateUserAcceptance(acceptanceId: string, data: UpdateAcceptanceRequest): Promise<{ success: boolean }> { - return await processRequest('PUT', { - path: `${TERMS_BASE_PATH}/user/update/${acceptanceId}`, - body: data, + static async getUserAcceptance( + userId: number + ): Promise<{ _id: string; topics: AcceptedTopic[]; user_id: string }> { + return await processGET({ + path: `${TERMS_BASE_PATH}/user/${userId}`, }); } - + static async updateUserAcceptance( + acceptanceId: string, + data: UpdateAcceptanceRequest + ): Promise<{ success: boolean }> { + return await processRequest( + 'PUT', + { + path: `${TERMS_BASE_PATH}/user/update/${acceptanceId}`, + body: data, + } + ); + } } - diff --git a/apps/web/src/service/service.ts b/apps/web/src/service/service.ts index 0cc1bff..0d4a6f7 100644 --- a/apps/web/src/service/service.ts +++ b/apps/web/src/service/service.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { Page, PageRequest, emptyPage } from '../schemas/pagination'; -import { getTokenFromLocalStorage } from '../store/storage'; +import { getLocalStorageData } from '../store/storage'; export const API_BASE_URL = 'http://127.0.0.1:5000'; export const AUTH_BASE_URL = 'http://127.0.0.1:3000'; @@ -37,7 +37,7 @@ export const processRequest = async ( params?: RequestParams ): Promise => { const { path, body, overrideURL } = params || {}; - const token = getTokenFromLocalStorage(); + const token = getLocalStorageData()?.token; const response = await axios.request({ url: `${overrideURL || API_BASE_URL}${path}`, diff --git a/apps/web/src/store/storage.ts b/apps/web/src/store/storage.ts index b13e2e0..90f2a9d 100644 --- a/apps/web/src/store/storage.ts +++ b/apps/web/src/store/storage.ts @@ -1,39 +1,35 @@ import { validate } from '../service/AuthService'; -export const getTokenFromLocalStorage = (): string | null => { - const userToken = localStorage.getItem('khali_api6:token'); - return userToken ? userToken : null; +export type UserData = { + token: string; + id: number; + permissions: string[]; }; -export const setTokenToLocalStorage = (token: string): void => { - localStorage.setItem('khali_api6:token', token); +export const setLocalStorageData = (userData: UserData): void => { + localStorage.setItem('khali_api6:user', JSON.stringify(userData)); }; -export const clearLocalStorageData = (): void => { - localStorage.removeItem('khali_api6:token'); +export const getLocalStorageData = (): UserData | null => { + const userData = localStorage.getItem('khali_api6:user'); + return userData ? JSON.parse(userData) : null; }; -export const isUserLoggedIn = async (): Promise => { - const token = getTokenFromLocalStorage(); - - if (!token) return false; - - return await validate(token); +export const putLocalStorageData = (userData: Partial): void => { + const currentData = getLocalStorageData(); + if (currentData) { + const updatedData = { ...currentData, ...userData }; + setLocalStorageData(updatedData); + } }; -export const getUserIdFromLocalStorage = (): number => { - const userId = localStorage.getItem('khali_api6:id'); - return userId ? parseInt(userId) : 0; -}; - -export const setUserIdToLocalStorage = (userId: string): void => { - localStorage.setItem('khali_api6:id', userId); +export const clearLocalStorageData = (): void => { + localStorage.removeItem('khali_api6:user'); }; +export const isUserLoggedIn = async (): Promise => { + const user = getLocalStorageData(); -export const clearUserIdFromLocalStorage = (): void => { - localStorage.removeItem('khali_api6:id'); -}; + if (!user?.token) return false; -export const setPermissionsToLocalStorage = (permissions: string[]): void => { - localStorage.setItem('khali_api6:permissions', JSON.stringify(permissions)); + return await validate(user.token); }; diff --git a/apps/web/tests/UserStorage.test.ts b/apps/web/tests/UserStorage.test.ts index f83045c..27970fd 100644 --- a/apps/web/tests/UserStorage.test.ts +++ b/apps/web/tests/UserStorage.test.ts @@ -1,97 +1,83 @@ import { validate } from '../src/service/AuthService'; import { - getTokenFromLocalStorage, - setTokenToLocalStorage, - clearLocalStorageData, - isUserLoggedIn, - getUserIdFromLocalStorage, - setUserIdToLocalStorage, - clearUserIdFromLocalStorage, + getLocalStorageData, + clearLocalStorageData, + isUserLoggedIn, + setLocalStorageData, } from '../src/store/storage'; jest.mock('../src/service/AuthService', () => ({ -validate: jest.fn(), + validate: jest.fn(), })); describe('storage', () => { -const tokenKey = 'khali_api6:utoken'; -const userIdKey = 'khali_api6:uid'; + const localStorageDataKey = 'khali_api6:user'; -beforeEach(() => { - localStorage.clear(); - jest.clearAllMocks(); -}); - -describe('getTokenFromLocalStorage', () => { - it('returns token if present', () => { - localStorage.setItem(tokenKey, 'abc123'); - expect(getTokenFromLocalStorage()).toBe('abc123'); - }); - - it('returns null if token not present', () => { - expect(getTokenFromLocalStorage()).toBeNull(); - }); -}); + const testLocalStorageDataObject = { + id: 1, + token: 'xyz789', + permissions: [], + }; -describe('setTokenToLocalStorage', () => { - it('sets token in localStorage', () => { - setTokenToLocalStorage('xyz789'); - expect(localStorage.getItem(tokenKey)).toBe('xyz789'); + beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); }); -}); -describe('clearLocalStorageData', () => { - it('removes token from localStorage', () => { - localStorage.setItem(tokenKey, 'abc123'); - clearLocalStorageData(); - expect(localStorage.getItem(tokenKey)).toBeNull(); - }); -}); + describe('getTokenFromLocalStorage', () => { + it('returns token if present', () => { + localStorage.setItem( + localStorageDataKey, + JSON.stringify(testLocalStorageDataObject) + ); + expect(getLocalStorageData()).toMatchObject(testLocalStorageDataObject); + }); -describe('isUserLoggedIn', () => { - it('returns false if no token', async () => { - (validate as jest.Mock).mockResolvedValue(true); - expect(await isUserLoggedIn()).toBe(false); - expect(validate).not.toHaveBeenCalled(); + it('returns null if data not present', () => { + expect(getLocalStorageData()).toBeNull(); + }); }); - it('calls validate and returns its result if token exists', async () => { - localStorage.setItem(tokenKey, 'abc123'); - (validate as jest.Mock).mockResolvedValue(true); - expect(await isUserLoggedIn()).toBe(true); - expect(validate).toHaveBeenCalledWith('abc123'); + describe('setLocalStorageData', () => { + it('sets data in localStorage', () => { + setLocalStorageData(testLocalStorageDataObject); + expect(localStorage.getItem(localStorageDataKey)).toMatchObject( + testLocalStorageDataObject + ); + }); }); - it('returns false if validate returns false', async () => { - localStorage.setItem(tokenKey, 'abc123'); - (validate as jest.Mock).mockResolvedValue(false); - expect(await isUserLoggedIn()).toBe(false); + describe('clearLocalStorageData', () => { + it('removes token from localStorage', () => { + localStorage.setItem(localStorageDataKey, 'abc123'); + clearLocalStorageData(); + expect(localStorage.getItem(localStorageDataKey)).toBeNull(); + }); }); -}); -describe('getUserIdFromLocalStorage', () => { - it('returns empty string if userId not present', () => { - expect(getUserIdFromLocalStorage()).toBe(''); + describe('isUserLoggedIn', () => { + it('returns false if no token', async () => { + expect(await isUserLoggedIn()).toBe(false); + expect(validate).not.toHaveBeenCalled(); }); - it('returns userId if present', () => { - localStorage.setItem(userIdKey, '42'); - expect(getUserIdFromLocalStorage()).toBe('42'); + it('calls validate and returns its result if token exists', async () => { + localStorage.setItem( + localStorageDataKey, + JSON.stringify(testLocalStorageDataObject) + ); + (validate as jest.Mock).mockResolvedValue(true); + expect(await isUserLoggedIn()).toBe(true); + expect(validate).toHaveBeenCalledWith('xyz789'); }); -}); -describe('setUserIdToLocalStorage', () => { - it('sets userId in localStorage', () => { - setUserIdToLocalStorage('1'); - expect(localStorage.getItem(userIdKey)).toBe('1'); - }); -}); - -describe('clearUserIdFromLocalStorage', () => { - it('removes userId from localStorage', () => { - localStorage.setItem(userIdKey, '42'); - clearUserIdFromLocalStorage(); - expect(localStorage.getItem(userIdKey)).toBeNull(); + it('returns false if validate returns false', async () => { + localStorage.setItem( + localStorageDataKey, + JSON.stringify(testLocalStorageDataObject) + ); + (validate as jest.Mock).mockResolvedValue(false); + expect(await isUserLoggedIn()).toBe(false); + }); }); }); -}); \ No newline at end of file