diff --git a/hartmann-foto-documentation-app/pom.xml b/hartmann-foto-documentation-app/pom.xml index 3d765e4..eb68343 100644 --- a/hartmann-foto-documentation-app/pom.xml +++ b/hartmann-foto-documentation-app/pom.xml @@ -54,6 +54,24 @@ 2.5.2.Final provided + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java index c9d5a6b..36cb010 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java @@ -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 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