diff --git a/.github/workflows/hndrs-gradle-check.yml b/.github/workflows/hndrs-gradle-check.yml index 341a2c5..0d91dae 100644 --- a/.github/workflows/hndrs-gradle-check.yml +++ b/.github/workflows/hndrs-gradle-check.yml @@ -12,6 +12,11 @@ jobs: check: runs-on: ubuntu-latest + services: + mongodb: + image: mongo + ports: + - 27017:27017 steps: - name: git checkout uses: actions/checkout@v2 @@ -33,5 +38,10 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: test + env: + ARTIFACTS_AUTH0_DOMAIN: ${{ secrets.ARTIFACTS_AUTH0_DOMAIN }} + ARTIFACTS_AUTH0_CLIENTID: ${{ secrets.ARTIFACTS_AUTH0_CLIENTID }} + ARTIFACTS_AUTH0_CLIENTSECRET: ${{ secrets.ARTIFACTS_AUTH0_CLIENTSECRET }} + HNDRS_JWT_KEYSTOREPATH: ${{ secrets.ARTIFACTS_AUTH0_KEYSTOREPATH }} run: | - ./gradlew check + ./gradlew check -Dspring.profiles.active=ci diff --git a/.github/workflows/hndrs-gradle-sonar.yml b/.github/workflows/hndrs-gradle-sonar.yml index efbe769..1224330 100644 --- a/.github/workflows/hndrs-gradle-sonar.yml +++ b/.github/workflows/hndrs-gradle-sonar.yml @@ -16,6 +16,12 @@ jobs: analyse: runs-on: ubuntu-latest + services: + mongodb: + image: mongo + ports: + - 27017:27017 + steps: - name: git checkout uses: actions/checkout@v2 @@ -46,5 +52,9 @@ jobs: - name: analyse env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + ARTIFACTS_AUTH0_DOMAIN: ${{ secrets.ARTIFACTS_AUTH0_DOMAIN }} + ARTIFACTS_AUTH0_CLIENTID: ${{ secrets.ARTIFACTS_AUTH0_CLIENTID }} + ARTIFACTS_AUTH0_CLIENTSECRET: ${{ secrets.ARTIFACTS_AUTH0_CLIENTSECRET }} + HNDRS_JWT_KEYSTOREPATH: ${{ secrets.ARTIFACTS_AUTH0_KEYSTOREPATH }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew check jacocoTestReport sonarqube --info + run: ./gradlew check jacocoTestReport sonarqube --info -Dspring.profiles.active=ci diff --git a/.gitignore b/.gitignore index c2065bc..1606c46 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ out/ ### VS Code ### .vscode/ + +### Spring Boot ### +/config/* +!/config/*.example diff --git a/application-ci.properties b/application-ci.properties new file mode 100644 index 0000000..481f7c4 --- /dev/null +++ b/application-ci.properties @@ -0,0 +1 @@ +spring.data.mongodb.uri=mongodb://mongodb/local_artifacts?retryWrites=true&w=majority diff --git a/application.properties b/application.properties new file mode 100644 index 0000000..b708994 --- /dev/null +++ b/application.properties @@ -0,0 +1,4 @@ +logging.level.org.springframework.boot.autoconfigure=DEBUG +#Mongo DB +spring.data.mongodb.auto-index-creation=true +spring.data.mongodb.uri=mongodb://localhost/local_artifacts?retryWrites=true&w=majority diff --git a/build.gradle.kts b/build.gradle.kts index cca834f..775229b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "2.4.3" - id("io.spring.dependency-management") version "1.0.11.RELEASE" - id("org.sonarqube").version("3.1.1") - kotlin("jvm") version "1.4.30" - kotlin("plugin.spring") version "1.4.30" + id("org.springframework.boot") + id("io.spring.dependency-management") + id("org.sonarqube").version("3.3") + kotlin("jvm") + kotlin("plugin.spring") + kotlin("kapt") jacoco } @@ -17,17 +18,32 @@ repositories { mavenCentral() } +dependencyManagement { + imports { + mavenBom("com.squareup.okhttp3:okhttp-bom:4.9.0") + } +} + + dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("com.auth0:auth0:1.33.0") + implementation("io.springfox:springfox-boot-starter:3.0.0") //hndrs implementation("io.hndrs:jsonapi-spring-boot-starter:1.0.0") + implementation("io.hndrs:jwt-auth-spring-boot-starter:1.0.0") + kapt(group = "org.springframework.boot", name = "spring-boot-configuration-processor") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "mockito-core") + } + testImplementation("com.ninja-squad:springmockk:3.0.1") } sonarqube { @@ -39,7 +55,7 @@ sonarqube { } jacoco { - toolVersion = "0.8.6" + toolVersion = "0.8.7" } tasks.withType { @@ -59,4 +75,7 @@ tasks.withType { tasks.withType { useJUnitPlatform() + System.getProperty("spring.profiles.active")?.let { + systemProperties.put("spring.profiles.active", it) + } } diff --git a/config/application.properties.example b/config/application.properties.example new file mode 100644 index 0000000..48db282 --- /dev/null +++ b/config/application.properties.example @@ -0,0 +1,4 @@ +hndrs.jwt.key-store-path= +artifacts.auth0.domain= +artifacts.auth0.client-id= +artifacts.auth0.client-secret= diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a528b49 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +kotlinVersion=1.5.20 +springDependencyManagement=1.0.11.RELEASE +springBootDependencies=2.5.3 diff --git a/settings.gradle.kts b/settings.gradle.kts index 91b4b62..64ee2e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,18 @@ rootProject.name = "artifacts" + +pluginManagement { + val kotlinVersion: String by settings + val springDependencyManagement: String by settings + val springBootDependencies: String by settings + + plugins { + id("org.springframework.boot").version(springBootDependencies) + id("io.spring.dependency-management").version(springDependencyManagement) + kotlin("jvm").version(kotlinVersion) + kotlin("plugin.spring").version(kotlinVersion) + kotlin("kapt").version(kotlinVersion) + id("idea") + } + repositories { + } +} diff --git a/src/main/kotlin/io/hndrs/artifacts/OpenApiConfiguration.kt b/src/main/kotlin/io/hndrs/artifacts/OpenApiConfiguration.kt new file mode 100644 index 0000000..1c6a2fb --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/OpenApiConfiguration.kt @@ -0,0 +1,39 @@ +package io.hndrs.artifacts + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import springfox.documentation.builders.RequestHandlerSelectors +import springfox.documentation.service.Tag +import springfox.documentation.spi.DocumentationType +import springfox.documentation.spi.DocumentationType.OAS_30 +import springfox.documentation.spi.service.OperationBuilderPlugin +import springfox.documentation.spi.service.contexts.OperationContext +import springfox.documentation.spring.web.plugins.Docket +import springfox.documentation.swagger.common.SwaggerPluginSupport + +@Configuration +class OpenApiConfiguration { + + + @Bean + fun docket(): Docket = Docket(OAS_30) + .tags(Tag("Artifacts Api", "analysis description")) + .select() + .apis(RequestHandlerSelectors.basePackage("io.hndrs")) + .build() +} + +@Component +@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1) +class GlobalTag : OperationBuilderPlugin { + override fun supports(delimiter: DocumentationType): Boolean { + return delimiter == OAS_30 + } + + override fun apply(context: OperationContext) { + context.operationBuilder().tags(setOf("Artifacts Api")) + } + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/configuration/Auth0Configuration.kt b/src/main/kotlin/io/hndrs/artifacts/configuration/Auth0Configuration.kt new file mode 100644 index 0000000..fb680d0 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/configuration/Auth0Configuration.kt @@ -0,0 +1,38 @@ +package io.hndrs.artifacts.configuration + +import com.auth0.client.auth.AuthAPI +import com.auth0.client.mgmt.ManagementAPI +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableConfigurationProperties(Auth0ConfigurationProperties::class) +class Auth0Configuration(private val properties: Auth0ConfigurationProperties) { + + @Bean + fun managementApi(): ManagementAPI { + val authAPI = AuthAPI( + properties.domain, + properties.clientId, + properties.clientSecret + ) + + val accessToken = authAPI.requestToken("${properties.domain}/api/v2/").execute().accessToken + + return ManagementAPI(properties.domain, accessToken) + } +} + +@ConfigurationProperties(prefix = "artifacts.auth0") +open class Auth0ConfigurationProperties { + + lateinit var domain: String + + lateinit var clientId: String + + lateinit var clientSecret: String + + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/configuration/OpenApiConfiguration.kt b/src/main/kotlin/io/hndrs/artifacts/configuration/OpenApiConfiguration.kt new file mode 100644 index 0000000..a9db3dd --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/configuration/OpenApiConfiguration.kt @@ -0,0 +1,2 @@ +package io.hndrs.artifacts.configuration + diff --git a/src/main/kotlin/io/hndrs/artifacts/domain/OrganisationController.kt b/src/main/kotlin/io/hndrs/artifacts/domain/OrganisationController.kt deleted file mode 100644 index 8a6c1a3..0000000 --- a/src/main/kotlin/io/hndrs/artifacts/domain/OrganisationController.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.hndrs.artifacts.domain - -import io.hndrs.api.response.JsonApiResponse -import io.hndrs.artifacts.shared.Organisation -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -@RestController -class OrganisationController { - - - @GetMapping("/organisations") - @JsonApiResponse - fun organisations(): List { - return listOf(Organisation("1")) - } - -} diff --git a/src/main/kotlin/io/hndrs/artifacts/domain/UserController.kt b/src/main/kotlin/io/hndrs/artifacts/module/artifacts/ArtifactsController.kt similarity index 55% rename from src/main/kotlin/io/hndrs/artifacts/domain/UserController.kt rename to src/main/kotlin/io/hndrs/artifacts/module/artifacts/ArtifactsController.kt index bd83a06..14a701c 100644 --- a/src/main/kotlin/io/hndrs/artifacts/domain/UserController.kt +++ b/src/main/kotlin/io/hndrs/artifacts/module/artifacts/ArtifactsController.kt @@ -1,18 +1,14 @@ -package io.hndrs.artifacts.domain +package io.hndrs.artifacts.module.artifacts import io.hndrs.api.response.JsonApiResponse -import io.hndrs.artifacts.shared.User import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController -class UserController { +class ArtifactsController { - - @GetMapping("/user") + @GetMapping("/artifacts") @JsonApiResponse - fun user(): User { - return User("id") + fun artifacts() { } - } diff --git a/src/main/kotlin/io/hndrs/artifacts/module/organisation/OrganisationArtifactsController.kt b/src/main/kotlin/io/hndrs/artifacts/module/organisation/OrganisationArtifactsController.kt new file mode 100644 index 0000000..e083954 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/organisation/OrganisationArtifactsController.kt @@ -0,0 +1,26 @@ +package io.hndrs.artifacts.module.organisation + +import io.hndrs.api.response.JsonApiResponse +import io.swagger.annotations.ApiImplicitParam +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController + +@RestController +class OrganisationArtifactsController { + + @ApiImplicitParam( + name = "Authorization", + value = "Id Token", + required = true, + allowEmptyValue = false, + paramType = "header", + dataTypeClass = String::class, + example = "Bearer id_token" + ) + @GetMapping("/organisations/{orgainsation_id}/artifacts") + @JsonApiResponse + fun organisationArtifacts(@PathVariable("orgainsation_id") organisationId: String) { + } + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/module/organisation/OrganisationsController.kt b/src/main/kotlin/io/hndrs/artifacts/module/organisation/OrganisationsController.kt new file mode 100644 index 0000000..b526111 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/organisation/OrganisationsController.kt @@ -0,0 +1,26 @@ +package io.hndrs.artifacts.module.organisation + +import io.hndrs.api.response.JsonApiResponse +import io.hndrs.jwt.Identity +import io.swagger.annotations.ApiImplicitParam +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class OrganisationsController { + + @ApiImplicitParam( + name = "Authorization", + value = "Id Token", + required = true, + allowEmptyValue = false, + paramType = "header", + dataTypeClass = String::class, + example = "Bearer id_token" + ) + @GetMapping("/organisations") + @JsonApiResponse + fun organisation(@Identity user: Map) { + } + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/module/project/ProjectsController.kt b/src/main/kotlin/io/hndrs/artifacts/module/project/ProjectsController.kt new file mode 100644 index 0000000..3846495 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/project/ProjectsController.kt @@ -0,0 +1,26 @@ +package io.hndrs.artifacts.module.project + +import io.hndrs.api.response.JsonApiResponse +import io.hndrs.jwt.Identity +import io.swagger.annotations.ApiImplicitParam +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class ProjectsController { + + @ApiImplicitParam( + name = "Authorization", + value = "Id Token", + required = true, + allowEmptyValue = false, + paramType = "header", + dataTypeClass = String::class, + example = "Bearer id_token" + ) + @GetMapping("/projects") + @JsonApiResponse + fun projects(@Identity user: Map) { + } + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/module/team/TeamsController.kt b/src/main/kotlin/io/hndrs/artifacts/module/team/TeamsController.kt new file mode 100644 index 0000000..a44df8a --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/team/TeamsController.kt @@ -0,0 +1,26 @@ +package io.hndrs.artifacts.module.team + +import io.hndrs.api.response.JsonApiResponse +import io.hndrs.jwt.Identity +import io.swagger.annotations.ApiImplicitParam +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class TeamsController { + + @ApiImplicitParam( + name = "Authorization", + value = "Id Token", + required = true, + allowEmptyValue = false, + paramType = "header", + dataTypeClass = String::class, + example = "Bearer id_token" + ) + @GetMapping("/teams") + @JsonApiResponse + fun teams(@Identity user: Map) { + } + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/module/user/User.kt b/src/main/kotlin/io/hndrs/artifacts/module/user/User.kt new file mode 100644 index 0000000..91f22d2 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/user/User.kt @@ -0,0 +1,29 @@ +package io.hndrs.artifacts.module.user + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import org.springframework.data.mongodb.core.mapping.Field +import org.springframework.data.mongodb.repository.MongoRepository +import java.time.Instant + +@Document("users") +data class User( + @Id + val id: String? = null, + + @Indexed(unique = true) + @Field("email") + val email: String, + + @Field("name") + val name: String?, + + @Field("createdAt") + val createdAt: Instant = Instant.now(), + + @Field("lastModifiedAt") + val lastModifiedAt: Instant = Instant.now(), +) + +interface UserRepository : MongoRepository diff --git a/src/main/kotlin/io/hndrs/artifacts/module/user/UserController.kt b/src/main/kotlin/io/hndrs/artifacts/module/user/UserController.kt new file mode 100644 index 0000000..1b56bb5 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/user/UserController.kt @@ -0,0 +1,27 @@ +package io.hndrs.artifacts.module.user + +import io.hndrs.api.response.JsonApiResponse +import io.hndrs.jwt.Identity +import io.swagger.annotations.ApiImplicitParam +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController() +class UserController { + + @ApiImplicitParam( + name = "Authorization", + value = "Id Token", + required = true, + allowEmptyValue = false, + paramType = "header", + dataTypeClass = String::class, + example = "Bearer id_token" + ) + @GetMapping("/user") + @JsonApiResponse + fun user(@Identity user: Map): Map { + return user + } + +} diff --git a/src/main/kotlin/io/hndrs/artifacts/module/user/registration/Registration.kt b/src/main/kotlin/io/hndrs/artifacts/module/user/registration/Registration.kt new file mode 100644 index 0000000..0425e25 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/user/registration/Registration.kt @@ -0,0 +1,28 @@ +package io.hndrs.artifacts.module.user.registration + +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class RegistrationController(private val registrationService: RegistrationService) { + + @PostMapping("/register") + fun register(@RequestBody request: RegistrationRequest) { + registrationService.register(request) + } +} + +data class RegistrationRequest( + + @param:JsonProperty("email") + val email: String, + + @param:JsonProperty("password") + val password: String, + + @param:JsonProperty("name") + val name: String? = null +) + diff --git a/src/main/kotlin/io/hndrs/artifacts/module/user/registration/RegistrationService.kt b/src/main/kotlin/io/hndrs/artifacts/module/user/registration/RegistrationService.kt new file mode 100644 index 0000000..d36feb8 --- /dev/null +++ b/src/main/kotlin/io/hndrs/artifacts/module/user/registration/RegistrationService.kt @@ -0,0 +1,28 @@ +package io.hndrs.artifacts.module.user.registration + +import com.auth0.client.mgmt.ManagementAPI +import com.auth0.json.mgmt.users.User +import io.hndrs.artifacts.module.user.UserRepository +import org.springframework.stereotype.Service + +@Service +class RegistrationService( + private val userRepository: UserRepository, + private val managementAPI: ManagementAPI +) { + fun register(request: RegistrationRequest) { + userRepository.save( + io.hndrs.artifacts.module.user.User( + email = request.email, + name = request.name + ) + ).let { + val user = User("Username-Password-Authentication") + user.id = it.id + user.email = it.email + user.name = it.name + user.setPassword(request.password.toCharArray()) + managementAPI.users().create(user).execute() + } + } +} diff --git a/src/main/kotlin/io/hndrs/artifacts/shared/Organisation.kt b/src/main/kotlin/io/hndrs/artifacts/shared/Organisation.kt deleted file mode 100644 index e220303..0000000 --- a/src/main/kotlin/io/hndrs/artifacts/shared/Organisation.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.hndrs.artifacts.shared - -import com.fasterxml.jackson.annotation.JsonProperty - -data class Organisation( - @field:JsonProperty(ID_PROPERTY_NAME) - @param:JsonProperty(ID_PROPERTY_NAME) - val id: String -) { - - companion object { - const val ID_PROPERTY_NAME = "id" - } -} diff --git a/src/main/kotlin/io/hndrs/artifacts/shared/User.kt b/src/main/kotlin/io/hndrs/artifacts/shared/User.kt deleted file mode 100644 index a14427f..0000000 --- a/src/main/kotlin/io/hndrs/artifacts/shared/User.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.hndrs.artifacts.shared - -import com.fasterxml.jackson.annotation.JsonProperty - -data class User( - @field:JsonProperty(ID_PROPERTY_NAME) - @param:JsonProperty(ID_PROPERTY_NAME) - val id: String -) { - - companion object { - const val ID_PROPERTY_NAME = "id" - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..b708994 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,4 @@ - +logging.level.org.springframework.boot.autoconfigure=DEBUG +#Mongo DB +spring.data.mongodb.auto-index-creation=true +spring.data.mongodb.uri=mongodb://localhost/local_artifacts?retryWrites=true&w=majority diff --git a/src/test/kotlin/io/hndrs/artifacts/module/user/registration/RegistrationServiceTest.kt b/src/test/kotlin/io/hndrs/artifacts/module/user/registration/RegistrationServiceTest.kt new file mode 100644 index 0000000..bb86fbd --- /dev/null +++ b/src/test/kotlin/io/hndrs/artifacts/module/user/registration/RegistrationServiceTest.kt @@ -0,0 +1,44 @@ +package io.hndrs.artifacts.module.user.registration + +import com.auth0.client.mgmt.ManagementAPI +import com.auth0.client.mgmt.UsersEntity +import io.hndrs.artifacts.module.user.User +import io.hndrs.artifacts.module.user.UserRepository +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Test +import java.util.* + +internal class RegistrationServiceTest { + + + @Test + fun register() { + val userRepository = mockk() { + every { save(any()) } returns User( + id = UUID.randomUUID().toString(), + email = "email@mail.com", + name = "name" + ) + } + val usersEntity = mockk(relaxed = true) { + every { create(any()) } returns mockk(relaxed = true) { + every { execute() } returns mockk() + } + } + val managementAPI = mockk() { + every { users() } returns usersEntity + } + RegistrationService(userRepository, managementAPI) + .register( + RegistrationRequest( + email = "email@mail.com", + password = "password", + name = "name" + ) + ) + + verify(exactly = 1) { userRepository.save(any()) } + } +}