This library serves as a starter to develop Atlassian Connect Jira and Confluence add on. It is dependent on http4s which favors pure functional programming.
Add this to your build.sbt
:
scalacOptions += "-Ypartial-unification"
To add library dependencies:
libraryDependencies += "com.github.allantl" %% "atlassian-connect-http4s" % "0.1.0"
This example is written using http4s version 0.20.0
.
1- Define lifecycle repository
You'll need to define an implementation to save and find Atlassian Host. Note that, when add on is uninstalled, it is never deleted from storage but simply change the installed status. In production, please use real database.
import cats.effect.IO
import cats.effect.concurrent.Ref
import com.allantl.atlassian.connect.http4s.domain.AtlassianHost
import com.allantl.atlassian.connect.http4s.repository.algebra.AtlassianHostRepositoryAlgebra
class AtlassianHostRepository(storage: Ref[IO, List[AtlassianHost]]) extends AtlassianHostRepositoryAlgebra[IO] {
override def findByClientKey(clientKey: String, onlyInstalled: Boolean): IO[Option[AtlassianHost]] =
if (onlyInstalled) {
storage.get.map(_.find(h => h.clientKey == clientKey && h.installed))
} else {
storage.get.map(_.find(_.clientKey == clientKey))
}
override def findByBaseUrl(baseUrl: String, onlyInstalled: Boolean): IO[Option[AtlassianHost]] =
if (onlyInstalled) {
storage.get.map(_.find(h => h.baseUrl == baseUrl && h.installed))
} else {
storage.get.map(_.find(_.baseUrl == baseUrl))
}
override def save(atlassianHost: AtlassianHost): IO[AtlassianHost] =
storage
.update(atlassianHost :: _.filter(_.clientKey == atlassianHost.clientKey))
.map(_ => atlassianHost)
}
2- Define your service endpoints
Use AcHttpRoutes
to define your endpoints and asAcAuth
to get the authenticated user.
import com.allantl.atlassian.connect.http4s._
must be in scope.
import cats.effect.IO
import com.allantl.atlassian.connect.http4s._
import org.http4s.dsl.Http4sDsl
class AcServiceEndpoints extends Http4sDsl[IO] {
val endpoints = AcHttpRoutes.of[IO] {
case GET -> Root / "ping" asAcAuth user =>
Ok(s"Received response from ${user.host.baseUrl}")
}
}
3- Initiliaze components
import cats.effect._
import cats.effect.concurrent.Ref
import cats.implicits._
import com.allantl.atlassian.connect.http4s.auth.atlassian.jwt.JwtValidator
import com.allantl.atlassian.connect.http4s.auth.middleware.AcHttpService
import com.allantl.atlassian.connect.http4s.domain.AtlassianHost
import com.allantl.atlassian.connect.http4s.endpoints.LifecycleEndpoints
import com.allantl.atlassian.connect.http4s.services.lifecycle.LifecycleService
import org.http4s.implicits._
import org.http4s.server.Router
import org.http4s.server.blaze.BlazeServerBuilder
object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
Ref.of[IO, List[AtlassianHost]](List.empty).flatMap { ref =>
implicit val atlassianHostRepo = new AtlassianHostRepository(ref)
implicit val jwtValidator = new JwtValidator[IO]()
// AcHttpService is needed to transform `AcHttpRoutes` into http4s `HttpRoutes`
val acHttpService: AcHttpService[IO] = AcHttpService(jwtValidator)
val myService = acHttpService.liftRoutes(new AcServiceEndpoints().endpoints)
val lifecycleService = new LifecycleService[IO](atlassianHostRepo, infoLogger = log => IO.delay(println(log)))
val lifecycleEndpoints = LifecycleEndpoints(jwtValidator, atlassianHostRepo, lifecycleService).endpoints
// It is recommended to split lifecycle to different routes
val httpApp = Router(
"/api/lifecycle" -> lifecycleEndpoints,
"/api" -> myService
).orNotFound
BlazeServerBuilder[IO]
.bindHttp(8080, "localhost")
.withHttpApp(httpApp)
.serve
.compile
.drain
.as(ExitCode.Success)
}
}
}
You can take a look at the implementation here and roll your own service instead.
You can provide your own implementation of LifecycleEventHandler
when initializing LifecycleEndpoints
.
Method will run asynchronously in the background and will not block lifecycle event.
AcHttpRoutes
can be composed with other AcHttpRoutes
.
Make sure you do not compose this with Http4sRoutes
, since AcHttpRoutes
needs authentication.
import cats.implicits._
val e1 = new AcServiceEndpoints()
val e2 = new AcServiceEndpoints()
val e3 = e1.endpoints <+> e2.endpoints
There is a middleware that handles license check.
import cats.effect._
import org.http4s.dsl.io._
// For development, you can set this to false
implicit val atlassianConnectConfig = AtlassianConnectConfig(licenseCheckEnabled = true)
val notLicensedEndpoints: Request[IO] => IO[Response[IO]] = _ => Ok("License not active")
val licenseCheck: LicenseCheck[IO] = new LicenseCheck[IO](notLicensedEndpoints)
val endpoints: HttpRoutes[IO] = ???
val licensedEndpoints = licenseCheck(endpoints)
To render frontend html page, you can use http4s with twirl, take a look at the documentation.
This is explained in atlassian connect documentation, under Retrieving context using AP.context.getToken().
If you need jira client, its available here.
Interop with this library:
object JiraClient {
type JiraClient[R[_]] = JiraMultiTenantClient[R]
def apply[R[_], S](acJwtConfig: AcJwtConfig)(
implicit sttpBackend: SttpBackend[R, S]
): JiraMultiTenantClient[R] =
JiraMultiTenantClient(acJwtConfig)
}
For those who is developing with scalajs-react and is looking for atlaskit components, please take a look here