add templates for kontor-gin
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s

This commit is contained in:
Thomas Peetz
2026-09-22 19:19:51 +02:00
parent f2f4a0aaf6
commit 044da5353a
24 changed files with 603 additions and 101 deletions
+11
View File
@@ -0,0 +1,11 @@
FROM docker.io/library/golang:1.26-alpine
# Install runtime dependencies you actually need
RUN apk add --no-cache ca-certificates tzdata curl git make musl-dev go
# Set the working directory
WORKDIR /app
# Copy the binary and set ownership
COPY ./kontor kontor
EXPOSE 8550
ENV DB_HOST=postgres DB_USER=kontor DB_PASSWORD=kontor DB_NAME=kontor DB_PORT=5432
# Set the entrypoint command
ENTRYPOINT ["/app/kontor"]
+18 -20
View File
@@ -10,16 +10,18 @@ import (
"kontor-api-gin/pkg/schema" "kontor-api-gin/pkg/schema"
"kontor-api-gin/templates" "kontor-api-gin/templates"
"log" "log"
"log/slog"
"net/http" "net/http"
"os" "os"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
gin_slog "github.com/gin-contrib/slog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func main() { func main() {
log.Println("Kontor started") slog.Info("Kontor started")
// Set Gin mode based on environment // Set Gin mode based on environment
// Production mode disables debug output // Production mode disables debug output
@@ -35,27 +37,30 @@ func main() {
// Create Gin router with default middleware // Create Gin router with default middleware
// Default includes Logger and Recovery middleware // Default includes Logger and Recovery middleware
//router := gin.Default() // router := gin.Default()
// Create a new router without default mmiddleware // Create a new router without default mmiddleware
router := gin.New() 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, "html/*.tmpl")) templ := template.Must(template.New("").ParseFS(templates.TemplateFiles, "html/*.tmpl"))
router.SetHTMLTemplate(templ) router.SetHTMLTemplate(templ)
router.StaticFS("/public", http.FS(assets.StaticFiles)) router.StaticFS("/public", http.FS(assets.StaticFiles))
// Add CORS middleware for frontend access
// router.Use(middleware.CorsMiddleware)
router.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{"/health", "/metrics"},
}))
// Add recovery middleware
router.Use(gin.Recovery())
router.Use(sessions.Sessions("session", cookie.NewStore(globals.Secret))) router.Use(sessions.Sessions("session", cookie.NewStore(globals.Secret)))
// Setup api routes // Setup api routes
api := routes.SetupApiRoutes(router) api := routes.SetupApiRoutes(router)
api.Use(middleware.CorsMiddleware)
routes.SetupMediaRoutes(api) routes.SetupMediaRoutes(api)
routes.SetupComicRoutes(api)
// Setup public Webapp routes // Setup public Webapp routes
public := router.Group("/") public := router.Group("/")
@@ -66,23 +71,16 @@ func main() {
protected.Use(middleware.AuthRequired) protected.Use(middleware.AuthRequired)
routes.ProtectedRoutes(protected) routes.ProtectedRoutes(protected)
// Health check endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
})
})
// Start server // Start server
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8550" port = "8550"
} }
log.Printf("Server starting on port %s", port) slog.Info("Server starting", "port", port)
if err := router.Run(":" + port); err != nil { if err := router.Run(":" + port); err != nil {
log.Fatal("Failed to start server:", err) log.Fatal("Failed to start server:", err)
} }
log.Println("Kontor finished") slog.Info("Kontor finished")
} }
+1
View File
@@ -9,6 +9,7 @@ require (
github.com/cloudwego/base64x v0.1.7 // indirect github.com/cloudwego/base64x v0.1.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/sessions v1.1.2 // indirect github.com/gin-contrib/sessions v1.1.2 // indirect
github.com/gin-contrib/slog v1.2.3 // indirect
github.com/gin-contrib/sse v1.1.2 // indirect github.com/gin-contrib/sse v1.1.2 // indirect
github.com/gin-gonic/gin v1.12.0 // indirect github.com/gin-gonic/gin v1.12.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
+2
View File
@@ -12,6 +12,8 @@ github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= 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 h1:utWRqOHT/kbqxxqF/mtutV9wfGLi1+uu5YuvWUhj/Ow=
github.com/gin-contrib/sessions v1.1.2/go.mod h1:qRxYwA/TYJw0wv28oFzs4gY/8G8z69id862SNgX9Gmw= 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/sse v1.1.2 h1:MU2fgl1RrdYTMcgJLtz2kJF+vPg3xrqaaKfUUU18tCo= 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-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 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
+17 -5
View File
@@ -3,10 +3,10 @@ package controllers
import ( import (
"kontor-api-gin/pkg/globals" "kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/helpers" "kontor-api-gin/pkg/helpers"
"log"
"net/http" "net/http"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/slog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -14,10 +14,13 @@ import (
func LoginGetHandler(c *gin.Context) { func LoginGetHandler(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
user := session.Get(globals.UserKey) user := session.Get(globals.UserKey)
logger := slog.Get(c)
logger.Info("LoginGetHandler", "user", user)
if user != nil { if user != nil {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"login": globals.LoginUrl,
"content": "Please logout first", "content": "Please logout first",
"user": user, "user": user,
}) })
@@ -26,6 +29,7 @@ func LoginGetHandler(c *gin.Context) {
c.HTML(http.StatusOK, "login.tmpl", gin.H{ c.HTML(http.StatusOK, "login.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"login": globals.LoginUrl,
"content": "", "content": "",
"user": user, "user": user,
}) })
@@ -34,10 +38,13 @@ func LoginGetHandler(c *gin.Context) {
func LoginPostHandler(c *gin.Context) { func LoginPostHandler(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
user := session.Get(globals.UserKey) user := session.Get(globals.UserKey)
logger := slog.Get(c)
logger.Info("LoginPostHandler", "user", user)
if user != nil { if user != nil {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"login": globals.LoginUrl,
"content": "Please logout first", "content": "Please logout first",
}) })
return return
@@ -50,6 +57,7 @@ func LoginPostHandler(c *gin.Context) {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{ c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"login": globals.LoginUrl,
"content": "Parameters can't be empty", "content": "Parameters can't be empty",
}) })
} }
@@ -58,6 +66,7 @@ func LoginPostHandler(c *gin.Context) {
c.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{ c.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"login": globals.LoginUrl,
"content": "Incorrect username or password", "content": "Incorrect username or password",
}) })
} }
@@ -67,10 +76,12 @@ func LoginPostHandler(c *gin.Context) {
c.HTML(http.StatusInternalServerError, "login.tmpl", gin.H{ c.HTML(http.StatusInternalServerError, "login.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"login": globals.LoginUrl,
"content": "Failed to save session", "content": "Failed to save session",
}) })
return return
} }
logger.Info("LoginGetHandler: redirect to index")
c.Redirect(http.StatusMovedPermanently, "/") c.Redirect(http.StatusMovedPermanently, "/")
} }
@@ -78,16 +89,17 @@ func LoginPostHandler(c *gin.Context) {
func LogoutGetHandler(c *gin.Context) { func LogoutGetHandler(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
user := session.Get(globals.UserKey) user := session.Get(globals.UserKey)
log.Printf("logging out user %v", user) logger := slog.Get(c)
logger.Info("LogoutGetHandler", "user", user)
if user == nil { if user == nil {
log.Println("Invalid session token") logger.Info("Invalid session token")
return return
} }
session.Delete(globals.UserKey) session.Delete(globals.UserKey)
if err := session.Save(); err != nil { if err := session.Save(); err != nil {
log.Printf("Failed to save session: %v", err) logger.Info("Failed to save session", "error", err)
return return
} }
logger.Info("LogoutGetHandler: redirect to index page")
c.Redirect(http.StatusMovedPermanently, "/") c.Redirect(http.StatusMovedPermanently, "/")
} }
+30
View File
@@ -0,0 +1,30 @@
package controllers
import (
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/schema"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/slog"
"github.com/gin-gonic/gin"
)
func ListComicsGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
var comics []schema.Comic
result := schema.DB.Find(&comics)
logger.Info("found Comics", "count", result.RowsAffected)
c.HTML(http.StatusOK, "comic_comics.tmpl", gin.H{
"title": "Kontor Comics",
"version": "0.4.0",
"content": "Media Files...",
"comics": comics,
"user": user,
})
}
+92
View File
@@ -0,0 +1,92 @@
package controllers
import (
"kontor-api-gin/pkg/schema"
"net/http"
"github.com/gin-contrib/slog"
"github.com/gin-gonic/gin"
)
func ListComics(c *gin.Context) {
var comics []schema.Comic
logger := slog.Get(c)
// query := schema.DB.Model(&schema.MediaFile{})
// result := query.Find(&files)
result := schema.DB.Find(&comics)
logger.Info("found Comics", "count", result.RowsAffected)
c.JSON(http.StatusOK, comics)
}
func ListComicPublishers(c *gin.Context) {
var publishers []schema.Publisher
logger := slog.Get(c)
result := schema.DB.Find(&publishers)
logger.Info("found Comic Publishers", "count", result.RowsAffected)
c.JSON(http.StatusOK, publishers)
}
func ListComicArtists(c *gin.Context) {
var artists []schema.Artist
logger := slog.Get(c)
result := schema.DB.Find(&artists)
logger.Info("found Comic Artists", "count", result.RowsAffected)
c.JSON(http.StatusOK, artists)
}
func ListComicIssues(c *gin.Context) {
var issues []schema.Issue
logger := slog.Get(c)
result := schema.DB.Find(&issues)
logger.Info("found Comic Issues", "count", result.RowsAffected)
c.JSON(http.StatusOK, issues)
}
func ListComicWorkTypes(c *gin.Context) {
var worktypes []schema.WorkType
logger := slog.Get(c)
result := schema.DB.Find(&worktypes)
logger.Info("found Comic WorkTypes", "count", result.RowsAffected)
c.JSON(http.StatusOK, worktypes)
}
func ListComicVolumes(c *gin.Context) {
var items []schema.Volume
logger := slog.Get(c)
result := schema.DB.Find(&items)
logger.Info("found Comic Volumes", "count", result.RowsAffected)
c.JSON(http.StatusOK, items)
}
func ListComicStoryArcs(c *gin.Context) {
var items []schema.StoryArc
logger := slog.Get(c)
result := schema.DB.Find(&items)
logger.Info("found Comic StoryArcs", "count", result.RowsAffected)
c.JSON(http.StatusOK, items)
}
func ListComicWorks(c *gin.Context) {
var items []schema.ComicWork
logger := slog.Get(c)
result := schema.DB.Find(&items)
logger.Info("found Comic ComicWorks", "count", result.RowsAffected)
c.JSON(http.StatusOK, items)
}
func ListComicIssueWorks(c *gin.Context) {
var items []schema.IssueWork
logger := slog.Get(c)
result := schema.DB.Find(&items)
logger.Info("found Comic IssueWorks", "count", result.RowsAffected)
c.JSON(http.StatusOK, items)
}
+4
View File
@@ -5,16 +5,20 @@ import (
"net/http" "net/http"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/slog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func IndexGetHandler(c *gin.Context) { func IndexGetHandler(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
user := session.Get(globals.UserKey) user := session.Get(globals.UserKey)
logger := slog.Get(c)
logger.Info("IndexGetHandler", "user", user)
c.HTML(http.StatusOK, "index.tmpl", gin.H{ c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Kontor Gin", "title": "Kontor Gin",
"version": "0.4.0", "version": "0.4.0",
"content": "This is an index page...", "content": "This is an index page...",
"logout": globals.LogoutUrl,
"user": user, "user": user,
}) })
} }
+15
View File
@@ -0,0 +1,15 @@
package controllers
import (
"kontor-api-gin/pkg/globals"
"net/http"
"github.com/gin-gonic/gin"
)
func HealthGetHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"version": globals.Version,
})
}
+59
View File
@@ -1,10 +1,13 @@
package controllers package controllers
import ( import (
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/schema" "kontor-api-gin/pkg/schema"
"log" "log"
"net/http" "net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/slog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -32,3 +35,59 @@ func ListMediaLoFis(c *gin.Context) {
// "lofis": lofis, // "lofis": lofis,
// }) // })
} }
func ListMediaActorsGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
var actors []schema.MediaActor
result := schema.DB.Find(&actors)
log.Printf("found %d MediaActors", result.RowsAffected)
c.HTML(http.StatusOK, "media_actor.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Media Actors...",
"actors": actors,
"user": user,
})
}
func ListMediaFilesGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
var files []schema.MediaFile
result := schema.DB.Find(&files)
logger.Info("found MediaFiles", "count", result.RowsAffected)
c.HTML(http.StatusOK, "media_files.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Media Files...",
"files": files,
"user": user,
})
}
func ListMediaLoFisGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
var lofis []schema.MediaLoFi
result := schema.DB.Find(&lofis)
logger.Info("found MediaActors", "count", result.RowsAffected)
c.HTML(http.StatusOK, "media_lofi.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Media LoFis...",
"lofis": lofis,
"logout": globals.LogoutUrl,
"user": user,
})
}
+6
View File
@@ -3,3 +3,9 @@ package globals
var Secret = []byte("secret") var Secret = []byte("secret")
const UserKey = "user" const UserKey = "user"
const Version = "0.4.0"
const LoginUrl = "/login"
const LogoutUrl = "/exit"
+7 -5
View File
@@ -1,20 +1,22 @@
package middleware package middleware
import ( import (
"log" "kontor-api-gin/pkg/globals"
"net/http" "net/http"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/slog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func AuthRequired(c *gin.Context) { func AuthRequired(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
user := session.Get("user") user := session.Get(globals.UserKey)
log.Printf("current user %v", user) logger := slog.Get(c)
logger.Info("AuthRequired", globals.UserKey, user)
if user == nil { if user == nil {
log.Println("User not logged in") logger.Info("User not logged in")
c.Redirect(http.StatusMovedPermanently, "/auth/login") c.Redirect(http.StatusMovedPermanently, globals.LoginUrl)
c.Abort() c.Abort()
return return
} }
+5 -3
View File
@@ -8,10 +8,12 @@ import (
// corsMiddleware adds CORS headers for cross-origin requests // corsMiddleware adds CORS headers for cross-origin requests
func CorsMiddleware(c *gin.Context) { func CorsMiddleware(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Allow-Credentials", "true")
// Handle preflight requests
if c.Request.Method == "OPTIONS" { if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent) c.AbortWithStatus(http.StatusNoContent)
return return
+30
View File
@@ -0,0 +1,30 @@
package routes
import (
"kontor-api-gin/pkg/controllers"
"github.com/gin-gonic/gin"
)
func SetupComicRoutes(router *gin.RouterGroup) {
comic := router.Group("/comics")
comics := comic.Group("/comics")
comics.GET("", controllers.ListComics)
publishers := comic.Group("/publishers")
publishers.GET("", controllers.ListComicPublishers)
artists := comic.Group("/artists")
artists.GET("/", controllers.ListComicArtists)
issues := comic.Group("/issues")
issues.GET("", controllers.ListComicIssues)
worktypes := comic.Group("/worktypes")
worktypes.GET("", controllers.ListComicWorkTypes)
volumes := comic.Group("/volumes")
volumes.GET("", controllers.ListComicVolumes)
storyarcs := comic.Group("/storyarcs")
storyarcs.GET("", controllers.ListComicStoryArcs)
comicworks := comic.Group("/comicworks")
comicworks.GET("", controllers.ListComicWorks)
issueworks := comic.Group("/issueworks")
issueworks.GET("", controllers.ListComicIssueWorks)
}
+2 -6
View File
@@ -10,12 +10,8 @@ func SetupMediaRoutes(router *gin.RouterGroup) {
media := router.Group("/media") media := router.Group("/media")
files := media.Group("/files") files := media.Group("/files")
{ files.GET("", controllers.ListMediaFiles)
files.GET("", controllers.ListMediaFiles)
}
lofi := media.Group("/lofi") lofi := media.Group("/lofi")
{ lofi.GET("/", controllers.ListMediaLoFis)
lofi.GET("", controllers.ListMediaLoFis)
}
} }
+10 -8
View File
@@ -2,20 +2,22 @@ package routes
import ( import (
"kontor-api-gin/pkg/controllers" "kontor-api-gin/pkg/controllers"
"kontor-api-gin/pkg/globals"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func PublicRoutes(g *gin.RouterGroup) { func PublicRoutes(g *gin.RouterGroup) {
g.GET("/auth/login", controllers.LoginGetHandler) g.GET("/health", controllers.HealthGetHandler)
g.POST("/auth/login", controllers.LoginPostHandler) g.GET(globals.LoginUrl, controllers.LoginGetHandler)
g.GET("/", controllers.IndexGetHandler) g.POST(globals.LoginUrl, controllers.LoginPostHandler)
g.GET("/comic/comics", controllers.ListComicsGetHandler)
g.GET("", controllers.IndexGetHandler)
} }
func ProtectedRoutes(g *gin.RouterGroup) { func ProtectedRoutes(g *gin.RouterGroup) {
g.GET("/media/actors", controllers.IndexGetHandler) g.GET(globals.LogoutUrl, controllers.LogoutGetHandler)
g.GET("/comic/comics", controllers.IndexGetHandler) g.GET("/media/actors", controllers.ListMediaActorsGetHandler)
g.GET("/media/files", controllers.IndexGetHandler) // g.GET("/media/files", controllers.ListMediaFilesGetHandler)
g.GET("/media/lofis", controllers.IndexGetHandler) g.GET("/media/lofis", controllers.ListMediaLoFisGetHandler)
g.GET("/auth/logout", controllers.LogoutGetHandler)
} }
+151
View File
@@ -0,0 +1,151 @@
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"
}
+51 -51
View File
@@ -1,69 +1,69 @@
package schema package schema
import ( import (
"fmt" "fmt"
"log" "log"
"os" "log/slog"
"time" "os"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var DB *gorm.DB var DB *gorm.DB
func Connect() { func Connect() {
// Build the connection string from environment variables // Build the connection string from environment variables
// This keeps sensitive credentials out of the codebase // This keeps sensitive credentials out of the codebase
dsn := fmt.Sprintf( dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
os.Getenv("DB_HOST"), os.Getenv("DB_HOST"),
os.Getenv("DB_USER"), os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"), os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"), os.Getenv("DB_NAME"),
os.Getenv("DB_PORT"), os.Getenv("DB_PORT"),
) )
// Configure GORM with custom settings // Configure GORM with custom settings
// The logger helps with debugging SQL queries during development // The logger helps with debugging SQL queries during development
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), Logger: logger.Default.LogMode(logger.Info),
}) })
if err != nil { if err != nil {
log.Fatal("Failed to connect to database:", err) log.Fatal("Failed to connect to database:", err)
} }
// Get the underlying SQL database connection for pool configuration // Get the underlying SQL database connection for pool configuration
sqlDB, err := db.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
log.Fatal("Failed to get database instance:", err) log.Fatal("Failed to get database instance:", err)
} }
// Configure connection pool settings // Configure connection pool settings
// MaxIdleConns: number of connections kept open when idle // MaxIdleConns: number of connections kept open when idle
sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxIdleConns(10)
// MaxOpenConns: maximum total connections to the database // MaxOpenConns: maximum total connections to the database
sqlDB.SetMaxOpenConns(100) sqlDB.SetMaxOpenConns(100)
// ConnMaxLifetime: how long a connection can be reused // ConnMaxLifetime: how long a connection can be reused
sqlDB.SetConnMaxLifetime(time.Hour) sqlDB.SetConnMaxLifetime(time.Hour)
DB = db DB = db
log.Println("Database connected successfully") slog.Info("Database connected successfully")
} }
func RunMigrations() { func RunMigrations() {
// AutoMigrate creates tables, missing columns, and missing indexes // AutoMigrate creates tables, missing columns, and missing indexes
// It will NOT delete unused columns to protect your data // It will NOT delete unused columns to protect your data
err := DB.AutoMigrate( err := DB.AutoMigrate(
&MediaActor{}, &MediaActor{},
&MediaFile{}, &MediaFile{},
&MediaActorFile{}, &MediaActorFile{},
) )
if err != nil { if err != nil {
log.Fatal("Migration failed:", err) log.Fatal("Migration failed:", err)
} }
log.Println("Database migration completed") slog.Info("Database migration completed")
} }
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
<div class="content">{{ .content }}</div>
<div class="row">
<form class="form-inline" action="/comic/comics">
<input type="text" placeholder="Search" name="query">
<label>
<input type="checkbox" name="completed" {{ if .completed }}checked{{ end }}>Completed
</label>
<label>
<input type="checkbox" name="order" {{ if .order }}checked{{ end }}>Order
</label>
<button type="submit">Search</button>
<div class="pill-nav"><a href="/comic/comic/add">Add Comic</a></div>
</form>
</div>
<div class="row">
<div class="maincolumn">
<table class="table table-hover">
<thead><tr>
<th scope="col">Title</th>
<th scope="col">Publisher</th>
<th scope="col">Completed</th>
<th colspan="2">Actions</th>
</tr></thead>
<tbody>
{{ range .comics }}
<tr>
<th scope="row"><a href="/comic/comics/{{.ID}}">{{.Title}}</a></th>
{{ if .Publisher }}
<td><a href="/comic/publishers/{{.Publisher.ID}}">{{.Publisher.Name}}</a></td>
{{ else }}
<td>Publisher not set</td>
{{ end }}
<td>{{ .Completed }}</td>
<td><a href="/comic/comics/edit/{{.ID}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
<td><a href="/comic/comics/delete/{{.ID}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
{{ template "footer.tmpl" . }}
</body>
</html>
+1 -1
View File
@@ -7,7 +7,7 @@
{{ template "navbar.tmpl" . }} {{ template "navbar.tmpl" . }}
<div class="content"> <div class="content">
<form action="/auth/login" method="post"> <form action={{ .login }} method="post">
<div class="container" style="border: 1px solid lightgray; border-radius: 3px; padding: 15px;"> <div class="container" style="border: 1px solid lightgray; border-radius: 3px; padding: 15px;">
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
<div class="content">{{ .content }}</div>
{{ template "footer.tmpl" . }}
</body>
</html>
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
<div class="content">{{ .content }}</div>
{{ template "footer.tmpl" . }}
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
<div class="content">{{ .content }}</div>
{{ template "footer.tmpl" . }}
</body>
</html>
+3 -2
View File
@@ -3,10 +3,11 @@
<a href="/">Kontor</a> <a href="/">Kontor</a>
<a href="/comic/comics">Comics</a> <a href="/comic/comics">Comics</a>
<a href="/media/files">Media</a> <a href="/media/files">Media</a>
<a href="/media/lofis">Lofi</a>
{{ if .user }} {{ if .user }}
<a style="float:right" href="/auth/logout/">Logout</a></li> <a style="float:right" href={{ .logout }}>Logout</a></li>
{{ else }} {{ else }}
<a style="float:right" href="/auth/login/">Login</a></li> <a style="float:right" href="/login">Login</a></li>
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }}