Skip to content

Commit 4957f82

Browse files
committed
Added basic management interface
1 parent c67968f commit 4957f82

11 files changed

+440
-32
lines changed

admin.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"github.com/ether/etherpad-proxy/ui"
6+
"go.uber.org/zap"
7+
"net/http"
8+
)
9+
10+
type AdminPanel struct {
11+
DB *DB
12+
logger *zap.SugaredLogger
13+
}
14+
15+
func (a *AdminPanel) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
16+
padIDMap, err := a.DB.getAllPads()
17+
if err != nil {
18+
http.Error(w, err.Error(), http.StatusInternalServerError)
19+
return
20+
}
21+
var component = ui.Management(padIDMap)
22+
if err := component.Render(context.Background(), w); err != nil {
23+
a.logger.Error("Error while rendering template: ", err)
24+
}
25+
}

db.go

+82-10
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package main
22

33
import (
44
"database/sql"
5-
"encoding/json"
65
"github.com/ether/etherpad-proxy/models"
76
_ "modernc.org/sqlite"
87
)
98

9+
import sq "github.com/Masterminds/squirrel"
10+
1011
type DB struct {
1112
Conn *sql.DB
1213
}
@@ -24,7 +25,11 @@ func NewDB(filename string) (*DB, error) {
2425
if _, err = db.Conn.Exec("PRAGMA foreign_keys = ON"); err != nil {
2526
return nil, err
2627
}
27-
if _, err = db.Conn.Exec("CREATE TABLE IF NOT EXISTS pad (id TEXT, data TEXT)"); err != nil {
28+
if _, err = db.Conn.Exec("CREATE TABLE IF NOT EXISTS pad (id TEXT, backend TEXT, PRIMARY KEY (id))"); err != nil {
29+
return nil, err
30+
}
31+
32+
if _, err = db.Conn.Exec("CREATE TABLE IF NOT EXISTS clashes (id TEXT, data TEXT, PRIMARY KEY (id, data))"); err != nil {
2833
return nil, err
2934
}
3035

@@ -37,28 +42,95 @@ func (db *DB) Close() error {
3742

3843
func (db *DB) Get(id string) (*models.DBBackend, error) {
3944
var data string
40-
err := db.Conn.QueryRow("SELECT data FROM pad WHERE id = ?", id).Scan(&data)
45+
var sqlGet, args, err = sq.Select("backend").From("pad").Where(sq.Eq{"id": id}).ToSql()
4146
if err != nil {
4247
return nil, err
4348
}
44-
45-
var actualData models.DBBackend
46-
47-
if err = json.Unmarshal([]byte(data), &actualData); err != nil {
49+
err = db.Conn.QueryRow(sqlGet, args...).Scan(&data)
50+
if err != nil {
4851
return nil, err
4952
}
5053

54+
var actualData = models.DBBackend{
55+
Backend: data,
56+
}
57+
5158
return &actualData, nil
5259
}
5360

54-
func (db *DB) Set(id string, dbModel models.DBBackend) error {
55-
data, err := json.Marshal(&dbModel)
61+
func (db *DB) CleanUpPads(padIds []string, padPrefix string) error {
62+
sqlDelete, args, err := sq.Delete("pad").Where(sq.And{sq.NotEq{"id": padIds},
63+
sq.Like{"backend": padPrefix}}).ToSql()
5664
if err != nil {
5765
return err
5866
}
59-
_, err = db.Conn.Exec("INSERT OR REPLACE INTO pad (id, data) VALUES (?, ?)", id, string(data))
67+
68+
_, err = db.Conn.Exec(sqlDelete, args...)
69+
return err
70+
}
71+
72+
func (db *DB) RecordClash(id string, data string) error {
73+
_, err := db.Conn.Exec("INSERT OR REPLACE INTO clashes (id, data) VALUES (?, ?)", id, data)
6074
if err != nil {
6175
return err
6276
}
6377
return nil
6478
}
79+
80+
func (db *DB) Set(id string, dbModel models.DBBackend) error {
81+
82+
_, err := db.Conn.Exec("INSERT OR REPLACE INTO pad (id, backend) VALUES (?, ?)", id, dbModel.Backend)
83+
if err != nil {
84+
return err
85+
}
86+
return nil
87+
}
88+
89+
func (db *DB) getAllPads() (map[string]string, error) {
90+
var padIDMap = make(map[string]string)
91+
var sqlGet, args, err = sq.Select("id, backend").From("pad").ToSql()
92+
if err != nil {
93+
return nil, err
94+
}
95+
rows, err := db.Conn.Query(sqlGet, args...)
96+
if err != nil {
97+
return nil, err
98+
}
99+
defer rows.Close()
100+
101+
for rows.Next() {
102+
var padID string
103+
var backend string
104+
if err := rows.Scan(&padID, &backend); err != nil {
105+
return nil, err
106+
}
107+
padIDMap[padID] = backend
108+
}
109+
110+
return padIDMap, nil
111+
}
112+
113+
func (db *DB) getClashByPadID(padId string) ([]string, error) {
114+
var sqlGet, args, err = sq.Select("data").From("clashes").Where(sq.Eq{"id": padId}).ToSql()
115+
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
rows, err := db.Conn.Query(sqlGet, args...)
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
defer rows.Close()
126+
var data = make([]string, 0)
127+
for rows.Next() {
128+
var clash string
129+
if err := rows.Scan(&clash); err != nil {
130+
return nil, err
131+
}
132+
data = append(data, clash)
133+
}
134+
135+
return data, nil
136+
}

go.mod

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ module github.com/ether/etherpad-proxy
33
go 1.24.0
44

55
require (
6+
github.com/PuerkitoBio/goquery v1.10.2
67
github.com/a-h/templ v0.3.857
78
go.uber.org/zap v1.27.0
9+
golang.org/x/oauth2 v0.29.0
810
modernc.org/sqlite v1.37.0
911
)
1012

1113
require (
12-
github.com/PuerkitoBio/goquery v1.10.2 // indirect
14+
github.com/Masterminds/squirrel v1.5.4 // indirect
1315
github.com/andybalholm/cascadia v1.3.3 // indirect
1416
github.com/dustin/go-humanize v1.0.1 // indirect
1517
github.com/google/uuid v1.6.0 // indirect
18+
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
19+
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
1620
github.com/mattn/go-isatty v0.0.20 // indirect
1721
github.com/ncruces/go-strftime v0.1.9 // indirect
1822
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect

go.sum

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
2+
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
13
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
24
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
35
github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg=
@@ -14,6 +16,10 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
1416
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
1517
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
1618
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
19+
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
20+
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
21+
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
22+
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
1723
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
1824
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
1925
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -22,6 +28,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
2228
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2329
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
2430
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
31+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
2532
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
2633
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
2734
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -57,6 +64,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
5764
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
5865
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
5966
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
67+
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
68+
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
6069
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
6170
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
6271
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

models/ListAllPadsModel.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package models
2+
3+
type ListAllPadsModel struct {
4+
Code int `json:"code"`
5+
Message string `json:"message"`
6+
Data struct {
7+
PadIds []string `json:"padIDs"`
8+
} `json:"data"`
9+
}

models/Settings.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ type DBSettings struct {
1313
}
1414

1515
type Backend struct {
16-
Host string `json:"host"`
17-
Port int `json:"port"`
16+
Host string `json:"host"`
17+
Port int `json:"port"`
18+
ClientId *string `json:"clientId"`
19+
ClientSecret *string `json:"clientSecret"`
20+
Scopes []string `json:"scopes"`
21+
TokenURL *string `json:"tokenUrl"`
22+
Username *string `json:"username"`
23+
Password *string `json:"password"`
1824
}

proxyHandler.go

+31-13
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import (
1919
import "math/rand/v2"
2020
import _ "github.com/ether/etherpad-proxy/ui"
2121

22-
const PadPrefix = "padId:"
23-
2422
type ProxyHandler struct {
2523
p map[string]httputil.ReverseProxy
2624
logger *zap.SugaredLogger
@@ -42,6 +40,14 @@ func (m *ResourceNotFound) Error() string {
4240
return "Resource not found"
4341
}
4442

43+
type ClashInPadId struct {
44+
padId string
45+
}
46+
47+
func (m *ClashInPadId) Error() string {
48+
return "Resource not found"
49+
}
50+
4551
func ScrapeJSFiles(backends models.Settings) {
4652
go func() {
4753
for {
@@ -114,24 +120,36 @@ func (ph *ProxyHandler) createRoute(padId *string, r *http.Request) (*httputil.R
114120
return &chosenBackend, nil
115121
}
116122

117-
var padRead, err = ph.db.Get(PadPrefix + *padId)
123+
var padRead, err = ph.db.Get(*padId)
118124
if len(AvailableBackends.Available) == 0 {
119125
return nil, errors.New("no backends available")
120126
}
121127
if errors.Is(err, sql.ErrNoRows) {
122128
// if no backend is stored for this pad, create a new connection
123-
AvailableBackends.Mutex.Lock()
124-
var newBackend = AvailableBackends.Available[rand.IntN(len(AvailableBackends.Available))]
125-
AvailableBackends.Mutex.Unlock()
129+
result, err := ph.db.getClashByPadID(*padId)
126130

127-
if err = ph.db.Set(PadPrefix+*padId, models.DBBackend{
128-
Backend: newBackend,
129-
}); err != nil {
130-
ph.logger.Info("Error while setting padId in DB: ", err)
131+
if err != nil && errors.Is(err, sql.ErrNoRows) || len(result) == 0 {
132+
AvailableBackends.Mutex.Lock()
133+
var newBackend = AvailableBackends.Available[rand.IntN(len(AvailableBackends.Available))]
134+
AvailableBackends.Mutex.Unlock()
135+
136+
if err = ph.db.Set(*padId, models.DBBackend{
137+
Backend: newBackend,
138+
}); err != nil {
139+
ph.logger.Info("Error while setting padId in DB: ", err)
140+
}
141+
var chosenBackend = ph.p[newBackend]
142+
143+
return &chosenBackend, nil
144+
} else if err != nil {
145+
return nil, err
131146
}
132-
var chosenBackend = ph.p[newBackend]
133147

134-
return &chosenBackend, nil
148+
// There is an active clash for this pad
149+
ph.logger.Warnf("Pad %s is in a clash with backends: %v", *padId, result)
150+
return nil, &ClashInPadId{
151+
padId: *padId,
152+
}
135153
}
136154

137155
if len(AvailableBackends.Available) == 0 {
@@ -145,7 +163,7 @@ func (ph *ProxyHandler) createRoute(padId *string, r *http.Request) (*httputil.R
145163
AvailableBackends.Mutex.Lock()
146164
newBackend := AvailableBackends.Up[rand.IntN(len(AvailableBackends.Up))]
147165
AvailableBackends.Mutex.Unlock()
148-
if err = ph.db.Set(PadPrefix+*padId, models.DBBackend{
166+
if err = ph.db.Set(*padId, models.DBBackend{
149167
Backend: newBackend,
150168
}); err != nil {
151169
ph.logger.Info("Error while setting padId in DB: ", err)

0 commit comments

Comments
 (0)