Events' Ticket Booking System with Spring-Boot, Docker and AWS

Technologies used

  • REST
  • Spring-Boot (Java)
  • Docker
  • Kubernetes
  • AWS
  • Postman
  • MySQL

booking

Description

For this project, I created a REST web service with Spring-Boot that allows different users to create events (concerts, plays, sports events, etc.) or buy tickets for them. This was deployed to AWS as a Docker container and later using Kubernetes. A user can have one or both roles: Customer and Organizer. In addition, I implemented the roles: Admin and Security Guard with more privileges. For the login functionality, I used Spring Security with Basic Authentication and OAuth2 for social logging (using Github, Google, and Facebook). An Organizer can create a new event with information such as Event Name, Date, Ticket Price, and Capacity. An Event Image can be uploaded as a MultipartFile that is stored in an AWS S3-instance. The system uses a H2 database (in memory) for the development environment and a MySQL database running on AWS RDS for the production environment. The application can be served using HTTPS. I also added an Email Service that sends a Token to verify new users or to reset the password of an existing user. Another feature I implemented is generating the tickets as QR-Codes which can facilitate the ticket validation by a Security Guard. The application provides extensive exceptions handling.

This file describes the solution of the task and the decisions that were made. I only provide code snippets here. For details, please contact me.

In the end, you find an overview of all requests that can be made, and you can see how I designed the REST API.

Important Decisions

Users without Inheritance

To Manage the different users, we do distinguish between them using class since this is managed using the Role object. Without inheritance a user can have multiple roles without that we need to create different instance for every role. Now a user can be a Customer and an Organizer at same time. Other roles are here also allowed since we just need to extend the role list of the customer. This makes the extension of the application easier.

Role as Object

I decided to manage the Roles as objects instead of just a list of strings or enums that contains the role names. This has following advantages:

  • Every Role has its own identity so it is easier to check if a Role is the same as another one when two Roles are compared.
  • A Role is not a property of a User. It is something that can stand alone, and it can be used for other entities if needed.
  • New Roles can be added to extend the application. I used this advantage and added a new Role: Security Guard (see later sections).

Currently the system provides following roles: ROLE_ADMIN, ROLE_CUSTOMER, ROLE_ORGANIZER, ROLE_SECURITY_GUARD.

DTO

The Data Transfer Object Pattern was used for developing this application. I decided for this because it helps to decouple the API from the models. This allows me to maintain and scale the service easily without changing the API. Also, the end-user (e.g. frontend developer) doesn’t need to know how the model is built, he just need to know which information he needs to provide to use the API correctly.

Following DTOs were used:

  • UserRegistrationDto: for sending information that are important for the registration. This allows to add new confirmation variable for the email and the password, and the roles can be sent as String array.

  • CustomerDto and OrganizerDto: for sending user data with his tickets/events because we aren’t using inheritance here and there are no Customer or Organizer models to transfer this data as response.

  • EventDto: to transfer information that are necessary for creating a new event. All other information are determined automatically (e.g. currentCapacity is at start always 0) or they can be set later (e.g. securityGurard of Event).

  • PasswordDto: to transfer data for resetting the password. It works like the UserRegistrationDto but without the other data fields.

Creating new events

For this task, the user (Organizer) has to provide two objects:

  • An EventDto that contains all important information to create a new event and
  • an Image as MultipartFile.

After creating the new event with Organizer as the currentUser, the image is stored and its download URI is then added to the event object before this new event is saved in the database.

public Event createEvent(EventDto eventDto, User organizer, MultipartFile image) throws FileStorageException {
    Event tempEvent = new Event(eventDto, organizer);
    String fileName = imageStorageService.storeFile(image, "event" + (eventRepository.getLastID()+1));
    String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
            .path("/event/image/").path(fileName).toUriString();
    tempEvent.setImageURL(fileDownloadUri);
    eventRepository.save(tempEvent);
    return tempEvent;
} 

Image Storing

The class ImageStorageService is responsible for storing, loading and deleting images. The images are now stored local on the server. The user can upload a new image when he creates a new event. This image will be deleted when the event is deleted. After creating a new event successfully, the server sent information about it including the image URL.

