Skip to content

Commit

Permalink
adding browser push notifiication support
Browse files Browse the repository at this point in the history
  • Loading branch information
rolandosborne committed Jun 6, 2024
1 parent 054284a commit 26ef4b6
Show file tree
Hide file tree
Showing 22 changed files with 335 additions and 20 deletions.
33 changes: 33 additions & 0 deletions doc/api.oa3
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,36 @@ paths:
required: false
schema:
type: string
- name: deviceToken
in: query
description: deviceToken for push notification
required: false
schema:
type: string
- name: webEndpoint
in: query
description: webpush endpoint
required: false
schema:
type: string
- name: webPublicKey
in: query
description: webpush public key
required: false
schema:
type: string
- name: webAuth
in: query
description: webpush authorization
required: false
schema:
type: string
- name: pushType
in: query
description: unifiedpush (up) or firebase (fcm), or webpush (web)
required: false
schema:
type: string
responses:
'201':
description: success
Expand Down Expand Up @@ -4164,6 +4194,7 @@ components:
- searchable
- pushEnabled
- multiFactorAuth
- webServerKey
properties:
disabled:
type: boolean
Expand All @@ -4189,6 +4220,8 @@ components:
type: boolean
multiFactorAuth:
type: boolean
webPushKey:
type: string

AccountProfile:
type: object
Expand Down
4 changes: 3 additions & 1 deletion net/server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ require (
github.com/stretchr/testify v1.7.0
github.com/theckman/go-securerandom v0.1.1
github.com/valyala/fastjson v1.6.4
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.24.0
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.9
)

