Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8e450b0
Set up confimation emails for submitting receipts
johngothe Apr 27, 2025
5098717
add bucket4j dependency
mnyflot Oct 19, 2025
1a5b6fa
Merge branch 'dev' into setup-email-confirmation-for-receipts
johngothe Oct 19, 2025
662cf8b
Merge pull request #81 from appKom/setup-email-confirmation-for-receipts
johngothe Oct 19, 2025
3634b99
Fixed mailservice
johngothe Oct 19, 2025
02f97d7
add bucket4j dependency
mnyflot Oct 19, 2025
b28fff8
fix: handle nullable ReceiptPaymentInformation safely in ReceiptService
johngothe Nov 2, 2025
f0ec41e
minor fix
johngothe Nov 2, 2025
c728145
merged with main
johngothe Nov 2, 2025
682cb82
add bucket4j dependency
mnyflot Nov 2, 2025
5802d55
add bucket4j dependency
mnyflot Nov 2, 2025
ae3742a
add bucket4j dependency
mnyflot Nov 2, 2025
0a76c23
Merge pull request #62 from appKom/setup-email-confirmation-for-receipts
johngothe Nov 2, 2025
e4d1c26
merge main
mnyflot Nov 2, 2025
e58bdc7
merged with main, major fix
johngothe Nov 2, 2025
53d2099
Merge pull request #86 from appKom/75-add-rate-limiting-with-heroku
mnyflot Nov 2, 2025
e79cb01
Merge pull request #87 from appKom/85-fix-deployment-authentication-f…
johngothe Dec 14, 2025
eb648ef
Revert "Merge pull request #87 from appKom/85-fix-deployment-authenti…
johngothe Dec 14, 2025
a0c4a36
Merge branch 'main' into 85-fix-deployment-authentication-failure-and…
madshermansen Dec 16, 2025
091956d
Update SecurityConfig.kt
madshermansen Dec 16, 2025
baa566a
Update SecurityConfig.kt
madshermansen Dec 16, 2025
b10f3e9
Merge pull request #88 from appKom/85-fix-deployment-authentication-f…
madshermansen Jan 1, 2026
2f062f6
merge main
mnyflot Jan 15, 2026
1102819
ops
mnyflot Jan 15, 2026
35a9a97
ops
mnyflot Jan 18, 2026
03a0167
Merge pull request #92 from appKom/fix-api-2
mnyflot Jan 18, 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
13 changes: 12 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,28 @@ ext {
}

dependencies {
implementation(platform("software.amazon.awssdk:bom:2.25.16"))
implementation("software.amazon.awssdk:ses")
implementation("software.amazon.awssdk:auth")
implementation("software.amazon.awssdk:regions")

implementation "com.microsoft.sqlserver:mssql-jdbc:${mssqlJdbcVersion}"
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
implementation "org.springframework.data:spring-data-jdbc:${springDataJdbcVersion}"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.jetbrains.kotlin:kotlin-reflect'
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.cache:cache-api")
implementation("org.ehcache:ehcache")
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:0.13.0")
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocOpenApiVersion}"
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

testImplementation "io.kotest:kotest-runner-junit5:${kotestVersion}"
testImplementation "io.kotest:kotest-assertions-core:${kotestVersion}"
testImplementation "io.kotest:kotest-property:${kotestVersion}"
Expand Down
31 changes: 31 additions & 0 deletions src/main/kotlin/com/example/autobank/config/CacheConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.autobank.config

import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.cache.configuration.MutableConfiguration
import javax.cache.expiry.CreatedExpiryPolicy
import javax.cache.expiry.Duration

@Configuration
@EnableCaching // You still need this
class CacheConfig {

@Bean
fun jCacheManagerCustomizer(): JCacheManagerCustomizer {
return JCacheManagerCustomizer { cacheManager ->
// This is where we manually create the "ip-buckets" cache
// that Bucket4j is looking for.
cacheManager.createCache("ip-buckets", jCacheConfiguration())
}
}

private fun jCacheConfiguration(): javax.cache.configuration.Configuration<Any, Any> {
return MutableConfiguration<Any, Any>()
.setTypes(Any::class.java, Any::class.java)
.setStoreByValue(false)
// Example: Make cache entries expire after 1 hour
.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_HOUR))
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
package com.example.autobank.controller


import com.example.autobank.data.authentication.AuthenticatedUserResponse
import com.example.autobank.service.AuthenticationService
import com.example.autobank.service.OnlineUserService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.slf4j.LoggerFactory
import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.http.ResponseCookie
import org.springframework.web.bind.annotation.*
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag


@RestController
@RequestMapping("/api/auth")
@Tag(name = "Authentication Controller", description = "Endpoints for user authentication")
class AuthenticationController {

private val logger = LoggerFactory.getLogger(AuthenticationController::class.java)

@Autowired
lateinit var onlineUserService: OnlineUserService
lateinit var onlineUserService: OnlineUserService;

@Operation(summary = "Check authenticated user", description = "Returns information about the currently authenticated user")
@GetMapping("/getuser")
fun checkUser(): ResponseEntity<AuthenticatedUserResponse> {
return try {
logger.info("=== /api/auth/getuser called ===")
val result = onlineUserService.checkUser()
logger.info("checkUser result: success=${result.success}, isadmin=${result.isadmin}")
ResponseEntity.ok().body(result)
} catch (e: Exception) {
logger.error("=== Exception in /api/auth/getuser ===", e)
logger.error("Exception type: ${e.javaClass.name}")
logger.error("Exception message: ${e.message}")
logger.error("Stack trace:", e)
ResponseEntity.internalServerError().build()
return try {
ResponseEntity.ok().body(onlineUserService.checkUser())
} catch (e: Exception) {
print(e)
ResponseEntity.badRequest().build();
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,19 @@ package com.example.autobank.controller

import com.example.autobank.data.models.Committee
import com.example.autobank.data.user.UserCommitteeResponseBody
import com.example.autobank.security.AudienceValidator
import com.example.autobank.service.CommitteeService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag

import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.core.OAuth2TokenValidator
import org.springframework.security.oauth2.jwt.Jwt

@RestController
@RequestMapping("/api/committee")
@Tag(name = "Committee Controller", description = "Endpoints for managing committees")
class CommitteeController(
) {

private val logger = LoggerFactory.getLogger(AudienceValidator::class.java)

@Autowired
lateinit var committeeService: CommitteeService

Expand All @@ -32,7 +25,7 @@ class CommitteeController(
val committees = committeeService.getAllCommittees()
ResponseEntity.ok(committees)
} catch (e: Exception) {
ResponseEntity.internalServerError().build()
ResponseEntity.badRequest().build()
}
}

Expand All @@ -43,9 +36,7 @@ class CommitteeController(
val userandcommittees = committeeService.getUserAndCommittees()
ResponseEntity.ok(userandcommittees)
} catch (e: Exception) {
logger.error("Error in getUserAndCommittees", e)
e.printStackTrace()
ResponseEntity.internalServerError().build()
ResponseEntity.badRequest().build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ReceiptController {
ResponseEntity.ok(res)
} catch (e: Exception) {
e.printStackTrace()
ResponseEntity.internalServerError().build()
ResponseEntity.badRequest().build()
}
}

Expand All @@ -50,7 +50,7 @@ class ReceiptController {
val res = receiptService.getReceipt(id)
ResponseEntity.ok(res)
} catch (e: Exception) {
ResponseEntity.internalServerError().build()
ResponseEntity.badRequest().build()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.example.autobank.data.authentication

import com.nimbusds.openid.connect.sdk.claims.Gender
import org.jetbrains.annotations.NotNull
import io.swagger.v3.oas.annotations.media.Schema

Expand Down
15 changes: 3 additions & 12 deletions src/main/kotlin/com/example/autobank/security/AudienceValidator.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
package com.example.autobank.security


import org.springframework.security.oauth2.core.OAuth2Error
import org.springframework.security.oauth2.core.OAuth2TokenValidator
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult
import org.springframework.security.oauth2.jwt.Jwt
import org.slf4j.LoggerFactory

class AudienceValidator(private val audience: String) : OAuth2TokenValidator<Jwt> {

private val logger = LoggerFactory.getLogger(AudienceValidator::class.java)

override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
logger.info("=== Validating JWT Audience ===")
logger.info("Expected audience: $audience")
logger.info("Token audiences: ${jwt.audience}")

val error = OAuth2Error("invalid_token", "The required audience is missing", null)
return if (jwt.audience.contains(audience)) {
logger.info("Audience validation: SUCCESS")
OAuth2TokenValidatorResult.success()
} else {
logger.error("Audience validation: FAILED - Required audience '$audience' not found in ${jwt.audience}")
OAuth2TokenValidatorResult.failure(error)
}
} else OAuth2TokenValidatorResult.failure(error)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -144,25 +144,29 @@ class AuthenticationService(
}

fun checkAdmin(): Boolean {
// Use the Auth0 sub to find the user in your local DB
val auth0Sub = getUserSub()
val user = onlineUserRepository.findByOnlineId(auth0Sub) ?: throw Exception("User not found")

val user = onlineUserRepository.findByOnlineId(getUserSub()) ?: throw Exception("User not found");
val now = LocalDateTime.now()
val hoursSinceUpdate = Duration.between(user.lastUpdated, now).toHours()

// Time check for users last update isAdmin
if (Duration.between(user.lastUpdated, LocalDateTime.now()).toMillis() > adminRecheckTime) {
user.lastUpdated = LocalDateTime.now()
// If never updated or 24h passed
if (user.lastUpdated == null || hoursSinceUpdate >= 24) {

// Check if the user is admin and set bool accordingly
user.isAdmin = fetchUserCommittees().contains(adminCommittee)
// This function calls user.getMe to get the internal UUID
// and then calls group.allByMember
val committees = fetchUserCommittees()

// I dont know what this is
onlineUserRepository.save(user)
}
user.isAdmin = committees.contains(adminCommittee)
user.lastUpdated = now

if (user.isAdmin) {
return true;
// onlineUserRepository.save(user) persists the isAdmin status
// to your database so you don't have to call the API every time.
onlineUserRepository.save(user)
}

return false;
return user.isAdmin
}

fun getExpiresAt(): Instant? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class CommitteeService(

fun getUserAndCommittees(): UserCommitteeResponseBody {
val userdetails = authenticationService.getUserDetails()

return UserCommitteeResponseBody(userdetails.name, userdetails.email, authenticationService.fetchUserCommittees())
}

Expand Down
59 changes: 59 additions & 0 deletions src/main/kotlin/com/example/autobank/service/MailService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.autobank.service

import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.ses.SesClient
import software.amazon.awssdk.services.ses.model.*

@Service
class MailService(
@Value("\${aws.access-key-id}") private val accessKeyId: String,
@Value("\${aws.secret-access-key}") private val secretAccessKey: String,
) {

private val sesClient: SesClient = SesClient.builder()
.region(Region.EU_NORTH_1)
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey)
)
)
.build()

fun sendEmail(toEmail: String, subject: String, htmlBody: String) {
val destination = Destination.builder()
.toAddresses(toEmail)
.build()

val content = Content.builder()
.data(subject)
.charset("UTF-8")
.build()

val body = Body.builder()
.html(Content.builder().data(htmlBody).charset("UTF-8").build())
.build()

val message = Message.builder()
.subject(content)
.body(body)
.build()

val request = SendEmailRequest.builder()
.destination(destination)
.message(message)
.source("kvittering@online.ntnu.no")
.build()

try {
sesClient.sendEmail(request)
println("Email sent successfully to $toEmail")
} catch (e: SesException) {
println("Failed to send email: ${e.awsErrorDetails().errorMessage()}")
throw e
}
}
}
28 changes: 25 additions & 3 deletions src/main/kotlin/com/example/autobank/service/ReceiptService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import com.example.autobank.data.receipt.ReceiptInfoResponseBody
import com.example.autobank.repository.receipt.*
import com.example.autobank.repository.receipt.specification.ReceiptInfoViewSpecification
import org.springframework.stereotype.Service
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import com.example.autobank.service.MailService

@Service
class ReceiptService(
Expand All @@ -16,7 +17,8 @@ class ReceiptService(
private val blobService: BlobService,
private val attachmentService: AttachmentService,
private val committeeService: CommitteeService,
private val receiptInfoRepository: ReceiptInfoRepositoryImpl
private val receiptInfoRepository: ReceiptInfoRepositoryImpl,
private val mailService: MailService
) {


Expand Down Expand Up @@ -62,7 +64,6 @@ class ReceiptService(

val storedReceipt = receiptRepository.save(receipt);


/**
* Save attachments
*/
Expand All @@ -82,6 +83,27 @@ class ReceiptService(
}


val emailContent = """
<h2>Detaljer for innsendt kvittering</h2>
<p><strong>Bruker:</strong> ${user.fullname}</p>
<p><strong>Brukerens e-post:</strong> ${user.email}</p>
<p><strong>Kvitterings-ID:</strong> ${storedReceipt.id}</p>
<p><strong>Beløp:</strong> ${storedReceipt.amount}</p>
<p><strong>Komité-ID:</strong> ${storedReceipt.committee.name}</p>
<p><strong>Anledning:</strong> ${storedReceipt.name}</p>
<p><strong>Beskrivelse:</strong> ${storedReceipt.description}</p>
<p><strong>Betalingsmetode:</strong> ${
if (receiptRequestBody.receiptPaymentInformation?.usedOnlineCard == true)
"Online-kort"
else
"Bankoverføring"
}</p>
<p><strong>Kontonummer:</strong> ${
receiptRequestBody.receiptPaymentInformation?.accountnumber ?: "Ikke oppgitt"
}</p>
""".trimIndent()

mailService.sendEmail(user.email, "Receipt Submission Details", emailContent)
return ReceiptResponseBody()

}
Expand Down
Loading