add remote implementation of admin store

This commit is contained in:
Umputun
2019-06-25 20:06:30 -05:00
parent 988206bed4
commit b30492556f
12 changed files with 196 additions and 181 deletions
+1 -17
View File
@@ -2,14 +2,12 @@ package cmd
import (
"path"
"time"
bolt "github.com/coreos/bbolt"
log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/mongo"
)
// AvatarCommand set of flags and command for avatar migration
@@ -18,7 +16,7 @@ import (
type AvatarCommand struct {
AvatarSrc AvatarGroup `group:"src" namespace:"src"`
AvatarDst AvatarGroup `group:"dst" namespace:"dst"`
Mongo MongoGroup `group:"mongo" namespace:"mongo" env-namespace:"MONGO"`
// Mongo MongoGroup `group:"mongo" namespace:"mongo" env-namespace:"MONGO"`
migrator AvatarMigrator
CommonOpts
@@ -78,13 +76,6 @@ func (ac *AvatarCommand) makeAvatarStore(gr AvatarGroup) (avatar.Store, error) {
return nil, err
}
return avatar.NewLocalFS(gr.FS.Path), nil
case "mongo":
mgServer, err := ac.makeMongo()
if err != nil {
return nil, errors.Wrap(err, "failed to create mongo server")
}
conn := mongo.NewConnection(mgServer, ac.Mongo.DB, "")
return avatar.NewGridFS(conn), nil
case "bolt":
if err := makeDirs(path.Dir(gr.Bolt.File)); err != nil {
return nil, err
@@ -93,10 +84,3 @@ func (ac *AvatarCommand) makeAvatarStore(gr AvatarGroup) (avatar.Store, error) {
}
return nil, errors.Errorf("unsupported avatar store type %s", gr.Type)
}
func (ac *AvatarCommand) makeMongo() (result *mongo.Server, err error) {
if ac.Mongo.URL == "" {
return nil, errors.New("no mongo URL provided")
}
return mongo.NewServerWithURL(ac.Mongo.URL, 10*time.Second)
}
+19 -30
View File
@@ -22,7 +22,6 @@ import (
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/provider"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/mongo"
"github.com/go-pkgz/rest/cache"
"github.com/umputun/remark/backend/app/migrator"
@@ -41,7 +40,6 @@ type ServerCommand struct {
Store StoreGroup `group:"store" namespace:"store" env-namespace:"STORE"`
Avatar AvatarGroup `group:"avatar" namespace:"avatar" env-namespace:"AVATAR"`
Cache CacheGroup `group:"cache" namespace:"cache" env-namespace:"CACHE"`
Mongo MongoGroup `group:"mongo" namespace:"mongo" env-namespace:"MONGO"`
Admin AdminGroup `group:"admin" namespace:"admin" env-namespace:"ADMIN"`
Notify NotifyGroup `group:"notify" namespace:"notify" env-namespace:"NOTIFY"`
Image ImageGroup `group:"image" namespace:"image" env-namespace:"IMAGE"`
@@ -89,11 +87,15 @@ type AuthGroup struct {
// StoreGroup defines options group for store params
type StoreGroup struct {
Type string `long:"type" env:"TYPE" description:"type of storage" choice:"bolt" choice:"mongo" default:"bolt"`
Type string `long:"type" env:"TYPE" description:"type of storage" choice:"bolt" choice:"remote" default:"bolt"`
Bolt struct {
Path string `long:"path" env:"PATH" default:"./var" description:"parent dir for bolt files"`
Timeout time.Duration `long:"timeout" env:"TIMEOUT" default:"30s" description:"bolt timeout"`
} `group:"bolt" namespace:"bolt" env-namespace:"BOLT"`
Remote struct {
API string `long:"api" env:"API" description:"remote extension api url"`
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" description:"http timeout"`
} `group:"remote" namespace:"remote" env-namespace:"REMOTE"`
}
// ImageGroup defines options group for store pictures
@@ -134,12 +136,6 @@ type CacheGroup struct {
} `group:"max" namespace:"max" env-namespace:"MAX"`
}
// MongoGroup holds all mongo params, used by store, avatar and cache
type MongoGroup struct {
URL string `long:"url" env:"URL" description:"mongo url"`
DB string `long:"db" env:"DB" default:"remark42" description:"mongo database"`
}
// AdminGroup defines options group for admin params
type AdminGroup struct {
Type string `long:"type" env:"TYPE" description:"type of admin store" choice:"shared" choice:"mongo" default:"shared"`
@@ -436,13 +432,13 @@ func (s *ServerCommand) makeAvatarStore() (avatar.Store, error) {
return nil, err
}
return avatar.NewLocalFS(s.Avatar.FS.Path), nil
case "mongo":
mgServer, err := s.makeMongo()
if err != nil {
return nil, errors.Wrap(err, "failed to create mongo server")
}
conn := mongo.NewConnection(mgServer, s.Mongo.DB, "")
return avatar.NewGridFS(conn), nil
// case "mongo":
// mgServer, err := s.makeMongo()
// if err != nil {
// return nil, errors.Wrap(err, "failed to create mongo server")
// }
// conn := mongo.NewConnection(mgServer, s.Mongo.DB, "")
// return avatar.NewGridFS(conn), nil
case "bolt":
if err := makeDirs(path.Dir(s.Avatar.Bolt.File)); err != nil {
return nil, err
@@ -485,13 +481,13 @@ func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
}
}
return admin.NewStaticStore(s.SharedSecret, s.Admin.Shared.Admins, s.Admin.Shared.Email), nil
case "mongo":
mgServer, e := s.makeMongo()
if e != nil {
return nil, errors.Wrap(e, "failed to create mongo server")
}
conn := mongo.NewConnection(mgServer, s.Mongo.DB, "admin")
return admin.NewMongoStore(conn, s.SharedSecret), nil
// case "mongo":
// mgServer, e := s.makeMongo()
// if e != nil {
// return nil, errors.Wrap(e, "failed to create mongo server")
// }
// conn := mongo.NewConnection(mgServer, s.Mongo.DB, "admin")
// return admin.NewMongoStore(conn, s.SharedSecret), nil
default:
return nil, errors.Errorf("unsupported admin store type %s", s.Admin.Type)
}
@@ -517,13 +513,6 @@ func (s *ServerCommand) makeCache() (cache.LoadingCache, error) {
return nil, errors.Errorf("unsupported cache type %s", s.Cache.Type)
}
func (s *ServerCommand) makeMongo() (result *mongo.Server, err error) {
if s.Mongo.URL == "" {
return nil, errors.New("no mongo URL provided")
}
return mongo.NewServerWithURL(s.Mongo.URL, 10*time.Second)
}
func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) {
providers := 0
+3 -1
View File
@@ -55,7 +55,9 @@ func TestServerApp(t *testing.T) {
body, _ = ioutil.ReadAll(resp.Body)
t.Log(string(body))
assert.Equal(t, "admin@demo.remark42.com", app.dataService.AdminStore.Email(""), "default admin email")
email, err := app.dataService.AdminStore.Email("")
assert.NoError(t, err)
assert.Equal(t, "admin@demo.remark42.com", email, "default admin email")
app.Wait()
}
+5 -2
View File
@@ -377,6 +377,9 @@ func (s *Rest) updateLimiter() float64 {
func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
admins, _ := s.DataService.AdminStore.Admins(siteID)
emails, _ := s.DataService.AdminStore.Email(siteID)
cnf := struct {
Version string `json:"version"`
EditDuration int `json:"edit_duration"`
@@ -393,8 +396,8 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
Version: s.Version,
EditDuration: int(s.DataService.EditDuration.Seconds()),
MaxCommentSize: s.DataService.MaxCommentSize,
Admins: s.DataService.AdminStore.Admins(siteID),
AdminEmail: s.DataService.AdminStore.Email(siteID),
Admins: admins,
AdminEmail: emails,
LowScore: s.ScoreThresholds.Low,
CriticalScore: s.ScoreThresholds.Critical,
PositiveScore: s.DataService.PositiveScore,
+15 -15
View File
@@ -10,25 +10,17 @@ import (
// Store defines interface returning admins info for given site
type Store interface {
Key() (key string, err error)
Admins(siteID string) (ids []string)
Email(siteID string) (email string)
Admins(siteID string) (ids []string, err error)
Email(siteID string) (email string, err error)
}
// StaticStore implements keys.Store with a single, predefined key
// StaticStore implements keys.Store with a single set of admins and email for all sites
type StaticStore struct {
admins []string
email string
key string
}
// Key returns static key for all sites, allows empty site
func (s *StaticStore) Key() (key string, err error) {
if s.key == "" {
return "", errors.New("empty key for static key store")
}
return s.key, nil
}
// NewStaticStore makes StaticStore instance with given key
func NewStaticStore(key string, admins []string, email string) *StaticStore {
log.Printf("[DEBUG] admin users %+v, email %s", admins, email)
@@ -40,12 +32,20 @@ func NewStaticKeyStore(key string) *StaticStore {
return &StaticStore{key: key, admins: []string{}, email: ""}
}
// Key returns static key, same for all sites
func (s *StaticStore) Key() (key string, err error) {
if s.key == "" {
return "", errors.New("empty key for static key store")
}
return s.key, nil
}
// Admins returns static list of admin's ids, the same for all sites
func (s *StaticStore) Admins(string) (ids []string) {
return s.admins
func (s *StaticStore) Admins(string) (ids []string, err error) {
return s.admins, nil
}
// Email gets static email address
func (s *StaticStore) Email(string) (email string) {
return s.email
func (s *StaticStore) Email(string) (email string, err error) {
return s.email, nil
}
+4 -44
View File
@@ -3,10 +3,7 @@ package admin
import (
"testing"
"github.com/globalsign/mgo"
"github.com/go-pkgz/mongo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStaticStore_Get(t *testing.T) {
@@ -16,48 +13,11 @@ func TestStaticStore_Get(t *testing.T) {
assert.NoError(t, err, "valid store")
assert.Equal(t, "key123", k, "valid site")
a := ks.Admins("any")
a, err := ks.Admins("any")
assert.NoError(t, err)
assert.Equal(t, []string{"123", "xyz"}, a)
email := ks.Email("blah")
email, err := ks.Email("blah")
assert.NoError(t, err)
assert.Equal(t, "aa@example.com", email)
}
func TestMongoStore_Get(t *testing.T) {
conn, err := mongo.MakeTestConnection(t)
require.NoError(t, err)
var ms Store = NewMongoStore(conn, "secret")
recs := []mongoRec{
{"site1", []string{"i11", "i12"}, "e1"},
{"site2", []string{"i21", "i22"}, "e2"},
}
err = conn.WithCollection(func(coll *mgo.Collection) error {
if e1 := coll.Insert(recs[0]); e1 != nil {
return e1
}
return coll.Insert(recs[1])
})
require.NoError(t, err)
admins := ms.Admins("site1")
assert.Equal(t, []string{"i11", "i12"}, admins)
email := ms.Email("site1")
assert.Equal(t, "e1", email)
key, err := ms.Key()
assert.NoError(t, err)
assert.Equal(t, "secret", key)
admins = ms.Admins("site2")
assert.Equal(t, []string{"i21", "i22"}, admins)
email = ms.Email("site2")
assert.Equal(t, "e2", email)
key, err = ms.Key()
assert.NoError(t, err)
assert.Equal(t, "secret", key)
admins = ms.Admins("no-site-in-db")
assert.Equal(t, []string{}, admins)
email = ms.Email("no-site-in-db")
assert.Equal(t, "", email)
}
-56
View File
@@ -1,56 +0,0 @@
package admin
import (
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
log "github.com/go-pkgz/lgr"
"github.com/go-pkgz/mongo"
)
// MongoStore implements admin.Store with mongo backend
type MongoStore struct {
connection *mongo.Connection
key string
}
type mongoRec struct {
SiteID string `bson:"site"`
IDs []string `bson:"admin_ids"`
Email string `bson:"admin_email"`
}
// NewMongoStore makes admin Store for mongo's connection
func NewMongoStore(conn *mongo.Connection, key string) *MongoStore {
log.Printf("[DEBUG] make mongo admin store with %+v", conn)
return &MongoStore{connection: conn, key: key}
}
// Key executes find by siteID and returns substructure with secret key
func (m *MongoStore) Key() (key string, err error) {
return m.key, nil
}
// Admins executes find by siteID and returns admins ids
func (m *MongoStore) Admins(siteID string) (ids []string) {
resp := mongoRec{}
err := m.connection.WithCollection(func(coll *mgo.Collection) error {
return coll.Find(bson.M{"site": siteID}).One(&resp)
})
if err != nil {
return []string{}
}
return resp.IDs
}
// Email executes find by siteID and returns admin's email
func (m *MongoStore) Email(siteID string) (email string) {
resp := mongoRec{}
err := m.connection.WithCollection(func(coll *mgo.Collection) error {
return coll.Find(bson.M{"site": siteID}).One(&resp)
})
if err != nil {
return ""
}
return resp.Email
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package admin
import (
"encoding/json"
"github.com/umputun/remark/backend/app/store/remote"
)
// Remote implements remote engine and delegates all Calls to remote http server
type Remote struct {
remote.Client
}
// Key returns the key, same for all sites
func (r *Remote) Key() (key string, err error) {
resp, err := r.Call("admin.key")
if err != nil {
return "", err
}
err = json.Unmarshal(*resp.Result, &key)
return key, err
}
// Admins returns list of admin's ids for given site
func (r *Remote) Admins(siteID string) (ids []string, err error) {
resp, err := r.Call("admin.admins", siteID)
if err != nil {
return []string{}, err
}
if err = json.Unmarshal(*resp.Result, &ids); err != nil {
return []string{}, err
}
return ids, nil
}
// Email gets email address for given site
func (r *Remote) Email(siteID string) (email string, err error) {
resp, err := r.Call("admin.email", siteID)
if err != nil {
return "", err
}
if err = json.Unmarshal(*resp.Result, &email); err != nil {
return "", err
}
return email, nil
}
+74
View File
@@ -0,0 +1,74 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package admin
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/store/remote"
)
func TestRemote_Key(t *testing.T) {
ts := testServer(t, `{"method":"admin.key","params":null,"id":1}`,
`{"result":"12345","id":1}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
res, err := c.Key()
assert.NoError(t, err)
assert.Equal(t, "12345", res)
t.Logf("%v %T", res, res)
}
func TestRemote_Admins(t *testing.T) {
ts := testServer(t, `{"method":"admin.admins","params":["site-1"],"id":1}`,
`{"result":["id1","id2"],"id":1}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
res, err := c.Admins("site-1")
assert.NoError(t, err)
assert.Equal(t, []string{"id1", "id2"}, res)
t.Logf("%v %T", res, res)
}
func TestRemote_Email(t *testing.T) {
ts := testServer(t, `{"method":"admin.email","params":["site-1"],"id":1}`,
`{"result":"bbb@example.com","id":1}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
res, err := c.Email("site-1")
assert.NoError(t, err)
assert.Equal(t, "bbb@example.com", res)
t.Logf("%v %T", res, res)
}
func testServer(t *testing.T, req, resp string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, req, string(body))
t.Logf("req: %s", string(body))
fmt.Fprintf(w, resp)
}))
}
+1 -1
View File
@@ -4,11 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
"log"
"strings"
"time"
bolt "github.com/coreos/bbolt"
log "github.com/go-pkgz/lgr"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
+14 -14
View File
@@ -16,7 +16,7 @@ import (
"github.com/umputun/remark/backend/app/store/remote"
)
func TestClient_Create(t *testing.T) {
func TestRemote_Create(t *testing.T) {
ts := testServer(t, `{"method":"store.create","params":{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"},"id":1}`,
`{"result":"12345","id":1}`)
defer ts.Close()
@@ -32,7 +32,7 @@ func TestClient_Create(t *testing.T) {
t.Logf("%v %T", res, res)
}
func TestClient_Get(t *testing.T) {
func TestRemote_Get(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, `{"result":{"id":"123","pid":"","text":"msg","delete":true}}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -44,7 +44,7 @@ func TestClient_Get(t *testing.T) {
t.Logf("%v %T", res, res)
}
func TestClient_GetWithErrorResult(t *testing.T) {
func TestRemote_GetWithErrorResult(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, `{"error":"failed"}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -54,7 +54,7 @@ func TestClient_GetWithErrorResult(t *testing.T) {
assert.EqualError(t, err, "failed")
}
func TestClient_GetWithErrorDecode(t *testing.T) {
func TestRemote_GetWithErrorDecode(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, ``)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -64,7 +64,7 @@ func TestClient_GetWithErrorDecode(t *testing.T) {
assert.EqualError(t, err, "failed to decode response for store.get: EOF")
}
func TestClient_GetWithErrorRemote(t *testing.T) {
func TestRemote_GetWithErrorRemote(t *testing.T) {
c := Remote{Client: remote.Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
@@ -73,7 +73,7 @@ func TestClient_GetWithErrorRemote(t *testing.T) {
assert.True(t, strings.Contains(err.Error(), "remote call failed for store.get:"), err.Error())
}
func TestClient_FailedStatus(t *testing.T) {
func TestRemote_FailedStatus(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
@@ -88,7 +88,7 @@ func TestClient_FailedStatus(t *testing.T) {
assert.EqualError(t, err, "bad status 400 for store.get")
}
func TestClient_Update(t *testing.T) {
func TestRemote_Update(t *testing.T) {
ts := testServer(t, `{"method":"store.update","params":{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site123","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"},"id":1}`, `{}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -99,7 +99,7 @@ func TestClient_Update(t *testing.T) {
}
func TestClient_Find(t *testing.T) {
func TestRemote_Find(t *testing.T) {
ts := testServer(t, `{"method":"store.find","params":{"locator":{"url":"http://example.com/url"},"sort":"-time","since":"0001-01-01T00:00:00Z","limit":10},"id":1}`, `{"result":[{"text":"1"},{"text":"2"}]}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -109,7 +109,7 @@ func TestClient_Find(t *testing.T) {
assert.Equal(t, []store.Comment{{Text: "1"}, {Text: "2"}}, res)
}
func TestClient_Info(t *testing.T) {
func TestRemote_Info(t *testing.T) {
ts := testServer(t, `{"method":"store.info","params":{"locator":{"url":"http://example.com/url"},"limit":10,"skip":5,"ro_age":10},"id":1}`, `{"result":[{"url":"u1","count":22},{"url":"u2","count":33}]}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -120,7 +120,7 @@ func TestClient_Info(t *testing.T) {
assert.Equal(t, []store.PostInfo{{URL: "u1", Count: 22}, {URL: "u2", Count: 33}}, res)
}
func TestClient_Flag(t *testing.T) {
func TestRemote_Flag(t *testing.T) {
ts := testServer(t, `{"method":"store.flag","params":{"flag":"verified","locator":{"url":"http://example.com/url"}},"id":1}`, `{"result":false}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -130,7 +130,7 @@ func TestClient_Flag(t *testing.T) {
assert.Equal(t, false, res)
}
func TestClient_ListFlag(t *testing.T) {
func TestRemote_ListFlag(t *testing.T) {
ts := testServer(t, `{"method":"store.list_flags","params":{"flag":"blocked","locator":{"site":"site_id","url":""}},"id":1}`, `{"result":[{"ID":"id1"},{"ID":"id2"}]}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -139,7 +139,7 @@ func TestClient_ListFlag(t *testing.T) {
assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res)
}
func TestClient_Count(t *testing.T) {
func TestRemote_Count(t *testing.T) {
ts := testServer(t, `{"method":"store.count","params":{"locator":{"url":"http://example.com/url"},"since":"0001-01-01T00:00:00Z"},"id":1}`, `{"result":11}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
@@ -149,7 +149,7 @@ func TestClient_Count(t *testing.T) {
assert.Equal(t, 11, res)
}
func TestClient_Delete(t *testing.T) {
func TestRemote_Delete(t *testing.T) {
ts := testServer(t, `{"method":"store.delete","params":{"locator":{"url":"http://example.com/url"},"del_mode":0},"id":1}`,
`{}`)
defer ts.Close()
@@ -159,7 +159,7 @@ func TestClient_Delete(t *testing.T) {
assert.NoError(t, err)
}
func TestClient_Close(t *testing.T) {
func TestRemote_Close(t *testing.T) {
ts := testServer(t, `{"method":"store.close","params":null,"id":1}`, `{}`)
defer ts.Close()
c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}}
+5 -1
View File
@@ -462,7 +462,11 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
// IsAdmin checks if usesID in the list of admins
func (s *DataStore) IsAdmin(siteID string, userID string) bool {
for _, a := range s.AdminStore.Admins(siteID) {
admins, err := s.AdminStore.Admins(siteID)
if err != nil {
return false
}
for _, a := range admins {
if a == userID {
return true
}