Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1e20538
feat: add service
sehyuk080101 Feb 26, 2026
fa8cba7
Merge branch 'main' into feature/add-service
sehyuk080101 Mar 3, 2026
062903f
style: apply ktlint format
sehyuk080101 Mar 3, 2026
b26c5ee
feat: add functionality to delete service
sehyuk080101 Mar 3, 2026
c0c092b
feat: add service
sehyuk080101 Mar 4, 2026
2bea435
refactor: change endpoint method from POST to DELETE
sehyuk080101 Mar 4, 2026
de09664
feat: add request validation using @Valid
sehyuk080101 Mar 4, 2026
56cac90
fix: apply provided port on AppService creation
sehyuk080101 Mar 4, 2026
41f160e
refactor: rename createAsyncJwt to createJwt
sehyuk080101 Mar 4, 2026
8ed53fc
refactor: replace absolute GitHub API URL with baseUrl
sehyuk080101 Mar 4, 2026
d760214
refactor: remove duplicate forbidden/access denied error type
sehyuk080101 Mar 4, 2026
0b1c8d4
refactor: optimize delete git connection logic
sehyuk080101 Mar 4, 2026
94ae25f
feat: add page response
sehyuk080101 Mar 4, 2026
046be68
feat: add OffsetLimit for pagination
sehyuk080101 Mar 4, 2026
ccdb69d
refactor: move Page to different package
sehyuk080101 Mar 4, 2026
22785d7
feat: add GitHub repository pagination and installation token refresh
sehyuk080101 Mar 4, 2026
52f00e2
fix: replace @NotBlank with @NotNull for UUID fields
sehyuk080101 Mar 5, 2026
ed5e030
fix: remove overridden JPA fields in DatabaseService
sehyuk080101 Mar 5, 2026
500736d
feat: add createdAt/updatedAt fields
sehyuk080101 Mar 5, 2026
c67913a
docs: add API documentation for services
sehyuk080101 Mar 5, 2026
23d361c
chore: delete unused field
sehyuk080101 Mar 6, 2026
cdda00e
docs: add GitConnectionControllerDocsTest for API documentation
sehyuk080101 Mar 6, 2026
f98ebd0
docs: add GitRepositoryControllerDocsTest for API documentation
sehyuk080101 Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions clients/client-git/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")

api("io.jsonwebtoken:jjwt-api:${property("jjwtVersion")}")
runtimeOnly("io.jsonwebtoken:jjwt-impl:${property("jjwtVersion")}")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:${property("jjwtVersion")}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kr.proxia.client.git

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestClient

