pictures = new HashSet<>();
public Long getCustomerId() {
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/JwtRefreshToken.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/JwtRefreshToken.java
new file mode 100644
index 0000000..f03d2a1
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/JwtRefreshToken.java
@@ -0,0 +1,191 @@
+package marketing.heyday.hartmann.fotodocumentation.core.model;
+
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import jakarta.persistence.*;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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;
+ }
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Picture.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Picture.java
index 81fbb3f..6750a73 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Picture.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Picture.java
@@ -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;
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/User.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/User.java
index 7b2b82d..fbfc5a7 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/User.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/User.java
@@ -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)
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java
new file mode 100644
index 0000000..0a1d7f7
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java
@@ -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;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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;
+
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java
index ab85205..5b19906 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java
@@ -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 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 getAll(String query) {
+ // FIXME: do query
+ List customers = queryService.callNamedQueryList(Customer.FIND_ALL);
+ customers.forEach(c -> c.getPictures().size());
+ return customers.parallelStream().map(c -> CustomerListValue.builder(c)).toList();
+ }
}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/JwtTokenService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/JwtTokenService.java
new file mode 100644
index 0000000..7ebf3c8
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/JwtTokenService.java
@@ -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;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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 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 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);
+ }
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/LoginService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/LoginService.java
new file mode 100644
index 0000000..1090303
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/LoginService.java
@@ -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;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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 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;
+
+ }
+
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java
new file mode 100644
index 0000000..9256f87
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java
@@ -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;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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 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;
+ }
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java
index 2f44e14..bf73385 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java
@@ -43,11 +43,14 @@ public class LoginUtils {
private Optional 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();
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResource.java
index 3128ab5..1901472 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResource.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResource.java
@@ -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();
-
}
-
}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResource.java
new file mode 100644
index 0000000..e41fc9f
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResource.java
@@ -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;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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();
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java
new file mode 100644
index 0000000..e456f28
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java
@@ -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;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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 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();
+ }
+
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java
index 0daffd7..174aaaf 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java
@@ -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> 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;
}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/CustomerListValue.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/CustomerListValue.java
new file mode 100644
index 0000000..6ebabf4
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/CustomerListValue.java
@@ -0,0 +1,23 @@
+package marketing.heyday.hartmann.fotodocumentation.rest.vo;
+
+import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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());
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/TokenPairValue.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/TokenPairValue.java
new file mode 100644
index 0000000..874bf29
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/TokenPairValue.java
@@ -0,0 +1,23 @@
+package marketing.heyday.hartmann.fotodocumentation.rest.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @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") + '\'' + '}';
+ }
+}
+
diff --git a/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml b/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml
index 06ffc00..17c2443 100644
--- a/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml
+++ b/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml
@@ -11,6 +11,7 @@
marketing.heyday.hartmann.fotodocumentation.core.model.User
marketing.heyday.hartmann.fotodocumentation.core.model.Customer
marketing.heyday.hartmann.fotodocumentation.core.model.Picture
+ marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken
diff --git a/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V1__init.sql b/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V1__init.sql
index cc70a32..afd1cd0 100644
--- a/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V1__init.sql
+++ b/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V1__init.sql
@@ -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;
diff --git a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/SecurityGenerator.java b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/SecurityGenerator.java
index c78b3b0..fc15e71 100644
--- a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/SecurityGenerator.java
+++ b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/SecurityGenerator.java
@@ -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);
diff --git a/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war b/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war
index 61d5722..267279b 100644
Binary files a/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war and b/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war differ
diff --git a/hartmann-foto-documentation-docker/src/main/docker/standalone-fotodocumentation.xml b/hartmann-foto-documentation-docker/src/main/docker/standalone-fotodocumentation.xml
index 6144f70..7baca11 100644
--- a/hartmann-foto-documentation-docker/src/main/docker/standalone-fotodocumentation.xml
+++ b/hartmann-foto-documentation-docker/src/main/docker/standalone-fotodocumentation.xml
@@ -40,6 +40,34 @@
+
@@ -299,9 +327,27 @@
+
+
+
-
+
+
+
+
+
+
@@ -356,7 +402,7 @@
-
+
diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/AbstractRestTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/AbstractRestTest.java
index ff8022a..e661c65 100644
--- a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/AbstractRestTest.java
+++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/AbstractRestTest.java
@@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.*;
import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -29,6 +31,8 @@ public abstract class AbstractRestTest extends AbstractTest {
private static final Log LOG = LogFactory.getLog(AbstractRestTest.class);
public static final String TEXT_PLAIN = "text/plain";
+ private static Map bearerToken = new HashMap<>();
+
public HttpResponse executeRequest(Request request) throws IOException {
var executor = Executor.newInstance();
return executor.use(new BasicCookieStore()).execute(request).returnResponse();
@@ -39,6 +43,23 @@ public abstract class AbstractRestTest extends AbstractTest {
}
protected String getAuthorization(String user, String pass) {
+
+ if (!bearerToken.containsKey(user)) {
+ String auth = user + ":" + pass;
+ String encoded = Base64.getEncoder().encodeToString(auth.getBytes());
+ String authorization = "Basic " + encoded;
+
+ bearerToken.put(user, getBearerToken(authorization));
+ }
+
+ return "Bearer " + bearerToken.getOrDefault(user, "");
+ }
+
+ protected String getBasicHeader() {
+ return getBasicHeader(username, password);
+ }
+
+ protected String getBasicHeader(String user, String pass) {
String auth = user + ":" + pass;
String encoded = Base64.getEncoder().encodeToString(auth.getBytes());
return "Basic " + encoded;
diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResourceTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResourceTest.java
index 74d8ec1..5621083 100644
--- a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResourceTest.java
+++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerPictureResourceTest.java
@@ -40,22 +40,24 @@ public class CustomerPictureResourceTest extends AbstractRestTest {
@Order(1)
public void doAddCustomerPicture() throws IOException {
LOG.info("doAddCustomerPicture");
-
+
+ String authorization = getBasicHeader();
+ LOG.info("authorization: " + authorization);
String path = deploymentURL + PATH;
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
- .addHeader("Authorization", getAuthorization())
+ .addHeader("Authorization", authorization)
.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
HttpResponse httpResponse = executeRequest(request);
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(200, code);
}
-
+
@Test
@Order(2)
public void doAddCustomerPictureNoAuth() throws IOException {
- LOG.info("doAddCustomerPicture");
-
+ LOG.info("doAddCustomerPictureNoAuth");
+
String path = deploymentURL + PATH;
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
@@ -64,4 +66,14 @@ public class CustomerPictureResourceTest extends AbstractRestTest {
int code = httpResponse.getStatusLine().getStatusCode();
assertEquals(401, code);
}
+
+ public static void main(String[] args) throws IOException {
+
+ var test = new CustomerPictureResourceTest();
+ test.deploymentURL = "http://localhost:8080/";
+ test.username = "adm";
+ test.password = "x1t0e7Pb49";
+
+ test.doAddCustomerPicture();
+ }
}
diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java
new file mode 100644
index 0000000..580f1e1
--- /dev/null
+++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java
@@ -0,0 +1,60 @@
+package marketing.heyday.hartmann.fotodocumentation.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.ContentType;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 14 Nov 2024
+ */
+@TestMethodOrder(OrderAnnotation.class)
+public class CustomerResourceTest extends AbstractRestTest {
+ private static final Log LOG = LogFactory.getLog(CustomerResourceTest.class);
+ private static final String PATH = "api/customer";
+ private static final String BASE_UPLOAD = "src/test/resources/upload/";
+ private static final String BASE_DOWNLOAD = "json/CustomerResourceTest-";
+
+ @BeforeAll
+ public static void init() {
+ initDB();
+ }
+
+ @Test
+ @Order(1)
+ public void doGetAll() throws IOException {
+ LOG.info("doGetAll");
+
+ String authorization = getAuthorization();
+ LOG.info("authorization: " + authorization);
+ String path = deploymentURL + PATH;
+ Request request = Request.Get(path).addHeader("Accept", "application/json; charset=utf-8")
+ .addHeader("Authorization", authorization);
+
+ HttpResponse httpResponse = executeRequest(request);
+ int code = httpResponse.getStatusLine().getStatusCode();
+ assertEquals(200, code);
+
+
+ String text = getResponseText(httpResponse, "doGetAll");
+ String expected = fileToString(BASE_DOWNLOAD + "doGetAll.json");
+ jsonAssert(expected, text);
+ }
+}
diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResourceTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResourceTest.java
new file mode 100644
index 0000000..fe0ab51
--- /dev/null
+++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResourceTest.java
@@ -0,0 +1,42 @@
+package marketing.heyday.hartmann.fotodocumentation.rest;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 21 Jan 2026
+ */
+
+@TestMethodOrder(OrderAnnotation.class)
+public class LoginResourceTest extends AbstractRestTest {
+ private static final Log LOG = LogFactory.getLog(LoginResourceTest.class);
+
+ @Test
+ @Order(2)
+ public void doTestLogin() {
+ LOG.info("doTestLogin");
+
+ String token = getBasicHeader();
+ assertNotNull(token);
+ }
+
+ public static void main(String[] args) {
+
+ var test = new LoginResourceTest();
+ test.deploymentURL = "http://localhost:8080/";
+ String token = test.getAuthorization("hartmann", "nvlev4YnTi");
+ System.out.println(token);
+ }
+}
diff --git a/hartmann-foto-documentation-docker/src/test/resources/datasets/dataset.xml b/hartmann-foto-documentation-docker/src/test/resources/datasets/dataset.xml
index 2f2aad4..526ac15 100644
--- a/hartmann-foto-documentation-docker/src/test/resources/datasets/dataset.xml
+++ b/hartmann-foto-documentation-docker/src/test/resources/datasets/dataset.xml
@@ -11,4 +11,17 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json b/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json
new file mode 100644
index 0000000..b5aab6b
--- /dev/null
+++ b/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json
@@ -0,0 +1,17 @@
+[
+ {
+ "name": "Meier Apotheke",
+ "customerNumber": "2345",
+ "amountOfPicture": 1
+ },
+ {
+ "name": "Müller Apotheke",
+ "customerNumber": "1234",
+ "amountOfPicture": 2
+ },
+ {
+ "name": "Schmidt Apotheke",
+ "customerNumber": "3456",
+ "amountOfPicture": 2
+ }
+]
\ No newline at end of file
diff --git a/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml b/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml
index 48df189..5c7886d 100644
--- a/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml
+++ b/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml
@@ -4,7 +4,7 @@
Secure
- /api/login
+ /api/customer
GET
POST
@@ -17,7 +17,7 @@
- BASIC
+ BEARER_TOKEN
fotoDocumentationRealm