@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+1
-1
@@ -55,5 +55,5 @@ func (r *rootResolver) Change() graphcore.ChangeResolver {
|
||||
}
|
||||
|
||||
func (r *rootResolver) Token() graphcore.TokenResolver {
|
||||
return &types.TokenResolver
|
||||
return types.TokenResolver(r.s)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,7 @@ input FilesFilter {
|
||||
public: Boolean
|
||||
|
||||
# Limit the MIME types of the files.
|
||||
mimeType: [String!]
|
||||
mimeTypes: [String!]
|
||||
}
|
||||
|
||||
# Input for editFile mutation
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+7
-6
@@ -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"
|
||||
@@ -12,7 +11,8 @@ import (
|
||||
)
|
||||
|
||||
type ChangeService struct {
|
||||
changes repositories.ChangeRepository
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+6
-6
@@ -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"
|
||||
@@ -19,7 +18,8 @@ var ErrCouldNotUploadFile = errors.New("could not upload file")
|
||||
|
||||
// FileService is a service for files.
|
||||
type FileService struct {
|
||||
files repositories.FileRepository
|
||||
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
|
||||
|
||||
+13
-14
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -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(),
|
||||
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
|
||||
|
||||
+25
-25
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user