diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100755
index 0000000..18e73b4
--- /dev/null
+++ b/Jenkinsfile
@@ -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}"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3ddec8c..9105a2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,14 @@
io.quarkus
quarkus-hibernate-validator
+
+ io.quarkus
+ quarkus-smallrye-jwt
+
+
+ io.quarkus
+ quarkus-smallrye-jwt-build
+
io.quarkus
quarkus-junit5
diff --git a/src/main/java/de/tavolio/AuthenticationService.java b/src/main/java/de/tavolio/AuthenticationService.java
new file mode 100644
index 0000000..57a893d
--- /dev/null
+++ b/src/main/java/de/tavolio/AuthenticationService.java
@@ -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());
+ }
+}
diff --git a/src/main/java/de/tavolio/account/AccountEntity.java b/src/main/java/de/tavolio/account/AccountEntity.java
index aa2c979..c061939 100644
--- a/src/main/java/de/tavolio/account/AccountEntity.java
+++ b/src/main/java/de/tavolio/account/AccountEntity.java
@@ -1,10 +1,10 @@
package de.tavolio.account;
+import de.tavolio.member.MembershipEntity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
+import jakarta.persistence.*;
+import java.util.Set;
import java.util.UUID;
@Entity
@@ -22,6 +22,12 @@ public class AccountEntity extends PanacheEntityBase
private String password;
+ @Enumerated(EnumType.STRING)
+ private AccountStatus status;
+
+ @OneToMany(mappedBy = "account")
+ private Set memberships;
+
public static AccountEntity init()
{
return new AccountEntity().setId(UUID.randomUUID().toString());
@@ -81,4 +87,26 @@ public class AccountEntity extends PanacheEntityBase
this.password = password;
return this;
}
+
+ public Set getMemberships()
+ {
+ return memberships;
+ }
+
+ public AccountEntity setMemberships(Set memberships)
+ {
+ this.memberships = memberships;
+ return this;
+ }
+
+ public AccountStatus getStatus()
+ {
+ return status;
+ }
+
+ public AccountEntity setStatus(AccountStatus status)
+ {
+ this.status = status;
+ return this;
+ }
}
diff --git a/src/main/java/de/tavolio/account/AccountMapper.java b/src/main/java/de/tavolio/account/AccountMapper.java
index e8a2b29..bf8c58c 100644
--- a/src/main/java/de/tavolio/account/AccountMapper.java
+++ b/src/main/java/de/tavolio/account/AccountMapper.java
@@ -2,13 +2,12 @@ package de.tavolio.account;
import de.tavolio.account.dto.Account;
import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.ws.rs.ApplicationPath;
@ApplicationScoped
public class AccountMapper
{
public Account map(AccountEntity accountEntity)
{
- return new Account(accountEntity.getId());
+ return new Account(accountEntity.getId(), accountEntity.getFirstname(), accountEntity.getLastname(), accountEntity.getEmail(), accountEntity.getStatus());
}
}
diff --git a/src/main/java/de/tavolio/account/AccountRepo.java b/src/main/java/de/tavolio/account/AccountRepo.java
index a172cc9..3580a12 100644
--- a/src/main/java/de/tavolio/account/AccountRepo.java
+++ b/src/main/java/de/tavolio/account/AccountRepo.java
@@ -1,9 +1,16 @@
package de.tavolio.account;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import io.quarkus.panache.common.Parameters;
import jakarta.enterprise.context.ApplicationScoped;
+import java.util.Optional;
+
@ApplicationScoped
public class AccountRepo implements PanacheRepositoryBase
{
+ public Optional findOptionalByEmail(String email)
+ {
+ return find("email = :email", Parameters.with("email", email)).firstResultOptional();
+ }
}
diff --git a/src/main/java/de/tavolio/account/AccountResource.java b/src/main/java/de/tavolio/account/AccountResource.java
index 0fd1484..3cb9b27 100644
--- a/src/main/java/de/tavolio/account/AccountResource.java
+++ b/src/main/java/de/tavolio/account/AccountResource.java
@@ -7,8 +7,8 @@ import jakarta.validation.Valid;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
import org.jboss.logging.Logger;
-import org.jboss.resteasy.reactive.common.NotImplementedYet;
@Path("/accounts")
public class AccountResource
@@ -28,8 +28,9 @@ public class AccountResource
}
@GET
- public Account get()
+ @Path("/{id}")
+ public Account get(@PathParam("id") String id)
{
- throw new NotImplementedYet();
+ return accountService.getUser(id);
}
}
diff --git a/src/main/java/de/tavolio/account/AccountService.java b/src/main/java/de/tavolio/account/AccountService.java
index d5db232..61577d5 100644
--- a/src/main/java/de/tavolio/account/AccountService.java
+++ b/src/main/java/de/tavolio/account/AccountService.java
@@ -1,21 +1,30 @@
package de.tavolio.account;
+import de.tavolio.AuthenticationService;
import de.tavolio.account.dto.Account;
import de.tavolio.account.dto.AccountCreation;
import io.quarkus.elytron.security.common.BcryptUtil;
+import io.quarkus.security.UnauthorizedException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
+import org.jboss.logging.Logger;
@ApplicationScoped
public class AccountService
{
+ @Inject
+ Logger LOG;
+
@Inject
AccountRepo accountRepo;
@Inject
AccountMapper accountMapper;
+ @Inject
+ AuthenticationService authenticationService;
+
@Transactional
public Account create(AccountCreation account)
{
@@ -23,8 +32,21 @@ public class AccountService
accountEntity.setEmail(account.email())
.setFirstname(account.firstname())
.setLastname(account.lastname())
- .setPassword(BcryptUtil.bcryptHash(account.password()));
+ .setPassword(BcryptUtil.bcryptHash(account.password()))
+ .setStatus(AccountStatus.INIT);
accountRepo.persist(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();
+ }
}
diff --git a/src/main/java/de/tavolio/account/AccountStatus.java b/src/main/java/de/tavolio/account/AccountStatus.java
new file mode 100644
index 0000000..ec92f05
--- /dev/null
+++ b/src/main/java/de/tavolio/account/AccountStatus.java
@@ -0,0 +1,6 @@
+package de.tavolio.account;
+
+public enum AccountStatus
+{
+ INIT, REGISTERED
+}
diff --git a/src/main/java/de/tavolio/account/dto/Account.java b/src/main/java/de/tavolio/account/dto/Account.java
index 9d9ba1f..1398fb2 100644
--- a/src/main/java/de/tavolio/account/dto/Account.java
+++ b/src/main/java/de/tavolio/account/dto/Account.java
@@ -1,5 +1,7 @@
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)
{
}
diff --git a/src/main/java/de/tavolio/member/AccountMembershipResource.java b/src/main/java/de/tavolio/member/AccountMembershipResource.java
new file mode 100644
index 0000000..00d942e
--- /dev/null
+++ b/src/main/java/de/tavolio/member/AccountMembershipResource.java
@@ -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))
+ );
+ }
+}
diff --git a/src/main/java/de/tavolio/member/MembershipEntity.java b/src/main/java/de/tavolio/member/MembershipEntity.java
new file mode 100644
index 0000000..dc0df40
--- /dev/null
+++ b/src/main/java/de/tavolio/member/MembershipEntity.java
@@ -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;
+ }
+}
diff --git a/src/main/java/de/tavolio/member/MembershipMapper.java b/src/main/java/de/tavolio/member/MembershipMapper.java
new file mode 100644
index 0000000..7071e29
--- /dev/null
+++ b/src/main/java/de/tavolio/member/MembershipMapper.java
@@ -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 map(List memberships)
+ {
+ return memberships.stream().map(this::map).toList();
+ }
+}
diff --git a/src/main/java/de/tavolio/member/MembershipRepo.java b/src/main/java/de/tavolio/member/MembershipRepo.java
new file mode 100644
index 0000000..4143778
--- /dev/null
+++ b/src/main/java/de/tavolio/member/MembershipRepo.java
@@ -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
+{
+ public List findByTenantTypeAndAccount(TenantType tenantType, AccountEntity account)
+ {
+ return list("tenantType = :tenantType AND account = :account", Parameters.with("tenantType", tenantType).and("account", account));
+ }
+
+ public List findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
+ {
+ return list("tenantType = :tenantType AND tenantId = :tenantId", Parameters.with("tenantType", tenantType).and("tenantId", tenantId));
+ }
+}
diff --git a/src/main/java/de/tavolio/member/MembershipRole.java b/src/main/java/de/tavolio/member/MembershipRole.java
new file mode 100644
index 0000000..c872438
--- /dev/null
+++ b/src/main/java/de/tavolio/member/MembershipRole.java
@@ -0,0 +1,6 @@
+package de.tavolio.member;
+
+public enum MembershipRole
+{
+ OWNER, ADMIN, MEMBER
+}
diff --git a/src/main/java/de/tavolio/member/MembershipService.java b/src/main/java/de/tavolio/member/MembershipService.java
new file mode 100644
index 0000000..58fe509
--- /dev/null
+++ b/src/main/java/de/tavolio/member/MembershipService.java
@@ -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 findByTenantType(TenantType tenantType)
+ {
+ return membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(tenantType, authenticationService.requireUser()));
+ }
+
+ public List findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
+ {
+ return membershipMapper.map(membershipRepo.findByTenantTypeAndTenantId(tenantType, tenantId));
+ }
+}
diff --git a/src/main/java/de/tavolio/member/TenantMembershipResource.java b/src/main/java/de/tavolio/member/TenantMembershipResource.java
new file mode 100644
index 0000000..3d887e9
--- /dev/null
+++ b/src/main/java/de/tavolio/member/TenantMembershipResource.java
@@ -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 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 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();
+ }
+}
diff --git a/src/main/java/de/tavolio/member/TenantType.java b/src/main/java/de/tavolio/member/TenantType.java
new file mode 100644
index 0000000..6f226b9
--- /dev/null
+++ b/src/main/java/de/tavolio/member/TenantType.java
@@ -0,0 +1,6 @@
+package de.tavolio.member;
+
+public enum TenantType
+{
+ ORGANISATION, RESTAURANT
+}
diff --git a/src/main/java/de/tavolio/member/dto/AccountMemberships.java b/src/main/java/de/tavolio/member/dto/AccountMemberships.java
new file mode 100644
index 0000000..00d1308
--- /dev/null
+++ b/src/main/java/de/tavolio/member/dto/AccountMemberships.java
@@ -0,0 +1,7 @@
+package de.tavolio.member.dto;
+
+import java.util.List;
+
+public record AccountMemberships(List organisations, List restaurants)
+{
+}
diff --git a/src/main/java/de/tavolio/member/dto/Membership.java b/src/main/java/de/tavolio/member/dto/Membership.java
new file mode 100644
index 0000000..e064db7
--- /dev/null
+++ b/src/main/java/de/tavolio/member/dto/Membership.java
@@ -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)
+{
+}
diff --git a/src/main/java/de/tavolio/member/dto/MembershipCreation.java b/src/main/java/de/tavolio/member/dto/MembershipCreation.java
new file mode 100644
index 0000000..a321d8e
--- /dev/null
+++ b/src/main/java/de/tavolio/member/dto/MembershipCreation.java
@@ -0,0 +1,7 @@
+package de.tavolio.member.dto;
+
+import de.tavolio.member.MembershipRole;
+
+public record MembershipCreation(String accountId, MembershipRole role)
+{
+}
diff --git a/src/main/java/de/tavolio/organisation/member/OrganisationMemberResource.java b/src/main/java/de/tavolio/organisation/member/OrganisationMemberResource.java
deleted file mode 100644
index 3a7f008..0000000
--- a/src/main/java/de/tavolio/organisation/member/OrganisationMemberResource.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.tavolio.organisation.member;
-
-public class OrganisationMemberResource
-{
-}
diff --git a/src/main/java/de/tavolio/organisation/member/OrganisationMembershipRepo.java b/src/main/java/de/tavolio/organisation/member/OrganisationMembershipRepo.java
deleted file mode 100644
index c3d22e8..0000000
--- a/src/main/java/de/tavolio/organisation/member/OrganisationMembershipRepo.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.tavolio.organisation.member;
-
-public class OrganisationMembershipRepo
-{
-}
diff --git a/src/main/java/de/tavolio/restaurant/member/OrganisationMemberResource.java b/src/main/java/de/tavolio/restaurant/member/OrganisationMemberResource.java
deleted file mode 100644
index 183ccf5..0000000
--- a/src/main/java/de/tavolio/restaurant/member/OrganisationMemberResource.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.tavolio.restaurant.member;
-
-public class OrganisationMemberResource
-{
-}
diff --git a/src/main/java/de/tavolio/restaurant/member/RestaurantMembershipRepo.java b/src/main/java/de/tavolio/restaurant/member/RestaurantMembershipRepo.java
deleted file mode 100644
index e45c289..0000000
--- a/src/main/java/de/tavolio/restaurant/member/RestaurantMembershipRepo.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.tavolio.restaurant.member;
-
-public class RestaurantMembershipRepo
-{
-}
diff --git a/src/main/java/de/tavolio/session/SessionResource.java b/src/main/java/de/tavolio/session/SessionResource.java
index 2c1c2e1..df4f768 100644
--- a/src/main/java/de/tavolio/session/SessionResource.java
+++ b/src/main/java/de/tavolio/session/SessionResource.java
@@ -1,15 +1,26 @@
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.Path;
-import org.jboss.resteasy.reactive.common.NotImplementedYet;
+import org.jboss.logging.Logger;
@Path("/sessions")
public class SessionResource
{
+ @Inject
+ Logger LOG;
+
+ @Inject
+ SessionService sessionService;
+
@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;
}
}
diff --git a/src/main/java/de/tavolio/session/SessionService.java b/src/main/java/de/tavolio/session/SessionService.java
new file mode 100644
index 0000000..24500be
--- /dev/null
+++ b/src/main/java/de/tavolio/session/SessionService.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/de/tavolio/session/dto/SessionCreation.java b/src/main/java/de/tavolio/session/dto/SessionCreation.java
new file mode 100644
index 0000000..cafc6b4
--- /dev/null
+++ b/src/main/java/de/tavolio/session/dto/SessionCreation.java
@@ -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)
+{
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index cb0c135..565742e 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -5,6 +5,14 @@ quarkus.http.test-port=9089
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.password=${DB_PASSWORD}
-%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=iam
\ No newline at end of file
+%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=iam
+
+# IAM Superuser
+%dev.iam.user.name=tavolio
+%dev.iam.user.password=tavolio
\ No newline at end of file
diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql
index 16aa523..6b68a2d 100644
--- a/src/main/resources/import.sql
+++ b/src/main/resources/import.sql
@@ -3,4 +3,10 @@
-- insert into myentity (id, field) values(1, 'field-1');
-- insert into myentity (id, field) values(2, 'field-2');
-- insert into myentity (id, field) values(3, 'field-3');
--- alter sequence myentity_seq restart with 4;
\ No newline at end of file
+-- 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');
\ No newline at end of file
diff --git a/src/test/java/de/tavolio/account/AccountResourceTest.java b/src/test/java/de/tavolio/account/AccountResourceTest.java
index e38826c..dc2b619 100644
--- a/src/test/java/de/tavolio/account/AccountResourceTest.java
+++ b/src/test/java/de/tavolio/account/AccountResourceTest.java
@@ -1,23 +1,34 @@
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.restassured.RestAssured;
+import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
+import io.restassured.response.Response;
import jakarta.inject.Inject;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Order;
+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 io.restassured.RestAssured.when;
+import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
public class AccountResourceTest
{
+ @Inject
+ Database database;
+
@Inject
AccountRepo accountRepo;
+ @AfterEach
+ void afterEach()
+ {
+ database.clear();
+ }
+
@Test
void testInsert()
{
@@ -40,4 +51,35 @@ public class AccountResourceTest
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"));
+ }
}
diff --git a/src/test/java/de/tavolio/session/SessionResourceTest.java b/src/test/java/de/tavolio/session/SessionResourceTest.java
new file mode 100644
index 0000000..63c4d2e
--- /dev/null
+++ b/src/test/java/de/tavolio/session/SessionResourceTest.java
@@ -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"));
+ }
+}
diff --git a/src/test/java/de/tavolio/utils/Database.java b/src/test/java/de/tavolio/utils/Database.java
new file mode 100644
index 0000000..1a21467
--- /dev/null
+++ b/src/test/java/de/tavolio/utils/Database.java
@@ -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 getAllTables() {
+ try(Connection connection = dataSource.getConnection()) {
+ ResultSet rs = connection.getMetaData().getTables(null, "public", "%", new String[] {"TABLE"});
+ connection.close();
+ List 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.");
+ }
+ }
+
+}
\ No newline at end of file