fix(canary-phase1): clear all post-phase-1 audit observations + header normalization

No 'logged for later' — every non-blocking finding from the Phase 1 audits
is fixed now, in this commit, before declaring the phase truly closed.

Code cleanups (5 items):
1. Database.SQLDB() *sql.DB accessor on core.Database; main.go uses
   db.SQLDB() instead of the awkward db.DB.DB triple-chain.
2. Hoisted list-default literals (50, 20) to package-level
   defaultListLimit consts in token + event repositories. MEMORY.md
   'no magic numbers' rule honored.
3. Moved ptr[T any] helper to internal/testutil/ptr.go as testutil.Ptr;
   removed local copies from token + event repository_test.go.
4. Renamed CHECK constraints in 0001_create_tokens.sql to match spec
   text exactly: chk_token_type → chk_type, chk_alert_channel → chk_channel.
   Spec was the original contract; impl now aligns.
5. Resolved APP_ENVIRONMENT vs ENVIRONMENT divergence per spec §12.1:
   - compose.yml: ENVIRONMENT=production → APP_ENVIRONMENT=production
   - dev.compose.yml: ENVIRONMENT=development → APP_ENVIRONMENT=development
   - config.go envKeyMap: ENVIRONMENT → APP_ENVIRONMENT mapping

Concurrency bug fix (real, not just polish):
- core/migrations.go: added sync.Mutex around goose calls. goose's
  package-level state (SetBaseFS, SetDialect) raced under t.Parallel()
  testcontainers tests. Race detector caught it under -race after
  parallel test counts climbed. Mutex serializes goose invocation
  globally; testcontainer-parallelism otherwise unaffected.

Header normalization (50 files):
- Bulk-normalized every project file's header to canonical
  '©AngelaMos | 2026' (no space, with © glyph, year 2026) per MEMORY.md
  style rule. Eliminated 4 distinct non-canonical variants: '// AngelaMos',
  '// ©AngelaMos | 2025', '// © AngelaMos | 202X', '# AngelaMos'.
- Verified clean: grep returns zero non-canonical headers across the
  whole project tree (excluding gitignored docs/ and node_modules/).

Backlog discipline:
- Created docs/plans/BACKLOG.md with strict format: open items only,
  HIGH/MEDIUM/LOW severity, must-clear-before-ship contract.
- BACKLOG currently has zero open items — every observation was fixed
  here, not deferred. Closed-items section logs what was cleared.

Verification:
- go build ./...                                     clean
- go vet ./...                                       clean
- go test -tags=integration -race ./internal/token/...  11/11 PASS
- go test -tags=integration -race ./internal/event/...   9/9 PASS
- docker compose -f compose.yml config              parses
- docker compose -f dev.compose.yml config          parses
- grep for non-canonical headers                     zero matches
This commit is contained in:
CarterPerez-dev 2026-05-10 06:15:26 -04:00
parent 7a98ef8d72
commit 697f4909d7
60 changed files with 85 additions and 69 deletions

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026
# ©AngelaMos | 2026
# .air.toml - Hot reload configuration
root = "."

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026
# ©AngelaMos | 2026
# .golangci.yml
version: "2"

View File

