encrypts integration secrets
This commit is contained in:
@@ -4,9 +4,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/twpayne/go-gpx"
|
||||
)
|
||||
|
||||
@@ -26,6 +29,11 @@ func SyncKomoot(app *pocketbase.PocketBase) error {
|
||||
}
|
||||
|
||||
for _, i := range integrations {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return errors.New("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
|
||||
userId := i.GetString("user")
|
||||
komootString := i.GetString("komoot")
|
||||
var komootIntegration KomootIntegration
|
||||
@@ -35,7 +43,13 @@ func SyncKomoot(app *pocketbase.PocketBase) error {
|
||||
continue
|
||||
}
|
||||
k := &KomootApi{}
|
||||
err = k.login(komootIntegration.Email, komootIntegration.Password)
|
||||
|
||||
decryptedPassword, err := security.Decrypt(komootIntegration.Password, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = k.Login(komootIntegration.Email, string(decryptedPassword))
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("komoot login failed: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
@@ -114,7 +128,7 @@ func sendRequest(url string, auth *BasicAuthToken) ([]byte, error) {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (k *KomootApi) login(email, password string) error {
|
||||
func (k *KomootApi) Login(email, password string) error {
|
||||
url := fmt.Sprintf("https://api.komoot.de/v006/account/email/%s/", email)
|
||||
|
||||
body, err := sendRequest(url, &BasicAuthToken{email, password})
|
||||
@@ -222,6 +236,11 @@ func createTrailFromTour(app *pocketbase.PocketBase, detailedTour *DetailedKomoo
|
||||
return err
|
||||
}
|
||||
|
||||
diffculty := detailedTour.Difficulty.Grade
|
||||
if diffculty == "" {
|
||||
diffculty = "easy"
|
||||
}
|
||||
|
||||
form.LoadData(map[string]any{
|
||||
"name": detailedTour.Name,
|
||||
"public": detailedTour.Status == "public",
|
||||
@@ -234,7 +253,7 @@ func createTrailFromTour(app *pocketbase.PocketBase, detailedTour *DetailedKomoo
|
||||
"external_id": strconv.Itoa(detailedTour.ID),
|
||||
"lat": detailedTour.StartPoint.Lat,
|
||||
"lon": detailedTour.StartPoint.Lng,
|
||||
"difficulty": detailedTour.Difficulty.Grade,
|
||||
"difficulty": diffculty,
|
||||
"category": categoryId,
|
||||
"waypoints": wpIds,
|
||||
"author": user,
|
||||
|
||||
@@ -2,6 +2,13 @@ package strava
|
||||
|
||||
import "time"
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientID int32 `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Code string `json:"code"`
|
||||
GrantType string `json:"grant_type"`
|
||||
}
|
||||
|
||||
type RefreshTokenRequest struct {
|
||||
ClientID int32 `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
@@ -3,9 +3,11 @@ package strava
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/twpayne/go-gpx"
|
||||
"github.com/twpayne/go-polyline"
|
||||
)
|
||||
@@ -29,16 +32,35 @@ func SyncStrava(app *pocketbase.PocketBase) error {
|
||||
}
|
||||
|
||||
for _, i := range integrations {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return errors.New("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
|
||||
userId := i.GetString("user")
|
||||
stravaString := i.GetString("strava")
|
||||
var stravaIntegration StravaIntegration
|
||||
json.Unmarshal([]byte(stravaString), &stravaIntegration)
|
||||
err := json.Unmarshal([]byte(stravaString), &stravaIntegration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !stravaIntegration.Active || stravaIntegration.RefreshToken == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
r, err := refreshStravaToken(stravaIntegration.ClientID, stravaIntegration.ClientSecret, stravaIntegration.RefreshToken)
|
||||
decryptedSecret, err := security.Decrypt(stravaIntegration.ClientSecret, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := RefreshTokenRequest{
|
||||
ClientID: stravaIntegration.ClientID,
|
||||
ClientSecret: string(decryptedSecret),
|
||||
RefreshToken: stravaIntegration.RefreshToken,
|
||||
GrantType: "refresh_token",
|
||||
}
|
||||
r, err := GetStravaToken(request)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("error refreshing strava access token: %v\n", err)
|
||||
fmt.Print(warning)
|
||||
@@ -60,7 +82,10 @@ func SyncStrava(app *pocketbase.PocketBase) error {
|
||||
return err
|
||||
}
|
||||
i.Set("strava", string(b))
|
||||
app.Dao().SaveRecord(i)
|
||||
err = app.Dao().SaveRecord(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stravaIntegration.Routes {
|
||||
page := 1
|
||||
@@ -110,15 +135,10 @@ func SyncStrava(app *pocketbase.PocketBase) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func refreshStravaToken(clientID int32, clientSecret, refreshToken string) (*RefreshTokenResponse, error) {
|
||||
func GetStravaToken(request any) (*RefreshTokenResponse, error) {
|
||||
const stravaTokenURL = "https://www.strava.com/oauth/token"
|
||||
|
||||
requestBody, err := json.Marshal(RefreshTokenRequest{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RefreshToken: refreshToken,
|
||||
GrantType: "refresh_token",
|
||||
})
|
||||
requestBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +157,7 @@ func refreshStravaToken(clientID int32, clientSecret, refreshToken string) (*Ref
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to refresh token: received status %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to get token: received status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var tokenResponse RefreshTokenResponse
|
||||
|
||||
269
db/main.go
269
db/main.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
|
||||
"pocketbase/integrations/komoot"
|
||||
"pocketbase/integrations/strava"
|
||||
@@ -76,6 +78,12 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
||||
app.OnRecordAfterCreateRequest("follows").Add(createFollowHandler(app))
|
||||
app.OnRecordAfterCreateRequest("comments").Add(createCommentHandler(app))
|
||||
|
||||
app.OnRecordsListRequest("integrations").Add(listIntegrationHandler())
|
||||
app.OnRecordAfterCreateRequest("integrations").Add(createIntegrationAfterHandler())
|
||||
app.OnRecordAfterUpdateRequest("integrations").Add(updateIntegrationAfterHandler())
|
||||
app.OnRecordBeforeCreateRequest("integrations").Add(createIntegrationBeforeHandler(app))
|
||||
app.OnRecordBeforeUpdateRequest("integrations").Add(updateIntegrationBeforeHandler(app))
|
||||
|
||||
app.OnRecordBeforeRequestEmailChangeRequest("users").Add(changeUserEmailHandler(app))
|
||||
app.OnBeforeServe().Add(onBeforeServeHandler(app, client))
|
||||
}
|
||||
@@ -330,6 +338,139 @@ func createCommentHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateE
|
||||
}
|
||||
}
|
||||
|
||||
func listIntegrationHandler() func(e *core.RecordsListEvent) error {
|
||||
return func(e *core.RecordsListEvent) error {
|
||||
info := apis.RequestInfo(e.HttpContext)
|
||||
if info.Admin != nil {
|
||||
return nil
|
||||
}
|
||||
for _, r := range e.Records {
|
||||
|
||||
err := censorIntegrationSecrets(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createIntegrationBeforeHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateEvent) error {
|
||||
return func(e *core.RecordCreateEvent) error {
|
||||
err := encryptIntegrationSecrets(app, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createIntegrationAfterHandler() func(e *core.RecordCreateEvent) error {
|
||||
return func(e *core.RecordCreateEvent) error {
|
||||
err := censorIntegrationSecrets(e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateIntegrationBeforeHandler(app *pocketbase.PocketBase) func(e *core.RecordUpdateEvent) error {
|
||||
return func(e *core.RecordUpdateEvent) error {
|
||||
err := encryptIntegrationSecrets(app, e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateIntegrationAfterHandler() func(e *core.RecordUpdateEvent) error {
|
||||
return func(e *core.RecordUpdateEvent) error {
|
||||
err := censorIntegrationSecrets(e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func censorIntegrationSecrets(r *models.Record) error {
|
||||
secrets := map[string][]string{
|
||||
"strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"},
|
||||
"komoot": {"password"},
|
||||
}
|
||||
for key, secretKeys := range secrets {
|
||||
if integrationString := r.GetString(key); integrationString != "" {
|
||||
var integration map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(integrationString), &integration); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, secretKey := range secretKeys {
|
||||
integration[secretKey] = ""
|
||||
}
|
||||
b, err := json.Marshal(integration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Set(key, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encryptIntegrationSecrets(app *pocketbase.PocketBase, r *models.Record) error {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil)
|
||||
}
|
||||
|
||||
secrets := map[string][]string{
|
||||
"strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"},
|
||||
"komoot": {"password"},
|
||||
}
|
||||
|
||||
original, _ := app.Dao().FindRecordById("integrations", r.Id)
|
||||
|
||||
for key, secretKeys := range secrets {
|
||||
if integrationString := r.GetString(key); integrationString != "" {
|
||||
var integration map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(integrationString), &integration); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, secretKey := range secretKeys {
|
||||
if secret, ok := integration[secretKey].(string); ok && len(secret) > 0 {
|
||||
encryptedSecret, err := security.Encrypt([]byte(secret), encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
integration[secretKey] = encryptedSecret
|
||||
} else if original != nil {
|
||||
|
||||
originalString := original.GetString(key)
|
||||
var originalIntegration map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(originalString), &originalIntegration); err != nil {
|
||||
return err
|
||||
}
|
||||
integration[secretKey] = originalIntegration[secretKey]
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(integration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Set(key, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func changeUserEmailHandler(app *pocketbase.PocketBase) func(e *core.RecordRequestEmailChangeEvent) error {
|
||||
return func(e *core.RecordRequestEmailChangeEvent) error {
|
||||
form := forms.NewRecordEmailChangeRequest(app, e.Record)
|
||||
@@ -402,22 +543,144 @@ func registerRoutes(e *core.ServeEvent, app *pocketbase.PocketBase, client meili
|
||||
return c.JSON(http.StatusOK, randomTrails)
|
||||
|
||||
})
|
||||
|
||||
e.Router.POST("/integration/strava/token", func(c echo.Context) error {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil)
|
||||
}
|
||||
|
||||
var data strava.TokenRequest
|
||||
if err := c.Bind(&data); err != nil {
|
||||
return apis.NewBadRequestError("Failed to read request data", err)
|
||||
}
|
||||
|
||||
user, success := c.Get(apis.ContextAuthRecordKey).(*models.Record)
|
||||
userId := ""
|
||||
if success {
|
||||
userId = user.Id
|
||||
}
|
||||
|
||||
integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("user = {:id}", dbx.Params{"id": userId}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(integrations) == 0 {
|
||||
return apis.NewBadRequestError("user has no integration", nil)
|
||||
}
|
||||
integration := integrations[0]
|
||||
stravaString := integration.GetString("strava")
|
||||
if len(stravaString) == 0 {
|
||||
return apis.NewBadRequestError("strava integration missing", nil)
|
||||
}
|
||||
var stravaIntegration strava.StravaIntegration
|
||||
err = json.Unmarshal([]byte(stravaString), &stravaIntegration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedSecret, err := security.Decrypt(stravaIntegration.ClientSecret, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := strava.TokenRequest{
|
||||
ClientID: stravaIntegration.ClientID,
|
||||
ClientSecret: string(decryptedSecret),
|
||||
Code: data.Code,
|
||||
GrantType: "authorization_code",
|
||||
}
|
||||
r, err := strava.GetStravaToken(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.AccessToken != "" {
|
||||
stravaIntegration.AccessToken = r.AccessToken
|
||||
}
|
||||
if r.RefreshToken != "" {
|
||||
stravaIntegration.RefreshToken = r.RefreshToken
|
||||
}
|
||||
if r.AccessToken != "" {
|
||||
stravaIntegration.ExpiresAt = r.ExpiresAt
|
||||
}
|
||||
|
||||
stravaIntegration.Active = true
|
||||
|
||||
b, err := json.Marshal(stravaIntegration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
integration.Set("strava", string(b))
|
||||
err = app.Dao().SaveRecord(integration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
|
||||
e.Router.GET("/integration/komoot/login", func(c echo.Context) error {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil)
|
||||
}
|
||||
|
||||
user, success := c.Get(apis.ContextAuthRecordKey).(*models.Record)
|
||||
userId := ""
|
||||
if success {
|
||||
userId = user.Id
|
||||
}
|
||||
|
||||
integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("user = {:id}", dbx.Params{"id": userId}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(integrations) == 0 {
|
||||
return apis.NewBadRequestError("user has no integration", nil)
|
||||
}
|
||||
integration := integrations[0]
|
||||
komootString := integration.GetString("komoot")
|
||||
if len(komootString) == 0 {
|
||||
return apis.NewBadRequestError("komoot integration missing", nil)
|
||||
}
|
||||
var komootIntegration komoot.KomootIntegration
|
||||
err = json.Unmarshal([]byte(komootString), &komootIntegration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedPassword, err := security.Decrypt(komootIntegration.Password, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k := &komoot.KomootApi{}
|
||||
|
||||
err = k.Login(komootIntegration.Email, string(decryptedPassword))
|
||||
if err != nil {
|
||||
return apis.NewUnauthorizedError("invalid credentials", nil)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func registerCronJobs(app *pocketbase.PocketBase) {
|
||||
scheduler := cron.New()
|
||||
|
||||
scheduler.MustAdd("integrations", "*/5 * * * *", func() {
|
||||
schedule := os.Getenv("POCKETBASE_CRON_SYNC_SCHEDULE")
|
||||
if len(schedule) == 0 {
|
||||
schedule = "0 2 * * *"
|
||||
}
|
||||
|
||||
scheduler.MustAdd("integrations", schedule, func() {
|
||||
err := strava.SyncStrava(app)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("Error syncing with strava: %v", err)
|
||||
fmt.Print(warning)
|
||||
fmt.Println(warning)
|
||||
app.Logger().Error(warning)
|
||||
}
|
||||
err = komoot.SyncKomoot(app)
|
||||
if err != nil {
|
||||
warning := fmt.Sprintf("Error syncing with komoot: %v", err)
|
||||
fmt.Print(warning)
|
||||
fmt.Println(warning)
|
||||
app.Logger().Error(warning)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user