Cybersecurity-Projects/PROJECTS/beginner/canary-token-generator/backend/internal/admin/handler.go

258 lines
5.6 KiB
Go

// ©AngelaMos | 2026
// handler.go
package admin
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
)
const (
urlParamID = "id"
queryParamOffset = "offset"
queryParamLimit = "limit"
defaultPageSize = 50
maxPageSize = 100
headerContentType = "Content-Type"
contentTypeJSON = "application/json"
errorCodeNotFound = "NOT_FOUND"
errorCodeBadParam = "BAD_PARAM"
errorCodeInternalError = "INTERNAL_ERROR"
respMessageNotFound = "not found"
respMessageBadOffset = "invalid offset"
respMessageInternalError = "internal server error"
)
type TokenRepository interface {
ListAll(ctx context.Context, opts token.ListOptions) ([]token.Token, error)
CountAll(ctx context.Context) (int64, error)
CountByType(ctx context.Context) ([]token.TypeCount, error)
CountByAlertChannel(ctx context.Context) ([]token.ChannelCount, error)
SetEnabled(ctx context.Context, id string, enabled bool) error
}
type EventRepository interface {
CountAll(ctx context.Context) (int64, error)
}
type URLBuilder interface {
TriggerURL(id string) string
ManageURL(manageID string) string
}
type Handler struct {
tokens TokenRepository
events EventRepository
urls URLBuilder
logger *slog.Logger
}
func NewHandler(
tokens TokenRepository,
events EventRepository,
urls URLBuilder,
logger *slog.Logger,
) *Handler {
if logger == nil {
logger = slog.Default()
}
return &Handler{
tokens: tokens,
events: events,
urls: urls,
logger: logger,
}
}
func (h *Handler) RegisterRoutes(r chi.Router) {
r.Get("/stats", h.GetStats)
r.Get("/tokens", h.ListTokens)
r.Post("/tokens/{"+urlParamID+"}/disable", h.DisableToken)
}
func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tokensCount, err := h.tokens.CountAll(ctx)
if err != nil {
h.logger.ErrorContext(ctx, "admin: count tokens", "error", err)
h.writeInternal(w)
return
}
eventsCount, err := h.events.CountAll(ctx)
if err != nil {
h.logger.ErrorContext(ctx, "admin: count events", "error", err)
h.writeInternal(w)
return
}
byType, err := h.tokens.CountByType(ctx)
if err != nil {
h.logger.ErrorContext(ctx, "admin: count by type", "error", err)
h.writeInternal(w)
return
}
byChannel, err := h.tokens.CountByAlertChannel(ctx)
if err != nil {
h.logger.ErrorContext(ctx, "admin: count by channel", "error", err)
h.writeInternal(w)
return
}
stats := Stats{
TokensCount: tokensCount,
EventsCount: eventsCount,
ByType: byType,
ByAlertChannel: byChannel,
}
h.writeJSON(w, http.StatusOK, envelopeData(stats))
}
func (h *Handler) ListTokens(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
offset, err := parseOffset(r.URL.Query().Get(queryParamOffset))
if err != nil {
h.writeJSON(w, http.StatusBadRequest, envelopeError(
errorCodeBadParam, respMessageBadOffset,
))
return
}
limit := parseLimit(r.URL.Query().Get(queryParamLimit))
rows, err := h.tokens.ListAll(ctx, token.ListOptions{
Limit: limit,
Offset: offset,
})
if err != nil {
h.logger.ErrorContext(ctx, "admin: list tokens", "error", err)
h.writeInternal(w)
return
}
total, err := h.tokens.CountAll(ctx)
if err != nil {
h.logger.ErrorContext(ctx, "admin: count tokens", "error", err)
h.writeInternal(w)
return
}
out := make([]token.Response, 0, len(rows))
for i := range rows {
out = append(out, rows[i].ToResponse(
h.urls.TriggerURL(rows[i].ID),
h.urls.ManageURL(rows[i].ManageID),
))
}
next := offset + len(rows)
hasMore := int64(next) < total
resp := TokenListResponse{
Tokens: out,
Total: total,
Page: TokenListPage{
NextOffset: next,
HasMore: hasMore,
},
}
h.writeJSON(w, http.StatusOK, envelopeData(resp))
}
func (h *Handler) DisableToken(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, urlParamID)
if id == "" {
h.writeJSON(w, http.StatusNotFound, envelopeError(
errorCodeNotFound, respMessageNotFound,
))
return
}
if err := h.tokens.SetEnabled(r.Context(), id, false); err != nil {
if errors.Is(err, token.ErrNotFound) {
h.writeJSON(w, http.StatusNotFound, envelopeError(
errorCodeNotFound, respMessageNotFound,
))
return
}
h.logger.ErrorContext(r.Context(), "admin: disable token",
"error", err, "token_id", id)
h.writeInternal(w)
return
}
w.WriteHeader(http.StatusNoContent)
}
func parseOffset(raw string) (int, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, nil
}
v, err := strconv.Atoi(raw)
if err != nil || v < 0 {
return 0, errors.New("invalid offset")
}
return v, nil
}
func parseLimit(raw string) int {
raw = strings.TrimSpace(raw)
if raw == "" {
return defaultPageSize
}
v, err := strconv.Atoi(raw)
if err != nil || v <= 0 {
return defaultPageSize
}
if v > maxPageSize {
return maxPageSize
}
return v
}
func (h *Handler) writeJSON(
w http.ResponseWriter,
status int,
body any,
) {
w.Header().Set(headerContentType, contentTypeJSON)
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(body); err != nil {
h.logger.Warn("write json response", "error", err)
}
}
func (h *Handler) writeInternal(w http.ResponseWriter) {
h.writeJSON(w, http.StatusInternalServerError, envelopeError(
errorCodeInternalError, respMessageInternalError,
))
}
func envelopeData(data any) map[string]any {
return map[string]any{"success": true, "data": data}
}
func envelopeError(code, message string) map[string]any {
return map[string]any{
"success": false,
"error": map[string]any{
"code": code,
"message": message,
},
}
}