https://github.com/jonaskahn/jooby-api-template
Just a working Jooby backend API example project in Java
https://github.com/jonaskahn/jooby-api-template
api-template backend-api backend-template java jooby
Last synced: about 2 months ago
JSON representation
Just a working Jooby backend API example project in Java
- Host: GitHub
- URL: https://github.com/jonaskahn/jooby-api-template
- Owner: jonaskahn
- License: apache-2.0
- Archived: true
- Created: 2024-07-25T11:11:10.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2024-08-25T16:01:23.000Z (9 months ago)
- Last Synced: 2025-03-29T08:07:59.767Z (about 2 months ago)
- Topics: api-template, backend-api, backend-template, java, jooby
- Language: Java
- Homepage:
- Size: 66.4 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Why Jooby?
- Fast, light, easy to learn
- I've been using Spring for almost my wokring time with Java/Kotlin. For large scale enterprise projects, Spring Stacks
are undoubtedly the way to go, but for minimal projects, Jooby is a good choice.## Looking for Kotlin version
- [Checkout](https://github.com/jonaskahn/kooby-api-template)
## What's included?
- [x] Support Default JWT
- [x] Support Role Access Layer
- [x] Hibernate, Flyway support by default
- [x] Add custom JPAQueryExecutor for the better querying
- [x] Add Jedis support instead of Lettuce
- [x] Using MapStruct for Object Mapper
- [x] Using Guice as Dependency Injection Framework
- [x] Multiple language support
- [x] Default admin user: `admin@localhost/admin`## Default API
1. Create users
```shell
curl --location 'http://localhost:8080/api/auth/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "test",
"email": "test@localhost",
"password": "test"
}'
```2. Generate token
```shell
curl --location 'http://localhost:8080/api/auth/generate-token' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "test@localhost",
"password": "test"
}'
```3. Get User Info
```shell
# With JPA Query
curl --location 'http://localhost:8080/api/secure/user/info' \
--header 'Accept-Language: vi'
--header 'Authorization: ••••••'
``````shell
# With JPAQueryExecutor
curl --location 'http://localhost:8080/api/secure/user/info-with-executor' \
--header 'Accept-Language: vi'
--header 'Authorization: ••••••'
```4. Test Role
```shell
curl --location 'http://localhost:8080/api/secure/test/admin-role' \
--header 'Authorization: ••••••'
```5. Logout
```shell
curl --location --request DELETE 'http://localhost:8080/api/auth/secure/logout' \
--header 'Authorization: ••••••'
```## HOW
### Implement JWT
- Using [Pac4j](https://jooby.io/modules/pac4j/) Module as Security Layer
```java
install(new Pac4jModule().client(
"/api/secure/*",
conf -> new HeaderClient(
"Authorization",
"Bearer ",
new AdvancedJwtAuthenticator(
require(JedisPooled.class),
new SecretSignatureConfiguration(conf.getString("jwt.salt")
)
)
)
)
);```
1. Using `HeaderClient` to tell Jooby read `Bearer` token from header
2. By default Jooby use the `JwtAuthenticator` from Pac4j, the problems are:
- Token is completed stateless
- What if user is lock/inactivated/deleted -> token may still valid by the `exp` -> user still can access to system
- There is no truly `logout`So, I solved these problems by store `jid` of JWT in Redis, after `validate` raw token, before `createProfile` I made a
simple check to ensure the `jid` exists in `redis`. If no, token is invalidSee [AdvancedJwtAuthenticator.kt](src/main/java/io/github/jonaskahn/middlewares/jwt/AdvancedJwtAuthenticator.java)
```java
public class AdvancedJwtAuthenticator extends JwtAuthenticator {
private final JedisPooled redis;public AdvancedJwtAuthenticator(JedisPooled redis, SignatureConfiguration signatureConfiguration) {
super(signatureConfiguration);
this.redis = redis;
}@Override
protected void createJwtProfile(TokenCredentials credentials, JWT jwt, WebContext context, SessionStore sessionStore) throws ParseException {
var jwtId = jwt.getJWTClaimsSet().getJWTID();
var uid = jwt.getJWTClaimsSet().getClaims().get(Jwt.Attribute.UID).toString();
if (!redis.exists(RedisNameSpace.getUserTokenExpirationKey(uid, jwtId))) {
throw new AuthorizationException();
}
super.createJwtProfile(credentials, jwt, context, sessionStore);
}
}```
3. For now, when you want `logout`, just delete the related `jid` in `redis`.
### [Role Access Verifier](src/main/java/io/github/jonaskahn/middlewares/role/AccessVerifier.java)
```java
@AllArgsConstructor(access = AccessLevel.PUBLIC, onConstructor = @__({@Inject}))
public class AccessVerifierImpl implements AccessVerifier {
private Context context;@Override
public boolean hasAnyRoles(String... roles) {
var rolesAsList = Arrays.asList(roles);
return Optional.ofNullable(context.getUser())
.map(UserProfile.class::cast)
.map(UserProfile::getRoles)
.stream()
.flatMap(Set::stream)
.anyMatch(rolesAsList::contains);
}@Override
public void requireAnyRoles(String... roles) {
if (!hasAnyRoles(roles)) {
throw new ForbiddenException();
}
}
}```
- `hasRole` or `hasAnyRoles` will check and return `true`/`false`, while `requireRole` and `requireAnyRoles` will
explicitly throw exception if you do not have access.### [JpaQueryExecutor](src/main/java/io/github/jonaskahn/assistant/query/JpaQueryExecutor.java)
- **Problem**: Sometimes we want to retrieve data from database via native query, but we do not want manually map
field's value from result to pojo class.
- To solve this problem we have so many ways. With the usage of Hibernate, I create JPA Query Executor to parse sql
result to object via Jackson```java
public Optional findCustomActivatedUserByPreferredUsername(Long preferredUsername) {
var query =
entityManager.createNativeQuery("select * from users where preferred_username = :preferredUsername and status = :status");
try {
var result = JpaQueryExecutor
.builder(UserDto.class)
.with(query, Map.of(
"preferredUsername", preferredUsername,
"status", Status.Code.ACTIVATED
)
)
.getSingleResult();
return Optional.of(result);
} catch (Exception e) {
log.warn("Could not find the users with given preferredUsername", e);
return Optional.empty();
}
}
```> So, we directly map database field's value to Pojo object via Jackson, if Pojo class does not have correct field name,
> please using @JsonAlias (Like database field `full_name`, pojo class `fullName`)