8 Commits

Author SHA1 Message Date
48b2ffd7b2 Streamlines the ongoing Hunt endpoint
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-17 22:11:52 -05:00
877e134166 Adds isAdmin to JWT
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-16 16:14:29 -05:00
ec2bb1bcc6 Adds Hunter name to login response
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-16 16:06:59 -05:00
6c3c94c5a3 Turns on CORS
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-16 15:54:24 -05:00
a34d2ddcf0 Opens up actuator endpoints
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-15 23:42:48 -05:00
b3801eb5e7 Updates Docker compose 2026-05-15 23:42:09 -05:00
4dfdb54bb4 Updates Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-05-15 14:32:15 -05:00
0a278530fb Merge pull request 'Adds docker and woodpecker files' (#5) from feature/docker into main
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: #5
2026-05-15 19:27:57 +00:00
8 changed files with 53 additions and 43 deletions

View File

@@ -3,7 +3,7 @@ WORKDIR /app
COPY gradlew . COPY gradlew .
COPY gradle/ gradle/ COPY gradle/ gradle/
COPY build.gradle.kts settings.gradle.kts ./ COPY build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon RUN chmod +x gradlew && ./gradlew dependencies --no-daemon
COPY src/ src/ COPY src/ src/
RUN ./gradlew bootJar --no-daemon RUN ./gradlew bootJar --no-daemon

View File

@@ -1,20 +1,13 @@
# All services use host networking so inter-service traffic goes over loopback with no bridge overhead.
# Ports (all bound directly on the host):
# API: 8080
# MariaDB: 3306
# Adminer: 8888
# MinIO API: 9000
# MinIO Console: 9001
services: services:
mariadb: mariadb:
image: mariadb:11 image: mariadb
network_mode: host
environment: environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME} MARIADB_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER} MARIADB_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD} MARIADB_PASSWORD: ${DB_PASSWORD}
ports:
- 3306:3306
volumes: volumes:
- mariadb_data:/var/lib/mysql - mariadb_data:/var/lib/mysql
healthcheck: healthcheck:
@@ -24,55 +17,54 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped restart: unless-stopped
adminer: adminer:
image: adminer image: adminer
network_mode: host ports:
command: php -S [::]:8888 -t /var/www/html - 8080:8080
restart: unless-stopped restart: unless-stopped
minio: minio:
image: minio/minio image: minio/minio
network_mode: host command: server /data --console-address ":9001"
command: server /data --console-address :9001
environment: environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
ports:
- 15900:9000 # API
- 15901:9001 # Web UI
volumes: volumes:
- minio_data:/data - minio_data:/data
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ["CMD", "curl", "-f", "http://192.168.187.181:15900/minio/health/live"]
start_period: 10s start_period: 10s
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped restart: unless-stopped
api: api:
build: . image: git.halfbinary.net/aarbit/scavengerhunt-api:2
network_mode: host
environment: environment:
DB_URL: jdbc:mariadb://localhost:3306/${DB_NAME} DB_URL: jdbc:mariadb://192.168.187.181:3306/${DB_NAME}
DB_USER: ${DB_USER} DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD} DB_PASSWORD: ${DB_PASSWORD}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
MINIO_ENDPOINT: http://localhost:9000 MINIO_ENDPOINT: http://192.168.187.181:15900
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_BUCKET: ${MINIO_BUCKET} MINIO_BUCKET: ${MINIO_BUCKET}
ports:
- 15808:8080
depends_on: depends_on:
mariadb: mariadb:
condition: service_healthy condition: service_healthy
minio: minio:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] test: ["CMD", "curl", "-f", "http://192.168.187.181:15808/actuator/health"]
start_period: 30s start_period: 30s
interval: 15s interval: 15s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped restart: unless-stopped
volumes: volumes:
mariadb_data: mariadb_data:
minio_data: minio_data:

View File

