This commit is contained in:
2026-03-22 12:10:13 -04:00
parent 9ed63b2695
commit 784e53c557
34 changed files with 2046 additions and 237 deletions

View File

@@ -941,7 +941,16 @@ func (s *Server) GetLogs(ctx context.Context, request GetLogsRequestObject) (Get
return GetLogs401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
}
// Get filter parameter (mirroring legacy)
page := int64(1)
if request.Params.Page != nil && *request.Params.Page > 0 {
page = *request.Params.Page
}
limit := int64(100)
if request.Params.Limit != nil && *request.Params.Limit > 0 {
limit = *request.Params.Limit
}
filter := ""
if request.Params.Filter != nil {
filter = strings.TrimSpace(*request.Params.Filter)
@@ -967,7 +976,6 @@ func (s *Server) GetLogs(ctx context.Context, request GetLogsRequestObject) (Get
}
}
// Open Log File (mirroring legacy)
logPath := filepath.Join(s.cfg.ConfigPath, "logs/antholume.log")
logFile, err := os.Open(logPath)
if err != nil {
@@ -975,58 +983,90 @@ func (s *Server) GetLogs(ctx context.Context, request GetLogsRequestObject) (Get
}
defer logFile.Close()
// Log Lines (mirroring legacy)
var logLines []string
offset := (page - 1) * limit
logLines := make([]string, 0, limit)
matchedCount := int64(0)
scanner := bufio.NewScanner(logFile)
for scanner.Scan() {
rawLog := scanner.Text()
// Attempt JSON Pretty (mirroring legacy)
var jsonMap map[string]any
err := json.Unmarshal([]byte(rawLog), &jsonMap)
if err != nil {
logLines = append(logLines, rawLog)
formattedLog, matched := formatLogLine(scanner.Text(), basicFilter, jqFilter)
if !matched {
continue
}
// Parse JSON (mirroring legacy)
rawData, err := json.MarshalIndent(jsonMap, "", " ")
if err != nil {
logLines = append(logLines, rawLog)
continue
if matchedCount >= offset && int64(len(logLines)) < limit {
logLines = append(logLines, formattedLog)
}
matchedCount++
}
// Basic Filter (mirroring legacy)
if basicFilter != "" && strings.Contains(string(rawData), basicFilter) {
logLines = append(logLines, string(rawData))
continue
}
if err := scanner.Err(); err != nil {
return GetLogs500JSONResponse{Code: 500, Message: "Unable to read AnthoLume log file"}, nil
}
// No JQ Filter (mirroring legacy)
if jqFilter == nil {
continue
}
// Error or nil (mirroring legacy)
result, _ := jqFilter.Run(jsonMap).Next()
if _, ok := result.(error); ok {
logLines = append(logLines, string(rawData))
continue
} else if result == nil {
continue
}
// Attempt filtered json (mirroring legacy)
filteredData, err := json.MarshalIndent(result, "", " ")
if err == nil {
rawData = filteredData
}
logLines = append(logLines, string(rawData))
var nextPage *int64
var previousPage *int64
if page > 1 {
previousPage = ptrOf(page - 1)
}
if offset+int64(len(logLines)) < matchedCount {
nextPage = ptrOf(page + 1)
}
return GetLogs200JSONResponse{
Logs: &logLines,
Filter: &filter,
Logs: &logLines,
Filter: &filter,
Page: &page,
Limit: &limit,
NextPage: nextPage,
PreviousPage: previousPage,
Total: &matchedCount,
}, nil
}
func formatLogLine(rawLog string, basicFilter string, jqFilter *gojq.Code) (string, bool) {
var jsonMap map[string]any
if err := json.Unmarshal([]byte(rawLog), &jsonMap); err != nil {
if basicFilter == "" && jqFilter == nil {
return rawLog, true
}
if basicFilter != "" && strings.Contains(rawLog, basicFilter) {
return rawLog, true
}
return "", false
}
rawData, err := json.MarshalIndent(jsonMap, "", " ")
if err != nil {
if basicFilter == "" && jqFilter == nil {
return rawLog, true
}
if basicFilter != "" && strings.Contains(rawLog, basicFilter) {
return rawLog, true
}
return "", false
}
formattedLog := string(rawData)
if basicFilter != "" {
return formattedLog, strings.Contains(formattedLog, basicFilter)
}
if jqFilter == nil {
return formattedLog, true
}
result, _ := jqFilter.Run(jsonMap).Next()
if _, ok := result.(error); ok {
return formattedLog, true
}
if result == nil {
return "", false
}
filteredData, err := json.MarshalIndent(result, "", " ")
if err == nil {
formattedLog = string(filteredData)
}
return formattedLog, true
}

152
api/v1/admin_test.go Normal file
View File

@@ -0,0 +1,152 @@
package v1
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
argon2 "github.com/alexedwards/argon2id"
"github.com/stretchr/testify/require"
"reichard.io/antholume/config"
"reichard.io/antholume/database"
)
func createAdminTestUser(t *testing.T, db *database.DBManager, username, password string) {
t.Helper()
md5Hash := fmt.Sprintf("%x", md5.Sum([]byte(password)))
hashedPassword, err := argon2.CreateHash(md5Hash, argon2.DefaultParams)
require.NoError(t, err)
authHash := "test-auth-hash"
_, err = db.Queries.CreateUser(context.Background(), database.CreateUserParams{
ID: username,
Pass: &hashedPassword,
AuthHash: &authHash,
Admin: true,
})
require.NoError(t, err)
}
func loginAdminTestUser(t *testing.T, srv *Server, username, password string) *http.Cookie {
t.Helper()
body, err := json.Marshal(LoginRequest{Username: username, Password: password})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
cookies := w.Result().Cookies()
require.Len(t, cookies, 1)
return cookies[0]
}
func TestGetLogsPagination(t *testing.T) {
configPath := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(configPath, "logs"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(configPath, "logs", "antholume.log"), []byte(
"{\"level\":\"info\",\"msg\":\"one\"}\n"+
"plain two\n"+
"{\"level\":\"error\",\"msg\":\"three\"}\n"+
"plain four\n",
), 0o644))
cfg := &config.Config{
ListenPort: "8080",
DBType: "memory",
DBName: "test",
ConfigPath: configPath,
CookieAuthKey: "test-auth-key-32-bytes-long-enough",
CookieEncKey: "0123456789abcdef",
CookieSecure: false,
CookieHTTPOnly: true,
Version: "test",
DemoMode: false,
RegistrationEnabled: true,
}
db := database.NewMgr(cfg)
srv := NewServer(db, cfg, nil)
createAdminTestUser(t, db, "admin", "password")
cookie := loginAdminTestUser(t, srv, "admin", "password")
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/logs?page=2&limit=2", nil)
req.AddCookie(cookie)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var resp LogsResponse
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
require.NotNil(t, resp.Logs)
require.Len(t, *resp.Logs, 2)
require.NotNil(t, resp.Page)
require.Equal(t, int64(2), *resp.Page)
require.NotNil(t, resp.Limit)
require.Equal(t, int64(2), *resp.Limit)
require.NotNil(t, resp.Total)
require.Equal(t, int64(4), *resp.Total)
require.Nil(t, resp.NextPage)
require.NotNil(t, resp.PreviousPage)
require.Equal(t, int64(1), *resp.PreviousPage)
require.Contains(t, (*resp.Logs)[0], "three")
require.Contains(t, (*resp.Logs)[1], "plain four")
}
func TestGetLogsPaginationWithBasicFilter(t *testing.T) {
configPath := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(configPath, "logs"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(configPath, "logs", "antholume.log"), []byte(
"{\"level\":\"info\",\"msg\":\"match-1\"}\n"+
"{\"level\":\"info\",\"msg\":\"skip\"}\n"+
"plain match-2\n"+
"{\"level\":\"info\",\"msg\":\"match-3\"}\n",
), 0o644))
cfg := &config.Config{
ListenPort: "8080",
DBType: "memory",
DBName: "test",
ConfigPath: configPath,
CookieAuthKey: "test-auth-key-32-bytes-long-enough",
CookieEncKey: "0123456789abcdef",
CookieSecure: false,
CookieHTTPOnly: true,
Version: "test",
DemoMode: false,
RegistrationEnabled: true,
}
db := database.NewMgr(cfg)
srv := NewServer(db, cfg, nil)
createAdminTestUser(t, db, "admin", "password")
cookie := loginAdminTestUser(t, srv, "admin", "password")
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/logs?filter=%22match%22&page=1&limit=2", nil)
req.AddCookie(cookie)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var resp LogsResponse
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
require.NotNil(t, resp.Logs)
require.Len(t, *resp.Logs, 2)
require.NotNil(t, resp.Total)
require.Equal(t, int64(3), *resp.Total)
require.NotNil(t, resp.NextPage)
require.Equal(t, int64(2), *resp.NextPage)
}

