lens @ 4d930c0c8cb585979798fac2bb254f991faa62fb

feat: Add initial user setup
  1diff --git a/cmd/server/main.go b/cmd/server/main.go
  2index f58366ff418564305b29238e67f5b6314b10e249..4942ac31216717bc8c568944e409c69bb93ea521 100644
  3--- a/cmd/server/main.go
  4+++ b/cmd/server/main.go
  5@@ -72,16 +72,6 @@
  6 	r := router.New()
  7 	r.GET("/static/{filepath:*}", ext.FileServer(img.StaticFS, "static/"))
  8 
  9-	authMiddleware := ext.NewAuthMiddleware(hexKey, logger.WithField("context", "auth"))
 10-	logMiddleware := ext.NewLogMiddleare(logger.WithField("context", "http"))
 11-
 12-	extRouter := ext.NewRouter(r)
 13-	extRouter.AddMiddleware(logMiddleware.HTTP)
 14-	extRouter.AddMiddleware(authMiddleware.LoggedIn)
 15-	extRouter.AddMiddleware(ext.HTML)
 16-
 17-	scheduler := worker.NewScheduler(*schedulerCount)
 18-
 19 	// repository
 20 	var (
 21 		userRepository       = sql.NewUserRepository(db)
 22@@ -90,12 +80,24 @@ 		fileSystemRepository = localfs.NewFileSystemRepository(*root)
 23 		mediaRepository      = sql.NewMediaRepository(db)
 24 	)
 25 
 26-	//TODO: remove later
 27-	userRepository.EnsureAdmin(context.Background())
 28+	// middleware
 29+	var (
 30+		authMiddleware    = ext.NewAuthMiddleware(hexKey, logger.WithField("context", "auth"))
 31+		logMiddleware     = ext.NewLogMiddleare(logger.WithField("context", "http"))
 32+		initialMiddleware = ext.NewInitialSetupMiddleware(userRepository)
 33+	)
 34+
 35+	extRouter := ext.NewRouter(r)
 36+	extRouter.AddMiddleware(ext.HTML)
 37+	extRouter.AddMiddleware(initialMiddleware.Check)
 38+	extRouter.AddMiddleware(authMiddleware.LoggedIn)
 39+	extRouter.AddMiddleware(logMiddleware.HTTP)
 40+
 41+	scheduler := worker.NewScheduler(*schedulerCount)
 42 
 43 	// controller
 44 	var (
 45-		userController       = auth.NewController(userRepository, hexKey)
 46+		userController       = auth.NewController(userRepository, userRepository, hexKey)
 47 		fileSystemController = filesystem.NewController(fileSystemRepository)
 48 	)
 49 
 50diff --git a/pkg/components/auth/controller.go b/pkg/components/auth/controller.go
 51index a81a1c0f802acc5a4c90bcc400d2d9f913c73b9b..2f30fb5d1027286fc809f6a683bc42416ed86db3 100644
 52--- a/pkg/components/auth/controller.go
 53+++ b/pkg/components/auth/controller.go
 54@@ -5,18 +5,26 @@ 	"context"
 55 
 56 	"golang.org/x/crypto/bcrypt"
 57 
 58+	"git.sr.ht/~gabrielgio/img/pkg/components"
 59+	"git.sr.ht/~gabrielgio/img/pkg/components/user"
 60 	"git.sr.ht/~gabrielgio/img/pkg/ext"
 61 )
 62 
 63 type Controller struct {
 64-	repository Repository
 65-	key        []byte
 66+	repository     Repository
 67+	userRepository user.Repository
 68+	key            []byte
 69 }
 70 
 71-func NewController(repository Repository, key []byte) *Controller {
 72+func NewController(
 73+	repository Repository,
 74+	userRepository user.Repository,
 75+	key []byte,
 76+) *Controller {
 77 	return &Controller{
 78-		repository: repository,
 79-		key:        key,
 80+		repository:     repository,
 81+		userRepository: userRepository,
 82+		key:            key,
 83 	}
 84 }
 85 
 86@@ -41,3 +49,29 @@ 		Username: string(username),
 87 	}
 88 	return ext.WriteToken(token, c.key)
 89 }
 90+
 91+// InitialRegister register a initial user, it will validate if there is another
 92+// user stored already. If so an error `InvlidaInput` will be returned
 93+func (c *Controller) InitialRegister(ctx context.Context, username, password []byte, path []byte) error {
 94+	exist, err := c.userRepository.Any(ctx)
 95+	if err != nil {
 96+		return err
 97+	}
 98+
 99+	if exist {
100+		return components.InvlidaInput
101+	}
102+
103+	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
104+	if err != nil {
105+		return err
106+	}
107+
108+	err = c.userRepository.Create(ctx, &user.CreateUser{
109+		Username: string(username),
110+		Password: hash,
111+		Path:     string(path),
112+	})
113+
114+	return err
115+}
116diff --git a/pkg/components/auth/controller_test.go b/pkg/components/auth/controller_test.go
117index 33aa901cf701dcc8675776c97f36b57e7a202882..6b4e3cd03ecdb792c56141c5e68ba1a53b343815 100644
118--- a/pkg/components/auth/controller_test.go
119+++ b/pkg/components/auth/controller_test.go
120@@ -9,6 +9,7 @@ 	"testing"
121 
122 	"github.com/samber/lo"
123 
124+	"git.sr.ht/~gabrielgio/img/pkg/components/user"
125 	"git.sr.ht/~gabrielgio/img/pkg/ext"
126 	"git.sr.ht/~gabrielgio/img/pkg/testkit"
127 )
128@@ -43,11 +44,11 @@ 	mockUserRepository := &MockUserRepository{}
129 	return &scene{
130 		ctx:            context.Background(),
131 		mockRepository: mockUserRepository,
132-		controller:     *NewController(mockUserRepository, key),
133+		controller:     *NewController(mockUserRepository, nil, key),
134 	}
135 }
136 
137-func TestRegisterAndLogin(t *testing.T) {
138+func TestInitialRegisterAndLogin(t *testing.T) {
139 	testCases := []struct {
140 		name     string
141 		username string
142@@ -64,7 +65,7 @@ 	for _, tc := range testCases {
143 		t.Run(tc.name, func(t *testing.T) {
144 			scene := setUp()
145 
146-			err := scene.controller.Register(scene.ctx, []byte(tc.username), tc.password)
147+			err := scene.controller.InitialRegister(scene.ctx, []byte(tc.username), tc.password, []byte("/"))
148 			testkit.TestFatalError(t, "Register", err)
149 
150 			userID := scene.mockRepository.GetLastId()
151@@ -85,8 +86,8 @@ 		})
152 	}
153 }
154 
155-func toUser(m *mockUser, _ int) *User {
156-	return &User{
157+func toUser(m *mockUser, _ int) *user.User {
158+	return &user.User{
159 		ID:       m.id,
160 		Username: m.username,
161 	}
162@@ -96,7 +97,7 @@ func (m *MockUserRepository) GetLastId() uint {
163 	return m.index
164 }
165 
166-func (m *MockUserRepository) List(ctx context.Context) ([]*User, error) {
167+func (m *MockUserRepository) List(ctx context.Context) ([]*user.User, error) {
168 	if m.err != nil {
169 		return nil, m.err
170 	}
171@@ -104,7 +105,7 @@
172 	return lo.Map(m.users, toUser), nil
173 }
174 
175-func (m *MockUserRepository) Get(ctx context.Context, id uint) (*User, error) {
176+func (m *MockUserRepository) Get(ctx context.Context, id uint) (*user.User, error) {
177 	if m.err != nil {
178 		return nil, m.err
179 	}
180@@ -143,7 +144,7 @@ 	}
181 	return nil, errors.New("Item not found")
182 }
183 
184-func (m *MockUserRepository) Create(ctx context.Context, createUser *CreateUser) (uint, error) {
185+func (m *MockUserRepository) Create(ctx context.Context, createUser *user.CreateUser) (uint, error) {
186 	if m.err != nil {
187 		return 0, m.err
188 	}
189@@ -159,7 +160,7 @@
190 	return m.index, nil
191 }
192 
193-func (m *MockUserRepository) Update(ctx context.Context, id uint, update *UpdateUser) error {
194+func (m *MockUserRepository) Update(ctx context.Context, id uint, update *user.UpdateUser) error {
195 	if m.err != nil {
196 		return m.err
197 	}
198diff --git a/pkg/components/errors.go b/pkg/components/errors.go
199new file mode 100644
200index 0000000000000000000000000000000000000000..aedbe889b5f1e2d54616f746b859da359a288be5
201--- /dev/null
202+++ b/pkg/components/errors.go
203@@ -0,0 +1,8 @@
204+package components
205+
206+import "errors"
207+
208+var (
209+	NotFound     = errors.New("Not found")
210+	InvlidaInput = errors.New("Invalid Input")
211+)
212diff --git a/pkg/components/media/model.go b/pkg/components/media/model.go
213index 0e17e9231f4f7ded99d4d6fa6e866f34756a5514..1962a23e2716418d3919b66d253cdbbfc46d2de3 100644
214--- a/pkg/components/media/model.go
215+++ b/pkg/components/media/model.go
216@@ -2,6 +2,7 @@ package media
217 
218 import (
219 	"context"
220+	"strings"
221 	"time"
222 )
223 
224@@ -57,3 +58,7 @@ 		GetEXIF(context.Context, uint) (*MediaEXIF, error)
225 		CreateEXIF(context.Context, uint, *MediaEXIF) error
226 	}
227 )
228+
229+func (m *Media) IsVideo() bool {
230+	return strings.HasPrefix(m.MIMEType, "video")
231+}
232diff --git a/pkg/components/user/model.go b/pkg/components/user/model.go
233index f957c39a95fb49179c319674a6c31827bebb695c..ce1b3a5986e2782ab36ae08dd05f02862f2d01a4 100644
234--- a/pkg/components/user/model.go
235+++ b/pkg/components/user/model.go
236@@ -20,7 +20,7 @@
237 	CreateUser struct {
238 		Username string
239 		Name     string
240-		Password string
241+		Password []byte
242 		IsAdmin  bool
243 		Path     string
244 	}
245@@ -29,5 +29,6 @@ 	Repository interface {
246 		List(ctx context.Context) ([]*User, error)
247 		Create(ctx context.Context, createUser *CreateUser) error
248 		Update(ctx context.Context, id uint, updateUser *UpdateUser) error
249+		Any(ctx context.Context) (bool, error)
250 	}
251 )
252diff --git a/pkg/database/sql/user.go b/pkg/database/sql/user.go
253index 2d74162713c10e1ed292a1dc116404e862f77bd3..a02b67b669b46c634051daf69000a5a9044a0336 100644
254--- a/pkg/database/sql/user.go
255+++ b/pkg/database/sql/user.go
256@@ -187,3 +187,18 @@ 		return result.Error
257 	}
258 	return nil
259 }
260+
261+func (u *UserRepository) Any(ctx context.Context) (bool, error) {
262+	var exists bool
263+	result := u.db.
264+		WithContext(ctx).
265+		Model(&User{}).
266+		Select("count(id) > 0").
267+		Find(&exists)
268+
269+	if result.Error != nil {
270+		return false, result.Error
271+	}
272+
273+	return exists, nil
274+}
275diff --git a/pkg/database/sql/user_test.go b/pkg/database/sql/user_test.go
276index 875b8e6022b0b97c1fbe0b6c0363a4beb12912df..473ce032516e298768da908b7e64f84e4f705bce 100644
277--- a/pkg/database/sql/user_test.go
278+++ b/pkg/database/sql/user_test.go
279@@ -12,7 +12,7 @@ 	"gorm.io/driver/sqlite"
280 	"gorm.io/gorm"
281 	"gorm.io/gorm/logger"
282 
283-	"git.sr.ht/~gabrielgio/img/pkg/components/auth"
284+	"git.sr.ht/~gabrielgio/img/pkg/components/user"
285 )
286 
287 func setup(t *testing.T) (*gorm.DB, func()) {
288@@ -48,7 +48,7 @@ 	defer tearDown()
289 
290 	repository := NewUserRepository(db)
291 
292-	id, err := repository.Create(context.Background(), &auth.CreateUser{
293+	err := repository.Create(context.Background(), &user.CreateUser{
294 		Username: "new_username",
295 		Name:     "new_name",
296 	})
297@@ -56,12 +56,12 @@ 	if err != nil {
298 		t.Fatalf("Error creating: %s", err.Error())
299 	}
300 
301-	got, err := repository.Get(context.Background(), id)
302+	got, err := repository.Get(context.Background(), 1)
303 	if err != nil {
304 		t.Fatalf("Error getting: %s", err.Error())
305 	}
306-	want := &auth.User{
307-		ID:       id,
308+	want := &user.User{
309+		ID:       1,
310 		Username: "new_username",
311 		Name:     "new_name",
312 	}
313@@ -78,7 +78,7 @@ 	defer tearDown()
314 
315 	repository := NewUserRepository(db)
316 
317-	id, err := repository.Create(context.Background(), &auth.CreateUser{
318+	err := repository.Create(context.Background(), &user.CreateUser{
319 		Username: "username",
320 		Name:     "name",
321 	})
322@@ -86,7 +86,7 @@ 	if err != nil {
323 		t.Fatalf("Error creating user: %s", err.Error())
324 	}
325 
326-	err = repository.Update(context.Background(), id, &auth.UpdateUser{
327+	err = repository.Update(context.Background(), 1, &user.UpdateUser{
328 		Username: "new_username",
329 		Name:     "new_name",
330 	})
331@@ -94,12 +94,12 @@ 	if err != nil {
332 		t.Fatalf("Error update user: %s", err.Error())
333 	}
334 
335-	got, err := repository.Get(context.Background(), id)
336+	got, err := repository.Get(context.Background(), 1)
337 	if err != nil {
338 		t.Fatalf("Error getting user: %s", err.Error())
339 	}
340-	want := &auth.User{
341-		ID:       id,
342+	want := &user.User{
343+		ID:       1,
344 		Username: "new_username",
345 		Name:     "new_name",
346 	}
347diff --git a/pkg/ext/middleware.go b/pkg/ext/middleware.go
348index 771c0ac6cf729661c1a6c33967910d908c1411bc..649272e6714bfd5f0f85355c4f6b439eb1e6620f 100644
349--- a/pkg/ext/middleware.go
350+++ b/pkg/ext/middleware.go
351@@ -4,6 +4,7 @@ import (
352 	"encoding/base64"
353 	"time"
354 
355+	"git.sr.ht/~gabrielgio/img/pkg/components/user"
356 	"github.com/sirupsen/logrus"
357 	"github.com/valyala/fasthttp"
358 )
359@@ -54,7 +55,7 @@
360 func (a *AuthMiddleware) LoggedIn(next fasthttp.RequestHandler) fasthttp.RequestHandler {
361 	return func(ctx *fasthttp.RequestCtx) {
362 		path := string(ctx.Path())
363-		if path == "/login" {
364+		if path == "/login" || path == "/initial" {
365 			next(ctx)
366 			return
367 		}
368@@ -87,3 +88,42 @@ 			Info("user recognized")
369 		next(ctx)
370 	}
371 }
372+
373+type InitialSetupMiddleware struct {
374+	userRepository user.Repository
375+}
376+
377+func NewInitialSetupMiddleware(userRepository user.Repository) *InitialSetupMiddleware {
378+	return &InitialSetupMiddleware{
379+		userRepository: userRepository,
380+	}
381+}
382+
383+func (i *InitialSetupMiddleware) Check(next fasthttp.RequestHandler) fasthttp.RequestHandler {
384+	return func(ctx *fasthttp.RequestCtx) {
385+		// if user has been set to context it is logged in already
386+		_, ok := ctx.UserValue("token").(*Token)
387+		if ok {
388+			next(ctx)
389+			return
390+		}
391+
392+		path := string(ctx.Path())
393+		if path == "/initial" {
394+			next(ctx)
395+			return
396+		}
397+
398+		exists, err := i.userRepository.Any(ctx)
399+		if err != nil {
400+			InternalServerError(ctx, err)
401+			return
402+		}
403+
404+		if exists {
405+			next(ctx)
406+			return
407+		}
408+		ctx.Redirect("/initial", 307)
409+	}
410+}
411diff --git a/pkg/view/auth.go b/pkg/view/auth.go
412index d44424d64290d5877761bcc1229b9244551ca08b..3f9e4140365dbed89f490c6d39e066670fc3d1b4 100644
413--- a/pkg/view/auth.go
414+++ b/pkg/view/auth.go
415@@ -68,10 +68,31 @@ func Index(ctx *fasthttp.RequestCtx) {
416 	ctx.Redirect("/login", 307)
417 }
418 
419+func (v *AuthView) InitialRegisterView(ctx *fasthttp.RequestCtx) error {
420+	return img.Render[interface{}](ctx, "register.html", nil)
421+}
422+
423+func (v *AuthView) InitialRegister(ctx *fasthttp.RequestCtx) error {
424+	username := ctx.FormValue("username")
425+	password := ctx.FormValue("password")
426+	path := ctx.FormValue("path")
427+
428+	err := v.userController.InitialRegister(ctx, username, password, path)
429+	if err != nil {
430+		return err
431+	}
432+
433+	ctx.Redirect("/login", 307)
434+	return nil
435+}
436+
437 func (v *AuthView) SetMyselfIn(r *ext.Router) {
438 	r.GET("/login", v.LoginView)
439 	r.POST("/login", v.Login)
440 
441 	r.GET("/logout", v.Logout)
442 	r.POST("/logout", v.Logout)
443+
444+	r.GET("/initial", v.InitialRegisterView)
445+	r.POST("/initial", v.InitialRegister)
446 }
447diff --git a/templates/login.html b/templates/login.html
448index f71d9d3d20b0ed31661aa7ccffc1f047c69c17cd..607faa101d1594e66bbf7c729880e4e42e80d25d 100644
449--- a/templates/login.html
450+++ b/templates/login.html
451@@ -1,5 +1,5 @@
452 {{template "layout.html" .}}
453-{{define "title"}} Register {{end}}
454+{{define "title"}} Login {{end}}
455 {{define "content"}}
456 <form action="/login" method="post">
457     <div class="field">
458diff --git a/templates/media.html b/templates/media.html
459index 478d8aee45db8dd3950fd6d9876aaef035dcd82d..6302a573451a953b2a02275611a63377930d0db3 100644
460--- a/templates/media.html
461+++ b/templates/media.html
462@@ -5,9 +5,15 @@ <div class="columns is-multiline">
463 {{range .Data.Medias}}
464 <div class="card">
465     <div class="card-image">
466+        {{ if .IsVideo }}
467+        <video controls muted="true" preload="metadata">
468+            <source src="/media/image?path_hash={{.PathHash}}" type="{{.MIMEType}}">
469+        </video>
470+        {{ else }}
471         <figure class="image is-fit">
472-            <img   src="/media/image?path_hash={{.PathHash}}">
473+            <img src="/media/image?path_hash={{.PathHash}}">
474         </figure>
475+        {{ end }}
476     </div>
477 </div>
478 {{end}}
479diff --git a/templates/register.html b/templates/register.html
480new file mode 100644
481index 0000000000000000000000000000000000000000..b026d33a60605574f7c71e79bd5fc8b555e94eb9
482--- /dev/null
483+++ b/templates/register.html
484@@ -0,0 +1,28 @@
485+{{template "layout.html" .}}
486+{{define "title"}} Initial Setup {{end}}
487+{{define "content"}}
488+<h1>Initial Setup</h1>
489+<form action="/initial" method="post">
490+    <div class="field">
491+        <label class="label">Username</label>
492+        <div class="control">
493+            <input class="input" name="username" type="text">
494+        </div>
495+    </div>
496+    <div class="field">
497+        <label class="label">Password</label>
498+        <div class="control">
499+            <input class="input" name="password" type="password">
500+        </div>
501+    </div>
502+    <div class="field">
503+        <label class="label">Root folder</label>
504+        <div class="control">
505+            <input class="input" name="path" type="text">
506+        </div>
507+    </div>
508+    <div class="field">
509+        <input class="button is-pulled-right" value="Save" type="submit">
510+    </div>
511+</form>
512+{{end}}