require (
github.com/SherClockHolmes/webpush-go v1.3.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions net/server/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand All @@ -7,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
Expand Down Expand Up @@ -43,12 +47,16 @@ github.com/theckman/go-securerandom v0.1.1/go.mod h1:bmkysLfBH6i891sBpcP4xRM3XIB
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
Expand Down
2 changes: 1 addition & 1 deletion net/server/internal/api_addAccountApp.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func AddAccountApp(w http.ResponseWriter, r *http.Request) {
session.Platform = platform
session.PushToken = deviceToken
session.PushType = pushType
session.PushEnabled = true
session.PushEnabled = pushType != ""
if res := tx.Save(session).Error; res != nil {
return res
}
Expand Down
1 change: 1 addition & 0 deletions net/server/internal/api_getAccountStatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func GetAccountStatus(w http.ResponseWriter, r *http.Request) {
status.Sealable = true
status.EnableIce = getBoolConfigValue(CNFEnableIce, false)
status.AllowUnsealed = getBoolConfigValue(CNFAllowUnsealed, false)
status.WebPushKey = getStrConfigValue(CNFWebPublicKey, "");
status.PushEnabled = session.PushEnabled
status.Seal = seal
WriteResponse(w, status)
Expand Down
33 changes: 33 additions & 0 deletions net/server/internal/api_setAccountNotification.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ func SetAccountNotification(w http.ResponseWriter, r *http.Request) {
return
}

deviceToken := r.FormValue("deviceToken")
webEndpoint := r.FormValue("webEndpoint")
webPublicKey := r.FormValue("webPublicKey")
webAuth := r.FormValue("webAuth");
pushType := r.FormValue("pushType");

var flag bool
if err := ParseRequest(r, w, &flag); err != nil {
ErrResponse(w, http.StatusBadRequest, err)
Expand All @@ -25,6 +31,33 @@ func SetAccountNotification(w http.ResponseWriter, r *http.Request) {
if res := tx.Model(session).Update("push_enabled", flag).Error; res != nil {
return res
}

if deviceToken != "" {
if res := tx.Model(session).Update("push_token", deviceToken).Error; res != nil {
return res
}
}
if webEndpoint != "" {
if res := tx.Model(session).Update("web_endpoint", webEndpoint).Error; res != nil {
return res
}
}
if webPublicKey != "" {
if res := tx.Model(session).Update("web_public_key", webPublicKey).Error; res != nil {
return res
}
}
if webAuth != "" {
if res := tx.Model(session).Update("web_auth", webAuth).Error; res != nil {
return res
}
}
if pushType != "" {
if res := tx.Model(session).Update("push_type", pushType).Error; res != nil {
return res
}
}

session.Account.AccountRevision += 1;
if res := tx.Model(session.Account).Update("account_revision", session.Account.AccountRevision).Error; res != nil {
return res
Expand Down
50 changes: 43 additions & 7 deletions net/server/internal/api_setPushEvent.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package databag

import (
webpush "github.com/SherClockHolmes/webpush-go"
"databag/internal/store"
"net/http"
"bytes"
Expand Down Expand Up @@ -47,7 +48,7 @@ func SendPushEvent(account store.Account, event string) {
}

// get all sessions supporting push for specified event
rows, err := store.DB.Table("sessions").Select("sessions.push_token, sessions.push_type, push_events.message_title, push_events.message_body").Joins("left join push_events on push_events.session_id = sessions.id").Where("sessions.account_id = ? AND sessions.push_enabled = ? AND push_events.event = ?", account.GUID, true, event).Rows();
rows, err := store.DB.Table("sessions").Select("sessions.push_token, sessions.push_type, sessions.web_auth, sessions.web_public_key, sessions.web_endpoint, push_events.message_title, push_events.message_body").Joins("left join push_events on push_events.session_id = sessions.id").Where("sessions.account_id = ? AND sessions.push_enabled = ? AND push_events.event = ?", account.GUID, true, event).Rows();
if err != nil {
ErrMsg(err);
return
Expand All @@ -59,16 +60,20 @@ func SendPushEvent(account store.Account, event string) {
var pushType string
var messageTitle string
var messageBody string
var webAuth string
var webPublicKey string
var webEndpoint string

rows.Scan(&pushToken, &pushType, &messageTitle, &messageBody)
if pushToken == "" || pushToken == "null" {
continue;
}
rows.Scan(&pushToken, &pushType, &webAuth, &webPublicKey, &webEndpoint, &messageTitle, &messageBody)
pushRef := pushType + ":" + pushToken + ":" + webAuth;

if _, exists := tokens[pushToken]; !exists {
tokens[pushToken] = true;
if _, exists := tokens[pushRef]; !exists {
tokens[pushRef] = true;

if pushType == "up" {
if pushToken == "" || pushToken == "null" {
continue;
}
message := []byte(messageTitle);
req, err := http.NewRequest(http.MethodPost, pushToken, bytes.NewBuffer(message))
if err != nil {
Expand All @@ -84,7 +89,38 @@ func SendPushEvent(account store.Account, event string) {
if resp.StatusCode != 200 {
ErrMsg(errors.New("failed to push notification"));
}
} else if pushType == "web" {
if webEndpoint == "" || webEndpoint == "null" {
continue;
}
keys := webpush.Keys{
Auth: webAuth,
P256dh: webPublicKey,
}
subscription := &webpush.Subscription{
Endpoint: webEndpoint,
Keys: keys,
}
msg := []byte("{ \"title\": \"Databag\", \"message\": \"" + messageTitle + "\" }")
options := &webpush.Options{
RecordSize: 0,
Topic: "Databag",
Subscriber: account.Handle,
Urgency: webpush.UrgencyHigh,
VAPIDPublicKey: getStrConfigValue(CNFWebPublicKey, ""),
VAPIDPrivateKey: getStrConfigValue(CNFWebPrivateKey, ""),
TTL: 30,
}
resp, err := webpush.SendNotification(msg, subscription, options);
defer resp.Body.Close()
if err != nil {
ErrMsg(err)
continue
}
} else {
if pushToken == "" || pushToken == "null" {
continue;
}
url := "https://fcm.googleapis.com/fcm/send"
payload := Payload{ Title: messageTitle, Body: messageBody, Sound: "default" };
message := Message{ Notification: payload, To: pushToken };
Expand Down
6 changes: 6 additions & 0 deletions net/server/internal/configUtil.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ const CNFMFASecret = "mfa_secret"
//CNFAdminSession sepcifies the admin session token
const CNFAdminSession = "admin_session"

//CNFWebPrivateKey specifies private key for webpush notifications
const CNFWebPrivateKey = "web_private_key"

//CNFWebPublicKey specifies public key for webpush notifications
const CNFWebPublicKey = "web_public_key"

func getStrConfigValue(configID string, empty string) string {
var config store.Config
err := store.DB.Where("config_id = ?", configID).First(&config).Error
Expand Down
2 changes: 2 additions & 0 deletions net/server/internal/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type AccountStatus struct {
EnableIce bool `json:"enableIce"`

AllowUnsealed bool `json:"allowUnsealed"`

WebPushKey string `json:"webPushKey"`
}

//Announce initial message sent on websocket
Expand Down
3 changes: 3 additions & 0 deletions net/server/internal/store/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ type Session struct {
PushEnabled bool
PushToken string
PushType string
WebEndpoint string
WebPublicKey string
WebAuth string
Created int64 `gorm:"autoCreateTime"`
Account Account `gorm:"references:GUID"`
Token string `gorm:"not null;index:sessguid,unique"`
Expand Down
40 changes: 40 additions & 0 deletions net/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
app "databag/internal"
"databag/internal/store"
"github.com/gorilla/handlers"
webpush "github.com/SherClockHolmes/webpush-go"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"errors"
"log"
"net/http"
"os"
Expand Down Expand Up @@ -36,6 +40,42 @@ func main() {
}

store.SetPath(storePath, transformPath);

// setup vapid keys
var config store.Config
err := store.DB.Where("config_id = ?", app.CNFWebPrivateKey).First(&config).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
if err != nil {
log.Fatal(err)
} else {
err = store.DB.Transaction(func(tx *gorm.DB) error {
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
}).Create(&store.Config{ConfigID: app.CNFWebPublicKey, StrValue: publicKey}).Error; res != nil {
return res
}
return nil
})
if err != nil {
log.Fatal(err);
}
err = store.DB.Transaction(func(tx *gorm.DB) error {
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
}).Create(&store.Config{ConfigID: app.CNFWebPrivateKey, StrValue: privateKey}).Error; res != nil {
return res
}
return nil
})
if err != nil {
log.Fatal(err);
}
}
}

router := app.NewRouter(webApp)
origins := handlers.AllowedOrigins([]string{"*"})
methods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"})
Expand Down
32 changes: 32 additions & 0 deletions net/web/public/push.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
self.addEventListener('push', function(event) {
if (!(self.Notification && self.Notification.permission === 'granted')) {
return;
}

var data = {};
if (event.data) {
data = event.data.json();
}
var title = data.title;
var message = data.message;
var icon = "favicon.ico";

self.clickTarget = self.location.origin;

event.waitUntil(self.registration.showNotification(title, {
body: message,
tag: 'Databag',
icon: icon,
}));
});

self.addEventListener('notificationclick', function(event) {
console.log('[Service Worker] Notification click Received.');

event.notification.close();

if(clients.openWindow){
event.waitUntil(clients.openWindow(self.clickTarget));
}
});

4 changes: 2 additions & 2 deletions net/web/src/api/setAccountAccess.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';

export async function setAccountAccess(token, appName, appVersion, platform) {
let access = await fetchWithTimeout(`/account/access?token=${token}&appName=${appName}&appVersion=${appVersion}&platform=${platform}`, { method: 'PUT', body: JSON.stringify([]) })
export async function setAccountAccess(token, appName, appVersion, platform, notifications) {
let access = await fetchWithTimeout(`/account/access?token=${token}&appName=${appName}&appVersion=${appVersion}&platform=${platform}`, { method: 'PUT', body: JSON.stringify(notifications) })
checkResponse(access)
return await access.json()
}
Expand Down
10 changes: 10 additions & 0 deletions net/web/src/api/setAccountNotifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';

export async function setAccountNotifications(token, webEndpoint, webPublicKey, webAuth, flag) {
const endpointEnc = encodeURIComponent(webEndpoint);
const publicKeyEnc = encodeURIComponent(webPublicKey);
const authEnc = encodeURIComponent(webAuth);
let res = await fetchWithTimeout(`/account/notification?agent=${token}&webEndpoint=${endpointEnc}&webPublicKey=${publicKeyEnc}&webAuth=${authEnc}&pushType=web`, { method: 'PUT', body: JSON.stringify(flag) })
checkResponse(res);
}

Loading

0 comments on commit 26ef4b6

Please sign in to comment.