Browse Source

A lot of delete and failed scream tests.

thegreatrefactor
Gisle Aune 5 years ago
parent
commit
e690e7ab97
  1. 9
      cmd/rpdata-server/main.go
  2. 2
      database/database.go
  3. 24
      database/mongodb/db.go
  4. 69
      database/mongodb/keys.go
  5. 41
      database/mongodb/users.go
  6. 3
      graph2/complexity.go
  7. 2
      graph2/gqlgen.yml
  8. 2
      graph2/graph.go
  9. 39
      graph2/resolvers/file.go
  10. 5
      graph2/resolvers/token.go
  11. 2
      graph2/schema/types/File.gql
  12. 17
      graph2/types/token.go
  13. 109
      internal/auth/key.go
  14. 33
      internal/auth/permissions.go
  15. 60
      internal/auth/permitted.go
  16. 167
      internal/auth/token.go
  17. 55
      internal/counter/counter.go
  18. 4
      internal/generate/id.go
  19. 78
      internal/loader/channel.go
  20. 146
      internal/loader/character.go
  21. 80
      internal/loader/loader.go
  22. 73
      internal/store/db.go
  23. 39
      internal/store/init.go
  24. 97
      internal/store/space.go
  25. 33
      models/channels/add.go
  26. 19
      models/channels/db.go
  27. 39
      models/channels/edit.go
  28. 33
      models/channels/ensure.go
  29. 11
      models/channels/find.go
  30. 46
      models/channels/list.go
  31. 22
      models/key.go
  32. 13
      repositories/key.go
  33. 11
      repositories/user.go
  34. 331
      services/auth.go
  35. 11
      services/changes.go
  36. 8
      services/channels.go
  37. 16
      services/characters.go
  38. 10
      services/files.go
  39. 27
      services/logs.go
  40. 15
      services/services.go
  41. 50
      services/stories.go

9
cmd/rpdata-server/main.go

@ -10,10 +10,8 @@ import (
"git.aiterp.net/rpdata/api/database"
"git.aiterp.net/rpdata/api/graph2"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/internal/instrumentation"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/services"
"github.com/99designs/gqlgen/handler"
@ -21,11 +19,6 @@ import (
)
func main() {
err := store.Init()
if err != nil {
log.Fatalln("Failed to init store:", err)
}
db, err := database.Init(config.Global().Database)
if err != nil {
log.Fatalln("Failed to init db:", err)
@ -108,7 +101,7 @@ func queryHandler(services *services.Bundle) http.HandlerFunc {
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
}
r = auth.RequestWithToken(r)
r = services.Auth.RequestWithToken(r)
handler.ServeHTTP(w, r)
}

2
database/database.go

@ -22,6 +22,8 @@ type Database interface {
Stories() repositories.StoryRepository
Chapters() repositories.ChapterRepository
Comments() repositories.CommentRepository
Keys() repositories.KeyRepository
Users() repositories.UserRepository
Close(ctx context.Context) error
}

24
database/mongodb/db.go

@ -24,6 +24,8 @@ type MongoDB struct {
stories repositories.StoryRepository
chapters repositories.ChapterRepository
comments repositories.CommentRepository
keys repositories.KeyRepository
users repositories.UserRepository
}
func (m *MongoDB) Changes() repositories.ChangeRepository {
@ -66,6 +68,14 @@ func (m *MongoDB) Comments() repositories.CommentRepository {
return m.comments
}
func (m *MongoDB) Keys() repositories.KeyRepository {
return m.keys
}
func (m *MongoDB) Users() repositories.UserRepository {
return m.users
}
func (m *MongoDB) Close(ctx context.Context) error {
m.session.Close()
return nil
@ -147,6 +157,18 @@ func Init(cfg config.Database) (*MongoDB, error) {
return nil, err
}
keys, err := newKeyRepository(db)
if err != nil {
session.Close()
return nil, err
}
users, err := newUserRepository(db)
if err != nil {
session.Close()
return nil, err
}
go posts.fixPositions(logs)
return &MongoDB{
@ -162,6 +184,8 @@ func Init(cfg config.Database) (*MongoDB, error) {
logs: logs,
posts: posts,
files: files,
keys: keys,
users: users,
}, nil
}

69
database/mongodb/keys.go

@ -0,0 +1,69 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
type keyRepository struct {
keys *mgo.Collection
}
func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
collection := db.C("auth.keys")
err := collection.EnsureIndexKey("user")
if err != nil {
return nil, err
}
return &keyRepository{keys: collection}, nil
}
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
key := models.Key{}
err := r.keys.FindId(id).One(&key)
if err != nil {
return nil, err
}
return &key, err
}
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
query := bson.M{}
if filter.UserID != nil {
query["user"] = *filter.UserID
}
keys := make([]*models.Key, 0, 4)
err := r.keys.Find(query).All(&keys)
if err != nil {
if err == mgo.ErrNotFound {
return nil, nil
}
return nil, err
}
return keys, nil
}
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
key.ID = generate.KeyID()
err := r.keys.Insert(&key)
if err != nil {
return nil, err
}
return &key, nil
}
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
return r.keys.RemoveId(key.ID)
}

41
database/mongodb/users.go

@ -0,0 +1,41 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
)
type userRepository struct {
users *mgo.Collection
}
func newUserRepository(db *mgo.Database) (repositories.UserRepository, error) {
collection := db.C("core.users")
return &userRepository{users: collection}, nil
}
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
user := new(models.User)
err := r.users.FindId(id).One(user)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, err
}
return user, nil
}
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
err := r.users.Insert(user)
if err != nil {
return nil, err
}
return &user, nil
}

3
graph2/complexity.go

@ -3,7 +3,6 @@ package graph2
import (
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/files"
)
func complexity() (cr graphcore.ComplexityRoot) {
@ -67,7 +66,7 @@ func complexity() (cr graphcore.ComplexityRoot) {
cr.Query.File = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Files = func(childComplexity int, filter *files.Filter) int {
cr.Query.Files = func(childComplexity int, filter *models.FileFilter) int {
return childComplexity + listComplexity
}
cr.Query.Changes = func(childComplexity int, filter *models.ChangeFilter) int {

2
graph2/gqlgen.yml

@ -52,7 +52,7 @@ models:
File:
model: git.aiterp.net/rpdata/api/models.File
FilesFilter:
model: git.aiterp.net/rpdata/api/models/files.Filter
model: git.aiterp.net/rpdata/api/models.FileFilter
Change:
model: git.aiterp.net/rpdata/api/models.Change
ChangeModel:

2
graph2/graph.go

@ -55,5 +55,5 @@ func (r *rootResolver) Change() graphcore.ChangeResolver {
}
func (r *rootResolver) Token() graphcore.TokenResolver {
return &types.TokenResolver
return types.TokenResolver(r.s)
}

39
graph2/resolvers/file.go

@ -2,48 +2,21 @@ package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/files"
)
func (r *queryResolver) File(ctx context.Context, id string) (*models.File, error) {
file, err := files.FindID(id)
if err != nil {
return nil, err
}
return &file, nil
return r.s.Files.Find(ctx, id)
}
func (r *queryResolver) Files(ctx context.Context, filter *files.Filter) ([]*models.File, error) {
token := auth.TokenFromContext(ctx)
func (r *queryResolver) Files(ctx context.Context, filter *models.FileFilter) ([]*models.File, error) {
if filter == nil {
filter = &files.Filter{}
}
public := true
// Only allow users to view public files that are not their own.
if token != nil {
if filter.Public == nil || *filter.Public == false {
filter.Author = &token.UserID
filter = &models.FileFilter{
Public: &public,
}
} else {
filter.Public = &trueValue
}
files, err := files.List(filter)
if err != nil {
return nil, err
}
files2 := make([]*models.File, len(files))
for i := range files {
files2[i] = &files[i]
}
return files2, nil
return r.s.Files.List(ctx, *filter)
}
var trueValue = true

5
graph2/resolvers/token.go

@ -4,14 +4,13 @@ import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
)
func (r *queryResolver) Token(ctx context.Context) (*models.Token, error) {
token := auth.TokenFromContext(ctx)
token := r.s.Auth.TokenFromContext(ctx)
if !token.Authenticated() {
return nil, errors.New("No (valid) token")
return nil, errors.New("no (valid) token")
}
return token, nil

2
graph2/schema/types/File.gql

@ -35,7 +35,7 @@ input FilesFilter {
public: Boolean
# Limit the MIME types of the files.
mimeType: [String!]
mimeTypes: [String!]
}
# Input for editFile mutation

17
graph2/types/token.go

@ -2,21 +2,20 @@ package types
import (
"context"
"git.aiterp.net/rpdata/api/services"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/users"
)
type tokenResolver struct{}
type tokenResolver struct {
auth *services.AuthService
}
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (*models.User, error) {
user, err := users.Find(token.UserID)
if err != nil {
return nil, err
}
return &user, nil
return r.auth.FindUser(ctx, token.UserID)
}
// TokenResolver is a resolver
var TokenResolver tokenResolver
func TokenResolver(s *services.Bundle) *tokenResolver {
return &tokenResolver{auth: s.Auth}
}

109
internal/auth/key.go

@ -1,109 +0,0 @@
package auth
import (
"crypto/rand"
"encoding/binary"
"errors"
"strconv"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
)
var keyCollection *mgo.Collection
// A Key contains a JWT secret and the limitations of it. There are two types of
// keys, single-user keys and wildcard keys. The former is used to authenticate
// a single user (e.g. the logbot) through an API while the latter is only for
// services that can be trusted to perform its own authentication (a frontend).
type Key struct {
ID string `bson:"_id"`
Name string `bson:"name"`
User string `bson:"user"`
Secret string `bson:"secret"`
}
// ValidForUser returns true if the key's user is the same as
// the user, or it's a wildcard key.
func (key *Key) ValidForUser(user string) bool {
return key.User == user || key.User == "*"
}
// FindKey finds a key by kid (key ID)
func FindKey(kid string) (Key, error) {
key := Key{}
err := keyCollection.FindId(kid).One(&key)
return key, err
}
// NewKey generates a new key for the user and name. This
// does not allow generating wildcard keys, they have to be
// manually inserted into the DB.
func NewKey(name, user string) (*Key, error) {
if user == "*" {
return nil, errors.New("auth: wildcard keys not allowed")
}
secret, err := makeKeySecret()
if err != nil {
return nil, err
}
key := &Key{
ID: makeKeyID(),
Name: name,
User: user,
Secret: secret,
}
if err := keyCollection.Insert(key); err != nil {
return nil, err
}
return key, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
keyCollection = db.C("auth.keys")
keyCollection.EnsureIndexKey("user")
})
}
// makeKeyID makes a random story ID that's 16 characters long
func makeKeyID() string {
result := "K"
offset := 0
data := make([]byte, 32)
rand.Read(data)
for len(result) < 16 {
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
offset += 8
if offset >= 32 {
rand.Read(data)
offset = 0
}
}
return result[:16]
}
func makeKeySecret() (string, error) {
data := make([]byte, 64)
alphabet := "0123456789abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSUTVWXYZ-_"
_, err := rand.Read(data)
if err != nil {
return "", err
}
for i := range data {
data[i] = alphabet[data[i]%64]
}
return string(data), nil
}

33
internal/auth/permissions.go

@ -1,33 +0,0 @@
package auth
// AllPermissions gets all permissions and their purpose
func AllPermissions() map[string]string {
return map[string]string{
"member": "Can add/edit/remove own content",
"user.edit": "Can edit non-owned users",
"character.add": "Can add non-owned characters",
"character.edit": "Can edit non-owned characters",
"character.remove": "Can remove non-owned characters",
"channel.add": "Can add channels",
"channel.edit": "Can edit channels",
"channel.remove": "Can remove channels",
"comment.edit": "Can edit non-owned comments",
"comment.remove": "Can remove non-owned comments",
"log.add": "Can add logs",
"log.edit": "Can edit logs",
"log.remove": "Can remove logs",
"post.add": "Can add posts",
"post.edit": "Can edit posts",
"post.move": "Can move posts",
"post.remove": "Can remove posts",
"story.add": "Can add non-owned stories",
"story.edit": "Can edit non-owned stories",
"story.remove": "Can remove non-owned stories",
"story.unlisted": "Can view non-owned unlisted stories",
"file.upload": "Can upload files",
"file.list": "Can list non-owned files",
"file.view": "Can view non-owned files",
"file.edit": "Can edit non-owned files",
"file.remove": "Can remove non-owned files",
}
}

60
internal/auth/permitted.go

@ -1,60 +0,0 @@
package auth
import (
"context"
"log"
"reflect"
"git.aiterp.net/rpdata/api/models"
)
// CheckPermission does some magic.
func CheckPermission(ctx context.Context, op string, obj interface{}) error {
token := TokenFromContext(ctx)
if token == nil {
return ErrUnauthenticated
}
if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {
ptr := reflect.PtrTo(v.Type())
ptrValue := reflect.New(ptr.Elem())
ptrValue.Elem().Set(v)
obj = ptrValue.Interface()
}
var authorized = false
switch v := obj.(type) {
case *models.Channel:
authorized = token.Permitted("channel." + op)
case *models.Character:
authorized = token.PermittedUser(v.Author, "member", "character."+op)
case *models.Chapter:
authorized = token.PermittedUser(v.Author, "member", "chapter."+op)
case *models.Comment:
if op == "add" && v.Author != token.UserID {
return ErrInvalidOperation
}
authorized = token.PermittedUser(v.Author, "member", "comment."+op)
case *models.File:
authorized = token.PermittedUser(v.Author, "member", "file."+op)
case *models.Log:
authorized = token.Permitted("log." + op)
case *models.Post:
authorized = token.Permitted("post." + op)
case *models.Story:
authorized = token.PermittedUser(v.Author, "member", "story."+op)
case *models.User:
authorized = token.Permitted("user." + op)
default:
log.Panicf("Invalid model %T: %#+v", v, v)
}
if !authorized {
return ErrUnauthorized
}
return nil
}

167
internal/auth/token.go

@ -1,167 +0,0 @@
package auth
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/users"
jwt "github.com/dgrijalva/jwt-go"
)
var contextKey = &struct{ data string }{"Token Context Key"}
// ErrNoKid is returned if the key id is missing from the jwt token header,
var ErrNoKid = errors.New("Missing \"kid\" field in token")
// ErrKeyNotFound is returned if the key wasn't found.
var ErrKeyNotFound = errors.New("Key not found")
// ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
var ErrInvalidClaims = errors.New("Invalid claims in token")
// ErrExpired is returned by parseClaims if the expiry date is in the past
var ErrExpired = errors.New("Claims have already expired")
// ErrWrongUser is returned by CheckToken if the key cannot represent this user
var ErrWrongUser = errors.New("Key is not valid for this user")
// ErrWrongPermissions is returned by CheckToken if the key cannot claim one or more of its permissions
var ErrWrongPermissions = errors.New("User does not have these permissions")
// ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist.
var ErrDeletedUser = errors.New("User was not found")
// ErrUnauthenticated is returned when the user is not authenticated
var ErrUnauthenticated = errors.New("You are not authenticated")
// ErrUnauthorized is returned when the user doesn't have access to this resource
var ErrUnauthorized = errors.New("You are not authorized to perform this action")
// ErrInvalidOperation is returned for operations that should never be allowed
var ErrInvalidOperation = errors.New("No permission exists for this operation")
// TokenFromContext gets the token from context.
func TokenFromContext(ctx context.Context) *models.Token {
token, ok := ctx.Value(contextKey).(*models.Token)
if !ok {
return nil
}
return token
}
// RequestWithToken either returns the request, or the request with a new context that
// has the token.
func RequestWithToken(r *http.Request) *http.Request {
header := r.Header.Get("Authorization")
if header == "" {
return r
}
if !strings.HasPrefix(header, "Bearer ") {
return r
}
token, err := CheckToken(header[7:])
if err != nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), contextKey, &token))
}
// CheckToken reads the token string and returns a token if everything is kosher.
func CheckToken(tokenString string) (token models.Token, err error) {
var key Key
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
}
kid, ok := jwtToken.Header["kid"].(string)
if !ok {
return nil, ErrNoKid
}
key, err = FindKey(kid)
if err != nil || key.ID == "" {
return nil, ErrKeyNotFound
}
return []byte(key.Secret), nil
})
if err != nil {
return models.Token{}, err
}
userid, permissions, err := parseClaims(jwtToken.Claims)
if err != nil {
return models.Token{}, err
}
if !key.ValidForUser(userid) {
return models.Token{}, ErrWrongUser
}
user, err := users.Ensure(userid)
if err != nil {
return models.Token{}, ErrDeletedUser
}
acceptedPermissions := make([]string, 0, len(user.Permissions))
if len(permissions) > 0 {
for _, permission := range permissions {
found := false
for _, userPermission := range user.Permissions {
if permission == userPermission {
found = true
break
}
}
if found {
acceptedPermissions = append(acceptedPermissions, permission)
}
}
} else {
acceptedPermissions = append(acceptedPermissions, user.Permissions...)
}
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
}
func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) {
mapClaims, ok := jwtClaims.(jwt.MapClaims)
if !ok {
return "", nil, ErrInvalidClaims
}
if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) {
return "", nil, ErrExpired
}
if userid, ok = mapClaims["user"].(string); !ok {
return "", nil, ErrInvalidClaims
}
if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok {
for _, permission := range claimedPermissions {
if permission, ok := permission.(string); ok {
permissions = append(permissions, permission)
}
}
if len(permissions) == 0 {
return "", nil, ErrInvalidClaims
}
}
return
}

