Production hardening: security, resilience, observability, and compliance
Password complexity: custom validator requiring uppercase, lowercase, digit (min 8 chars)
Token expiry: 90-day token lifetime with refresh endpoint (60-90 day renewal window)
Health check: /api/health/ now pings Postgres + Redis, returns 503 on failure
Audit logging: async audit_log table for auth events (login, register, delete, etc.)
Circuit breaker: APNs/FCM push sends wrapped with 5-failure threshold, 30s recovery
FK indexes: 27 missing foreign key indexes across all tables (migration 017)
CSP header: default-src 'none'; frame-ancestors 'none'
Gzip compression: level 5 with media endpoint skipper
Prometheus metrics: /metrics endpoint using existing monitoring service
External timeouts: 15s push, 30s SMTP, context timeouts on all external calls
Migrations: 016 (token created_at), 017 (FK indexes), 018 (audit_log)
Tests: circuit breaker (15), audit service (8), token refresh (7), health (4),
middleware expiry (5), validator (new)
This commit is contained in:
2
migrations/016_authtoken_created_at.down.sql
Normal file
2
migrations/016_authtoken_created_at.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- No-op: the created column is part of Django's original schema and should not
|
||||
-- be removed.
|
||||
6
migrations/016_authtoken_created_at.up.sql
Normal file
6
migrations/016_authtoken_created_at.up.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Ensure created column exists on user_authtoken (Django already creates it,
|
||||
-- but this migration guarantees it for fresh Go-only deployments).
|
||||
ALTER TABLE user_authtoken ADD COLUMN IF NOT EXISTS created TIMESTAMP WITH TIME ZONE DEFAULT NOW();
|
||||
|
||||
-- Backfill any rows that may have a NULL created timestamp.
|
||||
UPDATE user_authtoken SET created = NOW() WHERE created IS NULL;
|
||||
40
migrations/017_fk_indexes.down.sql
Normal file
40
migrations/017_fk_indexes.down.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- Rollback: 017_fk_indexes
|
||||
-- Drop all FK indexes added in the up migration.
|
||||
|
||||
-- auth / user tables
|
||||
DROP INDEX IF EXISTS idx_authtoken_user_id;
|
||||
DROP INDEX IF EXISTS idx_userprofile_user_id;
|
||||
DROP INDEX IF EXISTS idx_confirmationcode_user_id;
|
||||
DROP INDEX IF EXISTS idx_passwordresetcode_user_id;
|
||||
DROP INDEX IF EXISTS idx_applesocialauth_user_id;
|
||||
DROP INDEX IF EXISTS idx_googlesocialauth_user_id;
|
||||
|
||||
-- push notification device tables
|
||||
DROP INDEX IF EXISTS idx_apnsdevice_user_id;
|
||||
DROP INDEX IF EXISTS idx_gcmdevice_user_id;
|
||||
|
||||
-- notification tables
|
||||
DROP INDEX IF EXISTS idx_notificationpreference_user_id;
|
||||
|
||||
-- subscription tables
|
||||
DROP INDEX IF EXISTS idx_subscription_user_id;
|
||||
|
||||
-- residence tables
|
||||
DROP INDEX IF EXISTS idx_residence_owner_id;
|
||||
DROP INDEX IF EXISTS idx_sharecode_residence_id;
|
||||
DROP INDEX IF EXISTS idx_sharecode_created_by_id;
|
||||
|
||||
-- task tables
|
||||
DROP INDEX IF EXISTS idx_task_created_by_id;
|
||||
DROP INDEX IF EXISTS idx_task_assigned_to_id;
|
||||
DROP INDEX IF EXISTS idx_task_category_id;
|
||||
DROP INDEX IF EXISTS idx_task_priority_id;
|
||||
DROP INDEX IF EXISTS idx_task_frequency_id;
|
||||
DROP INDEX IF EXISTS idx_task_contractor_id;
|
||||
DROP INDEX IF EXISTS idx_task_parent_task_id;
|
||||
DROP INDEX IF EXISTS idx_completionimage_completion_id;
|
||||
DROP INDEX IF EXISTS idx_document_created_by_id;
|
||||
DROP INDEX IF EXISTS idx_document_task_id;
|
||||
DROP INDEX IF EXISTS idx_documentimage_document_id;
|
||||
DROP INDEX IF EXISTS idx_contractor_residence_id;
|
||||
DROP INDEX IF EXISTS idx_reminderlog_notification_id;
|
||||
131
migrations/017_fk_indexes.up.sql
Normal file
131
migrations/017_fk_indexes.up.sql
Normal file
@@ -0,0 +1,131 @@
|
||||
-- Migration: 017_fk_indexes
|
||||
-- Add indexes on all foreign key columns that are not already covered by existing indexes.
|
||||
-- Uses CREATE INDEX IF NOT EXISTS to be idempotent (safe to re-run).
|
||||
|
||||
-- =====================================================
|
||||
-- auth / user tables
|
||||
-- =====================================================
|
||||
|
||||
-- user_authtoken: user_id (unique FK, but ensure index exists)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_authtoken_user_id
|
||||
ON user_authtoken (user_id);
|
||||
|
||||
-- user_userprofile: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_userprofile_user_id
|
||||
ON user_userprofile (user_id);
|
||||
|
||||
-- user_confirmationcode: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_confirmationcode_user_id
|
||||
ON user_confirmationcode (user_id);
|
||||
|
||||
-- user_passwordresetcode: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_passwordresetcode_user_id
|
||||
ON user_passwordresetcode (user_id);
|
||||
|
||||
-- user_applesocialauth: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_applesocialauth_user_id
|
||||
ON user_applesocialauth (user_id);
|
||||
|
||||
-- user_googlesocialauth: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_googlesocialauth_user_id
|
||||
ON user_googlesocialauth (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- push notification device tables
|
||||
-- =====================================================
|
||||
|
||||
-- push_notifications_apnsdevice: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_apnsdevice_user_id
|
||||
ON push_notifications_apnsdevice (user_id);
|
||||
|
||||
-- push_notifications_gcmdevice: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_gcmdevice_user_id
|
||||
ON push_notifications_gcmdevice (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- notification tables
|
||||
-- =====================================================
|
||||
|
||||
-- notifications_notificationpreference: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_notificationpreference_user_id
|
||||
ON notifications_notificationpreference (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- subscription tables
|
||||
-- =====================================================
|
||||
|
||||
-- subscription_usersubscription: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_subscription_user_id
|
||||
ON subscription_usersubscription (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- residence tables
|
||||
-- =====================================================
|
||||
|
||||
-- residence_residence: owner_id
|
||||
CREATE INDEX IF NOT EXISTS idx_residence_owner_id
|
||||
ON residence_residence (owner_id);
|
||||
|
||||
-- residence_residencesharecode: residence_id (may already exist from model index tag via GORM)
|
||||
CREATE INDEX IF NOT EXISTS idx_sharecode_residence_id
|
||||
ON residence_residencesharecode (residence_id);
|
||||
|
||||
-- residence_residencesharecode: created_by_id
|
||||
CREATE INDEX IF NOT EXISTS idx_sharecode_created_by_id
|
||||
ON residence_residencesharecode (created_by_id);
|
||||
|
||||
-- =====================================================
|
||||
-- task tables
|
||||
-- =====================================================
|
||||
|
||||
-- task_task: created_by_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_created_by_id
|
||||
ON task_task (created_by_id);
|
||||
|
||||
-- task_task: assigned_to_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_assigned_to_id
|
||||
ON task_task (assigned_to_id);
|
||||
|
||||
-- task_task: category_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_category_id
|
||||
ON task_task (category_id);
|
||||
|
||||
-- task_task: priority_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_priority_id
|
||||
ON task_task (priority_id);
|
||||
|
||||
-- task_task: frequency_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_frequency_id
|
||||
ON task_task (frequency_id);
|
||||
|
||||
-- task_task: contractor_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_contractor_id
|
||||
ON task_task (contractor_id);
|
||||
|
||||
-- task_task: parent_task_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_parent_task_id
|
||||
ON task_task (parent_task_id);
|
||||
|
||||
-- task_taskcompletionimage: completion_id
|
||||
CREATE INDEX IF NOT EXISTS idx_completionimage_completion_id
|
||||
ON task_taskcompletionimage (completion_id);
|
||||
|
||||
-- task_document: created_by_id
|
||||
CREATE INDEX IF NOT EXISTS idx_document_created_by_id
|
||||
ON task_document (created_by_id);
|
||||
|
||||
-- task_document: task_id
|
||||
CREATE INDEX IF NOT EXISTS idx_document_task_id
|
||||
ON task_document (task_id);
|
||||
|
||||
-- task_documentimage: document_id
|
||||
CREATE INDEX IF NOT EXISTS idx_documentimage_document_id
|
||||
ON task_documentimage (document_id);
|
||||
|
||||
-- task_contractor: residence_id
|
||||
CREATE INDEX IF NOT EXISTS idx_contractor_residence_id
|
||||
ON task_contractor (residence_id);
|
||||
|
||||
-- task_reminderlog: notification_id
|
||||
CREATE INDEX IF NOT EXISTS idx_reminderlog_notification_id
|
||||
ON task_reminderlog (notification_id);
|
||||
2
migrations/018_audit_log.down.sql
Normal file
2
migrations/018_audit_log.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Rollback: 018_audit_log
|
||||
DROP TABLE IF EXISTS audit_log;
|
||||
16
migrations/018_audit_log.up.sql
Normal file
16
migrations/018_audit_log.up.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Migration: 018_audit_log
|
||||
-- Create audit_log table for tracking security-relevant events (login, register, etc.)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
event_type VARCHAR(50) NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
details JSONB,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_user_id ON audit_log(user_id);
|
||||
CREATE INDEX idx_audit_log_event_type ON audit_log(event_type);
|
||||
CREATE INDEX idx_audit_log_created_at ON audit_log(created_at);
|
||||
Reference in New Issue
Block a user