ImageStorageService checks if the uploaded or requested images have the correct format (PNG, JPEG or JPG). So, we can be sure no other file can be loaded if they are stored in same location on the server.

    private boolean validImageFormat(String fileName){
        return fileName.endsWith(PNG_FILE_FORMAT) ||
                fileName.endsWith(JPEG_FILE_FORMAT) ||
                fileName.endsWith(JPG_FILE_FORMAT);
    }

    private String getValidExtension(String fileName){
        if (!validImageFormat(fileName))
            throw new FileStorageException(INVALID_FILE_FORMAT);
        return fileName.substring(fileName.lastIndexOf("."));
    }

The image storage service (ImageStorageService) extends the file storage service (FileStorageService). This superclass can be extended later to be used for other service (e.g., TxtStorageSerivce).

Modifying events

For modifying events the EventDto can be used, since it contains only the fields that the user can change. This way we can ensure he can’t change the event image or its currentCapacity as it is required in this task. While modifying an event the new maxCapacity value is not allowed to be smaller than the currentCapacity value, so we can guarantee that there are tickets sold as the maxCapacity of the event and not more.

public Event updateEvent(EventDto eventDto, User organizer, Long id) throws ResourceNotFoundException {
    Event tempEvent = getEventById(id);
    if(!organizer.getId().equals(tempEvent.getOrganizer().getId())){
        throw new AccessNotAllowedException("Event modification not allowed");
    }
    if(tempEvent.getCurrentCapacity() > eventDto.getMaxCapacity()){
        throw new NotAcceptableStatusException("Max capacity can't be smaller than the current capacity");
    }
    tempEvent.updateValues(eventDto);
    eventRepository.save(tempEvent);
    return tempEvent;
}

Buy / Delete Tickets

If a user wants to buy a ticket for an event then first the existence of that event is checked after that its status. If the event if cancelled or soldout (no more tickets available) then it is not possible to buy a new ticket. If its status is available then the event currentCapacity is updated by +1 and its new status is determined.

