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.
  1diff --git a/Makefile b/Makefile
  2index bd7d016ebd52fe2cb834d19008208934681bd059..d9e89cca6601e7281ed1a2ae6c89915e313d872c 100644
  3--- a/Makefile
  4+++ b/Makefile
  5@@ -30,7 +30,8 @@ 		-I scss scss/main.scss static/main.css \
  6 		--style compressed
  7 
  8 tmpl:
  9-	qtc -dir=./templates
 10+	cd ./templates && \
 11+	qtc *
 12 
 13 test: test.unit test.integration
 14 
 15diff --git a/cmd/server/main.go b/cmd/server/main.go
 16index c7f901914667d673201e36568756c6201240f863..385c54f554a135737ec038c895fb6f32db917e2a 100644
 17--- a/cmd/server/main.go
 18+++ b/cmd/server/main.go
 19@@ -107,7 +107,7 @@ 	// view
 20 	for _, v := range []view.View{
 21 		view.NewAuthView(userController),
 22 		view.NewFileSystemView(*fileSystemController, settingsRepository),
 23-		view.NewSettingsView(settingsRepository, userRepository),
 24+		view.NewSettingsView(settingsRepository, userController),
 25 		view.NewMediaView(mediaRepository, userRepository, settingsRepository),
 26 	} {
 27 		v.SetMyselfIn(extRouter)
 28diff --git a/pkg/database/repository/user.go b/pkg/database/repository/user.go
 29index 358900782c1a41ec5bd7f0d068c03c7b1d80edfd..5b2e3869a6e3d2be7ffb5c0c947dc448cccf6a92 100644
 30--- a/pkg/database/repository/user.go
 31+++ b/pkg/database/repository/user.go
 32@@ -14,7 +14,8 @@
 33 	UpdateUser struct {
 34 		Username string
 35 		Name     string
 36-		Password string
 37+		IsAdmin  bool
 38+		Path     string
 39 	}
 40 
 41 	CreateUser struct {
 42@@ -31,6 +32,8 @@ 		GetPathFromUserID(ctx context.Context, id uint) (string, error)
 43 		List(ctx context.Context) ([]*User, error)
 44 		Create(ctx context.Context, createUser *CreateUser) (uint, error)
 45 		Update(ctx context.Context, id uint, updateUser *UpdateUser) error
 46+		Delete(ctx context.Context, id uint) error
 47+		UpdatePassword(ctx context.Context, id uint, password []byte) error
 48 		Any(ctx context.Context) (bool, error)
 49 	}
 50 )
 51diff --git a/pkg/database/sql/user.go b/pkg/database/sql/user.go
 52index 15dbe727b355e545be8aa1eb0fff93220d505b28..6b1cf0fd16436f1cbce0e4c0c94d23d372ec31b9 100644
 53--- a/pkg/database/sql/user.go
 54+++ b/pkg/database/sql/user.go
 55@@ -27,6 +27,7 @@ 	}
 56 )
 57 
 58 var _ repository.UserRepository = &UserRepository{}
 59+
 60 var _ repository.AuthRepository = &UserRepository{}
 61 
 62 func NewUserRepository(db *gorm.DB) *UserRepository {
 63@@ -141,6 +142,7 @@ 	user := &User{
 64 		Username: createUser.Username,
 65 		Name:     createUser.Name,
 66 		Path:     createUser.Path,
 67+		IsAdmin:  createUser.IsAdmin,
 68 		Password: string(createUser.Password),
 69 	}
 70 
 71@@ -161,11 +163,14 @@ 			ID: id,
 72 		},
 73 		Username: update.Username,
 74 		Name:     update.Name,
 75+		IsAdmin:  update.IsAdmin,
 76+		Path:     update.Path,
 77 	}
 78 
 79 	result := self.db.
 80 		WithContext(ctx).
 81-		Save(user)
 82+		Omit("password").
 83+		Updates(user)
 84 	if result.Error != nil {
 85 		return result.Error
 86 	}
 87@@ -174,14 +179,15 @@ 	return nil
 88 }
 89 
 90 func (self *UserRepository) Delete(ctx context.Context, id uint) error {
 91-	userID := struct {
 92-		ID uint
 93-	}{
 94-		ID: id,
 95+	user := &User{
 96+		Model: gorm.Model{
 97+			ID: id,
 98+		},
 99 	}
100+
101 	result := self.db.
102 		WithContext(ctx).
103-		Delete(userID)
104+		Delete(user)
105 	if result.Error != nil {
106 		return result.Error
107 	}
108@@ -219,3 +225,13 @@ 	}
109 
110 	return userPath, nil
111 }
112+
113+func (u *UserRepository) UpdatePassword(ctx context.Context, id uint, password []byte) error {
114+	result := u.db.
115+		WithContext(ctx).
116+		Model(&User{}).
117+		Where("id = ?", id).
118+		Update("password", password)
119+
120+	return result.Error
121+}
122diff --git a/pkg/service/auth.go b/pkg/service/auth.go
123index 1966e702241f86248aeb5101a1d2463bfa280a01..f27cf885dc73e0bd3b3ff706a129388ea0b65a9a 100644
124--- a/pkg/service/auth.go
125+++ b/pkg/service/auth.go
126@@ -82,6 +82,64 @@
127 	return err
128 }
129 
130+func (u *AuthController) List(ctx context.Context) ([]*repository.User, error) {
131+	return u.userRepository.List(ctx)
132+}
133+
134+func (u *AuthController) Get(ctx context.Context, id uint) (*repository.User, error) {
135+	return u.userRepository.Get(ctx, id)
136+}
137+
138+func (u *AuthController) Delete(ctx context.Context, id uint) error {
139+	return u.userRepository.Delete(ctx, id)
140+}
141+
142+func (u *AuthController) Upsert(
143+	ctx context.Context,
144+	id *uint,
145+	username string,
146+	name string,
147+	password []byte,
148+	isAdmin bool,
149+	path string,
150+) error {
151+	if id != nil {
152+		if err := u.userRepository.Update(ctx, *id, &repository.UpdateUser{
153+			Username: string(username),
154+			Name:     name,
155+			IsAdmin:  isAdmin,
156+			Path:     path,
157+		}); err != nil {
158+			return err
159+		}
160+
161+		if len(password) > 0 {
162+			hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
163+			if err != nil {
164+				return err
165+			}
166+
167+			return u.userRepository.UpdatePassword(ctx, *id, hash)
168+		}
169+		return nil
170+	}
171+
172+	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
173+	if err != nil {
174+		return err
175+	}
176+
177+	_, err = u.userRepository.Create(ctx, &repository.CreateUser{
178+		Username: username,
179+		Name:     name,
180+		Password: hash,
181+		IsAdmin:  isAdmin,
182+		Path:     path,
183+	})
184+
185+	return err
186+}
187+
188 type Token struct {
189 	UserID   uint
190 	Username string
191diff --git a/pkg/service/main_test.go b/pkg/service/main_test.go
192index e1214dccec1e7c9fcf99fd4697db2452b6400e07..8cef98525606ad0fd5873e93aee3b807f6bd3671 100644
193--- a/pkg/service/main_test.go
194+++ b/pkg/service/main_test.go
195@@ -28,6 +28,7 @@ 	}
196 )
197 
198 var _ repository.UserRepository = &UserRepository{}
199+
200 var _ repository.AuthRepository = &UserRepository{}
201 
202 func NewUserRepository() *UserRepository {
203@@ -86,10 +87,6 @@ 	}
204 
205 	user.Name = updateUser.Name
206 	user.Username = updateUser.Username
207-	if updateUser.Password != "" {
208-		user.Password = []byte(updateUser.Password)
209-	}
210-
211 	return nil
212 }
213 
214@@ -127,3 +124,11 @@ 	}
215 
216 	return "", errors.New("Not Found")
217 }
218+
219+func (u *UserRepository) UpdatePassword(ctx context.Context, id uint, password []byte) error {
220+	panic("not implemented") // TODO: Implement
221+}
222+
223+func (u *UserRepository) Delete(ctx context.Context, id uint) error {
224+	panic("not implemented") // TODO: Implement
225+}
226diff --git a/pkg/view/settings.go b/pkg/view/settings.go
227index ffce86b2ab39bfd887d06d37d2f22dfc7b324d13..5131362e2ba129a8791a42a73ce732e91322f5c2 100644
228--- a/pkg/view/settings.go
229+++ b/pkg/view/settings.go
230@@ -1,10 +1,13 @@
231 package view
232 
233 import (
234+	"strconv"
235+
236 	"github.com/valyala/fasthttp"
237 
238 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
239 	"git.sr.ht/~gabrielgio/img/pkg/ext"
240+	"git.sr.ht/~gabrielgio/img/pkg/service"
241 	"git.sr.ht/~gabrielgio/img/templates"
242 )
243 
244@@ -12,17 +15,17 @@ type (
245 	SettingsView struct {
246 		// there is not need to create a controller for this
247 		settingsRepository repository.SettingsRepository
248-		userRepository     repository.UserRepository
249+		userController     *service.AuthController
250 	}
251 )
252 
253 func NewSettingsView(
254 	settingsRespository repository.SettingsRepository,
255-	userRepository repository.UserRepository,
256+	userController *service.AuthController,
257 ) *SettingsView {
258 	return &SettingsView{
259 		settingsRepository: settingsRespository,
260-		userRepository:     userRepository,
261+		userController:     userController,
262 	}
263 }
264 
265@@ -32,7 +35,7 @@ 	if err != nil {
266 		return err
267 	}
268 
269-	users, err := self.userRepository.List(ctx)
270+	users, err := self.userController.List(ctx)
271 	if err != nil {
272 		return err
273 	}
274@@ -45,6 +48,76 @@
275 	return nil
276 }
277 
278+func (self *SettingsView) User(ctx *fasthttp.RequestCtx) error {
279+	id := string(ctx.FormValue("userId"))
280+	idValue, err := ParseUint(id)
281+	if err != nil {
282+		return err
283+	}
284+
285+	if idValue == nil {
286+		templates.WritePageTemplate(ctx, &templates.UserPage{})
287+	} else {
288+		user, err := self.userController.Get(ctx, *idValue)
289+		if err != nil {
290+			return err
291+		}
292+
293+		templates.WritePageTemplate(ctx, &templates.UserPage{
294+			ID:       idValue,
295+			Username: user.Username,
296+			Path:     user.Path,
297+			IsAdmin:  user.IsAdmin,
298+		})
299+	}
300+
301+	return nil
302+}
303+
304+func (self *SettingsView) UpsertUser(ctx *fasthttp.RequestCtx) error {
305+	var (
306+		username = string(ctx.FormValue("username"))
307+		password = ctx.FormValue("password")
308+		path     = string(ctx.FormValue("path"))
309+		isAdmin  = string(ctx.FormValue("isAdmin")) == "on"
310+		id       = string(ctx.FormValue("userId"))
311+	)
312+
313+	idValue, err := ParseUint(id)
314+	if err != nil {
315+		return err
316+	}
317+
318+	err = self.userController.Upsert(ctx, idValue, username, "", password, isAdmin, path)
319+	if err != nil {
320+		return err
321+	}
322+
323+	ctx.Redirect("/settings", 307)
324+	return nil
325+}
326+
327+func (self *SettingsView) Delete(ctx *fasthttp.RequestCtx) error {
328+	var (
329+		id = string(ctx.FormValue("userId"))
330+	)
331+
332+	idValue, err := ParseUint(id)
333+	if err != nil {
334+		return err
335+	}
336+
337+	if idValue != nil {
338+		err = self.userController.Delete(ctx, *idValue)
339+		if err != nil {
340+			return err
341+		}
342+	}
343+
344+	ctx.Redirect("/settings", 307)
345+	return nil
346+}
347+
348 func (self *SettingsView) Save(ctx *fasthttp.RequestCtx) error {
349 	var (
350 		showMode             = string(ctx.FormValue("showMode")) == "on"
351@@ -67,4 +140,22 @@
352 func (self *SettingsView) SetMyselfIn(r *ext.Router) {
353 	r.GET("/settings/", self.Index)
354 	r.POST("/settings/", self.Save)
355+
356+	r.GET("/users/", self.User)
357+	r.GET("/users/delete", self.Delete)
358+	r.POST("/users/", self.UpsertUser)
359+}
360+
361+func ParseUint(id string) (*uint, error) {
362+	var idValue *uint
363+	if id != "" {
364+		v, err := strconv.Atoi(id)
365+		if err != nil {
366+			return nil, err
367+		}
368+
369+		u := uint(v)
370+		idValue = &u
371+	}
372+	return idValue, nil
373 }
374diff --git a/templates/base.qtpl b/templates/base.qtpl
375index 0c0578234dc0fcf545c94664b8c4304cc67ed23b..5a7c3b769e7767d77cb6fe18985a98d41d5e39f2 100644
376--- a/templates/base.qtpl
377+++ b/templates/base.qtpl
378@@ -1,11 +1,23 @@
379 This is a base page template. All the other template pages implement this interface.
380 
381+{% import "strconv" %}
382+
383 {% interface
384 Page {
385 	Title()
386 	Content()
387     Script()
388 }
389+
390+%}
391+
392+{% code 
393+    func FromUInttoString(u *uint) string {
394+        if u != nil {
395+            return strconv.FormatUint(uint64(*u), 10)
396+        }
397+        return ""
398+    }
399 %}
400 
401 
402diff --git a/templates/settings.qtpl b/templates/settings.qtpl
403index 6eee1abd3d87e60ae598ea50651c5c04992932ec..4207439c7d9d685f429ba3298ff3f23383a75581 100644
404--- a/templates/settings.qtpl
405+++ b/templates/settings.qtpl
406@@ -52,10 +52,13 @@                 </div>
407                 <div class="column">
408                     {%s user.Path %}
409                 </div>
410-                <div class="column  has-text-right"><a href="#">Edit</a> / <a href="#" class="is-danger">Delete</a></div>
411+                <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>
412             </div>
413         </div>
414     {% endfor %}
415+        <div class="field">
416+            <a href="/users/" class="button">Create</a>
417+        </div>
418     </div>
419 </div>
420 {% endfunc %}
421diff --git a/templates/user.qtpl b/templates/user.qtpl
422new file mode 100644
423index 0000000000000000000000000000000000000000..ba2f071537abbcc25f8ec2d3fc1eb18f05b9ac1c
424--- /dev/null
425+++ b/templates/user.qtpl
426@@ -0,0 +1,54 @@
427+
428+{% code
429+type UserPage struct {
430+    ID *uint
431+    Username string
432+    Path string
433+    IsAdmin bool
434+}
435+
436+%}
437+
438+{% func (p *UserPage) Title() %}Login{% endfunc %}
439+
440+{% func (p *UserPage) Content() %}
441+<h1>Initial Setup</h1>
442+<form action="/users/" method="post">
443+    {% if p.ID != nil %} 
444+    <input type="hidden" name="userId" value="{%s FromUInttoString(p.ID) %}" />
445+    {% endif %}
446+    <div class="field">
447+        <label class="label">Username</label>
448+        <div class="control">
449+            <input class="input" name="username" type="text" value="{%s p.Username %}">
450+        </div>
451+    </div>
452+
453+    {% if p.ID == nil %} 
454+    <div class="field">
455+        <label class="label">Password</label>
456+        <div class="control">
457+            <input class="input" name="password" type="password">
458+        </div>
459+    </div>
460+    {% endif %}
461+    <div class="field">
462+        <label class="label">Root folder</label>
463+        <div class="control">
464+            <input class="input" name="path" type="text" value="{%s p.Path %}">
465+        </div>
466+    </div>
467+    <div class="field">
468+        <label class="label">Is Admin?</label>
469+        <div class="control">
470+            <input type="checkbox" name="isAdmin" type="password" {% if p.IsAdmin %}checked{% endif %}>
471+        </div>
472+    </div>
473+    <div class="field">
474+        <input class="button is-pulled-right" value="Save" type="submit">
475+    </div>
476+</form>
477+{% endfunc %}
478+
479+{% func (p *UserPage) Script() %}
480+{% endfunc %}