Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add notification-api-v1 #58

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ A modularize Spring Boot application that contains API modules for [Aam Digital'

1. Create additional databases in CouchDB: `report-calculation` and `notification-webhook` (used by the Reporting Module to store details)
2. Set up necessary environment variables (e.g. using an `application.env` file for docker compose):

- see [example .env](./docs/examples/application.env)
- CRYPTO_CONFIGURATION_SECRET: _a random secret used to encrypt data_

- see [example .env](./docs/examples/application.env)
- CRYPTO_CONFIGURATION_SECRET: _a random secret used to encrypt data_
3. See ndb-setup for instructions to enable the backend in an overall system: [ndb-setup README](https://github.com/Aam-Digital/ndb-setup?tab=readme-ov-file#api-integrations-and-sql-reports)

## API Modules
Expand Down
2 changes: 2 additions & 0 deletions application/aam-backend-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") // needed in some tests

implementation("com.google.firebase:firebase-admin:9.4.2")

runtimeOnly("org.postgresql:postgresql:42.7.4")

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.aamdigital.aambackendservice.reporting.changes.core
package com.aamdigital.aambackendservice.changes.core

import com.aamdigital.aambackendservice.changes.domain.DatabaseChangeEvent
import com.aamdigital.aambackendservice.changes.domain.DocumentChangeEvent
import com.aamdigital.aambackendservice.queue.core.QueueMessage
import com.aamdigital.aambackendservice.reporting.domain.event.DatabaseChangeEvent
import com.aamdigital.aambackendservice.reporting.domain.event.DocumentChangeEvent

interface ChangeEventPublisher {
fun publish(channel: String, event: DatabaseChangeEvent): QueueMessage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.aamdigital.aambackendservice.reporting.changes.core
package com.aamdigital.aambackendservice.changes.core

import com.aamdigital.aambackendservice.changes.di.ChangesQueueConfiguration.Companion.DB_CHANGES_QUEUE
import com.aamdigital.aambackendservice.changes.domain.DatabaseChangeEvent
import com.aamdigital.aambackendservice.changes.repository.SyncEntry
import com.aamdigital.aambackendservice.changes.repository.SyncRepository
import com.aamdigital.aambackendservice.couchdb.core.CouchDbClient
import com.aamdigital.aambackendservice.couchdb.core.getEmptyQueryParams
import com.aamdigital.aambackendservice.error.AamErrorCode
import com.aamdigital.aambackendservice.error.InvalidArgumentException
import com.aamdigital.aambackendservice.reporting.changes.di.ChangesQueueConfiguration.Companion.DB_CHANGES_QUEUE
import com.aamdigital.aambackendservice.reporting.changes.repository.SyncEntry
import com.aamdigital.aambackendservice.reporting.changes.repository.SyncRepository
import com.aamdigital.aambackendservice.reporting.domain.event.DatabaseChangeEvent
import com.fasterxml.jackson.databind.node.ObjectNode
import java.util.*
import kotlin.jvm.optionals.getOrDefault
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.aamdigital.aambackendservice.changes.core

import com.aamdigital.aambackendservice.changes.domain.DatabaseChangeEvent

interface CreateDocumentChangeUseCase {
fun createEvent(event: DatabaseChangeEvent)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aamdigital.aambackendservice.reporting.changes.core
package com.aamdigital.aambackendservice.changes.core

interface DatabaseChangeDetection {
fun checkForChanges()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aamdigital.aambackendservice.reporting.changes.core
package com.aamdigital.aambackendservice.changes.core

import com.rabbitmq.client.Channel
import org.springframework.amqp.core.Message
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.aamdigital.aambackendservice.reporting.changes.core
package com.aamdigital.aambackendservice.changes.core

import com.aamdigital.aambackendservice.changes.di.ChangesQueueConfiguration.Companion.DOCUMENT_CHANGES_EXCHANGE
import com.aamdigital.aambackendservice.changes.domain.DatabaseChangeEvent
import com.aamdigital.aambackendservice.changes.domain.DocumentChangeEvent
import com.aamdigital.aambackendservice.couchdb.core.CouchDbClient
import com.aamdigital.aambackendservice.couchdb.core.getEmptyQueryParams
import com.aamdigital.aambackendservice.error.AamException
import com.aamdigital.aambackendservice.reporting.changes.di.ChangesQueueConfiguration.Companion.DOCUMENT_CHANGES_EXCHANGE
import com.aamdigital.aambackendservice.reporting.domain.event.DatabaseChangeEvent
import com.aamdigital.aambackendservice.reporting.domain.event.DocumentChangeEvent
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import org.slf4j.LoggerFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aamdigital.aambackendservice.reporting.changes.core
package com.aamdigital.aambackendservice.changes.core

class NoopDatabaseChangeDetection : DatabaseChangeDetection {
override fun checkForChanges() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.aamdigital.aambackendservice.reporting.changes.di
package com.aamdigital.aambackendservice.changes.di

import com.aamdigital.aambackendservice.changes.core.ChangeEventPublisher
import com.aamdigital.aambackendservice.changes.core.CouchDbDatabaseChangeDetection
import com.aamdigital.aambackendservice.changes.core.CreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.changes.core.DatabaseChangeDetection
import com.aamdigital.aambackendservice.changes.core.DefaultCreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.changes.core.NoopDatabaseChangeDetection
import com.aamdigital.aambackendservice.changes.repository.SyncRepository
import com.aamdigital.aambackendservice.couchdb.core.CouchDbClient
import com.aamdigital.aambackendservice.reporting.changes.core.ChangeEventPublisher
import com.aamdigital.aambackendservice.reporting.changes.core.CouchDbDatabaseChangeDetection
import com.aamdigital.aambackendservice.reporting.changes.core.CreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.reporting.changes.core.DatabaseChangeDetection
import com.aamdigital.aambackendservice.reporting.changes.core.DefaultCreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.reporting.changes.core.NoopDatabaseChangeDetection
import com.aamdigital.aambackendservice.reporting.changes.repository.SyncRepository
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.aamdigital.aambackendservice.reporting.changes.di
package com.aamdigital.aambackendservice.changes.di

import com.aamdigital.aambackendservice.changes.core.ChangeEventPublisher
import com.aamdigital.aambackendservice.changes.core.CreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.changes.core.DatabaseChangeEventConsumer
import com.aamdigital.aambackendservice.changes.queue.DefaultChangeEventPublisher
import com.aamdigital.aambackendservice.changes.queue.DefaultDatabaseChangeEventConsumer
import com.aamdigital.aambackendservice.queue.core.QueueMessageParser
import com.aamdigital.aambackendservice.reporting.changes.core.ChangeEventPublisher
import com.aamdigital.aambackendservice.reporting.changes.core.CreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.reporting.changes.core.DatabaseChangeEventConsumer
import com.aamdigital.aambackendservice.reporting.changes.queue.DefaultChangeEventPublisher
import com.aamdigital.aambackendservice.reporting.changes.queue.DefaultDatabaseChangeEventConsumer
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.amqp.core.Binding
import org.springframework.amqp.core.BindingBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aamdigital.aambackendservice.reporting.domain.event
package com.aamdigital.aambackendservice.changes.domain

import com.aamdigital.aambackendservice.events.DomainEvent

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aamdigital.aambackendservice.reporting.domain.event
package com.aamdigital.aambackendservice.changes.domain

import com.aamdigital.aambackendservice.events.DomainEvent

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.aamdigital.aambackendservice.reporting.changes.jobs
package com.aamdigital.aambackendservice.changes.jobs

import com.aamdigital.aambackendservice.reporting.changes.core.DatabaseChangeDetection
import com.aamdigital.aambackendservice.changes.core.DatabaseChangeDetection
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.Scheduled
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.aamdigital.aambackendservice.reporting.changes.queue
package com.aamdigital.aambackendservice.changes.queue

import com.aamdigital.aambackendservice.changes.core.ChangeEventPublisher
import com.aamdigital.aambackendservice.changes.domain.DatabaseChangeEvent
import com.aamdigital.aambackendservice.changes.domain.DocumentChangeEvent
import com.aamdigital.aambackendservice.error.AamErrorCode
import com.aamdigital.aambackendservice.error.AamException
import com.aamdigital.aambackendservice.error.InternalServerException
import com.aamdigital.aambackendservice.queue.core.QueueMessage
import com.aamdigital.aambackendservice.reporting.changes.core.ChangeEventPublisher
import com.aamdigital.aambackendservice.reporting.domain.event.DatabaseChangeEvent
import com.aamdigital.aambackendservice.reporting.domain.event.DocumentChangeEvent
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import org.springframework.amqp.AmqpException
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.aamdigital.aambackendservice.reporting.changes.queue
package com.aamdigital.aambackendservice.changes.queue

import com.aamdigital.aambackendservice.changes.core.CreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.changes.core.DatabaseChangeEventConsumer
import com.aamdigital.aambackendservice.changes.di.ChangesQueueConfiguration.Companion.DB_CHANGES_QUEUE
import com.aamdigital.aambackendservice.changes.domain.DatabaseChangeEvent
import com.aamdigital.aambackendservice.error.AamException
import com.aamdigital.aambackendservice.queue.core.QueueMessageParser
import com.aamdigital.aambackendservice.reporting.changes.core.CreateDocumentChangeUseCase
import com.aamdigital.aambackendservice.reporting.changes.core.DatabaseChangeEventConsumer
import com.aamdigital.aambackendservice.reporting.changes.di.ChangesQueueConfiguration.Companion.DB_CHANGES_QUEUE
import com.aamdigital.aambackendservice.reporting.domain.event.DatabaseChangeEvent
import com.rabbitmq.client.Channel
import org.slf4j.LoggerFactory
import org.springframework.amqp.AmqpRejectAndDontRequeueException
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aamdigital.aambackendservice.reporting.changes.repository
package com.aamdigital.aambackendservice.changes.repository

import jakarta.persistence.Column
import jakarta.persistence.Entity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.aamdigital.aambackendservice.notification.controller

import com.aamdigital.aambackendservice.notification.repositiory.UserDeviceRepository
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.MulticastMessage
import com.google.firebase.messaging.Notification
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.data.domain.Pageable
import org.springframework.http.ResponseEntity
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

data class TestMessageResponse(
val receiverIds: List<String>,
)

@RestController
@RequestMapping("/v1/notification")
@ConditionalOnProperty(
prefix = "features.notification-api",
name = ["enabled"],
havingValue = "true",
matchIfMissing = false
)
class NotificationAdminController(
private val firebaseMessaging: FirebaseMessaging,
private val userDeviceRepository: UserDeviceRepository,
) {

@PostMapping("/message/device-test")
fun sendTestMessageToDevice(
authentication: JwtAuthenticationToken,
): ResponseEntity<TestMessageResponse> {
val userDevices =
userDeviceRepository.findByUserIdentifier(
authentication.name ?: authentication.tokenAttributes["username"].toString(), Pageable.unpaged()
)
.map {
it.deviceToken
}.toList()

if (userDevices.isEmpty()) {
return ResponseEntity.ok(
TestMessageResponse(
receiverIds = emptyList()
)
)
}

val message = MulticastMessage.builder()
.addAllTokens(userDevices)
.setNotification(Notification.builder().setTitle("Aam Digital Test").setBody("Hello World").build())
.build()

val response = firebaseMessaging.sendEachForMulticast(message)

val ids = response.responses.map { it.messageId }.toList()

return ResponseEntity.ok().body(
TestMessageResponse(
receiverIds = ids
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.aamdigital.aambackendservice.notification.controller

import com.aamdigital.aambackendservice.error.HttpErrorDto
import com.aamdigital.aambackendservice.notification.repositiory.UserDeviceEntity
import com.aamdigital.aambackendservice.notification.repositiory.UserDeviceRepository
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.transaction.annotation.Transactional
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.io.IOException
import kotlin.jvm.optionals.getOrNull

data class DeviceRegistrationDto(
val deviceName: String? = null,
val deviceToken: String,
)

@RestController
@RequestMapping("/v1/notification")
@ConditionalOnProperty(
prefix = "features.notification-api",
name = ["enabled"],
havingValue = "true",
matchIfMissing = false
)
@Transactional
class NotificationController(
private val userDeviceRepository: UserDeviceRepository,
) {
private val logger = LoggerFactory.getLogger(javaClass)

@PostMapping("/device")
@Validated
fun registerDevice(
@RequestBody deviceRegistrationDto: DeviceRegistrationDto,
authentication: JwtAuthenticationToken,
): ResponseEntity<Any> {

if (userDeviceRepository.existsByDeviceToken(deviceRegistrationDto.deviceToken)) {
return ResponseEntity.badRequest().body(
HttpErrorDto(
errorCode = "Bad Request",
errorMessage = "The device is already registered."
)
)
}

if (authentication.name == null) {
return ResponseEntity.badRequest().body(
HttpErrorDto(
errorCode = "Bad Request",
errorMessage = "No subject found in the token."
)
)
}

userDeviceRepository.save(
UserDeviceEntity(
userIdentifier = authentication.name,
deviceToken = deviceRegistrationDto.deviceToken,
deviceName = deviceRegistrationDto.deviceName,
)
)


return ResponseEntity.noContent().build()
}

@DeleteMapping("/device/{id}")
fun unregisterDevice(
@PathVariable id: String,
authentication: JwtAuthenticationToken,
): ResponseEntity<Any> {
val userDevice =
userDeviceRepository.findByDeviceToken(id).getOrNull() ?: return ResponseEntity.notFound().build()

if (userDevice.userIdentifier != (authentication.name
?: authentication.tokenAttributes["username"].toString())
) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(
HttpErrorDto(
errorCode = "Forbidden",
errorMessage = "Token does not belong to User",
)
)
}

try {
userDeviceRepository.deleteByDeviceToken(id)
} catch (ex: IOException) {
logger.warn("[NotificationController.unregisterDevice()] error: {}", ex.message)
}

return ResponseEntity.noContent().build()
}
}
Loading
Loading