public Ticket buyTicket(Long eventId, User customer) throws ResourceNotFoundException, NotAcceptableStatusException {
    Event tempEvent = eventService.buyTicketForEvent(eventId);
    //save and return the new ticket
    return ticketRepository.save(new Ticket(tempEvent, customer));
}
@Transactional(isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Event buyTicketForEvent(Long eventId) throws ResourceNotFoundException {
    Event tempEvent = getEventById(eventId);
    if (tempEvent.getStatus() != EventStatus.AVAILABLE)
        throw new NotAcceptableStatusException("This is event was "+tempEvent.getStatus());
    tempEvent.updateCurrentCapacity(+1);
    return tempEvent;
}
private void updateStatus() {
    if(maxCapacity <= 0)
        this.status = EventStatus.CANCELLED;
    else if(currentCapacity >= maxCapacity)
        this.status = EventStatus.SOLD_OUT;
    else
        this.status = EventStatus.AVAILABLE;
}

public void updateCurrentCapacity(int addedValue) {
    this.currentCapacity += addedValue;
    this.updateStatus();
}

For deleting a ticket, we do the opposite way. The currentCapacity is updated by -1 and the event’s new status is determined. Then the ticket is removed from the system. And for sure the ticket can only be deleted by its owner.

Extra Functions

Admin Role

This role has privileges to access and modify users, events, and tickets with no restrictions. The admin can request a list of all users in the systems or just a specific one using his id or email address. He also can request all users with a specific role (e.g., all users with CUSTOMER_ROLE). An admin is the only one who can make other users to an admin too, since registeration with ROLE_ADMIN is not allowed. When the admin deletes a user then all his events and tickets will be deleted (depending if he was a customer or an organizer or both).

@Transactional
public User deleteUser(Long id) throws ResourceNotFoundException {
    User tempUser = getUserById(id);
    eventService.deleteAllEventsOfOrganizer(tempUser);
    ticketService.deleteAllTicketsOfCustomer(tempUser);
    userRepository.delete(tempUser);
    return tempUser;
}

He can see tickets or just specific one using the ticket id or he can just request tickets for one event or only one specific ticket for one specific event. Also deleting tickets is possible. Since the events are public and everyone can request them, the admin doesn’t need any specific access here, but he can delete any event with no restriction (even if there were tickets sold for that event) and he can also cancel events.

Delete Event Restriction

This restriction is added to avoid that an organizer can’t delete an event if there were tickets sold for it. This can only be done by an admin. Before deleting the event the EventSerivce checks if the events belong to the organizer.

if(!organizer.getId().equals(tempEvent.getOrganizer().getId()))
    throw new AccessNotAllowedException("Event can only be deleted by its organizer");

If this condition was false, then the EventSerivce checks if there were tickets sold for this event. If yes, then an exception is thrown.

if(tempEvent.getCurrentCapacity() > 0)
    throw new NotAcceptableStatusException("Event can't be deleted if there are ticket associated with it, Contact the admin for help");

If this condition was false also then the deleteEvent(Long id) method is called. This method will delete all tickets of this event and then the event itself and at the end the event image is deleted from the storage.

@Transactional
public Event deleteEvent(Long id)throws ResourceNotFoundException {
    Event tempEvent = getEventById(id);
    ticketService.deleteTicketsOfEvent(tempEvent);
    eventRepository.delete(tempEvent);
    imageStorageService.deleteFile(tempEvent.getImageURL());
    return tempEvent;
}

Cancel Event Function

Deleting an event has restrictions for the organizer but he still can cancel the event. As the function before this function also checks if the organizer has the authority to cancel the event (event belongs to him). After that the eventStatus is checked to avoid cancelling an event multiple time.

if (tempEvent.getStatus() == EventStatus.CANCELLED)
    throw new NotAcceptableStatusException("Event is already cancelled");

After passing all conditions the event can be cancelled. The cancelEvent(Long id) method sets the event’s max capacity and the current capacity to 0 and then it calls the cancelTickets(Event event) method of the class TicketService.

@Transactional
public Event cancelEvent(Long id) throws ResourceNotFoundException{
    Event tempEvent = getEventById(id);
    tempEvent.setMaxCapacity(0);
    tempEvent.updateCurrentCapacity(-tempEvent.getCurrentCapacity());
    ticketService.cancelTickets(tempEvent);
    return tempEvent;
}

This latter method will set the price of all tickets to 0 and their status to CANCELLED.

Collection<Ticket> tempTickets = getAllTicketsOfEvent(event);
    for(Ticket t: tempTickets){
        t.setPrice(0);
        t.setStatus(TicketStatus.CANCELLED);
    }

Filter Events by Keyword in Name or Description

An unauthenticated user can see all events or just a specific one by providing its id. He also can filter the events using a keyword search that will only return all events that have the keyword in their name or description. This is done using the @Query annotation and a SQL query.

//use LOWER() or UPPER() for ignore case
@Query("SELECT e FROM Event e WHERE LOWER(e.name) like LOWER(CONCAT('%', :keyword, '%')) or LOWER(e.description) like LOWER(CONCAT('%', :keyword, '%'))")
Collection<Event> searchForEvent(String keyword);

Find Events regarding to Dates

Other filters were added to select event between two dates or events before or after a specific date.

Registeration Validation & Password Validation & Field Match

After a user sends his information for registeration, this information will be validated. Using the UserRegistrationDto facilitate this step since we can use annotations. No field of this DTO can be empty and the defined constraint @FieldMatch uses FieldMatchValidator and it checks if the email and password fields match, since these values should be provided twice.

@FieldMatch.List({
    @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
    @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})

Another defined constraint @PasswordValidation uses the PasswordConstraintValidator to check if the provided password meets certain requirements to inhance the security of user data. The password validator uses Passay (password policy for java) to send customized error messages. The requirements are defined as rules. I defined following rules for a valid password:

  • between 8 and 64 characters.
  • at least one upper case character.
  • at least one lower case character.
  • at least one digit.
  • at least one special character.
  • no white spaces.
  • no sequence of 5 consecutive letters.
  • no sequence of 5 consecutive digits.

If everything before went well then, the UserService checks if already a user with the username or email desired exists. If not, then the roles provided will be checked so that the new user has not privileges that he was not supposed to have. If these checks went well then, a new user and a new verification token are created and the verification token is sent to the email address provided.

Email Verification with Token

After a new user is created successfully the TokenService creates a new token of type TokenType.VERIFICATION for this user. The token is only 24 hours valid and after that the user will not be able to use it (I didn’t decide what will happen after the 24 hours). The token string (secret) is generated using UUID.randomUUID().toString();. The EmailService takes this token and sends it to the user. It uses JavaMailSender for sending emails.

registerToken

After receiving the email, the use can just click on the URL and confirm his email. The UserService will first check if this token was for verification porpose and then if then if will set the status of the user as “verified” and then delete the token.

@Transactional
public User verifyAccount(String tokenString) throws ResourceNotFoundException, RequestFailedException{
    Token token = tokenService.getToken(tokenString);
    if(!token.getTokenType().equals(TokenType.VERIFICATION)){
        throw new RequestFailedException("Token is not for verification");
    }
    User tempUser = userRepository.findByEmail(token.getUser().getEmail());
    tempUser.setVerified(true);
    // delete token after using it
    tokenService.deleteToken(token);
    return tempUser;
}

The user will not be able to login as long as his account is not verified. The UserRepositoryAuthProvider if responsible to check this.

if (!user.isVerified())
    throw new RequestFailedException("User is not verified yet");

Social Logging / OAuth2

Beside the http basic authentication with username and password the users have also the opportunity to authenticate themselves using OAuthentication2 that is known as social logging. The users can select one of three different authentication providers: GitHub, Facebook and Google.

http.httpBasic()
    .and()
    .oauth2Login()
    .userInfoEndpoint().userService(oAuthentication2UserService)

After successful logging in, the request data is forwarded to the OAuthentication2UserService, this class creates a OAuth2User using the request data provided. After this it checks if there is any user with same email address that is provided by the OAtuh2 provider. We assume that it’s the same user, since he verified his email before when he registered with username and password. If no user with this email was found, then a new user is created using the email as username and as email since this value is unique. The information about the authentication provider is saved into the user database table so we can use it later to filter users by authentication provider or for creating statistics.

public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    OAuth2User tempUser = super.loadUser(userRequest);
    String email = tempUser.getAttribute("email");
    String client = userRequest.getClientRegistration().getClientName();
    // check if the user already registered with this email
    // otherwise create a new user
    User user = userRepository.findByEmail(email);

    if (user == null) {
        // use email as username so it will be unique
        user = new User(email, email, 
                Arrays.asList(roleRepository.findByName
                (RoleName.valueOf("ROLE_CUSTOMER"))), client);
                userRepository.save(user);
    }
    userComponent.setLoggedUser(user);
    return new OAuthentication2User(user);
}

