Skip to content

Commit

Permalink
feat: add fcmtoken post api + test
Browse files Browse the repository at this point in the history
Signed-off-by: Stefano Cappa <[email protected]>
  • Loading branch information
Ks89 committed Dec 25, 2024
1 parent ff6f5bb commit a40d67b
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .env_template
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ HTTP_SENSOR_PORT=8000
HTTP_SENSOR_GETVALUE_API=/sensors/
HTTP_SENSOR_REGISTER_API=/sensors/register/
HTTP_SENSOR_KEEPALIVE_API=/keepalive/
HTTP_ONLINE_SERVER=http://localhost
HTTP_ONLINE_PORT=8000
HTTP_ONLINE_API=/online/
HTTP_ONLINE_FCMTOKEN_API=/fcmtoken/
HTTP_ONLINE_KEEPALIVE_API=/keepalive/
GRPC_URL=localhost:50051
GRPC_TLS=false
CERT_FOLDER_PATH=cert
Expand Down
138 changes: 138 additions & 0 deletions api/fcm_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package api

import (
"api-server/customerrors"
"api-server/db"
"api-server/models"
"api-server/utils"
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
"golang.org/x/net/context"
"io"
"net/http"
"os"
)

// InitFCMTokenReq struct
type InitFCMTokenReq struct {
APIToken string `json:"apiToken" validate:"required,uuid4"`
FCMToken string `json:"fcmToken" validate:"required"`
}

// FCMToken struct
type FCMToken struct {
client *mongo.Client
collProfiles *mongo.Collection
ctx context.Context
logger *zap.SugaredLogger
keepAliveOnlineURL string
fcmTokenOnlineURL string
validate *validator.Validate
}

// NewFCMToken function
func NewFCMToken(ctx context.Context, logger *zap.SugaredLogger, client *mongo.Client, validate *validator.Validate) *FCMToken {
onlineServerURL := os.Getenv("HTTP_ONLINE_SERVER") + ":" + os.Getenv("HTTP_ONLINE_PORT")
keepAliveOnlineURL := onlineServerURL + os.Getenv("HTTP_ONLINE_KEEPALIVE_API")
fcmTokenOnlineURL := onlineServerURL + os.Getenv("HTTP_ONLINE_FCMTOKEN_API")

return &FCMToken{
client: client,
collProfiles: db.GetCollections(client).Profiles,
ctx: ctx,
logger: logger,
keepAliveOnlineURL: keepAliveOnlineURL,
fcmTokenOnlineURL: fcmTokenOnlineURL,
validate: validate,
}
}

