Extended security for web front end and added first rest resources

This commit is contained in:
verboomp
2026-01-21 14:08:50 +01:00
parent 47ee7c3c25
commit d2e6f5164a
29 changed files with 983 additions and 39 deletions

View File

@@ -54,6 +54,24 @@
<version>2.5.2.Final</version>
<scope>provided</scope>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT Library -->
<!-- Apache POI -->

View File

@@ -20,10 +20,12 @@ import jakarta.persistence.*;
@Entity
@Table(name = "customer")
@NamedQuery(name = Customer.FIND_ALL, query = "select c from Customer c order by c.name")
@NamedQuery(name = Customer.FIND_BY_NUMBER, query = "select c from Customer c where c.customerNumber = :cutomerNumber")
public class Customer extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "customer_seq";
public static final String FIND_ALL = "Customer.findAll";
public static final String FIND_BY_NUMBER = "Customer.findByNumber";
public static final String PARAM_NUMBER = "cutomerNumber";
@@ -39,8 +41,7 @@ public class Customer extends AbstractDateEntity {
@Column(name = "name", nullable = false)
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "customer_id_fk")
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Picture> pictures = new HashSet<>();
public Long getCustomerId() {

View File

@@ -0,0 +1,191 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Entity
@Table(name = "jwt_refresh_token")
@NamedQuery(name = JwtRefreshToken.FIND_BY_HASH_REVOKE_NULL, query = "SELECT t FROM JwtRefreshToken t WHERE t.tokenHash = :hash AND t.revokedAt IS NULL")
@NamedQuery(name = JwtRefreshToken.FIND_BY_HASH, query = "SELECT t FROM JwtRefreshToken t WHERE t.tokenHash = :hash")
@NamedQuery(name = JwtRefreshToken.REVOKE_ALL_USER, query = "UPDATE JwtRefreshToken t SET t.revokedAt = :date WHERE t.user.userId = :userId AND t.revokedAt IS NULL")
public class JwtRefreshToken extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "jwt_refresh_token_seq";
public static final String FIND_BY_HASH_REVOKE_NULL = "JwtRefreshToken.forHashRevokeNull";
public static final String FIND_BY_HASH = "JwtRefreshToken.forHash";
public static final String REVOKE_ALL_USER = "JwtRefreshToken.revokeAllUser";
public static final String PARAM_HASH = "hash";
public static final String PARAM_USER_ID = "userId";
public static final String PARAM_DATE = "date";
@Id
@Column(name = "jwt_refresh_token_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long jwsRefreshTokenId;
@Column(name = "token_hash", nullable = false)
private String tokenHash;
@Column(name = "device_info", nullable = false)
private String deviceInfo;
@Column(name = "ip_address", nullable = false)
private String ipAddress;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "issued_at", nullable = false)
@JsonIgnore
private Date issuedAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "expires_at", nullable = false)
@JsonIgnore
private Date expiresAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "revoked_at", nullable = false)
@JsonIgnore
private Date revokedAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "last_used_at", nullable = false)
@JsonIgnore
private Date lastUsedAt;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id_fk", nullable = true)
private User user;
public Long getJwsRefreshTokenId() {
return jwsRefreshTokenId;
}
public void setJwsRefreshTokenId(Long jwsRefreshTokenId) {
this.jwsRefreshTokenId = jwsRefreshTokenId;
}
public String getTokenHash() {
return tokenHash;
}
public void setTokenHash(String tokenHash) {
this.tokenHash = tokenHash;
}
public String getDeviceInfo() {
return deviceInfo;
}
public void setDeviceInfo(String deviceInfo) {
this.deviceInfo = deviceInfo;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public Date getIssuedAt() {
return issuedAt;
}
public void setIssuedAt(Date issuedAt) {
this.issuedAt = issuedAt;
}
public Date getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Date expiresAt) {
this.expiresAt = expiresAt;
}
public Date getRevokedAt() {
return revokedAt;
}
public void setRevokedAt(Date revokedAt) {
this.revokedAt = revokedAt;
}
public Date getLastUsedAt() {
return lastUsedAt;
}
public void setLastUsedAt(Date lastUsedAt) {
this.lastUsedAt = lastUsedAt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public static class Builder {
private JwtRefreshToken instance = new JwtRefreshToken();
public Builder tokenHash(String tokenHash) {
instance.setTokenHash(tokenHash);
return this;
}
public Builder deviceInfo(String deviceInfo) {
instance.setDeviceInfo(deviceInfo);
return this;
}
public Builder ipAddress(String ipAddress) {
instance.setIpAddress(ipAddress);
return this;
}
public Builder expiresAt(Date expiresAt) {
instance.setExpiresAt(expiresAt);
return this;
}
public Builder issuedAt(Date issuedAt) {
instance.setIssuedAt(issuedAt);
return this;
}
public Builder revokedAt(Date revokedAt) {
instance.setRevokedAt(revokedAt);
return this;
}
public Builder lastUsedAt(Date lastUsedAt) {
instance.setLastUsedAt(lastUsedAt);
return this;
}
public Builder user(User user) {
instance.setUser(user);
return this;
}
public JwtRefreshToken build() {
return instance;
}
}
}

