@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -62,3 +62,7 @@ func CommentID() string {
|
||||
func FileID() string {
|
||||
return ID("F", 16)
|
||||
}
|
||||
|
||||
func KeyID() string {
|
||||
return ID("K", 32)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user