diff options
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/cmd/server/main.go | 6 | ||||
| -rw-r--r-- | backend/go.mod | 9 | ||||
| -rw-r--r-- | backend/go.sum | 22 | ||||
| -rw-r--r-- | backend/internal/api/handler.go | 40 | ||||
| -rw-r--r-- | backend/internal/api/router.go | 3 | ||||
| -rw-r--r-- | backend/internal/model/types.go | 8 | ||||
| -rw-r--r-- | backend/internal/monitor/checker.go | 26 | ||||
| -rw-r--r-- | backend/internal/monitor/worker.go | 9 | ||||
| -rw-r--r-- | backend/internal/servermetrics/collector.go | 84 | ||||
| -rw-r--r-- | backend/internal/store/memory.go | 19 | ||||
| -rw-r--r-- | backend/internal/store/postgres.go | 28 | ||||
| -rw-r--r-- | backend/internal/store/store.go | 3 |
12 files changed, 248 insertions, 9 deletions
diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index f734885..0093596 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -16,6 +16,7 @@ import ( "yaum/internal/api" "yaum/internal/config" "yaum/internal/monitor" + "yaum/internal/servermetrics" "yaum/internal/sse" "yaum/internal/store" ) @@ -77,7 +78,10 @@ func main() { } worker.WithSMTP(smtpCfg) } - handler := api.NewHandler(st, int(cfg.CheckTimeout().Seconds())) + metricsCollector := servermetrics.New(".") + go metricsCollector.Start(ctx) + + handler := api.NewHandler(st, int(cfg.CheckTimeout().Seconds()), metricsCollector) streamHandler := api.NewStreamHandler(broadcaster) authHandler := api.NewAuthHandler( cfg.AdminUsername(), diff --git a/backend/go.mod b/backend/go.mod index 68f40b1..8bda52f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,12 +5,21 @@ go 1.25.0 require github.com/jackc/pgx/v5 v5.9.2 require ( + github.com/ebitengine/purego v0.10.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/shirou/gopsutil/v4 v4.26.4 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 68f0bf7..ab65f74 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,8 +1,13 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -13,23 +18,40 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go index 5f934da..8da4290 100644 --- a/backend/internal/api/handler.go +++ b/backend/internal/api/handler.go @@ -10,6 +10,7 @@ import ( "yaum/internal/monitor" "yaum/internal/model" + "yaum/internal/servermetrics" "yaum/internal/store" ) @@ -21,10 +22,11 @@ type ServiceResponse struct { type Handler struct { store store.Store timeout int + metrics *servermetrics.Collector } -func NewHandler(s store.Store, timeoutSec int) *Handler { - return &Handler{store: s, timeout: timeoutSec} +func NewHandler(s store.Store, timeoutSec int, metrics *servermetrics.Collector) *Handler { + return &Handler{store: s, timeout: timeoutSec, metrics: metrics} } func (h *Handler) ListServices(w http.ResponseWriter, r *http.Request) { @@ -91,9 +93,14 @@ func (h *Handler) TestService(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), time.Duration(h.timeout)*time.Second) defer cancel() - hb := monitor.CheckHTTP(ctx, svc.URL, svc.KeywordToFind) + hb, ssl := monitor.CheckHTTP(ctx, svc.URL, svc.KeywordToFind) hb.ServiceID = svc.ID + if ssl != nil { + ssl.ServiceID = svc.ID + _ = h.store.SaveSSLInfo(*ssl) + } + if saverr := h.store.AddHeartbeat(hb); saverr != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": saverr.Error()}) return @@ -323,6 +330,33 @@ func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, stats) } +func (h *Handler) ServerStats(w http.ResponseWriter, r *http.Request) { + if h.metrics == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "server metrics nao disponivel"}) + return + } + stats := h.metrics.Get() + writeJSON(w, http.StatusOK, stats) +} + +func (h *Handler) GetSSLInfo(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id invalido"}) + return + } + info, err := h.store.GetSSLInfo(id) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + if info == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "ssl info nao disponivel"}) + return + } + writeJSON(w, http.StatusOK, info) +} + func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 8188b25..d28da71 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -19,6 +19,7 @@ func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handle mux.HandleFunc("DELETE /api/services/{id}", h.DeleteService) mux.HandleFunc("GET /api/services/{id}/history", h.GetHistory) mux.HandleFunc("GET /api/services/{id}/stats", h.GetStats) + mux.HandleFunc("GET /api/services/{id}/ssl", h.GetSSLInfo) mux.HandleFunc("GET /api/services/{id}/badge.svg", h.BadgeSVG) mux.HandleFunc("GET /api/active-maintenance", h.GetActiveMaintenance) @@ -28,6 +29,8 @@ func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handle mux.HandleFunc("PUT /api/maintenances/{id}", h.UpdateMaintenance) mux.HandleFunc("DELETE /api/maintenances/{id}", h.DeleteMaintenance) + mux.HandleFunc("GET /api/admin/server-stats", h.ServerStats) + mux.Handle("GET /api/stream", stream) return corsMiddleware(auth.Middleware(mux)) diff --git a/backend/internal/model/types.go b/backend/internal/model/types.go index 4e439b8..40dc006 100644 --- a/backend/internal/model/types.go +++ b/backend/internal/model/types.go @@ -50,3 +50,11 @@ type ServiceWithHeartbeat struct { Service LastHeartbeat *Heartbeat `json:"last_heartbeat,omitempty"` } + +type SSLInfo struct { + ServiceID int `json:"service_id"` + Issuer string `json:"issuer"` + ExpiresAt time.Time `json:"expires_at"` + DaysRemaining int `json:"days_remaining"` + CheckedAt time.Time `json:"checked_at"` +} diff --git a/backend/internal/monitor/checker.go b/backend/internal/monitor/checker.go index 07f807e..df386e6 100644 --- a/backend/internal/monitor/checker.go +++ b/backend/internal/monitor/checker.go @@ -12,14 +12,16 @@ import ( var httpClient = &http.Client{ Timeout: 10 * time.Second, + // do NOT set InsecureSkipVerify — we want proper TLS } -func CheckHTTP(ctx context.Context, url string, keyword string) model.Heartbeat { +func CheckHTTP(ctx context.Context, url string, keyword string) (model.Heartbeat, *model.SSLInfo) { start := time.Now() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return heartbeatFromError(start, err) + hb := heartbeatFromError(start, err) + return hb, nil } resp, err := httpClient.Do(req) @@ -32,7 +34,7 @@ func CheckHTTP(ctx context.Context, url string, keyword string) model.Heartbeat IsUp: false, ErrorMessage: err.Error(), TestedAt: time.Now(), - } + }, nil } defer resp.Body.Close() @@ -66,13 +68,29 @@ func CheckHTTP(ctx context.Context, url string, keyword string) model.Heartbeat } } - return model.Heartbeat{ + hb := model.Heartbeat{ StatusCode: resp.StatusCode, ResponseTimeMs: elapsed, IsUp: isUp, ErrorMessage: errMsg, TestedAt: time.Now(), } + + // Extract TLS certificate info + var ssl *model.SSLInfo + if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 { + cert := resp.TLS.PeerCertificates[0] + days := int(time.Until(cert.NotAfter).Hours() / 24) + ssl = &model.SSLInfo{ + ServiceID: 0, // filled in by caller + Issuer: cert.Issuer.CommonName, + ExpiresAt: cert.NotAfter, + DaysRemaining: days, + CheckedAt: time.Now(), + } + } + + return hb, ssl } func heartbeatFromError(start time.Time, err error) model.Heartbeat { diff --git a/backend/internal/monitor/worker.go b/backend/internal/monitor/worker.go index 70855a2..34a5cba 100644 --- a/backend/internal/monitor/worker.go +++ b/backend/internal/monitor/worker.go @@ -166,9 +166,16 @@ func (w *Worker) checkService(ctx context.Context, svc model.Service) { checkCtx, cancel := context.WithTimeout(ctx, w.timeout) defer cancel() - hb := CheckHTTP(checkCtx, svc.URL, svc.KeywordToFind) + hb, ssl := CheckHTTP(checkCtx, svc.URL, svc.KeywordToFind) hb.ServiceID = svc.ID + if ssl != nil { + ssl.ServiceID = svc.ID + if err := w.store.SaveSSLInfo(*ssl); err != nil { + log.Printf("[worker] erro ao salvar ssl info: %v", err) + } + } + w.results <- checkResult{Heartbeat: hb, Service: svc} } diff --git a/backend/internal/servermetrics/collector.go b/backend/internal/servermetrics/collector.go new file mode 100644 index 0000000..6c85582 --- /dev/null +++ b/backend/internal/servermetrics/collector.go @@ -0,0 +1,84 @@ +package servermetrics + +import ( + "context" + "log" + "sync" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/mem" +) + +type Stats struct { + CPUPercent float64 `json:"cpu_percent"` + MemoryUsed uint64 `json:"memory_used"` + MemoryTotal uint64 `json:"memory_total"` + MemoryPct float64 `json:"memory_percent"` + DiskUsed uint64 `json:"disk_used"` + DiskTotal uint64 `json:"disk_total"` + DiskPct float64 `json:"disk_percent"` +} + +type Collector struct { + mu sync.RWMutex + last Stats + diskPath string +} + +func New(diskPath string) *Collector { + return &Collector{diskPath: diskPath} +} + +func (c *Collector) Start(ctx context.Context) { + c.collectOnce(ctx, 500*time.Millisecond) + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + c.collectOnce(ctx, 0) + case <-ctx.Done(): + return + } + } +} + +func (c *Collector) collectOnce(ctx context.Context, cpuInterval time.Duration) { + cpuPct, err := cpu.PercentWithContext(ctx, cpuInterval, false) + if err != nil { + log.Printf("[servermetrics] erro cpu: %v", err) + return + } + + memStat, err := mem.VirtualMemoryWithContext(ctx) + if err != nil { + log.Printf("[servermetrics] erro mem: %v", err) + return + } + + diskStat, err := disk.UsageWithContext(ctx, c.diskPath) + if err != nil { + log.Printf("[servermetrics] erro disk: %v", err) + return + } + + c.mu.Lock() + c.last = Stats{ + CPUPercent: cpuPct[0], + MemoryUsed: memStat.Used, + MemoryTotal: memStat.Total, + MemoryPct: memStat.UsedPercent, + DiskUsed: diskStat.Used, + DiskTotal: diskStat.Total, + DiskPct: diskStat.UsedPercent, + } + c.mu.Unlock() +} + +func (c *Collector) Get() Stats { + c.mu.RLock() + defer c.mu.RUnlock() + return c.last +} diff --git a/backend/internal/store/memory.go b/backend/internal/store/memory.go index 1ee2e05..78defcd 100644 --- a/backend/internal/store/memory.go +++ b/backend/internal/store/memory.go @@ -14,6 +14,7 @@ type MemoryStore struct { services map[int]*model.Service heartbeats map[int][]*model.Heartbeat maintenances []model.Maintenance + sslInfo map[int]model.SSLInfo nextSvcID int nextHbID int nextMtnID int @@ -24,6 +25,7 @@ func NewMemoryStore() *MemoryStore { services: make(map[int]*model.Service), heartbeats: make(map[int][]*model.Heartbeat), maintenances: make([]model.Maintenance, 0), + sslInfo: make(map[int]model.SSLInfo), nextSvcID: 1, nextHbID: 1, nextMtnID: 1, @@ -346,3 +348,20 @@ func (s *MemoryStore) GetServiceStats(serviceID int) (*model.ServiceStats, error TotalChecks30d: t30, }, nil } + +func (s *MemoryStore) GetSSLInfo(serviceID int) (*model.SSLInfo, error) { + s.mu.RLock() + defer s.mu.RUnlock() + info, ok := s.sslInfo[serviceID] + if !ok { + return nil, nil + } + return &info, nil +} + +func (s *MemoryStore) SaveSSLInfo(info model.SSLInfo) error { + s.mu.Lock() + defer s.mu.Unlock() + s.sslInfo[info.ServiceID] = info + return nil +} diff --git a/backend/internal/store/postgres.go b/backend/internal/store/postgres.go index 57fa258..fb07bdc 100644 --- a/backend/internal/store/postgres.go +++ b/backend/internal/store/postgres.go @@ -393,3 +393,31 @@ func (s *PostgresStore) DeleteMaintenance(id int) error { } return nil } + +func (s *PostgresStore) GetSSLInfo(serviceID int) (*model.SSLInfo, error) { + var info model.SSLInfo + err := s.pool.QueryRow(context.Background(), + `SELECT service_id, issuer, expires_at, days_remaining, checked_at FROM ssl_info WHERE service_id = $1`, + serviceID, + ).Scan(&info.ServiceID, &info.Issuer, &info.ExpiresAt, &info.DaysRemaining, &info.CheckedAt) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("get ssl info: %w", err) + } + return &info, nil +} + +func (s *PostgresStore) SaveSSLInfo(info model.SSLInfo) error { + _, err := s.pool.Exec(context.Background(), + `INSERT INTO ssl_info (service_id, issuer, expires_at, days_remaining, checked_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (service_id) DO UPDATE SET issuer=$2, expires_at=$3, days_remaining=$4, checked_at=$5`, + info.ServiceID, info.Issuer, info.ExpiresAt, info.DaysRemaining, info.CheckedAt, + ) + if err != nil { + return fmt.Errorf("save ssl info: %w", err) + } + return nil +} diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 2c34b1c..3fc348d 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -25,4 +25,7 @@ type Store interface { AddMaintenance(model.Maintenance) (*model.Maintenance, error) UpdateMaintenance(int, model.Maintenance) (*model.Maintenance, error) DeleteMaintenance(int) error + + GetSSLInfo(int) (*model.SSLInfo, error) + SaveSSLInfo(model.SSLInfo) error } |