Skip to content

Commit

Permalink
feat: add notification-api-v1
Browse files Browse the repository at this point in the history
  • Loading branch information
tomwwinter committed Dec 23, 2024
1 parent 0cfe7c7 commit 497e1b1
Show file tree
Hide file tree
Showing 9 changed files with 399 additions and 9 deletions.
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
@@ -0,0 +1,64 @@
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 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.data.domain.Pageable
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
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: Authentication,
): ResponseEntity<TestMessageResponse> {
val userDevices =
userDeviceRepository.findByUserIdentifier(authentication.name, Pageable.unpaged())
.map {
it.userIdentifier
}.toList()

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

val message = MulticastMessage.builder()
.addAllTokens(userDevices)
.putData("body", "Hello World")
.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,96 @@
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.core.Authentication
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: Authentication,
): ResponseEntity<Any> {

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

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: Authentication,
): ResponseEntity<Any> {
val userDevice =
userDeviceRepository.findByDeviceToken(id).getOrNull() ?: return ResponseEntity.notFound().build()

if (userDevice.userIdentifier != authentication.name) {
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.aamdigital.aambackendservice.notification.di

import com.google.auth.oauth2.GoogleCredentials
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.messaging.FirebaseMessaging
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.*


@ConfigurationProperties("notification-firebase-configuration")
class NotificationFirebaseClientConfiguration(
val credentialFileBase64: String,
)

@Configuration
@ConditionalOnProperty(
prefix = "features.notification-api",
name = ["enabled"],
havingValue = "true",
matchIfMissing = false
)
class FirebaseConfiguration {

@Bean
@ConditionalOnProperty(
prefix = "features.notification-api",
name = ["mode"],
havingValue = "firebase",
matchIfMissing = false
)
fun firebaseApp(notificationFirebaseClientConfiguration: NotificationFirebaseClientConfiguration): FirebaseApp {
val credentialFile =
Base64.getDecoder().decode(notificationFirebaseClientConfiguration.credentialFileBase64)

val options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(credentialFile.inputStream()))
.build()

return FirebaseApp.initializeApp(options)
}

@Bean
@ConditionalOnProperty(
prefix = "features.notification-api",
name = ["mode"],
havingValue = "firebase",
matchIfMissing = false
)
fun firebaseMessaging(firebaseApp: FirebaseApp): FirebaseMessaging {
return FirebaseMessaging.getInstance(firebaseApp)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.aamdigital.aambackendservice.notification.domain

import com.aamdigital.aambackendservice.domain.DomainReference
import java.time.Instant

/**
* Representation of a user device
*/
data class UserDevice(
var id: String,
var deviceName: String?,
var deviceToken: String,
var user: DomainReference,
var createdAt: Instant,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.aamdigital.aambackendservice.notification.repositiory

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.SourceType
import java.time.OffsetDateTime

@Entity
data class UserDeviceEntity(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long = 0,

@Column
var deviceName: String?,

@Column(
unique = true,
nullable = false,
updatable = false,
)
var deviceToken: String,

@Column
var userIdentifier: String,

@CreationTimestamp(source = SourceType.DB)
@Column
var createdAt: OffsetDateTime? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.aamdigital.aambackendservice.notification.repositiory

import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.PagingAndSortingRepository
import java.util.*

interface UserDeviceRepository : PagingAndSortingRepository<UserDeviceEntity, Long>,
CrudRepository<UserDeviceEntity, Long> {
fun findByUserIdentifier(userIdentifier: String, pageable: Pageable): Page<UserDeviceEntity>
fun findByDeviceToken(deviceToken: String): Optional<UserDeviceEntity>
fun existsByDeviceToken(deviceToken: String): Boolean
fun deleteByDeviceToken(deviceToken: String)
}
Loading

0 comments on commit 497e1b1

Please sign in to comment.