// PostFCMToken function to associate smartphone app with Firebase client to this server via APIToken
// This will be sent to online server to store that data in Redis to be able to send Push Notifications
func (handler *FCMToken) PostFCMToken(c *gin.Context) {
handler.logger.Info("REST - PostFCMToken called")

var initFCMTokenBody InitFCMTokenReq
if err := c.ShouldBindJSON(&initFCMTokenBody); err != nil {
handler.logger.Errorf("REST - PostFCMToken - Cannot bind request body. Err = %v\n", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request payload"})
return
}

err := handler.validate.Struct(initFCMTokenBody)
if err != nil {
handler.logger.Errorf("REST - PostFCMToken - request body is not valid, err %#v", err)
var errFields = utils.GetErrorMessage(err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body, these fields are not valid:" + errFields})
return
}

// search if profile token exists and retrieve profile
var profileFound models.Profile
errProfile := handler.collProfiles.FindOne(handler.ctx, bson.M{
"apiToken": initFCMTokenBody.APIToken,
}).Decode(&profileFound)
if errProfile != nil {
handler.logger.Errorf("REST - PostFCMToken - Cannot find profile with that apiToken. Err = %v\n", errProfile)
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot initialize FCM Token, profile token missing or not valid"})
return
}

err = handler.initFCMTokenViaHTTP(&initFCMTokenBody)
if err != nil {
handler.logger.Errorf("REST - PostFCMToken - cannot initialize FCM Token via HTTP. Err %v\n", err)
if re, ok := err.(*customerrors.ErrorWrapper); ok {
handler.logger.Errorf("REST - PostFCMToken - cannot initialize FCM Token with status = %d, message = %s\n", re.Code, re.Message)
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot initialize FCM Token"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "FCMToken assigned to APIToken"})
}

func (handler *FCMToken) initFCMTokenViaHTTP(obj *InitFCMTokenReq) error {
// check if service is available calling keep-alive
// TODO remove this in a production code
_, _, keepAliveErr := handler.keepAliveOnlineService(handler.keepAliveOnlineURL)
if keepAliveErr != nil {
return customerrors.Wrap(http.StatusInternalServerError, keepAliveErr, "Cannot call keepAlive of remote online service")
}

// do the real call to the remote online service
payloadJSON, err := json.Marshal(obj)
if err != nil {
return customerrors.Wrap(http.StatusInternalServerError, err, "Cannot create payload to call fcmToken service")
}

_, _, err = handler.initFCMToken(handler.fcmTokenOnlineURL, payloadJSON)
if err != nil {
return customerrors.Wrap(http.StatusInternalServerError, err, "Cannot init fcmToken")
}
return nil
}

func (handler *FCMToken) keepAliveOnlineService(urlKeepAlive string) (int, string, error) {
response, err := http.Get(urlKeepAlive)
if err != nil {
return -1, "", customerrors.Wrap(http.StatusInternalServerError, err, "Cannot call keepAlive of the remote online service via HTTP")
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
return response.StatusCode, string(body), nil
}

func (handler *FCMToken) initFCMToken(urlOnline string, payloadJSON []byte) (int, string, error) {
var payloadBody = bytes.NewBuffer(payloadJSON)
response, err := http.Post(urlOnline, "application/json", payloadBody)
if err != nil {
return -1, "", customerrors.Wrap(http.StatusInternalServerError, err, "Cannot call fcmToken service via HTTP")
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
return response.StatusCode, string(body), nil
}
2 changes: 1 addition & 1 deletion api/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (handler *Register) PostRegister(c *gin.Context) {
}).Decode(&profileFound)
if errProfile != nil {
handler.logger.Errorf("REST - PostRegister - Cannot find profile with that apiToken. Err = %v\n", errProfile)
c.JSON(http.StatusBadRequest, gin.H{"error": "cnnot register, profile token missing or not valid"})
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot register, profile token missing or not valid"})
return
}

Expand Down
5 changes: 5 additions & 0 deletions initialization/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ func printEnv(logger *zap.SugaredLogger) {
logger.Info("HTTP_SENSOR_GETVALUE_API = " + os.Getenv("HTTP_SENSOR_GETVALUE_API"))
logger.Info("HTTP_SENSOR_REGISTER_API = " + os.Getenv("HTTP_SENSOR_REGISTER_API"))
logger.Info("HTTP_SENSOR_KEEPALIVE_API = " + os.Getenv("HTTP_SENSOR_KEEPALIVE_API"))
logger.Info("HTTP_ONLINE_SERVER = " + os.Getenv("HTTP_ONLINE_SERVER"))
logger.Info("HTTP_ONLINE_PORT = " + os.Getenv("HTTP_ONLINE_PORT"))
logger.Info("HTTP_ONLINE_API = " + os.Getenv("HTTP_ONLINE_API"))
logger.Info("HTTP_ONLINE_FCMTOKEN_API = " + os.Getenv("HTTP_ONLINE_FCMTOKEN_API"))
logger.Info("HTTP_ONLINE_KEEPALIVE_API = " + os.Getenv("HTTP_ONLINE_KEEPALIVE_API"))
logger.Info("GRPC_URL = " + os.Getenv("GRPC_URL"))
logger.Info("GRPC_TLS = " + os.Getenv("GRPC_TLS"))
logger.Info("CERT_FOLDER_PATH = " + os.Getenv("CERT_FOLDER_PATH"))
Expand Down
15 changes: 12 additions & 3 deletions initialization/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ var assignDevices *api.AssignDevice
var devicesValues *api.DevicesValues
var profiles *api.Profiles
var register *api.Register
var fcmToken *api.FCMToken
var keepAlive *api.KeepAlive

var oauthCallbackURL string
var oauthScopes = []string{"repo"} //https://developer.github.com/v3/oauth/#scopes

// SetupRouter function
func SetupRouter(httpServer string, port string, oauthCallback string, logger *zap.SugaredLogger) (*gin.Engine, cookie.Store) {
func SetupRouter(logger *zap.SugaredLogger) (*gin.Engine, cookie.Store) {
port := os.Getenv("HTTP_PORT")
httpServer := os.Getenv("HTTP_SERVER")
oauthCallback := os.Getenv("OAUTH_CALLBACK")

// init oauthCallbackURL based on httpOrigin
oauthCallbackURL = oauthCallback
logger.Info("SetupRouter - oauthCallbackURL is = " + oauthCallbackURL)
Expand Down Expand Up @@ -78,7 +83,8 @@ func SetupRouter(httpServer string, port string, oauthCallback string, logger *z
logger.Info("SetupRouter - CORS disabled")
}

// 11. Configure Gin to serve a Single Page Application
// 11. Configure Gin to serve a SPA for non-production env
// In prod we will use nginx, so this will be ignored!
// GIN is terrible with SPA, because you can configure static.serve
// but if you refresh the SPA it will return an error, and you cannot add something like /*
// The only way is to manage this manually passing the filename in case it's a file, otherwise it must redirect
Expand Down Expand Up @@ -107,20 +113,23 @@ func RegisterRoutes(ctx context.Context, router *gin.Engine, cookieStore *cookie
oauthGithub = api.NewGithub(ctx, logger, client, oauthCallbackURL, oauthScopes)
auth = api.NewAuth(ctx, logger)

keepAlive = api.NewKeepAlive(ctx, logger)
homes = api.NewHomes(ctx, logger, client, validate)
devices = api.NewDevices(ctx, logger, client)
assignDevices = api.NewAssignDevice(ctx, logger, client, validate)
devicesValues = api.NewDevicesValues(ctx, logger, client, validate)
profiles = api.NewProfiles(ctx, logger, client)
register = api.NewRegister(ctx, logger, client, validate)
keepAlive = api.NewKeepAlive(ctx, logger)
// FCM = Firebase Cloud Messaging => used to associate smartphone to an APIToken
fcmToken = api.NewFCMToken(ctx, logger, client, validate)

// 12. Configure oAuth2 authentication
router.Use(sessions.Sessions("session", *cookieStore)) // session called "session"
// public API to get Login URL
router.GET("/api/login", oauthGithub.GetLoginURL)
// public APIs
router.POST("/api/register", register.PostRegister)
router.POST("/api/fcmtoken", fcmToken.PostFCMToken)
router.GET("/api/keepalive", keepAlive.GetKeepAlive)
// oAuth2 config to register the oauth callback API
authorized := router.Group("/api/callback")
Expand Down
9 changes: 3 additions & 6 deletions initialization/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,13 @@ func Start() (*zap.SugaredLogger, *gin.Engine, context.Context, *mongo.Client) {
client := db.InitDb(ctx, logger)

// 4. Init server
port := os.Getenv("HTTP_PORT")
httpServer := os.Getenv("HTTP_SERVER")
oauthCallback := os.Getenv("OAUTH_CALLBACK")
router, ctx := BuildServer(ctx, httpServer, port, oauthCallback, logger, client)
router, ctx := BuildServer(ctx, logger, client)

return logger, router, ctx, client
}

// BuildServer - Exposed only for testing purposes
func BuildServer(ctx context.Context, httpServer string, port string, oauthCallback string, logger *zap.SugaredLogger, client *mongo.Client) (*gin.Engine, context.Context) {
func BuildServer(ctx context.Context, logger *zap.SugaredLogger, client *mongo.Client) (*gin.Engine, context.Context) {
// Create a singleton validator instance. Validate is designed to be used as a singleton instance.
// It caches information about struct and validations.
validate := validator.New()
Expand All @@ -44,7 +41,7 @@ func BuildServer(ctx context.Context, httpServer string, port string, oauthCallb

// Instantiate GIN and apply some middlewares
logger.Info("BuildServer - GIN - Initializing...")
router, cookieStore := SetupRouter(httpServer, port, oauthCallback, logger)
router, cookieStore := SetupRouter(logger)
RegisterRoutes(ctx, router, &cookieStore, logger, validate, client)
return router, ctx
}
Expand Down
Loading

0 comments on commit a40d67b

Please sign in to comment.