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

simple notification request, apns config #8

Merged
merged 11 commits into from
Nov 21, 2024
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
Loading