Long overdue initial commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var globalMutex sync.Mutex
|
||||
var global *Config
|
||||
|
||||
// Config is configuration
|
||||
type Config struct {
|
||||
Space struct {
|
||||
Host string `json:"host"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Bucket string `json:"bucket"`
|
||||
MaxSize int64 `json:"maxSize"`
|
||||
Root string `json:"root"`
|
||||
} `json:"space"`
|
||||
|
||||
Database struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Db string `json:"db"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Mechanism string `json:"mechanism"`
|
||||
} `json:"database"`
|
||||
|
||||
Wiki struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"wiki"`
|
||||
}
|
||||
|
||||
// Load loads config stuff
|
||||
func (config *Config) Load(filename string) error {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.NewDecoder(file).Decode(config)
|
||||
}
|
||||
|
||||
// LoadAny loads the first of these files it can find
|
||||
func (config *Config) LoadAny(filenames ...string) error {
|
||||
for _, filename := range filenames {
|
||||
if err := config.Load(filename); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
*config = Config{}
|
||||
}
|
||||
|
||||
return errors.New("Failed to load configuration files")
|
||||
}
|
||||
|
||||
// Global gets the global configuration, loading it if this is the first caller
|
||||
func Global() Config {
|
||||
globalMutex.Lock()
|
||||
if global == nil {
|
||||
global = &Config{}
|
||||
err := global.LoadAny("/etc/aiterp/rpdata.json", "./config.json")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
globalMutex.Unlock()
|
||||
|
||||
return *global
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package session
|
||||
|
||||
import "context"
|
||||
|
||||
type contextKeyType struct{ name string }
|
||||
|
||||
func (ck *contextKeyType) String() string {
|
||||
return ck.name
|
||||
}
|
||||
|
||||
var contextKey = &contextKeyType{name: "session context key"}
|
||||
|
||||
// FromContext gets a session fron the context.
|
||||
func FromContext(ctx context.Context) *Session {
|
||||
return ctx.Value(contextKey).(*Session)
|
||||
}
|
||||
|
||||
func contextWithSession(parent context.Context, session *Session) context.Context {
|
||||
return context.WithValue(parent, contextKey, session)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package session
|
||||
|
||||
// DefaultPermissions gets the default permissions
|
||||
func DefaultPermissions() []string {
|
||||
return []string{
|
||||
"member",
|
||||
"log.edit",
|
||||
"log.reorder",
|
||||
"post.edit",
|
||||
"post.move",
|
||||
}
|
||||
}
|
||||
|
||||
// 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 any users",
|
||||
"character.add": "Can add any characters",
|
||||
"character.edit": "Can edit any characters",
|
||||
"character.remove": "Can remove any characters",
|
||||
"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.mvoe": "Can mvoe posts",
|
||||
"post.remove": "Can remove posts",
|
||||
"story.add": "Can add any stories",
|
||||
"story.edit": "Can edit any stories",
|
||||
"story.remove": "Can remove any stories",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/aiterp/wikiauth"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/config"
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var sessionCollection *mgo.Collection
|
||||
|
||||
// A Session represents a login session.
|
||||
type Session struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
ID string `bson:"_id"`
|
||||
Time time.Time `bson:"time"`
|
||||
UserID string `bson:"userId"`
|
||||
|
||||
user *User
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
// Load loads a session from a cookie, returning either `r` or a request
|
||||
// with the session context.
|
||||
func Load(w http.ResponseWriter, r *http.Request) *http.Request {
|
||||
cookie, err := r.Cookie("aiterp_session")
|
||||
if err != nil {
|
||||
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
|
||||
}
|
||||
|
||||
id := cookie.Value
|
||||
|
||||
session := Session{}
|
||||
err = sessionCollection.FindId(id).One(&session)
|
||||
if err != nil || time.Since(session.Time) > time.Hour*168 {
|
||||
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
|
||||
}
|
||||
|
||||
if session.ID != "" && time.Since(session.Time) > time.Second*30 {
|
||||
session.Time = time.Now()
|
||||
go sessionCollection.UpdateId(id, bson.M{"$set": bson.M{"time": session.Time}})
|
||||
}
|
||||
|
||||
cookie.Expires = time.Now().Add(time.Hour * 168)
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
session.w = w
|
||||
|
||||
return r.WithContext(contextWithSession(r.Context(), &session))
|
||||
}
|
||||
|
||||
// Login logs a user in.
|
||||
func (session *Session) Login(username, password string) error {
|
||||
auth := wikiauth.New(config.Global().Wiki.URL)
|
||||
|
||||
err := auth.Login(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Allow bot passwords
|
||||
username = strings.SplitN(username, "@", 2)[0]
|
||||
|
||||
data := make([]byte, 32)
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.ID = hex.EncodeToString(data)
|
||||
session.UserID = username
|
||||
session.Time = time.Now()
|
||||
|
||||
err = sessionCollection.Insert(&session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(session.w, &http.Cookie{
|
||||
Name: "aiterp_session",
|
||||
Value: session.ID,
|
||||
Expires: time.Now().Add(time.Hour * 2160), // 90 days
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
user, err := FindUser(session.UserID)
|
||||
if err == mgo.ErrNotFound {
|
||||
user = User{ID: username, Nick: "", Permissions: DefaultPermissions()}
|
||||
|
||||
err := userCollection.Insert(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout logs out the session
|
||||
func (session *Session) Logout() {
|
||||
http.SetCookie(session.w, &http.Cookie{
|
||||
Name: "aiterp_session",
|
||||
Value: "",
|
||||
Expires: time.Unix(0, 0),
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
session.mutex.Lock()
|
||||
session.user = nil
|
||||
session.UserID = ""
|
||||
session.ID = ""
|
||||
session.mutex.Unlock()
|
||||
|
||||
sessionCollection.RemoveId(session.ID)
|
||||
}
|
||||
|
||||
// User gets the user information for the session.
|
||||
func (session *Session) User() *User {
|
||||
session.mutex.Lock()
|
||||
defer session.mutex.Unlock()
|
||||
|
||||
if session.user != nil {
|
||||
return session.user
|
||||
}
|
||||
|
||||
if session.UserID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := FindUser(session.UserID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &user
|
||||
}
|
||||
|
||||
// NameOrPermitted is a shorthand for checking the username OR permissions, e.g. to check
|
||||
// if a logged in user can edit a certain post.
|
||||
func (session *Session) NameOrPermitted(userid string, permissions ...string) bool {
|
||||
if session.UserID == userid {
|
||||
return true
|
||||
}
|
||||
|
||||
user := session.User()
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return user.Permitted()
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
sessionCollection = db.C("core.sessions")
|
||||
|
||||
sessionCollection.EnsureIndexKey("nick")
|
||||
sessionCollection.EnsureIndexKey("userId")
|
||||
|
||||
err := sessionCollection.EnsureIndex(mgo.Index{
|
||||
Name: "time",
|
||||
Key: []string{"time"},
|
||||
ExpireAfter: time.Hour * 168,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
var userCollection *mgo.Collection
|
||||
|
||||
// A User represents user information about a user that has logged in.
|
||||
type User struct {
|
||||
ID string `bson:"_id" json:"id"`
|
||||
Nick string `bson:"nick,omitempty" json:"nick,omitempty"`
|
||||
Permissions []string `bson:"permissions" json:"permissions"`
|
||||
}
|
||||
|
||||
// Permitted returns true if either of the permissions can be found
|
||||
//
|
||||
// `user.ID == page.Author || user.Permitted("story.edit")`
|
||||
func (user *User) Permitted(permissions ...string) bool {
|
||||
for i := range permissions {
|
||||
for j := range user.Permissions {
|
||||
if permissions[i] == user.Permissions[j] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FindUser finds a user by userid
|
||||
func FindUser(userid string) (User, error) {
|
||||
user := User{}
|
||||
err := userCollection.FindId(userid).One(&user)
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
userCollection = db.C("core.users")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
err = ConnectSpace(sconf.Host, sconf.AccessKey, sconf.SecretKey, sconf.Bucket, sconf.MaxSize, sconf.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasInitialized = true
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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) {
|
||||
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, path, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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