View File

@@ -37,11 +37,17 @@ public class Picture extends AbstractDateEntity {
@Column(name = "picture_date", nullable = false)
private Date pictureDate;
@Basic(fetch = FetchType.LAZY)
private String comment;
@Column(name = "image")
@Basic(fetch = FetchType.LAZY)
private String image;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id_fk")
private Customer customer;
public Long getPictureId() {
return pictureId;
}
@@ -82,6 +88,14 @@ public class Picture extends AbstractDateEntity {
this.image = image;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(pictureId).toHashCode();
@@ -117,6 +131,11 @@ public class Picture extends AbstractDateEntity {
instance.setImage(image);
return this;
}
public Builder customer(Customer customer) {
instance.setCustomer(customer);
return this;
}
public Picture build() {
return instance;

View File

@@ -20,9 +20,12 @@ import jakarta.persistence.*;
@Entity
@Table(name = "x_user")
@NamedQuery(name = User.BY_USERNAME, query = "select u from User u where u.username like :username")
public class User extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "user_seq";
public static final String BY_USERNAME = "user.byUsername";
public static final String PARAM_USERNAME = "username";
@Id
@Column(name = "user_id", length = 22)

View File

@@ -0,0 +1,35 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import jakarta.annotation.Resource;
import jakarta.ejb.EJB;
import jakarta.ejb.EJBContext;
import jakarta.ejb.SessionContext;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
public abstract class AbstractService {
@Resource
protected EJBContext ejbContext;
@PersistenceContext
protected EntityManager entityManager;
@Resource
protected SessionContext sessionContext;
@EJB
protected QueryService queryService;
}

View File

@@ -1,16 +1,15 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import java.util.List;
import java.util.Optional;
import jakarta.ejb.EJB;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
/**
@@ -24,25 +23,26 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
*/
@Stateless
@LocalBean
public class CustomerPictureService {
@PersistenceContext
private EntityManager entityManager;
@EJB
private QueryService queryService;
@PermitAll
public class CustomerPictureService extends AbstractService {
public boolean addCustomerPicture(CustomerPictureValue customerPictureValue) {
Optional<Customer> customerOpt = queryService.callNamedQuerySingleResult(Customer.FIND_BY_NUMBER, new Param(Customer.PARAM_NUMBER, customerPictureValue.customerNumber()));
Customer customer = customerOpt.orElseGet(() -> new Customer.Builder().customerNumber(customerPictureValue.customerNumber()).name(customerPictureValue.pharmacyName()).build());
Picture picture = new Picture.Builder().username(customerPictureValue.username()).comment(customerPictureValue.comment()).image(customerPictureValue.base64String()).pictureDate(customerPictureValue.date()).build();
customer = entityManager.merge(customer);
Picture picture = new Picture.Builder().customer(customer).username(customerPictureValue.username()).comment(customerPictureValue.comment()).image(customerPictureValue.base64String()).pictureDate(customerPictureValue.date()).build();
customer.getPictures().add(picture);
entityManager.persist(picture);
entityManager.merge(customer);
entityManager.flush();
return true;
}
public List<CustomerListValue> getAll(String query) {
// FIXME: do query
List<Customer> customers = queryService.callNamedQueryList(Customer.FIND_ALL);
customers.forEach(c -> c.getPictures().size());
return customers.parallelStream().map(c -> CustomerListValue.builder(c)).toList();
}
}

View File

@@ -0,0 +1,101 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import static marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;
import java.util.Optional;
import io.jsonwebtoken.Claims;
import jakarta.inject.Inject;
import marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken;
import marketing.heyday.hartmann.fotodocumentation.core.model.Right;
import marketing.heyday.hartmann.fotodocumentation.core.model.User;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.utils.JwtTokenUtil;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
public class JwtTokenService extends AbstractService {
private static final Long EXPIRED_DELTA = 30L * 24 * 60 * 60 * 1000;
@Inject
private JwtTokenUtil jwtTokenUtil;
public TokenPairValue generateTokenPair(User user, String deviceInfo, String ipAddress) {
String accessToken = jwtTokenUtil.generateAccessToken(
user.getUserId(),
user.getUsername(),
user.getRights().stream().map(Right::getCode).toList());
String refreshToken = jwtTokenUtil.generateRefreshToken(user.getUserId());
// Store refresh token (optional - for revocation support)
var refreshTokenEntity = new JwtRefreshToken.Builder()
.user(user)
.tokenHash(hashToken(refreshToken))
.deviceInfo(deviceInfo)
.ipAddress(ipAddress)
.issuedAt(new Date())
.expiresAt(new Date(System.currentTimeMillis() + EXPIRED_DELTA)).build();
entityManager.persist(refreshTokenEntity);
return new TokenPairValue(accessToken, refreshToken);
}
public String refreshAccessToken(String refreshToken) throws Exception {
Claims claims = jwtTokenUtil.validateAndExtractClaims(refreshToken);
Long userId = Long.parseLong(claims.getSubject());
// Verify refresh token exists and not revoked
String tokenHash = hashToken(refreshToken);
Optional<JwtRefreshToken> tokenEntityOpt = queryService.callNamedQuerySingleResult(FIND_BY_HASH_REVOKE_NULL, new Param(PARAM_HASH, tokenHash));
if (tokenEntityOpt.isEmpty()) {
// FIXME: do error handling
}
var tokenEntity = tokenEntityOpt.get();
tokenEntity.setLastUsedAt(new Date());
entityManager.merge(tokenEntity);
User user = entityManager.find(User.class, userId);
return jwtTokenUtil.generateAccessToken(user.getUserId(), user.getUsername(), user.getRights().stream().map(Right::getCode).toList());
}
public void revokeRefreshToken(String refreshToken) {
String tokenHash = hashToken(refreshToken);
Optional<JwtRefreshToken> tokenEntityOpt = queryService.callNamedQuerySingleResult(FIND_BY_HASH, new Param(PARAM_HASH, tokenHash));
if (tokenEntityOpt.isPresent()) {
JwtRefreshToken tokenEntity = tokenEntityOpt.get();
tokenEntity.setRevokedAt(new Date());
entityManager.merge(tokenEntity);
}
}
public void revokeAllUserTokens(Long userId) {
queryService.callNamedQueryUpdate(REVOKE_ALL_USER, new Param(PARAM_DATE, new Date()), new Param(PARAM_USER_ID, userId));
}
private String hashToken(String token) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,47 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import java.util.Optional;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import marketing.heyday.hartmann.fotodocumentation.core.model.User;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Stateless
@LocalBean
public class LoginService extends AbstractService{
@Inject
private JwtTokenService jwtTokenService;
public TokenPairValue authenticateUser(String username, String deviceInfo, String ipAddress) {
// Get logged-in user from database
Optional<User> userOpt = queryService.callNamedQuerySingleResult(User.BY_USERNAME, new Param(User.PARAM_USERNAME, username));
if (userOpt.isEmpty()) {
// FIXME: implement me
}
User user = userOpt.get();
// Verify user is active
if (!user.isActive()) {
throw new IllegalArgumentException("User account is inactive");
}
TokenPairValue tokens = jwtTokenService.generateTokenPair(user, deviceInfo, ipAddress);
// Logout from the temporary login (we're using tokens now, not session)
return tokens;
}
}

View File

@@ -0,0 +1,124 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.PostConstruct;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Stateless
@LocalBean
public class JwtTokenUtil {
private static final Log LOG = LogFactory.getLog(JwtTokenUtil.class);
private static final long ACCESS_TOKEN_VALIDITY = 60 * 60 * 1000L; // 1 hour
private static final long REFRESH_TOKEN_VALIDITY = 30 * 24 * 60 * 60 * 1000L; // 30 days
private static final long TEMP_2FA_TOKEN_VALIDITY = 5 * 60 * 1000L; // 5 minutes
private static final String ISSUER = "skillmatrix-jwt-issuer";
private static final String AUDIENCE = "skillmatrix-api";
private PrivateKey privateKey;
private PublicKey publicKey;
@PostConstruct
public void init() {
// Load key from wildfly system property
try {
String pem = System.getProperty("jwt.secret.key");
pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(pem);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
LOG.error("Failed to load JWT PrivateKey " + e.getMessage(), e);
}
}
public String generateAccessToken(Long userId, String username, List<String> groups) {
return Jwts.builder()
.issuer(ISSUER)
.audience().add(AUDIENCE).and()
.subject(userId.toString())
.claim("username", username)
.claim("type", "access")
.claim("groups", groups)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
}
public String generateRefreshToken(Long userId) {
return Jwts.builder()
.issuer(ISSUER)
.audience().add(AUDIENCE).and()
.subject(userId.toString())
.claim("type", "refresh")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_VALIDITY))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
}
public String generateTemp2FAToken(Long userId) {
return Jwts.builder()
.issuer(ISSUER)
.audience().add(AUDIENCE).and()
.subject(userId.toString())
.claim("type", "temp_2fa")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + TEMP_2FA_TOKEN_VALIDITY))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
}
public Claims validateAndExtractClaims(String token) {
return Jwts.parser()
.verifyWith(publicKey) // FIXME: not working need public key that we currently didn't load
.build()
.parseUnsecuredClaims(token)
.getPayload();
}
public Long extractUserId(String token) {
Claims claims = validateAndExtractClaims(token);
return Long.parseLong(claims.getSubject());
}
public boolean isTokenExpired(String token) {
try {
Claims claims = validateAndExtractClaims(token);
return claims.getExpiration().before(new Date());
} catch (JwtException e) {
LOG.warn("Failed to get expiration date from token " + e.getMessage());
return true;
}
}
}

View File

@@ -43,11 +43,14 @@ public class LoginUtils {
private Optional<SecurityIdentity> authenticate(String username, String password) {
try {
LOG.error("Login with username: " + username + " password: " + password);
Principal principal = new NamePrincipal(username);
PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray());
SecurityDomain sd = SecurityDomain.getCurrent();
return Optional.ofNullable(sd.authenticate(principal, evidence));
SecurityIdentity identity = sd.authenticate(principal, evidence);
LOG.error("Login identity: " + identity);
return Optional.ofNullable(identity);
} catch (RealmUnavailableException | SecurityException e) {
LOG.warn("Failed to authenticate user " + e.getMessage(), e);
return Optional.empty();

View File

@@ -58,11 +58,7 @@ public class CustomerPictureResource {
return Response.status(Status.UNAUTHORIZED).build();
}
boolean success = customerPictureService.addCustomerPicture(customerPictureValue);
return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
}
}

View File

@@ -0,0 +1,53 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("customer")
public class CustomerResource {
private static final Log LOG = LogFactory.getLog(CustomerResource.class);
@EJB
private CustomerPictureService customerPictureService;
@GZIP
@GET
@Path("")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Get customer list")
@ApiResponse(responseCode = "200", description = "Successfully retrieved customer list", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = CustomerListValue.class))))
public Response doAddCustomerPicture(@QueryParam("query") String query) {
LOG.debug("Query customers for query " + query);
var retVal = customerPictureService.getAll(query);
return Response.ok().entity(retVal).build();
}
}

