{"id":15514432,"url":"https://github.com/jonaskahn/kooby-api-template","last_synced_at":"2025-07-20T09:37:25.302Z","repository":{"id":249032855,"uuid":"830457632","full_name":"jonaskahn/kooby-api-template","owner":"jonaskahn","description":"Just a working Jooby backend API example project in Kotlin","archived":false,"fork":false,"pushed_at":"2024-11-25T15:04:18.000Z","size":148,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-10T13:57:14.655Z","etag":null,"topics":["api-template","backend-api-boilerplate","jooby","kooby","kotlin","template"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","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-18T10:05:01.000Z","updated_at":"2024-11-25T15:04:47.000Z","dependencies_parsed_at":"2024-11-05T06:41:04.231Z","dependency_job_id":"6c5d833d-0c73-48a8-9d87-d9543cafdaa5","html_url":"https://github.com/jonaskahn/kooby-api-template","commit_stats":null,"previous_names":["jonaskahn/kooby-api-template"],"tags_count":1,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fkooby-api-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fkooby-api-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fkooby-api-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fkooby-api-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonaskahn","download_url":"https://codeload.github.com/jonaskahn/kooby-api-template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonaskahn%2Fkooby-api-template/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259184739,"owners_count":22818267,"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-boilerplate","jooby","kooby","kotlin","template"],"created_at":"2024-10-02T09:59:09.982Z","updated_at":"2025-06-11T02:06:32.261Z","avatar_url":"https://github.com/jonaskahn.png","language":"Kotlin","readme":"# Why Kooby?\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 are undoubtedly the way to go, but for minimal projects, Kooby is a good choice.\n\n## Looking for Java version\n- [Checkout](https://github.com/jonaskahn/jooby-api-template)\n\n## What's included?\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\n1. Create users \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```\n2. Generate token\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```\n3. Get User Info\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```shell\n# With JPAQueryExecutor\ncurl --location 'http://localhost:8080/api/secure/user/info-with-executor' \\\n--header 'Accept-Language: vi'\n--header 'Authorization: ••••••'\n```\n4. Test Role\n```shell\ncurl --location 'http://localhost:8080/api/secure/test/admin-role' \\\n--header 'Authorization: ••••••'\n```\n5. Logout\n```shell\ncurl --location --request DELETE 'http://localhost:8080/api/auth/secure/logout' \\\n--header 'Authorization: ••••••'\n```\n## HOW\n### Implement JWT\n- Using [Pac4j](https://jooby.io/modules/pac4j/) Module as Security Layer\n```kotlin\ninstall(\n        Pac4jModule()\n            .client(\"/api/secure/*\") {\n                HeaderClient(\n                    \"Authorization\",\n                    \"Bearer \",\n                    AdvancedJwtAuthenticator(\n                        require(JedisPooled::class.java),\n                        SecretSignatureConfiguration(it.getString(\"jwt.salt\"))\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 simple check to ensure the `jid` exists in `redis`. If no, token is invalid\n\nSee [AdvancedJwtAuthenticator.kt](src/main/kotlin/io/github/jonaskahn/middlewares/jwt/AdvancedJwtAuthenticator.kt)\n```kotlin\nclass AdvancedJwtAuthenticator(private val redis: JedisPooled, signatureConfiguration: SignatureConfiguration) :\n    JwtAuthenticator(signatureConfiguration) {\n    @Throws(ParseException::class)\n    override fun createJwtProfile(\n        credentials: TokenCredentials, jwt: JWT, context: WebContext?,\n        sessionStore: SessionStore?\n    ) {\n        val jwtId = jwt.jwtClaimsSet.jwtid.toString()\n        val uid = jwt.jwtClaimsSet.claims[Jwt.Attribute.UID].toString()\n        if (!redis.exists(RedisNameSpace.getUserTokenExpirationKey(uid, jwtId))) {\n            throw AuthorizationException()\n        }\n        super.createJwtProfile(credentials, jwt, context, sessionStore)\n    }\n}\n```\n3. For now, when you want `logout`, just delete the related `jid` in `redis`.\n\n### [Role Access Verifier](src/main/kotlin/io/github/jonaskahn/middlewares/role/AccessVerifier.kt)\n\n```kotlin\ninternal class AccessVerifierImpl @Inject constructor(private val context: Context) : AccessVerifier {\n    override fun hasRole(role: String): Boolean {\n        val user = context.getUser\u003cUserProfile\u003e()\n        return user?.roles?.contains(role) ?: false\n    }\n\n    override fun requireRole(role: String) {\n        if (!hasRole(role)) {\n            throw ForbiddenAccessException()\n        }\n    }\n\n    override fun hasAnyRoles(vararg roles: String): Boolean {\n        val user = context.getUser\u003cUserProfile\u003e()\n        return user?.roles?.any { roles.contains(it) } ?: false\n    }\n\n    override fun requireAnyRoles(vararg roles: String) {\n        if (!hasAnyRoles(*roles)) {\n            throw ForbiddenAccessException()\n        }\n    }\n}\n```\n- `hasRole` or `hasAnyRoles` will check and return `true`/`false`, while `requireRole` and `requireAnyRoles` will explicitly throw exception if you do not have access.\n\n### [JpaQueryExecutor](src/main/kotlin/io/github/jonaskahn/assistant/query/JpaQueryExecutor.kt)\n- **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. \n- 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\n```kotlin\nclass UserRepositoryImpl @Inject constructor(\n    private val em: EntityManager\n) : UserRepository {\n    override fun findCustomActivatedUserByPreferredUsername(preferredUsername: Long): UserInfoDto? {\n        val query =\n            em.createNativeQuery(\"select * from users where preferred_username = :preferredUsername and status = ${StatusCode.ACTIVATED}\")\n        return JpaQueryExecutor.builder\u003cUserInfoDto\u003e()\n            .with(query, mutableMapOf(\"preferredUsername\" to preferredUsername))\n            .map(UserInfoDto::class.java)\n            .getSingleResult()\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, 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%2Fkooby-api-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonaskahn%2Fkooby-api-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonaskahn%2Fkooby-api-template/lists"}