mirror of
https://github.com/balzack/databag.git
synced 2025-03-13 00:50:03 +00:00
adding browser push notifiication support
This commit is contained in:
parent
054284a6ed
commit
26ef4b6800
33
doc/api.oa3
33
doc/api.oa3
@ -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
|
||||
@ -4164,6 +4194,7 @@ components:
|
||||
- searchable
|
||||
- pushEnabled
|
||||
- multiFactorAuth
|
||||
- webServerKey
|
||||
properties:
|
||||
disabled:
|
||||
type: boolean
|
||||
@ -4189,6 +4220,8 @@ components:
|
||||
type: boolean
|
||||
multiFactorAuth:
|
||||
type: boolean
|
||||
webPushKey:
|
||||
type: string
|
||||
|
||||
AccountProfile:
|
||||
type: object
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
@ -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=
|
||||
@ -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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
@ -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
|
||||
|
@ -1,6 +1,7 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
webpush "github.com/SherClockHolmes/webpush-go"
|
||||
"databag/internal/store"
|
||||
"net/http"
|
||||
"bytes"
|
||||
@ -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
|
||||
@ -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 {
|
||||
@ -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 };
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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"
|
||||
@ -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"})
|
||||
|
32
net/web/public/push.js
Normal file
32
net/web/public/push.js
Normal file
@ -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));
|
||||
}
|
||||
});
|
||||
|
@ -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()
|
||||
}
|
||||
|
10
net/web/src/api/setAccountNotifications.js
Normal file
10
net/web/src/api/setAccountNotifications.js
Normal file
@ -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);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
var base64 = require('base-64');
|
||||
|
||||
export async function setLogin(username, password, code, appName, appVersion, userAgent) {
|
||||
export async function setLogin(username, password, code, appName, appVersion, userAgent, notifications) {
|
||||
const platform = encodeURIComponent(userAgent);
|
||||
const mfa = code ? `&code=${code}` : '';
|
||||
let headers = new Headers()
|
||||
headers.append('Authorization', 'Basic ' + base64.encode(username + ":" + password));
|
||||
let login = await fetchWithTimeout(`/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}${mfa}`, { method: 'POST', body: JSON.stringify([]), headers: headers })
|
||||
let login = await fetchWithTimeout(`/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}${mfa}`, { method: 'POST', body: JSON.stringify(notifications), headers: headers })
|
||||
checkResponse(login)
|
||||
return await login.json()
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export const en = {
|
||||
allDevices: 'Logout of all devices',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
enableNotifications: 'Push Notifications',
|
||||
|
||||
new: 'New',
|
||||
newMessage: 'New Message',
|
||||
@ -217,6 +218,7 @@ export const fr = {
|
||||
allDevices: 'Déconnexion de tous les appareils',
|
||||
ok: 'OK',
|
||||
cancel: 'Annuler',
|
||||
enableNotifications: 'Notifications Push',
|
||||
|
||||
new: 'Nouveau',
|
||||
newMessage: 'Nouveau Message',
|
||||
@ -426,6 +428,7 @@ export const sp = {
|
||||
allDevices: 'Cerrar sesión en todos los dispositivos',
|
||||
ok: 'Aceptar',
|
||||
cancel: 'Cancelar',
|
||||
enableNotifications: 'Notificaciones Push',
|
||||
|
||||
new: 'Nuevo',
|
||||
newMessage: 'Nuevo mensaje',
|
||||
@ -634,6 +637,7 @@ export const pt = {
|
||||
allDevices: 'Desconectar de todos os dispositivos',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancelar',
|
||||
enableNotifications: 'Notificações Push',
|
||||
|
||||
new: 'Novo',
|
||||
newMessage: 'Nova mensagem',
|
||||
@ -842,6 +846,7 @@ export const de = {
|
||||
allDevices: 'Alle Geräte abmelden',
|
||||
ok: 'OK',
|
||||
cancel: 'Abbrechen',
|
||||
enableNotifications: 'Mitteilungen',
|
||||
|
||||
new: 'Neu',
|
||||
newMessage: 'Neue Nachricht',
|
||||
@ -1050,6 +1055,7 @@ export const ru = {
|
||||
allDevices: 'Выйти со всех устройств',
|
||||
ok: 'OK',
|
||||
cancel: 'Отмена',
|
||||
enableNotifications: 'Уведомления',
|
||||
|
||||
new: 'Новый',
|
||||
newMessage: 'Новое сообщение',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useEffect, useContext, useState, useRef } from 'react';
|
||||
import { setAccountSearchable } from 'api/setAccountSearchable';
|
||||
import { setAccountNotifications } from 'api/setAccountNotifications';
|
||||
import { setAccountSeal } from 'api/setAccountSeal';
|
||||
import { getAccountStatus } from 'api/getAccountStatus';
|
||||
import { setAccountLogin } from 'api/setAccountLogin';
|
||||
@ -8,12 +9,28 @@ import { setAccountMFA } from 'api/setAccountMFA';
|
||||
import { removeAccountMFA } from 'api/removeAccountMFA';
|
||||
import { StoreContext } from './StoreContext';
|
||||
|
||||
function urlB64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (var i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
export function useAccountContext() {
|
||||
const [state, setState] = useState({
|
||||
offsync: false,
|
||||
status: null,
|
||||
seal: null,
|
||||
sealKey: null,
|
||||
webPushKey: null,
|
||||
});
|
||||
const access = useRef(null);
|
||||
const setRevision = useRef(null);
|
||||
@ -40,8 +57,10 @@ export function useAccountContext() {
|
||||
const token = access.current;
|
||||
const revision = curRevision.current;
|
||||
const status = await getAccountStatus(token);
|
||||
const { seal, webPushKey } = status || {};
|
||||
|
||||
setRevision.current = revision;
|
||||
updateState({ offsync: false, status, seal: status.seal });
|
||||
updateState({ offsync: false, status, seal, webPushKey });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
@ -75,6 +94,39 @@ export function useAccountContext() {
|
||||
setSearchable: async (flag) => {
|
||||
await setAccountSearchable(access.current, flag);
|
||||
},
|
||||
setPushEnabled: async (flag) => {
|
||||
if (flag) {
|
||||
const status = await Notification.requestPermission();
|
||||
if (status == 'granted') {
|
||||
const registration = await navigator.serviceWorker.register('push.js');
|
||||
await navigator.serviceWorker.ready;
|
||||
const params = { userVisibleOnly: true, applicationServerKey: urlB64ToUint8Array(state.webPushKey) };
|
||||
const subscription = await registration.pushManager.subscribe(params);
|
||||
|
||||
const endpoint = subscription.endpoint;
|
||||
const binPublicKey = subscription.getKey('p256dh');
|
||||
const binAuth = subscription.getKey('auth');
|
||||
|
||||
if (endpoint && binPublicKey && binAuth) {
|
||||
const numPublicKey = [];
|
||||
(new Uint8Array(binPublicKey)).forEach(val => {
|
||||
numPublicKey.push(val);
|
||||
});
|
||||
const numAuth = [];
|
||||
(new Uint8Array(binAuth)).forEach(val => {
|
||||
numAuth.push(val);
|
||||
});
|
||||
const publicKey = btoa(String.fromCharCode.apply(null, numPublicKey));
|
||||
const auth = btoa(String.fromCharCode.apply(null, numAuth));
|
||||
|
||||
await setAccountNotifications(access.current, endpoint, publicKey, auth, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
await setAccountNotifications(access.current, '', '', '', false);
|
||||
}
|
||||
},
|
||||
enableMFA: async () => {
|
||||
const secret = await addAccountMFA(access.current);
|
||||
return secret;
|
||||
|
@ -70,6 +70,16 @@ export function useAppContext(websocket) {
|
||||
clearWebsocket();
|
||||
}
|
||||
|
||||
const notifications = [
|
||||
{ event: 'contact.addCard', messageTitle: 'New Contact Request' },
|
||||
{ event: 'contact.updateCard', messageTitle: 'Contact Update' },
|
||||
{ event: 'content.addChannel.superbasic', messageTitle: 'New Topic' },
|
||||
{ event: 'content.addChannel.sealed', messageTitle: 'New Topic' },
|
||||
{ event: 'content.addChannelTopic.superbasic', messageTitle: 'New Topic Message' },
|
||||
{ event: 'content.addChannelTopic.sealed', messageTitle: 'New Topic Message' },
|
||||
{ event: 'ring', messageTitle: 'Incoming Call' },
|
||||
];
|
||||
|
||||
const actions = {
|
||||
logout: async (all) => {
|
||||
await appLogout(all);
|
||||
@ -96,7 +106,7 @@ export function useAppContext(websocket) {
|
||||
throw new Error('invalid session state');
|
||||
}
|
||||
await addAccount(username, password, token);
|
||||
const access = await setLogin(username, password, null, appName, appVersion, userAgent);
|
||||
const access = await setLogin(username, password, null, appName, appVersion, userAgent, notifications);
|
||||
storeContext.actions.setValue('login:timestamp', access.created);
|
||||
setSession(access.appToken);
|
||||
appToken.current = access.appToken;
|
||||
@ -112,7 +122,7 @@ export function useAppContext(websocket) {
|
||||
if (appToken.current || !checked.current) {
|
||||
throw new Error('invalid session state');
|
||||
}
|
||||
const access = await setLogin(username, password, code, appName, appVersion, userAgent);
|
||||
const access = await setLogin(username, password, code, appName, appVersion, userAgent, notifications);
|
||||
storeContext.actions.setValue('login:timestamp', access.created);
|
||||
setSession(access.appToken);
|
||||
appToken.current = access.appToken;
|
||||
@ -128,7 +138,7 @@ export function useAppContext(websocket) {
|
||||
if (appToken.current || !checked.current) {
|
||||
throw new Error('invalid session state');
|
||||
}
|
||||
const access = await setAccountAccess(token, appName, appVersion, userAgent);
|
||||
const access = await setAccountAccess(token, appName, appVersion, userAgent, notifications);
|
||||
storeContext.actions.setValue('login:timestamp', access.created);
|
||||
setSession(access.appToken);
|
||||
appToken.current = access.appToken;
|
||||
|
@ -40,6 +40,20 @@ export function AccountAccess() {
|
||||
}
|
||||
};
|
||||
|
||||
const savePushEnabled = async (enable) => {
|
||||
try {
|
||||
await actions.setPushEnabled(enable);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
modal.error({
|
||||
title: <span style={state.menuStyle}>{state.strings.operationFailed}</span>,
|
||||
content: <span style={state.menuStyle}>{state.strings.tryAgain}</span>,
|
||||
bodyStyle: { borderRadius: 8, padding: 16, ...state.menuStyle },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const enableMFA = async (enable) => {
|
||||
try {
|
||||
if (enable) {
|
||||
@ -104,6 +118,12 @@ export function AccountAccess() {
|
||||
</div>
|
||||
<div className="switchLabel">{state.strings.registry}</div>
|
||||
</div>
|
||||
<div className="switch">
|
||||
<div className="control">
|
||||
<Switch size="small" checked={state.pushEnabled} onChange={enable => savePushEnabled(enable)} />
|
||||
</div>
|
||||
<div className="switchLabel">{state.strings.enableNotifications}</div>
|
||||
</div>
|
||||
<div className="switch">
|
||||
<div className="control">
|
||||
<Switch size="small" checked={state.mfaEnabled} onChange={enable => enableMFA(enable)} />
|
||||
|
@ -19,6 +19,8 @@ export function useAccountAccess() {
|
||||
searchable: null,
|
||||
checked: true,
|
||||
|
||||
pushEnabled: false,
|
||||
webPushKey: null,
|
||||
sealEnabled: false,
|
||||
sealMode: null,
|
||||
sealPassword: null,
|
||||
@ -67,8 +69,9 @@ export function useAccountAccess() {
|
||||
}, [profile.state]);
|
||||
|
||||
useEffect(() => {
|
||||
const { seal, sealKey, status } = account.state;
|
||||
updateState({ searchable: status?.searchable, mfaEnabled: status?.mfaEnabled, seal, sealKey });
|
||||
const { seal, sealKey, status, webPushKey } = account.state;
|
||||
const { searchable, mfaEnabled, pushEnabled } = status || {};
|
||||
updateState({ searchable, mfaEnabled, seal, sealKey, webPushKey, pushEnabled });
|
||||
}, [account.state]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -315,6 +318,20 @@ export function useAccountAccess() {
|
||||
}
|
||||
}
|
||||
},
|
||||
setPushEnabled: async (flag) => {
|
||||
if (!state.busy) {
|
||||
try {
|
||||
updateState({ busy: true });
|
||||
await account.actions.setPushEnabled(flag);
|
||||
updateState({ busy: false });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
updateState({ busy: false });
|
||||
throw new Error('failed to set searchable');
|
||||
}
|
||||
}
|
||||
},
|
||||
setCode: async (code) => {
|
||||
updateState({ mfaCode: code });
|
||||
},
|
||||
|
@ -126,7 +126,7 @@ export function AddTopic({ contentKey }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="message">
|
||||
<Input.TextArea ref={msg} placeholder={state.strings.newMessage} spellCheck="true" autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
<Input.TextArea className="messageInput" ref={msg} placeholder={state.strings.newMessage} spellCheck="true" autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
enterkeyhint="send" onKeyDown={(e) => keyDown(e)} onChange={(e) => actions.setMessageText(e.target.value)}
|
||||
value={state.messageText} autocapitalize="none" />
|
||||
</div>
|
||||
|
@ -26,6 +26,10 @@ export const AddTopicWrapper = styled.div`
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
background-color: ${props => props.theme.inputArea};
|
||||
}
|
||||
|
||||
.assets {
|
||||
margin-top: 8px;
|
||||
height: 128px;
|
||||
|
Loading…
Reference in New Issue
Block a user