View File

@@ -0,0 +1,75 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wildfly.security.auth.server.SecurityIdentity;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import marketing.heyday.hartmann.fotodocumentation.core.service.LoginService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.LoginUtils;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@RequestScoped
@Path("login")
@PermitAll
public class LoginResource {
private static final Log LOG = LogFactory.getLog(LoginResource.class);
@EJB
private LoginService loginService;
@Inject
private LoginUtils loginUtils;
@GET
@Path("/")
@Produces(JSON_OUT)
@Operation(summary = "Get logged in user")
@ApiResponse(responseCode = "200", description = "Successfully retrieved logged in user", content = @Content(mediaType = JSON_OUT, schema = @Schema(implementation = TokenPairValue.class)))
@ApiResponse(responseCode = "500", description = "Internal server error")
public Response doLogin(@Context HttpServletRequest httpServletRequest) {
Optional<SecurityIdentity> identity = loginUtils.authenticate(httpServletRequest);
if (identity.isEmpty()) {
LOG.debug("identity empty login invalid");
return Response.status(401).build();
}
String username = identity.get().getPrincipal().getName();
LOG.debug("Login valid returning jwt");
String deviceInfo = loginUtils.extractDeviceInfo(httpServletRequest);
String ipAddress = loginUtils.extractIpAddress(httpServletRequest);
TokenPairValue tokenPairValue = loginService.authenticateUser(username, deviceInfo, ipAddress);
//FIXME: check if we can do a logout to free user from WildFly httpServletRequest.logout();
return Response.ok(tokenPairValue).build();
}
}

