diff options
| -rw-r--r-- | backend/admin.go | 53 | ||||
| -rw-r--r-- | backend/router.go | 6 | ||||
| -rw-r--r-- | backend/storage/db.go (renamed from backend/db.go) | 6 | ||||
| -rw-r--r-- | backend/storage/migrations/000_init.sql (renamed from backend/migrations/000_init.sql) | 0 | ||||
| -rw-r--r-- | backend/storage/stats.go (renamed from backend/stats.go) | 78 | ||||
| -rw-r--r-- | main.go | 5 |
6 files changed, 79 insertions, 69 deletions
diff --git a/backend/admin.go b/backend/admin.go new file mode 100644 index 0000000..087a3ea --- /dev/null +++ b/backend/admin.go @@ -0,0 +1,53 @@ +package backend + +import ( + "log/slog" + "net/http" + "strconv" + + "git.anhgelus.world/anhgelus/small-web/backend/storage" + "github.com/go-chi/chi/v5" +) + +type adminData struct { + *data + Visits []storage.StatsRow + Rows []storage.StatsRow + 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 = storage.GetStatsRows(ctx, uint(page)) + if err != nil { + panic(err) + } + d.Visits, err = storage.GetUnionStatsRows(ctx) + if err != nil { + panic(err) + } + d.PagesNumber = page + max(len(d.Rows)-storage.StatsPerPage+1, 0) + d.CurrentPage = page + d.handleGeneric(w, r, "admin", d) + }) +} diff --git a/backend/router.go b/backend/router.go index f2ed775..161433d 100644 --- a/backend/router.go +++ b/backend/router.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "git.anhgelus.world/anhgelus/small-web/backend/storage" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/httplog/v3" @@ -24,7 +25,6 @@ const ( configKey = "config" assetsFSKey = "assets_fs" debugKey = "debug" - dbKey = "db" loginKey = "login" ) @@ -95,7 +95,7 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { ctx = context.WithValue(ctx, configKey, cfg) ctx = context.WithValue(ctx, assetsFSKey, assets) ctx = context.WithValue(ctx, debugKey, debug) - return context.WithValue(ctx, dbKey, db) + return context.WithValue(ctx, storage.DBKey, db) } r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -108,7 +108,7 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { go func(r *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - if err := UpdateStats(setContext(ctx), r); err != nil { + if err := storage.UpdateStats(setContext(ctx), r, cfg.Domain); err != nil { slog.Error("updating stats", "error", err) } }(r) diff --git a/backend/db.go b/backend/storage/db.go index 3828880..7a7c6e0 100644 --- a/backend/db.go +++ b/backend/storage/db.go @@ -1,4 +1,4 @@ -package backend +package storage import ( "context" @@ -18,8 +18,8 @@ var migrations embed.FS var nameReg = regexp.MustCompile(`(\d{3})_[a-zA-Z_-]+.sql`) -func ConnectDatabase(cfg *Config) *sql.DB { - db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared", cfg.Database)) +func ConnectDatabase(file string) *sql.DB { + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared", file)) if err != nil { panic(err) } diff --git a/backend/migrations/000_init.sql b/backend/storage/migrations/000_init.sql index 6dd385c..6dd385c 100644 --- a/backend/migrations/000_init.sql +++ b/backend/storage/migrations/000_init.sql diff --git a/backend/stats.go b/backend/storage/stats.go index f081194..757168d 100644 --- a/backend/stats.go +++ b/backend/storage/stats.go @@ -1,4 +1,4 @@ -package backend +package storage import ( "context" @@ -8,14 +8,13 @@ import ( "net/http" "regexp" "slices" - "strconv" "strings" "sync" "time" - - "github.com/go-chi/chi/v5" ) +const DBKey = "db" + type loaded struct { data map[string]struct{} mu *sync.RWMutex @@ -52,10 +51,10 @@ var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) var load = newLoaded() func getDB(ctx context.Context) *sql.DB { - return ctx.Value(dbKey).(*sql.DB) + return ctx.Value(DBKey).(*sql.DB) } -func UpdateStats(ctx context.Context, r *http.Request) error { +func UpdateStats(ctx context.Context, r *http.Request, domain string) error { target := r.URL.Path if strings.HasPrefix(target, "/static") || strings.HasPrefix(target, "/admin") { return nil @@ -69,7 +68,7 @@ func UpdateStats(ctx context.Context, r *http.Request) error { return nil } ref = subs[1] - if ref == ctx.Value(configKey).(*Config).Domain || ref == fmt.Sprintf("localhost:%d", 8000) { + if ref == domain || ref == fmt.Sprintf("localhost:%d", 8000) { ref = subs[0][strings.Index(subs[0], ref)+len(ref):] if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" { return nil @@ -116,28 +115,28 @@ func UpdateStats(ctx context.Context, r *http.Request) error { return err } -type statRow struct { +type StatsRow struct { Origin string Target string Visit uint } -const statPerPage = 25 +const StatsPerPage = 25 -func GetStatRows(ctx context.Context, page uint) ([]statRow, error) { +func GetStatsRows(ctx context.Context, page uint) ([]StatsRow, error) { rows, err := getDB(ctx).QueryContext( ctx, "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?", - statPerPage, (page-1)*statPerPage, + StatsPerPage, (page-1)*StatsPerPage, ) if err != nil { return nil, err } defer rows.Close() - statRows := make([]statRow, statPerPage) + statRows := make([]StatsRow, StatsPerPage) var i uint8 for i = 0; rows.Next(); i++ { - var stat statRow + var stat StatsRow err = rows.Scan(&stat.Origin, &stat.Target, &stat.Visit) if err != nil { return nil, err @@ -150,7 +149,7 @@ func GetStatRows(ctx context.Context, page uint) ([]statRow, error) { return statRows[:i], nil } -func GetUnionStatRows(ctx context.Context) ([]statRow, error) { +func GetUnionStatsRows(ctx context.Context) ([]StatsRow, error) { rows, err := getDB(ctx).QueryContext(ctx, "SELECT target, visit FROM stats ORDER BY visit DESC") if err != nil { return nil, err @@ -158,7 +157,7 @@ func GetUnionStatRows(ctx context.Context) ([]statRow, error) { defer rows.Close() data := make(map[string]uint) for rows.Next() { - var stat statRow + var stat StatsRow err = rows.Scan(&stat.Target, &stat.Visit) if err != nil { return nil, err @@ -169,58 +168,15 @@ func GetUnionStatRows(ctx context.Context) ([]statRow, error) { data[stat.Target] += stat.Visit } } - var statRows []statRow + var statRows []StatsRow for k, v := range data { - statRows = append(statRows, statRow{ + statRows = append(statRows, StatsRow{ Target: k, Visit: v, }) } - slices.SortFunc(statRows, func(a, b statRow) int { + slices.SortFunc(statRows, func(a, b StatsRow) int { return int(b.Visit) - int(a.Visit) }) return statRows, nil } - -type adminData struct { - *data - Visits []statRow - 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.Visits, err = GetUnionStatRows(ctx) - if err != nil { - panic(err) - } - d.PagesNumber = page + max(len(d.Rows)-statPerPage+1, 0) - d.CurrentPage = page - d.handleGeneric(w, r, "admin", d) - }) -} @@ -15,6 +15,7 @@ import ( "time" "git.anhgelus.world/anhgelus/small-web/backend" + "git.anhgelus.world/anhgelus/small-web/backend/storage" "github.com/joho/godotenv" ) @@ -61,9 +62,9 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - db := backend.ConnectDatabase(cfg) + db := storage.ConnectDatabase(cfg.Database) defer db.Close() - err := backend.RunMigration(ctx, db) + err := storage.RunMigration(ctx, db) if err != nil { panic(err) } |