View File

@@ -314,8 +314,13 @@ type LoginResponse struct {
// LogsResponse defines model for LogsResponse.
type LogsResponse struct {
Filter *string `json:"filter,omitempty"`
Logs *[]LogEntry `json:"logs,omitempty"`
Filter *string `json:"filter,omitempty"`
Limit *int64 `json:"limit,omitempty"`
Logs *[]LogEntry `json:"logs,omitempty"`
NextPage *int64 `json:"next_page,omitempty"`
Page *int64 `json:"page,omitempty"`
PreviousPage *int64 `json:"previous_page,omitempty"`
Total *int64 `json:"total,omitempty"`
}
// MessageResponse defines model for MessageResponse.
@@ -465,6 +470,8 @@ type PostImportFormdataBody struct {
// GetLogsParams defines parameters for GetLogs.
type GetLogsParams struct {
Filter *string `form:"filter,omitempty" json:"filter,omitempty"`
Page *int64 `form:"page,omitempty" json:"page,omitempty"`
Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"`
}
// UpdateUserFormdataBody defines parameters for UpdateUser.
@@ -862,6 +869,22 @@ func (siw *ServerInterfaceWrapper) GetLogs(w http.ResponseWriter, r *http.Reques
return
}
// ------------- Optional query parameter "page" -------------
err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), &params.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err})
return
}
// ------------- Optional query parameter "limit" -------------
err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), &params.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetLogs(w, r, params)
}))

View File

@@ -594,6 +594,21 @@ components:
$ref: '#/components/schemas/LogEntry'
filter:
type: string
page:
type: integer
format: int64
limit:
type: integer
format: int64
next_page:
type: integer
format: int64
previous_page:
type: integer
format: int64
total:
type: integer
format: int64
InfoResponse:
type: object
@@ -1764,6 +1779,18 @@ paths:
in: query
schema:
type: string
- name: page
in: query
schema:
type: integer
format: int64
minimum: 1
- name: limit
in: query
schema:
type: integer
format: int64
minimum: 1
security:
- BearerAuth: []
responses: