feat: shot-crafter-calculator with H2 persistence and production history
Quarkus 3.20.1 monolith serving React 18 + TypeScript + Tailwind SPA. Features: - Three-tab calculator (Insumos, Fórmulas, Calculadora) for Soulshot, Spiritshot and Blessed Spiritshot crafting in Lineage 2 Interlude/Clásico with all 15 grades and pre-loaded recipes - Real-time profitability computation (cristales → ore → crafteos → shots → cost → sale → ganancia) - Multi-user auth with JWT in httpOnly cookie (bcrypt + RSA 2048) - H2 file-based persistence in ./data/shots.mv.db (file-based, H2) - Auto-save on state changes (debounced 500ms) - Production history with stats (total/avg/best/worst/last5avg) and per-run detail modal with snapshot of insumos+formulas Stack: - Backend: Quarkus REST + Hibernate ORM Panache + smallrye-jwt - Frontend: React 18 + TypeScript + Vite + Tailwind 3 - Build: Maven runs frontend-maven-plugin (Node 22 + npm ci) then copies dist to META-INF/resources for Quarkus to serve Verified: - 5 backend endpoints + 5 history endpoints with curl - 35/35 browser tests via Playwright + Chromium - All TS strict, all builds green
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package com.l2.shots.auth;
|
||||
|
||||
import io.quarkus.security.Authenticated;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.NewCookie;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Path("/api/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class AuthResource {
|
||||
|
||||
@Inject
|
||||
AuthService authService;
|
||||
|
||||
@Inject
|
||||
JwtCookieAuth jwtCookieAuth;
|
||||
|
||||
@ConfigProperty(name = "app.auth.cookie-name")
|
||||
String cookieName;
|
||||
|
||||
@ConfigProperty(name = "app.auth.cookie-max-age-seconds")
|
||||
int cookieMaxAge;
|
||||
|
||||
@POST
|
||||
@Path("/register")
|
||||
public Response register(Credentials creds) {
|
||||
Optional<User> result = authService.register(creds.username, creds.password);
|
||||
if (result.isEmpty()) {
|
||||
return Response.status(409)
|
||||
.entity(new ErrorBody("username no disponible o datos inválidos"))
|
||||
.build();
|
||||
}
|
||||
User user = result.get();
|
||||
String token = authService.buildToken(user.id);
|
||||
return Response.ok(AuthService.toAuthMe(user))
|
||||
.cookie(buildAuthCookie(token))
|
||||
.build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/login")
|
||||
public Response login(Credentials creds) {
|
||||
Optional<User> result = authService.authenticate(creds.username, creds.password);
|
||||
if (result.isEmpty()) {
|
||||
return Response.status(401)
|
||||
.entity(new ErrorBody("credenciales inválidas"))
|
||||
.build();
|
||||
}
|
||||
User user = result.get();
|
||||
String token = authService.buildToken(user.id);
|
||||
return Response.ok(AuthService.toAuthMe(user))
|
||||
.cookie(buildAuthCookie(token))
|
||||
.build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/logout")
|
||||
public Response logout() {
|
||||
return Response.noContent()
|
||||
.cookie(clearAuthCookie())
|
||||
.build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/me")
|
||||
public Response me(@Context HttpHeaders headers) {
|
||||
Optional<JsonWebToken> jwt = jwtCookieAuth.extractToken(headers);
|
||||
if (jwt.isEmpty()) {
|
||||
return Response.status(401).build();
|
||||
}
|
||||
return authService.getUserFromToken(jwt.get())
|
||||
.map(u -> Response.ok(u).build())
|
||||
.orElse(Response.status(401).build());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/check")
|
||||
@Authenticated
|
||||
public Response check() {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
private NewCookie buildAuthCookie(String token) {
|
||||
return new NewCookie.Builder(cookieName)
|
||||
.value(token)
|
||||
.path("/")
|
||||
.httpOnly(true)
|
||||
.secure(false)
|
||||
.sameSite(NewCookie.SameSite.LAX)
|
||||
.maxAge(cookieMaxAge)
|
||||
.build();
|
||||
}
|
||||
|
||||
private NewCookie clearAuthCookie() {
|
||||
return new NewCookie.Builder(cookieName)
|
||||
.value("")
|
||||
.path("/")
|
||||
.httpOnly(true)
|
||||
.secure(false)
|
||||
.sameSite(NewCookie.SameSite.LAX)
|
||||
.maxAge(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static class ErrorBody {
|
||||
public String error;
|
||||
public ErrorBody() {}
|
||||
public ErrorBody(String error) { this.error = error; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user