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

This commit is contained in:
Thomas Peetz
2026-09-23 17:09:48 +02:00
parent 6b5167a5c0
commit a0ad4e5c26
23 changed files with 295 additions and 172 deletions
+5 -3
View File
@@ -10,7 +10,7 @@ RUN go mod download
# Copy the source code
COPY . .
# Build the Go application (strip debug info for smaller size)
RUN go build -trimpath -ldflags="-s -w" -o kontor cmd/kontor/main.go
RUN go build -trimpath -ldflags="-s -w" -o kontor-gin cmd/kontor-gin/main.go
# ---------- Stage 2: Final ----------
FROM alpine:latest
@@ -19,7 +19,9 @@ WORKDIR /app
# Install runtime dependencies you actually need
RUN apk add --no-cache ca-certificates tzdata curl
# Copy the binary and set ownership
COPY --from=builder /app/kontor /app/kontor
COPY --from=builder /app/kontor-gin /app/kontor-gin
COPY --from=builder /app/templates /app/templates
COPY --from=builder /app/assets /app/assets
ENV DB_HOST=postgres DB_USER=kontor DB_PASSWORD=kontor DB_NAME=kontor DB_PORT=5432 GIN_MODE=release
# Set the entrypoint command
ENTRYPOINT ["/app/kontor"]
ENTRYPOINT ["/app/kontor-gin"]
+88
View File
@@ -0,0 +1,88 @@
package main
import (
// "kontor-api-gin/pkg/handler"
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/middleware"
"kontor-api-gin/pkg/routes"
"kontor-api-gin/pkg/schema"
"log"
"log/slog"
"os"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
gin_slog "github.com/gin-contrib/slog"
"github.com/gin-gonic/gin"
)
func main() {
slog.Info("Kontor started")
// Set Gin mode based on environment
// Production mode disables debug output
if os.Getenv("GIN_MODE") == "release" {
gin.SetMode(gin.ReleaseMode)
}
// Connect to database
schema.Connect()
// 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),
))
router.LoadHTMLGlob("templates/**/*.html")
router.Static("/public", "./assets")
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 public Webapp routes
routes.PublicRoutes(router)
// Setup protected Webapp routes
protected := router.Group("/auth")
protected.Use(middleware.AuthRequired)
routes.ProtectedRoutes(router)
comics := router.Group("/comics")
comics.Use(middleware.AuthRequired)
routes.ComicRoutes(router)
media := router.Group("/media")
media.Use(middleware.AuthRequired)
routes.MediaRoutes(router)
// Start server
port := os.Getenv("PORT")
if port == "" {
port = "8550"
}
slog.Info("Server starting", "port", port)
if err := router.Run(":" + port); err != nil {
log.Fatal("Failed to start server:", err)
}
slog.Info("Kontor finished")
}
+1 -1
View File
@@ -49,7 +49,7 @@ func main() {
gin_slog.WithServerErrorLevel(slog.LevelInfo),
))
templ := template.Must(template.New("").ParseFS(templates.TemplateFiles, "html/*.tmpl"))
templ := template.Must(template.New("").ParseFS(templates.TemplateFiles, "layouts/*.html", "pages/*.html"))
router.SetHTMLTemplate(templ)
router.StaticFS("/public", http.FS(assets.StaticFiles))
+8 -8
View File
@@ -15,9 +15,9 @@ func LoginGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
logger.Info("LoginGetHandler", "user", user)
logger.Info("LoginGetHandler", globals.UserKey, user)
if user != nil {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
c.HTML(http.StatusBadRequest, "login.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"login": globals.LoginUrl,
@@ -26,7 +26,7 @@ func LoginGetHandler(c *gin.Context) {
})
return
}
c.HTML(http.StatusOK, "login.tmpl", gin.H{
c.HTML(http.StatusOK, "login.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"login": globals.LoginUrl,
@@ -41,7 +41,7 @@ func LoginPostHandler(c *gin.Context) {
logger := slog.Get(c)
logger.Info("LoginPostHandler", "user", user)
if user != nil {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
c.HTML(http.StatusBadRequest, "login.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"login": globals.LoginUrl,
@@ -54,7 +54,7 @@ func LoginPostHandler(c *gin.Context) {
password := c.PostForm("password")
if helpers.EmptyUserPass(username, password) {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
c.HTML(http.StatusBadRequest, "login.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"login": globals.LoginUrl,
@@ -63,7 +63,7 @@ func LoginPostHandler(c *gin.Context) {
}
if !helpers.CheckUserPass(username, password) {
c.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"login": globals.LoginUrl,
@@ -73,7 +73,7 @@ func LoginPostHandler(c *gin.Context) {
session.Set(globals.UserKey, username)
if err := session.Save(); err != nil {
c.HTML(http.StatusInternalServerError, "login.tmpl", gin.H{
c.HTML(http.StatusInternalServerError, "login.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"login": globals.LoginUrl,
@@ -90,7 +90,7 @@ func LogoutGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
logger.Info("LogoutGetHandler", "user", user)
logger.Info("LogoutGetHandler", globals.UserKey, user)
if user == nil {
logger.Info("Invalid session token")
return
+3 -2
View File
@@ -13,11 +13,12 @@ func IndexGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
logger.Info("IndexGetHandler", "user", user)
c.HTML(http.StatusOK, "index.tmpl", gin.H{
logger.Info("IndexGetHandler", globals.UserKey, user)
c.HTML(http.StatusOK, "home.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "This is an index page...",
"login": globals.LoginUrl,
"logout": globals.LogoutUrl,
"user": user,
})
+12 -11
View File
@@ -30,25 +30,24 @@ func ListMediaLoFis(c *gin.Context) {
result := schema.DB.Find(&lofis)
log.Printf("found %d MediaLoFis", result.RowsAffected)
c.JSON(http.StatusOK, lofis)
// c.JSON(http.StatusOK, gin.H{
// "message": "All MediaLoFis",
// "lofis": lofis,
// })
}
func ListMediaActorsGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
logger := slog.Get(c)
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{
logger.Info("ListMediaActorsGetHandler", "count", result.RowsAffected)
c.HTML(http.StatusOK, "mediaactors.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Media Actors...",
"actors": actors,
"login": globals.LoginUrl,
"logout": globals.LogoutUrl,
"user": user,
})
}
@@ -61,12 +60,14 @@ func ListMediaFilesGetHandler(c *gin.Context) {
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{
logger.Info("ListMediaFilesGetHandler", "count", result.RowsAffected)
c.HTML(http.StatusOK, "mediafiles.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Media Files...",
"files": files,
"login": globals.LoginUrl,
"logout": globals.LogoutUrl,
"user": user,
})
@@ -80,14 +81,14 @@ func ListMediaLoFisGetHandler(c *gin.Context) {
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{
logger.Info("ListMediaLoFisGetHandler", "count", result.RowsAffected)
c.HTML(http.StatusOK, "media_lofi.html", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Media LoFis...",
"lofis": lofis,
"login": globals.LoginUrl,
"logout": globals.LogoutUrl,
"user": user,
})
}
+2 -2
View File
@@ -6,6 +6,6 @@ const UserKey = "user"
const Version = "0.4.0"
const LoginUrl = "/login"
const LoginUrl = "/auth/login"
const LogoutUrl = "/exit"
const LogoutUrl = "/auth/logout"
+19 -8
View File
@@ -3,21 +3,32 @@ package routes
import (
"kontor-api-gin/pkg/controllers"
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/middleware"
"github.com/gin-gonic/gin"
)
func PublicRoutes(g *gin.RouterGroup) {
func PublicRoutes(g *gin.Engine) {
g.GET("/health", controllers.HealthGetHandler)
g.GET(globals.LoginUrl, controllers.LoginGetHandler)
g.POST(globals.LoginUrl, controllers.LoginPostHandler)
g.GET("/comic/comics", controllers.ListComicsGetHandler)
g.GET("", controllers.IndexGetHandler)
g.GET("/", controllers.IndexGetHandler)
}
func ProtectedRoutes(g *gin.RouterGroup) {
g.GET(globals.LogoutUrl, controllers.LogoutGetHandler)
g.GET("/media/actors", controllers.ListMediaActorsGetHandler)
// g.GET("/media/files", controllers.ListMediaFilesGetHandler)
g.GET("/media/lofis", controllers.ListMediaLoFisGetHandler)
func ComicRoutes(g *gin.Engine) {
g.GET("/comic/comics", controllers.ListComicsGetHandler)
}
func MediaRoutes(g *gin.Engine) {
media := g.Group("/media")
media.Use(middleware.AuthRequired)
media.GET("/actors", controllers.ListMediaActorsGetHandler)
media.GET("/files", controllers.ListMediaFilesGetHandler)
media.GET("/lofis", controllers.ListMediaLoFisGetHandler)
}
func ProtectedRoutes(g *gin.Engine) {
auth := g.Group("/auth")
auth.Use(middleware.AuthRequired)
auth.GET("/logout", controllers.LogoutGetHandler)
}
@@ -1,58 +0,0 @@
<!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>
{{ if .Completed }}
<img src="/tick.png" alt="true" width="24" height="24">
{{ else }}
<img src="/cross.png" alt="true" width="24" height="24">
{{ end }}
</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>
-3
View File
@@ -1,3 +0,0 @@
{{ define "footer.tmpl" }}
<div class="footer">{{ .version }}</div>
{{end}}
-14
View File
@@ -1,14 +0,0 @@
{{ define "header.tmpl" }}
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/public/style/main.css" />
<title>{{ .title }}</title>
</head>
{{ end }}
-12
View File
@@ -1,12 +0,0 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
<div class="content">{{ .title }}</div>
{{ template "footer.tmpl" . }}
</body>
</html>
@@ -1,12 +0,0 @@
<!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>
@@ -1,12 +0,0 @@
<!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
@@ -1,12 +0,0 @@
<!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>
+24
View File
@@ -0,0 +1,24 @@
{{ define "main" }}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/public/style/main.css" />
<title>{{ .title }}</title>
</head>
<body>
<div class="header"></div>
{{ template "navbar" . }}
{{ template "content" . }}
<div class="footer">{{ .version }}</div>
</body>
</html>
{{ end }}
@@ -1,13 +1,13 @@
{{ define "navbar.tmpl" }}
{{ define "navbar" }}
<div class="topnav">
<a href="/">Kontor</a>
<a href="/comic/comics">Comics</a>
<a href="/media/files">Media</a>
<a href="/media/lofis">Lofi</a>
{{ if .user }}
<a style="float:right" href={{ .logout }}>Logout</a></li>
<a style="float:right" href="{{ .logout }}">Logout</a></li>
{{ else }}
<a style="float:right" href="/login">Login</a></li>
<a style="float:right" href="{{ .login }}">Login</a></li>
{{ end }}
</div>
{{ end }}
+52
View File
@@ -0,0 +1,52 @@
{{ define "comics.html" }}
{{ template "main" . }}
{{ end }}
{{ define "content" }}
<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>
{{ if .Completed }}
<img src="/tick.png" alt="true" width="24" height="24">
{{ else }}
<img src="/cross.png" alt="true" width="24" height="24">
{{ end }}
</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>
{{ end }}
+7
View File
@@ -0,0 +1,7 @@
{{ define "home.html" }}
{{ template "main" . }}
{{ end }}
{{ define "content" }}
<h1>{{ .content }}</h1>
{{ end }}
+52
View File
@@ -0,0 +1,52 @@
{{ define "media_lofi.html" }}
{{ template "main" . }}
{{ end }}
{{ define "content" }}
<div class="row">
<form class="form-inline" action="/media/lofi">
<input type="text" placeholder="Search" name="query">
<label>
<input type="checkbox" name="review" {{ if .Review }}checked{{ end }}>Completed
</label>
<label>
<input type="checkbox" name="should_download" {{ if .ShouldDownload }}checked{{ end }}>Order
</label>
<button type="submit">Search</button>
<div class="pill-nav"><a href="/media/lofi/add">Add Lofi</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="/media/lofi/{{.ID}}">{{.Title}}</a></th>
{{ if .WebLink }}
<td><a href="{{.WebLink}}">{{.Title}}</a></td>
{{ else }}
<td>WebLink not set</td>
{{ end }}
<td>
{{ if .Review }}
<img src="/tick.png" alt="true" width="24" height="24">
{{ else }}
<img src="/cross.png" alt="true" width="24" height="24">
{{ end }}
</td>
<td><a href="/media/lofi/edit/{{.ID}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
<td><a href="/media/lofi/delete/{{.ID}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
{{ end }}
@@ -1,11 +1,8 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
{{ define "login.html" }}
{{ template "main" . }}
{{ end }}
{{ define "content" }}
<div class="content">
<form action={{ .login }} method="post">
@@ -34,7 +31,4 @@
</div>
</form>
</div>
{{ template "footer.tmpl" . }}
</body>
</html>
{{ end }}
@@ -0,0 +1,7 @@
{{ define "home.html" }}
{{ template "main" . }}
{{ end }}
{{ define "content" }}
<h1>{{ .content }}</h1>
{{ end }}
@@ -0,0 +1,7 @@
{{ define "mediafiles.html" }}
{{ template "main" . }}
{{ end }}
{{ define "content" }}
<h1>{{ .content }}</h1>
{{ end }}