@ -67,7 +67,7 @@ func run(configPath string) error {
}
logger.Info("database connected")
if err := core.RunMigrations(db.DB.DB); err != nil {
if err := core.RunMigrations(db.SQLDB()); err != nil {
return fmt.Errorf("run migrations: %w", err)
}
logger.Info("migrations applied")

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026
# ©AngelaMos | 2026
# config.yaml - Default configuration
app:

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// config.go
package config
@ -199,7 +199,7 @@ func loadDefaults(k *koanf.Koanf) error {
var envKeyMap = map[string]string{
"DATABASE_URL": "database.url",
"REDIS_URL": "redis.url",
"ENVIRONMENT": "app.environment",
"APP_ENVIRONMENT": "app.environment",
"HOST": "server.host",
"PORT": "server.port",
"LOG_LEVEL": "log.level",

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// database.go
package core
@ -20,6 +20,10 @@ type Database struct {
DB *sqlx.DB
}
func (d *Database) SQLDB() *sql.DB {
return d.DB.DB
}
func NewDatabase(
ctx context.Context,
cfg config.DatabaseConfig,

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// errors.go
package core

View File

@ -7,6 +7,7 @@ import (
"database/sql"
"embed"
"fmt"
"sync"
"github.com/pressly/goose/v3"
)
@ -14,7 +15,12 @@ import (
//go:embed migrations/*.sql
var migrationsFS embed.FS
var gooseMu sync.Mutex
func RunMigrations(db *sql.DB) error {
gooseMu.Lock()
defer gooseMu.Unlock()
goose.SetBaseFS(migrationsFS)
if err := goose.SetDialect("postgres"); err != nil {

View File

@ -25,9 +25,9 @@ CREATE TABLE tokens (
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
CONSTRAINT chk_token_type CHECK (type IN
CONSTRAINT chk_type CHECK (type IN
('webbug', 'slowredirect', 'docx', 'pdf', 'kubeconfig', 'envfile', 'mysql')),
CONSTRAINT chk_alert_channel CHECK (alert_channel IN ('telegram', 'webhook')),
CONSTRAINT chk_channel CHECK (alert_channel IN ('telegram', 'webhook')),
CONSTRAINT chk_telegram_complete CHECK (
alert_channel <> 'telegram' OR
(telegram_bot IS NOT NULL AND telegram_chat IS NOT NULL)

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// redis.go
package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// response.go
package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// security.go
package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// telemetry.go
package core

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// validation.go
package core

View File

@ -16,6 +16,8 @@ import (
var ErrNotFound = errors.New("event not found")
const defaultListLimit = 20
type Repository struct {
db *sqlx.DB
}
@ -77,7 +79,7 @@ func (r *Repository) ListByToken(
ctx context.Context, tokenID string, opts ListOptions,
) (ListResult, error) {
if opts.Limit <= 0 {
opts.Limit = 20
opts.Limit = defaultListLimit
}
q := `SELECT ` + selectColumns + `

View File

@ -20,8 +20,6 @@ import (
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
)
func ptr[T any](v T) *T { return &v }
func newRepos(t *testing.T) (*sqlx.DB, *token.Repository, *event.Repository) {
t.Helper()
db := sqlx.NewDb(testutil.NewTestDB(t), "pgx")
@ -36,7 +34,7 @@ func seedToken(t *testing.T, repo *token.Repository, id string) *token.Token {
Type: token.TypeWebbug,
Memo: "event-test",
AlertChannel: token.ChannelWebhook,
WebhookURL: ptr("https://example.com/hook"),
WebhookURL: testutil.Ptr("https://example.com/hook"),
CreatedIP: "203.0.113.1",
CreatedFP: "abcdef0123456789",
Metadata: json.RawMessage(`{}`),
@ -80,7 +78,7 @@ func TestRepository_ListByToken_CursorPagination(t *testing.T) {
e := &event.Event{
TokenID: tok.ID,
SourceIP: "203.0.113.45",
UserAgent: ptr(string(rune('A' + i))),
UserAgent: testutil.Ptr(string(rune('A' + i))),
}
require.NoError(t, evtRepo.Insert(ctx, e))
}
@ -223,7 +221,7 @@ func TestRepository_PruneToLimit_KeepsNewest(t *testing.T) {
var ids []int64
for i := range 5 {
e := &event.Event{TokenID: tok.ID, SourceIP: "203.0.113.45", UserAgent: ptr(string(rune('A' + i)))}
e := &event.Event{TokenID: tok.ID, SourceIP: "203.0.113.45", UserAgent: testutil.Ptr(string(rune('A' + i)))}
require.NoError(t, evtRepo.Insert(ctx, e))
ids = append(ids, e.ID)
time.Sleep(2 * time.Millisecond)

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// handler.go
package health

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// headers.go
package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// logging.go
package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// ratelimit.go
package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// request_id.go
package middleware

View File

@ -1,4 +1,4 @@
// AngelaMos | 2026
// ©AngelaMos | 2026
// server.go
package server

View File

@ -0,0 +1,6 @@
// ©AngelaMos | 2026
// ptr.go
package testutil
func Ptr[T any](v T) *T { return &v }

View File

@ -14,6 +14,8 @@ import (
var ErrNotFound = errors.New("token not found")
const defaultListLimit = 50
type Repository struct {
db *sqlx.DB
}
@ -130,7 +132,7 @@ type ListOptions struct {
func (r *Repository) ListAll(ctx context.Context, opts ListOptions) ([]Token, error) {
if opts.Limit <= 0 {
opts.Limit = 50
opts.Limit = defaultListLimit
}
q := `SELECT ` + selectColumns + ` FROM tokens
ORDER BY created_at DESC

View File

@ -25,8 +25,6 @@ func newRepo(t *testing.T) *token.Repository {
return token.NewRepository(db)
}
func ptr[T any](v T) *T { return &v }
func sampleWebhookToken(id string) *token.Token {
return &token.Token{
ID: id,
@ -34,7 +32,7 @@ func sampleWebhookToken(id string) *token.Token {
Type: token.TypeWebbug,
Memo: "test webbug",
AlertChannel: token.ChannelWebhook,
WebhookURL: ptr("https://example.com/hook"),
WebhookURL: testutil.Ptr("https://example.com/hook"),
CreatedIP: "203.0.113.10",
CreatedFP: "0123456789abcdef",
Metadata: json.RawMessage(`{}`),
@ -48,10 +46,10 @@ func sampleTelegramToken(id string) *token.Token {
ManageID: uuid.New().String(),
Type: token.TypeDocx,
Memo: "Q4 bonuses",
Filename: ptr("Q4_Bonuses_2024.docx"),
Filename: testutil.Ptr("Q4_Bonuses_2024.docx"),
AlertChannel: token.ChannelTelegram,
TelegramBot: ptr("123456:ABCDEF"),
TelegramChat: ptr("-1001234567890"),
TelegramBot: testutil.Ptr("123456:ABCDEF"),
TelegramChat: testutil.Ptr("-1001234567890"),
CreatedIP: "198.51.100.5",
CreatedFP: "fedcba9876543210",
Metadata: json.RawMessage(`{"include_keys":["aws"]}`),

View File

@ -42,7 +42,7 @@ services:
dockerfile: ../infra/docker/canary.prod
container_name: ${APP_NAME:-canary-token-generator}-canary
environment:
- ENVIRONMENT=production
- APP_ENVIRONMENT=production
- DATABASE_URL=postgres://canary:${POSTGRES_PASSWORD}@postgres:5432/canary?sslmode=disable
- REDIS_URL=redis://redis:6379/0
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL}

View File

@ -54,7 +54,7 @@ services:
- canary_gocache:/root/.cache/go-build
- canary_gomodcache:/go/pkg/mod
environment:
- ENVIRONMENT=development
- APP_ENVIRONMENT=development
- DATABASE_URL=postgres://canary:canary@postgres:5432/canary?sslmode=disable
- REDIS_URL=redis://redis:6379/0
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://localhost:58495}

View File

@ -1,4 +1,4 @@
# ©AngelaMos | 2025
# ©AngelaMos | 2026
# .stylelintignore
# Dependencies

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// index.ts
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// index.ts
// ===================

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// index.ts
// ===================

View File

@ -1,4 +1,4 @@
/**
* ©AngelaMos | 2025
* ©AngelaMos | 2026
* index.tsx
*/

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// config.ts
// ===================
const API_VERSION = 'v1'

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// api.config.ts
// ===================

View File

@ -1,5 +1,5 @@
/**
* ©AngelaMos | 2025
* ©AngelaMos | 2026
* errors.ts
*/

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// index.ts
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// query.config.ts
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// routers.tsx
// ===================

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// shell.module.scss
// ===================

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// toast.module.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// index.ts
// ===================

View File

@ -1,5 +1,5 @@
/**
* ©AngelaMos | 2025
* ©AngelaMos | 2026
* ui.store.ts
*/

View File

@ -1,5 +1,5 @@
// ===========================
// ©AngelaMos | 2025
// ©AngelaMos | 2026
// main.tsx
// ===========================

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// dashboard.module.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// index.tsx
// ===================

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// landing.module.scss
// ===================

View File

@ -1,4 +1,4 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// settings.module.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// styles.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// _fonts.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// _index.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// _mixins.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2026
// ©AngelaMos | 2026
// _reset.scss
// ===================

View File

@ -1,5 +1,5 @@
// ===================
// © AngelaMos | 2025
// ©AngelaMos | 2026
// _tokens.scss
// ===================

View File

@ -1,4 +1,4 @@
// ©AngelaMos | 2025
// ©AngelaMos | 2026
// stylelint.config.js
/** @type {import('stylelint').Config} */

View File

@ -1,5 +1,5 @@
/**
* ©AngelaMos | 2025
* ©AngelaMos | 2026
* vite.config.ts
*/

View File

@ -1,5 +1,5 @@
# =============================================================================
# AngelaMos | 2026
# ©AngelaMos | 2026
# dev.nginx
# =============================================================================
# Development server block: proxies to Vite dev server with HMR

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026
# ©AngelaMos | 2026
# nginx.conf
# Development nginx configuration

View File

@ -1,4 +1,4 @@
# AngelaMos | 2026
# ©AngelaMos | 2026
# nginx.prod.conf
# Production nginx configuration

View File

@ -1,5 +1,5 @@
# =============================================================================
# AngelaMos | 2026
# ©AngelaMos | 2026
# prod.nginx
# =============================================================================
# Production server block: serves built static files

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# =============================================================================
# AngelaMos | 2026
# ©AngelaMos | 2026
# randomize-ports.sh
# =============================================================================
# Picks 4 unique random ports (10000-65000) and updates: