The code has been developed and tested on a Linux Mint 20.2 Uma machine with kernel version 5.15.0-78-generic and Java 17.
We employ a MySQL database for persistence (version 8.0.33-0ubuntu0.20.04.2), and an H2 database for tests. The application.properties file of the
application lets it create all the entities on the database, so minimal database legwork should be required.
You just need to create the database cards_app_db, a user named cardsappuser with the provided password
and grant all privileges on cards_app_db to cardsappuser. This is how we did it in our machine. Open up a shell and type:
sudo mysql --password Input your sudo password, and this should open up the mysql prompt, where you should type:
create database cards_app_db; -- Creates the new database
create user 'cardsappuser'@'%' identified by 'ThePassword882100##'; -- Same password we have in the application.properties
grant all on cards_app_db.* to 'cardsappuser'@'%'; -- Gives all privileges to the new user on the newly created databaseYou can now run the Spring Server by running the CardsApplication class. Once the server
is up - and - running, for security reasons, we recommend downgrading the privileges of 'cardsappuser' to just the absolutely
necessary ones through the mysql prompt:
revoke all on cards_app_db.* from 'cardsappuser'@'%';
grant select, insert, delete, update on cards_app_db.* to 'cardsappuser'@'%';You can use plain curl calls, a tool like POSTMAN or even OpenAPI 3.
Some details for Postman and OpenAPI follow.
We provide a POSTMAN collection in the file Cards App.postman_collection.json. This file
has example calls that you can use to interact with the API. Every call to the
cards API has the Authorization header assigned to the string Bearer {{BEARER_TOKEN}}, where
{{BEARER_TOKEN}} is a POSTMAN environment variable that contains the JWT
returned by the authentication endpoint (see below, section "Registration & Authentication"). Here is how
you can set this variable. First, click on the "Environment Quick Look" icon on the
upper-right corner, then on "Edit":
This will pull up the "Environments" tab, and you can set a variable called
BEARER_TOKEN with the JWT as the current value.
We have prepared an OpenApi bean in class OpenAPI30Configuration and have several annotations in our
controllers and DTOs that make the OpenApi 3 page rendered in a user-friendly
fashion. Once the app is running, access the page by sending your browser to
http://localhost:8080/swagger-ui/index.html#.
To authenticate using the OpenAPI page, make the same POST call described above
to the cardsapi/authenticate endpoint (make sure the user has been registered first!), copy the JWT returned and then click on the "Authorize" button:
Paste the JWT and click on "Authorize":
This should now "unlock" all the card REST calls so that you can perform them without getting a 401 "Unauthorized" Http Status code.
An advantage of using the OpenAPI page over the Postman collection is better documentation of the endpoints, with examples of the status codes that are returned as well as various example formats of the payloads. A disadvantage is that you will have to re-authenticate if you refresh the page.
The API generates JWTs for authentication, with the secret stored in application.properties. The provided POSTMAN collection
shows some examples of user registration, but you can also use the OpenAPI page
if you prefer. For example, POST-ing the following payload
to the cardsapi/register endpoint registers Maria Giannarou as an admin:
{
"email" : "m.giannarou@logicea.com",
"password" : "mgiannaroupass",
"role" : "ADMIN"
}while the following registers Orestis Ktenas as a member:
{
"email" : "o.ktenas@logicea.com",
"password" : "oktenaspass",
"role" : "MEMBER"
}Attempting to register a user twice will result in a 409 CONFLICT Http Error Code
sent back. All exceptional situations are decorated with Exception Advices implemented in
the class ExceptionAdvice.
After registering, you should receive a JSON with just your username (password ommitted for security) and a 201 CREATED Http Response code:
{
"username": <THE_USERNAME_YOU_CHOSE>
}To receive the Bearer Token, POST your username and password
to the /cardsapi/authenticate endpoint, for example:
{
"email" : "m.giannarou@logicea.com",
"password" : "mgiannaroupass"
}You should receive a JSON with your JWT alongside a 200 OK.
{
"jwtToken": <A_JWT>
}The token has been configured to last 5 hours by default, but you can
tune that by changing the value of the variable JWT_VALIDITY in the Constants class.
Once logged in as either an admin or a member, you can POST the following payload
to the /cardsapi/card endpoint to create a card with name CARD-1:
{
"name" : "CARD-1",
"description" : "My first card",
"color": "#093HGG"
}If successful, this should return a payload with a DB-generated unique ID,
a status of TODO, audit information generated by extending the Auditable interface
and some HAL-formatted links to the GET endpoints
for the card itself and all the cards:
{
"id": 1,
"name": "CARD-1",
"description": "My first card",
"color": "#093HGG",
"status": "TODO",
"createdDateTime": "02/08/2023 13:04:21.428",
"createdBy": "m.giannarou@logicea.com",
"lastModifiedDateTime": "02/08/2023 13:04:21.428",
"lastModifiedBy": "m.giannarou@logicea.com",
"_links": {
"self": {
"href": "http://localhost:8080/cardsapi/card/1"
},
"all_cards": {
"href": "http://localhost:8080/cardsapi/card?page=0&items_in_page=5&sort_by_field=id&sort_order=ASC"
}
}
}We use some SpringHATEOAS static methods to render
the links. Check the class CardModelAssembler for details.
Additionally, we ignore any efforts of the user to change the value of the status field since a newly
created card should have status TODO. The user can also attempt to change the value of the audit fields
or pretty much set any irrelevant key-value pair they like, but those values will also be quietly ignored by the API.
For example, try POST-ing the following by logging in as any user and see for yourself:
{
"name" : "TST",
"description" : "A test card",
"color" : "#A89012",
"createdBy": "memeuser@memes.com",
"foo" : "bar"
}You should get back a well-formed card (we are logged in as Dimitris Polissiou in this example):
{
"id": 11,
"name": "TST",
"description": "A test card",
"color": "#A89012",
"status": "TODO",
"createdDateTime": "02/08/2023 18:51:54.223",
"createdBy": "d.polissiou@logicea.com",
"lastModifiedDateTime": "02/08/2023 18:51:54.223",
"lastModifiedBy": "d.polissiou@logicea.com",
"_links": {
"self": {
"href": "http://localhost:8080/cardsapi/card/11"
},
"all_cards": {
"href": "http://localhost:8080/cardsapi/card?page=0&items_in_page=5&sort_by_field=id&sort_order=ASC"
}
}
}Performing a GET at the endpoint cardsapi/card/{id} will retrieve the information of the card uniquely
identified by {id}. For example, a GET at localhost:8080/cardsapi/card/1
should retrieve the same payload that the POST above retrieved:
{
"id": 1,
"name": "CARD-1",
"description": "My first card",
"color": "#093HGG",
"status": "TODO",
"createdDateTime": "02/08/2023 13:04:21.428",
"createdBy": "m.giannarou@logicea.com",
"lastModifiedDateTime": "02/08/2023 13:04:21.428",
"lastModifiedBy": "m.giannarou@logicea.com",
"_links": {
"self": {
"href": "http://localhost:8080/cardsapi/card/1"
},
"all_cards": {
"href": "http://localhost:8080/cardsapi/card?page=0&items_in_page=5&sort_by_field=id&sort_order=ASC"
}
}
}However, if in the meantime we have logged in as a different "member" user, we will
get a 403 FORBIDDEN error, since we do not allow member users to retrieve the cards
of other members or admins.
Let's replace the card we posted above by PUT-ting the following payload
to cardsapi/card/1:
{
"name" : "CARD-1-REPLACEMENT",
"description" : "Replacement of card 1",
"status" : "IN_PROGRESS"
}We should receive the following updated card.
{
"id": 1,
"name": "CARD-1-REPLACEMENT",
"description": "Replacement of card 1",
"color": null,
"status": "IN_PROGRESS",
"createdDateTime": "02/08/2023 13:04:21.428",
"createdBy": "m.giannarou@logicea.com",
"lastModifiedDateTime": "02/08/2023 13:48:08.499",
"lastModifiedBy": "m.giannarou@logicea.com",
"_links": {
"self": {
"href": "http://localhost:8080/cardsapi/card/1"
},
"all_cards": {
"href": "http://localhost:8080/cardsapi/card?page=0&items_in_page=5&sort_by_field=id&sort_order=ASC"
}
}
}Notice that the color has now been nullified since
we did not provide it in the payload we PUT.
Also notice that, as a design choice, when a user PUTs a card, the "created" audit information
is NOT changed. The "last modified" information, on the other hand, is, of course, changed.
As with the POST endpoint, a user MUST supply a name for the card.
Let's now PATCH the same card by changing its color and status. Send a PATCH with the following payload to
cardsapi/card/1:
{
"color" : "#FFFFFF",
"status" : "DONE"
}You should receive:
{
"id": 1,
"name": "CARD-1-REPLACEMENT",
"description": "Replacement of card 1",
"color": "#FFFFFF",
"status": "DONE",
"createdDateTime": "02/08/2023 13:04:21.428",
"createdBy": "m.giannarou@logicea.com",
"lastModifiedDateTime": "02/08/2023 13:50:43.403",
"lastModifiedBy": "m.giannarou@logicea.com",
"_links": {
"self": {
"href": "http://localhost:8080/cardsapi/card/1"
},
"all_cards": {
"href": "http://localhost:8080/cardsapi/card?page=0&items_in_page=5&sort_by_field=id&sort_order=ASC"
}
}
}The PATCH endpoint ignores fields that are missing or NULL, assuming that the user simply does not
want to change those fields. For example, neither the name nor the descripton attributes of
this card were changed. Additionally, if the
user attempts to clear the name of a card by supplying a whitespace-only string, they will receive a
400 BAD_request response; we do not allow for the clearing of the name field.
To delete a card, send a DELETE request at cardsapi/cards/{id}. So, for example,
the card above can be deleted by sending a DELETE request at cardsapi/cards/1.
Note that our semantics for DELETE are "hard-delete". Calling DELETE on an ID
removes the physical entry from the database. We also return a 404 NOT_FOUND Http error
if the ID we want to DELETE is not in the database. The community seems to be divided on whether
one should return a 204 NO_CONTENT or a 404 NOT_FOUND in this case, and what exactly is meant by "idempotent" when
we say that "DELETE should be an idempotent operation".
Under src/test/java you can find unit and integration tests. Unit tests make extensive use
of Mockito, while integration tests load the spring context and use the default in-memory
H2 database.
The following are the code coverage metrics generated by IntelliJ:
We use basic AOP to enable logging at the INFO and WARN levels for all public methods at the controller,
service and persistence layers. Examine the package com.logicea.cardsapp.util.logger for the implementation,
and peek at the Spring terminal after every call to the API to see the logging in action.
Timestamps in the application are formatted in a European-friendly format, specifically
"dd/MM/yyyy HH:mm:ss.SSS". This is the format that is expected, for example, if filtering by
creation / ending date-time in aggregate GET queries and yes, this unfortunately includes
miliseconds...
We unfortunately did not have time to implement some interesting features such as:
- An implementation of the GET ALL queries at the persistence layer using the
Spring Data JPA Specification
interface. The current implementation uses the Criteria API directly,
and is somewhat spaghetti-fied. Refer to class
AggregateCardQueryRepositoryImplfor details. - It would have also been nice to cache the previous and next page of data after a user requests a specific data page with specific sorting criteria, for faster access.
- "Soft" deletes of cards, perhaps with a column called "active" in the
CARDdatabase table. This would allow for some historical queries of form "How many cards did user XYZ create within a given timeframe", even if some (or all) of said cards had been deleted in the meantime. - Setting up the MySQL database through a
docker-composescript so that any OS could run the app without issue.
- If you call an endpoint that requires an ID request parameter (e.g
DELETEorGETat/cardapi/card/{id}) but neglect to pass the request parameter{id}, you will get a401 UnauthorizedHTTP Error. This is because of the way that thecommence()method has been overloaded inJwtAuthenticationEntryPointand could probably have been handled better. - We use the Hibernate
@Emailvalidator for validating e-mails, and that validator is sensitive to whitespace. Please be careful when typing e-mail addresses in authentication endpoints, and if filtering by e-mail in aggregate GET calls. - This is a bit far-fetched, but still noteworthy. If a member tries to access a card that
is not in the repository, then we send them a
404 NOT_FOUND. Since we hard-delete, this means that even if the member would not have had access to the card in the first place, we send them a404instead of a403 FORBIDDEN. If we had instead implemented a soft-delete, we could be fetching an entity from the database to which the user could be determined to not have access, even in its current "deleted" (or "inactive") state. So it's possible that we are leaking some information to members that should be kept private. Now, if the card was never there in the first place, a404would be appropriate.




