Skip to content

Commit

Permalink
Merge pull request #8 from SwimResults/develop
Browse files Browse the repository at this point in the history
simple notification request, apns config
  • Loading branch information
konrad2002 authored Nov 21, 2024
2 parents 7046ddc + 175ff5f commit d245711
Show file tree
Hide file tree
Showing 17 changed files with 454 additions and 14 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
*.so
*.dylib

# private apns for apns
*.p8
!AuthKey.p8

# Test binary, built with `go test -c`
*.test

Expand Down
4 changes: 3 additions & 1 deletion .idea/user-service.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions apns/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package apns

import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"github.com/dgrijalva/jwt-go"
"os"
"sync"
"time"
)

const (
// Timeout is the period of time in seconds that a token is valid for.
// If the timestamp for token issue is not within the last hour, APNs
// rejects subsequent push messages. This is set to under an hour so that
// we generate a new token before the existing one expires.
Timeout = 3000
)

// Possible errors when parsing a .p8 file.
var (
ErrAuthKeyNotPem = errors.New("token: AuthKey must be a valid .p8 PEM file")
ErrAuthKeyNotECDSA = errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
ErrAuthKeyNil = errors.New("token: AuthKey was nil")
)

// Token represents an Apple Provider Authentication Token (JSON Web Token).
type Token struct {
sync.Mutex
AuthKey *ecdsa.PrivateKey
KeyID string
TeamID string
IssuedAt int64
Bearer string
}

// AuthKeyFromFile loads a .p8 certificate from a local file and returns a
// *ecdsa.PrivateKey.
func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) {
bytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return AuthKeyFromBytes(bytes)
}

// AuthKeyFromBytes loads a .p8 certificate from an in memory byte array and
// returns an *ecdsa.PrivateKey.
func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(bytes)
if block == nil {
return nil, ErrAuthKeyNotPem
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
if pk, ok := key.(*ecdsa.PrivateKey); ok {
return pk, nil
}
return nil, ErrAuthKeyNotECDSA
}

// GenerateIfExpired checks to see if the token is about to expire and
// generates a new token.
func (t *Token) GenerateIfExpired() (bearer string) {
t.Lock()
defer t.Unlock()
if t.Expired() {
t.Generate()
}
return t.Bearer
}

// Expired checks to see if the token has expired.
func (t *Token) Expired() bool {
return time.Now().Unix() >= (t.IssuedAt + Timeout)
}

// Generate creates a new token.
func (t *Token) Generate() (bool, error) {
if t.AuthKey == nil {
return false, ErrAuthKeyNil
}
issuedAt := time.Now().Unix()
jwtToken := &jwt.Token{
Header: map[string]interface{}{
"alg": "ES256",
"kid": t.KeyID,
},
Claims: jwt.MapClaims{
"iss": t.TeamID,
"iat": issuedAt,
},
Method: jwt.SigningMethodES256,
}
bearer, err := jwtToken.SignedString(t.AuthKey)
if err != nil {
return false, err
}
t.IssuedAt = issuedAt
t.Bearer = bearer
return true, nil
}
46 changes: 46 additions & 0 deletions apns/token_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package apns

import (
"encoding/json"
"fmt"
"github.com/swimresults/user-service/model"
"os"
"sync"
)

var token Token

func GetToken() string {
token.GenerateIfExpired()
return token.Bearer
}

func Init() {

authKey, err := AuthKeyFromFile("config/apns/AuthKey.p8")
if err != nil {
println("failed reading apns token file")
}

dat, err1 := os.ReadFile("config/apns/token_config.json")
if err1 != nil {
println(err1.Error())
return
}

var tokenConfig model.ApnsTokenConfig

err = json.Unmarshal(dat, &tokenConfig)
if err != nil {
println(err.Error())
return
}
fmt.Printf("set token config to: key: '%s'; team: '%s'\n", tokenConfig.KeyId, tokenConfig.TeamId)

token = Token{
Mutex: sync.Mutex{},
AuthKey: authKey,
KeyID: tokenConfig.KeyId,
TeamID: tokenConfig.TeamId,
}
}
3 changes: 3 additions & 0 deletions config/apns/AuthKey.p8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
### EXAMPLE FILE ###
-----END PRIVATE KEY-----
4 changes: 4 additions & 0 deletions config/apns/token_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"key_id": "",
"team_id": ""
}
1 change: 1 addition & 0 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func Run() {
widgetController()
dashboardController()
notificationUserController()
notificationController()

router.GET("/actuator", actuator)

Expand Down
60 changes: 60 additions & 0 deletions controller/notification_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package controller

import (
"github.com/gin-gonic/gin"
"github.com/swimresults/user-service/dto"
"github.com/swimresults/user-service/service"
"net/http"
)

func notificationController() {

router.POST("/notification/test/:device", sendTestNotification)
router.POST("/notification/:device", sendNotification)

router.OPTIONS("/notification/test/:device", okay)
router.OPTIONS("/notification/:device", okay)
}

func sendTestNotification(c *gin.Context) {

if failIfNotRoot(c) {
return
}

device := c.Param("device")

err := service.SendTestPushNotification(device)
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}

c.Status(http.StatusOK)
}

