Add membership feature

This commit is contained in:
andreas.dinauer 2025-09-14 10:20:05 +02:00
parent 23955b33df
commit f1751250a1
33 changed files with 870 additions and 40 deletions

47
Jenkinsfile vendored Executable file
View File

@ -0,0 +1,47 @@
pipeline {
agent any
stages {
stage('Set Image Name') {
steps {
script {
env.TAG = "${env.BUILD_NUMBER}"
env.REFERENCE = "harbor.dinauer.dev/tavolio/iam-backend"
env.IMAGE = "${env.REFERENCE}:${env.BUILD_NUMBER}";
}
}
}
stage('Build Quarkus application') {
steps {
script {
sh './gradlew build'
}
}
}
stage('Build Docker Image') {
steps {
script {
sh "docker build --no-cache -t ${env.IMAGE} -f src/main/docker/Dockerfile.jvm ."
}
}
}
stage('Push Image to Docker Hub') {
steps {
script {
withCredentials([usernamePassword(credentialsId: 'harbor', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
sh 'echo ${PASSWORD} | docker login harbor.dinauer.dev -u ${USERNAME} --password-stdin'
sh "docker push ${env.IMAGE}"
sh "docker logout"
}
}
}
}
stage('Remove image from host') {
steps {
script {
sh "docker image rm --force ${env.IMAGE}"
}
}
}
}
}

View File

@ -63,6 +63,14 @@
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId> <artifactId>quarkus-hibernate-validator</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId> <artifactId>quarkus-junit5</artifactId>

View File

@ -0,0 +1,93 @@
package de.tavolio;
import de.tavolio.account.AccountEntity;
import de.tavolio.account.AccountRepo;
import io.quarkus.security.UnauthorizedException;
import io.vertx.core.http.HttpServerRequest;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.security.Principal;
import java.util.Base64;
@ApplicationScoped
public class AuthenticationService
{
@Inject
Logger LOG;
@Inject
AccountRepo accountRepo;
@Inject
SecurityContext securityContext;
@Inject
HttpServerRequest request;
@ConfigProperty(name = "iam.user.name")
String superuserName;
@ConfigProperty(name = "iam.user.password")
String superuserPassword;
public boolean isSuperUser()
{
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && !authHeader.isBlank())
{
String[] sections = authHeader.split("\\s+");
if (sections.length == 2 && sections[0].equals("Basic"))
{
String value = new String(Base64.getDecoder().decode(sections[1]));
String[] parts = value.split(":");
if (parts.length == 2)
{
String username = parts[0];
String password = parts[1];
boolean isSuperuser = username.equals(superuserName) && password.equals(superuserPassword);
if (isSuperuser)
{
return true;
}
{
LOG.errorf("Invalid username or password", getRequestPath());
return false;
}
}
else
{
LOG.errorf("Invalid base64 credentials %s", getRequestPath());
}
}
}
return false;
}
public AccountEntity requireUser()
{
Principal principal = securityContext.getUserPrincipal();
if(principal != null)
{
AccountEntity accountEntity = accountRepo.findById(principal.getName());
if(accountEntity != null)
{
return accountEntity;
}
LOG.warnf("No account found for request %s", getRequestPath());
throw new NotFoundException();
}
LOG.warnf("Unauthorized request %s", getRequestPath());
throw new UnauthorizedException();
}
private String getRequestPath()
{
return String.format("[%s, %s]", request.method().name(), request.path());
}
}

View File

@ -1,10 +1,10 @@
package de.tavolio.account; package de.tavolio.account;
import de.tavolio.member.MembershipEntity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Entity; import jakarta.persistence.*;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@ -22,6 +22,12 @@ public class AccountEntity extends PanacheEntityBase
private String password; private String password;
@Enumerated(EnumType.STRING)
private AccountStatus status;
@OneToMany(mappedBy = "account")
private Set<MembershipEntity> memberships;
public static AccountEntity init() public static AccountEntity init()
{ {
return new AccountEntity().setId(UUID.randomUUID().toString()); return new AccountEntity().setId(UUID.randomUUID().toString());
@ -81,4 +87,26 @@ public class AccountEntity extends PanacheEntityBase
this.password = password; this.password = password;
return this; return this;
} }
public Set<MembershipEntity> getMemberships()
{
return memberships;
}
public AccountEntity setMemberships(Set<MembershipEntity> memberships)
{
this.memberships = memberships;
return this;
}
public AccountStatus getStatus()
{
return status;
}
public AccountEntity setStatus(AccountStatus status)
{
this.status = status;
return this;
}
} }

