From 4d930c0c8cb585979798fac2bb254f991faa62fb Mon Sep 17 00:00:00 2001 From: Gabriel Arakaki Giovanini Date: Mon, 26 Jun 2023 22:26:10 +0200 Subject: feat: Add initial user setup --- cmd/server/main.go | 28 ++++++++++++---------- pkg/components/auth/controller.go | 44 ++++++++++++++++++++++++++++++---- pkg/components/auth/controller_test.go | 19 ++++++++------- pkg/components/errors.go | 8 +++++++ pkg/components/media/model.go | 5 ++++ pkg/components/user/model.go | 3 ++- pkg/database/sql/user.go | 15 ++++++++++++ pkg/database/sql/user_test.go | 20 ++++++++-------- pkg/ext/middleware.go | 42 +++++++++++++++++++++++++++++++- pkg/view/auth.go | 21 ++++++++++++++++ templates/login.html | 2 +- templates/media.html | 8 ++++++- templates/register.html | 28 ++++++++++++++++++++++ 13 files changed, 202 insertions(+), 41 deletions(-) create mode 100644 pkg/components/errors.go create mode 100644 templates/register.html diff --git a/cmd/server/main.go b/cmd/server/main.go index f58366f..4942ac3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -72,16 +72,6 @@ func main() { r := router.New() r.GET("/static/{filepath:*}", ext.FileServer(img.StaticFS, "static/")) - authMiddleware := ext.NewAuthMiddleware(hexKey, logger.WithField("context", "auth")) - logMiddleware := ext.NewLogMiddleare(logger.WithField("context", "http")) - - extRouter := ext.NewRouter(r) - extRouter.AddMiddleware(logMiddleware.HTTP) - extRouter.AddMiddleware(authMiddleware.LoggedIn) - extRouter.AddMiddleware(ext.HTML) - - scheduler := worker.NewScheduler(*schedulerCount) - // repository var ( userRepository = sql.NewUserRepository(db) @@ -90,12 +80,24 @@ func main() { mediaRepository = sql.NewMediaRepository(db) ) - //TODO: remove later - userRepository.EnsureAdmin(context.Background()) + // middleware + var ( + authMiddleware = ext.NewAuthMiddleware(hexKey, logger.WithField("context", "auth")) + logMiddleware = ext.NewLogMiddleare(logger.WithField("context", "http")) + initialMiddleware = ext.NewInitialSetupMiddleware(userRepository) + ) + + extRouter := ext.NewRouter(r) + extRouter.AddMiddleware(ext.HTML) + extRouter.AddMiddleware(initialMiddleware.Check) + extRouter.AddMiddleware(authMiddleware.LoggedIn) + extRouter.AddMiddleware(logMiddleware.HTTP) + + scheduler := worker.NewScheduler(*schedulerCount) // controller var ( - userController = auth.NewController(userRepository, hexKey) + userController = auth.NewController(userRepository, userRepository, hexKey) fileSystemController = filesystem.NewController(fileSystemRepository) ) diff --git a/pkg/components/auth/controller.go b/pkg/components/auth/controller.go index a81a1c0..2f30fb5 100644 --- a/pkg/components/auth/controller.go +++ b/pkg/components/auth/controller.go @@ -5,18 +5,26 @@ import ( "golang.org/x/crypto/bcrypt" + "git.sr.ht/~gabrielgio/img/pkg/components" + "git.sr.ht/~gabrielgio/img/pkg/components/user" "git.sr.ht/~gabrielgio/img/pkg/ext" ) type Controller struct { - repository Repository - key []byte + repository Repository + userRepository user.Repository + key []byte } -func NewController(repository Repository, key []byte) *Controller { +func NewController( + repository Repository, + userRepository user.Repository, + key []byte, +) *Controller { return &Controller{ - repository: repository, - key: key, + repository: repository, + userRepository: userRepository, + key: key, } } @@ -41,3 +49,29 @@ func (c *Controller) Login(ctx context.Context, username, password []byte) ([]by } return ext.WriteToken(token, c.key) } + +// InitialRegister register a initial user, it will validate if there is another +// user stored already. If so an error `InvlidaInput` will be returned +func (c *Controller) InitialRegister(ctx context.Context, username, password []byte, path []byte) error { + exist, err := c.userRepository.Any(ctx) + if err != nil { + return err + } + + if exist { + return components.InvlidaInput + } + + hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost) + if err != nil { + return err + } + + err = c.userRepository.Create(ctx, &user.CreateUser{ + Username: string(username), + Password: hash, + Path: string(path), + }) + + return err +} diff --git a/pkg/components/auth/controller_test.go b/pkg/components/auth/controller_test.go index 33aa901..6b4e3cd 100644 --- a/pkg/components/auth/controller_test.go +++ b/pkg/components/auth/controller_test.go @@ -9,6 +9,7 @@ import ( "github.com/samber/lo" + "git.sr.ht/~gabrielgio/img/pkg/components/user" "git.sr.ht/~gabrielgio/img/pkg/ext" "git.sr.ht/~gabrielgio/img/pkg/testkit" ) @@ -43,11 +44,11 @@ func setUp() *scene { return &scene{ ctx: context.Background(), mockRepository: mockUserRepository, - controller: *NewController(mockUserRepository, key), + controller: *NewController(mockUserRepository, nil, key), } } -func TestRegisterAndLogin(t *testing.T) { +func TestInitialRegisterAndLogin(t *testing.T) { testCases := []struct { name string username string @@ -64,7 +65,7 @@ func TestRegisterAndLogin(t *testing.T) { t.Run(tc.name, func(t *testing.T) { scene := setUp() - err := scene.controller.Register(scene.ctx, []byte(tc.username), tc.password) + err := scene.controller.InitialRegister(scene.ctx, []byte(tc.username), tc.password, []byte("/")) testkit.TestFatalError(t, "Register", err) userID := scene.mockRepository.GetLastId() @@ -85,8 +86,8 @@ func TestRegisterAndLogin(t *testing.T) { } } -func toUser(m *mockUser, _ int) *User { - return &User{ +func toUser(m *mockUser, _ int) *user.User { + return &user.User{ ID: m.id, Username: m.username, } @@ -96,7 +97,7 @@ func (m *MockUserRepository) GetLastId() uint { return m.index } -func (m *MockUserRepository) List(ctx context.Context) ([]*User, error) { +func (m *MockUserRepository) List(ctx context.Context) ([]*user.User, error) { if m.err != nil { return nil, m.err } @@ -104,7 +105,7 @@ func (m *MockUserRepository) List(ctx context.Context) ([]*User, error) { return lo.Map(m.users, toUser), nil } -func (m *MockUserRepository) Get(ctx context.Context, id uint) (*User, error) { +func (m *MockUserRepository) Get(ctx context.Context, id uint) (*user.User, error) { if m.err != nil { return nil, m.err } @@ -143,7 +144,7 @@ func (m *MockUserRepository) GetPassword(ctx context.Context, id uint) ([]byte, return nil, errors.New("Item not found") } -func (m *MockUserRepository) Create(ctx context.Context, createUser *CreateUser) (uint, error) { +func (m *MockUserRepository) Create(ctx context.Context, createUser *user.CreateUser) (uint, error) { if m.err != nil { return 0, m.err } @@ -159,7 +160,7 @@ func (m *MockUserRepository) Create(ctx context.Context, createUser *CreateUser) return m.index, nil } -func (m *MockUserRepository) Update(ctx context.Context, id uint, update *UpdateUser) error { +func (m *MockUserRepository) Update(ctx context.Context, id uint, update *user.UpdateUser) error { if m.err != nil { return m.err } diff --git a/pkg/components/errors.go b/pkg/components/errors.go new file mode 100644 index 0000000..aedbe88 --- /dev/null +++ b/pkg/components/errors.go @@ -0,0 +1,8 @@ +package components + +import "errors" + +var ( + NotFound = errors.New("Not found") + InvlidaInput = errors.New("Invalid Input") +) diff --git a/pkg/components/media/model.go b/pkg/components/media/model.go index 0e17e92..1962a23 100644 --- a/pkg/components/media/model.go +++ b/pkg/components/media/model.go @@ -2,6 +2,7 @@ package media import ( "context" + "strings" "time" ) @@ -57,3 +58,7 @@ type ( CreateEXIF(context.Context, uint, *MediaEXIF) error } ) + +func (m *Media) IsVideo() bool { + return strings.HasPrefix(m.MIMEType, "video") +} diff --git a/pkg/components/user/model.go b/pkg/components/user/model.go index f957c39..ce1b3a5 100644 --- a/pkg/components/user/model.go +++ b/pkg/components/user/model.go @@ -20,7 +20,7 @@ type ( CreateUser struct { Username string Name string - Password string + Password []byte IsAdmin bool Path string } @@ -29,5 +29,6 @@ type ( List(ctx context.Context) ([]*User, error) Create(ctx context.Context, createUser *CreateUser) error Update(ctx context.Context, id uint, updateUser *UpdateUser) error + Any(ctx context.Context) (bool, error) } ) diff --git a/pkg/database/sql/user.go b/pkg/database/sql/user.go index 2d74162..a02b67b 100644 --- a/pkg/database/sql/user.go +++ b/pkg/database/sql/user.go @@ -187,3 +187,18 @@ func (self *UserRepository) Delete(ctx context.Context, id uint) error { } return nil } + +func (u *UserRepository) Any(ctx context.Context) (bool, error) { + var exists bool + result := u.db. + WithContext(ctx). + Model(&User{}). + Select("count(id) > 0"). + Find(&exists) + + if result.Error != nil { + return false, result.Error + } + + return exists, nil +} diff --git a/pkg/database/sql/user_test.go b/pkg/database/sql/user_test.go index 875b8e6..473ce03 100644 --- a/pkg/database/sql/user_test.go +++ b/pkg/database/sql/user_test.go @@ -12,7 +12,7 @@ import ( "gorm.io/gorm" "gorm.io/gorm/logger" - "git.sr.ht/~gabrielgio/img/pkg/components/auth" + "git.sr.ht/~gabrielgio/img/pkg/components/user" ) func setup(t *testing.T) (*gorm.DB, func()) { @@ -48,7 +48,7 @@ func TestCreate(t *testing.T) { repository := NewUserRepository(db) - id, err := repository.Create(context.Background(), &auth.CreateUser{ + err := repository.Create(context.Background(), &user.CreateUser{ Username: "new_username", Name: "new_name", }) @@ -56,12 +56,12 @@ func TestCreate(t *testing.T) { t.Fatalf("Error creating: %s", err.Error()) } - got, err := repository.Get(context.Background(), id) + got, err := repository.Get(context.Background(), 1) if err != nil { t.Fatalf("Error getting: %s", err.Error()) } - want := &auth.User{ - ID: id, + want := &user.User{ + ID: 1, Username: "new_username", Name: "new_name", } @@ -78,7 +78,7 @@ func TestUpdate(t *testing.T) { repository := NewUserRepository(db) - id, err := repository.Create(context.Background(), &auth.CreateUser{ + err := repository.Create(context.Background(), &user.CreateUser{ Username: "username", Name: "name", }) @@ -86,7 +86,7 @@ func TestUpdate(t *testing.T) { t.Fatalf("Error creating user: %s", err.Error()) } - err = repository.Update(context.Background(), id, &auth.UpdateUser{ + err = repository.Update(context.Background(), 1, &user.UpdateUser{ Username: "new_username", Name: "new_name", }) @@ -94,12 +94,12 @@ func TestUpdate(t *testing.T) { t.Fatalf("Error update user: %s", err.Error()) } - got, err := repository.Get(context.Background(), id) + got, err := repository.Get(context.Background(), 1) if err != nil { t.Fatalf("Error getting user: %s", err.Error()) } - want := &auth.User{ - ID: id, + want := &user.User{ + ID: 1, Username: "new_username", Name: "new_name", } diff --git a/pkg/ext/middleware.go b/pkg/ext/middleware.go index 771c0ac..649272e 100644 --- a/pkg/ext/middleware.go +++ b/pkg/ext/middleware.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "time" + "git.sr.ht/~gabrielgio/img/pkg/components/user" "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" ) @@ -54,7 +55,7 @@ func NewAuthMiddleware(key []byte, log *logrus.Entry) *AuthMiddleware { func (a *AuthMiddleware) LoggedIn(next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) - if path == "/login" { + if path == "/login" || path == "/initial" { next(ctx) return } @@ -87,3 +88,42 @@ func (a *AuthMiddleware) LoggedIn(next fasthttp.RequestHandler) fasthttp.Request next(ctx) } } + +type InitialSetupMiddleware struct { + userRepository user.Repository +} + +func NewInitialSetupMiddleware(userRepository user.Repository) *InitialSetupMiddleware { + return &InitialSetupMiddleware{ + userRepository: userRepository, + } +} + +func (i *InitialSetupMiddleware) Check(next fasthttp.RequestHandler) fasthttp.RequestHandler { + return func(ctx *fasthttp.RequestCtx) { + // if user has been set to context it is logged in already + _, ok := ctx.UserValue("token").(*Token) + if ok { + next(ctx) + return + } + + path := string(ctx.Path()) + if path == "/initial" { + next(ctx) + return + } + + exists, err := i.userRepository.Any(ctx) + if err != nil { + InternalServerError(ctx, err) + return + } + + if exists { + next(ctx) + return + } + ctx.Redirect("/initial", 307) + } +} diff --git a/pkg/view/auth.go b/pkg/view/auth.go index d44424d..3f9e414 100644 --- a/pkg/view/auth.go +++ b/pkg/view/auth.go @@ -68,10 +68,31 @@ func Index(ctx *fasthttp.RequestCtx) { ctx.Redirect("/login", 307) } +func (v *AuthView) InitialRegisterView(ctx *fasthttp.RequestCtx) error { + return img.Render[interface{}](ctx, "register.html", nil) +} + +func (v *AuthView) InitialRegister(ctx *fasthttp.RequestCtx) error { + username := ctx.FormValue("username") + password := ctx.FormValue("password") + path := ctx.FormValue("path") + + err := v.userController.InitialRegister(ctx, username, password, path) + if err != nil { + return err + } + + ctx.Redirect("/login", 307) + return nil +} + func (v *AuthView) SetMyselfIn(r *ext.Router) { r.GET("/login", v.LoginView) r.POST("/login", v.Login) r.GET("/logout", v.Logout) r.POST("/logout", v.Logout) + + r.GET("/initial", v.InitialRegisterView) + r.POST("/initial", v.InitialRegister) } diff --git a/templates/login.html b/templates/login.html index f71d9d3..607faa1 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,5 +1,5 @@ {{template "layout.html" .}} -{{define "title"}} Register {{end}} +{{define "title"}} Login {{end}} {{define "content"}}
diff --git a/templates/media.html b/templates/media.html index 478d8ae..6302a57 100644 --- a/templates/media.html +++ b/templates/media.html @@ -5,9 +5,15 @@ {{range .Data.Medias}}
+ {{ if .IsVideo }} + + {{ else }}
- +
+ {{ end }}
{{end}} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..b026d33 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,28 @@ +{{template "layout.html" .}} +{{define "title"}} Initial Setup {{end}} +{{define "content"}} +

Initial Setup

+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +{{end}} -- cgit v1.2.3