lens @ 3b9d27649a31e5af3fb137ff5b3378e7b8f7b9ae

feat: Add user management

As many things it is on crude state. The settings.go has become a big
mess, but I have achieve MVP, so from now one things shall improve as
I'll spent more time on refactoring.
diff --git a/Makefile b/Makefile
index bd7d016ebd52fe2cb834d19008208934681bd059..d9e89cca6601e7281ed1a2ae6c89915e313d872c 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,8 @@ 		-I scss scss/main.scss static/main.css \
 		--style compressed
 
 tmpl:
-	qtc -dir=./templates
+	cd ./templates && \
+	qtc *
 
 test: test.unit test.integration
 
diff --git a/cmd/server/main.go b/cmd/server/main.go
index c7f901914667d673201e36568756c6201240f863..385c54f554a135737ec038c895fb6f32db917e2a 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -107,7 +107,7 @@ 	// view
 	for _, v := range []view.View{
 		view.NewAuthView(userController),
 		view.NewFileSystemView(*fileSystemController, settingsRepository),
-		view.NewSettingsView(settingsRepository, userRepository),
+		view.NewSettingsView(settingsRepository, userController),
 		view.NewMediaView(mediaRepository, userRepository, settingsRepository),
 	} {
 		v.SetMyselfIn(extRouter)
diff --git a/pkg/database/repository/user.go b/pkg/database/repository/user.go
index 358900782c1a41ec5bd7f0d068c03c7b1d80edfd..5b2e3869a6e3d2be7ffb5c0c947dc448cccf6a92 100644
--- a/pkg/database/repository/user.go
+++ b/pkg/database/repository/user.go
@@ -14,7 +14,8 @@
 	UpdateUser struct {
 		Username string
 		Name     string
-		Password string
+		IsAdmin  bool
+		Path     string
 	}
 
 	CreateUser struct {
@@ -31,6 +32,8 @@ 		GetPathFromUserID(ctx context.Context, id uint) (string, error)
 		List(ctx context.Context) ([]*User, error)
 		Create(ctx context.Context, createUser *CreateUser) (uint, error)
 		Update(ctx context.Context, id uint, updateUser *UpdateUser) error
+		Delete(ctx context.Context, id uint) error
+		UpdatePassword(ctx context.Context, id uint, password []byte) error
 		Any(ctx context.Context) (bool, error)
 	}
 )
diff --git a/pkg/database/sql/user.go b/pkg/database/sql/user.go
index 15dbe727b355e545be8aa1eb0fff93220d505b28..6b1cf0fd16436f1cbce0e4c0c94d23d372ec31b9 100644
--- a/pkg/database/sql/user.go
+++ b/pkg/database/sql/user.go
@@ -27,6 +27,7 @@ 	}
 )
 
 var _ repository.UserRepository = &UserRepository{}
+
 var _ repository.AuthRepository = &UserRepository{}
 
 func NewUserRepository(db *gorm.DB) *UserRepository {
@@ -141,6 +142,7 @@ 	user := &User{
 		Username: createUser.Username,
 		Name:     createUser.Name,
 		Path:     createUser.Path,
+		IsAdmin:  createUser.IsAdmin,
 		Password: string(createUser.Password),
 	}
 
@@ -161,11 +163,14 @@ 			ID: id,
 		},
 		Username: update.Username,
 		Name:     update.Name,
+		IsAdmin:  update.IsAdmin,
+		Path:     update.Path,
 	}
 
 	result := self.db.
 		WithContext(ctx).
-		Save(user)
+		Omit("password").
+		Updates(user)
 	if result.Error != nil {
 		return result.Error
 	}
@@ -174,14 +179,15 @@ 	return nil
 }
 
 func (self *UserRepository) Delete(ctx context.Context, id uint) error {
-	userID := struct {
-		ID uint
-	}{
-		ID: id,
+	user := &User{
+		Model: gorm.Model{
+			ID: id,
+		},
 	}
+
 	result := self.db.
 		WithContext(ctx).
-		Delete(userID)
+		Delete(user)
 	if result.Error != nil {
 		return result.Error
 	}
@@ -219,3 +225,13 @@ 	}
 
 	return userPath, nil
 }
+
+func (u *UserRepository) UpdatePassword(ctx context.Context, id uint, password []byte) error {
+	result := u.db.
+		WithContext(ctx).
+		Model(&User{}).
+		Where("id = ?", id).
+		Update("password", password)
+
+	return result.Error
+}
diff --git a/pkg/service/auth.go b/pkg/service/auth.go
index 1966e702241f86248aeb5101a1d2463bfa280a01..f27cf885dc73e0bd3b3ff706a129388ea0b65a9a 100644
--- a/pkg/service/auth.go
+++ b/pkg/service/auth.go
@@ -82,6 +82,64 @@
 	return err
 }
 