func sendNotification(c *gin.Context) {

if failIfNotRoot(c) {
return
}

device := c.Param("device")

var request dto.NotificationRequestDto
if err := c.BindJSON(&request); err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}

apnsId, body, status, err := service.SendPushNotification(device, request.Title, request.Subtitle, request.Message)
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}

c.IndentedJSON(status, dto.NotificationResponseDto{
ApnsId: apnsId,
Body: body,
})
}
33 changes: 31 additions & 2 deletions controller/notification_user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ func notificationUserController() {
router.GET("/notification_user/:id", getNotificationUserById)

router.POST("/notification_user", addNotificationUser)
router.POST("/notification_user/register", registerNotificationUser)
router.POST("/notification_user/register", registerNotificationUserWithoutToken)
router.POST("/notification_user/register/user", registerNotificationUser)

router.DELETE("/notification_user/:id", removeNotificationUser)

router.PUT("/notification_user", updateNotificationUser)

router.OPTIONS("/notification_user", okay)
router.OPTIONS("/notification_user/register", okay)
router.OPTIONS("/notification_user/register/user", okay)
}

func getNotificationUsers(c *gin.Context) {
Expand Down Expand Up @@ -148,14 +150,41 @@ func updateNotificationUser(c *gin.Context) {
c.IndentedJSON(http.StatusOK, r)
}

func registerNotificationUserWithoutToken(c *gin.Context) {
var request dto.RegisterNotificationUserRequestDto
if err := c.BindJSON(&request); err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}

r, err := service.RegisterNotificationUser(request.Token, request.Device, request.Settings, nil)
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}

c.IndentedJSON(http.StatusOK, r)
}

func registerNotificationUser(c *gin.Context) {
var request dto.RegisterNotificationUserRequestDto
if err := c.BindJSON(&request); err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}

r, err := service.RegisterNotificationUser(request.Token)
claims, err1 := getClaimsFromAuthHeader(c)

var user model.User
if err1 == nil {
user, err1 = service.GetUserByKeycloakId(claims.Sub)
if err1 != nil {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": err1.Error()})
return
}
}

r, err := service.RegisterNotificationUser(request.Token, request.Device, request.Settings, &user)
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
Expand Down
12 changes: 12 additions & 0 deletions dto/notification_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dto

type NotificationRequestDto struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Message string `json:"message"`
}

type NotificationResponseDto struct {
ApnsId string `json:"apns_id"`
Body string `json:"body"`
}
6 changes: 5 additions & 1 deletion dto/register_notification_user_request.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package dto

import "github.com/swimresults/user-service/model"

type RegisterNotificationUserRequestDto struct {
Token string `json:"token,omitempty"`
Token string `json:"token,omitempty"`
Device model.Device `json:"device,omitempty"`
Settings model.NotificationSettings `json:"settings,omitempty"`
}
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"github.com/swimresults/user-service/apns"
"github.com/swimresults/user-service/controller"
"github.com/swimresults/user-service/service"
"go.mongodb.org/mongo-driver/mongo"
Expand All @@ -16,6 +17,7 @@ var client *mongo.Client

func main() {
ctx := connectDB()
apns.Init()
service.Init(client)
controller.Run()

Expand Down
6 changes: 6 additions & 0 deletions model/ApnsTokenConfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package model

type ApnsTokenConfig struct {
KeyId string `json:"key_id"`
TeamId string `json:"team_id"`
}
13 changes: 13 additions & 0 deletions model/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package model

type Device struct {
Name string `json:"name" bson:"name"`
Model string `json:"model" bson:"model"`
LocalizedModel string `json:"localized_model" bson:"localized_model"`
SystemName string `json:"system_name" bson:"system_name"`
SystemVersion string `json:"system_version" bson:"system_version"`
Type string `json:"type" bson:"type"`
UISize string `json:"ui_size" bson:"ui_size"`
Language string `json:"language" bson:"language"`
AppVersion string `json:"app_version" bson:"app_version"`
}
Loading

0 comments on commit d245711

Please sign in to comment.