umstellung auf Service und Repository Verwendung - WIP
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func InitConfig() error {
|
||||
viper.SetConfigName("kontor-gin")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("$HOME/.config")
|
||||
viper.AddConfigPath("/etc/kontor")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// 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")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("initConfig", "Using config file: ", viper.ConfigFileUsed())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func NewDB(dsn string) (*gorm.DB, error) {
|
||||
// Configure GORM with custom settings
|
||||
// The logger helps with debugging SQL queries during development
|
||||
return gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
// 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")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package globals
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func InitConfig() error {
|
||||
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("port", 8550)
|
||||
viper.SetDefault("gin.mode", "release")
|
||||
|
||||
viper.SetConfigName("kontor-gin")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("$HOME/.config")
|
||||
viper.AddConfigPath("/etc/kontor")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// 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")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("initConfig", "Using config file: ", viper.ConfigFileUsed())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package globals
|
||||
|
||||
var Secret = []byte("secret")
|
||||
|
||||
const UserKey = "user"
|
||||
|
||||
const Version = "0.4.0"
|
||||
|
||||
const LoginUrl = "/login"
|
||||
|
||||
const LogoutUrl = "/auth/logout"
|
||||
@@ -0,0 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"kontor-api-gin/internal/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
Service *service.AuthService
|
||||
}
|
||||
|
||||
func (handler *AuthHandler) Login(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"token": "bla"})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"kontor-api-gin/internal/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ComicHandler struct {
|
||||
Service *service.ComicService
|
||||
}
|
||||
|
||||
func (handler *ComicHandler) List(c *gin.Context) {
|
||||
comics, err := handler.Service.List(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, comics)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package helpers
|
||||
|
||||
import "strings"
|
||||
|
||||
func CheckUserPass(username, password string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func EmptyUserPass(username, password string) bool {
|
||||
return strings.Trim(username, " ") == "" || strings.Trim(password, " ") == ""
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/slog"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GenerateToken(profile_id string) (string, error) {
|
||||
|
||||
// token_lifespan := viper.GetInt("token_hour_lifespan")
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"name": "Thomas Peetz",
|
||||
"admin": true,
|
||||
"authorized": true,
|
||||
"profile_id": profile_id,
|
||||
"exp": time.Now().Add(time.Hour * 72).Unix(),
|
||||
}
|
||||
// claims["authorized"] = true
|
||||
// claims["profile_id"] = profile_id
|
||||
// claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
return token.SignedString([]byte(viper.GetString("api_secret")))
|
||||
|
||||
}
|
||||
|
||||
func TokenValid(c *gin.Context) error {
|
||||
logger := slog.Get(c)
|
||||
|
||||
tokenString := ExtractToken(c)
|
||||
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
logger.Info("TokenValid", "Unexpected signing method", token.Header["alg"])
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(viper.GetString("api_secret")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractToken(c *gin.Context) string {
|
||||
logger := slog.Get(c)
|
||||
|
||||
token := c.Query("token")
|
||||
if token != "" {
|
||||
logger.Info("ExtractToken", "token from query", token)
|
||||
return token
|
||||
}
|
||||
bearerToken := c.Request.Header.Get("Authorization")
|
||||
if len(strings.Split(bearerToken, " ")) == 2 {
|
||||
logger.Info("ExtractToken", "token from query", token)
|
||||
|
||||
return strings.Split(bearerToken, " ")[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ExtractTokenID(c *gin.Context) (string, error) {
|
||||
|
||||
tokenString := ExtractToken(c)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(viper.GetString("api_secret")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if ok && token.Valid {
|
||||
uid, ok := claims["profile_id"].(string)
|
||||
if !ok {
|
||||
return "", err
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func JWT(secret []byte) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
h := c.GetHeader("Authorization")
|
||||
if !strings.HasPrefix(h, "Bearer ") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized,
|
||||
gin.H{"error": "missing bearer token"})
|
||||
return
|
||||
}
|
||||
raw := strings.TrimPrefix(h, "Bearer ")
|
||||
token, err := jwt.Parse(raw, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized,
|
||||
gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
c.Set("profile_id", claims["sub"].(string))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func RateLimit(rps int, burst int) gin.HandlerFunc {
|
||||
limiter := rate.NewLimiter(rate.Limit(rps), burst)
|
||||
return func(c *gin.Context) {
|
||||
if !limiter.Allow() {
|
||||
c.AbortWithStatusJSON(429, gin.H{"error": "too many requests"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.GetHeader("X-Request-ID")
|
||||
if id == "" {
|
||||
id = uuid.NewString()
|
||||
}
|
||||
c.Set("request_id", id)
|
||||
c.Writer.Header().Set("X-Request-ID", id)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type Artist struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
WebLink string `gorm:"column:weblink" json:"weblink"`
|
||||
}
|
||||
|
||||
func (artist *Artist) TableName() string {
|
||||
return "artist"
|
||||
}
|
||||
|
||||
type CreateArtistDTO struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package comics
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Comic struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Title string `gorm:"uniqueIndex" json:"title"`
|
||||
CurrentOrder bool `gorm:"column:current_order" json:"current_order"`
|
||||
Completed bool `gorm:"column:completed" json:"completed"`
|
||||
WebLink string `gorm:"column:weblink" json:"weblink"`
|
||||
PublisherID *string `gorm:"column:publisher_id" json:"publisher_id"`
|
||||
Issues []Issue `gorm:"foreignKey:ComicID"`
|
||||
StoryArcs []StoryArc `gorm:"foreignKey:ComicID"`
|
||||
TradePaperbacks []TradePaperback `gorm:"foreignKey:ComicID"`
|
||||
Volumes []Volume `gorm:"foreignKey:ComicID"`
|
||||
ComicWorks []ComicWork `gorm:"foreignKey:ComicID"`
|
||||
}
|
||||
|
||||
func (comic *Comic) TableName() string {
|
||||
return "comic"
|
||||
}
|
||||
|
||||
type CreateComicDTO struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
}
|
||||
|
||||
type UpdateComicDTO struct {
|
||||
Title *string `json:"title,omitempty" binding:"omitempty"`
|
||||
WebLink *string `json:"weblink,omitempty" binding:"omitempty"`
|
||||
Completed *bool `json:"cimpleted,omitempty" binding:"omitempty"`
|
||||
CurrentOrder *bool `json:"current_order,omitempty" binding:"omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type ComicWork struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
ArtistID *string `gorm:"column:artist_id" json:"artist_id"`
|
||||
Artist *Artist `gorm:"foreignKey:ArtistID"`
|
||||
ComicID *string `gorm:"column:comic_id" json:"comic_id"`
|
||||
Comic *Comic `gorm:"foreignKey:ComicID"`
|
||||
WorkTypeID *string `gorm:"column:work_type_id" json:"work_type_id"`
|
||||
WorkType *WorkType `gorm:"foreignKey:WorkTypeID"`
|
||||
}
|
||||
|
||||
func (comic_work *ComicWork) TableName() string {
|
||||
return "comic_work"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type Issue struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
InStock bool `gorm:"column:in_stock" json:"in_stock"`
|
||||
IsRead bool `gorm:"column:is_read" json:"is_read"`
|
||||
IssueNumber string `gorm:"column:issue_number" json:"issue_number"`
|
||||
Title string `gorm:"column:title" json:"title"`
|
||||
PublishedOn time.Time `gorm:"column:published_on" json:"published_on"`
|
||||
ComicID *string `gorm:"column:comic_id" json:"comic_id"`
|
||||
Comic *Comic `gorm:"foreignKey:ComicID"`
|
||||
StoryArcID *string `gorm:"column:story_arc_id" json:"story_arc_id"`
|
||||
StoryArc *StoryArc `gorm:"foreignKey:StoryArcID"`
|
||||
VolumeID *string `gorm:"column:volume_id" json:"volume_id"`
|
||||
Volume *Volume `gorm:"foreignKey:VolumeID"`
|
||||
}
|
||||
|
||||
func (issue *Issue) TableName() string {
|
||||
return "issue"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type IssueWork struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
ArtistID *string `gorm:"column:artist_id" json:"artist_id"`
|
||||
Artist *Artist `gorm:"foreignKey:ArtistID"`
|
||||
IssueID *string `gorm:"column:issue_id" json:"issue_id"`
|
||||
Issue *Issue `gorm:"foreignKey:IssueID"`
|
||||
WorkTypeID *string `gorm:"column:work_type_id" json:"work_type_id"`
|
||||
WorkType *WorkType `gorm:"foreignKey:WorkTypeID"`
|
||||
}
|
||||
|
||||
func (issue_work *IssueWork) TableName() string {
|
||||
return "issue_work"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type Publisher struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
WebLink string `gorm:"column:weblink" json:"weblink"`
|
||||
ParentPublisherID *string `gorm:"column:parent_publisher_id" json:"parent_publisher_id"`
|
||||
ParentPublisher *Publisher `gorm:"foreignKey:ParentPublisherID"`
|
||||
Imprints []Publisher `gorm:"foreignKey:ParentPublisherID"`
|
||||
Comics []Comic `gorm:"foreignKey:PublisherID"`
|
||||
}
|
||||
|
||||
func (publisher *Publisher) TableName() string {
|
||||
return "publisher"
|
||||
}
|
||||
|
||||
type CreatePublisherDTO struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
WebLink string `json:"weblink" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type StoryArc struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
ComicID *string `gorm:"column:comic_id" json:"comic_id"`
|
||||
Comic *Comic `gorm:"foreignKey:ComicID"`
|
||||
VolumeID *string `gorm:"column:volume_id" json:"volume_id"`
|
||||
Volume *Volume `gorm:"foreignKey:VolumeID"`
|
||||
}
|
||||
|
||||
func (story_arc *StoryArc) TableName() string {
|
||||
return "story_arc"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type TradePaperback struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
IssueStart int `gorm:"column:issue_start" json:"issue_start"`
|
||||
IssueEnd int `gorm:"column:issue_end" json:"issue_end"`
|
||||
ComicID *string `gorm:"column:comic_id" json:"comic_id"`
|
||||
Comic *Comic `gorm:"foreignKey:ComicID"`
|
||||
}
|
||||
|
||||
func (trade_paperback *TradePaperback) TableName() string {
|
||||
return "trade_paperback"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type Volume struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
ComicID *string `gorm:"column:comic_id" json:"comic_id"`
|
||||
Comic *Comic `gorm:"foreignKey:ComicID"`
|
||||
}
|
||||
|
||||
func (volume *Volume) TableName() string {
|
||||
return "volume"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package comics
|
||||
|
||||
import "time"
|
||||
|
||||
type WorkType struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `gorm:"column:created_date" json:"created_date"`
|
||||
UpdatedAt time.Time `gorm:"column:last_modified_date" json:"last_modified_date"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_date" json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
}
|
||||
|
||||
func (worktype *WorkType) TableName() string {
|
||||
return "worktype"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package repository
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ComicRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewComicRepository(db *gorm.DB) *ComicRepository {
|
||||
repo := &ComicRepository{db: db}
|
||||
return repo
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProfileRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProfileRepository(db *gorm.DB) *ProfileRepository {
|
||||
repository := &ProfileRepository{db: db}
|
||||
return repository
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"kontor-api-gin/assets"
|
||||
"kontor-api-gin/internal/handlers"
|
||||
"kontor-api-gin/internal/middleware"
|
||||
"kontor-api-gin/templates"
|
||||
"net/http"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
gin_slog "github.com/gin-contrib/slog"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func New(comics *handlers.ComicHandler, auth *handlers.AuthHandler, secret []byte) *gin.Engine {
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(gin_slog.SetLogger(
|
||||
gin_slog.WithSkipPath([]string{"/health", "/public/style/main.css"}),
|
||||
gin_slog.WithDefaultLevel(slog.LevelInfo),
|
||||
gin_slog.WithClientErrorLevel(slog.LevelInfo),
|
||||
gin_slog.WithServerErrorLevel(slog.LevelInfo),
|
||||
))
|
||||
router.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"https://app.example.com"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||
AllowHeaders: []string{"Authorization", "Content-Type"},
|
||||
}))
|
||||
templ := template.Must(template.New("").ParseFS(templates.TemplateFiles, "layouts/*.html", "pages/*.html"))
|
||||
router.SetHTMLTemplate(templ)
|
||||
|
||||
router.StaticFS("/public", http.FS(assets.StaticFiles))
|
||||
|
||||
router.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
|
||||
|
||||
api := router.Group("/api")
|
||||
api.POST("/login", auth.Login)
|
||||
|
||||
v1 := api.Group("/v1")
|
||||
{
|
||||
secured := v1.Group("")
|
||||
secured.Use(middleware.JWT(secret))
|
||||
secured.GET("comics/comics", comics.List)
|
||||
}
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"kontor-api-gin/internal/helpers"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func VerifyPassword(password, hashedPassword string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
}
|
||||
|
||||
func LoginCheck(email string, password string) (string, error) {
|
||||
|
||||
var err error
|
||||
|
||||
profile := Profile{}
|
||||
|
||||
err = DB.Model(Profile{}).Where("email = ?", email).Take(&profile).Error
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = VerifyPassword(password, profile.Password)
|
||||
|
||||
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := helpers.GenerateToken(profile.ID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
First_Name *string `json:"first_name"`
|
||||
Last_Name *string `json:"last_name"`
|
||||
User_Name string `json:"user_name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
Assignments []Assignment `gorm:"foreignKey:ProfileID" json:"assignments"`
|
||||
Tokens []Token `gorm:"foreignKey:ProfileID" json:"tokens"`
|
||||
}
|
||||
|
||||
func (profile *Profile) TableName() string {
|
||||
return "profile"
|
||||
}
|
||||
|
||||
func (profile *Profile) GetFullName() string {
|
||||
full_name := ""
|
||||
if profile.First_Name != nil {
|
||||
full_name += *profile.First_Name
|
||||
}
|
||||
if profile.Last_Name != nil {
|
||||
full_name += " "
|
||||
full_name += *profile.Last_Name
|
||||
}
|
||||
return full_name
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Token string `gorm:"uniqueIndex" json:"token"`
|
||||
Name string `json:"name"`
|
||||
Last_Used_date time.Time `json:"last_used_date"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ProfileID *string `json:"profile_id"`
|
||||
Profile *Profile `gorm:"foreignKey:ProfileID" json:"profile"`
|
||||
}
|
||||
|
||||
func (token *Token) TableName() string {
|
||||
return "token"
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `json:"name"`
|
||||
Assignments []Assignment `gorm:"foreignKey:PermissionID" json:"assignments"`
|
||||
}
|
||||
|
||||
func (permission *Permission) TableName() string {
|
||||
return "permission"
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
ProfileID *string `json:"profile_id"`
|
||||
Profile *Profile `gorm:"foreignKey:ProfileID" json:"profile"`
|
||||
PermissionID *string `json:"permission_id"`
|
||||
Permission *Profile `gorm:"foreignKey:PermissionID" json:"permission"`
|
||||
}
|
||||
|
||||
func (assignment *Assignment) TableName() string {
|
||||
return "assignment"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
CreatedAt time.Time `json:"created_date"`
|
||||
UpdatedAt time.Time `json:"last_modified_date"`
|
||||
DeletedAt *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
}
|
||||
|
||||
// BeforeCreate will set a UUID rather than numeric ID.
|
||||
func (b *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
b.ID = uuid.New().String()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func Connect() {
|
||||
// Build the connection string from environment variables
|
||||
// This keeps sensitive credentials out of the codebase
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
|
||||
viper.GetString("db.host"),
|
||||
viper.GetString("db.user"),
|
||||
viper.GetString("db.password"),
|
||||
viper.GetString("db.name"),
|
||||
viper.GetInt("db.port"),
|
||||
)
|
||||
|
||||
// Configure GORM with custom settings
|
||||
// The logger helps with debugging SQL queries during development
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
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)
|
||||
|
||||
DB = db
|
||||
slog.Info("Database connected successfully")
|
||||
}
|
||||
|
||||
func RunMigrations() {
|
||||
// AutoMigrate creates tables, missing columns, and missing indexes
|
||||
// It will NOT delete unused columns to protect your data
|
||||
err := DB.AutoMigrate()
|
||||
if err != nil {
|
||||
log.Fatal("Migration failed:", err)
|
||||
}
|
||||
|
||||
slog.Info("Database migration completed")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package schema
|
||||
|
||||
import "time"
|
||||
|
||||
type MediaLoFi struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Url string `gorm:"uniqueIndex" json:"url"`
|
||||
Review bool `json:"review"`
|
||||
ShouldDownload bool `json:"should_download"`
|
||||
Title string `gorm:"size:300" json:"title"`
|
||||
FileName string `json:"file_name"`
|
||||
}
|
||||
|
||||
func (media_lofi *MediaLoFi) TableName() string {
|
||||
return "media_lofi"
|
||||
}
|
||||
|
||||
type MediaFile struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
CloudLink string `gorm:"column:cloud_link" json:"cloud_link"`
|
||||
FileName string `json:"file_name"`
|
||||
Path string `json:"path"`
|
||||
Review bool `json:"review"`
|
||||
Title string `gorm:"size:300" json:"title"`
|
||||
WebLink string `gorm:"column:url" json:"url"`
|
||||
ShouldDownload bool `json:"should_download"`
|
||||
|
||||
MediaActorFiles []MediaActorFile `gorm:"foreignKey:MediaFileID" json:"media_actor_files"`
|
||||
}
|
||||
|
||||
func (media_file *MediaFile) TableName() string {
|
||||
return "media_file"
|
||||
}
|
||||
|
||||
type MediaActor struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
|
||||
MediaActorFiles []MediaActorFile `gorm:"foreignKey:MediaActorID" json:"media_actor_files"`
|
||||
}
|
||||
|
||||
func (media_actor *MediaActor) TableName() string {
|
||||
return "media_actor"
|
||||
}
|
||||
|
||||
type MediaActorFile struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
MediaActorID *string `json:"media_actor_id"`
|
||||
MediaActor *MediaActor `gorm:"foreignKey:MediaActorID" json:"media_actor"`
|
||||
MediaFileID *string `json:"media_file_id"`
|
||||
MediaFile *MediaFile `gorm:"foreignKey:MediaFileID" json:"media_file"`
|
||||
}
|
||||
|
||||
func (media_file *MediaActorFile) TableName() string {
|
||||
return "media_actor_file"
|
||||
}
|
||||
|
||||
type MediaArticle struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
Review bool `json:"review"`
|
||||
Title string `gorm:"size:300" json:"title"`
|
||||
Url string `gorm:"uniqueIndex" json:"url"`
|
||||
}
|
||||
|
||||
func (media_article *MediaArticle) TableName() string {
|
||||
return "media_article"
|
||||
}
|
||||
|
||||
type MediaVideo struct {
|
||||
ID string `gorm:"type:string;primary_key;"`
|
||||
Created_Date time.Time `json:"created_date"`
|
||||
Last_Modified_date time.Time `json:"last_modified_date"`
|
||||
Deleted_Date *time.Time `json:"deleted_date"`
|
||||
Version int `gorm:"default:0" json:"version"`
|
||||
CloudLink string `json:"cloud_link"`
|
||||
FileName string `json:"file_name"`
|
||||
Path string `json:"path"`
|
||||
Review bool `json:"review"`
|
||||
Title string `gorm:"size:300" json:"title"`
|
||||
Url string `gorm:"uniqueIndex" json:"url"`
|
||||
ShouldDownload bool `json:"should_download"`
|
||||
}
|
||||
|
||||
func (media_video *MediaVideo) TableName() string {
|
||||
return "media_video"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package service
|
||||
|
||||
import "kontor-api-gin/internal/repository"
|
||||
|
||||
type AuthService struct {
|
||||
repository *repository.ProfileRepository
|
||||
}
|
||||
|
||||
func NewAuthService(repository *repository.ProfileRepository, secret []byte) *AuthService {
|
||||
service := &AuthService{repository: repository}
|
||||
return service
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kontor-api-gin/internal/models/comics"
|
||||
"kontor-api-gin/internal/repository"
|
||||
)
|
||||
|
||||
type ComicService struct {
|
||||
repository *repository.ComicRepository
|
||||
}
|
||||
|
||||
func NewComicService(repo *repository.ComicRepository) *ComicService {
|
||||
service := &ComicService{repository: repo}
|
||||
return service
|
||||
}
|
||||
|
||||
func (service *ComicService) List(context context.Context) ([]comics.Comic, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user