Reset Password

As we have seen in previous steps there is a TokenService available, so this can also be used to reset the password if a user forgot it. First, he will provide the system with an email address, if this exists in the system then a new token of type TokenType.NEW_PASSWORD will be created and send to the email address provided. Same conditions of the verification token are valid here. The token is also only for 24 hours valid.

pwdToken

After clicking on the link, the user still need to provide a new password that should meet the same password requirements as the password for the registration. To send it PasswordDto is used. The new password will be sent in the request body and the token as request parameter. In a real use case, we will have a frontend page that helps the user to reset his password.

@Transactional
public String setNewPassword(String tokenString, PasswordDto pwdDto, BindingResult result) throws ResourceNotFoundException, RequestFailedException {
    Token token = tokenService.getToken(tokenString);
    if(!token.getTokenType().equals(TokenType.NEW_PASSWORD)){
        throw new RequestFailedException("Token is not for setting a new password");
    }
    User tempUser = userRepository.findByEmail(token.getUser().getEmail());
    if (result.hasErrors()) {
        throw new RequestFailedException(result.toString());
    }
    tempUser.setPassword(pwdDto.getNewPassword());
    // delete token after using it
    tokenService.deleteToken(token);
    return "Password changed successfully";
}

Set / Change Password

Users that registered for first time using OAuth2 they don’t have any password and they can’t login using the same email address that was used for the OAuth2 provider. This function allows them to set a new password that should meet all requirements for a password in the registration. For validation purposes we use PasswordDto to send the new password. This function can also be used to change the current password if the use already has one. Here we have a condition that checks if the new password doesn’t match with the old password and another one that checks if the current password was given correctly.

public String changePassword(User loggedUser, PasswordDto pwdDto, BindingResult result){
    // check if the user has already set a password
    // if not then he was authenticated with OAuth2 and now he can set a new password for first time
    if(loggedUser.getPassword() != null){
        if(!new BCryptPasswordEncoder().matches(pwdDto.getCurrentPassword(), loggedUser.getPassword())) {
            throw new BadCredentialsException("Current password was wrong");
        }
        if(pwdDto.getCurrentPassword().equals(pwdDto.getNewPassword())){
            throw new RequestFailedException("The new password is identical to the current one");
        }
    }
    if (result.hasErrors()) {
        throw new RequestFailedException(result.toString());
    }
    loggedUser.setPassword(pwdDto.getNewPassword());
    userRepository.save(loggedUser);
    return "Password changed successfully!\nPlease login next time with the new password";
}

Security Guard Role

To make the application more useful and create more use cases I added the new role of Security Guard. Basically, it the person that checks if a ticket is valid or not and then he let the people go inside or not. Each event has an user with the role ROLE_SECURITY_GUARD. The attribute User securityGuard can be empty and assigned later before the event starts. A Security Guard can see all events assigned to him and he is allowed to check the validation of the tickets that belong to events he is assigned to. The TicketService checks if the security guard if really assigned to the event of the ticket, if this is true then it checks if the ticket status is VALID, if yes, then the ticket status will be set to USED.

@Transactional
public Ticket checkTicket(User securityGuard, Long ticketId) throws ResourceNotFoundException {
    Ticket tempTicket = getTicketById(ticketId);
    if(!securityGuard.getId().equals(tempTicket.getEvent().getSecurityGuard().getId())) {
        throw new AccessNotAllowedException("Ticket access not allowed");
    }
    if(tempTicket.getStatus() == TicketStatus.USED){
        throw new NotAcceptableStatusException("Ticket is already used");
    }
    // set ticket status to "used"
    tempTicket.setStatus(TicketStatus.USED);
    return tempTicket;
}

Ticket as QR Code

To provide the customers with ticket IDs that are easy to check by the security guard I created a function that generate QR Code that contains the URL to check a ticket with the ticket id as parameter. The security guard just need to scan the code to check if the ticket is valid. The scan function can now be tested with mobile phone but in a real world application the frontend will provide such service for the security guard.

public byte[] generateQRCode(User customer, Long ticketId) throws ResourceNotFoundException, AccessNotAllowedException {
    try {
        getTicketOfCustomerById(customer, ticketId);
        QRCodeWriter barcodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = barcodeWriter.encode(BARCODE_TEXT+ticketId, BarcodeFormat.QR_CODE, BARCODE_WIDTH, BARCODE_HEIGHT);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        MatrixToImageWriter.writeToStream(bitMatrix, "png", byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
        //return MatrixToImageWriter.toBufferedImage(bitMatrix);
    } catch (Exception e) {
        throw new RequestFailedException("Couldn't generate QR-Code");
    }
}

The generated QR Code will be sent to the user as a png image.

qrcode

Exceptions handling

To catch every exception and to be able to identify every error it is important that every component handles its errors and throws appropriate exceptions. This way the controller class just need to catch these exceptions and forward them to the end user. Alle other components should throw all exceptions of other components that they use. An example for this is the deleteTicketForCustomer function of the class TicketService, this uses its own function getTicketById(Loing ticketId) that thrwos a ResourceNotFoundException if the ticket is not found. Then the function deleteTicketForCustomer throws an AccessNotAllowedException if the customer is not the owner of the ticket. And finally it uses EventService to update the event capacity. So at the end the controller class is not responsible to check if there are any errors, it just should propagate the exceptions.

@DeleteMapping(value = "/ticket")
public ResponseEntity<Ticket> deleteTicketById(@RequestParam("ticketId") Long ticketId) {
    try {
        Ticket tempTicket = ticketService.deleteTicketForCustomer(ticketId, userComponent.getLoggedUser());
        return new ResponseEntity<>(tempTicket, HttpStatus.OK);
    } catch (ResourceNotFoundException ex) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, ex.getMessage(), ex);
    } catch (AccessNotAllowedException ex) {
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ex.getMessage(), ex);
    }
}

Second Part of the Project

AWS and profiles in Spring

ImageService

This component was changed by using an interface ImageService that depends on current profile, a local storage was used for development environment and AWS S3 for production environment.

    @Bean
    @Profile("development")
    public ImageService localService() {
        return new LocalImageService();
    }

    @Bean
    @Profile("production")
    public ImageService S3Service() {
        return new AWSImageService();
    }

To avoid errors when the app starts the @Autowired of this interface in EventService uses the option required = false.

    @Autowired(required = false)
    private ImageService imageService;

To provide valid like to images this class uses the @Value("${images-url}") that is set in the properties files of each profile.

  • Dev-Profile: images-url=https://localhost:8443/events/
  • Prod-Profile: images-url= https://s3.amazonaws.com/${BUCKET_NAME}/

The class AWSImageService takes the AWS credentials from the environment variables that are definde in the application-production.properties.

amazon.s3.bucket-name= ${BUCKET_NAME}
amazon.s3.access-key= ${ACCESS_KEY}
amazon.s3.secret-key= ${SECRET_KEY}
amazon.s3.session-token= ${SESSION_TOKEN}
    @Value("${amazon.s3.access-key}")
    private String accessKey;
    @Value("${amazon.s3.secret-key}")
    private String secretKey;
    @Value("${amazon.s3.session-token}")
    private String sessionToken;
    @Value("${amazon.s3.bucket-name}")
    private String bucketName;

Region is setted to Regions.US_EAST_1, because other methods to find by name didn’t work well. However this is standard when using a student account.

//BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
BasicSessionCredentials credentials = new BasicSessionCredentials(accessKey, secretKey, sessionToken);

When initializing the S3 instance a check is made to be sure that the bucket exists and if not then it will be created.

    if(!amazonS3.doesBucketExistV2(bucketName)) {
        amazonS3.createBucket(bucketName);
    }

Datasource

Both profiles use the file application.properties and extend it in their own properties file. So we keep the app thin and avoid redunadncy.

Defining the datasource properties like this always as to use the app with different databases (H2 or MySQL). This was defined like this to be also to use MySQL in dev-profile and prod-profile in Docker-Compose and if we run the app with no MySQL variables then H2 will be used.

spring.datasource.url= ${MYSQL_URL:"jdbc:h2:mem:testdb"}
spring.datasource.username=${MYSQL_USERNAME:sa}
spring.datasource.password=${MYSQL_PASSWORD:password}
spring.jpa.database-platform= org.hibernate.dialect.${MYSQL_DIALECT:H2Dialect}
spring.jpa.hibernate.ddl-auto=update

Docker and Docker Compose

Dockerfile

The Dockerfile builds the docker image in two stages: first one uses Maven and the other one Java. It copies all necessary files for building, here the file wait-for-it.sh was added so it can be used be next section when Docker-Compose is created.

The PowerShell script file docker-build-push-script.ps1 was created to build and publish the image to DockerHub under the tagged name yousif94/event-app:v1.

Docker Compose

The docker-compose-dev.yml uses the wait-for-it.sh script to make the application waiting for the database before it starts. Here the argument --timeout=90 was setted to keep the app waiting for longer time because the script waits only 15 seconds if the argument is not setted, and this may lead to failure. Also two volumes were used to persist the local images and the database

    volumes:
      - uploads-files:/usr/app/eventApp/uploads
    ...
    volumes:
      - database-volume:/var/lib/mysql
    ...