55
internal/counter/counter.go

@ -1,55 +0,0 @@
package counter
import (
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var collection *mgo.Collection
type counter struct {
ID string `bson:"_id"`
Value int `bson:"value"`
}
// Next gets the next value of a counter, or an error if it hasn't.
func Next(category, name string) (int, error) {
id := category + "." + name
doc := counter{}
_, err := collection.Find(bson.M{"_id": id}).Apply(mgo.Change{
Update: bson.M{"$inc": bson.M{"value": 1}},
Upsert: true,
ReturnNew: true,
}, &doc)
if err != nil {
return -1, err
}
return doc.Value, nil
}
// NextMany gets the next value of a counter, or an error if it hasn't, and increments by a specified value.
// Any value `returned` to `returned+(increment-1)` should then be safe to use.
func NextMany(category, name string, increment int) (int, error) {
id := category + "." + name
doc := counter{}
_, err := collection.Find(bson.M{"_id": id}).Apply(mgo.Change{
Update: bson.M{"$inc": bson.M{"value": increment}},
Upsert: true,
ReturnNew: true,
}, &doc)
if err != nil {
return -1, err
}
return doc.Value, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("core.counters")
})
}

4
internal/generate/id.go

@ -62,3 +62,7 @@ func CommentID() string {
func FileID() string {
return ID("F", 16)
}
func KeyID() string {
return ID("K", 32)
}

78
internal/loader/channel.go

@ -1,78 +0,0 @@
package loader
import (
"context"
"errors"
"strings"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/channels"
"github.com/graph-gophers/dataloader"
)
// Channel gets a character by key
func (loader *Loader) Channel(key, value string) (*models.Channel, error) {
if !strings.HasPrefix(key, "Channel.") {
key = "Channel." + key
}
if loader.loaders[key] == nil {
return nil, errors.New("unsupported key")
}
loader.loadPrimed(key)
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
res, err := thunk()
if err != nil {
return nil, err
}
channel, ok := res.(models.Channel)
if !ok {
return nil, errors.New("incorrect type")
}
return &channel, nil
}
// PrimeChannels primes channels for loading along with the first one.
func (loader *Loader) PrimeChannels(key string, values ...string) {
if !strings.HasPrefix(key, "Channel.") {
key = "Channel." + key
}
loader.prime(key, values)
}
func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
var results []*dataloader.Result
names := keys.Keys()
channels, err := channels.ListNames(names...)
if err != nil {
for range names {
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
}
return results
}
for _, name := range names {
found := false
for i := range channels {
if channels[i].Name == name {
results = append(results, &dataloader.Result{Data: channels[i]})
found = true
break
}
}
if !found {
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
}
}
return results
}

146
internal/loader/character.go

@ -1,146 +0,0 @@
package loader
import (
"context"
"errors"
"strings"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/characters"
"github.com/graph-gophers/dataloader"
)
// Character gets a character by key
func (loader *Loader) Character(key, value string) (models.Character, error) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
loader.loadPrimed(key)
if loader.loaders[key] == nil {
return models.Character{}, errors.New("unsupported key")
}
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
res, err := thunk()
if err != nil {
return models.Character{}, err
}
char, ok := res.(models.Character)
if !ok {
return models.Character{}, errors.New("incorrect type")
}
return char, nil
}
// Characters gets characters by key
func (loader *Loader) Characters(key string, values ...string) ([]models.Character, error) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
if loader.loaders[key] == nil {
return nil, errors.New("unsupported key")
}
loader.loadPrimed(key)
thunk := loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(values))
res, errs := thunk()
for _, err := range errs {
if err != nil && err != ErrNotFound {
return nil, err
}
}
chars := make([]models.Character, len(res))
for i := range res {
char, ok := res[i].(models.Character)
if !ok {
return nil, errors.New("incorrect type")
}
chars[i] = char
}
return chars, nil
}
// PrimeCharacters adds a set of characters to be loaded if, and only if, characters
// are going to be loaded. This will fill up the cache and speed up subsequent dataloader
// runs.
func (loader *Loader) PrimeCharacters(key string, values ...string) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
loader.prime(key, values)
}
func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
results := make([]*dataloader.Result, 0, len(keys))
ids := keys.Keys()
characters, err := characters.List(&characters.Filter{IDs: ids})
if err != nil {
for range ids {
results = append(results, &dataloader.Result{Error: err})
}
return results
}
for _, id := range ids {
found := false
for _, character := range characters {
if character.ID == id {
results = append(results, &dataloader.Result{Data: character})
found = true
break
}
}
if !found {
results = append(results, &dataloader.Result{Data: models.Character{}, Error: ErrNotFound})
}
}
return results
}
func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
var results []*dataloader.Result
nicks := keys.Keys()
characters, err := characters.List(&characters.Filter{Nicks: nicks})
if err != nil {
for range nicks {
results = append(results, &dataloader.Result{Error: err})
}
return results
}
for _, nick := range nicks {
found := false
for i := range characters {
if characters[i].HasNick(nick) {
results = append(results, &dataloader.Result{Data: characters[i]})
found = true
break
}
}
if !found {
results = append(results, &dataloader.Result{Data: models.Character{}, Error: err})
}
}
return results
}

80
internal/loader/loader.go

