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

This commit is contained in:
Thomas Peetz
2026-09-21 19:55:46 +02:00
parent 652aa3005b
commit f2f4a0aaf6
28 changed files with 787 additions and 41 deletions
+8
View File
@@ -0,0 +1,8 @@
package assets
import (
"embed"
)
//go:embed style/*
var StaticFiles embed.FS
+172
View File
@@ -0,0 +1,172 @@
* {
box-sizing: border-box;
}
body {
font-family: Arial;
padding: 10px;
background: lightgrey;
}
/* Header/Blog Title */
.header {
padding: 30px;
text-align: center;
background-color: lightblue;
}
.header h1 {
font-size: 50px;
}
/* Style the top navigation bar */
.topnav {
overflow: hidden;
background-color: darkgrey;
}
/* Style the topnav links */
.topnav a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
/* Change color on hover */
.topnav a:hover {
background-color: #ddd;
color: black;
}
.subnav {
overflow: hidden;
background-color: grey;
}
.subnav a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.form-inline {
display: flex;
flex-flow: row wrap;
align-items: center;
padding-top: 10px;
padding-left: 20px;
}
.form-inline label {
margin: 5px 10px 5px 0;
}
.form-inline input {
padding-right: 50px;
margin-right: 10px;
}
.form-inline button {
padding: 10px 20px;
background-color: dodgerblue;
border: 1px solid #ddd;
color: white;
border-radius: 10px;
}
.form-inline button:hover {
background-color: royalblue;
}
.pill-nav a {
display: inline-block;
color: white;
background-color: dodgerblue;
text-align: center;
padding: 10px;
text-decoration: none;
border-radius: 10px;
margin-left: 20px;
}
/* Change the color of links on mouse-over */
.pill-nav a:hover {
background-color: royalblue;
color: white;
}
/* Add a color to the active/current link */
.pill-nav a.active {
background-color: royalblue;
color: white;
}
.maincolumn {
float:left;
width: 100%;
}
/* Create two unequal columns that floats next to each other */
/* Left column */
.leftcolumn {
float: left;
width: 75%;
}
/* Right column */
.rightcolumn {
float: left;
width: 25%;
background-color: #f1f1f1;
padding-left: 20px;
}
/* Fake image */
.fakeimg {
background-color: #aaa;
width: 100%;
padding: 20px;
}
/* Add a card effect for articles */
.card {
background-color: white;
padding: 20px;
margin-top: 20px;
}
/* Clear floats after the columns */
.row::after {
content: "";
display: table;
clear: both;
}
/* Footer */
.footer {
padding: 20px;
text-align: center;
background: #ddd;
margin-top: 20px;
}
/* Responsive layout - when the screen is less than 800px wide, make the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 800px) {
.leftcolumn, .rightcolumn {
width: 100%;
padding: 0;
}
}
/* Responsive layout - when the screen is less than 400px wide, make the navigation links stack on top of each other instead of next to each other */
@media screen and (max-width: 400px) {
.topnav a {
float: none;
width: 100%;
}
}
+172
View File
@@ -0,0 +1,172 @@
* {
box-sizing: border-box;
}
body {
font-family: Arial;
padding: 10px;
background: lightgrey;
}
/* Header/Blog Title */
.header {
padding: 30px;
text-align: center;
background-color: lightblue;
}
.header h1 {
font-size: 50px;
}
/* Style the top navigation bar */
.topnav {
overflow: hidden;
background-color: darkgrey;
}
/* Style the topnav links */
.topnav a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
/* Change color on hover */
.topnav a:hover {
background-color: #ddd;
color: black;
}
.subnav {
overflow: hidden;
background-color: grey;
}
.subnav a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.form-inline {
display: flex;
flex-flow: row wrap;
align-items: center;
padding-top: 10px;
padding-left: 20px;
}
.form-inline label {
margin: 5px 10px 5px 0;
}
.form-inline input {
padding-right: 50px;
margin-right: 10px;
}
.form-inline button {
padding: 10px 20px;
background-color: dodgerblue;
border: 1px solid #ddd;
color: white;
border-radius: 10px;
}
.form-inline button:hover {
background-color: royalblue;
}
.pill-nav a {
display: inline-block;
color: white;
background-color: dodgerblue;
text-align: center;
padding: 10px;
text-decoration: none;
border-radius: 10px;
margin-left: 20px;
}
/* Change the color of links on mouse-over */
.pill-nav a:hover {
background-color: royalblue;
color: white;
}
/* Add a color to the active/current link */
.pill-nav a.active {
background-color: royalblue;
color: white;
}
.maincolumn {
float:left;
width: 100%;
}
/* Create two unequal columns that floats next to each other */
/* Left column */
.leftcolumn {
float: left;
width: 75%;
}
/* Right column */
.rightcolumn {
float: left;
width: 25%;
background-color: #f1f1f1;
padding-left: 20px;
}
/* Fake image */
.fakeimg {
background-color: #aaa;
width: 100%;
padding: 20px;
}
/* Add a card effect for articles */
.card {
background-color: white;
padding: 20px;
margin-top: 20px;
}
/* Clear floats after the columns */
.row::after {
content: "";
display: table;
clear: both;
}
/* Footer */
.footer {
padding: 20px;
text-align: center;
background: #ddd;
margin-top: 20px;
}
/* Responsive layout - when the screen is less than 800px wide, make the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 800px) {
.leftcolumn, .rightcolumn {
width: 100%;
padding: 0;
}
}
/* Responsive layout - when the screen is less than 400px wide, make the navigation links stack on top of each other instead of next to each other */
@media screen and (max-width: 400px) {
.topnav a {
float: none;
width: 100%;
}
}
+34 -20
View File
@@ -2,11 +2,19 @@ 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"
"log"
"net/http"
"os"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
@@ -27,18 +35,40 @@ func main() {
// Create Gin router with default middleware
// Default includes Logger and Recovery middleware
router := gin.Default()
//router := gin.Default()
// Create a new router without default mmiddleware
router := gin.New()
templ := template.Must(template.New("").ParseFS(templates.TemplateFiles, "html/*.tmpl"))
router.SetHTMLTemplate(templ)
router.StaticFS("/public", http.FS(assets.StaticFiles))
// Add CORS middleware for frontend access
router.Use(corsMiddleware())
// router.Use(middleware.CorsMiddleware)
// Setup application routes
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)))
// Setup api routes
api := routes.SetupApiRoutes(router)
routes.SetupMediaRoutes(api)
// Setup public Webapp routes
public := router.Group("/")
routes.PublicRoutes(public)
// Setup protected Webapp routes
protected := router.Group("/")
protected.Use(middleware.AuthRequired)
routes.ProtectedRoutes(protected)
// Health check endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
})
})
@@ -56,19 +86,3 @@ func main() {
log.Println("Kontor finished")
}
// corsMiddleware adds CORS headers for cross-origin requests
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
+4
View File
@@ -8,6 +8,7 @@ require (
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/sessions v1.1.2 // indirect
github.com/gin-contrib/sse v1.1.2 // indirect
github.com/gin-gonic/gin v1.12.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -16,6 +17,9 @@ require (
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // 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
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.11.0 // indirect
+8
View File
@@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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=
@@ -27,6 +29,12 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+93
View File
@@ -0,0 +1,93 @@
package controllers
import (
"kontor-api-gin/pkg/globals"
"kontor-api-gin/pkg/helpers"
"log"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func LoginGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
if user != nil {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Please logout first",
"user": user,
})
return
}
c.HTML(http.StatusOK, "login.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "",
"user": user,
})
}
func LoginPostHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
if user != nil {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Please logout first",
})
return
}
username := c.PostForm("username")
password := c.PostForm("password")
if helpers.EmptyUserPass(username, password) {
c.HTML(http.StatusBadRequest, "login.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Parameters can't be empty",
})
}
if !helpers.CheckUserPass(username, password) {
c.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Incorrect username or password",
})
}
session.Set(globals.UserKey, username)
if err := session.Save(); err != nil {
c.HTML(http.StatusInternalServerError, "login.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "Failed to save session",
})
return
}
c.Redirect(http.StatusMovedPermanently, "/")
}
func LogoutGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
log.Printf("logging out user %v", user)
if user == nil {
log.Println("Invalid session token")
return
}
session.Delete(globals.UserKey)
if err := session.Save(); err != nil {
log.Printf("Failed to save session: %v", err)
return
}
c.Redirect(http.StatusMovedPermanently, "/")
}
+20
View File
@@ -0,0 +1,20 @@
package controllers
import (
"kontor-api-gin/pkg/globals"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func IndexGetHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get(globals.UserKey)
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Kontor Gin",
"version": "0.4.0",
"content": "This is an index page...",
"user": user,
})
}
@@ -1,4 +1,4 @@
package handlers
package controllers
import (
"kontor-api-gin/pkg/schema"
@@ -20,3 +20,15 @@ func ListMediaFiles(c *gin.Context) {
"files": files,
})
}
func ListMediaLoFis(c *gin.Context) {
var lofis []schema.MediaLoFi
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,
// })
}
+5
View File
@@ -0,0 +1,5 @@
package globals
var Secret = []byte("secret")
const UserKey = "user"
+11
View File
@@ -0,0 +1,11 @@
package helpers
import "strings"
func CheckUserPass(username, password string) bool {
return true
}
func EmptyUserPass(username, password string) bool {
return strings.Trim(username, " ") == "" || strings.Trim(password, " ") == ""
}
+22
View File
@@ -0,0 +1,22 @@
package middleware
import (
"log"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func AuthRequired(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")
log.Printf("current user %v", user)
if user == nil {
log.Println("User not logged in")
c.Redirect(http.StatusMovedPermanently, "/auth/login")
c.Abort()
return
}
c.Next()
}
+21
View File
@@ -0,0 +1,21 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// corsMiddleware adds CORS headers for cross-origin requests
func CorsMiddleware(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
+7 -2
View File
@@ -1,7 +1,7 @@
package routes
import (
"kontor-api-gin/pkg/handlers"
"kontor-api-gin/pkg/controllers"
"github.com/gin-gonic/gin"
)
@@ -11,6 +11,11 @@ func SetupMediaRoutes(router *gin.RouterGroup) {
files := media.Group("/files")
{
files.GET("", handlers.ListMediaFiles)
files.GET("", controllers.ListMediaFiles)
}
lofi := media.Group("/lofi")
{
lofi.GET("", controllers.ListMediaLoFis)
}
}
+21
View File
@@ -0,0 +1,21 @@
package routes
import (
"kontor-api-gin/pkg/controllers"
"github.com/gin-gonic/gin"
)
func PublicRoutes(g *gin.RouterGroup) {
g.GET("/auth/login", controllers.LoginGetHandler)
g.POST("/auth/login", controllers.LoginPostHandler)
g.GET("/", controllers.IndexGetHandler)
}
func ProtectedRoutes(g *gin.RouterGroup) {
g.GET("/media/actors", controllers.IndexGetHandler)
g.GET("/comic/comics", controllers.IndexGetHandler)
g.GET("/media/files", controllers.IndexGetHandler)
g.GET("/media/lofis", controllers.IndexGetHandler)
g.GET("/auth/logout", controllers.LogoutGetHandler)
}
+25 -14
View File
@@ -1,37 +1,48 @@
package schema
import (
)
type MediaLoFi struct {
Base
WebLink 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"`
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"
return "media_file"
}
type MediaActor struct {
Base
Name string `json:"name"`
WebLink string `gorm:"uniqueIndex" json:"url"`
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"
return "media_actor"
}
type MediaActorFile struct {
@@ -44,7 +55,7 @@ type MediaActorFile struct {
}
func (media_file *MediaActorFile) TableName() string {
return "media_actor_file"
return "media_actor_file"
}
// type MediaArticle struct {
+3
View File
@@ -0,0 +1,3 @@
{{ define "footer.tmpl" }}
<div class="footer">{{ .version }}</div>
{{end}}
+14
View File
@@ -0,0 +1,14 @@
{{ 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
@@ -0,0 +1,12 @@
<!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>
+40
View File
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="de-DE">
{{ template "header.tmpl" . }}
<body>
<div class="header"></div>
{{ template "navbar.tmpl" . }}
<div class="content">
<form action="/auth/login" method="post">
<div class="container" style="border: 1px solid lightgray; border-radius: 3px; padding: 15px;">
{{ if .content }}
<div class="notification is-danger">{{ .content }}</div>
{{ end}}
<div class="field">
<label class="label">Username</label>
<div class="control">
<input class="input" type="text" placeholder="Enter Username" name="username" required/>
</div>
</div>
<div class="field">
<label class="label">Passwort</label>
<div class="control">
<input class="input" type="password" placeholder="Enter Password" name="password" required/>
</div>
</div>
<div class="field">
<button class="button is-link">Submit</button>
</div>
</div>
</form>
</div>
{{ template "footer.tmpl" . }}
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
{{ define "navbar.tmpl" }}
<div class="topnav">
<a href="/">Kontor</a>
<a href="/comic/comics">Comics</a>
<a href="/media/files">Media</a>
{{ if .user }}
<a style="float:right" href="/auth/logout/">Logout</a></li>
{{ else }}
<a style="float:right" href="/auth/login/">Login</a></li>
{{ end }}
</div>
{{ end }}
+8
View File
@@ -0,0 +1,8 @@
package templates
import (
"embed"
)
//go:embed html/*
var TemplateFiles embed.FS