init: first init
This commit is contained in:
2
internal/cache/redis.go
vendored
Normal file
2
internal/cache/redis.go
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
package cache
|
||||
|
||||
37
internal/config/database.go
Normal file
37
internal/config/database.go
Normal 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
48
internal/config/env/env.go
vendored
Normal 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
|
||||
}
|
||||
2
internal/config/frontend.go
Normal file
2
internal/config/frontend.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package config
|
||||
|
||||
40
internal/config/server.go
Normal file
40
internal/config/server.go
Normal 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)
|
||||
}
|
||||
93
internal/database/connection.go
Normal file
93
internal/database/connection.go
Normal 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()
|
||||
}
|
||||
2
internal/email/service.go
Normal file
2
internal/email/service.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package email
|
||||
|
||||
2
internal/markdown/parser.go
Normal file
2
internal/markdown/parser.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package markdown
|
||||
|
||||
1
internal/models/analytics.go
Normal file
1
internal/models/analytics.go
Normal file
@@ -0,0 +1 @@
|
||||
package models
|
||||
3
internal/models/api.go
Normal file
3
internal/models/api.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package models
|
||||
|
||||
|
||||
79
internal/models/blog.go
Normal file
79
internal/models/blog.go
Normal 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"`
|
||||
}
|
||||
1
internal/models/newsletter.go
Normal file
1
internal/models/newsletter.go
Normal file
@@ -0,0 +1 @@
|
||||
package models
|
||||
2
internal/repositories/analytics.go
Normal file
2
internal/repositories/analytics.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package repositories
|
||||
|
||||
2
internal/repositories/newsletter.go
Normal file
2
internal/repositories/newsletter.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package repositories
|
||||
|
||||
6
internal/rss/rss.go
Normal file
6
internal/rss/rss.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
1
internal/seo/embed.go
Normal file
1
internal/seo/embed.go
Normal file
@@ -0,0 +1 @@
|
||||
package seo
|
||||
2
internal/services/analytics.go
Normal file
2
internal/services/analytics.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package services
|
||||
|
||||
2
internal/services/newsletter.go
Normal file
2
internal/services/newsletter.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package services
|
||||
|
||||
2
internal/services/strapi.go
Normal file
2
internal/services/strapi.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package services
|
||||
|
||||
5
internal/sitemap/sitemap.go
Normal file
5
internal/sitemap/sitemap.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package sitemap
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
2
internal/strapi/client.go
Normal file
2
internal/strapi/client.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package strapi
|
||||
|
||||
Reference in New Issue
Block a user