@ -1,80 +0,0 @@
package loader
import (
"context"
"errors"
"sync"
"time"
"github.com/graph-gophers/dataloader"
)
var contextKey struct{}
// ErrNotFound is returned in batches when one or more things weren't found. Usually harmless.
var ErrNotFound = errors.New("not found")
// A Loader is a collection of data loaders and functions to act on them. It's supposed to be
// request-scoped, and will thus keep things cached indefinitely.
type Loader struct {
mutex sync.Mutex
ctx context.Context
loaders map[string]*dataloader.Loader
primedKeys map[string]map[string]bool
}
// New initializes the loader.
func New() *Loader {
return &Loader{
ctx: context.Background(),
loaders: map[string]*dataloader.Loader{
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond*2)),
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond*2)),
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond*2)),
},
primedKeys: make(map[string]map[string]bool),
}
}
// FromContext gets the Loader from context.
func FromContext(ctx context.Context) *Loader {
value := ctx.Value(&contextKey)
if value == nil {
return nil
}
return value.(*Loader)
}
// ToContext gets a context with the loader as a value
func (loader *Loader) ToContext(ctx context.Context) context.Context {
loader.ctx = ctx
return context.WithValue(ctx, &contextKey, loader)
}
func (loader *Loader) prime(key string, values []string) {
loader.mutex.Lock()
if loader.primedKeys[key] == nil {
loader.primedKeys[key] = make(map[string]bool)
}
for _, value := range values {
loader.primedKeys[key][value] = true
}
loader.mutex.Unlock()
}
func (loader *Loader) loadPrimed(key string) {
loader.mutex.Lock()
if len(loader.primedKeys[key]) > 0 {
primedKeys := make([]string, 0, len(loader.primedKeys[key]))
for key := range loader.primedKeys[key] {
primedKeys = append(primedKeys, key)
}
loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(primedKeys))
loader.primedKeys[key] = nil
}
loader.mutex.Unlock()
}

73
internal/store/db.go

@ -1,73 +0,0 @@
package store
import (
"fmt"
"time"
"github.com/globalsign/mgo"
)
var db *mgo.Database
var dbInits []func(db *mgo.Database)
// ConnectDB connects to a mongodb database.
func ConnectDB(host string, port int, database, username, password, mechanism string) error {
session, err := mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{fmt.Sprintf("%s:%d", host, port)},
Timeout: 30 * time.Second,
Database: database,
Username: username,
Password: password,
Mechanism: mechanism,
Source: database,
})
if err != nil {
return err
}
db = session.DB(database)
return setupDB()
}
// HandleInit handles the initialization of the database
func HandleInit(function func(db *mgo.Database)) {
dbInits = append(dbInits, function)
}
func setupDB() error {
db.C("common.characters").EnsureIndexKey("name")
db.C("common.characters").EnsureIndexKey("shortName")
db.C("common.characters").EnsureIndexKey("author")
err := db.C("common.characters").EnsureIndex(mgo.Index{
Key: []string{"nicks"},
Unique: true,
DropDups: true,
})
if err != nil {
return err
}
db.C("logbot3.logs").EnsureIndexKey("date")
db.C("logbot3.logs").EnsureIndexKey("channel")
db.C("logbot3.logs").EnsureIndexKey("channel", "open")
db.C("logbot3.logs").EnsureIndexKey("open")
db.C("logbot3.logs").EnsureIndexKey("oldId")
db.C("logbot3.logs").EnsureIndexKey("characterIds")
db.C("logbot3.logs").EnsureIndexKey("event")
db.C("logbot3.logs").EnsureIndexKey("$text:channel", "$text:title", "$text:event", "$text:description", "$text:posts.nick", "$text:posts.text")
err = db.C("server.changes").EnsureIndex(mgo.Index{
Key: []string{"date"},
ExpireAfter: time.Hour * (24 * 14),
})
if err != nil {
return err
}
for _, dbInit := range dbInits {
dbInit(db)
}
return nil
}

39
internal/store/init.go

@ -1,39 +0,0 @@
package store
import (
"sync"
"git.aiterp.net/rpdata/api/internal/config"
)
var initMuted sync.Mutex
var hasInitialized bool
// Init initalizes the store
func Init() error {
initMuted.Lock()
defer initMuted.Unlock()
if hasInitialized {
return nil
}
conf := config.Global()
dbconf := conf.Database
err := ConnectDB(dbconf.Host, dbconf.Port, dbconf.Db, dbconf.Username, dbconf.Password, dbconf.Mechanism)
if err != nil {
return err
}
sconf := conf.Space
if sconf.Enabled {
err = ConnectSpace(sconf.Host, sconf.AccessKey, sconf.SecretKey, sconf.Bucket, sconf.MaxSize, sconf.Root)
if err != nil {
return err
}
}
hasInitialized = true
return nil
}

97
internal/store/space.go

@ -1,97 +0,0 @@
package store
import (
"context"
"errors"
"fmt"
"io"
minio "github.com/minio/minio-go"
)
var spaceBucket string
var spaceURLRoot string
var spaceRoot string
var spaceClient *minio.Client
var spaceMaxSize int64
// ConnectSpace connects to a S3 space.
func ConnectSpace(host, accessKey, secretKey, bucket string, maxSize int64, rootDirectory string) error {
client, err := minio.New(host, accessKey, secretKey, true)
if err != nil {
return err
}
exists, err := client.BucketExists(bucket)
if err != nil {
return err
}
if !exists {
return errors.New("Bucket not found")
}
spaceClient = client
spaceBucket = bucket
spaceURLRoot = fmt.Sprintf("https://%s.%s/%s/", bucket, host, rootDirectory)
spaceMaxSize = maxSize
spaceRoot = rootDirectory
return nil
}
// UploadFile uploads the file to the space. This does not do any checks on it, so the endpoints should
// ensure that's all okay.
func UploadFile(ctx context.Context, folder string, name string, mimeType string, reader io.Reader, size int64) (string, error) {
if spaceClient == nil {
return "", errors.New("This functionality is not enabled")
}
path := folder + "/" + name
if size > spaceMaxSize {
return "", errors.New("File is too big")
}
_, err := spaceClient.PutObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, reader, size, minio.PutObjectOptions{
ContentType: mimeType,
UserMetadata: map[string]string{
"x-amz-acl": "public-read",
},
})
if err != nil {
return "", err
}
_, err = spaceClient.StatObject(spaceBucket, spaceRoot+"/"+path, minio.StatObjectOptions{})
if err != nil {
return "", err
}
return path, nil
}
// RemoveFile removes a file from the space
func RemoveFile(folder string, name string) error {
if spaceClient == nil {
return errors.New("This functionality is not enabled")
}
path := folder + "/" + name
return spaceClient.RemoveObject(spaceBucket, spaceRoot+"/"+path)
}
// DownloadFile opens a file for download, using the same path format as the UploadFile function. Remember to Close it!
func DownloadFile(ctx context.Context, path string) (io.ReadCloser, error) {
if spaceClient == nil {
return nil, errors.New("This functionality is not enabled")
}
return spaceClient.GetObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, minio.GetObjectOptions{})
}
// URLFromPath gets the URL from the path returned by UploadFile
func URLFromPath(path string) string {
return spaceURLRoot + path
}

33
models/channels/add.go

@ -1,33 +0,0 @@
package channels
import (
"errors"
"strings"
"git.aiterp.net/rpdata/api/models"
)
// ErrInvalidName is an error for an invalid channel name.
var ErrInvalidName = errors.New("Invalid channel name")
// Add creates a new channel.
func Add(name string, logged, hub bool, event, location string) (models.Channel, error) {
if len(name) < 3 && !strings.HasPrefix(name, "#") {
return models.Channel{}, ErrInvalidName
}
channel := models.Channel{
Name: name,
Logged: logged,
Hub: hub,
EventName: event,
LocationName: location,
}
err := collection.Insert(channel)
if err != nil {
return models.Channel{}, err
}
return channel, nil
}

19
models/channels/db.go

@ -1,19 +0,0 @@
package channels
import (
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
)
var collection *mgo.Collection
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.channels")
collection.EnsureIndexKey("logged")
collection.EnsureIndexKey("hub")
collection.EnsureIndexKey("event")
collection.EnsureIndexKey("location")
})
}

39
models/channels/edit.go

