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 %}