aboutsummaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/config.go16
-rw-r--r--backend/router.go18
-rw-r--r--backend/section.go2
-rw-r--r--backend/stats.go81
-rw-r--r--backend/templates/admin.html33
-rw-r--r--backend/templates/components.html24
6 files changed, 152 insertions, 22 deletions
diff --git a/backend/config.go b/backend/config.go
index b221978..33d5852 100644
--- a/backend/config.go
+++ b/backend/config.go
@@ -29,13 +29,14 @@ type Replacer struct {
}
type Config struct {
- Domain string `toml:"domain"`
- Name string `toml:"name"`
- Description string `toml:"description"`
- DefaultImage string `toml:"default_image"`
- Quotes []string `toml:"quotes"`
- Language string `toml:"language"`
- Database string `toml:"database"`
+ Domain string `toml:"domain"`
+ Name string `toml:"name"`
+ Description string `toml:"description"`
+ DefaultImage string `toml:"default_image"`
+ Quotes []string `toml:"quotes"`
+ Language string `toml:"language"`
+ Database string `toml:"database"`
+ AdminPassword string `toml:"admin_password"`
Sections []Section `toml:"section"`
@@ -75,6 +76,7 @@ func (c *Config) DefaultValues() {
c.RootFolder = "data"
c.PublicFolder = "public"
c.Database = "database.sqlite"
+ c.AdminPassword = "Ch@ngeM€Please!"
c.Quotes = []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do."}
c.Replacers = []Replacer{{"~", " "}}
}
diff --git a/backend/router.go b/backend/router.go
index 14abc06..f2ed775 100644
--- a/backend/router.go
+++ b/backend/router.go
@@ -2,6 +2,8 @@ package backend
import (
"context"
+ "crypto/sha256"
+ "crypto/subtle"
"database/sql"
"embed"
"fmt"
@@ -23,6 +25,7 @@ const (
assetsFSKey = "assets_fs"
debugKey = "debug"
dbKey = "db"
+ loginKey = "login"
)
//go:embed templates
@@ -112,6 +115,21 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux {
next.ServeHTTP(w, r)
})
})
+ // login
+ r.Use(func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ _, pass, ok := r.BasicAuth()
+ ctx := r.Context()
+ if ok {
+ cfg := ctx.Value(configKey).(*Config)
+ passHash := sha256.Sum256([]byte(pass))
+ rightPassHash := sha256.Sum256([]byte(cfg.AdminPassword))
+ ok = subtle.ConstantTimeCompare(passHash[:], rightPassHash[:]) == 1
+ }
+ ctx = context.WithValue(ctx, loginKey, ok)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+ })
return r
}
diff --git a/backend/section.go b/backend/section.go
index e2150cb..1b5ab9f 100644
--- a/backend/section.go
+++ b/backend/section.go
@@ -270,7 +270,7 @@ func (s *Section) handlePagination(w http.ResponseWriter, r *http.Request, maxLo
page, err = strconv.Atoi(rawPage)
if err != nil || page < 1 {
slog.Warn("invalid page number", "rawPage", rawPage)
- w.WriteHeader(http.StatusBadRequest)
+ http.Error(w, "Bad request", http.StatusBadRequest)
return nil
}
}
diff --git a/backend/stats.go b/backend/stats.go
index 2b07221..dfdf040 100644
--- a/backend/stats.go
+++ b/backend/stats.go
@@ -7,7 +7,10 @@ import (
"log/slog"
"net/http"
"regexp"
+ "strconv"
"strings"
+
+ "github.com/go-chi/chi/v5"
)
var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`)
@@ -18,7 +21,9 @@ func getDB(ctx context.Context) *sql.DB {
func UpdateStats(ctx context.Context, r *http.Request) error {
target := r.URL.Path
- if strings.HasPrefix(target, "/assets") || strings.HasPrefix(target, "/static") {
+ if strings.HasPrefix(target, "/assets") ||
+ strings.HasPrefix(target, "/static") ||
+ strings.HasPrefix(target, "/admin") {
return nil
}
ref := r.Header.Get("Referer")
@@ -32,7 +37,7 @@ func UpdateStats(ctx context.Context, r *http.Request) error {
ref = subs[1]
if ref == ctx.Value(configKey).(*Config).Domain || ref == fmt.Sprintf("localhost:%d", 8000) {
ref = subs[0][strings.Index(subs[0], ref)+len(ref):]
- if ref == target {
+ if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" {
return nil
}
}
@@ -63,3 +68,75 @@ func UpdateStats(ctx context.Context, r *http.Request) error {
_, err = db.ExecContext(ctx, "UPDATE stats SET visit = ? WHERE id = ?", nb+1, id)
return err
}
+
+type statRow struct {
+ Origin string
+ Target string
+ Visit uint
+}
+
+const statPerPage = 25
+
+func GetStatRows(ctx context.Context, page uint) ([]statRow, error) {
+ rows, err := getDB(ctx).QueryContext(
+ ctx,
+ "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?",
+ statPerPage, (page-1)*statPerPage,
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ statRows := make([]statRow, statPerPage)
+ var i uint8
+ for i = 0; rows.Next(); i++ {
+ var stat statRow
+ err = rows.Scan(&stat.Origin, &stat.Target, &stat.Visit)
+ if err != nil {
+ return nil, err
+ }
+ statRows[i] = stat
+ }
+ if i == 0 {
+ return nil, nil
+ }
+ return statRows[:i], nil
+}
+
+type adminData struct {
+ *data
+ Rows []statRow
+ PagesNumber int
+ CurrentPage int
+}
+
+func HandleAdmin(r *chi.Mux) {
+ r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ if !ctx.Value(loginKey).(bool) {
+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ d := new(adminData)
+ d.data = new(data)
+ rawPage := r.URL.Query().Get("page")
+ page := 1
+ var err error
+ if rawPage != "" {
+ page, err = strconv.Atoi(rawPage)
+ if err != nil || page < 1 {
+ slog.Warn("invalid page number", "rawPage", rawPage)
+ http.Error(w, "Bad request", http.StatusBadRequest)
+ return
+ }
+ }
+ d.Rows, err = GetStatRows(ctx, uint(page))
+ if err != nil {
+ panic(err)
+ }
+ d.PagesNumber = page + max(len(d.Rows)-statPerPage+1, 0)
+ d.CurrentPage = page
+ d.handleGeneric(w, r, "admin", d)
+ })
+}
diff --git a/backend/templates/admin.html b/backend/templates/admin.html
new file mode 100644
index 0000000..41addc9
--- /dev/null
+++ b/backend/templates/admin.html
@@ -0,0 +1,33 @@
+{{define "body"}}
+<main id="content">
+ <div class="introduction">
+ <h1>Administration</h1>
+ <p>
+ Panel d'administration très simple. Il affiche les stats et possiblement les webmentions et autre contenu à
+ modérer.
+ </p>
+ </div>
+ <article>
+ <h2>Stats</h2>
+ <table>
+ <thead>
+ <tr>
+ <th>Origin</th>
+ <th>Target</th>
+ <th>Visits</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{ range .Rows }}
+ <tr>
+ <td>{{ .Origin }}</td>
+ <td>{{ .Target }}</td>
+ <td>{{ .Visit }}</td>
+ </tr>
+ {{ end }}
+ </tbody>
+ </table>
+ <div class="pagination">{{ template "pagination" . }}</div>
+ </article>
+</main>
+{{end}}
diff --git a/backend/templates/components.html b/backend/templates/components.html
index b4ffbe6..b180123 100644
--- a/backend/templates/components.html
+++ b/backend/templates/components.html
@@ -1,4 +1,14 @@
-{{define "section_display"}}
+{{ define "pagination" }}
+<nav>
+ {{ if ne .CurrentPage 1 }}<a href="?page={{ before .CurrentPage }}">Précédent</a>{{else}}
+ <p></p>
+ {{end}}
+ <p>{{ .CurrentPage }}/{{ .PagesNumber }}</p>
+ {{ if ne .CurrentPage .PagesNumber }}<a href="?page={{ next .CurrentPage }}">Suivant</a>{{else}}
+ <p></p>
+ {{end}}
+</nav>
+{{ end }} {{define "section_display"}}
<article>
{{ $uri := .URI }} {{ range .Data }}
<article>
@@ -11,17 +21,7 @@
</article>
{{ end }}
<div class="pagination">
- {{ if .Paginate }}
- <nav>
- {{ if ne .CurrentPage 1 }}<a href="?page={{ before .CurrentPage }}">Précédent</a>{{else}}
- <p></p>
- {{end}}
- <p>{{ .CurrentPage }}/{{ .PagesNumber }}</p>
- {{ if ne .CurrentPage .PagesNumber }}<a href="?page={{ next .CurrentPage }}">Suivant</a>{{else}}
- <p></p>
- {{end}}
- </nav>
- {{ else }} {{ if eq (len .Data) .LenMax }}
+ {{ if .Paginate }} {{ template "pagination" . }} {{ else }} {{ if eq (len .Data) .LenMax }}
<a href="/{{ $uri }}/">Voir plus</a>
{{ end }} {{ end }}
</div>