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