lens @ ae10e121875982d6956d6bff453544cc59a75616

feat: Add admin control

Now only admins can access settings.
  1diff --git a/cmd/server/main.go b/cmd/server/main.go
  2index 58256fab291203cc3c2a8b7644559e8e6fa1bf2f..41b2b4a781170e387e42db4d187b3ff91fbfd7d5 100644
  3--- a/cmd/server/main.go
  4+++ b/cmd/server/main.go
  5@@ -72,7 +72,7 @@ 	if err != nil {
  6 		panic("failed to decode key database: " + err.Error())
  7 	}
  8 
  9-	r := mux.NewRouter()
 10+	r := mux.NewRouter().StrictSlash(false)
 11 	r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static))))
 12 
 13 	// repository
 14@@ -85,7 +85,7 @@ 	)
 15 
 16 	// middleware
 17 	var (
 18-		authMiddleware    = ext.NewAuthMiddleware(baseKey, logger.WithField("context", "auth"))
 19+		authMiddleware    = ext.NewAuthMiddleware(baseKey, logger.WithField("context", "auth"), userRepository)
 20 		logMiddleware     = ext.NewLogMiddleare(logger.WithField("context", "http"))
 21 		initialMiddleware = ext.NewInitialSetupMiddleware(userRepository)
 22 	)
 23diff --git a/pkg/database/sql/user.go b/pkg/database/sql/user.go
 24index 2ec86229435c1508c25f5b67f98178a6cfd67647..0c503c2e3c8ea8d30ca2fb70679644e168cb49d4 100644
 25--- a/pkg/database/sql/user.go
 26+++ b/pkg/database/sql/user.go
 27@@ -158,20 +158,16 @@ 	return user.Model.ID, nil
 28 }
 29 
 30 func (self *UserRepository) Update(ctx context.Context, id uint, update *repository.UpdateUser) error {
 31-	user := &User{
 32-		Model: gorm.Model{
 33-			ID: id,
 34-		},
 35-		Username: update.Username,
 36-		Name:     update.Name,
 37-		IsAdmin:  update.IsAdmin,
 38-		Path:     update.Path,
 39-	}
 40-
 41 	result := self.db.
 42 		WithContext(ctx).
 43+		Model(&User{}).
 44 		Omit("password").
 45-		Updates(user)
 46+		Where("id = ?", id).
 47+		Update("username", update.Username).
 48+		Update("name", update.Name).
 49+		Update("is_admin", update.IsAdmin).
 50+		Update("path", update.Path)
 51+
 52 	if result.Error != nil {
 53 		return wrapError(result.Error)
 54 	}
 55diff --git a/pkg/ext/middleware.go b/pkg/ext/middleware.go
 56index 061cf7cfb849699600e614d2aa7491b8736ef0b0..6a94c4f9442ed01ba1a98eb375b3c106aeda81e5 100644
 57--- a/pkg/ext/middleware.go
 58+++ b/pkg/ext/middleware.go
 59@@ -20,9 +20,17 @@ 		next(w, r)
 60 	}
 61 }
 62 
 63-type LogMiddleware struct {
 64-	entry *logrus.Entry
 65-}
 66+type (
 67+	User string
 68+
 69+	LogMiddleware struct {
 70+		entry *logrus.Entry
 71+	}
 72+)
 73+
 74+const (
 75+	UserKey User = "user"
 76+)
 77 
 78 func NewLogMiddleare(log *logrus.Entry) *LogMiddleware {
 79 	return &LogMiddleware{
 80@@ -43,14 +51,20 @@ 	}
 81 }
 82 
 83 type AuthMiddleware struct {
 84-	key   []byte
 85-	entry *logrus.Entry
 86+	key            []byte
 87+	entry          *logrus.Entry
 88+	userRepository repository.UserRepository
 89 }
 90 
 91-func NewAuthMiddleware(key []byte, log *logrus.Entry) *AuthMiddleware {
 92+func NewAuthMiddleware(
 93+	key []byte,
 94+	log *logrus.Entry,
 95+	userRepository repository.UserRepository,
 96+) *AuthMiddleware {
 97 	return &AuthMiddleware{
 98-		key:   key,
 99-		entry: log.WithField("context", "auth"),
100+		key:            key,
101+		entry:          log.WithField("context", "auth"),
102+		userRepository: userRepository,
103 	}
104 }
105 
106@@ -82,7 +96,14 @@ 			a.entry.Error(err)
107 			http.Redirect(w, r, redirectLogin, http.StatusTemporaryRedirect)
108 			return
109 		}
110-		r = r.WithContext(context.WithValue(r.Context(), service.TokenKey, token))
111+
112+		user, err := a.userRepository.Get(r.Context(), token.UserID)
113+		if err != nil {
114+			a.entry.Error(err)
115+			return
116+		}
117+
118+		r = r.WithContext(context.WithValue(r.Context(), UserKey, user))
119 		a.entry.
120 			WithField("userID", token.UserID).
121 			WithField("username", token.Username).
122@@ -91,9 +112,9 @@ 		next(w, r)
123 	}
124 }
125 
126-func GetTokenFromCtx(r *http.Request) *service.Token {
127-	tokenValue := r.Context().Value(service.TokenKey)
128-	if token, ok := tokenValue.(*service.Token); ok {
129+func GetUserFromCtx(r *http.Request) *repository.User {
130+	tokenValue := r.Context().Value(UserKey)
131+	if token, ok := tokenValue.(*repository.User); ok {
132 		return token
133 	}
134 	return nil
135@@ -113,7 +134,7 @@ func (i *InitialSetupMiddleware) Check(next http.HandlerFunc) http.HandlerFunc {
136 	return func(w http.ResponseWriter, r *http.Request) {
137 
138 		// if user has been set to context it is logged in already
139-		token := GetTokenFromCtx(r)
140+		token := GetUserFromCtx(r)
141 		if token != nil {
142 			next(w, r)
143 			return
144diff --git a/pkg/ext/responses.go b/pkg/ext/responses.go
145index 34e5f278bf1d8e22dc9af9a9fd2584060b81abb1..d8941e8d6df1cb819a90e2834b6d4ffeca28e826 100644
146--- a/pkg/ext/responses.go
147+++ b/pkg/ext/responses.go
148@@ -10,12 +10,12 @@
149 func NotFound(w http.ResponseWriter) {
150 	templates.WritePageTemplate(w, &templates.ErrorPage{
151 		Err: "Not Found",
152-	})
153+	}, false)
154 }
155 
156 func InternalServerError(w http.ResponseWriter, err error) {
157 	w.WriteHeader(http.StatusInternalServerError)
158 	templates.WritePageTemplate(w, &templates.ErrorPage{
159 		Err: fmt.Sprintf("Internal Server Error:\n%s", err.Error()),
160-	})
161+	}, false)
162 }
163diff --git a/pkg/service/auth.go b/pkg/service/auth.go
164index 2fc06e383f8f34d2e1ac843d1d48fb1930c6f80a..3811965cf321f71b95522f89d3dc9bf56883fea7 100644
165--- a/pkg/service/auth.go
166+++ b/pkg/service/auth.go
167@@ -147,14 +147,11 @@ 	return err
168 }
169 
170 type (
171-	AuthKey string
172-	Token   struct {
173+	Token struct {
174 		UserID   uint
175 		Username string
176 	}
177 )
178-
179-const TokenKey AuthKey = "token"
180 
181 func ReadToken(data []byte, key []byte) (*Token, error) {
182 	block, err := aes.NewCipher(key)
183diff --git a/pkg/view/album.go b/pkg/view/album.go
184index b19e381bb5008ca4468fc6bd5f7b492ab07d8148..e0ee4053fc724ca3c79404f036b84ebe63218996 100644
185--- a/pkg/view/album.go
186+++ b/pkg/view/album.go
187@@ -30,10 +30,10 @@ }
188 
189 func (self *AlbumView) Index(w http.ResponseWriter, r *http.Request) error {
190 	p := getPagination(r)
191-	token := ext.GetTokenFromCtx(r)
192+	user := ext.GetUserFromCtx(r)
193 
194 	// TODO: optmize call, GetPathFromUserID may no be necessary
195-	userPath, err := self.userRepository.GetPathFromUserID(r.Context(), token.UserID)
196+	userPath, err := self.userRepository.GetPathFromUserID(r.Context(), user.ID)
197 	if err != nil {
198 		return err
199 	}
200@@ -91,12 +91,12 @@ 		},
201 		Settings: settings,
202 	}
203 
204-	templates.WritePageTemplate(w, page)
205+	templates.WritePageTemplate(w, page, user.IsAdmin)
206 
207 	return nil
208 }
209 
210 func (self *AlbumView) SetMyselfIn(r *ext.Router) {
211-	r.GET("/album/", self.Index)
212-	r.POST("/album/", self.Index)
213+	r.GET("/album", self.Index)
214+	r.POST("/album", self.Index)
215 }
216diff --git a/pkg/view/auth.go b/pkg/view/auth.go
217index 8d870352e8b7220c8df36582adaeeab7beb054b1..318d0a33ff2bd4a3ef106b71b7334cd4e5af1117 100644
218--- a/pkg/view/auth.go
219+++ b/pkg/view/auth.go
220@@ -20,8 +20,8 @@ 		userController: userController,
221 	}
222 }
223 
224-func (v *AuthView) LoginView(w http.ResponseWriter, _ *http.Request) error {
225-	templates.WritePageTemplate(w, &templates.LoginPage{})
226+func (v *AuthView) LoginView(w http.ResponseWriter, r *http.Request) error {
227+	templates.WritePageTemplate(w, &templates.LoginPage{}, false)
228 	return nil
229 }
230 
231@@ -46,12 +46,15 @@ 		password = []byte(r.FormValue("password"))
232 	)
233 
234 	auth, err := v.userController.Login(r.Context(), username, password)
235+	if err != nil {
236+		return err
237+	}
238 
239 	if errors.Is(err, service.InvalidLogin) {
240 		templates.WritePageTemplate(w, &templates.LoginPage{
241 			Username: r.FormValue("username"),
242 			Err:      err.Error(),
243-		})
244+		}, false)
245 		return nil
246 	}
247 
248@@ -82,8 +85,8 @@ func Index(w http.ResponseWriter, r *http.Request) {
249 	http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
250 }
251 
252-func (v *AuthView) InitialRegisterView(w http.ResponseWriter, _ *http.Request) error {
253-	templates.WritePageTemplate(w, &templates.RegisterPage{})
254+func (v *AuthView) InitialRegisterView(w http.ResponseWriter, r *http.Request) error {
255+	templates.WritePageTemplate(w, &templates.RegisterPage{}, false)
256 	return nil
257 }
258 
259diff --git a/pkg/view/filesystem.go b/pkg/view/filesystem.go
260index 24f0ce61e6d169b2406b7081feeb965377b63036..9071ec0e0cc95d53713c5e753d5a942539db8fec 100644
261--- a/pkg/view/filesystem.go
262+++ b/pkg/view/filesystem.go
263@@ -34,10 +34,10 @@
264 func (self *FileSystemView) Index(w http.ResponseWriter, r *http.Request) error {
265 	var (
266 		pathValue = r.FormValue("path")
267-		token     = ext.GetTokenFromCtx(r)
268+		user      = ext.GetUserFromCtx(r)
269 	)
270 
271-	page, err := self.fsService.GetPage(r.Context(), token.UserID, pathValue)
272+	page, err := self.fsService.GetPage(r.Context(), user.ID, pathValue)
273 	if err != nil {
274 		return err
275 	}
276@@ -51,7 +51,7 @@ 	templates.WritePageTemplate(w, &templates.FilePage{
277 		Page:      page,
278 		ShowMode:  settings.ShowMode,
279 		ShowOwner: settings.ShowOwner,
280-	})
281+	}, user.IsAdmin)
282 
283 	return nil
284 }
285@@ -59,6 +59,6 @@
286 func (self *FileSystemView) SetMyselfIn(r *ext.Router) {
287 	r.GET("/", self.Index)
288 	r.POST("/", self.Index)
289-	r.GET("/fs/", self.Index)
290-	r.POST("/fs/", self.Index)
291+	r.GET("/fs", self.Index)
292+	r.POST("/fs", self.Index)
293 }
294diff --git a/pkg/view/media.go b/pkg/view/media.go
295index 3041998f465d49976d644a3a8b83f292ebe8cb87..8a10fe0792a75afa70e81bd0cc05b080012c4c80 100644
296--- a/pkg/view/media.go
297+++ b/pkg/view/media.go
298@@ -71,9 +71,9 @@ }
299 
300 func (self *MediaView) Index(w http.ResponseWriter, r *http.Request) error {
301 	p := getPagination(r)
302-	token := ext.GetTokenFromCtx(r)
303+	user := ext.GetUserFromCtx(r)
304 
305-	userPath, err := self.userRepository.GetPathFromUserID(r.Context(), token.UserID)
306+	userPath, err := self.userRepository.GetPathFromUserID(r.Context(), user.ID)
307 	if err != nil {
308 		return err
309 	}
310@@ -98,7 +98,7 @@ 		},
311 		Settings: settings,
312 	}
313 
314-	templates.WritePageTemplate(w, page)
315+	templates.WritePageTemplate(w, page, user.IsAdmin)
316 
317 	return nil
318 }
319@@ -132,9 +132,9 @@ 	return nil
320 }
321 
322 func (self *MediaView) SetMyselfIn(r *ext.Router) {
323-	r.GET("/media/", self.Index)
324-	r.POST("/media/", self.Index)
325+	r.GET("/media", self.Index)
326+	r.POST("/media", self.Index)
327 
328-	r.GET("/media/image/", self.GetImage)
329-	r.GET("/media/thumbnail/", self.GetThumbnail)
330+	r.GET("/media/image", self.GetImage)
331+	r.GET("/media/thumbnail", self.GetThumbnail)
332 }
333diff --git a/pkg/view/settings.go b/pkg/view/settings.go
334index bf2dca6d27190c1ada6765d2ee61588edb8e85f0..cdd7baa30aecd90c205e34c3061a52fbc2359aa9 100644
335--- a/pkg/view/settings.go
336+++ b/pkg/view/settings.go
337@@ -39,23 +39,28 @@ 	if err != nil {
338 		return err
339 	}
340 
341+	user := ext.GetUserFromCtx(r)
342+
343 	templates.WritePageTemplate(w, &templates.SettingsPage{
344 		Settings: s,
345 		Users:    users,
346-	})
347+	}, user.IsAdmin)
348 
349 	return nil
350 }
351 
352 func (self *SettingsView) User(w http.ResponseWriter, r *http.Request) error {
353-	id := r.FormValue("userId")
354+	var (
355+		id   = r.URL.Query().Get("userId")
356+		user = ext.GetUserFromCtx(r)
357+	)
358 	idValue, err := ParseUint(id)
359 	if err != nil {
360 		return err
361 	}
362 
363 	if idValue == nil {
364-		templates.WritePageTemplate(w, &templates.UserPage{})
365+		templates.WritePageTemplate(w, &templates.UserPage{}, user.IsAdmin)
366 	} else {
367 		user, err := self.userController.Get(r.Context(), *idValue)
368 		if err != nil {
369@@ -67,7 +72,7 @@ 			ID:       idValue,
370 			Username: user.Username,
371 			Path:     user.Path,
372 			IsAdmin:  user.IsAdmin,
373-		})
374+		}, user.IsAdmin)
375 	}
376 
377 	return nil
378@@ -87,7 +92,15 @@ 	if err != nil {
379 		return err
380 	}
381 
382-	err = self.userController.Upsert(r.Context(), idValue, username, "", password, isAdmin, path)
383+	err = self.userController.Upsert(
384+		r.Context(),
385+		idValue,
386+		username,
387+		"",
388+		password,
389+		isAdmin,
390+		path,
391+	)
392 	if err != nil {
393 		return err
394 	}
395@@ -137,12 +150,12 @@ 	return self.Index(w, r)
396 }
397 
398 func (self *SettingsView) SetMyselfIn(r *ext.Router) {
399-	r.GET("/settings/", self.Index)
400-	r.POST("/settings/", self.Save)
401+	r.GET("/settings", Protect(self.Index))
402+	r.POST("/settings", Protect(self.Save))
403 
404-	r.GET("/users/", self.User)
405-	r.GET("/users/delete", self.Delete)
406-	r.POST("/users/", self.UpsertUser)
407+	r.GET("/users", Protect(self.User))
408+	r.GET("/users/delete", Protect(self.Delete))
409+	r.POST("/users", Protect(self.UpsertUser))
410 }
411 
412 func ParseUint(id string) (*uint, error) {
413diff --git a/pkg/view/view.go b/pkg/view/view.go
414index 663738b6c6ac87955d83753445b9ff868959e29a..f8dfa16f0ccfc5261207919be4c1ff7fc5a71a09 100644
415--- a/pkg/view/view.go
416+++ b/pkg/view/view.go
417@@ -1,7 +1,22 @@
418 package view
419 
420-import "git.sr.ht/~gabrielgio/img/pkg/ext"
421+import (
422+	"net/http"
423+
424+	"git.sr.ht/~gabrielgio/img/pkg/ext"
425+)
426 
427 type View interface {
428 	SetMyselfIn(r *ext.Router)
429 }
430+
431+func Protect(next ext.ErrorRequestHandler) ext.ErrorRequestHandler {
432+	return func(w http.ResponseWriter, r *http.Request) error {
433+		user := ext.GetUserFromCtx(r)
434+		if !user.IsAdmin {
435+			http.NotFound(w, r)
436+			return nil
437+		}
438+		return next(w, r)
439+	}
440+}
441diff --git a/templates/album.qtpl b/templates/album.qtpl
442index 835db5703c2c07cc98145f76fd435e52d4f5707e..58fc499ccceb04cbb33f4c8f9ec495293b043020 100644
443--- a/templates/album.qtpl
444+++ b/templates/album.qtpl
445@@ -23,14 +23,14 @@ {% func (p *AlbumPage) Content() %}
446 <h1 class="title text-size-1">{%s p.Name %}</h1>
447 <div class="tags are-large">
448 {% for _, a := range p.Albums %}
449-  <a href="/album/?albumId={%s FromUInttoString(&a.ID) %}" class="tag text-size-2">{%s a.Name %}</a>
450+  <a href="/album?albumId={%s FromUInttoString(&a.ID) %}" class="tag text-size-2">{%s a.Name %}</a>
451 {% endfor %}
452 </div>
453 <div class="columns">
454 {%= Mosaic(p.Medias, p.PreloadAttr()) %}
455 </div>
456 <div>
457-    <a href="/album/?albumId={%s FromUInttoString(p.Next.AlbumID) %}&page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
458+    <a href="/album?albumId={%s FromUInttoString(p.Next.AlbumID) %}&page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
459 </div>
460 {% endfunc %}
461 
462diff --git a/templates/base.qtpl b/templates/base.qtpl
463index a80803a50b720b01160069c224084f74ea86fe8b..30b084ebfd71f3a56ecf96c687d66260179ed7b8 100644
464--- a/templates/base.qtpl
465+++ b/templates/base.qtpl
466@@ -21,7 +21,7 @@ %}
467 
468 
469 Page prints a page implementing Page interface.
470-{% func PageTemplate(p Page) %}
471+{% func PageTemplate(p Page, isAdmin bool) %}
472 <html lang="en">
473     <head>
474         <meta charset="utf-8">
475@@ -33,18 +33,20 @@     </head>
476     <body>
477         <nav class="navbar">
478             <div class="navbar-start">
479-                <a href="/fs/" class="navbar-item text-size-1">
480+                <a href="/fs" class="navbar-item text-size-1">
481                     file
482                 </a>
483-                <a href="/media/" class="navbar-item text-size-1">
484+                <a href="/media" class="navbar-item text-size-1">
485                     media
486                 </a>
487-                <a href="/album/" class="navbar-item text-size-1">
488+                <a href="/album" class="navbar-item text-size-1">
489                     album
490                 </a>
491-                <a href="/settings/" class="navbar-item text-size-1">
492+                {% if isAdmin  %}
493+                <a href="/settings" class="navbar-item text-size-1">
494                     settings
495                 </a>
496+                {% endif %}
497             </div>
498         </nav>
499         <div class="container is-fullhd">
500diff --git a/templates/media.qtpl b/templates/media.qtpl
501index 4251deb32c1b561ecdbad544bb7adbe42fec0571..737d03d63a2ae301fc37dd3a9926e216a6fe325a 100644
502--- a/templates/media.qtpl
503+++ b/templates/media.qtpl
504@@ -22,7 +22,7 @@ <div class="columns">
505 {%= Mosaic(p.Medias, p.PreloadAttr()) %}
506 </div>
507 <div>
508-    <a href="/media/?page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
509+    <a href="/media?page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
510 </div>
511 {% endfunc %}
512 
513diff --git a/templates/mosaic.qtpl b/templates/mosaic.qtpl
514index 3e6ccf86d9ac5359d8a2a097af4269589af25fb7..18dbcba489fef78faa1326522e834762f3efadba 100644
515--- a/templates/mosaic.qtpl
516+++ b/templates/mosaic.qtpl
517@@ -8,12 +8,12 @@     <div class="column is-2">
518     {% for _, media := range c %}
519     <div class="card-image">
520        {% if media.IsVideo() %}
521-       <video class="image is-fit" controls muted="true" poster="/media/thumbnail/?path_hash={%s media.PathHash %}" preload="{%s preloadAttr %}">
522-           <source src="/media/image/?path_hash={%s media.PathHash %}" type="{%s media.MIMEType %}">
523+       <video class="image is-fit" controls muted="true" poster="/media/thumbnail?path_hash={%s media.PathHash %}" preload="{%s preloadAttr %}">
524+           <source src="/media/image?path_hash={%s media.PathHash %}" type="{%s media.MIMEType %}">
525        </video>
526        {% else %}
527         <figure class="image is-fit">
528-            <img src="/media/thumbnail/?path_hash={%s media.PathHash %}">
529+            <img src="/media/thumbnail?path_hash={%s media.PathHash %}">
530         </figure>
531         {% endif %}
532     </div>
533diff --git a/templates/settings.qtpl b/templates/settings.qtpl
534index 4439c77c99d834bc2c71f9b277a85c097f3ae8f9..b720a88dc4b7028bb415d668e9844c49f9d0d1e7 100644
535--- a/templates/settings.qtpl
536+++ b/templates/settings.qtpl
537@@ -58,7 +58,7 @@         </div>
538     </div>
539     {% endfor %}
540     <div class="field">
541-        <a href="/users/" class="button">create</a>
542+        <a href="/users" class="button">create</a>
543     </div>
544 </div>
545 {% endfunc %}
546diff --git a/templates/user.qtpl b/templates/user.qtpl
547index 6ec783d6d9bbc32dcb34efe3deba29230cdc3e39..6fc3ce6cf25de65a3e3ed52ace6c9510740b9049 100644
548--- a/templates/user.qtpl
549+++ b/templates/user.qtpl
550@@ -13,7 +13,7 @@ {% func (p *UserPage) Title() %}User{% endfunc %}
551 
552 {% func (p *UserPage) Content() %}
553 <h1>Initial Setup</h1>
554-<form action="/users/" method="post">
555+<form action="/users" method="post">
556     {% if p.ID != nil %} 
557     <input type="hidden" name="userId" value="{%s FromUInttoString(p.ID) %}" />
558     {% endif %}
559@@ -41,7 +41,7 @@     </div>
560     <div class="field">
561         <label class="label">Is Admin?</label>
562         <div class="control">
563-            <input type="checkbox" name="isAdmin" type="password" {% if p.IsAdmin %}checked{% endif %}>
564+            <input type="checkbox" name="isAdmin" {% if p.IsAdmin %}checked{% endif %}>
565         </div>
566     </div>
567     <div class="field">