View File

@ -2,13 +2,12 @@ package de.tavolio.account;
import de.tavolio.account.dto.Account; import de.tavolio.account.dto.Account;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.ApplicationPath;
@ApplicationScoped @ApplicationScoped
public class AccountMapper public class AccountMapper
{ {
public Account map(AccountEntity accountEntity) public Account map(AccountEntity accountEntity)
{ {
return new Account(accountEntity.getId()); return new Account(accountEntity.getId(), accountEntity.getFirstname(), accountEntity.getLastname(), accountEntity.getEmail(), accountEntity.getStatus());
} }
} }

View File

@ -1,9 +1,16 @@
package de.tavolio.account; package de.tavolio.account;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import java.util.Optional;
@ApplicationScoped @ApplicationScoped
public class AccountRepo implements PanacheRepositoryBase<AccountEntity, String> public class AccountRepo implements PanacheRepositoryBase<AccountEntity, String>
{ {
public Optional<AccountEntity> findOptionalByEmail(String email)
{
return find("email = :email", Parameters.with("email", email)).firstResultOptional();
}
} }

View File

@ -7,8 +7,8 @@ import jakarta.validation.Valid;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.NotImplementedYet;
@Path("/accounts") @Path("/accounts")
public class AccountResource public class AccountResource
@ -28,8 +28,9 @@ public class AccountResource
} }
@GET @GET
public Account get() @Path("/{id}")
public Account get(@PathParam("id") String id)
{ {
throw new NotImplementedYet(); return accountService.getUser(id);
} }
} }

View File

@ -1,21 +1,30 @@
package de.tavolio.account; package de.tavolio.account;
import de.tavolio.AuthenticationService;
import de.tavolio.account.dto.Account; import de.tavolio.account.dto.Account;
import de.tavolio.account.dto.AccountCreation; import de.tavolio.account.dto.AccountCreation;
import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.security.UnauthorizedException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;
@ApplicationScoped @ApplicationScoped
public class AccountService public class AccountService
{ {
@Inject
Logger LOG;
@Inject @Inject
AccountRepo accountRepo; AccountRepo accountRepo;
@Inject @Inject
AccountMapper accountMapper; AccountMapper accountMapper;
@Inject
AuthenticationService authenticationService;
@Transactional @Transactional
public Account create(AccountCreation account) public Account create(AccountCreation account)
{ {
@ -23,8 +32,21 @@ public class AccountService
accountEntity.setEmail(account.email()) accountEntity.setEmail(account.email())
.setFirstname(account.firstname()) .setFirstname(account.firstname())
.setLastname(account.lastname()) .setLastname(account.lastname())
.setPassword(BcryptUtil.bcryptHash(account.password())); .setPassword(BcryptUtil.bcryptHash(account.password()))
.setStatus(AccountStatus.INIT);
accountRepo.persist(accountEntity); accountRepo.persist(accountEntity);
return accountMapper.map(accountEntity); return accountMapper.map(accountEntity);
} }
public Account getUser(String id)
{
AccountEntity account = authenticationService.requireUser();
AccountEntity requestedAccount = accountRepo.findById(id);
if (requestedAccount != null && requestedAccount.getId().equals(account.getId()))
{
return accountMapper.map(authenticationService.requireUser());
}
LOG.errorf("Cannot access account");
throw new UnauthorizedException();
}
} }

View File

@ -0,0 +1,6 @@
package de.tavolio.account;
public enum AccountStatus
{
INIT, REGISTERED
}

View File

@ -1,5 +1,7 @@
package de.tavolio.account.dto; package de.tavolio.account.dto;
public record Account(String id) import de.tavolio.account.AccountStatus;
public record Account(String id, String firstname, String lastname, String email, AccountStatus status)
{ {
} }

View File