@ -1,39 +0,0 @@
package channels
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Edit edits a channels
func Edit(channel models.Channel, logged, hub *bool, eventName, locationName *string) (models.Channel, error) {
mutation := bson.M{}
if logged != nil && *logged != channel.Logged {
mutation["logged"] = *logged
channel.Logged = *logged
}
if hub != nil && *hub != channel.Hub {
mutation["hub"] = *hub
channel.Hub = *hub
}
if eventName != nil && *eventName != channel.EventName {
mutation["eventName"] = *eventName
channel.EventName = *eventName
}
if locationName != nil && *locationName != channel.LocationName {
mutation["locationName"] = *locationName
channel.LocationName = *locationName
}
if len(mutation) == 0 {
return channel, nil
}
err := collection.UpdateId(channel.Name, bson.M{"$set": mutation})
if err != nil {
return models.Channel{}, err
}
return channel, nil
}

33
models/channels/ensure.go

@ -1,33 +0,0 @@
package channels
import (
"strings"
"github.com/globalsign/mgo"
"git.aiterp.net/rpdata/api/models"
)
// Ensure adds a channel if it doesn't exist. If logged is set and the found channel isn't logged,
// that is changed.
func Ensure(name string, logged bool) (models.Channel, error) {
if len(name) < 3 && !strings.HasPrefix(name, "#") {
return models.Channel{}, ErrInvalidName
}
channel, err := FindName(name)
if err == mgo.ErrNotFound {
return Add(name, logged, false, "", "")
} else if err != nil {
return models.Channel{}, err
}
if logged && !channel.Logged {
channel, err = Edit(channel, &logged, nil, nil, nil)
if err != nil {
return models.Channel{}, err
}
}
return channel, nil
}

11
models/channels/find.go

@ -1,11 +0,0 @@
package channels
import "git.aiterp.net/rpdata/api/models"
// FindName finds a channel by its id (its name).
func FindName(name string) (models.Channel, error) {
channel := models.Channel{}
err := collection.FindId(name).One(&channel)
return channel, err
}

46
models/channels/list.go

@ -1,46 +0,0 @@
package channels
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Filter for searching
type Filter struct {
Logged *bool `json:"logged"`
EventName string `json:"eventName"`
LocationName string `json:"locationName"`
}
// List finds channels, if logged is true it will be limited to logged
// channels
func List(filter *Filter) ([]models.Channel, error) {
query := bson.M{}
if filter != nil {
if filter.Logged != nil {
query["logged"] = *filter.Logged
}
if filter.EventName != "" {
query["eventName"] = filter.EventName
}
if filter.LocationName != "" {
query["locationName"] = filter.LocationName
}
}
channels := make([]models.Channel, 0, 128)
err := collection.Find(query).Sort("_id").All(&channels)
return channels, err
}
// ListNames finds channels by the names provided
func ListNames(names ...string) ([]models.Channel, error) {
query := bson.M{"_id": bson.M{"$in": names}}
channels := make([]models.Channel, 0, 32)
err := collection.Find(query).All(&channels)
return channels, err
}

22
models/key.go

@ -0,0 +1,22 @@
package models
// A Key contains a JWT secret and the limitations of it. There are two types of
// keys, single-user keys and wildcard keys. The former is used to authenticate
// a single user (e.g. the logbot) through an API while the latter is only for
// services that can be trusted to perform its own authentication (a frontend).
type Key struct {
ID string `bson:"_id"`
Name string `bson:"name"`
User string `bson:"user"`
Secret string `bson:"secret"`
}
// ValidForUser returns true if the key's user is the same as
// the user, or it's a wildcard key.
func (key *Key) ValidForUser(user string) bool {
return key.User == user || key.User == "*"
}
type KeyFilter struct {
UserID *string
}

13
repositories/key.go

@ -0,0 +1,13 @@
package repositories
import (
"context"
"git.aiterp.net/rpdata/api/models"
)
type KeyRepository interface {
Find(ctx context.Context, id string) (*models.Key, error)
List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error)
Insert(ctx context.Context, key models.Key) (*models.Key, error)
Delete(ctx context.Context, key models.Key) error
}

11
repositories/user.go

@ -0,0 +1,11 @@
package repositories
import (
"context"
"git.aiterp.net/rpdata/api/models"
)
type UserRepository interface {
Find(ctx context.Context, id string) (*models.User, error)
Insert(ctx context.Context, user models.User) (*models.User, error)
}

331
services/auth.go

