{"id":15514429,"url":"https://github.com/jonaskahn/jooby-api-template","last_synced_at":"2025-04-04T11:30:33.218Z","repository":{"id":250291859,"uuid":"833594432","full_name":"jonaskahn/jooby-api-template","owner":"jonaskahn","description":"Just a working Jooby backend API example project in Java","archived":true,"fork":false,"pushed_at":"2024-08-25T16:01:23.000Z","size":68,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-29T08:07:59.767Z","etag":null,"topics":["api-template","backend-api","backend-template","java","jooby"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jonaskahn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-25T11:11:10.000Z","updated_at":"2024-10-29T16:09:52.000Z","dependencies_parsed_at":"2024-08-07T08:15:25.546Z","dependency_job_id":"83041d63-f4c2-48d5-af06-6bd214fdb53a","html_url":"https://github.com/jonaskahn/jooby-api-template","commit_stats":null,"previous_names":["jonaskahn/jooby-api-template"],"tags_count":0,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fjooby-api-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fjooby-api-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fjooby-api-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fjooby-api-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonaskahn","download_url":"https://codeload.github.com/jonaskahn/jooby-api-template/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247169787,"owners_count":20895348,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-template","backend-api","backend-template","java","jooby"],"created_at":"2024-10-02T09:59:08.826Z","updated_at":"2025-04-04T11:30:28.209Z","avatar_url":"https://github.com/jonaskahn.png","language":"Java","readme":"# Why Jooby?\n\n- Fast, light, easy to learn\n- I've been using Spring for almost my wokring time with Java/Kotlin. For large scale enterprise projects, Spring Stacks\n  are undoubtedly the way to go, but for minimal projects, Jooby is a good choice.\n\n## Looking for Kotlin version\n\n- [Checkout](https://github.com/jonaskahn/kooby-api-template)\n\n## What's included?\n\n- [x] Support Default JWT\n- [x] Support Role Access Layer\n- [x] Hibernate, Flyway support by default\n- [x] Add custom JPAQueryExecutor for the better querying\n- [x] Add Jedis support instead of Lettuce\n- [x] Using MapStruct for Object Mapper\n- [x] Using Guice as Dependency Injection Framework\n- [x] Multiple language support\n- [x] Default admin user: `admin@localhost/admin`\n\n## Default API\n\n1. Create users\n\n```shell\ncurl --location 'http://localhost:8080/api/auth/register' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"name\": \"test\",\n    \"email\": \"test@localhost\",\n    \"password\": \"test\"\n}'\n```\n\n2. Generate token\n\n```shell\ncurl --location 'http://localhost:8080/api/auth/generate-token' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"username\": \"test@localhost\",\n    \"password\": \"test\"\n}'\n```\n\n3. Get User Info\n\n```shell\n# With JPA Query\ncurl --location 'http://localhost:8080/api/secure/user/info' \\\n--header 'Accept-Language: vi'\n--header 'Authorization: ••••••'\n```\n\n```shell\n# With JPAQueryExecutor\ncurl --location 'http://localhost:8080/api/secure/user/info-with-executor' \\\n--header 'Accept-Language: vi'\n--header 'Authorization: ••••••'\n```\n\n4. Test Role\n\n```shell\ncurl --location 'http://localhost:8080/api/secure/test/admin-role' \\\n--header 'Authorization: ••••••'\n```\n\n5. Logout\n\n```shell\ncurl --location --request DELETE 'http://localhost:8080/api/auth/secure/logout' \\\n--header 'Authorization: ••••••'\n```\n\n## HOW\n\n### Implement JWT\n\n- Using [Pac4j](https://jooby.io/modules/pac4j/) Module as Security Layer\n\n```java\n\n        install(new Pac4jModule().client(\n                        \"/api/secure/*\",\n                        conf -\u003e new HeaderClient(\n                                \"Authorization\",\n                                \"Bearer \",\n                                new AdvancedJwtAuthenticator(\n                                        require(JedisPooled.class),\n                                        new SecretSignatureConfiguration(conf.getString(\"jwt.salt\")\n                                        )\n                                )\n                        )\n                )\n        );\n\n\n```\n\n1. Using `HeaderClient` to tell Jooby read `Bearer` token from header\n2. By default Jooby use the `JwtAuthenticator` from Pac4j, the problems are:\n    - Token is completed stateless\n    - What if user is lock/inactivated/deleted -\u003e token may still valid by the `exp` -\u003e user still can access to system\n    - There is no truly `logout`\n\nSo, I solved these problems by store `jid` of JWT in Redis, after `validate` raw token, before `createProfile` I made a\nsimple check to ensure the `jid` exists in `redis`. If no, token is invalid\n\nSee [AdvancedJwtAuthenticator.kt](src/main/java/io/github/jonaskahn/middlewares/jwt/AdvancedJwtAuthenticator.java)\n\n```java\npublic class AdvancedJwtAuthenticator extends JwtAuthenticator {\n    private final JedisPooled redis;\n\n    public AdvancedJwtAuthenticator(JedisPooled redis, SignatureConfiguration signatureConfiguration) {\n        super(signatureConfiguration);\n        this.redis = redis;\n    }\n\n    @Override\n    protected void createJwtProfile(TokenCredentials credentials, JWT jwt, WebContext context, SessionStore sessionStore) throws ParseException {\n        var jwtId = jwt.getJWTClaimsSet().getJWTID();\n        var uid = jwt.getJWTClaimsSet().getClaims().get(Jwt.Attribute.UID).toString();\n        if (!redis.exists(RedisNameSpace.getUserTokenExpirationKey(uid, jwtId))) {\n            throw new AuthorizationException();\n        }\n        super.createJwtProfile(credentials, jwt, context, sessionStore);\n    }\n}\n\n```\n\n3. For now, when you want `logout`, just delete the related `jid` in `redis`.\n\n### [Role Access Verifier](src/main/java/io/github/jonaskahn/middlewares/role/AccessVerifier.java)\n\n```java\n\n@AllArgsConstructor(access = AccessLevel.PUBLIC, onConstructor = @__({@Inject}))\npublic class AccessVerifierImpl implements AccessVerifier {\n    private Context context;\n\n    @Override\n    public boolean hasAnyRoles(String... roles) {\n        var rolesAsList = Arrays.asList(roles);\n        return Optional.ofNullable(context.getUser())\n                .map(UserProfile.class::cast)\n                .map(UserProfile::getRoles)\n                .stream()\n                .flatMap(Set::stream)\n                .anyMatch(rolesAsList::contains);\n    }\n\n    @Override\n    public void requireAnyRoles(String... roles) {\n        if (!hasAnyRoles(roles)) {\n            throw new ForbiddenException();\n        }\n    }\n}\n\n```\n\n- `hasRole` or `hasAnyRoles` will check and return `true`/`false`, while `requireRole` and `requireAnyRoles` will\n  explicitly throw exception if you do not have access.\n\n### [JpaQueryExecutor](src/main/java/io/github/jonaskahn/assistant/query/JpaQueryExecutor.java)\n\n- **Problem**: Sometimes we want to retrieve data from database via native query, but we do not want manually map\n  field's value from result to pojo class.\n- To solve this problem we have so many ways. With the usage of Hibernate, I create JPA Query Executor to parse sql\n  result to object via Jackson\n\n```java\npublic Optional\u003cUserDto\u003e findCustomActivatedUserByPreferredUsername(Long preferredUsername) {\n    var query =\n            entityManager.createNativeQuery(\"select * from users where preferred_username = :preferredUsername and status = :status\");\n    try {\n        var result = JpaQueryExecutor\n                .builder(UserDto.class)\n                .with(query, Map.of(\n                                \"preferredUsername\", preferredUsername,\n                                \"status\", Status.Code.ACTIVATED\n                        )\n                )\n                .getSingleResult();\n        return Optional.of(result);\n    } catch (Exception e) {\n        log.warn(\"Could not find the users with given preferredUsername\", e);\n        return Optional.empty();\n    }\n}\n```\n\n\u003e So, we directly map database field's value to Pojo object via Jackson, if Pojo class does not have correct field name,\n\u003e please using @JsonAlias (Like database field `full_name`, pojo class `fullName`)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonaskahn%2Fjooby-api-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonaskahn%2Fjooby-api-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonaskahn%2Fjooby-api-template/lists"}