@ -0,0 +1,33 @@
package de.tavolio.member;
import de.tavolio.AuthenticationService;
import de.tavolio.account.AccountEntity;
import de.tavolio.member.dto.AccountMemberships;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@ApplicationScoped
@Path("/memberships")
public class AccountMembershipResource
{
@Inject
MembershipRepo membershipRepo;
@Inject
AuthenticationService authenticationService;
@Inject
MembershipMapper membershipMapper;
@GET
public AccountMemberships get()
{
AccountEntity account = authenticationService.requireUser();
return new AccountMemberships(
membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(TenantType.ORGANISATION, account)),
membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(TenantType.RESTAURANT, account))
);
}
}

View File

@ -0,0 +1,105 @@
package de.tavolio.member;
import de.tavolio.account.AccountEntity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import java.time.ZonedDateTime;
import java.util.UUID;
@Entity
@Table(name = "membership")
public class MembershipEntity extends PanacheEntityBase
{
@Id
private String id;
@Column(name = "tenant_type")
@Enumerated(EnumType.STRING)
private TenantType tenantType;
@Column(name = "tenant_id")
private String tenantId;
@Column(name = "member_role")
@Enumerated(EnumType.STRING)
private MembershipRole role;
@Column(name = "member_since")
private ZonedDateTime memberSince;
@ManyToOne
@JoinColumn(name = "account_id")
private AccountEntity account;
public static MembershipEntity init()
{
return new MembershipEntity().setId(UUID.randomUUID().toString());
}
public String getId()
{
return id;
}
public MembershipEntity setId(String id)
{
this.id = id;
return this;
}
public TenantType getTenantType()
{
return tenantType;
}
public MembershipEntity setTenantType(TenantType tenantType)
{
this.tenantType = tenantType;
return this;
}
public String getTenantId()
{
return tenantId;
}
public MembershipEntity setTenantId(String tenantId)
{
this.tenantId = tenantId;
return this;
}
public MembershipRole getRole()
{
return role;
}
public MembershipEntity setRole(MembershipRole role)
{
this.role = role;
return this;
}
public ZonedDateTime getMemberSince()
{
return memberSince;
}
public MembershipEntity setMemberSince(ZonedDateTime memberSince)
{
this.memberSince = memberSince;
return this;
}
public AccountEntity getAccount()
{
return account;
}
public MembershipEntity setAccount(AccountEntity account)
{
this.account = account;
return this;
}
}

View File

@ -0,0 +1,25 @@
package de.tavolio.member;
import de.tavolio.account.AccountMapper;
import de.tavolio.member.dto.Membership;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
@ApplicationScoped
public class MembershipMapper
{
@Inject
AccountMapper accountMapper;
public Membership map(MembershipEntity membership)
{
return new Membership(membership.getId(), membership.getTenantType(), membership.getTenantId(), membership.getRole(), accountMapper.map(membership.getAccount()));
}
public List<Membership> map(List<MembershipEntity> memberships)
{
return memberships.stream().map(this::map).toList();
}
}

View File

@ -0,0 +1,22 @@
package de.tavolio.member;
import de.tavolio.account.AccountEntity;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
@ApplicationScoped
public class MembershipRepo implements PanacheRepositoryBase<MembershipEntity, String>
{
public List<MembershipEntity> findByTenantTypeAndAccount(TenantType tenantType, AccountEntity account)
{
return list("tenantType = :tenantType AND account = :account", Parameters.with("tenantType", tenantType).and("account", account));
}
public List<MembershipEntity> findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
{
return list("tenantType = :tenantType AND tenantId = :tenantId", Parameters.with("tenantType", tenantType).and("tenantId", tenantId));
}
}

View File

@ -0,0 +1,6 @@
package de.tavolio.member;
public enum MembershipRole
{
OWNER, ADMIN, MEMBER
}

View File

