Compare commits
No commits in common. "53f64dd15ff03637ad3b188899b8af235532cf48" and "91ec90f810fbacdfe1ba895fcb9eb75c306afb88" have entirely different histories.
53f64dd15f
...
91ec90f810
58
README.md
58
README.md
|
|
@ -1,36 +1,38 @@
|
|||
# Servers Manager
|
||||
# Hit Server Manager
|
||||
|
||||
Plataforma interna para catalogar servidores corporativos e facilitar consultas rápidas pelo time de desenvolvimento/analistas. Inclui backend em Spring Boot (JWT + refresh token em cookies) e frontend React/Vite consumindo os endpoints.
|
||||
Plataforma interna da Hit Communications para catalogar os servidores corporativos e expor essas informações de forma rápida ao time de Análise de Sistemas e Desenvolvimento. O backend centraliza cadastros, contexto técnico (tipo, aplicação e banco) e credenciais operacionais, facilitando consultas padronizadas e mantendo histórico de alterações.
|
||||
|
||||
## Estrutura
|
||||
- `backend/`: API em Spring Boot 4 (Java 21), JWT stateless, cookies para auth.
|
||||
- `frontned/`: Front em React + Vite (TS), login integrado via cookies.
|
||||
- `postman_collection.json`: Coleção para testar autenticação e CRUDs de usuários/servidores.
|
||||
## Principais recursos
|
||||
- CRUD completo de usuários internos e servidores
|
||||
- Validação de domínio corporativo para criação de usuários
|
||||
- Mapeamentos DTO ↔ entidades com MapStruct
|
||||
- API REST documentada via coleção Postman (`postman_collection.json`)
|
||||
- Persistência em H2 (memória/disco) com Spring Data JPA e geração automática de IDs personalizados para servidores
|
||||
|
||||
## Requisitos
|
||||
- JDK 21
|
||||
- Node 20+ (Yarn ou npm)
|
||||
## Stack
|
||||
- Java 21 + Spring Boot 4.0
|
||||
- Spring Web MVC, Validation, Data JPA
|
||||
- H2 Database
|
||||
- MapStruct 1.6
|
||||
- Lombok
|
||||
- Spring Security (JWT em breve)
|
||||
- Docker Compose para orquestração local
|
||||
- ReactJS
|
||||
|
||||
## Rodando local
|
||||
1. **Backend**
|
||||
## Como rodar
|
||||
1. **Pré-requisitos**: JDK 21 e acesso ao Gradle Wrapper (`./gradlew`).
|
||||
2. **Instalar dependências e rodar testes**:
|
||||
```bash
|
||||
./gradlew clean test
|
||||
```
|
||||
3. **Subir o backend**:
|
||||
```bash
|
||||
cd backend
|
||||
./gradlew bootRun
|
||||
```
|
||||
Variáveis úteis: `JWT_SECRET` (>=32 chars), `DB_*` (Postgres). Por padrão usa `localhost:8080`.
|
||||
4. A API ficará disponível em `http://localhost:8080`. Utilize a coleção Postman incluída para exercitar os endpoints de `Users` e `Servers` (`{{base_url}}` já configurado com esse endereço).
|
||||
|
||||
2. **Frontend**
|
||||
```bash
|
||||
cd frontned
|
||||
cp .env.example .env # ajuste VITE_BACKEND_URL se necessário
|
||||
yarn install # ou npm install
|
||||
yarn dev # ou npm run dev
|
||||
```
|
||||
Acesse `http://localhost:5173`.
|
||||
|
||||
3. **Testar via Postman**
|
||||
Importe `postman_collection.json`. Rode "Auth / Login" para setar cookies e seguir para os demais endpoints.
|
||||
|
||||
## Documentação específica
|
||||
- Backend: `backend/README.md`
|
||||
- Frontend: `frontned/README.md`
|
||||
## Próximos passos
|
||||
- [ ] Substituir H2 por PostgreSQL para persistência
|
||||
- [ ] Proteger os endpoints com autenticação (Spring Security com JWT)
|
||||
- [ ] Criar o frontend React integrado ao backend
|
||||
- [ ] Containerizar backend + frontend com Docker Compose
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
# Backend - Servers Manager
|
||||
|
||||
API REST em Spring Boot 4 (Java 21) com autenticação JWT stateless e refresh token em cookie HttpOnly.
|
||||
|
||||
## Stack
|
||||
- Spring Boot 4.0 (Web MVC, Validation, Data JPA)
|
||||
- Spring Security (JWT, stateless)
|
||||
- MapStruct 1.6, Lombok
|
||||
- PostgreSQL (padrão) ou H2
|
||||
- JJWT 0.12 para geração/validação de tokens
|
||||
|
||||
## Configuração
|
||||
Variáveis principais:
|
||||
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWD` — Postgres
|
||||
- `JWT_SECRET` — chave HMAC >= 32 chars
|
||||
- `security.jwt.access-token-expiration` — padrão 30m
|
||||
- `security.jwt.refresh-token-expiration` — padrão 30d
|
||||
|
||||
Arquivo `src/main/resources/application.yaml` já traz defaults; adicione um `.env` ou exporte variáveis conforme o ambiente.
|
||||
|
||||
Seed: `src/main/resources/data.sql` cria usuário padrão `default@hittelco.com` (senha `senha123`, bcrypt).
|
||||
|
||||
## Endpoints chave
|
||||
- `POST /api/auth/login` — autentica e devolve `access_token` (cookie) + `refresh_token` (HttpOnly).
|
||||
- `POST /api/auth/refresh` — reemite cookies a partir do refresh.
|
||||
- `POST /api/auth/signup` — cria usuário (requer auth).
|
||||
- CRUD `/api/users` e `/api/servers` protegidos.
|
||||
|
||||
## Rodar local
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
API em `http://localhost:8080` com CORS liberado para `http://localhost:5173`.
|
||||
|
||||
## Testes
|
||||
```bash
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
## Postman
|
||||
Use `postman_collection.json` na raiz; o login grava cookies e variáveis `access_token`/`refresh_token`.
|
||||
|
|
@ -30,15 +30,11 @@ dependencies {
|
|||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
|
||||
implementation 'org.mapstruct:mapstruct:1.6.3'
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-data-jdbc-test'
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Auth",
|
||||
"name": "Users",
|
||||
"item": [
|
||||
{
|
||||
"name": "Login",
|
||||
"name": "Create User",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
|
|
@ -18,114 +18,82 @@
|
|||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"{{login_username}}\",\n \"password\": \"{{login_password}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/auth/login",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"auth",
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [],
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.environment.set(\"access_token\", pm.cookies.get('access_token') || \"\");",
|
||||
"pm.environment.set(\"refresh_token\", pm.cookies.get('refresh_token') || \"\");"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Refresh Token",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/auth/refresh",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"auth",
|
||||
"refresh"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [],
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.environment.set(\"access_token\", pm.cookies.get('access_token') || \"\");",
|
||||
"pm.environment.set(\"refresh_token\", pm.cookies.get('refresh_token') || \"\");"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Signup (autenticado)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"firstName\": \"João\",\n \"lastName\": \"Silva\",\n \"email\": \"joao.silva@hittelco.com\",\n \"password\": \"senha123\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/auth/signup",
|
||||
"raw": "{{base_url}}/api/users",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"auth",
|
||||
"signup"
|
||||
"users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"item": [
|
||||
},
|
||||
{
|
||||
"name": "Create User - Test 2",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"firstName\": \"Maria\",\n \"lastName\": \"Santos\",\n \"email\": \"maria.santos@accesscommunications.com\",\n \"password\": \"senha456\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create User - Invalid Domain",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"firstName\": \"Carlos\",\n \"lastName\": \"Oliveira\",\n \"email\": \"carlos@gmail.com\",\n \"password\": \"senha789\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get All Users",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users",
|
||||
"host": [
|
||||
|
|
@ -143,12 +111,7 @@
|
|||
"name": "Get User by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users/{{user_id}}",
|
||||
"host": [
|
||||
|
|
@ -167,14 +130,9 @@
|
|||
"name": "Get User by Username",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users/username/{{username}}",
|
||||
"raw": "{{base_url}}/api/users/username/joao.silva@hittelco.com",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
|
|
@ -182,7 +140,7 @@
|
|||
"api",
|
||||
"users",
|
||||
"username",
|
||||
"{{username}}"
|
||||
"joao.silva@hittelco.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -192,14 +150,9 @@
|
|||
"name": "Get User by Email",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users/email/{{email}}",
|
||||
"raw": "{{base_url}}/api/users/email/maria.santos@accesscommunications.com",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
|
|
@ -207,7 +160,7 @@
|
|||
"api",
|
||||
"users",
|
||||
"email",
|
||||
"{{email}}"
|
||||
"maria.santos@accesscommunications.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -221,10 +174,6 @@
|
|||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
|
|
@ -249,12 +198,7 @@
|
|||
"name": "Delete User",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/users/{{user_id}}",
|
||||
"host": [
|
||||
|
|
@ -282,10 +226,6 @@
|
|||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
|
|
@ -313,10 +253,6 @@
|
|||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
|
|
@ -340,12 +276,7 @@
|
|||
"name": "Get All Servers",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/servers",
|
||||
"host": [
|
||||
|
|
@ -363,12 +294,7 @@
|
|||
"name": "Get Server by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/servers/{{server_id}}",
|
||||
"host": [
|
||||
|
|
@ -387,12 +313,7 @@
|
|||
"name": "Get Server by Name",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/servers/name/Production DB Server",
|
||||
"host": [
|
||||
|
|
@ -412,12 +333,7 @@
|
|||
"name": "Get Servers by Type",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/servers/type/PRODUCTION",
|
||||
"host": [
|
||||
|
|
@ -461,10 +377,6 @@
|
|||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
|
|
@ -489,12 +401,7 @@
|
|||
"name": "Delete Server",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{access_token}}"
|
||||
}
|
||||
],
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/servers/{{server_id}}",
|
||||
"host": [
|
||||
|
|
@ -518,41 +425,11 @@
|
|||
"value": "http://localhost:8080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "login_username",
|
||||
"value": "joao.silva@hittelco.com",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "login_password",
|
||||
"value": "senha123",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "email",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "user_id",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "access_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "refresh_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "server_id",
|
||||
"value": "",
|
||||
|
|
@ -560,3 +437,4 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
package com.hitcommunications.servermanager.config.security;
|
||||
|
||||
import com.hitcommunications.servermanager.repositories.UsersRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UsersRepository usersRepository;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
return usersRepository.findByUsername(username)
|
||||
.or(() -> usersRepository.findByEmail(username))
|
||||
.map(UserPrincipal::fromUser)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
package com.hitcommunications.servermanager.config.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
public static final String ACCESS_TOKEN_COOKIE = "access_token";
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
final String jwt = resolveToken(request);
|
||||
try {
|
||||
if (jwt != null && SecurityContextHolder.getContext().getAuthentication() == null
|
||||
&& JwtService.TokenType.ACCESS == jwtService.extractTokenType(jwt)) {
|
||||
String username = jwtService.extractUsername(jwt);
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||
if (jwtService.isTokenValid(jwt, userDetails, JwtService.TokenType.ACCESS)) {
|
||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Let the security chain handle unauthorized requests
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Arrays.stream(cookies)
|
||||
.filter(cookie -> ACCESS_TOKEN_COOKIE.equals(cookie.getName()))
|
||||
.findFirst()
|
||||
.map(Cookie::getValue)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package com.hitcommunications.servermanager.config.security;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "security.jwt")
|
||||
public class JwtProperties {
|
||||
/**
|
||||
* Secret key used to sign the JWTs.
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* Access token validity window.
|
||||
*/
|
||||
private Duration accessTokenExpiration;
|
||||
|
||||
/**
|
||||
* Refresh token validity window.
|
||||
*/
|
||||
private Duration refreshTokenExpiration;
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package com.hitcommunications.servermanager.config.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class JwtService {
|
||||
|
||||
public static final String TOKEN_TYPE_CLAIM = "token_type";
|
||||
|
||||
private final JwtProperties properties;
|
||||
|
||||
public String generateAccessToken(UserDetails userDetails) {
|
||||
return buildToken(Collections.singletonMap(TOKEN_TYPE_CLAIM, TokenType.ACCESS.name()), userDetails, properties.getAccessTokenExpiration());
|
||||
}
|
||||
|
||||
public String generateRefreshToken(UserDetails userDetails) {
|
||||
return buildToken(Collections.singletonMap(TOKEN_TYPE_CLAIM, TokenType.REFRESH.name()), userDetails, properties.getRefreshTokenExpiration());
|
||||
}
|
||||
|
||||
public String extractUsername(String token) {
|
||||
return extractClaim(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
public TokenType extractTokenType(String token) {
|
||||
String tokenType = extractClaim(token, claims -> claims.get(TOKEN_TYPE_CLAIM, String.class));
|
||||
try {
|
||||
return tokenType == null ? null : TokenType.valueOf(tokenType);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTokenValid(String token, UserDetails userDetails) {
|
||||
final String username = extractUsername(token);
|
||||
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
|
||||
}
|
||||
|
||||
public boolean isTokenValid(String token, UserDetails userDetails, TokenType expectedType) {
|
||||
final String username = extractUsername(token);
|
||||
TokenType tokenType = extractTokenType(token);
|
||||
return username.equals(userDetails.getUsername()) && expectedType == tokenType && !isTokenExpired(token);
|
||||
}
|
||||
|
||||
private boolean isTokenExpired(String token) {
|
||||
return extractExpiration(token).before(new Date());
|
||||
}
|
||||
|
||||
private Date extractExpiration(String token) {
|
||||
return extractClaim(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = extractAllClaims(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
private Claims extractAllClaims(String token) {
|
||||
// parser() is deprecated in docs, but current JJWT version exposes only this entrypoint
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSignInKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, Duration expiration) {
|
||||
Instant now = Instant.now();
|
||||
Instant expiry = now.plus(expiration.toMillis(), ChronoUnit.MILLIS);
|
||||
|
||||
return Jwts.builder()
|
||||
.claims(extraClaims)
|
||||
.subject(userDetails.getUsername())
|
||||
.issuedAt(Date.from(now))
|
||||
.expiration(Date.from(expiry))
|
||||
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
private SecretKey getSignInKey() {
|
||||
byte[] keyBytes = properties.getSecret().getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
public enum TokenType {
|
||||
ACCESS,
|
||||
REFRESH
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
package com.hitcommunications.servermanager.config.security;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableConfigurationProperties(JwtProperties.class)
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthFilter;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(Customizer.withDefaults())
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/api/auth/login", "/api/auth/refresh").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.authenticationProvider(authenticationProvider())
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.httpBasic(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(List.of(
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173"
|
||||
));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setExposedHeaders(List.of("Set-Cookie"));
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package com.hitcommunications.servermanager.config.security;
|
||||
|
||||
import com.hitcommunications.servermanager.model.Users;
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public class UserPrincipal implements UserDetails {
|
||||
|
||||
private final UUID id;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final Collection<? extends GrantedAuthority> authorities;
|
||||
|
||||
private UserPrincipal(UUID id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
public static UserPrincipal fromUser(Users user) {
|
||||
return new UserPrincipal(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
List.of(new SimpleGrantedAuthority("ROLE_USER"))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
package com.hitcommunications.servermanager.controllers;
|
||||
|
||||
import com.hitcommunications.servermanager.config.security.JwtAuthenticationFilter;
|
||||
import com.hitcommunications.servermanager.config.security.JwtProperties;
|
||||
import com.hitcommunications.servermanager.model.dtos.AuthResponse;
|
||||
import com.hitcommunications.servermanager.model.dtos.AuthTokens;
|
||||
import com.hitcommunications.servermanager.model.dtos.LoginRequest;
|
||||
import com.hitcommunications.servermanager.model.dtos.NewUserDTO;
|
||||
import com.hitcommunications.servermanager.model.dtos.UserDTO;
|
||||
import com.hitcommunications.servermanager.services.AuthService;
|
||||
import com.hitcommunications.servermanager.utils.CookieUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
public static final String REFRESH_TOKEN_COOKIE = "refresh_token";
|
||||
|
||||
private final AuthService authService;
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<AuthResponse> login(@RequestBody @Valid LoginRequest request, HttpServletRequest httpRequest) {
|
||||
AuthTokens tokens = authService.login(request);
|
||||
return buildResponse(tokens, httpRequest);
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<AuthResponse> refresh(HttpServletRequest request) {
|
||||
String refreshToken = CookieUtils.getCookieValue(request, REFRESH_TOKEN_COOKIE);
|
||||
AuthTokens tokens = authService.refresh(refreshToken);
|
||||
return buildResponse(tokens, request);
|
||||
}
|
||||
|
||||
@PostMapping("/signup")
|
||||
public ResponseEntity<UserDTO> signup(@RequestBody @Valid NewUserDTO request) throws IllegalAccessException {
|
||||
UserDTO user = authService.signup(request);
|
||||
return ResponseEntity.ok(user);
|
||||
}
|
||||
|
||||
private ResponseEntity<AuthResponse> buildResponse(AuthTokens tokens, HttpServletRequest request) {
|
||||
boolean secure = request.isSecure();
|
||||
ResponseCookie accessCookie = CookieUtils.buildCookie(
|
||||
JwtAuthenticationFilter.ACCESS_TOKEN_COOKIE,
|
||||
tokens.accessToken(),
|
||||
jwtProperties.getAccessTokenExpiration(),
|
||||
false,
|
||||
secure
|
||||
);
|
||||
|
||||
ResponseCookie refreshCookie = CookieUtils.buildCookie(
|
||||
REFRESH_TOKEN_COOKIE,
|
||||
tokens.refreshToken(),
|
||||
jwtProperties.getRefreshTokenExpiration(),
|
||||
true,
|
||||
secure
|
||||
);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, accessCookie.toString(), refreshCookie.toString())
|
||||
.body(new AuthResponse(tokens.user()));
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,11 @@ public class UsersController {
|
|||
|
||||
private final UsersService usersService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserDTO> create(@RequestBody @Valid NewUserDTO crateDTO) throws IllegalAccessException {
|
||||
return ResponseEntity.ok().body(usersService.create(crateDTO));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserDTO> getById(@PathVariable UUID id) {
|
||||
return ResponseEntity.ok().body(usersService.getById(id));
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class Users {
|
|||
@Column(nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 120)
|
||||
@Column(nullable = false, length = 20)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false, length = 30)
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.hitcommunications.servermanager.model.dtos;
|
||||
|
||||
public record AuthResponse(
|
||||
UserDTO user
|
||||
) {
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.hitcommunications.servermanager.model.dtos;
|
||||
|
||||
public record AuthTokens(
|
||||
String accessToken,
|
||||
String refreshToken,
|
||||
UserDTO user
|
||||
) {
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.hitcommunications.servermanager.model.dtos;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record LoginRequest(
|
||||
@NotBlank String username,
|
||||
@NotBlank String password
|
||||
) {
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
package com.hitcommunications.servermanager.services;
|
||||
|
||||
import com.hitcommunications.servermanager.config.security.JwtService;
|
||||
import com.hitcommunications.servermanager.config.security.UserPrincipal;
|
||||
import com.hitcommunications.servermanager.model.Users;
|
||||
import com.hitcommunications.servermanager.model.dtos.AuthTokens;
|
||||
import com.hitcommunications.servermanager.model.dtos.LoginRequest;
|
||||
import com.hitcommunications.servermanager.model.dtos.NewUserDTO;
|
||||
import com.hitcommunications.servermanager.model.dtos.UserDTO;
|
||||
import com.hitcommunications.servermanager.mappers.UsersMapper;
|
||||
import com.hitcommunications.servermanager.repositories.UsersRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final UsersService usersService;
|
||||
private final UsersRepository usersRepository;
|
||||
private final UsersMapper usersMapper;
|
||||
private final JwtService jwtService;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
public AuthTokens login(LoginRequest request) {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.username(), request.password())
|
||||
);
|
||||
|
||||
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
|
||||
Users user = usersRepository.findById(principal.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found after authentication."));
|
||||
|
||||
updateLastLogin(user);
|
||||
return issueTokens(user, principal);
|
||||
}
|
||||
|
||||
public AuthTokens refresh(String refreshToken) {
|
||||
if (refreshToken == null || refreshToken.isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Refresh token is missing.");
|
||||
}
|
||||
|
||||
try {
|
||||
String username = jwtService.extractUsername(refreshToken);
|
||||
var userDetails = userDetailsService.loadUserByUsername(username);
|
||||
if (!jwtService.isTokenValid(refreshToken, userDetails, JwtService.TokenType.REFRESH)) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid refresh token.");
|
||||
}
|
||||
|
||||
Users user = usersRepository.findByUsername(username)
|
||||
.or(() -> usersRepository.findByEmail(username))
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found for refresh token."));
|
||||
|
||||
return issueTokens(user, UserPrincipal.fromUser(user));
|
||||
} catch (ResponseStatusException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid refresh token.");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserDTO signup(NewUserDTO createDTO) throws IllegalAccessException {
|
||||
return usersService.create(createDTO);
|
||||
}
|
||||
|
||||
private AuthTokens issueTokens(Users user, UserPrincipal principal) {
|
||||
String accessToken = jwtService.generateAccessToken(principal);
|
||||
String refreshToken = jwtService.generateRefreshToken(principal);
|
||||
return new AuthTokens(accessToken, refreshToken, usersMapper.toDTO(user));
|
||||
}
|
||||
|
||||
private void updateLastLogin(Users user) {
|
||||
user.setLastLogin(Timestamp.from(Instant.now()));
|
||||
usersRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.hitcommunications.servermanager.model.dtos.UserDTO;
|
|||
import com.hitcommunications.servermanager.repositories.UsersRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
|
@ -18,7 +17,6 @@ import java.util.UUID;
|
|||
public class UsersService {
|
||||
private final UsersMapper mapper;
|
||||
private final UsersRepository repo;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final String ALLOWED_DOMAIN = Arrays.asList("hittelco.com", "accesscommunications.com").toString();
|
||||
|
||||
|
|
@ -33,7 +31,6 @@ public class UsersService {
|
|||
});
|
||||
|
||||
Users entity = mapper.toEntity(createDTO);
|
||||
entity.setPassword(passwordEncoder.encode(createDTO.password()));
|
||||
entity = repo.save(entity);
|
||||
return mapper.toDTO(entity);
|
||||
}
|
||||
|
|
@ -80,7 +77,6 @@ public class UsersService {
|
|||
});
|
||||
|
||||
mapper.partialUpdate(updateDTO, entity);
|
||||
entity.setPassword(passwordEncoder.encode(updateDTO.password()));
|
||||
entity = repo.save(entity);
|
||||
return mapper.toDTO(entity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
package com.hitcommunications.servermanager.utils;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class CookieUtils {
|
||||
|
||||
private CookieUtils() {
|
||||
}
|
||||
|
||||
public static String getCookieValue(HttpServletRequest request, String name) {
|
||||
if (request.getCookies() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<Cookie> cookie = Arrays.stream(request.getCookies())
|
||||
.filter(c -> name.equals(c.getName()))
|
||||
.findFirst();
|
||||
|
||||
return cookie.map(Cookie::getValue).orElse(null);
|
||||
}
|
||||
|
||||
public static ResponseCookie buildCookie(String name, String value, Duration maxAge, boolean httpOnly, boolean secure) {
|
||||
return ResponseCookie.from(name, value)
|
||||
.httpOnly(httpOnly)
|
||||
.secure(secure)
|
||||
.path("/")
|
||||
.sameSite("Lax")
|
||||
.maxAge(maxAge)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -22,9 +22,3 @@ spring:
|
|||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
format_sql: true
|
||||
default_schema: server-manager
|
||||
|
||||
security:
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:change-me-change-me-change-me-change-me-change-me}
|
||||
access-token-expiration: PT30M
|
||||
refresh-token-expiration: P30D
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
INSERT INTO "server-manager".tab_users (
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
first_name,
|
||||
last_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'default',
|
||||
'default@hittelco.com',
|
||||
'$2b$10$4hvMSZp/AC0k9HXqxtdO2e0KUQReRgcnJEcY5gWYTPiDMgu2D4bSK',
|
||||
'Default',
|
||||
'User',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Example env file for Vite
|
||||
# Copy to .env and set your backend URL
|
||||
VITE_BACKEND_URL=http://localhost:8080
|
||||
|
|
@ -1,36 +1,73 @@
|
|||
# Frontend - Servers Manager
|
||||
# React + TypeScript + Vite
|
||||
|
||||
## Stack
|
||||
- React 19, React Router 7
|
||||
- Axios (withCredentials)
|
||||
- Tailwind (config in `tailwind.config.js`)
|
||||
- react-hot-toast para feedback
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
## Configuração
|
||||
Crie `.env` a partir de `.env.example`:
|
||||
```
|
||||
VITE_BACKEND_URL=http://localhost:8080
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
## Rodar
|
||||
```bash
|
||||
yarn install # ou npm install
|
||||
yarn dev # ou npm run dev
|
||||
```
|
||||
Acesse `http://localhost:5173`.
|
||||
|
||||
## Fluxo de login
|
||||
- Página `/login` envia `POST /api/auth/login` com email/senha.
|
||||
- Tokens vêm em cookies (`access_token` e `refresh_token` HttpOnly). Axios usa `withCredentials`.
|
||||
- Após sucesso, redireciona para `/` (ajuste rota/dashboard conforme evoluir).
|
||||
|
||||
## Estrutura
|
||||
- `src/Api.ts`: cliente axios com `withCredentials`.
|
||||
- `src/pages/Login.tsx`: tela de login e toasts.
|
||||
- `src/components/Layout.tsx`: layout base.
|
||||
|
||||
## Build/Lint
|
||||
```bash
|
||||
yarn build
|
||||
yarn lint
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export default defineConfig([
|
|||
'react/jsx-indent': ['error', 4],
|
||||
'react/jsx-indent-props': ['error', 4],
|
||||
'semi': ['error', 'always'],
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import axios from 'axios'
|
||||
|
||||
const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || 'http://localhost:8080';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export const setAuthToken = (token?: string) => {
|
||||
if (token) {
|
||||
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
} else {
|
||||
delete api.defaults.headers.common['Authorization'];
|
||||
}
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import { Login } from './pages/Login';
|
||||
import { Dashboard } from './pages/Dashboard';
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<>
|
||||
<App />
|
||||
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
||||
</>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export const Dashboard = () => {
|
||||
return <div>Dashboard Page</div>;
|
||||
};
|
||||
|
|
@ -1,20 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Layout } from "../components/Layout";
|
||||
import type { LoginProps } from "../types/Login";
|
||||
import { Eye, EyeOff } from "lucide-icons-react";
|
||||
import api from "../Api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const Login = () => {
|
||||
const [form, setForm] = useState<LoginProps>({
|
||||
email: "",
|
||||
password: ""
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({
|
||||
|
|
@ -23,36 +15,15 @@ export const Login = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleShowPassword = () => {
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await api.post("/api/auth/login", {
|
||||
username: form.email,
|
||||
password: form.password
|
||||
});
|
||||
navigate("/");
|
||||
toast.success("Login realizado com sucesso!");
|
||||
} catch (err: any) {
|
||||
const message = err?.response?.data?.message || "Falha ao autenticar. Verifique as credenciais.";
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
console.log(form);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<Layout className={Styles.layout}>
|
||||
<img src="/logo.webp " alt="Logo" className={Styles.logo} />
|
||||
<div className={Styles.card}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form>
|
||||
<div>
|
||||
<label htmlFor="email" className={Styles.label}>Email:</label>
|
||||
<input
|
||||
|
|
@ -64,10 +35,10 @@ export const Login = () => {
|
|||
className={Styles.input}
|
||||
onChange={handleChange}/>
|
||||
</div>
|
||||
<div className={Styles.inputWrapper}>
|
||||
<div>
|
||||
<label htmlFor="password" className={Styles.label}>Password:</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="********"
|
||||
|
|
@ -75,19 +46,8 @@ export const Login = () => {
|
|||
className={Styles.input}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
className={Styles.iconButton}
|
||||
onClick={handleShowPassword}
|
||||
>
|
||||
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
||||
<button type="submit" disabled={loading} className={Styles.button}>
|
||||
{loading ? "Autenticando..." : "Login"}
|
||||
</button>
|
||||
<button type="submit" className={Styles.button}>Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
@ -95,12 +55,10 @@ export const Login = () => {
|
|||
};
|
||||
|
||||
const Styles = {
|
||||
layout: "h-screen flex flex-col gap-4 justify-center animate-fade-up",
|
||||
layout: "h-screen flex flex-col gap-4 justify-center",
|
||||
logo: "w-36 h-36 mx-auto mb-4",
|
||||
card: "w-96 p-8 shadow-lg rounded-lg border border-cardBorder bg-card",
|
||||
label: "block mb-2 text-md font-medium text-text",
|
||||
inputWrapper: "relative",
|
||||
input: "bg-gray-50 border border-cardBorder text-text text-md rounded-lg outline-none block w-full p-2.5",
|
||||
iconButton: "absolute right-3 top-1/2 text-text p-1 focus:outline-none",
|
||||
button: "w-full bg-accent p-2 rounded-md mt-4 text-white font-bold text-lg hover:bg-hover transition duration-150",
|
||||
};
|
||||
|
|
@ -15,15 +15,6 @@ export default {
|
|||
hover: '#D04A0F',
|
||||
accent: '#E95A1B',
|
||||
},
|
||||
keyframes: {
|
||||
'fade-up': {
|
||||
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'fade-up': 'fade-up 300ms ease-out forwards',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
|
|||
Loading…
Reference in New Issue