init: first init

This commit is contained in:
darwincereska
2026-02-12 19:16:31 -05:00
commit 7d457b373d
33 changed files with 1403 additions and 0 deletions

2
internal/cache/redis.go vendored Normal file
View File

@@ -0,0 +1,2 @@
package cache

View File

@@ -0,0 +1,37 @@
package config
import (
"github.com/charmbracelet/log"
"blog/internal/config/env"
"fmt"
)
type DatabaseConfig struct {
Host string
Port int
Name string
User string
Password string
SSLMode string
TimeZone string
}
func NewDatabaseConfig() *DatabaseConfig {
return &DatabaseConfig{}
}
func (c *DatabaseConfig) LoadConfig() {
c.Host = env.GetString("DB_HOST", "localhost")
c.Port = env.GetInt("DB_PORT", 5432)
c.Name = env.GetString("DB_NAME", "blog")
c.User = env.GetString("DB_USER", "blog")
c.Password = env.GetString("DB_PASSWORD", "blog")
c.SSLMode = env.GetString("DB_SSL_MODE", "disable")
c.TimeZone = env.GetString("DB_TIME_ZONE", "America/New_York")
log.Info("Successfully loaded database config")
}
func (c *DatabaseConfig) GetDSN() string {
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s", c.Host, c.User, c.Password, c.Name, c.Port, c.SSLMode, c.TimeZone)
}

48
internal/config/env/env.go vendored Normal file
View File

@@ -0,0 +1,48 @@
package env
import (
"os"
"strconv"
)
// GetString returns the string value from an environment variable
func GetString(name, defaultValue string) string {
value := os.Getenv(name)
if value == "" {
return defaultValue
}
return value
}
// GetInt returns the int value from an environment variable
func GetInt(name string, defaultValue int) int {
value := os.Getenv(name)
if value == "" {
return defaultValue
}
// Convert string to int
intValue, err := strconv.Atoi(value)
if err != nil {
return defaultValue
}
return intValue
}
// GetBool returns the bool value from an environment variable
func GetBool(name string, defaultValue bool) bool {
value := os.Getenv(name)
if value == "" {
return defaultValue
}
// Convert string to bool
boolValue, err := strconv.ParseBool(value)
if err != nil {
return defaultValue
}
return boolValue
}

View File

@@ -0,0 +1,2 @@
package config

40
internal/config/server.go Normal file
View File

@@ -0,0 +1,40 @@
package config
import (
"github.com/charmbracelet/log"
"blog/internal/config/env"
)
type ServerConfig struct {
Host string
Port int
StrapiHost string
RedisHost string
RedisPort int
StrapiApiKey string
CacheTTL int
EchoMode string
}
func NewServerConfig() *ServerConfig {
return &ServerConfig{}
}
func (c *ServerConfig) LoadConfig() {
c.Host = env.GetString("HOST", "0.0.0.0")
c.Port = env.GetInt("PORT", 3000)
c.StrapiHost = env.GetString("STRAPI_HOST", "https://strapi.darwincereska.dev")
c.StrapiApiKey = env.GetString("STRAPI_API_KEY", "")
c.RedisHost = env.GetString("REDIS_HOST", "localhost")
c.RedisPort = env.GetInt("REDIS_PORT", 6379)
c.CacheTTL = env.GetInt("CACHE_TTL", 3600)
c.EchoMode = env.GetString("ECHO_MODE", "release")
log.Info("Sucessfully loaded server config")
log.Info("Host", "host", c.Host)
log.Info("Port", "port", c.Port)
log.Info("Redis Host", "host", c.RedisHost)
log.Info("Strapi URL", "host", c.StrapiHost)
log.Info("Echo Mode", "mode", c.EchoMode)
log.Info("Cache TTL", "ttl", c.CacheTTL)
}

View File