+func (u *AuthController) List(ctx context.Context) ([]*repository.User, error) {
+	return u.userRepository.List(ctx)
+}
+
+func (u *AuthController) Get(ctx context.Context, id uint) (*repository.User, error) {
+	return u.userRepository.Get(ctx, id)
+}
+
+func (u *AuthController) Delete(ctx context.Context, id uint) error {
+	return u.userRepository.Delete(ctx, id)
+}
+
+func (u *AuthController) Upsert(
+	ctx context.Context,
+	id *uint,
+	username string,
+	name string,
+	password []byte,
+	isAdmin bool,
+	path string,
+) error {
+	if id != nil {
+		if err := u.userRepository.Update(ctx, *id, &repository.UpdateUser{
+			Username: string(username),
+			Name:     name,
+			IsAdmin:  isAdmin,
+			Path:     path,
+		}); err != nil {
+			return err
+		}
+
+		if len(password) > 0 {
+			hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
+			if err != nil {
+				return err
+			}
+
+			return u.userRepository.UpdatePassword(ctx, *id, hash)
+		}
+		return nil
+	}
+
+	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
+	if err != nil {
+		return err
+	}
+
+	_, err = u.userRepository.Create(ctx, &repository.CreateUser{
+		Username: username,
+		Name:     name,
+		Password: hash,
+		IsAdmin:  isAdmin,
+		Path:     path,
+	})
+
+	return err
+}
+
 type Token struct {
 	UserID   uint
 	Username string
diff --git a/pkg/service/main_test.go b/pkg/service/main_test.go
index e1214dccec1e7c9fcf99fd4697db2452b6400e07..8cef98525606ad0fd5873e93aee3b807f6bd3671 100644
--- a/pkg/service/main_test.go
+++ b/pkg/service/main_test.go
@@ -28,6 +28,7 @@ 	}
 )
 
 var _ repository.UserRepository = &UserRepository{}
+
 var _ repository.AuthRepository = &UserRepository{}
 
 func NewUserRepository() *UserRepository {
@@ -86,10 +87,6 @@ 	}
 
 	user.Name = updateUser.Name
 	user.Username = updateUser.Username
-	if updateUser.Password != "" {
-		user.Password = []byte(updateUser.Password)
-	}
-
 	return nil
 }
 
@@ -127,3 +124,11 @@ 	}
 
 	return "", errors.New("Not Found")
 }
+
+func (u *UserRepository) UpdatePassword(ctx context.Context, id uint, password []byte) error {
+	panic("not implemented") // TODO: Implement
+}
+
+func (u *UserRepository) Delete(ctx context.Context, id uint) error {
+	panic("not implemented") // TODO: Implement
+}
diff --git a/pkg/view/settings.go b/pkg/view/settings.go
index ffce86b2ab39bfd887d06d37d2f22dfc7b324d13..5131362e2ba129a8791a42a73ce732e91322f5c2 100644
--- a/pkg/view/settings.go
+++ b/pkg/view/settings.go
@@ -1,10 +1,13 @@
 package view
 
 import (
+	"strconv"
+
 	"github.com/valyala/fasthttp"
 
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/ext"
+	"git.sr.ht/~gabrielgio/img/pkg/service"
 	"git.sr.ht/~gabrielgio/img/templates"
 )
 
@@ -12,17 +15,17 @@ type (
 	SettingsView struct {
 		// there is not need to create a controller for this
 		settingsRepository repository.SettingsRepository
-		userRepository     repository.UserRepository
+		userController     *service.AuthController
 	}
 )
 
 func NewSettingsView(
 	settingsRespository repository.SettingsRepository,
-	userRepository repository.UserRepository,
+	userController *service.AuthController,
 ) *SettingsView {
 	return &SettingsView{
 		settingsRepository: settingsRespository,
-		userRepository:     userRepository,
+		userController:     userController,
 	}
 }
 
@@ -32,7 +35,7 @@ 	if err != nil {
 		return err
 	}
 
-	users, err := self.userRepository.List(ctx)
+	users, err := self.userController.List(ctx)
 	if err != nil {
 		return err
 	}
@@ -45,6 +48,76 @@
 	return nil
 }
 
