This commit is contained in:
Eden Kirin
2024-06-18 22:01:31 +02:00
commit 21dcabe180
21 changed files with 1039 additions and 0 deletions

106
app/lib/cfg/cfg.go Normal file
View File

@ -0,0 +1,106 @@
package cfg
import (
"fmt"
"os"
"github.com/kelseyhightower/envconfig"
"gopkg.in/yaml.v3"
)
type ServiceConfig struct {
Address string `yaml:"address"`
Port int `yaml:"port"`
}
type DatabaseConfig struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
Name string `yaml:"name"`
Username string `yaml:"username"`
Password string `yaml:"password" json:"-"`
DebugSQL bool `yaml:"debugSQL"`
}
type ApplicationConfig struct {
LogLevel string `yaml:"logLevel"`
LogFile string `yaml:"logFile"`
Debug bool `yaml:"debug"`
DebugPrint bool `yaml:"debugPrint"`
DisableSendMail bool `yaml:"disableSendMail"`
IsProduction bool `yaml:"isProduction"`
MetricsPrefix string `yaml:"metricsPrefix"`
}
type S3StorageConfig struct {
AccessKey string `yaml:"accessKey"`
SecretKey string `yaml:"secretKey"`
RegionName string `yaml:"regionName"`
EndpointUrl string `yaml:"endpointUrl"`
BucketName string `yaml:"bucketName"`
CertPath string `yaml:"certPath"`
}
type CertConfig struct {
FinaDemoCaCert string `yaml:"finaDemoCaCert"`
FinaProdCaCert string `yaml:"finaProdCaCert"`
DemoCertOib string `yaml:"demoCertOib"`
}
type configStruct struct {
Service ServiceConfig `yaml:"service"`
Database DatabaseConfig `yaml:"database"`
Application ApplicationConfig `yaml:"application"`
S3Storage S3StorageConfig `yaml:"s3storage"`
Cert CertConfig `yaml:"cert"`
}
const ENV_PREFIX = "FISKALATOR"
const ENV_CONFIG = ENV_PREFIX + "_CONFIG"
const DEFAULT_CONFIG_FILE = "config.yaml"
var Config configStruct
func panicWithError(err error) {
panic("Config file error: " + err.Error())
}
func readFile(cfgFile string, cfg *configStruct) {
f, err := os.Open(cfgFile)
if err != nil {
panicWithError(err)
}
defer f.Close()
decoder := yaml.NewDecoder(f)
err = decoder.Decode(cfg)
if err != nil {
panicWithError(err)
}
}
func readEnv(cfg *configStruct) {
err := envconfig.Process(ENV_PREFIX, cfg)
if err != nil {
panicWithError(err)
}
}
func Init() {
cfgFile := os.Getenv(ENV_CONFIG)
if cfgFile == "" {
cfgFile = DEFAULT_CONFIG_FILE
}
readFile(cfgFile, &Config)
readEnv(&Config)
maskedCfg := Config
maskedCfg.Database.Password = "**password hidden**"
maskedCfg.S3Storage.AccessKey = "**access key hidden**"
maskedCfg.S3Storage.SecretKey = "**secret key hidden**"
fmt.Println("--- CONFIG -------------------------------")
fmt.Printf("%+v\n", maskedCfg)
fmt.Println("------------------------------------------")
}

65
app/lib/db/db.go Normal file
View File

