Files
admin-panel/internal/database/database.go
coderabbitai[bot] 2adb7e3605 📝 Add docstrings to feat/go-rewrite
Docstrings generation was requested by @blakeridgway.

* https://github.com/RideAware/admin-panel/pull/1#issuecomment-3528008426

The following files were modified:

* `cmd/admin-panel/main.go`
* `internal/config/config.go`
* `internal/database/database.go`
* `internal/email/email.go`
* `internal/handlers/auth.go`
* `internal/handlers/newsletter.go`
* `internal/handlers/subscribers.go`
* `internal/middleware/auth.go`
2025-11-13 14:11:06 +00:00

186 lines
5.3 KiB
Go

package database
import (
"database/sql"
"fmt"
"log"
"net/url"
"github.com/rideaware/admin-panel/internal/config"
_ "github.com/lib/pq"
"golang.org/x/crypto/bcrypt"
)
var db *sql.DB
type Admin struct {
Username string
Password string
}
// Init initializes the package database connection using values from cfg, sets connection pool limits,
// creates required tables, and ensures a default admin user exists.
// It assigns the opened *sql.DB to the package-level db and will terminate the program if establishing
// or verifying the connection fails.
//
// cfg provides PostgreSQL connection parameters and the default admin credentials used to create the
// default admin user when missing.
func Init(cfg *config.Config) {
password := url.QueryEscape(cfg.PGPassword)
psqlInfo := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=require",
cfg.PGUser, password, cfg.PGHost, cfg.PGPort, cfg.PGDatabase,
)
log.Printf("Connecting to database: postgres://%s:***@%s:%s/%s",
cfg.PGUser, cfg.PGHost, cfg.PGPort, cfg.PGDatabase)
var err error
db, err = sql.Open("postgres", psqlInfo)
if err != nil {
log.Fatalf("Database connection error: %v", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
if err = db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
log.Println("Database connection successful!")
createTables()
createDefaultAdmin(cfg)
}
// Close closes the package-level database connection if it has been initialized.
// It is safe to call multiple times; if no connection exists, the call is a no-op.
func Close() {
if db != nil {
db.Close()
}
}
// createTables creates the required database tables if they do not already exist.
// It ensures the subscribers, admin_users, and newsletters tables are present; errors
// encountered while creating individual tables are logged but do not abort the process.
func createTables() {
queries := []string{
`CREATE TABLE IF NOT EXISTS subscribers (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS newsletters (
id SERIAL PRIMARY KEY,
subject TEXT NOT NULL,
body TEXT NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)`,
}
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
log.Printf("Error creating table: %v", err)
}
}
log.Println("Database tables ready.")
}
// GetAllEmails retrieves all subscriber email addresses from the database.
// It returns a slice of email strings and any error encountered while querying or scanning rows.
func GetAllEmails() ([]string, error) {
rows, err := db.Query("SELECT email FROM subscribers")
if err != nil {
log.Printf("Error retrieving emails: %v", err)
return nil, err
}
defer rows.Close()
var emails []string
for rows.Next() {
var email string
if err := rows.Scan(&email); err != nil {
return nil, err
}
emails = append(emails, email)
}
return emails, rows.Err()
}
// GetAdmin retrieves the admin user with the given username.
// It returns a pointer to the Admin when a matching row exists. If no admin is found, it returns an error "admin not found"; other database errors are returned unchanged.
func GetAdmin(username string) (*Admin, error) {
var admin Admin
err := db.QueryRow(
"SELECT username, password FROM admin_users WHERE username=$1",
username,
).Scan(&admin.Username, &admin.Password)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("admin not found")
}
return nil, err
}
return &admin, nil
}
// createDefaultAdmin ensures a default admin user exists by inserting cfg.AdminUsername
// with a bcrypt-hashed cfg.AdminPassword into the admin_users table; the insert is
// idempotent (no-op if the username already exists). If password hashing fails the
// function terminates the process; insertion errors are logged.
func createDefaultAdmin(cfg *config.Config) {
hashedPassword, err := hashPassword(cfg.AdminPassword)
if err != nil {
log.Fatalf("Error hashing password: %v", err)
}
_, err = db.Exec(
"INSERT INTO admin_users (username, password) VALUES ($1, $2) "+
"ON CONFLICT (username) DO NOTHING",
cfg.AdminUsername, hashedPassword,
)
if err != nil {
log.Printf("Error creating default admin: %v", err)
} else {
log.Println("Default admin user ready.")
}
}
// LogNewsletter inserts a newsletter record with the provided subject and body into the newsletters table.
// It returns any error encountered while inserting the record.
func LogNewsletter(subject, body string) error {
_, err := db.Exec(
"INSERT INTO newsletters (subject, body) VALUES ($1, $2)",
subject, body,
)
return err
}
// hashPassword generates a bcrypt hash for the given plaintext password.
// It uses bcrypt.DefaultCost and returns the hashed password as a string and any error encountered.
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword(
[]byte(password),
bcrypt.DefaultCost,
)
return string(hash), err
}
// VerifyPassword reports whether the provided plaintext password matches the given bcrypt hash.
// It returns true if the password matches, false otherwise.
func VerifyPassword(hash, password string) bool {
return bcrypt.CompareHashAndPassword(
[]byte(hash),
[]byte(password),
) == nil
}