add project kontor-api-echo

This commit is contained in:
2026-01-03 02:53:50 +01:00
parent fe919eaa35
commit 0392ac49fb
33 changed files with 949 additions and 26 deletions
+30 -8
View File
@@ -42,7 +42,7 @@ services:
image: kontor-api:0.2.0-SNAPSHOT image: kontor-api:0.2.0-SNAPSHOT
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api:8800/health"] test: ["CMD", "curl", "-f", "http://kontor-api:8500/health"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
@@ -51,22 +51,22 @@ services:
- integration - integration
- frontend - frontend
ports: ports:
- 8800:8800 - 8500:8500
volumes: volumes:
- images-data:/data/images - images-data:/data/images
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
kontor-api-go: kontor-api-fiber:
build: build:
context: ./kontor-api-go context: ./kontor-api-fiber
dockerfile: Dockerfile dockerfile: Dockerfile
tags: tags:
- kontor-api-go:0.2.0-SNAPSHOT - kontor-api-fiber:0.2.0-SNAPSHOT
image: kontor-api-go:0.2.0-SNAPSHOT image: kontor-api-fiber:0.2.0-SNAPSHOT
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api-go:8900/health"] test: ["CMD", "curl", "-f", "http://kontor-api-fiber:8600/health"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
@@ -75,7 +75,29 @@ services:
- integration - integration
- frontend - frontend
ports: ports:
- 8900:8900 - 8600:8600
depends_on:
postgres:
condition: service_healthy
kontor-api-echo:
build:
context: ./kontor-api-echo
dockerfile: Dockerfile
tags:
- kontor-api-echo:0.2.0-SNAPSHOT
image: kontor-api-echo:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api-echo:8700/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8700:8700
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"fmt"
"kontor-api-echo/pkg/handler"
"kontor-api-echo/pkg/schema"
"log"
// jwtware "github.com/gofiber/contrib/jwt"
// "github.com/gofiber/fiber/v2/middleware/logger"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
log.Println("Kontor started")
if err := schema.Connect(); err != nil {
log.Fatal(err)
}
e := echo.New()
e.GET("/health", handler.GetHealth)
e.POST("/login", handler.Login)
skipper := func(c echo.Context) bool {
// Skip health check endpoint
return c.Request().URL.Path == "/health"
}
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogStatus: true,
LogURI: true,
Skipper: skipper,
BeforeNextFunc: func(c echo.Context) {
c.Set("customValueFromContext", 42)
},
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
value, _ := c.Get("customValueFromContext").(int)
fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value)
return nil
},
}))
group := e.Group("/api")
group.Use(echojwt.WithConfig(echojwt.Config{SigningKey: []byte("secret")}))
handler.SetupComicRoutes(group)
handler.SetupMediaRoutes(group)
e.Logger.Fatal(e.Start(":8700"))
log.Println("Kontor finished")
}
+39
View File
@@ -0,0 +1,39 @@
module kontor-api-echo
go 1.24.2
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/labstack/echo-jwt/v4 v4.4.0 // indirect
github.com/labstack/echo/v4 v4.15.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/bun v1.2.16 // indirect
github.com/uptrace/bun/dialect/pgdialect v1.2.16 // indirect
github.com/uptrace/bun/driver/pgdriver v1.2.16 // indirect
github.com/uptrace/bun/extra/bundebug v1.2.16 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
mellium.im/sasl v0.3.2 // indirect
)
+68
View File
@@ -0,0 +1,68 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo-jwt/v4 v4.4.0 h1:nrXaEnJupfc2R4XChcLRDyghhMZup77F8nIzHnBK19U=
github.com/labstack/echo-jwt/v4 v4.4.0/go.mod h1:kYXWgWms9iFqI3ldR+HAEj/Zfg5rZtR7ePOgktG4Hjg=
github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc=
github.com/uptrace/bun v1.2.16/go.mod h1:jMoNg2n56ckaawi/O/J92BHaECmrz6IRjuMWqlMaMTM=
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
github.com/uptrace/bun/driver/pgdriver v1.2.16 h1:b1kpXKUxtTSGYow5Vlsb+dKV3z0R7aSAJNfMfKp61ZU=
github.com/uptrace/bun/driver/pgdriver v1.2.16/go.mod h1:H6lUZ9CBfp1X5Vq62YGSV7q96/v94ja9AYFjKvdoTk0=
github.com/uptrace/bun/extra/bundebug v1.2.16 h1:3OXAfHTU4ydu2+4j05oB1BxPx6+ypdWIVzTugl/7zl0=
github.com/uptrace/bun/extra/bundebug v1.2.16/go.mod h1:vk6R/1i67/S2RvUI5AH/m3P5e67mOkfDCmmCsAPUumo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
+66
View File
@@ -0,0 +1,66 @@
package handler
import (
"context"
"kontor-api-echo/pkg/schema"
"kontor-api-echo/pkg/utils"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/uptrace/bun"
)
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}
func Login(c echo.Context) error {
user := c.FormValue("user")
pass := c.FormValue("pass")
var profile schema.Profile
var err error
var db *bun.DB
ctx := context.Background()
db, _ = schema.GetDatabase()
err = db.NewSelect().Model(&profile).Where("email = ?", user).Scan(ctx)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
if !utils.ComparePassword(profile.Password, pass) {
return echo.ErrUnauthorized
}
// Set custom claims
claims := &jwtCustomClaims{
"Jon Snow",
true,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{"token": t})
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome "+name+"!")
}
+78
View File
@@ -0,0 +1,78 @@
package handler
import (
"context"
"kontor-api-echo/pkg/schema"
"log"
"net/http"
"github.com/labstack/echo/v4"
"github.com/uptrace/bun"
)
func SetupComicRoutes(api *echo.Group) {
comics := api.Group("/comics")
comics.GET("/comics", GetAllComics)
comics.GET("/publishers", GetAllPublishers)
comics.GET("/comicworks", GetAllComicWorks)
}
func GetAllComics(c echo.Context) error {
var comics []schema.Comic
var err error
var db *bun.DB
ctx := context.Background()
db, err = schema.GetDatabase()
if err != nil {
log.Fatal(err)
}
err = db.NewSelect().Model(&comics).Relation("Publisher").Scan(ctx)
if err != nil {
log.Fatal(err)
return echo.ErrInternalServerError
}
return c.JSON(http.StatusOK, comics)
}
func GetAllPublishers(c echo.Context) error {
var publishers []schema.Publisher
var err error
var db *bun.DB
ctx := context.Background()
db, err = schema.GetDatabase()
if err != nil {
log.Fatal(err)
}
err = db.NewSelect().Model(&publishers).Relation("ParentPublisher").Scan(ctx)
if err != nil {
log.Fatal(err)
return echo.ErrInternalServerError
}
return c.JSON(http.StatusOK, publishers)
}
func GetAllComicWorks(c echo.Context) error {
var comic_works []schema.ComicWork
var err error
var db *bun.DB
ctx := context.Background()
db, err = schema.GetDatabase()
if err != nil {
log.Fatal(err)
}
err = db.NewSelect().Model(&comic_works).Relation("Comic").Relation("Artist").Relation("WorkType").Scan(ctx)
if err != nil {
log.Fatal(err)
return echo.ErrInternalServerError
}
return c.JSON(http.StatusOK, comic_works)
}
+17
View File
@@ -0,0 +1,17 @@
package handler
import (
"net/http"
"github.com/labstack/echo/v4"
)
type Status struct {
Status string `json:"status"`
}
func GetHealth(c echo.Context) error {
status := new(Status)
status.Status = "ok"
return c.JSON(http.StatusOK, status)
}
+67
View File
@@ -0,0 +1,67 @@
package handler
import (
"context"
"fmt"
"kontor-api-echo/pkg/schema"
"log"
"net/http"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/uptrace/bun"
)
func SetupMediaRoutes(api *echo.Group) {
media := api.Group("/media")
media.GET("/files", GetAllFiles)
media.POST("/files", AddFile)
}
func GetAllFiles(c echo.Context) error {
var files []schema.MediaFile
var err error
var db *bun.DB
ctx := context.Background()
db, err = schema.GetDatabase()
if err != nil {
log.Fatal(err)
}
err = db.NewSelect().Model(&files).Relation("MediaActorFiles").Scan(ctx)
if err != nil {
log.Fatal(err)
return echo.ErrInternalServerError
}
return c.JSON(http.StatusOK, files)
}
type Link struct {
URL string `json:"url"`
}
func AddFile(c echo.Context) error {
var err error
var db *bun.DB
ctx := context.Background()
link := new(Link)
if err = c.Bind(link); err != nil {
return err
}
log.Printf("URL %s has been sent", link)
db, err = schema.GetDatabase()
if err != nil {
log.Fatal(err)
}
id := uuid.NewString()
timestamp := time.Now()
mediafile := &schema.MediaFile{ID: id, CreatedAt: timestamp, UpdatedAt: timestamp, WebLink: fmt.Sprintf("%s", link), Version: 1, ShouldDownload: true, Review: true}
_, err = db.NewInsert().Model(mediafile).Exec(ctx)
if err != nil {
return err
}
return c.String(http.StatusCreated, "Link created")
}
@@ -1,7 +1,7 @@
package utils package utils
import ( import (
"kontor-api-go/pkg/schema" "kontor-api-echo/pkg/schema"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
+28
View File
@@ -0,0 +1,28 @@
# ---------- Stage 1: Build ----------
FROM golang:1.25-alpine AS builder
# Set the working directory
WORKDIR /app
# Ensure a portable, static-ish binary
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
# Copy and download dependencies
COPY go.mod go.sum ./
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
# ---------- Stage 2: Final ----------
FROM alpine:latest
# Set the working directory
WORKDIR /app
# Install runtime dependencies you actually need
RUN apk add --no-cache ca-certificates tzdata curl
# Create non-root user for security
RUN addgroup -S kontor && adduser -S -G kontor -H -s /sbin/nologin kontor
# Copy the binary and set ownership
COPY --from=builder --chown=kontor:kontor /app/kontor /app/kontor
# Run as non-root user
USER kontor
# Set the entrypoint command
ENTRYPOINT ["/app/kontor"]
@@ -1,8 +1,8 @@
package main package main
import ( import (
"kontor-api-go/pkg/handler" "kontor-api-fiber/pkg/handler"
"kontor-api-go/pkg/schema" "kontor-api-fiber/pkg/schema"
"log" "log"
jwtware "github.com/gofiber/contrib/jwt" jwtware "github.com/gofiber/contrib/jwt"
@@ -35,7 +35,7 @@ func main() {
handler.SetupComicRoutes(api) handler.SetupComicRoutes(api)
handler.SetupMediaRoutes(api) handler.SetupMediaRoutes(api)
// Listen on port 8900 // Listen on port 8900
app.Listen(":8900") app.Listen(":8600")
log.Println("Kontor finished") log.Println("Kontor finished")
} }
@@ -1,4 +1,4 @@
module kontor-api-go module kontor-api-fiber
go 1.24.2 go 1.24.2
@@ -2,8 +2,8 @@ package handler
import ( import (
"context" "context"
"kontor-api-go/pkg/schema" "kontor-api-fiber/pkg/schema"
"kontor-api-go/pkg/utils" "kontor-api-fiber/pkg/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -2,7 +2,7 @@ package handler
import ( import (
"context" "context"
"kontor-api-go/pkg/schema" "kontor-api-fiber/pkg/schema"
"log" "log"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"kontor-api-go/pkg/schema" "kontor-api-fiber/pkg/schema"
"log" "log"
"time" "time"
+62
View File
@@ -0,0 +1,62 @@
package schema
import (
"time"
"github.com/uptrace/bun"
)
type Profile struct {
bun.BaseModel `bun:"table:profile"`
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"`
FirstName string `bun:"first_name"`
LastName string `bun:"last_name"`
UserName string `bun:"user_name,unique:user_name"`
Email string `bun:"email"`
Password string `bun:"password"`
Enabled bool `bun:"enabled"`
Assignments []Assignment `bun:"rel:has-many,join:id=profile_id"`
Tokens []Token `bun:"rel:has-many,join:id=profile_id"`
}
type Permission struct {
bun.BaseModel `bun:"table:permission"`
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"`
Name string `bun:"name,unique:name"`
Assignments []Assignment `bun:"rel:has-many,join:id=permission_id"`
}
type Token struct {
bun.BaseModel `bun:"table:token"`
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"`
Name string `bun:"name,unique:name"`
LastUsedAt time.Time `bun:"last_used_date,nullzero,notnull,default:current_timestamp"`
Enabled bool `bun:"enabled,default:true"`
ProfileID *string `bun:"profile_id"`
Profile *Profile `bun:"rel:belongs-to,join:profile_id=id"`
}
type Assignment struct {
bun.BaseModel `bun:"table:assignment"`
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"`
ProfileID *string `bun:"profile_id"`
Profile *Profile `bun:"rel:belongs-to,join:profile_id=id"`
PermissionID *string `bun:"permission_id"`
Permission *Permission `bun:"rel:belongs-to,join:permission_id=id"`
}
+140
View File
@@ -0,0 +1,140 @@
package schema
import (
"time"
"github.com/uptrace/bun"
)
type Publisher struct {
bun.BaseModel `bun:"table:publisher"`
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"`
Name string `bun:"name,unique:name"`
WebLink string `bun:"weblink"`
ParentPublisherID *string `bun:"parent_publisher_id"`
ParentPublisher *Publisher `bun:"rel:belongs-to,join:parent_publisher_id=id"`
Imprints []Publisher `bun:"rel:has-many,join:id=parent_publisher_id"`
Comics []Comic `bun:"rel:has-many,join:id=publisher_id"`
}
type Comic struct {
bun.BaseModel `bun:"table:comic"`
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"`
Title string `bun:"title,unique:title,notnull"`
CurrentOrder bool `bun:"current_order"`
Completed bool `bun:"completed"`
WebLink string `bun:"weblink"`
PublisherID *string `bun:"publisher_id"`
Publisher *Publisher `bun:"rel:belongs-to,join:publisher_id=id"`
Issues []Issue `bun:"rel:has-many,join:id=comic_id"`
StoryArcs []StoryArc `bun:"rel:has-many,join:id=comic_id"`
TradePaperbacks []TradePaperback `bun:"rel:has-many,join:id=comic_id"`
Volumes []Volume `bun:"rel:has-many,join:id=comic_id"`
ComicWorks []ComicWork `bun:"rel:has-many,join:id=comic_id"`
}
type Artist struct {
bun.BaseModel `bun:"table:artist"`
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"`
Name string `bun:"name,unique:title,notnull"`
WebLink string `bun:"weblink"`
}
type Issue struct {
bun.BaseModel `bun:"table:issue"`
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"`
InStock bool `bun:"in_stock"`
IsRead bool `bun:"is_read"`
IssueNumber string `bu:"issue_number"`
Title string `bun:"title"`
PublishedOn time.Time `bun:"published_on"`
ComicID *string `bun:"comic_id"`
Comic *Comic `bun:"rel:belongs-to,join:comic_id=id"`
StoryArcID *string `bun:"story_arc_id"`
StoryArc *StoryArc `bun:"rel:belongs-to,join:story_arc_id=id"`
VolumeID *string `bun:"volume_id"`
Volume *Volume `bun:"rel:belongs-to,join:volume_id=id"`
}
type StoryArc struct {
bun.BaseModel `bun:"table:story_arc"`
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"`
Name string `bun:"name,unique:name,notnull"`
ComicID *string `bun:"comic_id"`
Comic *Comic `bun:"rel:belongs-to,join:comic_id=id"`
VolumeID *string `bun:"volume_id"`
Volume *Volume `bun:"rel:belongs-to,join:volume_id=id"`
}
type TradePaperback struct {
bun.BaseModel `bun:"table:trade_paperback"`
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"`
Name string `bun:"name,unique:name,notnull"`
IssueStart int `bun:"issue_start"`
IssueEnd int `bun:"issue_end"`
ComicID *string `bun:"comic_id"`
Comic *Comic `bun:"rel:belongs-to,join:comic_id=id"`
}
type Volume struct {
bun.BaseModel `bun:"table:volume"`
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"`
Name string `bun:"name,unique:name,notnull"`
ComicID *string `bun:"comic_id"`
Comic *Comic `bun:"rel:belongs-to,join:comic_id=id"`
}
type WorkType struct {
bun.BaseModel `bun:"table:worktype"`
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"`
Name string `bun:"name,unique:name,notnull"`
}
type ComicWork struct {
bun.BaseModel `bun:"table:comic_work"`
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"`
ArtistID *string `bun:"artist_id"`
Artist *Artist `bun:"rel:belongs-to,join:artist_id=id"`
ComicID *string `bun:"comic_id"`
Comic *Comic `bun:"rel:belongs-to,join:comic_id=id"`
WorkTypeID *string `bun:"work_type_id"`
WorkType *WorkType `bun:"rel:belongs-to,join:work_type_id=id"`
}
+101
View File
@@ -0,0 +1,101 @@
package schema
import (
"context"
"log"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"database/sql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/extra/bundebug"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
func GetTestDatabase() (*bun.DB, error) {
var err error
dsn := "postgres://kontor:kontor@localhost:5432/kontor?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
sqldb.SetMaxOpenConns(4)
sqldb.SetMaxIdleConns(4)
DB := bun.NewDB(sqldb, pgdialect.New())
DB.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
if err = DB.Ping(); err != nil {
return nil, err
}
log.Println("Returned Database Connection")
return DB, nil
}
func TestMain(m *testing.M) {
log.Println("Setup Test")
exitCode := m.Run()
os.Exit(exitCode)
}
func TestSelectComics(t *testing.T) {
var comics []Comic
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&comics).Relation("Publisher").Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 168, len(comics))
}
func TestSelectPublishers(t *testing.T) {
var publishers []Publisher
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&publishers).Relation("ParentPublisher").Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 19, len(publishers))
}
func TestSelectWorkTypes(t *testing.T) {
var comic_works []ComicWork
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
if err != nil {
log.Fatal(err)
}
err = db.NewSelect().Model(&comic_works).Relation("Comic").Relation("Artist").Relation("WorkType").Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 59, len(comic_works))
}
+50
View File
@@ -0,0 +1,50 @@
package schema
import (
"database/sql"
"log"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
var DB *bun.DB
func Connect() error {
var err error
dsn := "postgres://kontor:kontor@postgres:5432/kontor?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
sqldb.SetMaxOpenConns(4)
sqldb.SetMaxIdleConns(4)
DB := bun.NewDB(sqldb, pgdialect.New())
if err = DB.Ping(); err != nil {
return err
}
log.Println("Connection Opened to Database")
return nil
}
func GetDatabase() (*bun.DB, error) {
var err error
dsn := "postgres://kontor:kontor@postgres:5432/kontor?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
sqldb.SetMaxOpenConns(4)
sqldb.SetMaxIdleConns(4)
DB := bun.NewDB(sqldb, pgdialect.New())
if err = DB.Ping(); err != nil {
return nil, err
}
log.Println("Returned Database Connection")
return DB, nil
}
+79
View File
@@ -0,0 +1,79 @@
package schema
import (
"time"
"github.com/uptrace/bun"
)
type MediaFile struct {
bun.BaseModel `bun:"table:media_file"`
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"`
MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_file_id"`
}
type MediaActor struct {
bun.BaseModel `bun:"table:media_actor"`
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"`
Name string `bun:"name,unique:name"`
WebLink string `bun:"url,unique:url"`
MediaActorFiles []MediaActorFile `bun:"rel:has-many,join:id=media_actor_id"`
}
type MediaActorFile struct {
bun.BaseModel `bun:"table:media_actor_file"`
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"`
MediaActorID *string `bun:"media_actor_id"`
MediaActor *MediaActor `bun:"rel:belongs-to,join:media_actor_id=id"`
MediaFileID *string `bun:"media_file_id"`
MediaFile *MediaFile `bun:"rel:belongs-to,join:media_file_id=id"`
}
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"`
}
+8
View File
@@ -0,0 +1,8 @@
package utils
import "golang.org/x/crypto/bcrypt"
func ComparePassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
+38
View File
@@ -0,0 +1,38 @@
package utils
import (
"kontor-api-fiber/pkg/schema"
"time"
"github.com/golang-jwt/jwt/v5"
)
func GenerateToken(user schema.Profile) (string, error) {
// Create the Claims
claims := jwt.MapClaims{
"name": user.FirstName + ", " + user.LastName,
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return "", err
}
return t, nil
}
func VerifyToken(tokenString string) (bool, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil
})
if err != nil {
return false, err
}
return token.Valid, nil
}
+1 -1
View File
@@ -51,5 +51,5 @@ ENV PATH="/app/.venv/bin:$PATH"
EXPOSE $PORT EXPOSE $PORT
# Start the application with Uvicorn in production mode, using environment variable references # Start the application with Uvicorn in production mode, using environment variable references
CMD ["uvicorn", "src.main:kontor", "--log-level", "info", "--host", "0.0.0.0" , "--port", "8800"] CMD ["uvicorn", "src.main:kontor", "--log-level", "info", "--host", "0.0.0.0" , "--port", "8500"]
+16 -8
View File
@@ -1,17 +1,17 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from src.apis.base import api_router from src.apis.base import api_router
from src.apis.version1.healthcheck import health_router from src.apis.version1.healthcheck import health_router
from src.core.config import settings
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.base import Base
from src.db.session import engine from src.db.session import engine
from src.db.utils import check_db_connected, check_db_disconnected from src.db.utils import check_db_connected, check_db_disconnected
from src.webapps.base import api_router as web_app_router from src.webapps.base import api_router as web_app_router
from src.core.config import settings
from src.db.models.base import Base
@asynccontextmanager @asynccontextmanager
@@ -20,33 +20,41 @@ async def lifespan(app: FastAPI):
yield yield
await check_db_disconnected() await check_db_disconnected()
def include_router(app: FastAPI): def include_router(app: FastAPI):
app.include_router(api_router) app.include_router(api_router)
app.include_router(web_app_router) app.include_router(web_app_router)
app.include_router(health_router) app.include_router(health_router)
def configure_static(app: FastAPI): def configure_static(app: FastAPI):
app.mount("/static", StaticFiles(directory="src/static"), name="static") app.mount("/static", StaticFiles(directory="src/static"), name="static")
def add_middle_ware(app: FastAPI): def add_middle_ware(app: FastAPI):
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=['*'], allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=['*'], allow_methods=["*"],
allow_headers=['*'], allow_headers=["*"],
) )
def create_tables(): def create_tables():
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
def start_application(log): def start_application(log):
log.info(f"using database: {settings.DATABASE_URL}") log.info(f"using database: {settings.DATABASE_URL}")
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan) app = FastAPI(
title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan
)
include_router(app) include_router(app)
configure_static(app) configure_static(app)
add_middle_ware(app) add_middle_ware(app)
create_tables() create_tables()
return app return app
kontor = start_application(logger) kontor = start_application(logger)