@ -0,0 +1,65 @@
package db
import (
"fmt"
"repo-pattern/app/lib/cfg"
"repo-pattern/app/lib/logging"
"strconv"
"strings"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
)
const CONNECTION_MAX_IDLE_TIME = time.Minute * 1
const DB_CONNECTION_TIMEOUT = 5
var DBConn *gorm.DB
func ConnectToDb(config *cfg.DatabaseConfig) (*gorm.DB, error) {
var connectionString = strings.Join([]string{
"postgres://",
config.Username, ":",
config.Password, "@",
config.Host, ":",
config.Port, "/",
config.Name,
"?sslmode=disable",
"&TimeZone=UTC",
"&connect_timeout=", strconv.Itoa(DB_CONNECTION_TIMEOUT),
}, "")
var logLevel gormLogger.LogLevel
if config.DebugSQL {
logLevel = gormLogger.Info
} else {
logLevel = gormLogger.Silent
}
conn, err := gorm.Open(postgres.Open(connectionString), &gorm.Config{
Logger: gormLogger.Default.LogMode(logLevel),
})
if err != nil {
return nil, err
}
return conn, nil
}
func InitDB() *gorm.DB {
var err error
DBConn, err = ConnectToDb(&cfg.Config.Database)
if err != nil {
msg := fmt.Sprintf("Error connecting to database: %s. Terminating!", err)
logging.Log.Error(msg)
panic(msg)
}
// set connection autodisconnect after idle time
db, _ := DBConn.DB()
db.SetConnMaxIdleTime(CONNECTION_MAX_IDLE_TIME)
return DBConn
}

View File

@ -0,0 +1,39 @@
package helpers
import (
"reflect"
"gorm.io/gorm"
)
type Factory struct {
dbConn *gorm.DB
}
func NewFactory(dbConn *gorm.DB) *Factory {
return &Factory{
dbConn: dbConn,
}
}
func (f *Factory) CreateModel(model interface{}) {
if f.dbConn == nil {
panic("Factory created without db connection. Unable to create model.")
}
f.dbConn.Create(model)
}
func MergeValuesToModel(model interface{}, values map[string]any) {
st := reflect.ValueOf(model).Elem()
for key, value := range values {
field := st.FieldByName(key)
var v reflect.Value
if value != nil {
v = reflect.ValueOf(value)
} else {
v = reflect.Zero(field.Type())
}
field.Set(v)
}
}

27
app/lib/helpers/util.go Normal file
View File

@ -0,0 +1,27 @@
package helpers
import (
"encoding/json"
"time"
)
func UTCNow() time.Time {
utc, _ := time.LoadLocation("UTC")
return time.Now().In(utc)
}
// UNUSED allows unused variables to be included in Go programs
func UNUSED(x ...interface{}) {}
func StructToMap(data interface{}) (map[string]interface{}, error) {
dataBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
mapData := make(map[string]interface{})
err = json.Unmarshal(dataBytes, &mapData)
if err != nil {
return nil, err
}
return mapData, nil
}

View File

@ -0,0 +1,70 @@
package helpers
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_StructToMap(t *testing.T) {
t.Run("Convert simple struct to map", func(t *testing.T) {
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Height float32 `json:"height"`
}
user := &User{
Name: "Pero",
Age: 66,
Height: 187.6,
}
result, _ := StructToMap(user)
expected := make(map[string]interface{})
expected["name"] = "Pero"
expected["age"] = 66
expected["height"] = 187.6
assert.Equal(t, fmt.Sprintf("%v", result), fmt.Sprintf("%v", expected))
})
t.Run("Convert nested struct to map", func(t *testing.T) {
type Address struct {
City string `json:"city"`
PostalCode int `json:"postalCode"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Height float32 `json:"height"`
Address Address `json:"address"`
}
user := &User{
Name: "Pero",
Age: 66,
Height: 187.6,
Address: Address{
City: "Zagreb",
PostalCode: 10020,
},
}
result, _ := StructToMap(user)
address := make(map[string]interface{})
address["city"] = "Zagreb"
address["postalCode"] = 10020
expected := make(map[string]interface{})
expected["name"] = "Pero"
expected["age"] = 66
expected["height"] = 187.6
expected["address"] = make(map[string]interface{})
expected["address"] = address
assert.Equal(t, fmt.Sprintf("%v", result), fmt.Sprintf("%v", expected))
})
}

View File

@ -0,0 +1,65 @@
package logging
import (
"repo-pattern/app/lib/cfg"
"go.uber.org/zap"
)
var Log *zap.Logger = nil
func Init() {
var err error
logConfig := zap.NewProductionConfig()
logConfig.Level, err = zap.ParseAtomicLevel(cfg.Config.Application.LogLevel)
if err != nil {
panic(err)
}
logConfig.OutputPaths = []string{
cfg.Config.Application.LogFile,
"stderr",
}
Log, err = logConfig.Build()
if err != nil {
panic(err)
}
/*
logLevel, err := logrus.ParseLevel(cfg.Config.Application.LogLevel)
if err != nil {
panic(fmt.Sprintf("Invalid configured logLevel: %s\n", cfg.Config.Application.LogLevel))
}
Log.SetLevel(logLevel)
Log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
PadLevelText: true,
DisableQuote: true,
})
LogFile := cfg.Config.Application.LogFile
file, err := os.OpenFile(
LogFile,
os.O_CREATE|os.O_WRONLY|os.O_APPEND,
0655,
)
if err != nil {
msg := fmt.Sprintf("Failed to log to file %s: %s", cfg.Config.Application.LogFile, err)
Log.Warning(msg)
panic(msg)
}
mw := io.MultiWriter(os.Stdout, file)
Log.SetOutput(mw)
configJson, err := json.Marshal(cfg.Config)
if err == nil {
Info(fmt.Sprintf("Using config: %s", configJson))
}
*/
}

20
app/main.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"fmt"
"repo-pattern/app/lib/cfg"
"repo-pattern/app/lib/db"
"repo-pattern/app/lib/logging"
"repo-pattern/app/repository"
)
func main() {
cfg.Init()
logging.Init()
defer logging.Log.Sync()
db := db.InitDB()
repository.Dao = repository.CreateDAO(db)
fmt.Println("Running...")
}

23
app/models/api_key.go Normal file
View File

@ -0,0 +1,23 @@
package models
import (
"time"
"github.com/google/uuid"
)
type ApiKey struct {
Key uuid.UUID `gorm:"type(uuid);unique;default:uuid_generate_v4()"`
CompanyId uuid.UUID `gorm:"type(uuid)" faker:"-"`
ActiveFrom *time.Time `faker:"-"`
ActiveTo *time.Time `faker:"-"`
LastUsed *time.Time
CreatedAt time.Time `faker:"-"`
UpdatedAt time.Time `faker:"-"`
Company Company `faker:"-"`
}
func (m *ApiKey) TableName() string {
return "api_keys"
}

28
app/models/cert.go Normal file
View File

@ -0,0 +1,28 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Cert struct {
Id uuid.UUID `gorm:"type(uuid);unique;default:uuid_generate_v4()"`
CompanyId uuid.UUID `gorm:"type(uuid)" faker:"-"`
Company Company `faker:"-"`
Alive bool `faker:"-"`
SerialNumber string
Issuer string
Subject string
ValidAfter time.Time `faker:"-"`
ValidBefore time.Time `faker:"-"`
CertFile string
KeyFile string
KeyPassword string
CreatedAt time.Time `faker:"-"`
UpdatedAt time.Time `faker:"-"`
}
func (m *Cert) TableName() string {
return "certificates"
}

30
app/models/company.go Normal file
View File

@ -0,0 +1,30 @@
package models
import (
"time"
"github.com/google/uuid"
"github.com/mozillazg/go-slugify"
)
type Company struct {
Id uuid.UUID `gorm:"type(uuid);unique;default:uuid_generate_v4()"`
CertificateId *uuid.UUID `gorm:"type(uuid)" faker:"-"`
Name string `faker:"name"`
Address string
City string
Email string `faker:"email"`
Oib string `faker:"-"`
TaxSystem bool
IsActive bool `faker:"-"`
CreatedAt time.Time `faker:"-"`
UpdatedAt time.Time `faker:"-"`
}
func (m *Company) TableName() string {
return "companies"
}
func (m *Company) SlugifyName() string {
return slugify.Slugify(m.Name)
}

View File

@ -0,0 +1,27 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/datatypes"
)
type FiskLogItem struct {
Id uuid.UUID `gorm:"type(uuid);unique"`
ApiKeyId uuid.UUID `gorm:"type(uuid)"`
CompanyId uuid.UUID `gorm:"type(uuid)"`
IsDemo bool
RequestData datatypes.JSONMap `gorm:"type:jsonb;not null"`
ResponseData *datatypes.JSONMap `gorm:"type:jsonb"`
IznosUkupno int
Zki *string
Jir *uuid.UUID `gorm:"type(uuid)"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (m *FiskLogItem) TableName() string {
return "fisk_log"
}

View File

@ -0,0 +1,89 @@
package repository
import (
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ApiKeysRepository struct {
db *gorm.DB
}
type ApiKeysFilter struct {
Key *uuid.UUID
CompanyId *uuid.UUID
IsActive *bool
CompanyIsActive *bool
}
func CreateApiKeysRepository(db *gorm.DB) *ApiKeysRepository {
return &ApiKeysRepository{db}
}
func applyApiKeyFilter(db *gorm.DB, query *gorm.DB, filter *ApiKeysFilter) *gorm.DB {
if filter.Key != nil {
query.Where("key = ?", *filter.Key)
}
if filter.IsActive != nil {
now := helpers.UTCNow()
if *filter.IsActive {
query.Where("active_from IS NULL OR active_from <= ?", now)
query.Where("active_to IS NULL OR active_to >= ?", now)
} else {
query.Where(
"active_from IS NOT NULL AND active_from >= ?", now,
).Or(
"active_to IS NOT NULL AND active_to =< ?", now,
)
}
}
if filter.CompanyId != nil {
query.Where("company_id = ?", *filter.CompanyId)
}
if filter.CompanyIsActive != nil {
query.Joins("Company").Where(
map[string]interface{}{"Company.is_active": *filter.CompanyIsActive},
)
}
return query
}
func (r *ApiKeysRepository) New(companyId uuid.UUID) *models.ApiKey {
now := helpers.UTCNow()
return &models.ApiKey{
CompanyId: companyId,
ActiveFrom: &now,
CreatedAt: now,
UpdatedAt: now,
}
}
func (r *ApiKeysRepository) Get(filter *ApiKeysFilter) *models.ApiKey {
var api_key models.ApiKey
query := r.db.Model(&models.ApiKey{})
applyApiKeyFilter(r.db, query, filter)
result := query.First(&api_key).Joins("Company")
if result.Error != nil {
return nil
}
return &api_key
}
func (r *ApiKeysRepository) Save(model *models.ApiKey) {
model.UpdatedAt = helpers.UTCNow()
// we don't have standard model Id, but Key as primary key.
// this could be possible reason for save not assigning generated key uuid
// to model.Key. but this works.
if model.Key == uuid.Nil {
r.db.Create(model)
} else {
r.db.Save(model)
}
}

101
app/repository/certs.go Normal file
View File

@ -0,0 +1,101 @@
package repository
import (
"errors"
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CertRepository struct {
db *gorm.DB
}
type CertFilter struct {
Alive *bool
Id *uuid.UUID
CompanyId *uuid.UUID
CompanyIsActive *bool
}
func CreateCertRepository(db *gorm.DB) *CertRepository {
return &CertRepository{db}
}
func applyCertFilter(db *gorm.DB, query *gorm.DB, filter *CertFilter) *gorm.DB {
if filter.Id != nil {
query.Where("certificates.id = ?", *filter.Id)
}
if filter.Alive == nil {
query.Where("certificates.alive = TRUE")
} else {
query.Where("certificates.alive = ?", *filter.Alive)
}
if filter.CompanyId != nil {
query.Where("certificates.company_id = ?", *filter.CompanyId)
}
if filter.CompanyIsActive != nil {
query.Joins("LEFT JOIN companies on companies.is_active = TRUE")
// query.Joins("Company").Where(
// map[string]interface{}{"Company.is_active": *filter.CompanyIsActive},
// )
}
return query
}
func (r *CertRepository) New(companyId uuid.UUID) *models.Cert {
now := helpers.UTCNow()
return &models.Cert{
Alive: true,
CompanyId: companyId,
CreatedAt: now,
UpdatedAt: now,
}
}
func (r *CertRepository) Get(filter *CertFilter) *models.Cert {
var cert models.Cert
query := r.db.Model(&models.Cert{})
applyCertFilter(r.db, query, filter)
result := query.First(&cert).Joins("Company")
if result.Error != nil {
return nil
}
return &cert
}
func (r *CertRepository) GetForCompany(companyId uuid.UUID) *models.Cert {
isAlive := true
filter := CertFilter{
Alive: &isAlive,
CompanyId: &companyId,
}
return r.Get(&filter)
}
func (r *CertRepository) Save(model *models.Cert) {
model.UpdatedAt = helpers.UTCNow()
r.db.Save(model)
}
func (r *CertRepository) Exists(filter *CertFilter) bool {
var row models.Cert
query := r.db.Model(&models.Cert{})
query = applyCertFilter(r.db, query, filter)
result := query.Select("certificates.id").First(&row)
return !errors.Is(result.Error, gorm.ErrRecordNotFound) && result.Error == nil
}
func (r *CertRepository) Delete(filter *CertFilter) {
query := r.db.Model(&models.Cert{})
applyCertFilter(r.db, query, filter)
query.Update("alive", false).Joins("Company")
}

View File

@ -0,0 +1,57 @@
package repository
import (
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CompanyRepository struct {
db *gorm.DB
}
type CompanyFilter struct {
Id *uuid.UUID
}
func CreateCompanyRepository(db *gorm.DB) *CompanyRepository {
return &CompanyRepository{db}
}
func applyCompanyFilter(db *gorm.DB, query *gorm.DB, filter *CompanyFilter) *gorm.DB {
if filter.Id != nil {
query.Where("id = ?", *filter.Id)
}
query.Where("is_active = ?", true)
return query
}
func (r *CompanyRepository) New() *models.Company {
now := helpers.UTCNow()
return &models.Company{
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
}
func (r *CompanyRepository) Get(filter *CompanyFilter) *models.Company {
var company models.Company
query := r.db.Model(&models.Company{})
applyCompanyFilter(r.db, query, filter)
result := query.First(&company)
if result.Error != nil {
return nil
}
return &company
}
func (r *CompanyRepository) Save(model *models.Company) {
model.UpdatedAt = helpers.UTCNow()
r.db.Save(model)
}

23
app/repository/dao.go Normal file
View File

@ -0,0 +1,23 @@
package repository
import "gorm.io/gorm"
type DAO struct {
db *gorm.DB
ApiKeysRepository *ApiKeysRepository
CertRepository *CertRepository
CompanyRepository *CompanyRepository
FiskLogRepository *FiskLogRepository
}
var Dao DAO
func CreateDAO(db *gorm.DB) DAO {
return DAO{
db: db,
ApiKeysRepository: CreateApiKeysRepository(db),
CertRepository: CreateCertRepository(db),
CompanyRepository: CreateCompanyRepository(db),
FiskLogRepository: CreateFiskLogRepository(db),
}
}

View File

@ -0,0 +1,29 @@
package repository
import (
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"gorm.io/gorm"
)
type FiskLogRepository struct {
db *gorm.DB
}
func CreateFiskLogRepository(db *gorm.DB) *FiskLogRepository {
return &FiskLogRepository{db}
}
func (r *FiskLogRepository) New(model models.FiskLogItem) *models.FiskLogItem {
now := helpers.UTCNow()
model.CreatedAt = now
model.UpdatedAt = now
return &model
}
func (r *FiskLogRepository) Save(model *models.FiskLogItem) error {
model.UpdatedAt = helpers.UTCNow()
result := r.db.Save(model)
return result.Error
}