@@ -27,9 +27,10 @@ class JwtUtil {
} }
// Generate JWT token // Generate JWT token
fun generateToken(email: String): String { fun generateToken(email: String, isAdmin: Boolean): String {
return Jwts.builder() return Jwts.builder()
.subject(email) .subject(email)
.claim("isAdmin", isAdmin)
.issuedAt(Date()) .issuedAt(Date())
.expiration(Date(System.currentTimeMillis() + jwtExpirationMs)) .expiration(Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(key) .signWith(key)

View File

@@ -7,7 +7,6 @@ import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
@@ -16,6 +15,10 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@Configuration @Configuration
@@ -48,13 +51,25 @@ class SecurityConfig(private val authEntrypointJwt: AuthEntrypointJwt,
return BCryptPasswordEncoder() return BCryptPasswordEncoder()
} }
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val config = CorsConfiguration()
config.allowedOriginPatterns = listOf("*")
config.allowedMethods = listOf("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
config.allowedHeaders = listOf("*")
config.allowCredentials = true
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)
return source
}
@Bean @Bean
@Throws(Exception::class) @Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
// Updated configuration for Spring Security 6.x // Updated configuration for Spring Security 6.x
http http
.csrf { csrf: CsrfConfigurer<HttpSecurity> -> csrf.disable() } // Disable CSRF .csrf { csrf: CsrfConfigurer<HttpSecurity> -> csrf.disable() }
.cors { cors: CorsConfigurer<HttpSecurity> -> cors.disable() } // Disable CORS (or configure if needed) .cors { cors -> cors.configurationSource(corsConfigurationSource()) }
.exceptionHandling { exceptionHandling: ExceptionHandlingConfigurer<HttpSecurity> -> .exceptionHandling { exceptionHandling: ExceptionHandlingConfigurer<HttpSecurity> ->
exceptionHandling.authenticationEntryPoint( exceptionHandling.authenticationEntryPoint(
authEntrypointJwt authEntrypointJwt
@@ -67,7 +82,7 @@ class SecurityConfig(private val authEntrypointJwt: AuthEntrypointJwt,
} }
.authorizeHttpRequests { authorizeRequests -> .authorizeHttpRequests { authorizeRequests ->
authorizeRequests authorizeRequests
.requestMatchers("/auth/**", "/signup", "/docs/**") .requestMatchers("/auth/**", "/signup", "/docs/**", "/actuator/**")
.permitAll() .permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
} }

View File

@@ -24,9 +24,9 @@ class AuthController(private val loginService: LoginService, private val jwtUtil
@PostMapping("/login") @PostMapping("/login")
fun login(@Valid @RequestBody body: LoginRequest): ResponseEntity<LoginResponse> { fun login(@Valid @RequestBody body: LoginRequest): ResponseEntity<LoginResponse> {
val result = loginService.login(body.toDomain()) val result = loginService.login(body.toDomain())
val accessToken = jwtUtils.generateToken(result.email) val accessToken = jwtUtils.generateToken(result.email, result.isAdmin)
val refreshToken = refreshTokenService.generateRefreshToken(result.email) val refreshToken = refreshTokenService.generateRefreshToken(result.email)
val loginResponse = LoginResponse(accessToken, refreshToken) val loginResponse = LoginResponse(accessToken, refreshToken, result.name)
return ResponseEntity.ok(loginResponse) return ResponseEntity.ok(loginResponse)
} }

View File

@@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@@ -27,13 +26,13 @@ class HunterController(private val hunterService: HunterService,
@GetMapping("/hunt/ongoing") @GetMapping("/hunt/ongoing")
@Operation(summary = "Gets list of all currently running Hunts (filtered by the calling hunter)") @Operation(summary = "Gets list of all currently running Hunts (filtered by the calling hunter)")
fun getOngoingHunts(authentication: Authentication, @RequestParam status: HuntStatus?): ResponseEntity<List<HuntResponse>> { fun getOngoingHunts(authentication: Authentication): ResponseEntity<List<HuntResponse>> {
val email = authentication.name val email = authentication.name
val isAdmin = hunterService.getHunterByEmail(email).isAdmin val isAdmin = hunterService.getHunterByEmail(email).isAdmin
return if(isAdmin) { return if(isAdmin) {
ResponseEntity.ok(huntService.getAllHunts(HuntStatus.ONGOING).map { it.toResponse() }) ResponseEntity.ok(huntService.getAllHunts(HuntStatus.ONGOING).map { it.toResponse() })
} else { } else {
ResponseEntity.ok(huntService.getHuntsByEmail(email, status).map { it.toResponse() }) ResponseEntity.ok(huntService.getHuntsByEmail(email, HuntStatus.ONGOING).map { it.toResponse() })
} }
} }

View File

@@ -4,5 +4,6 @@ import net.halfbinary.scavengerhuntapi.model.RefreshId
data class LoginResponse( data class LoginResponse(
val accessToken: String, val accessToken: String,
val refreshToken: RefreshId val refreshToken: RefreshId,
val name: String
) )

View File

@@ -5,6 +5,7 @@ import net.halfbinary.scavengerhuntapi.error.exception.ExpiredRefreshTokenExcept
import net.halfbinary.scavengerhuntapi.error.exception.InvalidRefreshTokenException import net.halfbinary.scavengerhuntapi.error.exception.InvalidRefreshTokenException
import net.halfbinary.scavengerhuntapi.model.RefreshId import net.halfbinary.scavengerhuntapi.model.RefreshId
import net.halfbinary.scavengerhuntapi.model.record.RefreshTokenRecord import net.halfbinary.scavengerhuntapi.model.record.RefreshTokenRecord
import net.halfbinary.scavengerhuntapi.repository.HunterRepository
import net.halfbinary.scavengerhuntapi.repository.RefreshTokenRepository import net.halfbinary.scavengerhuntapi.repository.RefreshTokenRepository
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
@@ -13,7 +14,7 @@ import java.time.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@Service @Service
class RefreshTokenService(private val refreshTokenRepository: RefreshTokenRepository, private val jwtUtil: JwtUtil) { class RefreshTokenService(private val refreshTokenRepository: RefreshTokenRepository, private val jwtUtil: JwtUtil, private val hunterRepository: HunterRepository) {
companion object { companion object {
private val log = LoggerFactory.getLogger(RefreshTokenService::class.java) private val log = LoggerFactory.getLogger(RefreshTokenService::class.java)
@@ -25,7 +26,8 @@ class RefreshTokenService(private val refreshTokenRepository: RefreshTokenReposi
removeToken(tokenId) removeToken(tokenId)
throw ExpiredRefreshTokenException(tokenId) throw ExpiredRefreshTokenException(tokenId)
} else { } else {
jwtUtil.generateToken(refreshToken.email) val isAdmin = hunterRepository.findByEmail(refreshToken.email)?.isAdmin ?: false
jwtUtil.generateToken(refreshToken.email, isAdmin)
} }
}?: throw InvalidRefreshTokenException(tokenId) }?: throw InvalidRefreshTokenException(tokenId)
} }