volumes:
  database-volume:
  uploads-files:

Defining docker-compose-prod.yml was much easier since no volumes were needed because we are using AWS RDS and S3.

Kubernetes

Here three .yaml files were created: deployment.yaml, service.yaml and ingress.yaml.

  • deployment.yaml: defines the application that will be deployed with all necessary environment variables and it overwrites the Docker image command to disable SSL and use port 8080.
  • service.yaml: defines a ClusterIP service to use the deployed application in port 8080.
  • ingress.yaml: defines a Ingress-Serivce that allows us to access the service defined before by using ClusterIP/app. This is achieved by specifying a rule for the path path: /app.

REST Controllers / API

Access Controller /**

This class is responsible for all public requests that are used to access the server. It provides functions for logging in and out and for registering as new user. It also provides other functions to reset the password if the user forgot it.

GET /login logging in

GET /oauth2/authorization/<OAith2 Provider> logging in using social media

GET /oauth2providers get a list with all OAuth2 providers available

POST /register register a new user as customer or organizer or both

GET /accountVerification verify your account using a token

GET /passwordForgot request a token for resetting the password

POST /newPassword set a new password using token

Event Controller /event/**

This class manages the access to the events on the server. Every user (authorized or unauthorized) can call the functions of this controller and he can request all the events or just a specific one. The user can also search for events that contain a specific word in their name or description. He also can filter the events to get only events between two dates.

GET / all events available

GET /info one event with specific event id

GET /search events with keyword in name or description

GET /date/between events between two dates

GET /date/after events after a certain date

GET /date/before events before a certain date

GET /image/{fileName:.+} image of an event

User Controller /user/**

It is accessible for every authorized user. It contains functions that can be used by every user no matter which roles he has. It provides a function to retrieve the user information and a function to change the password.

GET /loggedUser general information about current logged user without tickets or events

POST /changePassword change or set password for first time

Admin Controller /admin/**

This class provides functions for users with the role Admin. He has the privileges to handle like every user with every role without any restrictions. For example, the admin can request or delete any event or ticket. He also can do more than that. He can delete any event even if there are tickets sold for it. He can delete users and their owned properties (tickets or events). He can request information about all users or a specific one and he also can search users based on their role and he can filter them by the email address.

GET /all_users list of all users in system

GET /user information about a user with specific id

GET /user/role list of all users with specific role

GET /user/email user with a specific email address

POST /user/makeAdmin a the role admin to a user

GET /all_tickets all tickets in system

GET /ticket one ticket with specific id

GET /event/all_tickets all tickets of an event with specific id

GET /event/ticket ticket with specific id of an event with specific id

DELETE /user delete a user and all his tickets and events

DELETE /ticket delete a ticket

DELETE /event delete an event and all its tickets

PUT /event/cancel change event status to cancelled

Customer Controller /customer/**

It manages all requests that a user with the role Customer can send. A customer can buy tickets and delete them. He can request his own information and all his tickets or just a specific one.

GET /about information about the customer and all his tickets

GET /all_tickets all tickets of current logged customer

GET /ticket one ticket with specific id

GET /ticket/qrcode QR code for a ticket with specific id

POST /ticket buy a ticket for an event with specific id

DELETE /ticket delete a ticket using its id

Organizer Controller /organizer/**

It manages all requests that a user with the role Organizer can send. An organizer can create events, modify and delete them. He can request his own information and all his events or just a specific one.

GET /about information about the organizer and all his events

GET /event all events of current logged organizer

GET /event/all_tickets tickets for an event with specific id

GET /event/ticket one ticket for an event with specific id

POST /event create new event

PUT /event modify an event

DELETE /event delete an event

PUT /event/cancel change event status to cancelled

Security Guard Controller /securityGuard/**

This class is responsible for responding to request for the new created role Security Guard. A security guard can retrieve information about himself and also see all the events where he is going to work at. He has the privilege to check the tickets and mark them as “used”.

GET /about information about the security guard and all events he is assigned to

GET /event all events the security guard is assigned to

PUT /checkTicket check if a ticket is valid and validate it