diff --git a/kontor-echo/internal/config/config.go b/kontor-echo/internal/config/config.go new file mode 100644 index 0000000..09a92d4 --- /dev/null +++ b/kontor-echo/internal/config/config.go @@ -0,0 +1,162 @@ +package config + +import ( + "errors" + "fmt" + "log/slog" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + CfgFile string + Verbose int + Overwrite bool + ServerName string + Mediatype MediaType + Config CliConfig + ServerConfig ServerConfiguration + DryRun bool + FileName string +) + +const ( + mediaTypeMedia MediaType = "media" + mediaTypeVideo MediaType = "video" + mediaTypeLofi MediaType = "lofi" +) + +type MediaType string + +func (e *MediaType) String() string { + return string(*e) +} + +func (e *MediaType) Set(v string) error { + switch v { + case "media", "video", "lofi": + *e = MediaType(v) + return nil + default: + return errors.New(`must be one of "media", "video", "lofi"`) + } +} + +func (e *MediaType) Type() string { + return "MediaType" +} + +func myEnumCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{ + "media\thelp text for media", + "video\thelp text for video", + "lofi\thelp text for lofi", + }, cobra.ShellCompDirectiveDefault +} + +type Server struct { + Url string `mapstructure:"url"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` +} + +type Database struct { + Type string `mapstructure:"type" validate:"required"` + Path string `mapstructure:"path" validate:"required"` + Host string `mapstructure:"host" validate:"required"` + Name string `mapstructure:"name" validate:"required"` + Port int `mapstructure:"port" validate:"required"` + Password string `mapstructure:"password" validate:"required"` + User string `mapstructure:"user" validate:"required"` +} + +type CliConfig struct { + ApiVersion string + Server map[string]Server `mapstructure:"server"` + DB Database `mapstructure:"db"` +} + +type ServerConfiguration struct { + Server struct { + Port int `mapstructure:"port" validate:"required"` + Host string `mapstructure:"host"` + } `mapstructure:"server"` + DB Database `mapstructure:"db"` + Gin struct { + Mode string `mapstructure:"mode" validate:"required"` + } `mapstructure:"gin"` +} + +func InitCliConfig() { + slog.Info("InitCliConfig") + if CfgFile != "" { + viper.SetConfigFile(CfgFile) + } else { + viper.SetConfigName("kli") // without extension + viper.SetConfigType("yaml") + viper.AddConfigPath(".") // look for config in working directory + viper.AddConfigPath("/home/tpeetz/.config") + } + Mediatype = mediaTypeMedia + + // Read the configuration file + if err := viper.ReadInConfig(); err != nil { + // Handle the case where config file is not found + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + slog.Info("No config file found, using defaults") + if err := viper.SafeWriteConfig(); err != nil { + slog.Info("error writing config file", "error", err.Error()) + } + } else { + slog.Info("error reading config file", "error", err.Error()) + } + } else { + slog.Info("InitCliConfig", "Using config file: ", viper.ConfigFileUsed()) + if err := viper.Unmarshal(&Config); err != nil { + slog.Info("parsing of config file failed", "error", err.Error()) + } + } +} + +func InitConfig() error { + + viper.SetConfigName("kontor-gin") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + viper.AddConfigPath("/home/tpeetz/.config") + viper.AddConfigPath("/etc/kontor") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + viper.SetDefault("db.type", "postgres") + viper.SetDefault("db.host", "localhost") + viper.SetDefault("db.port", 5432) + viper.SetDefault("db.name", "kontor") + viper.SetDefault("db.user", "kontor") + viper.SetDefault("db.password", "kontor") + viper.SetDefault("db.path", "data/kontor.db") + viper.SetDefault("server.port", 8550) + viper.SetDefault("gin.mode", "release") + // Read the config file + if err := viper.ReadInConfig(); err != nil { + // Handle the case where config file is not found + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + slog.Info("No config file found, using defaults") + if err := viper.SafeWriteConfig(); err != nil { + slog.Info("error writing config file", "error", err.Error()) + return fmt.Errorf("error writing config file: %w", err) + } + } else { + return fmt.Errorf("error reading config file: %w", err) + } + } else { + slog.Info("InitConfig", "Using config file: ", viper.ConfigFileUsed()) + if err := viper.Unmarshal(&ServerConfig); err != nil { + slog.Info("parsing of config file failed", "error", err.Error()) + } + } + + return nil +} diff --git a/kontor-echo/internal/config/database.go b/kontor-echo/internal/config/database.go new file mode 100644 index 0000000..9f10f1c --- /dev/null +++ b/kontor-echo/internal/config/database.go @@ -0,0 +1,60 @@ +package config + +import ( + "fmt" + "log/slog" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func NewDB(db Database) (*gorm.DB, error) { + db_type := db.Type + slog.Info("using database", "type", db_type) + + switch db_type { + case "postgres": + dsn := fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", + db.Host, + db.User, + db.Password, + db.Name, + db.Port, + ) + + return gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + case "sqlite": + return gorm.Open(sqlite.Open(db.Path), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + default: + return nil, fmt.Errorf("unknown db.type=%s", db_type) + } + // Configure GORM with custom settings + // The logger helps with debugging SQL queries during development + // if err != nil { + // log.Fatal("Failed to connect to database:", err) + // } + + // // Get the underlying SQL database connection for pool configuration + // sqlDB, err := db.DB() + // if err != nil { + // log.Fatal("Failed to get database instance:", err) + // } + + // // Configure connection pool settings + // // MaxIdleConns: number of connections kept open when idle + // sqlDB.SetMaxIdleConns(10) + // // MaxOpenConns: maximum total connections to the database + // sqlDB.SetMaxOpenConns(100) + // // ConnMaxLifetime: how long a connection can be reused + // sqlDB.SetConnMaxLifetime(time.Hour) + + // slog.Info("Database connected successfully") + +} diff --git a/kontor-echo/internal/handlers/admin.go b/kontor-echo/internal/handlers/admin.go new file mode 100644 index 0000000..71620d9 --- /dev/null +++ b/kontor-echo/internal/handlers/admin.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "github.com/uptrace/bun" +) + +type AdminHandler struct { + AuthHandler *AuthHandler + ProfileHandler *ProfileHandler + PermissionHandler *PermissionHandler + AssignmentHandler *AssignmentHandler + TokenHandler *TokenHandler + MailAccountHandler *MailAccountHandler + MailHandler *MailHandler + Secret []byte +} + +func NewAdminHandler(db *bun.DB, secret []byte) *AdminHandler { + adminHandler := &AdminHandler{ + AuthHandler: &AuthHandler{Service: service.NewAuthService(repository.NewProfileRepository(db), secret)}, + ProfileHandler: NewProfileHandler(db), + PermissionHandler: &PermissionHandler{Service: service.NewPermissionService(repository.NewPermissionRepository(db))}, + AssignmentHandler: NewAssignmentHandler(db), + TokenHandler: NewTokenHandler(db), + MailAccountHandler: NewMailAccountHandler(db), + MailHandler: NewMailHandler(db), + Secret: secret, + } + return adminHandler +} diff --git a/kontor-echo/internal/models/media_actor.go b/kontor-echo/internal/models/media_actor.go new file mode 100644 index 0000000..0fd68cf --- /dev/null +++ b/kontor-echo/internal/models/media_actor.go @@ -0,0 +1,21 @@ +package schema + +import ( + "time" + + "github.com/uptrace/bun" +) + +type MediaActor struct { + bun.BaseModel `bun:"table:media_actor"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + Name string `bun:"name"` + WebLink string `bun:"url,unique:url"` + + MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_actor_id"` +} + diff --git a/kontor-echo/internal/models/media_actor_file.go b/kontor-echo/internal/models/media_actor_file.go new file mode 100644 index 0000000..9448989 --- /dev/null +++ b/kontor-echo/internal/models/media_actor_file.go @@ -0,0 +1,21 @@ +package schema + +import ( + "time" + + "github.com/uptrace/bun" +) + +type MediaActorFile struct { + bun.BaseModel `bun:"table:media_actor_file"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + MediaActorID *string `bun:"media_actor_id"` + MediaActor *MediaActor `bun:"rel:belongs-to,join:media_actor_id=id"` + MediaFileID *string `bun:"media_file_id"` + MediaFile *MediaFile `bun:"rel:belongs-to,join:media_file_id=id"` +} + diff --git a/kontor-echo/internal/models/media_article.go b/kontor-echo/internal/models/media_article.go new file mode 100644 index 0000000..f2b5a4f --- /dev/null +++ b/kontor-echo/internal/models/media_article.go @@ -0,0 +1,79 @@ +package schema + +import ( + "time" + + "github.com/uptrace/bun" +) + +type MediaFile struct { + bun.BaseModel `bun:"table:media_file"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + CloudLink string `bun:"cloud_link"` + FileName string `bun:"file_name"` + Path string `bun:"path"` + Review bool `bun:"review"` + Title string `bun:"title"` + WebLink string `bun:"url,unique:url"` + ShouldDownload bool `bun:"should_download"` + + MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_file_id"` +} + +type MediaActor struct { + bun.BaseModel `bun:"table:media_actor"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + Name string `bun:"name"` + WebLink string `bun:"url,unique:url"` + + MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_actor_id"` +} + +type MediaActorFile struct { + bun.BaseModel `bun:"table:media_actor_file"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + MediaActorID *string `bun:"media_actor_id"` + MediaActor *MediaActor `bun:"rel:belongs-to,join:media_actor_id=id"` + MediaFileID *string `bun:"media_file_id"` + MediaFile *MediaFile `bun:"rel:belongs-to,join:media_file_id=id"` +} + +type MediaArticle struct { + bun.BaseModel `bun:"table:media_article"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + Review bool `bun:"review"` + Title string `bun:"title"` + WebLink string `bun:"url,unique:url"` +} + +type MediaVideo struct { + bun.BaseModel `bun:"table:media_article"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + CloudLink string `bun:"cloud_link"` + FileName string `bun:"file_name"` + Path string `bun:"path"` + Review bool `bun:"review"` + Title string `bun:"title"` + WebLink string `bun:"url,unique:url"` + ShouldDownload bool `bun:"should_download"` +} diff --git a/kontor-echo/internal/models/media_file.go b/kontor-echo/internal/models/media_file.go new file mode 100644 index 0000000..df47c72 --- /dev/null +++ b/kontor-echo/internal/models/media_file.go @@ -0,0 +1,26 @@ +package models + +import ( + "time" + + "github.com/uptrace/bun" +) + +type MediaFile struct { + bun.BaseModel `bun:"table:media_file"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + CloudLink string `bun:"cloud_link"` + FileName string `bun:"file_name"` + Path string `bun:"path"` + Review bool `bun:"review"` + Title string `bun:"title"` + WebLink string `bun:"url,unique:url"` + ShouldDownload bool `bun:"should_download"` + + MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_file_id"` +} + diff --git a/kontor-echo/internal/models/media_video.go b/kontor-echo/internal/models/media_video.go new file mode 100644 index 0000000..d4d487f --- /dev/null +++ b/kontor-echo/internal/models/media_video.go @@ -0,0 +1,24 @@ +package schema + +import ( + "time" + + "github.com/uptrace/bun" +) + +type MediaVideo struct { + bun.BaseModel `bun:"table:media_article"` + + ID string `bun:"id,pk"` + CreatedAt time.Time `bun:"created_date,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"last_modified_date,nullzero,notnull,default:current_timestamp"` + Version int `bun:"version,default:0"` + CloudLink string `bun:"cloud_link"` + FileName string `bun:"file_name"` + Path string `bun:"path"` + Review bool `bun:"review"` + Title string `bun:"title"` + WebLink string `bun:"url,unique:url"` + ShouldDownload bool `bun:"should_download"` +} + diff --git a/kontor-echo/internal/router/router.go b/kontor-echo/internal/router/router.go new file mode 100644 index 0000000..961c61d --- /dev/null +++ b/kontor-echo/internal/router/router.go @@ -0,0 +1,33 @@ +package router + +import ( + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func New(admin *handlers.AdminHandler, media *handlers.MediaHandler) *echo.Router { + e := echo.New() + + skipper := func(c echo.Context) bool { + // Skip health check endpoint + return c.Request().URL.Path == "/health" + } + + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogError: true, + HandleError: true, + Skipper: skipper, + LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + slog.Info("REQUEST", "uri", v.URI, "status", v.Status) + } else { + slog.Info("REQUEST-ERROR", "uri",v.URI, "status", v.Status,"err", v.Error.Error()) + } + return nil + }, + })) + return e +}