Skip to content

Commit

Permalink
chore:less boilerplate heavy ios notifications (#272)
Browse files Browse the repository at this point in the history
* removed a bit of ios boilerplate

* removed even more boilerplate

* removed another instance of boilerplate

* more refactoring

* more simplification work

* refactored the enum mirator to also be simpler to maintain
  • Loading branch information
CommanderStorm authored Oct 24, 2023
1 parent 5682ad9 commit 4d668ae
Show file tree
Hide file tree
Showing 25 changed files with 773 additions and 960 deletions.
1,290 changes: 644 additions & 646 deletions server/api/tumdev/campus_backend.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/api/tumdev/campus_backend.proto
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ enum DeviceType {

message CreateDeviceRequest {
string device_id = 1;
optional string public_key = 2;
string public_key = 2;
DeviceType device_type = 3;
}

Expand Down
14 changes: 7 additions & 7 deletions server/backend/cafeteria.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func queryTags(cafeteriaID int32, dishID int32, ratingType ModelType, tx *gorm.D
}

if err != nil {
log.WithError(err).Error("while querying the tags for the request.")
log.WithError(err).Error("while querying the tags for the request")
}

//needed since the gRPC element does not specify column names - cannot be directly queried into the grpc message object.
Expand Down Expand Up @@ -672,25 +672,25 @@ func (s *CampusServer) GetCafeterias(ctx context.Context, _ *pb.ListCanteensRequ
}, requestStatus
}

func (s *CampusServer) ListDishes(ctx context.Context, request *pb.ListDishesRequest) (*pb.ListDishesReply, error) {
if request.Year < 2022 {
func (s *CampusServer) ListDishes(ctx context.Context, req *pb.ListDishesRequest) (*pb.ListDishesReply, error) {
if req.Year < 2022 {
return &pb.ListDishesReply{}, status.Error(codes.Internal, "Years must be larger or equal to 2022 ") // currently, no previous values have been added
}
if request.Week < 1 || request.Week > 53 {
if req.Week < 1 || req.Week > 53 {
return &pb.ListDishesReply{}, status.Error(codes.Internal, "Weeks must be in the range 1 - 53")
}
if request.Day < 0 || request.Day > 4 {
if req.Day < 0 || req.Day > 4 {
return &pb.ListDishesReply{}, status.Error(codes.Internal, "Days must be in the range 1 (Monday) - 4 (Friday)")
}

var requestStatus error = nil
var results []string
err := s.db.WithContext(ctx).Table("dishes_of_the_week weekly").
Where("weekly.day = ? AND weekly.week = ? and weekly.year = ?", request.Day, request.Week, request.Year).
Where("weekly.day = ? AND weekly.week = ? and weekly.year = ?", req.Day, req.Week, req.Year).
Select("weekly.dishID").
Joins("JOIN dish d ON d.dish = weekly.dishID").
Joins("JOIN cafeteria c ON c.cafeteria = d.cafeteriaID").
Where("c.name LIKE ?", request.CanteenId).
Where("c.name LIKE ?", req.CanteenId).
Select("d.name").
Find(&results).Error

Expand Down
5 changes: 4 additions & 1 deletion server/backend/campus_api/campus_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"fmt"
"io"
"net/http"
"os"

"github.com/TUM-Dev/Campus-Backend/server/model"
log "github.com/sirupsen/logrus"
)

func FetchExamResultsPublished(token string) (*model.TUMAPIPublishedExamResults, error) {
// FetchExamResultsPublished fetches all published exam results from the TUM Campus API using CAMPUS_API_TOKEN.
func FetchExamResultsPublished() (*model.TUMAPIPublishedExamResults, error) {
var examResultsPublished model.TUMAPIPublishedExamResults
token := os.Getenv("CAMPUS_API_TOKEN")
err := RequestCampusApi("/wbservicesbasic.pruefungenErgebnisse", token, &examResultsPublished)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions server/backend/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (s *CampusServer) CreateDevice(_ context.Context, req *pb.CreateDeviceReque
return nil, status.Error(codes.InvalidArgument, err.Error())
}

switch req.GetDeviceType() {
switch req.DeviceType {
case pb.DeviceType_ANDROID:
return nil, status.Error(codes.Unimplemented, "android device creation not implemented")
case pb.DeviceType_IOS:
Expand All @@ -146,7 +146,7 @@ func (s *CampusServer) DeleteDevice(_ context.Context, req *pb.DeleteDeviceReque
return nil, status.Error(codes.InvalidArgument, err.Error())
}

switch req.GetDeviceType() {
switch req.DeviceType {
case pb.DeviceType_ANDROID:
return nil, status.Error(codes.Unimplemented, "android device remove not implemented")
case pb.DeviceType_IOS:
Expand Down
34 changes: 10 additions & 24 deletions server/backend/ios_notifications/apns/jwt_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,6 @@ import (
log "github.com/sirupsen/logrus"
)

const (
// TokenTimeout for the token in seconds
TokenTimeout = 3000
)

var (
ErrorAuthKeyNotPem = errors.New("failed to parse token: AuthKey must be a valid .p8 PEM file")
ErrorAuthKeyNotEcdsa = errors.New("failed to parse token: AuthKey must be of type ecdsa.PrivateKey")
ErrorAuthKeyNil = errors.New("failed to parse token: AuthKey was nil")
ApnsKeyId = os.Getenv("APNS_KEY_ID")
ApnsTeamId = os.Getenv("APNS_TEAM_ID")
ApnsP8FilePath = os.Getenv("APNS_P8_FILE_PATH")
)

type JWTToken struct {
sync.Mutex
EncryptionKey *ecdsa.PrivateKey
Expand All @@ -39,15 +25,15 @@ type JWTToken struct {
}

func NewToken() (*JWTToken, error) {
encryptionKey, err := APNsEncryptionKeyFromFile()
encryptionKey, err := EncryptionKeyFromFile()
if err != nil {
return nil, err
}

token := JWTToken{
EncryptionKey: encryptionKey,
KeyId: ApnsKeyId,
TeamId: ApnsTeamId,
KeyId: os.Getenv("APNS_KEY_ID"),
TeamId: os.Getenv("APNS_TEAM_ID"),
}

if err = token.Generate(); err != nil {
Expand All @@ -57,11 +43,11 @@ func NewToken() (*JWTToken, error) {
return &token, nil
}

// APNsEncryptionKeyFromFile reads the APNs encryption key from the file system
// EncryptionKeyFromFile reads the APNs encryption key from the file system
// and returns it as an ecdsa.PrivateKey
// The file location is defined by the APNS_P8_FILE_PATH environment variable
func APNsEncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
path, err := filepath.Abs(ApnsP8FilePath)
func EncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
path, err := filepath.Abs(os.Getenv("APNS_P8_FILE_PATH"))

if err != nil {
log.Error("No valid path to AuthKey")
Expand All @@ -81,7 +67,7 @@ func APNsEncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
if block == nil {
log.Error("Could not decode APNs encryption key from file")

return nil, ErrorAuthKeyNotPem
return nil, errors.New("failed to parse token: AuthKey must be a valid .p8 PEM file")
}

key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
Expand All @@ -96,7 +82,7 @@ func APNsEncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
return ecdsaKey, nil
}

return nil, ErrorAuthKeyNotEcdsa
return nil, errors.New("failed to parse token: AuthKey must be of type ecdsa.PrivateKey")
}

func (t *JWTToken) GenerateNewTokenIfExpired() (bearer string) {
Expand All @@ -114,12 +100,12 @@ func (t *JWTToken) GenerateNewTokenIfExpired() (bearer string) {
}

func (t *JWTToken) IsExpired() bool {
return currentTimestamp() >= (t.IssuedAt + TokenTimeout)
return currentTimestamp() >= (t.IssuedAt + 3000)
}

func (t *JWTToken) Generate() error {
if t.EncryptionKey == nil {
return ErrorAuthKeyNil
return errors.New("failed to parse token: AuthKey was nil")
}

issuedAt := currentTimestamp()
Expand Down
53 changes: 12 additions & 41 deletions server/backend/ios_notifications/apns/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,6 @@ import (
"gorm.io/gorm"
)

const (
// BundleId from the Apple Developer Portal
BundleId = "de.tum.tca"
// ReadIdleTimeout is the idle time after which the http2 transport will do a health check
ReadIdleTimeout = 15 * time.Second
// HTTPClientTimeout is the timeout for the http client used to send notifications
HTTPClientTimeout = 60 * time.Second
)

const (
ApnsDevelopmentURL = "https://api.sandbox.push.apple.com:443"
ApnsProductionURL = "https://api.push.apple.com:443"
)

var (
ErrCouldNotSendNotification = errors.New("could not send notification")
ErrCouldNotDecodeAPNsResponse = errors.New("could not decode apns response")
)

type Repository struct {
DB gorm.DB
Token *JWTToken
Expand All @@ -43,11 +24,11 @@ type Repository struct {

// ApnsUrl uses the environment variable ENVIRONMENT to determine whether
// to use the production or development APNs URL.
func (r *Repository) ApnsUrl() string {
func (r *Repository) ApnsUrl(DeviceId string) string {
if env.IsProd() {
return ApnsProductionURL
return "https://api.push.apple.com:443/3/device/" + DeviceId
}
return ApnsDevelopmentURL
return "https://api.sandbox.push.apple.com:443/3/device/" + DeviceId
}

// CreateCampusTokenRequest creates a request log in the database that can be referred to
Expand All @@ -72,34 +53,24 @@ func (r *Repository) CreateRequest(deviceId string, requestType model.IOSBackgro
return &request, nil
}

func (r *Repository) SendAlertNotification(payload *model.IOSNotificationPayload) (*model.IOSRemoteNotificationResponse, error) {
return r.SendNotification(payload, model.IOSAPNSPushTypeAlert, 10)
}

func (r *Repository) SendBackgroundNotification(payload *model.IOSNotificationPayload) (*model.IOSRemoteNotificationResponse, error) {
return r.SendNotification(payload, model.IOSAPNSPushTypeBackground, 10)
}

func (r *Repository) SendNotification(notification *model.IOSNotificationPayload, apnsPushType model.IOSAPNSPushType, priority int) (*model.IOSRemoteNotificationResponse, error) {

url := r.ApnsUrl() + "/3/device/" + notification.DeviceId
func (r *Repository) SendNotification(notification *model.IOSNotificationPayload, apnsPushType model.IOSAPNSPushType) (*model.IOSRemoteNotificationResponse, error) {
body, _ := notification.MarshalJSON()

req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
req, _ := http.NewRequest(http.MethodPost, r.ApnsUrl(notification.DeviceId), bytes.NewBuffer(body))

// can be e.g. alert or background
req.Header.Set("apns-push-type", apnsPushType.String())
req.Header.Set("apns-topic", BundleId)
req.Header.Set("apns-topic", "de.tum.tca")
// can be a value between 1 and 10
req.Header.Set("apns-priority", strconv.Itoa(priority))
req.Header.Set("apns-priority", strconv.Itoa(10))

bearer := r.Token.GenerateNewTokenIfExpired()
req.Header.Set("authorization", "bearer "+bearer)

resp, err := r.httpClient.Do(req)
if err != nil {
log.WithError(err).Error("Could not send notification")
return nil, ErrCouldNotSendNotification
return nil, errors.New("could not send notification")
}
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
Expand All @@ -108,25 +79,25 @@ func (r *Repository) SendNotification(notification *model.IOSNotificationPayload
}(resp.Body)

var response model.IOSRemoteNotificationResponse
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil && err != io.EOF {
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil && err != io.EOF {
log.WithError(err).Error("Could not decode APNs response")
return nil, ErrCouldNotDecodeAPNsResponse
return nil, errors.New("could not decode apns response")
}

return &response, nil
}

func NewRepository(db *gorm.DB, token *JWTToken) *Repository {
transport := &http2.Transport{
ReadIdleTimeout: ReadIdleTimeout,
ReadIdleTimeout: 15 * time.Second,
}

return &Repository{
DB: *db,
Token: token,
httpClient: &http.Client{
Transport: transport,
Timeout: HTTPClientTimeout,
Timeout: 60 * time.Second,
},
}
}
Expand Down
22 changes: 8 additions & 14 deletions server/backend/ios_notifications/apns/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package apns

import (
"errors"
"os"

"github.com/TUM-Dev/Campus-Backend/server/model"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -32,30 +33,23 @@ func (s *Service) RequestGradeUpdateForDevice(deviceID string) error {

notification := model.NewIOSNotificationPayload(deviceID).Background(campusRequestToken.RequestID, model.IOSBackgroundCampusTokenRequest)

if _, err := s.Repository.SendBackgroundNotification(notification); err != nil {
if _, err := s.Repository.SendNotification(notification, model.IOSAPNSPushTypeBackground); err != nil {
log.WithError(err).Error("Could not send background notification")
return ErrCouldNotSendNotification
return errors.New("could not send notification")
}
return nil
}

func ValidateRequirementsForIOSNotificationsService() error {
if ApnsKeyId == "" {
return errors.New("APNS_KEY_ID env variable is not set")
for _, envVar := range []string{"APNS_KEY_ID", "APNS_TEAM_ID", "APNS_P8_FILE_PATH"} {
if os.Getenv(envVar) == "" {
return errors.New(envVar + " env variable is not set")
}
}

if ApnsTeamId == "" {
return errors.New("APNS_TEAM_ID env variable is not set")
}

if ApnsP8FilePath == "" {
return errors.New("APNS_P8_FILE_PATH env variable is not set")
}

if _, err := APNsEncryptionKeyFromFile(); err != nil {
if _, err := EncryptionKeyFromFile(); err != nil {
return errors.New("APNS P8 token is not valid or not set")
}

return nil
}

Expand Down
8 changes: 3 additions & 5 deletions server/backend/ios_notifications/crypto/encrypted_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,19 @@ func AsymmetricEncrypt(plaintext string, publicKey string) (*EncryptedString, er

func StringToPublicKey(pub string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pub))

if block == nil {
return nil, errors.New("failed to parse PEM block containing the public key")
}

key, err := x509.ParsePKIXPublicKey(block.Bytes)

if err != nil {
return nil, errors.New("failed to parse DER encoded public key: " + err.Error())
}

if pubKey, ok := key.(*rsa.PublicKey); ok {
return pubKey, nil
} else {
if pubKey, ok := key.(*rsa.PublicKey); !ok {
return nil, errors.New("failed to parse DER encoded public key: " + err.Error())
} else {
return pubKey, nil
}
}

Expand Down
Loading

0 comments on commit 4d668ae

Please sign in to comment.