@Configuration
@EnableConfigurationProperties(GitProperties::class)
class GitConfig {
@Bean
fun githubWebRestClient(): RestClient =
RestClient
.builder()
.baseUrl("https://api.github.com")
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.proxia.client.git

import kr.proxia.client.git.github.GitHubProperties
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "git")
data class GitProperties(
val github: GitHubProperties,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kr.proxia.client.git.github

import io.jsonwebtoken.Jwts
import org.springframework.stereotype.Component
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import java.util.Date

@Component
class GitHubAppJwtProvider {
fun createJwt(
appId: String,
privateKeyPem: String,
): String {
val now = System.currentTimeMillis()
val issuedAt = Date(now - 60_000)
val expiration = Date(now + 600_000)

return Jwts
.builder()
.issuer(appId)
.issuedAt(issuedAt)
.expiration(expiration)
.signWith(decodePrivateKey(privateKeyPem))
.compact()
}

private fun decodePrivateKey(pem: String): PrivateKey {
val cleanPem =
pem
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s".toRegex(), "")

val decoded = Base64.getDecoder().decode(cleanPem)
val spec = PKCS8EncodedKeySpec(decoded)
val kf = KeyFactory.getInstance("RSA")

return kf.generatePrivate(spec)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.proxia.client.git.github

import kr.proxia.client.git.GitProperties
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.client.body

@Component
class GitHubConnectionClient(
private val githubWebRestClient: RestClient,
private val jwtProvider: GitHubAppJwtProvider,
private val properties: GitProperties,
) {
fun getInstallationToken(installationId: String): InstallationTokenResponse {
val jwt = jwtProvider.createJwt(properties.github.appId, properties.github.privateKey)

return githubWebRestClient
.post()
.uri("/app/installations/$installationId/access_tokens")
.header("Authorization", "Bearer $jwt")
.header("Accept", "application/vnd.github+json")
.retrieve()
.body<InstallationTokenResponse>()!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.proxia.client.git.github

data class GitHubProperties(
val appId: String,
val privateKey: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.proxia.client.git.github

import com.fasterxml.jackson.annotation.JsonProperty

data class GitHubRepositoriesApiResponse(
@JsonProperty("total_count")
val totalCount: Int,
@JsonProperty("repositories")
val repositories: List<GitHubRepository>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.proxia.client.git.github

data class GitHubRepositoriesResponse(
val repositories: List<GitHubRepository>,
val hasNext: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.proxia.client.git.github

import com.fasterxml.jackson.annotation.JsonProperty

data class GitHubRepository(
val id: Long,
val name: String,
@JsonProperty("full_name")
val fullName: String,
@JsonProperty("default_branch")
val defaultBranch: String,
val private: Boolean,
@JsonProperty("html_url")
val htmlUrl: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package kr.proxia.client.git.github

import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient

@Component
class GitHubRepositoryClient(
private val githubWebRestClient: RestClient,
) {
fun getGithubRepositories(
accessToken: String,
page: Int,
perPage: Int,
): GitHubRepositoriesResponse {
val entity =
githubWebRestClient
.get()
.uri {
it
.path("/installation/repositories")
.queryParam("page", page)
.queryParam("per_page", perPage)
.build()
}.header("Authorization", "Bearer $accessToken")
.header("Accept", "application/vnd.github+json")
.retrieve()
.toEntity(GitHubRepositoriesApiResponse::class.java)

return GitHubRepositoriesResponse(
repositories = entity.body!!.repositories,
hasNext = entity.headers.hasNextPage(),
)
}

fun verifyRepositoryAccess(
token: String,
repoFullName: String,
): Boolean =
try {
githubWebRestClient
.get()
.uri("/repos/$repoFullName")
.header("Authorization", "Bearer $token")
.header("Accept", "application/vnd.github+json")
.retrieve()
.toBodilessEntity()
.statusCode.is2xxSuccessful
} catch (_: Exception) {
false
}

private fun HttpHeaders.hasNextPage(): Boolean {
val link = getFirst("Link") ?: return false
return NEXT_PAGE_REGEX.containsMatchIn(link)
}

companion object {
private val NEXT_PAGE_REGEX = Regex("""<([^>]+)>\s*;\s*rel="next"""")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.proxia.client.git.github

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime

data class InstallationTokenResponse(
val token: String,
@JsonProperty("expires_at")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
val expiresAt: LocalDateTime,
val permissions: Map<String, String>? = null,
@JsonProperty("repository_selection")
val repositorySelection: String? = null,
)
4 changes: 4 additions & 0 deletions clients/client-git/src/main/resources/client-git.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
git:
github:
app-id: ${GITHUB_APP_ID:}
private-key: ${GITHUB_PRIVATE_KEY:}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.client.RestClient
import org.springframework.web.client.body

@Component
class GitHubOAuthClient(
Expand All @@ -29,7 +30,7 @@ class GitHubOAuthClient(
.accept(MediaType.APPLICATION_JSON)
.body(params)
.retrieve()
.body(GitHubTokenResponse::class.java)!!
.body<GitHubTokenResponse>()!!
}

fun getUserInfo(accessToken: String): GitHubUserInfo =
Expand All @@ -38,7 +39,7 @@ class GitHubOAuthClient(
.uri("/user")
.header("Authorization", "Bearer $accessToken")
.retrieve()
.body(GitHubUserInfo::class.java)!!
.body<GitHubUserInfo>()!!

fun getUserEmails(accessToken: String): List<GitHubEmail> =
githubApiRestClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.client.RestClient
import org.springframework.web.client.body

@Component
class GoogleOAuthClient(
Expand All @@ -28,7 +29,7 @@ class GoogleOAuthClient(
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(params)
.retrieve()
.body(GoogleTokenResponse::class.java)!!
.body<GoogleTokenResponse>()!!
}

fun getUserInfo(accessToken: String): GoogleUserInfo =
Expand All @@ -37,5 +38,5 @@ class GoogleOAuthClient(
.uri("/oauth2/v2/userinfo")
.header("Authorization", "Bearer $accessToken")
.retrieve()
.body(GoogleUserInfo::class.java)!!
.body<GoogleUserInfo>()!!
}
1 change: 1 addition & 0 deletions core/core-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
implementation(project(":storage:db-core"))
implementation(project(":clients:client-oauth"))
implementation(project(":clients:client-aws"))
implementation(project(":clients:client-git"))
implementation(project(":clients:client-docker"))
implementation(project(":support:logging"))
implementation(project(":support:monitoring"))
Expand Down
Loading
Loading