diff --git a/backend/cmd/threadule/main.go b/backend/cmd/threadule/main.go index cf194a2..e74e41d 100644 --- a/backend/cmd/threadule/main.go +++ b/backend/cmd/threadule/main.go @@ -31,6 +31,12 @@ func main() { AccessLog: log.New(os.Stdout, "access", log.Ldate|log.Lmicroseconds|log.Lmsgprefix), } + ctx.Log.Info("setting up persistence layer") + ctx.Data, err = data.Setup(ctx) + if err != nil { + ctx.Log.Fatal("couldn't setup persistence layer") + } + ctx.Log.Info("setting up logic layer") ctx.Logic, err = logic.Setup(ctx) if err != nil { @@ -38,12 +44,6 @@ func main() { ctx.Log.Fatal(err) } - ctx.Log.Info("setting up persistence layer") - ctx.Data, err = data.Setup(ctx) - if err != nil { - ctx.Log.Fatal("couldn't setup persistence layer") - } - ctx.Log.Info("setting up routes") handler := router.Setup(ctx) err = web.StartServer(ctx, handler) diff --git a/backend/go.mod b/backend/go.mod index dd58179..478a7eb 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,6 +10,7 @@ require ( github.com/kr/pretty v0.1.0 // indirect github.com/pelletier/go-toml/v2 v2.0.0-beta.3 github.com/satori/go.uuid v1.2.0 + golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gorm.io/driver/mysql v1.1.2 gorm.io/gorm v1.21.13 diff --git a/backend/go.sum b/backend/go.sum index 9f403cd..b69290f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -35,8 +35,16 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU= github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= +golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ= +golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/app/data.go b/backend/internal/app/data.go index 1292b61..6e6451f 100644 --- a/backend/internal/app/data.go +++ b/backend/internal/app/data.go @@ -3,6 +3,11 @@ package app import "threadule/backend/internal/data/models" type Data interface { + CountUsers() (int64, error) + CreateUser(user *models.User) error + + AddGroup(group *models.Group) error + GetSession(id string) (*models.Session, error) UpdateSession(session *models.Session) error CleanupSessions() error diff --git a/backend/internal/data/group.go b/backend/internal/data/group.go new file mode 100644 index 0000000..ea7b30b --- /dev/null +++ b/backend/internal/data/group.go @@ -0,0 +1,9 @@ +package data + +import "threadule/backend/internal/data/models" + +func (d *Data) AddGroup(group *models.Group) error { + return d.db. + Create(group). + Error +} diff --git a/backend/internal/data/models/group.go b/backend/internal/data/models/group.go index da05986..502b60f 100644 --- a/backend/internal/data/models/group.go +++ b/backend/internal/data/models/group.go @@ -4,10 +4,25 @@ type Group struct { BaseModel Users []*User `gorm:"many2many:user_groups;"` - Name string - DisplayName string - + Name string + DisplayName string + AdminGroup bool + ManageUsers bool + ManageGroups bool LimitAccounts uint LimitThreads uint LimitTweets uint } + +func GetDefaultAdminGroup() *Group { + return &Group{ + Name: "admin", + DisplayName: "Administrators", + AdminGroup: true, + ManageUsers: true, + ManageGroups: true, + LimitAccounts: 0, + LimitThreads: 0, + LimitTweets: 0, + } +} diff --git a/backend/internal/data/models/user.go b/backend/internal/data/models/user.go index 865d85e..1e7a5a8 100644 --- a/backend/internal/data/models/user.go +++ b/backend/internal/data/models/user.go @@ -7,3 +7,9 @@ type User struct { Username string Password string } + +func GetDefaultAdminUser() *User { + return &User{ + Username: "admin", + } +} diff --git a/backend/internal/data/user.go b/backend/internal/data/user.go new file mode 100644 index 0000000..d03d237 --- /dev/null +++ b/backend/internal/data/user.go @@ -0,0 +1,18 @@ +package data + +import "threadule/backend/internal/data/models" + +func (d *Data) CountUsers() (int64, error) { + var c int64 + err := d.db. + Model(&models.User{}). + Count(&c). + Error + return c, err +} + +func (d *Data) CreateUser(user *models.User) error { + return d.db. + Create(user). + Error +} diff --git a/backend/internal/logic/logic.go b/backend/internal/logic/logic.go index af10219..219f7fc 100644 --- a/backend/internal/logic/logic.go +++ b/backend/internal/logic/logic.go @@ -9,13 +9,3 @@ type Logic struct { } var _ app.Logic = &Logic{} - -func Setup(ctx *app.Context) (app.Logic, error) { - logic := &Logic{ - ctx: ctx, - } - - logic.startScheduler() - - return logic, nil -} diff --git a/backend/internal/logic/password.go b/backend/internal/logic/password.go new file mode 100644 index 0000000..f41d228 --- /dev/null +++ b/backend/internal/logic/password.go @@ -0,0 +1,30 @@ +package logic + +import ( + "golang.org/x/crypto/bcrypt" + "math/rand" + "strings" +) + +const defaultPasswordLength = 16 +const defaultPasswordCharSet = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789" + + "=!$%&+#-_.,;:" + +func (l *Logic) defaultPassword() string { + builder := strings.Builder{} + for i := 0; i < defaultPasswordLength; i++ { + builder.WriteRune(rune(defaultPasswordCharSet[rand.Intn(len(defaultPasswordCharSet))])) + } + return builder.String() +} + +func (l *Logic) hashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hashedPassword), err +} + +func (l *Logic) checkPassword(hash, password string) bool { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil +} diff --git a/backend/internal/logic/setup.go b/backend/internal/logic/setup.go new file mode 100644 index 0000000..ae83919 --- /dev/null +++ b/backend/internal/logic/setup.go @@ -0,0 +1,65 @@ +package logic + +import ( + "fmt" + "threadule/backend/internal/app" + "threadule/backend/internal/data/models" +) + +func Setup(ctx *app.Context) (app.Logic, error) { + logic := &Logic{ + ctx: ctx, + } + + logic.startScheduler() + + err := logic.firstTimeSetup() + if err != nil { + return nil, err + } + + return logic, nil +} + +func (l *Logic) firstTimeSetup() error { + c, err := l.ctx.Data.CountUsers() + if err != nil { + l.ctx.Log.Errorf("error during first time setup check: %v", err) + return err + } + + if c != 0 { + return nil + } + + // no users -> probably first time setup + l.ctx.Log.Info("executing first time setup") + + adminGroup := models.GetDefaultAdminGroup() + err = l.ctx.Data.AddGroup(adminGroup) + if err != nil { + l.ctx.Log.Errorf("couldn't create admin group: %v", err) + return err + } + + adminUser := models.GetDefaultAdminUser() + adminUser.Groups = []*models.Group{adminGroup} + password := l.defaultPassword() + adminUser.Password, err = l.hashPassword(password) + if err != nil { + // if this fails we can't recover anyway + l.ctx.Log.Fatal(err) + } + + err = l.ctx.Data.CreateUser(adminUser) + if err != nil { + l.ctx.Log.Errorf("couldn't create admin user: %v", err) + return err + } + + fmt.Println("initial credentials:") + fmt.Printf("Username: %s\n", adminUser.Username) + fmt.Printf("Password: %s\n", password) + + return nil +}