@ -0,0 +1,331 @@
package services
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/dgrijalva/jwt-go"
"log"
"net/http"
"reflect"
"strings"
"time"
)
var contextKey = &struct{ data string }{"Token Context Key"}
// ErrNoKid is returned if the key id is missing from the jwt token header,
var ErrNoKid = errors.New("missing \"kid\" field in token")
// ErrKeyNotFound is returned if the key wasn't found.
var ErrKeyNotFound = errors.New("key not found")
// ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
var ErrInvalidClaims = errors.New("invalid claims in token")
// ErrExpired is returned by parseClaims if the expiry date is in the past
var ErrExpired = errors.New("claims have already expired")
// ErrWrongUser is returned by CheckToken if the key cannot represent this user
var ErrWrongUser = errors.New("key is not valid for this user")
// ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist.
var ErrDeletedUser = errors.New("user was not found")
// ErrUnauthenticated is returned when the user is not authenticated
var ErrUnauthenticated = errors.New("you are not authenticated")
// ErrUnauthorized is returned when the user doesn't have access to this resource
var ErrUnauthorized = errors.New("you are not authorized to perform this action")
// ErrInvalidOperation is returned for operations that should never be allowed
var ErrInvalidOperation = errors.New("no permission exists for this operation")
// AuthService is a service for handling the 'orizations and 'entications.
type AuthService struct {
keys repositories.KeyRepository
users repositories.UserRepository
}
// FindKey finds a key by id.
func (s *AuthService) FindKey(ctx context.Context, id string) (*models.Key, error) {
return s.keys.Find(ctx, id)
}
// FindKey finds a key by id.
func (s *AuthService) FindUser(ctx context.Context, id string) (*models.User, error) {
return s.users.Find(ctx, id)
}
// CreateKey generates a new key for the user and name. This
// does not allow generating wildcard keys, they have to be
// manually inserted into the DB.
func (s *AuthService) CreateKey(ctx context.Context, name, user string) (*models.Key, error) {
if user == "*" {
return nil, errors.New("auth: wildcard keys not allowed")
}
secret, err := s.generateSecret()
if err != nil {
return nil, err
}
key := &models.Key{
Name: name,
User: user,
Secret: secret,
}
key, err = s.keys.Insert(ctx, *key)
if err != nil {
return nil, err
}
return key, nil
}
// CheckPermission does some magic.
func (s *AuthService) CheckPermission(ctx context.Context, op string, obj interface{}) error {
token := s.TokenFromContext(ctx)
if token == nil {
return ErrUnauthenticated
}
if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {
ptr := reflect.PtrTo(v.Type())
ptrValue := reflect.New(ptr.Elem())
ptrValue.Elem().Set(v)
obj = ptrValue.Interface()
}
var authorized = false
switch v := obj.(type) {
case *models.Channel:
authorized = token.Permitted("channel." + op)
case *models.Character:
authorized = token.PermittedUser(v.Author, "member", "character."+op)
case *models.Chapter:
authorized = token.PermittedUser(v.Author, "member", "chapter."+op)
case *models.Comment:
if op == "add" && v.Author != token.UserID {
return ErrInvalidOperation
}
authorized = token.PermittedUser(v.Author, "member", "comment."+op)
case *models.File:
authorized = token.PermittedUser(v.Author, "member", "file."+op)
case *models.Log:
authorized = token.Permitted("log." + op)
case *models.Post:
authorized = token.Permitted("post." + op)
case *models.Story:
authorized = token.PermittedUser(v.Author, "member", "story."+op)
case *models.User:
authorized = token.Permitted("user." + op)
default:
log.Panicf("Invalid model %T: %#+v", v, v)
}
if !authorized {
return ErrUnauthorized
}
return nil
}
// TokenFromContext gets the token from context.
func (s *AuthService) TokenFromContext(ctx context.Context) *models.Token {
token, ok := ctx.Value(contextKey).(*models.Token)
if !ok {
return nil
}
return token
}
// RequestWithToken either returns the request, or the request with a new context that
// has the token.
func (s *AuthService) RequestWithToken(r *http.Request) *http.Request {
header := r.Header.Get("Authorization")
if header == "" {
return r
}
if !strings.HasPrefix(header, "Bearer ") {
return r
}
token, err := s.CheckToken(r.Context(), header[7:])
if err != nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), contextKey, &token))
}
// CheckToken reads the token string and returns a token if everything is kosher.
func (s *AuthService) CheckToken(ctx context.Context, tokenString string) (token models.Token, err error) {
var key *models.Key
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
}
kid, ok := jwtToken.Header["kid"].(string)
if !ok {
return nil, ErrNoKid
}
key, err = s.FindKey(ctx, kid)
if err != nil || key.ID == "" {
return nil, ErrKeyNotFound
}
return []byte(key.Secret), nil
})
if err != nil {
return models.Token{}, err
}
userid, permissions, err := s.parseClaims(jwtToken.Claims)
if err != nil {
return models.Token{}, err
}
if !key.ValidForUser(userid) {
return models.Token{}, ErrWrongUser
}
user, err := s.ensureUser(ctx, userid)
if err != nil {
return models.Token{}, ErrDeletedUser
}
acceptedPermissions := make([]string, 0, len(user.Permissions))
if len(permissions) > 0 {
for _, permission := range permissions {
found := false
for _, userPermission := range user.Permissions {
if permission == userPermission {
found = true
break
}
}
if found {
acceptedPermissions = append(acceptedPermissions, permission)
}
}
} else {
acceptedPermissions = append(acceptedPermissions, user.Permissions...)
}
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
}
// AllPermissions gets all permissions and their purpose
func (s *AuthService) AllPermissions() map[string]string {
return map[string]string{
"member": "Can add/edit/remove own content",
"user.edit": "Can edit non-owned users",
"character.add": "Can add non-owned characters",
"character.edit": "Can edit non-owned characters",
"character.remove": "Can remove non-owned characters",
"channel.add": "Can add channels",
"channel.edit": "Can edit channels",
"channel.remove": "Can remove channels",
"comment.edit": "Can edit non-owned comments",
"comment.remove": "Can remove non-owned comments",
"log.add": "Can add logs",
"log.edit": "Can edit logs",
"log.remove": "Can remove logs",
"post.add": "Can add posts",
"post.edit": "Can edit posts",
"post.move": "Can move posts",
"post.remove": "Can remove posts",
"story.add": "Can add non-owned stories",
"story.edit": "Can edit non-owned stories",
"story.remove": "Can remove non-owned stories",
"story.unlisted": "Can view non-owned unlisted stories",
"file.upload": "Can upload files",
"file.list": "Can list non-owned files",
"file.view": "Can view non-owned files",
"file.edit": "Can edit non-owned files",
"file.remove": "Can remove non-owned files",
}
}
func (s *AuthService) parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) {
mapClaims, ok := jwtClaims.(jwt.MapClaims)
if !ok {
return "", nil, ErrInvalidClaims
}
if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) {
return "", nil, ErrExpired
}
if userid, ok = mapClaims["user"].(string); !ok {
return "", nil, ErrInvalidClaims
}
if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok {
for _, permission := range claimedPermissions {
if permission, ok := permission.(string); ok {
permissions = append(permissions, permission)
}
}
if len(permissions) == 0 {
return "", nil, ErrInvalidClaims
}
}
return
}
func (s *AuthService) generateSecret() (string, error) {
var data [64]byte
_, err := rand.Read(data[:])
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(data[:]), nil
}
func (s *AuthService) ensureUser(ctx context.Context, id string) (*models.User, error) {
user, err := s.users.Find(ctx, id)
if err == repositories.ErrNotFound {
user = &models.User{
ID: id,
Nick: "",
Permissions: []string{
"member",
"log.edit",
"post.edit",
"post.move",
"post.remove",
"file.upload",
},
}
user, err = s.users.Insert(ctx, *user)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
return user, err
}

11
services/changes.go

@ -2,7 +2,6 @@ package services
import (
"context"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/internal/notifier"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
@ -13,6 +12,7 @@ import (
type ChangeService struct {
changes repositories.ChangeRepository
authService *AuthService
mutex sync.RWMutex
buffer []models.Change
@ -31,7 +31,7 @@ func (s *ChangeService) List(ctx context.Context, filter models.ChangeFilter) ([
}
func (s *ChangeService) Submit(ctx context.Context, model models.ChangeModel, op string, listed bool, keys []models.ChangeKey, objects ...interface{}) {
token := auth.TokenFromContext(ctx)
token := s.authService.TokenFromContext(ctx)
if token == nil {
panic("no token!")
}
@ -127,15 +127,16 @@ func (s *ChangeService) Subscribe(ctx context.Context, filter models.ChangeFilte
func (s *ChangeService) loop() {
for change := range s.submitQueue {
timeout, cancel := context.WithTimeout(context.Background(), time.Second*15)
timeout, cancel := context.WithTimeout(context.Background(), time.Second*5)
change, err := s.changes.Insert(timeout, *change)
if err != nil {
log.Println("Failed to insert change:")
} else {
log.Println("Change", change.ID, "inserted.")
continue
}
log.Println("Change", change.ID, "inserted.")
s.mutex.Lock()
s.buffer = append(s.buffer, *change)
if len(s.buffer) > 16 {

8
services/channels.go

@ -2,7 +2,6 @@ package services
import (
"context"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changekeys"
"git.aiterp.net/rpdata/api/repositories"
@ -14,6 +13,7 @@ type ChannelService struct {
channels repositories.ChannelRepository
loader *loaders.ChannelLoader
changeService *ChangeService
authService *AuthService
}
func (s *ChannelService) Find(ctx context.Context, id string) (*models.Channel, error) {
@ -44,7 +44,7 @@ func (s *ChannelService) List(ctx context.Context, filter models.ChannelFilter)
}
func (s *ChannelService) Create(ctx context.Context, name string, logged, hub bool, eventName, locationName string) (*models.Channel, error) {
err := auth.CheckPermission(ctx, "add", &models.Channel{})
err := s.authService.CheckPermission(ctx, "add", &models.Channel{})
if err != nil {
return nil, err
}
@ -91,7 +91,7 @@ func (s *ChannelService) Update(ctx context.Context, name string, update models.
return nil, err
}
err = auth.CheckPermission(ctx, "edit", channel)
err = s.authService.CheckPermission(ctx, "edit", channel)
if err != nil {
return nil, err
}
@ -114,7 +114,7 @@ func (s *ChannelService) Delete(ctx context.Context, name string) (*models.Chann
return nil, err
}
err = auth.CheckPermission(ctx, "remove", channel)
err = s.authService.CheckPermission(ctx, "remove", channel)
if err != nil {
return nil, err
}

16
services/characters.go

@ -3,7 +3,6 @@ package services
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changekeys"
"git.aiterp.net/rpdata/api/repositories"
@ -16,6 +15,7 @@ type CharacterService struct {
characters repositories.CharacterRepository
loader *loaders.CharacterLoader
changeService *ChangeService
authService *AuthService
}
// Find uses the loader to find the character by the ID.
@ -65,9 +65,9 @@ func (s *CharacterService) List(ctx context.Context, filter models.CharacterFilt
}
func (s *CharacterService) Create(ctx context.Context, nick, name, shortName, author, description string) (*models.Character, error) {
token := auth.TokenFromContext(ctx)
token := s.authService.TokenFromContext(ctx)
if token == nil {
return nil, auth.ErrUnauthenticated
return nil, ErrUnauthenticated
}
if name == "" {
@ -91,7 +91,7 @@ func (s *CharacterService) Create(ctx context.Context, nick, name, shortName, au
Description: description,
}
err := auth.CheckPermission(ctx, "add", character)
err := s.authService.CheckPermission(ctx, "add", character)
if err != nil {
return nil, err
}
@ -112,7 +112,7 @@ func (s *CharacterService) Update(ctx context.Context, id string, name, shortNam
return nil, err
}
err = auth.CheckPermission(ctx, "edit", character)
err = s.authService.CheckPermission(ctx, "edit", character)
if err != nil {
return nil, err
}
@ -140,7 +140,7 @@ func (s *CharacterService) AddNick(ctx context.Context, id string, nick string)
return nil, err
}
err = auth.CheckPermission(ctx, "edit", character)
err = s.authService.CheckPermission(ctx, "edit", character)
if err != nil {
return nil, err
}
@ -164,7 +164,7 @@ func (s *CharacterService) RemoveNick(ctx context.Context, id string, nick strin
return nil, err
}
err = auth.CheckPermission(ctx, "edit", character)
err = s.authService.CheckPermission(ctx, "edit", character)
if err != nil {
return nil, err
}
@ -188,7 +188,7 @@ func (s *CharacterService) Delete(ctx context.Context, id string) (*models.Chara
return nil, err
}
err = auth.CheckPermission(ctx, "edit", character)
err = s.authService.CheckPermission(ctx, "edit", character)
if err != nil {
return nil, err
}

10
services/files.go

@ -3,7 +3,6 @@ package services
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/h2non/filetype"
@ -20,6 +19,7 @@ var ErrCouldNotUploadFile = errors.New("could not upload file")
// FileService is a service for files.
type FileService struct {
files repositories.FileRepository
authService *AuthService
}
func (s *FileService) Find(ctx context.Context, id string) (*models.File, error) {
@ -29,7 +29,7 @@ func (s *FileService) Find(ctx context.Context, id string) (*models.File, error)
}
if !file.Public {
err := auth.CheckPermission(ctx, "view", file)
err := s.authService.CheckPermission(ctx, "view", file)
if err != nil {
return nil, repositories.ErrNotFound
}
@ -39,7 +39,7 @@ func (s *FileService) Find(ctx context.Context, id string) (*models.File, error)
}
func (s *FileService) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
token := auth.TokenFromContext(ctx)
token := s.authService.TokenFromContext(ctx)
if filter.Public != nil {
if *filter.Public == false {
@ -48,7 +48,7 @@ func (s *FileService) List(ctx context.Context, filter models.FileFilter) ([]*mo
}
if !token.PermittedUser(*filter.Author, "member", "file.list") {
return nil, auth.ErrUnauthorized
return nil, ErrUnauthorized
}
}
}
@ -83,7 +83,7 @@ func (s *FileService) Edit(ctx context.Context, id string, name *string, public
return nil, err
}
err = auth.CheckPermission(ctx, "edit", file)
err = s.authService.CheckPermission(ctx, "edit", file)
if err != nil {
if !file.Public {
return nil, repositories.ErrNotFound

27
services/logs.go

@ -3,10 +3,8 @@ package services
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changekeys"
"git.aiterp.net/rpdata/api/models/channels"
"git.aiterp.net/rpdata/api/repositories"
"git.aiterp.net/rpdata/api/services/parsers"
"golang.org/x/sync/errgroup"
@ -23,6 +21,7 @@ type LogService struct {
changeService *ChangeService
channelService *ChannelService
characterService *CharacterService
authService *AuthService
unknownNicks map[string]int
unknownNicksMutex sync.Mutex
@ -93,7 +92,7 @@ func (s *LogService) Create(ctx context.Context, title, description, channelName
Open: open,
}
if err := auth.CheckPermission(ctx, "add", log); err != nil {
if err := s.authService.CheckPermission(ctx, "add", log); err != nil {
return nil, err
}
@ -114,7 +113,7 @@ func (s *LogService) Create(ctx context.Context, title, description, channelName
// Import creates new logs from common formats.
func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]*models.Log, error) {
if err := auth.CheckPermission(ctx, "add", &models.Log{}); err != nil {
if err := s.authService.CheckPermission(ctx, "add", &models.Log{}); err != nil {
return nil, err
}
@ -126,7 +125,7 @@ func (s *LogService) Import(ctx context.Context, importer models.LogImporter, da
}
eventName := ""
if channel, err := channels.FindName(channelName); err == nil {
if channel, err := s.channelService.Find(ctx, channelName); err == nil {
eventName = channel.EventName
}
@ -212,7 +211,7 @@ func (s *LogService) Update(ctx context.Context, id string, update models.LogUpd
return nil, err
}
if err := auth.CheckPermission(ctx, "edit", log); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", log); err != nil {
return nil, err
}
@ -232,7 +231,7 @@ func (s *LogService) SplitLog(ctx context.Context, logId string, startPostId str
if err != nil {
return nil, err
}
if err := auth.CheckPermission(ctx, "add", l); err != nil {
if err := s.authService.CheckPermission(ctx, "add", l); err != nil {
return nil, err
}
@ -314,11 +313,11 @@ func (s *LogService) SplitLog(ctx context.Context, logId string, startPostId str
func (s *LogService) MergeLogs(ctx context.Context, targetID string, sourceID string, removeAfter bool) (*models.Log, error) {
// Check permissions
if err := auth.CheckPermission(ctx, "edit", &models.Log{}); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", &models.Log{}); err != nil {
return nil, err
}
if removeAfter {
if err := auth.CheckPermission(ctx, "remove", &models.Log{}); err != nil {
if err := s.authService.CheckPermission(ctx, "remove", &models.Log{}); err != nil {
return nil, err
}
}
@ -393,7 +392,7 @@ func (s *LogService) AddPost(ctx context.Context, logId string, time time.Time,
Time: time,
}
if err := auth.CheckPermission(ctx, "add", post); err != nil {
if err := s.authService.CheckPermission(ctx, "add", post); err != nil {
return nil, err
}
@ -424,7 +423,7 @@ func (s *LogService) EditPost(ctx context.Context, id string, update models.Post
return nil, err
}
if err := auth.CheckPermission(ctx, "edit", post); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", post); err != nil {
return nil, err
}
@ -460,7 +459,7 @@ func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*
return nil, err
}
if err := auth.CheckPermission(ctx, "move", post); err != nil {
if err := s.authService.CheckPermission(ctx, "move", post); err != nil {
return nil, err
}
@ -491,7 +490,7 @@ func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, e
return nil, err
}
if err := auth.CheckPermission(ctx, "remove", post); err != nil {
if err := s.authService.CheckPermission(ctx, "remove", post); err != nil {
return nil, err
}
@ -523,7 +522,7 @@ func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error)
return nil, err
}
if err := auth.CheckPermission(ctx, "remove", log); err != nil {
if err := s.authService.CheckPermission(ctx, "remove", log); err != nil {
return nil, err
}

15
services/services.go

@ -13,24 +13,37 @@ type Bundle struct {
Logs *LogService
Channels *ChannelService
Stories *StoryService
Auth *AuthService
Files *FileService
}
// NewBundle creates a new bundle.
func NewBundle(db database.Database) *Bundle {
bundle := &Bundle{}
bundle.Auth = &AuthService{
keys: db.Keys(),
users: db.Users(),
}
bundle.Files = &FileService{
files: nil,
authService: bundle.Auth,
}
bundle.Changes = &ChangeService{
changes: db.Changes(),
authService: bundle.Auth,
}
bundle.Tags = &TagService{tags: db.Tags()}
bundle.Characters = &CharacterService{
characters: db.Characters(),
loader: loaders.CharacterLoaderFromRepository(db.Characters()),
changeService: bundle.Changes,
authService: bundle.Auth,
}
bundle.Channels = &ChannelService{
channels: db.Channels(),
loader: loaders.ChannelLoaderFromRepository(db.Channels()),
changeService: bundle.Changes,
authService: bundle.Auth,
}
bundle.Logs = &LogService{
logs: db.Logs(),
@ -38,6 +51,7 @@ func NewBundle(db database.Database) *Bundle {
changeService: bundle.Changes,
channelService: bundle.Channels,
characterService: bundle.Characters,
authService: bundle.Auth,
unknownNicks: make(map[string]int, 512),
}
@ -47,6 +61,7 @@ func NewBundle(db database.Database) *Bundle {
comments: db.Comments(),
changeService: bundle.Changes,
characterService: bundle.Characters,
authService: bundle.Auth,
}
return bundle

50
services/stories.go

@ -3,7 +3,6 @@ package services
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changekeys"
@ -18,6 +17,7 @@ type StoryService struct {
comments repositories.CommentRepository
changeService *ChangeService
characterService *CharacterService
authService *AuthService
}
func (s *StoryService) FindStory(ctx context.Context, id string) (*models.Story, error) {
@ -46,9 +46,9 @@ func (s *StoryService) ListComments(ctx context.Context, chapter models.Chapter,
func (s *StoryService) CreateStory(ctx context.Context, name string, author *string, category models.StoryCategory, listed, open bool, tags []models.Tag, createdDate, fictionalDate time.Time) (*models.Story, error) {
if author == nil {
token := auth.TokenFromContext(ctx)
token := s.authService.TokenFromContext(ctx)
if token == nil {
return nil, auth.ErrUnauthenticated
return nil, ErrUnauthenticated
}
author = &token.UserID
@ -66,7 +66,7 @@ func (s *StoryService) CreateStory(ctx context.Context, name string, author *str
UpdatedDate: createdDate,
}
if err := auth.CheckPermission(ctx, "add", story); err != nil {
if err := s.authService.CheckPermission(ctx, "add", story); err != nil {
return nil, err
}
@ -82,9 +82,9 @@ func (s *StoryService) CreateStory(ctx context.Context, name string, author *str
func (s *StoryService) CreateChapter(ctx context.Context, story models.Story, title, source string, author *string, createdDate time.Time, fictionalDate *time.Time, commentMode models.ChapterCommentMode) (*models.Chapter, error) {
if author == nil {
token := auth.TokenFromContext(ctx)
token := s.authService.TokenFromContext(ctx)
if token == nil {
return nil, auth.ErrUnauthenticated
return nil, ErrUnauthenticated
}
author = &token.UserID
@ -106,11 +106,11 @@ func (s *StoryService) CreateChapter(ctx context.Context, story models.Story, ti
}
if story.Open {
if !auth.TokenFromContext(ctx).Permitted("member", "chapter.add") {
return nil, auth.ErrUnauthorized
if !s.authService.TokenFromContext(ctx).Permitted("member", "chapter.add") {
return nil, ErrUnauthorized
}
} else {
if err := auth.CheckPermission(ctx, "add", chapter); err != nil {
if err := s.authService.CheckPermission(ctx, "add", chapter); err != nil {
return nil, err
}
}
@ -143,10 +143,10 @@ func (s *StoryService) CreateComment(ctx context.Context, chapter models.Chapter
}
if author == "" {
if token := auth.TokenFromContext(ctx); token != nil {
if token := s.authService.TokenFromContext(ctx); token != nil {
author = token.UserID
} else {
return nil, auth.ErrUnauthenticated
return nil, ErrUnauthenticated
}
}
@ -162,7 +162,7 @@ func (s *StoryService) CreateComment(ctx context.Context, chapter models.Chapter
EditedDate: createdDate,
Source: source,
}
if err := auth.CheckPermission(ctx, "add", comment); err != nil {
if err := s.authService.CheckPermission(ctx, "add", comment); err != nil {
return nil, err
}
@ -185,7 +185,7 @@ func (s *StoryService) EditStory(ctx context.Context, story *models.Story, name
panic("StoryService.Edit called with nil story")
}
if err := auth.CheckPermission(ctx, "edit", story); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", story); err != nil {
return nil, err
}
@ -206,7 +206,7 @@ func (s *StoryService) EditStory(ctx context.Context, story *models.Story, name
}
func (s *StoryService) AddStoryTag(ctx context.Context, story models.Story, tag models.Tag) (*models.Story, error) {
if err := auth.CheckPermission(ctx, "edit", &story); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", &story); err != nil {
return nil, err
}
@ -223,7 +223,7 @@ func (s *StoryService) AddStoryTag(ctx context.Context, story models.Story, tag
}
func (s *StoryService) RemoveStoryTag(ctx context.Context, story models.Story, tag models.Tag) (*models.Story, error) {
if err := auth.CheckPermission(ctx, "edit", &story); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", &story); err != nil {
return nil, err
}
@ -249,7 +249,7 @@ func (s *StoryService) EditChapter(ctx context.Context, chapter *models.Chapter,
panic("StoryService.EditChapter called with nil chapter")
}
if err := auth.CheckPermission(ctx, "edit", chapter); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", chapter); err != nil {
return nil, err
}
@ -274,16 +274,16 @@ func (s *StoryService) EditChapter(ctx context.Context, chapter *models.Chapter,
}
func (s *StoryService) MoveChapter(ctx context.Context, chapter *models.Chapter, from, to models.Story) (*models.Chapter, error) {
if err := auth.CheckPermission(ctx, "move", chapter); err != nil {
if err := s.authService.CheckPermission(ctx, "move", chapter); err != nil {
return nil, err
}
if to.Open {
if !auth.TokenFromContext(ctx).Permitted("member", "chapter.add") {
return nil, auth.ErrUnauthorized
if !s.authService.TokenFromContext(ctx).Permitted("member", "chapter.add") {
return nil, ErrUnauthorized
}
} else {
if err := auth.CheckPermission(ctx, "add", chapter); err != nil {
if err := s.authService.CheckPermission(ctx, "add", chapter); err != nil {
return nil, err
}
}
@ -304,7 +304,7 @@ func (s *StoryService) EditComment(ctx context.Context, comment *models.Comment,
panic("StoryService.EditChapter called with nil chapter")
}
if err := auth.CheckPermission(ctx, "edit", comment); err != nil {
if err := s.authService.CheckPermission(ctx, "edit", comment); err != nil {
return nil, err
}
@ -343,7 +343,7 @@ func (s *StoryService) EditComment(ctx context.Context, comment *models.Comment,
}
func (s *StoryService) RemoveStory(ctx context.Context, story *models.Story) error {
if err := auth.CheckPermission(ctx, "add", story); err != nil {
if err := s.authService.CheckPermission(ctx, "add", story); err != nil {
return err
}
@ -358,7 +358,7 @@ func (s *StoryService) RemoveStory(ctx context.Context, story *models.Story) err
}
func (s *StoryService) RemoveChapter(ctx context.Context, chapter *models.Chapter) error {
if err := auth.CheckPermission(ctx, "remove", chapter); err != nil {
if err := s.authService.CheckPermission(ctx, "remove", chapter); err != nil {
return err
}
@ -377,7 +377,7 @@ func (s *StoryService) RemoveChapter(ctx context.Context, chapter *models.Chapte
}
func (s *StoryService) RemoveComment(ctx context.Context, comment *models.Comment) error {
if err := auth.CheckPermission(ctx, "remove", comment); err != nil {
if err := s.authService.CheckPermission(ctx, "remove", comment); err != nil {
return err
}
@ -409,7 +409,7 @@ func (s *StoryService) permittedCharacter(ctx context.Context, permissionKind, c
return errors.New("character could not be found")
}
token := auth.TokenFromContext(ctx)
token := s.authService.TokenFromContext(ctx)
if character.Author != token.UserID && !token.Permitted(permissionKind+".edit") {
return errors.New("you are not permitted to use others' character")
}

Loading…
Cancel
Save