Skip to content

Commit

Permalink
feat: Add authentication to google assistant / slack (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Naimi committed Dec 16, 2019
1 parent 7da42db commit b137ce4
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 59 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
buildScanVersion = '1.13.2'
gradlePythonPluginVersion = '1.0.2'
gradleMkdocsPluginVersion = '1.0.1'
actionsOnGoogleVersion = '1.7.0'
actionsOnGoogleVersion = '1.8.0'
}
repositories {
maven {
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/io/saagie/astonparking/dao/UserDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ import org.springframework.stereotype.Repository
interface UserDao : CrudRepository<User, String> {
fun findByEnable(active: Boolean): List<User>
fun findByUnregister(unregister: Boolean): List<User>
fun findTopByEmail(mail: String): User?
fun existsByEmailAndIdNot(mail: String, id: String): Boolean
}
11 changes: 11 additions & 0 deletions src/main/kotlin/io/saagie/astonparking/exceptions/PickException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.saagie.astonparking.exceptions

import java.lang.IllegalArgumentException
import java.time.LocalDate

sealed class PickException(message: String): IllegalArgumentException(message) {
data class NoScheduleError(private val date: LocalDate) : PickException("No schedule for the date $date")
data class NoFreeSpotsError(private val date: LocalDate) : PickException("No free spot for the date $date")
object AlreadyPickError : PickException("A spot is already reserved for you")

}
133 changes: 84 additions & 49 deletions src/main/kotlin/io/saagie/astonparking/googleActions/Actions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,118 @@ import com.google.actions.api.ActionRequest
import com.google.actions.api.ActionResponse
import com.google.actions.api.DialogflowApp
import com.google.actions.api.ForIntent
import com.google.actions.api.response.helperintent.SignIn
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken
import io.saagie.astonparking.exceptions.PickException
import io.saagie.astonparking.security.BasicAuthConfig
import io.saagie.astonparking.service.DrawService
import io.saagie.astonparking.service.UserService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.time.LocalDate





@Component
class Actions(private val drawService: DrawService) : DialogflowApp() {
class Actions(private val drawService: DrawService, private val userService: UserService, private val basicAuthConfig: BasicAuthConfig) : DialogflowApp() {

@ForIntent("today spots")
fun todaySpots(request: ActionRequest): ActionResponse {
LOGGER.info("today spots.")
@ForIntent("today spot")
fun todaySpot(request: ActionRequest): ActionResponse {
LOGGER.info("today spot.")
val responseBuilder = getResponseBuilder(request)
with(drawService.getSchedule(LocalDate.now())) {
if (this == null) {
responseBuilder.add("No spots attributed today.")
} else {
val message = assignedSpots.sortedBy { it.spotNumber }.map { spot ->
"${spot.spotNumber} ${spot.username}"
}.plus(
freeSpots.sorted().map { spotNumber ->
"$spotNumber FREE"
}
).joinToString(" ")
responseBuilder.add(message)
}
responseBuilder.endConversation();
return responseBuilder.build()
if (request.user?.userVerificationStatus != "VERIFIED") {
responseBuilder.add("Vous n'êtes pas autorise a reserver une place")
responseBuilder.endConversation()
}
}

@ForIntent("today free spots")
fun todayFreeSpots(request: ActionRequest): ActionResponse {
LOGGER.info("today free spots.")
val responseBuilder = getResponseBuilder(request)
with(drawService.getSchedule(LocalDate.now())) {
if (this == null) {
responseBuilder.add("No spots attributed today.")
if (request.user != null && userIsSignedIn(request)) {
val mail = getUserProfile(request.user!!.idToken).email
val user = userService.getByMail(mail)
if (user == null) {
responseBuilder.add("L'email qui vous utilisez n'est pas lie a votre compte aston parking. " +
"S'il vous plait, utilisez la commande slack ap-link pour lier votre email")
responseBuilder.endConversation()
} else {
val message = freeSpots.sorted().joinToString(" ") { spotNumber ->
"$spotNumber FREE"
}
if (message.isBlank()) {
responseBuilder.add("No available spots.")
} else {
responseBuilder.add(message)
with(drawService.getSchedule(LocalDate.now())) {
if (this == null) {
responseBuilder.add("Pas de places attribuees aujourd'hui")
responseBuilder.endConversation()
} else {
val message = assignedSpots.filter { it.userId == user.id }.joinToString(" ") { spot ->
"Votre place est la place ${spot.spotNumber}"
}
if (message.isBlank()) {
responseBuilder.add("Vous n'avez pas de place aujourd'hui")
responseBuilder.add("Voulez vous reserver une place?")
} else {
responseBuilder.add(message)
responseBuilder.endConversation()
}
}
return responseBuilder.build()
}
}
responseBuilder.endConversation();
} else {
responseBuilder.add(
SignIn()
.setContext("You are signed in"))
return responseBuilder.build()
}
responseBuilder.endConversation()
return responseBuilder.build()
}

@ForIntent("today spot")
fun todaySpot(request: ActionRequest): ActionResponse {
LOGGER.info("today spot.")
@ForIntent("pick today")
fun pickToday(request: ActionRequest): ActionResponse {
LOGGER.debug("pick today.")
val responseBuilder = getResponseBuilder(request)
val usernameParam = request.getParameter("username") as String
with(drawService.getSchedule(LocalDate.now())) {
if (this == null) {
responseBuilder.add("No spots attributed today.")

if (request.user?.userVerificationStatus != "VERIFIED") {
responseBuilder.add("Vous n'etes pas autorises a prendre une place")
}

if (request.user != null && userIsSignedIn(request)) {
val mail = getUserProfile(request.user!!.idToken).email
val user = userService.getByMail(mail)
if (user == null) {
responseBuilder.add("L'email qui vous utilisez n'est pas lie a votre compte aston parking. " +
"S'il vous plait, utilisez la commande slack ap-link pour lier votre email")
} else {
val message = assignedSpots.filter { it.username.toLowerCase() == usernameParam.toLowerCase() }.map { spot ->
"${spot.spotNumber} ${spot.username}"
}.joinToString(" ")
if (message.isBlank()) {
responseBuilder.add("You have no spot today.")
} else {
try {
val spot = drawService.pick(user.id!!, LocalDate.now())
responseBuilder.add("Vous avez la place $spot pour aujourd'hui.")
} catch (e: Exception) {
val message = when (e) {
is PickException.NoScheduleError -> "Pas de places attribuees aujourd'hui"
is PickException.NoFreeSpotsError -> "Pas de places disponibles aujourd'hui"
is PickException.AlreadyPickError -> "Une place est deja reserve pour vous aujourd'hui"
else -> "Un erreur s'est produite."
}
responseBuilder.add(message)
}
}
responseBuilder.endConversation();
} else {
responseBuilder.add(
SignIn()
.setContext("You are signed in"))
return responseBuilder.build()
}
responseBuilder.endConversation()
return responseBuilder.build()
}

private fun userIsSignedIn(request: ActionRequest): Boolean {
val idToken = request.user?.idToken
LOGGER.info(String.format("Id token: %s", idToken))
return !(idToken == null || idToken.isEmpty())
}

private fun getUserProfile(idToken: String): GoogleIdToken.Payload {
return TokenDecoder(basicAuthConfig.clientId).decodeIdToken(idToken)
}

companion object {

private val LOGGER = LoggerFactory.getLogger(Actions::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.saagie.astonparking.googleActions


import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.json.jackson2.JacksonFactory
import java.io.IOException
import java.security.GeneralSecurityException

class TokenDecoder(private val clientId: String) {

@Throws(GeneralSecurityException::class, IOException::class)
fun decodeIdToken(idTokenString: String): GoogleIdToken.Payload {
val transport = GoogleNetHttpTransport.newTrustedTransport()
val jsonFactory = JacksonFactory.getDefaultInstance()

val verifier = GoogleIdTokenVerifier.Builder(transport, jsonFactory)
// Specify the CLIENT_ID of the app that accesses the backend:
.setAudience(listOf(clientId))
.build()

val idToken = verifier.verify(idTokenString)
return idToken.payload
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "user")
@ConfigurationProperties(prefix = "google.actions")
class BasicAuthConfig {

var username: String? = ""
var password: String? = ""
var username: String = ""
var password: String = ""
var clientId: String = ""
}
7 changes: 4 additions & 3 deletions src/main/kotlin/io/saagie/astonparking/service/DrawService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.saagie.astonparking.dao.PropositionDao
import io.saagie.astonparking.dao.RequestDao
import io.saagie.astonparking.dao.ScheduleDao
import io.saagie.astonparking.domain.*
import io.saagie.astonparking.exceptions.PickException
import io.saagie.astonparking.rule.DrawRules
import io.saagie.astonparking.slack.SlackBot
import org.springframework.scheduling.annotation.Async
Expand Down Expand Up @@ -278,11 +279,11 @@ class DrawService(
fun pick(userId: String, date: LocalDate): Int {
val user = userService.get(userId)
val schedule = scheduleDao.findByDate(date)
?: throw IllegalArgumentException("No schedule for the date ${date}")
?: throw PickException.NoScheduleError(date)
if (schedule.freeSpots.isEmpty())
throw IllegalArgumentException("No free spot for the date ${date}")
throw PickException.NoFreeSpotsError(date)
if (schedule.assignedSpots.count { it.userId == userId } > 0)
throw IllegalArgumentException("A spot is already reserved for you")
throw PickException.AlreadyPickError
val freeSpot = schedule.freeSpots.first()
schedule.freeSpots.removeAt(0)
if (!schedule.userSelected.contains(user.id)) {
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/io/saagie/astonparking/service/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class UserService(
throw IllegalArgumentException("User (id:${id}) not found")
}

fun getByMail(mail: String): User? = userDao.findTopByEmail(mail)

fun isMailAlreadyTaken(mail: String, id: String): Boolean = userDao.existsByEmailAndIdNot(mail, id)

fun getAll(): List<User> {
return userDao.findAll() as List<User>
}
Expand All @@ -60,6 +64,10 @@ class UserService(
userDao.save(user)
}

fun changeMail(id: String, mail: String) {
userDao.save(get(id).copy(email = mail))
}

fun save(user: User) {
userDao.save(user)
}
Expand Down
32 changes: 30 additions & 2 deletions src/main/kotlin/io/saagie/astonparking/slack/SlackSlashCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.text.DecimalFormat
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.ArrayList
import kotlin.math.round


@RestController
Expand Down Expand Up @@ -107,6 +105,9 @@ class SlackSlashCommand(
},
Attachment().apply {
setText("/ap-request-cancel : to cancel your current request")
},
Attachment().apply {
setText("/ap-link mail : to link your aston parking profile to your google assistant mail")
}
)
richMessage.attachments = attachments
Expand Down Expand Up @@ -600,4 +601,31 @@ class SlackSlashCommand(
return Message(iae.message)
}
}

@RequestMapping(value = ["/slack/link"],
method = [(RequestMethod.POST)],
consumes = [(MediaType.APPLICATION_FORM_URLENCODED_VALUE)])
fun onReceiveLinkCommand(@RequestParam("token") token: String,
@RequestParam("team_id") teamId: String,
@RequestParam("team_domain") teamDomain: String,
@RequestParam("channel_id") channelId: String,
@RequestParam("channel_name") channelName: String,
@RequestParam("user_id") userId: String,
@RequestParam("user_name") userName: String,
@RequestParam("command") command: String,
@RequestParam("text") text: String,
@RequestParam("response_url") responseUrl: String): Message {

// validate token
if (token != slackVerificationToken) {
return Message("No luck man, You're not allowed to do that.")
}
if (userService.isMailAlreadyTaken(text, userId)) {
return Message("Sorry, this mail is already taken by another user.")
}
userService.changeMail(userId, text)
return Message("You have linked your aston parking profile with the google assistant account ${text}.")


}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ spring:
# MONGO
data:
mongodb:
uri: mongodb://${ASTON_PARKING_MONGO_URI}
uri: mongodb://${ASTON_PARKING_MONGO_URI:test}
##########################
# SECURITY
slack:
Expand Down

0 comments on commit b137ce4

Please sign in to comment.