@ -0,0 +1,87 @@
package de.tavolio.member;
import de.tavolio.AuthenticationService;
import de.tavolio.account.AccountEntity;
import de.tavolio.account.AccountRepo;
import de.tavolio.account.AccountStatus;
import de.tavolio.member.dto.Membership;
import de.tavolio.member.dto.MembershipCreation;
import io.quarkus.security.UnauthorizedException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BadRequestException;
import org.jboss.logging.Logger;
import java.time.ZonedDateTime;
import java.util.List;
@ApplicationScoped
public class MembershipService
{
@Inject
Logger LOG;
@Inject
AuthenticationService authenticationService;
@Inject
MembershipRepo membershipRepo;
@Inject
MembershipMapper membershipMapper;
@Inject
AccountRepo accountRepo;
@Transactional
public Membership create(TenantType tenantType, String tenantId, MembershipCreation membershipCreation)
{
switch (tenantType)
{
case ORGANISATION ->
{
if (membershipCreation.role().equals(MembershipRole.OWNER))
{
if (authenticationService.isSuperUser())
{
AccountEntity account = accountRepo.findById(membershipCreation.accountId());
MembershipEntity membership = MembershipEntity.init();
membership.setAccount(account);
membership.setRole(membershipCreation.role());
membership.setTenantType(TenantType.ORGANISATION);
membership.setTenantId(tenantId);
membership.setMemberSince(ZonedDateTime.now());
membershipRepo.persist(membership);
account.setStatus(AccountStatus.REGISTERED);
accountRepo.persist(account);
return membershipMapper.map(membership);
}
LOG.errorf("Membership with role 'Owner' cannot be created without superuser permissions");
throw new UnauthorizedException();
}
}
case RESTAURANT ->
{
}
default ->
{
}
}
throw new BadRequestException();
}
public List<Membership> findByTenantType(TenantType tenantType)
{
return membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(tenantType, authenticationService.requireUser()));
}
public List<Membership> findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
{
return membershipMapper.map(membershipRepo.findByTenantTypeAndTenantId(tenantType, tenantId));
}
}

View File

@ -0,0 +1,80 @@
package de.tavolio.member;
import de.tavolio.member.dto.Membership;
import de.tavolio.member.dto.MembershipCreation;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.NotImplementedYet;
import java.util.List;
@Path("/{tenant-type}")
public class TenantMembershipResource
{
@Inject
Logger LOG;
@Inject
MembershipService membershipService;
@POST
@Path("/{tenant-id}/memberships")
public Membership post(@PathParam("tenant-type") String tenantType, @PathParam("tenant-id") String tenantId, MembershipCreation membershipCreation)
{
switch (tenantType)
{
case "organisations" ->
{
Membership membership = membershipService.create(TenantType.ORGANISATION, tenantId, membershipCreation);
LOG.infof("Created membership for organisation %s", tenantId);
return membership;
}
case "restaurants" ->
{
Membership membership = membershipService.create(TenantType.RESTAURANT, tenantId, membershipCreation);
LOG.infof("Created membership for restaurant %s", tenantId);
return membership;
}
}
throw new BadRequestException();
}
@GET
@Path("/{tenant-id}/memberships")
public List<Membership> get(@PathParam("tenant-type") String tenantType, @PathParam("tenant-id") String tenantId)
{
switch (tenantType)
{
case "organisations" ->
{
return membershipService.findByTenantTypeAndTenantId(TenantType.ORGANISATION, tenantId);
}
case "restaurants" ->
{
throw new NotImplementedYet();
}
}
LOG.errorf("Unknown tenant type %s", tenantType);
throw new BadRequestException();
}
@GET
@Path("/memberships")
public List<Membership> get(@PathParam("tenant-type") String tenantType)
{
switch (tenantType)
{
case "organisations" ->
{
return membershipService.findByTenantType(TenantType.ORGANISATION);
}
case "restaurants" ->
{
return membershipService.findByTenantType(TenantType.RESTAURANT);
}
}
LOG.errorf("Unknown tenant type %s", tenantType);
throw new BadRequestException();
}
}

View File

@ -0,0 +1,6 @@
package de.tavolio.member;
public enum TenantType
{
ORGANISATION, RESTAURANT
}

View File

@ -0,0 +1,7 @@
package de.tavolio.member.dto;
import java.util.List;
public record AccountMemberships(List<Membership> organisations, List<Membership> restaurants)
{
}

View File

@ -0,0 +1,9 @@
package de.tavolio.member.dto;
import de.tavolio.account.dto.Account;
import de.tavolio.member.MembershipRole;
import de.tavolio.member.TenantType;
public record Membership(String id, TenantType tenantType, String tenantId, MembershipRole role, Account account)
{
}

View File

@ -0,0 +1,7 @@
package de.tavolio.member.dto;
import de.tavolio.member.MembershipRole;
public record MembershipCreation(String accountId, MembershipRole role)
{
}

View File

@ -1,5 +0,0 @@
package de.tavolio.organisation.member;
public class OrganisationMemberResource
{
}

