lens @ 4d930c0c8cb585979798fac2bb254f991faa62fb

feat: Add initial user setup
diff --git a/cmd/server/main.go b/cmd/server/main.go
index f58366ff418564305b29238e67f5b6314b10e249..4942ac31216717bc8c568944e409c69bb93ea521 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -72,16 +72,6 @@
 	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 @@ 		fileSystemRepository = localfs.NewFileSystemRepository(*root)
 		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 a81a1c0f802acc5a4c90bcc400d2d9f913c73b9b..2f30fb5d1027286fc809f6a683bc42416ed86db3 100644
--- a/pkg/components/auth/controller.go
+++ b/pkg/components/auth/controller.go
@@ -5,18 +5,26 @@ 	"context"
 
 	"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 @@ 		Username: string(username),
 	}
 	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 33aa901cf701dcc8675776c97f36b57e7a202882..6b4e3cd03ecdb792c56141c5e68ba1a53b343815 100644
--- a/pkg/components/auth/controller_test.go
+++ b/pkg/components/auth/controller_test.go
@@ -9,6 +9,7 @@ 	"testing"
 
 	"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 @@ 	mockUserRepository := &MockUserRepository{}
 	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 @@ 	for _, tc := range testCases {
 		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 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 @@
 	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 @@ 	}
 	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 @@
 	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 0000000000000000000000000000000000000000..aedbe889b5f1e2d54616f746b859da359a288be5
--- /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 0e17e9231f4f7ded99d4d6fa6e866f34756a5514..1962a23e2716418d3919b66d253cdbbfc46d2de3 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 @@ 		GetEXIF(context.Context, uint) (*MediaEXIF, error)
 		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 f957c39a95fb49179c319674a6c31827bebb695c..ce1b3a5986e2782ab36ae08dd05f02862f2d01a4 100644
--- a/pkg/components/user/model.go
+++ b/pkg/components/user/model.go
@@ -20,7 +20,7 @@
 	CreateUser struct {
 		Username string
 		Name     string
-		Password string
+		Password []byte
 		IsAdmin  bool
 		Path     string
 	}
@@ -29,5 +29,6 @@ 	Repository interface {
 		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 2d74162713c10e1ed292a1dc116404e862f77bd3..a02b67b669b46c634051daf69000a5a9044a0336 100644
--- a/pkg/database/sql/user.go
+++ b/pkg/database/sql/user.go
@@ -187,3 +187,18 @@ 		return result.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 875b8e6022b0b97c1fbe0b6c0363a4beb12912df..473ce032516e298768da908b7e64f84e4f705bce 100644
--- a/pkg/database/sql/user_test.go
+++ b/pkg/database/sql/user_test.go
@@ -12,7 +12,7 @@ 	"gorm.io/driver/sqlite"
 	"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 @@ 	defer tearDown()
 
 	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 @@ 	if err != nil {
 		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 @@ 	defer tearDown()
 
 	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 @@ 	if err != nil {
 		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 @@ 	if err != nil {
 		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 771c0ac6cf729661c1a6c33967910d908c1411bc..649272e6714bfd5f0f85355c4f6b439eb1e6620f 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 (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 @@ 			Info("user recognized")
 		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 d44424d64290d5877761bcc1229b9244551ca08b..3f9e4140365dbed89f490c6d39e066670fc3d1b4 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 f71d9d3d20b0ed31661aa7ccffc1f047c69c17cd..607faa101d1594e66bbf7c729880e4e42e80d25d 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"}}
 <form action="/login" method="post">
     <div class="field">
diff --git a/templates/media.html b/templates/media.html
index 478d8aee45db8dd3950fd6d9876aaef035dcd82d..6302a573451a953b2a02275611a63377930d0db3 100644
--- a/templates/media.html
+++ b/templates/media.html
@@ -5,9 +5,15 @@ <div class="columns is-multiline">
 {{range .Data.Medias}}
 <div class="card">
     <div class="card-image">
+        {{ if .IsVideo }}
+        <video controls muted="true" preload="metadata">
+            <source src="/media/image?path_hash={{.PathHash}}" type="{{.MIMEType}}">
+        </video>
+        {{ else }}
         <figure class="image is-fit">
-            <img   src="/media/image?path_hash={{.PathHash}}">
+            <img src="/media/image?path_hash={{.PathHash}}">
         </figure>
+        {{ end }}
     </div>
 </div>
 {{end}}
diff --git a/templates/register.html b/templates/register.html
new file mode 100644
index 0000000000000000000000000000000000000000..b026d33a60605574f7c71e79bd5fc8b555e94eb9
--- /dev/null
+++ b/templates/register.html
@@ -0,0 +1,28 @@
+{{template "layout.html" .}}
+{{define "title"}} Initial Setup {{end}}
+{{define "content"}}
+<h1>Initial Setup</h1>
+<form action="/initial" method="post">
+    <div class="field">
+        <label class="label">Username</label>
+        <div class="control">
+            <input class="input" name="username" type="text">
+        </div>
+    </div>
+    <div class="field">
+        <label class="label">Password</label>
+        <div class="control">
+            <input class="input" name="password" type="password">
+        </div>
+    </div>
+    <div class="field">
+        <label class="label">Root folder</label>
+        <div class="control">
+            <input class="input" name="path" type="text">
+        </div>
+    </div>
+    <div class="field">
+        <input class="button is-pulled-right" value="Save" type="submit">
+    </div>
+</form>
+{{end}}