View File

@@ -15,6 +15,8 @@ import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.MediaType;
import marketing.heyday.hartmann.fotodocumentation.rest.CustomerPictureResource;
import marketing.heyday.hartmann.fotodocumentation.rest.CustomerResource;
import marketing.heyday.hartmann.fotodocumentation.rest.LoginResource;
import marketing.heyday.hartmann.fotodocumentation.rest.MonitoringResource;
/**
@@ -42,9 +44,10 @@ public class ApplicationConfigApi extends Application {
Set<Class<?>> retVal = new HashSet<>();
retVal.add(OpenApiResource.class);
retVal.add(ValidatedMessageBodyReader.class);
//retVal.add(AuthenticateFilter.class);
retVal.add(LoginResource.class);
retVal.add(MonitoringResource.class);
retVal.add(CustomerPictureResource.class);
retVal.add(CustomerResource.class);
LOG.info("returning rest api classes " + retVal);
return retVal;
}

View File

@@ -0,0 +1,23 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
public record CustomerListValue(String name, String customerNumber, int amountOfPicture) {
public static CustomerListValue builder(Customer customer) {
if (customer == null) {
return null;
}
return new CustomerListValue(customer.getName(), customer.getCustomerNumber(), customer.getPictures().size());
}
}

View File

@@ -0,0 +1,23 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import io.swagger.v3.oas.annotations.media.Schema;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 21 Jan 2026
*/
@Schema(name = "TokenPair")
public record TokenPairValue(String accessToken, String refreshToken) {
@Override
public String toString() {
return "TokenPair{" + "accessToken='" + (accessToken != null ? "[REDACTED]" : "null") + '\'' + ", refreshToken='" + (refreshToken != null ? "[REDACTED]" : "null") + '\'' + '}';
}
}