View File

@ -1,5 +0,0 @@
package de.tavolio.organisation.member;
public class OrganisationMembershipRepo
{
}

View File

@ -1,5 +0,0 @@
package de.tavolio.restaurant.member;
public class OrganisationMemberResource
{
}

View File

@ -1,5 +0,0 @@
package de.tavolio.restaurant.member;
public class RestaurantMembershipRepo
{
}

View File

@ -1,15 +1,26 @@
package de.tavolio.session; package de.tavolio.session;
import de.tavolio.session.dto.SessionCreation;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.common.NotImplementedYet; import org.jboss.logging.Logger;
@Path("/sessions") @Path("/sessions")
public class SessionResource public class SessionResource
{ {
@Inject
Logger LOG;
@Inject
SessionService sessionService;
@POST @POST
public String get() public String get(@Valid SessionCreation sessionCreation)
{ {
throw new NotImplementedYet(); String token = sessionService.generateBySessionCreation(sessionCreation);
LOG.infof("Generated token for email %s", sessionCreation.email());
return token;
} }
} }

View File

@ -0,0 +1,39 @@
package de.tavolio.session;
import de.tavolio.account.AccountEntity;
import de.tavolio.account.AccountRepo;
import de.tavolio.session.dto.SessionCreation;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.smallrye.jwt.build.Jwt;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.time.ZonedDateTime;
import java.util.Optional;
@ApplicationScoped
public class SessionService
{
@Inject
AccountRepo accountRepo;
public String generateBySessionCreation(SessionCreation sessionCreation)
{
Optional<AccountEntity> accountEntityOptional = accountRepo.findOptionalByEmail(sessionCreation.email());
if (accountEntityOptional.isPresent())
{
AccountEntity accountEntity = accountEntityOptional.get();
if (BcryptUtil.matches(sessionCreation.password(), accountEntity.getPassword()))
{
return generateToken(accountEntity.getId());
}
}
throw new NotFoundException();
}
private String generateToken(String upn)
{
return Jwt.upn(upn).expiresAt(ZonedDateTime.now().plusYears(1).toInstant()).issuer("https://tavolio.de").sign();
}
}

View File

@ -0,0 +1,10 @@
package de.tavolio.session.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record SessionCreation(
@Email String email,
@NotBlank String password)
{
}

View File

@ -5,6 +5,14 @@ quarkus.http.test-port=9089
quarkus.hibernate-orm.schema-management.strategy=drop-and-create quarkus.hibernate-orm.schema-management.strategy=drop-and-create
smallrye.jwt.sign.key.location=private.key
mp.jwt.verify.publickey.location=public.crt
mp.jwt.verify.issuer=https://tavolio.de
%dev.quarkus.datasource.username=postgres %dev.quarkus.datasource.username=postgres
%dev.quarkus.datasource.password=${DB_PASSWORD} %dev.quarkus.datasource.password=${DB_PASSWORD}
%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=iam %dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=iam
# IAM Superuser
%dev.iam.user.name=tavolio
%dev.iam.user.password=tavolio

View File

@ -3,4 +3,10 @@
-- insert into myentity (id, field) values(1, 'field-1'); -- insert into myentity (id, field) values(1, 'field-1');
-- insert into myentity (id, field) values(2, 'field-2'); -- insert into myentity (id, field) values(2, 'field-2');
-- insert into myentity (id, field) values(3, 'field-3'); -- insert into myentity (id, field) values(3, 'field-3');
-- alter sequence myentity_seq restart with 4; -- alter sequence myentity_seq restart with 4;
INSERT INTO account (id, firstname, lastname, email, password, status)
VALUES ('66b261fe-4c5a-4728-9857-67717f02d4e1', 'Andreas', 'Dinauer', 'andreas.j.dinauer@gmail.com', '$2a$12$cdrzIY4sMFAXiz29uo9Ul.MPy0RN0FGS2yjVzb5BTe6bSijn4eGQy', 'REGISTERED');
INSERT INTO membership(id, tenant_type, tenant_id, member_role, member_since, account_id)
VALUES ('cd20d271-6e76-48c4-9cfb-a67f8892d46c', 'ORGANISATION', 'b3912be6-7503-4a13-b8ab-5d65af036742', 'OWNER', '2025-08-20 15:30:12.123456+02', '66b261fe-4c5a-4728-9857-67717f02d4e1');

