Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.26
go-version: '1.26'

- name: Install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
### Feature in the Development
- Add refresh expired token in the cache ( adding the logic for cache )
- Add refresh expired token in the cache ( adding the logic for cache )


go run ./cmd/migrate up

go run ./cmd/migrate down
2 changes: 1 addition & 1 deletion cmd/migrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func loadMigrations(dir, direction string) []migration {
for _, f := range files {
base := filepath.Base(f)
name := strings.TrimSuffix(base, "."+direction+".sql")
content, err := os.ReadFile(f)
content, err := os.ReadFile(f) // #nosec G304 -- CLI tool reading its own SQL files, no user input
if err != nil {
log.Fatalf("ReadFile %q: %v", f, err)
}
Expand Down
15 changes: 15 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/streadway/amqp"
"github.com/vviveksharma/auth/cache"
"github.com/vviveksharma/auth/db"
dbmigrations "github.com/vviveksharma/auth/db/migrations"
"github.com/vviveksharma/auth/initsetup"
"github.com/vviveksharma/auth/internal/controllers"
orgcontrollers "github.com/vviveksharma/auth/internal/controllers/orgControllers"
Expand Down Expand Up @@ -49,6 +50,20 @@ func InitializeSharedResources() (*SharedResources, error) {
log.Println("⚠️ Warning: Error loading .env file", err)
}
db.ConnectDB()

// Run all pending SQL migrations before anything else touches the DB.
// On a fresh/wiped DB this creates every table; on a running system it
// is a no-op (already-applied migrations are skipped).
sqlDB, err := db.DB.DB()
if err != nil {
initError = fmt.Errorf("get underlying sql.DB for migrations: %w", err)
return
}
if err := dbmigrations.RunUp(sqlDB); err != nil {
initError = fmt.Errorf("auto-migration failed: %w", err)
return
}

redisClient := cache.ConnectCache()
if redisClient == nil {
initError = fmt.Errorf("failed to connect to Redis: client is nil")
Expand Down
15 changes: 13 additions & 2 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
"strings"
"time"

"gorm.io/driver/postgres"
Expand All @@ -14,11 +15,21 @@ var DB *gorm.DB

func ConnectDB() {
// Get DB host from environment, default to localhost for local development
dbHost := os.Getenv("DB_HOST")
// Strip newlines from env vars to prevent log injection (G706)
stripNL := func(s string) string {
return strings.Map(func(r rune) rune {
if r == '\n' || r == '\r' {
return -1
}
return r
}, s)
}

dbHost := stripNL(os.Getenv("DB_HOST"))
if dbHost == "" {
dbHost = "localhost"
}
dbPort := os.Getenv("DB_PORT")
dbPort := stripNL(os.Getenv("DB_PORT"))
if dbPort == "" {
dbPort = "26257"
}
Expand Down
144 changes: 144 additions & 0 deletions db/migrations/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package migrations

import (
"database/sql"
"embed"
"fmt"
"log"
"sort"
"strings"
)

//go:embed sql
var sqlFS embed.FS

const migrationsTable = "schema_migrations"

// RunUp applies every pending *.up.sql file that has not yet been recorded in
// schema_migrations. SQL files are embedded at compile time so no filesystem
// path is needed — the binary is fully self-contained in Docker or anywhere else.
// It is idempotent and safe to call on every server startup.
func RunUp(db *sql.DB) error {
if err := ensureMigrationsTable(db); err != nil {
return fmt.Errorf("ensure migrations table: %w", err)
}

files := loadEmbeddedFiles("up")
applied := appliedSet(db)

ran := 0
for _, f := range files {
if applied[f.name] {
continue
}
log.Printf("⬆ Applying migration: %s", f.name)
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("begin tx for %s: %w", f.name, err)
}
// Strip any BEGIN/COMMIT/ROLLBACK the file may contain — the runner
// manages the transaction itself, and CockroachDB rejects nested BEGIN.
if _, err := tx.Exec(stripTransactionWrappers(f.sql)); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
log.Printf("⚠️ rollback failed after migration exec error: %v", rbErr)
}
return fmt.Errorf("execute migration %s: %w", f.name, err)
}
if _, err := tx.Exec(
fmt.Sprintf(`INSERT INTO %s (name) VALUES ($1)`, migrationsTable),
f.name,
); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
log.Printf("⚠️ rollback failed after mark-applied error: %v", rbErr)
}
return fmt.Errorf("mark migration %s applied: %w", f.name, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit migration %s: %w", f.name, err)
}
log.Printf("✅ Migration applied: %s", f.name)
ran++
}

if ran == 0 {
log.Println("✅ All migrations already applied — nothing to do.")
} else {
log.Printf("✅ Applied %d migration(s) successfully.", ran)
}
return nil
}

type migFile struct {
name string
sql string
}

func ensureMigrationsTable(db *sql.DB) error {
_, err := db.Exec(fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`, migrationsTable))
return err
}

func loadEmbeddedFiles(direction string) []migFile {
entries, err := sqlFS.ReadDir("sql")
if err != nil {
log.Fatalf("Cannot read embedded migrations: %v", err)
}

suffix := "." + direction + ".sql"
var files []migFile
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), suffix) {
continue
}
content, err := sqlFS.ReadFile("sql/" + e.Name())
if err != nil {
log.Fatalf("Cannot read embedded migration file %s: %v", e.Name(), err)
}
// Store without direction suffix so up/down share the same key in schema_migrations
name := strings.TrimSuffix(e.Name(), suffix)
files = append(files, migFile{name: name, sql: string(content)})
}

sort.Slice(files, func(i, j int) bool { return files[i].name < files[j].name })
return files
}

// stripTransactionWrappers removes bare BEGIN, COMMIT, and ROLLBACK statements
// from SQL text so migration files that include them for use with external
// tools (psql, migrate CLI) can be run safely inside the runner's own tx.
func stripTransactionWrappers(sql string) string {
lines := strings.Split(sql, "\n")
kept := lines[:0]
for _, line := range lines {
trimmed := strings.TrimSpace(strings.ToUpper(line))
if trimmed == "BEGIN" || trimmed == "BEGIN;" ||
trimmed == "COMMIT" || trimmed == "COMMIT;" ||
trimmed == "ROLLBACK" || trimmed == "ROLLBACK;" {
continue
}
kept = append(kept, line)
}
return strings.Join(kept, "\n")
}

func appliedSet(db *sql.DB) map[string]bool {
rows, err := db.Query(fmt.Sprintf(`SELECT name FROM %s`, migrationsTable))
if err != nil {
log.Fatalf("Query applied migrations: %v", err)
}
defer rows.Close()

applied := map[string]bool{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Fatalf("Scan migration name: %v", err)
}
applied[name] = true
}
return applied
}
11 changes: 11 additions & 0 deletions db/migrations/sql/000014_seed_system_roles.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- =============================================================
-- Migration 000014 DOWN: Remove all gr.* system roles
-- =============================================================

BEGIN;

DELETE FROM route_role_tbl WHERE role_name LIKE 'gr.%';
DELETE FROM role_tbl WHERE role LIKE 'gr.%';
DROP FUNCTION IF EXISTS uuid_generate_v7();

COMMIT;
Loading