umstellung auf Service und Repository Verwendung - WIP
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s

This commit is contained in:
Thomas Peetz
2026-09-25 16:57:23 +02:00
parent 131296c644
commit f0f1819a1f
42 changed files with 998 additions and 351 deletions
+8
View File
@@ -0,0 +1,8 @@
package main
import "log/slog"
func main() {
slog.Info("Kontor started")
slog.Info("Kontor finished")
}
+5 -2
View File
@@ -3,10 +3,10 @@ package main
import (
// "kontor-api-gin/pkg/handler"
"kontor-api-gin/pkg/globals"
"kontor-api-gin/internal/globals"
"kontor-api-gin/internal/schema"
"kontor-api-gin/pkg/middleware"
"kontor-api-gin/pkg/routes"
"kontor-api-gin/pkg/schema"
"log"
"log/slog"
"os"
@@ -27,6 +27,8 @@ func main() {
viper.SetDefault("db.user", "kontor")
viper.SetDefault("db.password", "kontor")
viper.SetDefault("port", 8550)
viper.SetDefault("token_hour_lifespan", 1)
viper.SetDefault("api_secret", "secret")
if err := globals.InitConfig(); err != nil {
slog.Info("Failed to initialize config", "error", err)
@@ -66,6 +68,7 @@ func main() {
// Setup api routes
api := routes.SetupApiRoutes(router)
api.Use(middleware.CorsMiddleware)
api.Use(middleware.JwtRequired)
routes.SetupMediaRoutes(api)
routes.SetupComicRoutes(api)
+61 -64
View File
@@ -1,23 +1,23 @@
package main
import (
// "kontor-api-gin/pkg/handler"
"html/template"
"kontor-api-gin/assets"
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/middleware"
"kontor-api-gin/pkg/routes"
"kontor-api-gin/pkg/schema"
"kontor-api-gin/templates"
"context"
"fmt"
"kontor-api-gin/internal/config"
"kontor-api-gin/internal/globals"
"kontor-api-gin/internal/handlers"
"kontor-api-gin/internal/repository"
"kontor-api-gin/internal/router"
"kontor-api-gin/internal/service"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
gin_slog "github.com/gin-contrib/slog"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
@@ -25,70 +25,67 @@ import (
func main() {
slog.Info("Kontor started")
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")
if err := globals.InitConfig(); err != nil {
log.Fatalf("Failed to initialize config: %v", err)
}
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"),
)
db, err := config.NewDB(dsn)
if err != nil {
log.Fatal(err)
}
if err := db.AutoMigrate(); err != nil {
log.Fatal(err)
}
comicRepo := repository.NewComicRepository(db)
profileRepo := repository.NewProfileRepository(db)
comicService := service.NewComicService(comicRepo)
authService := service.NewAuthService(profileRepo, []byte(viper.GetString("api_secret")))
// Set Gin mode based on environment
// Production mode disables debug output
if viper.GetString("gin.mode") == "release" {
gin.SetMode(gin.ReleaseMode)
}
// Connect to database
schema.Connect()
r := router.New(
&handlers.ComicHandler{Service: comicService},
&handlers.AuthHandler{Service: authService},
[]byte(viper.GetString("api_secret")),
)
// router.Use(sessions.Sessions("session", cookie.NewStore(globals.Secret)))
// Run migrations
schema.RunMigrations()
// Create Gin router with default middleware
// Default includes Logger and Recovery middleware
// router := gin.Default()
// Create a new router without default mmiddleware
router := gin.New()
// Add recovery middleware
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),
))
templ := template.Must(template.New("").ParseFS(templates.TemplateFiles, "layouts/*.html", "pages/*.html"))
router.SetHTMLTemplate(templ)
router.StaticFS("/public", http.FS(assets.StaticFiles))
router.Use(sessions.Sessions("session", cookie.NewStore(globals.Secret)))
// Setup api routes
api := routes.SetupApiRoutes(router)
api.Use(middleware.CorsMiddleware)
routes.SetupMediaRoutes(api)
routes.SetupComicRoutes(api)
// Setup Webapp routes
routes.AuthRoutes(router)
routes.ComicRoutes(router)
routes.MediaRoutes(router)
routes.PublicRoutes(router)
// Start server
port := viper.GetInt("port")
slog.Info("Server starting", "port", port)
if err := router.Run(":" + strconv.Itoa(port)); err != nil {
log.Fatal("Failed to start server:", err)
srv := &http.Server{
Addr: ":" + strconv.Itoa(viper.GetInt("port")),
Handler: r,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
slog.Info("server up", "address", srv.Addr)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
slog.Info("server stopped")
slog.Info("Kontor finished")
}
+11 -10
View File
@@ -3,10 +3,13 @@ module kontor-api-gin
go 1.26.8
require (
github.com/gin-contrib/sessions v1.1.2
github.com/gin-contrib/slog v1.2.3
github.com/gin-gonic/gin v1.12.0
github.com/gin-contrib/sessions v1.0.2
github.com/gin-contrib/slog v1.1.0
github.com/gin-gonic/gin v1.10.1
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/spf13/viper v1.21.0
golang.org/x/crypto v0.57.0
gorm.io/driver/postgres v1.6.3
gorm.io/gorm v1.31.2
)
@@ -18,13 +21,13 @@ require (
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/cors v1.7.2 // indirect
github.com/gin-contrib/sse v1.1.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.4 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
@@ -41,24 +44,22 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.62.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/stretchr/testify v1.12.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.2 // indirect
go.mongodb.org/mongo-driver/v2 v2.9.1 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/arch v0.31.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.16.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+25 -20
View File
@@ -9,32 +9,36 @@ github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/gin-contrib/sessions v1.1.2 h1:utWRqOHT/kbqxxqF/mtutV9wfGLi1+uu5YuvWUhj/Ow=
github.com/gin-contrib/sessions v1.1.2/go.mod h1:qRxYwA/TYJw0wv28oFzs4gY/8G8z69id862SNgX9Gmw=
github.com/gin-contrib/slog v1.2.3 h1:lTTPqZENURDHFXs9V1rfYWo3zJteryClDZJAppMovzE=
github.com/gin-contrib/slog v1.2.3/go.mod h1:kHUAHrvyd65WrT2p5IykUe4dH51enJoClwagGgAGqAw=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4=
github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY=
github.com/gin-contrib/sse v1.1.2 h1:MU2fgl1RrdYTMcgJLtz2kJF+vPg3xrqaaKfUUU18tCo=
github.com/gin-contrib/sse v1.1.2/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w=
github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -64,6 +68,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
@@ -78,12 +86,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8=
github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@@ -114,10 +118,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc=
github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.9.1 h1:jewiFs2m1/VOQp8qhFshX6hWZ+EAXDhZHXExAUMcOgQ=
go.mongodb.org/mongo-driver/v2 v2.9.1/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/arch v0.31.0 h1:22MlEb14/O/EPCYHFxsDdv5TuLD5dMjT5e2QeJw4ULk=
@@ -132,10 +132,15 @@ golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.3 h1:bAn6O2pUa8LtpWEvL5NFU4+52Tfx8Ut7IVaIacCLcI0=
gorm.io/driver/postgres v1.6.3/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk=
@@ -1,4 +1,4 @@
package globals
package config
import (
"fmt"
+35
View File
@@ -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")
}
+40
View File
@@ -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
}
+16
View File
@@ -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"})
}
+21
View File
@@ -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)
}
+90
View File
@@ -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
}
+35
View File
@@ -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"
}
+12
View File
@@ -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
}
+14
View File
@@ -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
}
+49
View File
@@ -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
}
+121
View File
@@ -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"
}
@@ -5,6 +5,7 @@ import (
"log"
"log/slog"
"time"
"github.com/spf13/viper"
"gorm.io/driver/postgres"
@@ -56,11 +57,7 @@ func Connect() {
func RunMigrations() {
// AutoMigrate creates tables, missing columns, and missing indexes
// It will NOT delete unused columns to protect your data
err := DB.AutoMigrate(
&MediaActor{},
&MediaFile{},
&MediaActorFile{},
)
err := DB.AutoMigrate()
if err != nil {
log.Fatal("Migration failed:", err)
}
+107
View File
@@ -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"
}
+12
View File
@@ -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
}
+20
View File
@@ -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
}
+29
View File
@@ -3,6 +3,7 @@ package controllers
import (
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/helpers"
"kontor-api-gin/pkg/schema"
"net/http"
"github.com/gin-contrib/sessions"
@@ -11,6 +12,34 @@ import (
"github.com/gin-gonic/gin"
)
type LoginInput struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
func LoginHandler(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
profile := schema.Profile{}
profile.Email = input.Email
profile.Password = input.Password
token, err := schema.LoginCheck(profile.Email, profile.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is incorrect."})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}
func LoginGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
+2
View File
@@ -3,6 +3,7 @@ package controllers
import (
"kontor-api-gin/pkg/globals"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
@@ -11,5 +12,6 @@ func HealthGetHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"version": globals.Version,
"time": time.Now().UTC().Format(time.RFC3339),
})
}
+1 -4
View File
@@ -18,10 +18,7 @@ func ListMediaFiles(c *gin.Context) {
// result := query.Find(&files)
result := schema.DB.Find(&files)
logger.Info("ListMediaFiles", "count", result.RowsAffected)
c.JSON(http.StatusOK, gin.H{
"message": "All MediaFiles",
"files": files,
})
c.JSON(http.StatusOK, files)
}
func ListMediaLoFis(c *gin.Context) {
@@ -1,7 +1,8 @@
package middleware
import (
"kontor-api-gin/pkg/globals"
"kontor-api-gin/internal/globals"
"kontor-api-gin/internal/helpers"
"net/http"
"github.com/gin-contrib/sessions"
@@ -22,3 +23,15 @@ func AuthRequired(c *gin.Context) {
}
c.Next()
}
func JwtRequired(c *gin.Context) {
logger := slog.Get(c)
err := helpers.TokenValid(c)
logger.Info("JwtRequired", "TokenValid", err)
if err != nil {
c.String(http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
}
c.Next()
}
+5 -1
View File
@@ -1,10 +1,14 @@
package routes
import (
"kontor-api-gin/pkg/controllers"
"github.com/gin-gonic/gin"
)
func SetupApiRoutes(router *gin.Engine) *gin.RouterGroup {
v1 := router.Group("api/v1")
api := router.Group("/api")
api.POST("/login", controllers.LoginHandler)
v1 := api.Group("/v1")
return v1
}
-151
View File
@@ -1,151 +0,0 @@
package schema
import (
"time"
)
type Publisher struct {
Base
Name string `gorm:"uniqueIndex" json:"name"`
WebLink string `json:"weblink"`
ParentPublisherID *string `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 Comic struct {
Base
Title string `gorm:"uniqueIndex" json:"title"`
CurrentOrder bool `json:"current_order"`
Completed bool `json:"completed"`
WebLink string `json:"weblink"`
PublisherID *string `json:"publisher_id"`
Publisher *Publisher `gorm:"foreignKey:PublisherID"`
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 Artist struct {
Base
Name string `gorm:"uniqueIndex" json:"name"`
WebLink string `json:"weblink"`
}
func (artist *Artist) TableName() string {
return "artist"
}
type Issue struct {
Base
InStock bool `json:"in_stock"`
IsRead bool `json:"is_read"`
IssueNumber string `json:"issue_number"`
Title string `json:"title"`
PublishedOn time.Time `json:"published_on"`
ComicID *string `json:"comic_id"`
Comic *Comic `gorm:"foreignKey:ComicID"`
StoryArcID *string `json:"story_arc_id"`
StoryArc *StoryArc `gorm:"foreignKey:StoryArcID"`
VolumeID *string `json:"volume_id"`
Volume *Volume `gorm:"foreignKey:VolumeID"`
}
func (issue *Issue) TableName() string {
return "issue"
}
type StoryArc struct {
Base
Name string `gorm:"uniqueIndex" json:"name"`
ComicID *string `json:"comic_id"`
Comic *Comic `gorm:"foreignKey:ComicID"`
VolumeID *string `json:"volume_id"`
Volume *Volume `gorm:"foreignKey:VolumeID"`
}
func (story_arc *StoryArc) TableName() string {
return "story_arc"
}
type TradePaperback struct {
Base
Name string `gorm:"uniqueIndex" json:"name"`
IssueStart int `json:"issue_start"`
IssueEnd int `json:"issue_end"`
ComicID *string `json:"comic_id"`
Comic *Comic `gorm:"foreignKey:ComicID"`
}
func (trade_paperback *TradePaperback) TableName() string {
return "trade_paperback"
}
type Volume struct {
Base
Name string `gorm:"uniqueIndex" json:"name"`
ComicID *string `json:"comic_id"`
Comic *Comic `gorm:"foreignKey:ComicID"`
}
func (volume *Volume) TableName() string {
return "volume"
}
type WorkType struct {
Base
Name string `gorm:"uniqueIndex" json:"name"`
}
func (worktype *WorkType) TableName() string {
return "worktype"
}
type ComicWork struct {
Base
ArtistID *string `json:"artist_id"`
Artist *Artist `gorm:"foreignKey:ArtistID"`
ComicID *string `json:"comic_id"`
Comic *Comic `gorm:"foreignKey:ComicID"`
WorkTypeID *string `json:"work_type_id"`
WorkType *WorkType `gorm:"foreignKey:WorkTypeID"`
}
func (comic_work *ComicWork) TableName() string {
return "comic_work"
}
type IssueWork struct {
Base
ArtistID *string `json:"artist_id"`
Artist *Artist `gorm:"foreignKey:ArtistID"`
IssueID *string `json:"issue_id"`
Issue *Issue `gorm:"foreignKey:IssueID"`
WorkTypeID *string `json:"work_type_id"`
WorkType *WorkType `gorm:"foreignKey:WorkTypeID"`
}
func (issue_work *IssueWork) TableName() string {
return "issue_work"
}
-92
View File
@@ -1,92 +0,0 @@
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 {
Base
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"`
WebLink string `gorm:"uniqueIndex" 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 {
Base
Name string `json:"name"`
WebLink string `gorm:"uniqueIndex" json:"url"`
MediaActorFiles []MediaActorFile `gorm:"foreignKey:MediaActorID" json:"media_actor_files"`
}
func (media_actor *MediaActor) TableName() string {
return "media_actor"
}
type MediaActorFile struct {
Base
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 {
// 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"`
// }