@@ -0,0 +1,93 @@
package database
import (
"database/sql"
"fmt"
"time"
"log/slog"
"os"
"github.com/charmbracelet/log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type DB struct {
*gorm.DB
}
// Connect connects to database and returns DB
func Connect(dsn string) (*DB, error) {
// Charmbracelet's "log" as a slog handler
charmHandler := log.NewWithOptions(os.Stdout, log.Options{
ReportCaller: false,
ReportTimestamp: true,
Prefix: "GORM",
})
slogger := slog.New(charmHandler)
// Create GORM logger with slog
gormLogger := logger.New(
slog.NewLogLogger(slogger.Handler(), slog.LevelDebug),
logger.Config{
SlowThreshold: time.Millisecond * 200,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: false,
Colorful: true,
},
)
// Connect to database
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormLogger,
})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Get underlying sql.DB to configure connection pooling
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
// Maximum open connections
sqlDB.SetMaxOpenConns(25)
// Maximum idle connections
sqlDB.SetMaxIdleConns(5)
// Maximum lifetime for connections
sqlDB.SetConnMaxLifetime(time.Hour)
// Maximum idle time for connections
sqlDB.SetConnMaxIdleTime(time.Minute * 10)
// Test the connection
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
log.Info("Successfully connected to database")
return &DB{DB: db}, nil
}
// Close closes the database connection
func (db *DB) Close() error {
sqlDB, err := db.DB.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
// GetStats returns database connection stats
func (db *DB) GetStats() sql.DBStats {
sqlDB, _ := db.DB.DB()
return sqlDB.Stats()
}

View File

@@ -0,0 +1,2 @@
package email

View File

@@ -0,0 +1,2 @@
package markdown

View File

@@ -0,0 +1 @@
package models

3
internal/models/api.go Normal file
View File

@@ -0,0 +1,3 @@
package models

79
internal/models/blog.go Normal file
View File

@@ -0,0 +1,79 @@
package models
import "time"
type Post struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
Excerpt string `json:"excerpt"`
FeaturedImage *Media `json:"featured_image"`
Published bool `json:"published"`
PublishedAt *time.Time `json:"published_at"`
MetaTitle string `json:"meta_title"`
MetaDesc string `json:"meta_description"`
ReadingTime int `json:"reading_time"`
Author Author `json:"author"`
Tags []Tag `json:"tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Tag struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Color string `json:"color,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Author struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Bio string `json:"bio"`
Avatar *Media `json:"avatar"`
SocialLinks map[string]string `json:"social_links"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Media struct {
ID uint `json:"id"`
Name string `json:"name"`
AlternativeText string `json:"alternativeText"`
Caption string `json:"caption"`
Width int `json:"width"`
Height int `json:"height"`
Formats MediaFormats `json:"formats"`
Hash string `json:"hash"`
Ext string `json:"ext"`
Mime string `json:"mime"`
Size float64 `json:"size"`
URL string `json:"url"`
PreviewURL string `json:"previewUrl"`
Provider string `json:"provider"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type MediaFormats struct {
Large *MediaFormat `json:"large,omitempty"`
Medium *MediaFormat `json:"medium,omitempty"`
Small *MediaFormat `json:"small,omitempty"`
Thumbnail *MediaFormat `json:"thumbnail,omitempty"`
}
type MediaFormat struct {
Name string `json:"name"`
Hash string `json:"hash"`
Ext string `json:"ext"`
Mime string `json:"mime"`
Width int `json:"width"`
Height int `json:"height"`
Size float64 `json:"size"`
Path string `json:"path"`
URL string `json:"url"`
}

View File

@@ -0,0 +1 @@
package models

View File

@@ -0,0 +1,2 @@
package repositories

View File

@@ -0,0 +1,2 @@
package repositories

6
internal/rss/rss.go Normal file
View File

@@ -0,0 +1,6 @@
package rss
import (
"encoding/xml"
)

1
internal/seo/embed.go Normal file
View File

@@ -0,0 +1 @@
package seo

View File

@@ -0,0 +1,2 @@
package services

View File

@@ -0,0 +1,2 @@
package services

View File

@@ -0,0 +1,2 @@
package services

View File

@@ -0,0 +1,5 @@
package sitemap
import (
"encoding/xml"
)

View File

@@ -0,0 +1,2 @@
package strapi