View File

@ -1,23 +1,34 @@
package de.tavolio.account; package de.tavolio.account;
import io.quarkus.deployment.dev.testing.TestConfig; import de.tavolio.utils.Database;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured; import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import io.restassured.response.Response;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals; import static io.restassured.RestAssured.when;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest @QuarkusTest
public class AccountResourceTest public class AccountResourceTest
{ {
@Inject
Database database;
@Inject @Inject
AccountRepo accountRepo; AccountRepo accountRepo;
@AfterEach
void afterEach()
{
database.clear();
}
@Test @Test
void testInsert() void testInsert()
{ {
@ -40,4 +51,35 @@ public class AccountResourceTest
assertEquals(1, accountRepo.count()); assertEquals(1, accountRepo.count());
} }
@Test
@TestSecurity(user = "66609092-6c98-4466-af52-9a3e9d633108")
void testRetrieval()
{
// given
database.setup(() -> {
AccountEntity account = new AccountEntity()
.setId("66609092-6c98-4466-af52-9a3e9d633108")
.setFirstname("Andreas")
.setLastname("Dinauer")
.setEmail("andreas.j.dinauer@gmail.com")
.setPassword(BcryptUtil.bcryptHash("pw"));
accountRepo.persist(account);
});
// when
Response response = when()
.get("accounts")
.then()
.extract()
.response();
// then
String body = response.getBody().prettyPrint();
assertFalse(body.isEmpty());
assertTrue(body.contains("66609092-6c98-4466-af52-9a3e9d633108"));
assertTrue(body.contains("Andreas"));
assertTrue(body.contains("Dinauer"));
assertTrue(body.contains("andreas.j.dinauer@gmail.com"));
}
} }

View File

@ -0,0 +1,67 @@
package de.tavolio.session;
import de.tavolio.account.AccountEntity;
import de.tavolio.account.AccountRepo;
import de.tavolio.utils.Database;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import jakarta.inject.Inject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@QuarkusTest
public class SessionResourceTest
{
@Inject
Database database;
@Inject
AccountRepo accountRepo;
@AfterEach
void afterEach()
{
database.clear();
}
@Test
void testGet()
{
// given
database.setup(() -> {
AccountEntity accountEntity = AccountEntity.init()
.setEmail("andreas.j.dinauer@gmail.com")
.setFirstname("Andreas")
.setLastname("Dinauer")
.setPassword(BcryptUtil.bcryptHash("pw"));
accountRepo.persist(accountEntity);
});
String loginRequest = """
{
"email": "andreas.j.dinauer@gmail.com",
"password": "pw"
}
""";
// when
Response response = given()
.contentType(ContentType.JSON)
.body(loginRequest)
.when()
.post("/sessions")
.then()
.extract()
.response();
// then
assertEquals(200, response.statusCode());
assertTrue(response.getBody().prettyPrint().startsWith("ey"));
}
}

View File

@ -0,0 +1,67 @@
package de.tavolio.utils;
import io.agroal.api.AgroalDataSource;
import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;
@ApplicationScoped
public class Database {
@Inject
AgroalDataSource dataSource;
public void setup(Runnable setup) {
QuarkusTransaction.begin();
setup.run();
QuarkusTransaction.commit();
}
public void clear() {
forceForeignKeys(false);
getAllTables().forEach(this::truncateTable);
forceForeignKeys(true);
}
private List<String> getAllTables() {
try(Connection connection = dataSource.getConnection()) {
ResultSet rs = connection.getMetaData().getTables(null, "public", "%", new String[] {"TABLE"});
connection.close();
List<String> tables = new LinkedList<>();
while(rs.next()) {
tables.add(rs.getString(3));
}
return tables;
} catch (SQLException e) {
throw new RuntimeException();
}
}
private void forceForeignKeys(boolean force) {
if(force) {
execute("SET session_replication_role = replica");
} else {
execute("SET session_replication_role = DEFAULT");
}
}
private void truncateTable(String table) {
execute("TRUNCATE TABLE public." + table + " RESTART IDENTITY CASCADE");
}
private void execute(String sql) {
try(Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
statement.execute(sql);
} catch (SQLException e) {
System.out.println(sql + ": Error executing SQL.");
}
}
}