View File

@@ -11,6 +11,7 @@
<class>marketing.heyday.hartmann.fotodocumentation.core.model.User</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Customer</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Picture</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken</class>
<properties>
<property name="hibernate.format_sql" value="false" />

View File

@@ -42,6 +42,28 @@ create table user_to_right (
);
-- jwt_refresh_token
CREATE SEQUENCE jwt_refresh_token_seq START 1 INCREMENT 1;
CREATE TABLE jwt_refresh_token (
jwt_refresh_token_id BIGINT PRIMARY KEY,
token_hash VARCHAR(255) NOT NULL, -- SHA-256 hash of refresh token
device_info VARCHAR(255), -- Browser/device identifier
ip_address VARCHAR(45),
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
last_used_at TIMESTAMP,
user_id_fk BIGINT NOT NULL REFERENCES x_user(user_id),
UNIQUE (token_hash)
);
CREATE INDEX idx_jwt_refresh_token_user ON jwt_refresh_token(user_id_fk);
CREATE INDEX idx_jwt_refresh_token_expires ON jwt_refresh_token(expires_at);
-- customer
create sequence customer_seq start 25;

View File

@@ -39,11 +39,6 @@ public class SecurityGenerator {
}
public byte[] createPassword(String password, String salt) throws NoSuchAlgorithmException {
byte[] saltBytes = salt.getBytes(Charset.forName("utf-8"));
return createPassword(password, saltBytes);
}
public byte[] createPassword(String password, byte[] salt) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] passwordBytes = password.getBytes(Charset.forName("utf-8"));
@@ -66,8 +61,8 @@ public class SecurityGenerator {
byte[] salt = createSalt();
String saltHash = encode(salt);
byte[] passwordByte = createPassword(password, salt);
String passwordHash = encode(passwordByte);
byte[] digest = createPassword(password, salt);
String passwordHash = encode(digest);
System.out.println("Password " + password);
System.out.println("PasswordHash " + passwordHash);