+func (self *SettingsView) User(ctx *fasthttp.RequestCtx) error {
+	id := string(ctx.FormValue("userId"))
+	idValue, err := ParseUint(id)
+	if err != nil {
+		return err
+	}
+
+	if idValue == nil {
+		templates.WritePageTemplate(ctx, &templates.UserPage{})
+	} else {
+		user, err := self.userController.Get(ctx, *idValue)
+		if err != nil {
+			return err
+		}
+
+		templates.WritePageTemplate(ctx, &templates.UserPage{
+			ID:       idValue,
+			Username: user.Username,
+			Path:     user.Path,
+			IsAdmin:  user.IsAdmin,
+		})
+	}
+
+	return nil
+}
+
+func (self *SettingsView) UpsertUser(ctx *fasthttp.RequestCtx) error {
+	var (
+		username = string(ctx.FormValue("username"))
+		password = ctx.FormValue("password")
+		path     = string(ctx.FormValue("path"))
+		isAdmin  = string(ctx.FormValue("isAdmin")) == "on"
+		id       = string(ctx.FormValue("userId"))
+	)
+
+	idValue, err := ParseUint(id)
+	if err != nil {
+		return err
+	}
+
+	err = self.userController.Upsert(ctx, idValue, username, "", password, isAdmin, path)
+	if err != nil {
+		return err
+	}
+
+	ctx.Redirect("/settings", 307)
+	return nil
+}
+
+func (self *SettingsView) Delete(ctx *fasthttp.RequestCtx) error {
+	var (
+		id = string(ctx.FormValue("userId"))
+	)
+
+	idValue, err := ParseUint(id)
+	if err != nil {
+		return err
+	}
+
+	if idValue != nil {
+		err = self.userController.Delete(ctx, *idValue)
+		if err != nil {
+			return err
+		}
+	}
+
+	ctx.Redirect("/settings", 307)
+	return nil
+}
+
 func (self *SettingsView) Save(ctx *fasthttp.RequestCtx) error {
 	var (
 		showMode             = string(ctx.FormValue("showMode")) == "on"
@@ -67,4 +140,22 @@
 func (self *SettingsView) SetMyselfIn(r *ext.Router) {
 	r.GET("/settings/", self.Index)
 	r.POST("/settings/", self.Save)
+
+	r.GET("/users/", self.User)
+	r.GET("/users/delete", self.Delete)
+	r.POST("/users/", self.UpsertUser)
+}
+
+func ParseUint(id string) (*uint, error) {
+	var idValue *uint
+	if id != "" {
+		v, err := strconv.Atoi(id)
+		if err != nil {
+			return nil, err
+		}
+
+		u := uint(v)
+		idValue = &u
+	}
+	return idValue, nil
 }
diff --git a/templates/base.qtpl b/templates/base.qtpl
index 0c0578234dc0fcf545c94664b8c4304cc67ed23b..5a7c3b769e7767d77cb6fe18985a98d41d5e39f2 100644
--- a/templates/base.qtpl
+++ b/templates/base.qtpl
@@ -1,11 +1,23 @@
 This is a base page template. All the other template pages implement this interface.
 
+{% import "strconv" %}
+
 {% interface
 Page {
 	Title()
 	Content()
     Script()
 }
+
+%}
+
+{% code 
+    func FromUInttoString(u *uint) string {
+        if u != nil {
+            return strconv.FormatUint(uint64(*u), 10)
+        }
+        return ""
+    }
 %}
 
 
diff --git a/templates/settings.qtpl b/templates/settings.qtpl
index 6eee1abd3d87e60ae598ea50651c5c04992932ec..4207439c7d9d685f429ba3298ff3f23383a75581 100644
--- a/templates/settings.qtpl
+++ b/templates/settings.qtpl
@@ -52,10 +52,13 @@                 </div>
                 <div class="column">
                     {%s user.Path %}
                 </div>
-                <div class="column  has-text-right"><a href="#">Edit</a> / <a href="#" class="is-danger">Delete</a></div>
+                <div class="column  has-text-right"><a href="/users?userId={%s FromUInttoString(&user.ID) %}">Edit</a> <a href="/users/delete?userId={%s FromUInttoString(&user.ID) %}" class="is-danger">Delete</a></div>
             </div>
         </div>
     {% endfor %}
+        <div class="field">
+            <a href="/users/" class="button">Create</a>
+        </div>
     </div>
 </div>
 {% endfunc %}
diff --git a/templates/user.qtpl b/templates/user.qtpl
new file mode 100644
index 0000000000000000000000000000000000000000..ba2f071537abbcc25f8ec2d3fc1eb18f05b9ac1c
--- /dev/null
+++ b/templates/user.qtpl
@@ -0,0 +1,54 @@
+
+{% code
+type UserPage struct {
+    ID *uint
+    Username string
+    Path string
+    IsAdmin bool
+}
+
+%}
+
+{% func (p *UserPage) Title() %}Login{% endfunc %}
+
+{% func (p *UserPage) Content() %}
+<h1>Initial Setup</h1>
+<form action="/users/" method="post">
+    {% if p.ID != nil %} 
+    <input type="hidden" name="userId" value="{%s FromUInttoString(p.ID) %}" />
+    {% endif %}
+    <div class="field">
+        <label class="label">Username</label>
+        <div class="control">
+            <input class="input" name="username" type="text" value="{%s p.Username %}">
+        </div>
+    </div>
+
+    {% if p.ID == nil %} 
+    <div class="field">
+        <label class="label">Password</label>
+        <div class="control">
+            <input class="input" name="password" type="password">
+        </div>
+    </div>
+    {% endif %}
+    <div class="field">
+        <label class="label">Root folder</label>
+        <div class="control">
+            <input class="input" name="path" type="text" value="{%s p.Path %}">
+        </div>
+    </div>
+    <div class="field">
+        <label class="label">Is Admin?</label>
+        <div class="control">
+            <input type="checkbox" name="isAdmin" type="password" {% if p.IsAdmin %}checked{% endif %}>
+        </div>
+    </div>
+    <div class="field">
+        <input class="button is-pulled-right" value="Save" type="submit">
+    </div>
+</form>
+{% endfunc %}
+
+{% func (p *UserPage) Script() %}
+{% endfunc %}