2 Commits

Author SHA1 Message Date
tpeetz 7eae6e07f4 Merge branch 'develop/0.2.0' into 'main'
Vorbereitung Release 0.2.0

See merge request tpeetz/kontor!39
2026-01-29 23:50:41 +01:00
tpeetz b26b5ecc9c Vorbereitung Release 0.2.0 2026-01-29 23:50:41 +01:00
122 changed files with 4487 additions and 913 deletions
+1 -2
View File
@@ -1,4 +1,3 @@
services:
postgres:
image: postgres
@@ -36,4 +35,4 @@ secrets:
networks:
database:
name: database
external: true
+69 -25
View File
@@ -42,7 +42,75 @@ services:
image: kontor-api:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api:8800/health"]
test: ["CMD", "curl", "-f", "http://kontor-api:8500/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8500:8500
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
kontor-fiber:
build:
context: ./kontor-fiber
dockerfile: Dockerfile
tags:
- kontor-fiber:0.2.0-SNAPSHOT
image: kontor-fiber:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-fiber:8600/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8600:8600
depends_on:
postgres:
condition: service_healthy
kontor-echo:
build:
context: ./kontor-echo
dockerfile: Dockerfile
tags:
- kontor-echo:0.2.0-SNAPSHOT
image: kontor-echo:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-echo:8700/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8700:8700
depends_on:
postgres:
condition: service_healthy
kontor-quarkus:
build:
context: ./kontor-quarkus
dockerfile: Dockerfile
tags:
- kontor-quarkus:0.2.0-SNAPSHOT
image: kontor-quarkus:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-quarkus:8800/q/health"]
interval: 10s
timeout: 5s
retries: 3
@@ -52,30 +120,6 @@ services:
- frontend
ports:
- 8800:8800
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
kontor-api-go:
build:
context: ./kontor-api-go
dockerfile: Dockerfile
tags:
- kontor-api-go:0.2.0-SNAPSHOT
image: kontor-api-go:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api-go:8900/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8900:8900
depends_on:
postgres:
condition: service_healthy
-35
View File
@@ -1,35 +0,0 @@
package handler
import (
"context"
"kontor-api-go/pkg/schema"
"log"
"github.com/gofiber/fiber/v2"
"github.com/uptrace/bun"
)
func SetupComicRoutes(api fiber.Router) {
comics := api.Group("/comics")
comics.Get("/comics", GetAllComics)
}
func GetAllComics(c *fiber.Ctx) 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 fiber.NewError(fiber.StatusInternalServerError)
}
return c.JSON(comics)
}
-45
View File
@@ -1,45 +0,0 @@
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"`
}
+1 -1
View File
@@ -51,5 +51,5 @@ ENV PATH="/app/.venv/bin:$PATH"
EXPOSE $PORT
# 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"]
+2 -2
View File
@@ -1,7 +1,7 @@
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, HTTPException, status, Depends, Response
from fastapi import APIRouter, Body, HTTPException, status, Depends, Response
from fastapi.security import OAuth2PasswordRequestForm
from src.core.config import settings
@@ -31,7 +31,7 @@ def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depen
# @router.post("/token-cookie", response_model=Token)
def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
user = authenticate_user(form_data.username, form_data.password) # type: ignore
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
+41
View File
@@ -0,0 +1,41 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from src.core.config import settings
from src.core.log_conf import logger
from src.core.security import authenticate_user, create_access_token
from src.schema.admin import Token
login_router = APIRouter()
class LoginRequest(BaseModel):
email: str | None = None
password: str | None = None
@login_router.post(
"/login",
tags=["login"],
summary="Login and get token",
response_description="Return HTTP status code 200 (OK)",
status_code=status.HTTP_200_OK,
)
def login(request: LoginRequest) -> Token:
logger.info(f"login with {request.email}")
user = authenticate_user(request.email, request.password) # type: ignore
scopes = ["admin", "read"]
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "scope": " ".join(scopes)},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
+15 -4
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, status, HTTPException
from sqlalchemy import select
from src.core.log_conf import logger
from src.db.repository.media import create_new_mediaactor
from src.db.repository.media import create_new_mediaactor, delete_mediaactor
from src.db.session import SessionDep
from src.schema.media.actor import Actor, MediaActorResponse, get_actor_details
from src.db.models.media import MediaActor
@@ -10,7 +10,7 @@ router = APIRouter()
@router.get("/actors", response_model=list[MediaActorResponse])
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]:
def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]: # type: ignore
results: list[MediaActorResponse] = []
actors = db.scalars(select(MediaActor)).all()
for mediaactor in actors:
@@ -19,15 +19,26 @@ def get_all_actors(db: SessionDep, review: bool = False, download: bool = False)
return results
@router.get("/actors/{actor_id}", response_model=MediaActorResponse)
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse:
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse: # type: ignore
media_actor = db.get(MediaActor, actor_id)
if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
response = get_actor_details(media_actor)
return response
@router.delete("/actors/{actor_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_actor(actor_id: str, db: SessionDep): # type: ignore
media_actor = db.get(MediaActor, actor_id)
if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
logger.info(f"delete MediaActor: {actor_id}")
actor_files = media_actor.media_actor_files
logger.info(f"MediaActorFiles links {len(actor_files)}")
if len(actor_files) == 0:
delete_mediaactor(db, media_actor.id)
@router.post("/actors", status_code=status.HTTP_201_CREATED)
def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse:
def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse: # type: ignore
logger.info(f"add actor {new_actor.url}")
try:
mediaActor: MediaActor = create_new_mediaactor(new_actor, db)
@@ -8,7 +8,7 @@ from src.schema.media.actorfile import MediaActorFileResponse, get_actorfile_det
router = APIRouter()
@router.get("/actorfiles", response_model=list[MediaActorFileResponse])
def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]:
def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]: # type: ignore
results: list[MediaActorFileResponse] = []
actorfiles = db.scalars(select(MediaActorFile)).all()
for mediaactorfile in actorfiles:
@@ -17,7 +17,7 @@ def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]:
return results
@router.get("/actorfiles/{actorfile_id}", response_model=MediaActorFileResponse)
def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse:
def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse: # type: ignore
media_actorfile = db.get(MediaActorFile, actorfile_id)
if not media_actorfile:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
@@ -25,7 +25,7 @@ def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse:
return response
@router.delete("/actorfiles/{actorfile_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_actorfile(actorfile_id: str, db: SessionDep):
def delete_actorfile(actorfile_id: str, db: SessionDep): # type: ignore
media_actorfile = db.get(MediaActorFile, actorfile_id)
if not media_actorfile:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
+19 -8
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, status, HTTPException, Depends
from sqlalchemy import select, Sequence
from src.core.log_conf import logger
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile, delete_mediafile
from src.db.session import SessionDep
from src.schema.media.actor import MediaActorResponse
from src.schema.media.actorfile import MediaActorFileResponse
@@ -11,7 +11,7 @@ from src.db.models.media import MediaFile
router = APIRouter()
@router.get("/update-titles")
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
def update_titles(db: SessionDep) -> list[MediaFileResponse]: # type: ignore
results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review == True).all()
for mediafile in files:
@@ -25,7 +25,7 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]:
@router.get("/files", response_model=list[MediaFileResponse])
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaFileResponse]:
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaFileResponse]: # type: ignore
results: list[MediaFileResponse] = []
files: Sequence[MediaFile]
if review:
@@ -40,15 +40,26 @@ def get_all_files(db: SessionDep, review: bool = False, download: bool = False)
return results
@router.get("/files/{file_id}", response_model=MediaFileResponse)
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse:
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse: # type: ignore
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
response = get_file_details(mediafile)
return response
@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_file(file_id: str, db: SessionDep): # type: ignore
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
logger.info(f"delete MediaFile: {file_id}")
actor_files = mediafile.media_actor_files
logger.info(f"MediaActorFiles links {len(actor_files)}")
if len(actor_files) == 0:
delete_mediafile(db, mediafile.id)
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]:
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]: # type: ignore
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
@@ -61,7 +72,7 @@ def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]:
return results
@router.put("/files/{file_id}/actors", response_model=list[MediaActorFileResponse])
def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResponse]) -> list[MediaActorFileResponse]:
def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResponse]) -> list[MediaActorFileResponse]: # type: ignore
mediafile = db.get(MediaFile, file_id)
if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
@@ -85,7 +96,7 @@ def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResp
return results
@router.put("/files/{file_id}", response_model=MediaFileResponse)
def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse: # type: ignore
mediaFile = db.get(MediaFile, file_id)
if not mediaFile:
raise HTTPException(status_code=404, detail="MediaFile could not be found")
@@ -99,7 +110,7 @@ def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaF
return response
@router.post("/files", status_code=status.HTTP_201_CREATED)
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: # type: ignore
logger.info(f"add url {new_link.url}")
try:
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
+1 -1
View File
@@ -14,7 +14,7 @@ class Settings:
DB_USER: str = os.getenv("DB_USER", "kontor")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
DB_SERVER: str = os.getenv("DB_SERVER", "postgres")
DB_PORT: str = os.getenv("DB_PORT", 5432)
DB_PORT: int = int(os.getenv("DB_PORT", 5432))
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}"
SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=")
+10 -6
View File
@@ -8,15 +8,15 @@ LOGGING_CONFIG: dict[str, Any] = {
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(asctime)s - %(name)s - %(levelprefix)s %(message)s"
"fmt": "%(asctime)s - %(name)s - %(levelprefix)s %(message)s",
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
},
"access_file": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
"use_colors": False,
},
},
@@ -34,12 +34,16 @@ LOGGING_CONFIG: dict[str, Any] = {
},
"loggers": {
"root": {"handlers": ["default"], "level": "INFO", "propagate": False},
"kontor": {"handlers": ["default"], "level": "INFO", "propagate": True},
"kontor": {"handlers": ["default"], "level": "INFO", "propagate": False},
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
"uvicorn.error": {"level": "INFO"},
"uvicorn.access": {"handlers": ["default"], "level": "WARNING", "propagate": False},
"uvicorn.access": {
"handlers": ["default"],
"level": "WARNING",
"propagate": False,
},
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger('kontor')
logger = logging.getLogger("kontor")
+34 -35
View File
@@ -1,29 +1,21 @@
import logging
from datetime import datetime, timezone
from datetime import timedelta
from typing import Optional, Annotated, List
from typing import Dict
from typing import Optional
from fastapi import HTTPException, Security
from fastapi import Request
from fastapi import status
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security.utils import get_authorization_scheme_param
from datetime import datetime, timedelta, timezone
from typing import Annotated, Dict, List, Optional
import bcrypt
from fastapi import Depends
from fastapi.security import SecurityScopes, OAuth2PasswordBearer, OAuth2
from fastapi import Depends, HTTPException, Request, Security, status
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security import OAuth2, OAuth2PasswordBearer, SecurityScopes
from fastapi.security.utils import get_authorization_scheme_param
from jose import JWTError, jwt
from pydantic import ValidationError
from src.core.config import settings
from jose import jwt, JWTError
from src.core.log_conf import logger
from src.db.models.admin import Profile
from src.db.repository.admin import get_profile
from src.db.session import SessionLocal
from src.schema.admin import TokenData, ProfileModel
from src.schema.admin import ProfileModel, TokenData
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/api/login/token",
@@ -33,19 +25,19 @@ oauth2_scheme = OAuth2PasswordBearer(
class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__(
self,
tokenUrl: str,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
self,
tokenUrl: str,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) # type: ignore
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie
authorization: str = request.cookies.get("access_token") # type: ignore # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
@@ -63,7 +55,7 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
def authenticate_user(username: str, password: str) -> Optional[Profile]:
with SessionLocal() as db:
user = get_profile(username=username, db=db)
logger.info(user)
logger.debug(user)
if not user:
return None
if bcrypt.checkpw(password.encode(), user.password.encode()):
@@ -88,7 +80,9 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
return encoded_jwt
async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)]):
async def get_current_user(
security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)]
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
@@ -99,8 +93,10 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub")
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username)
if username is None:
raise credentials_exception
@@ -110,7 +106,7 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str
except (JWTError, ValidationError):
raise credentials_exception
with SessionLocal() as db:
user = get_profile(username=token_data.username, db=db)
user = get_profile(username=token_data.username, db=db) # type: ignore
if user is None:
raise credentials_exception
for scope in security_scopes.scopes:
@@ -124,17 +120,20 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str
async def get_current_active_user(
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
) -> ProfileModel:
if not current_user.enabled:
if not current_user.enabled: # type: ignore
raise HTTPException(status_code=400, detail="Inactive user")
user_model = ProfileModel(username=current_user.user_name, email=current_user.email,
first_name=current_user.first_name, last_name=current_user.last_name,
active=current_user.enabled)
user_model = ProfileModel(
username=current_user.user_name,
email=current_user.email, # type: ignore
first_name=current_user.first_name,
last_name=current_user.last_name, # type: ignore
active=current_user.enabled,
) # type: ignore
return user_model
def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -144,7 +143,7 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub")
username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username)
if username is None:
raise credentials_exception
+12
View File
@@ -38,6 +38,12 @@ def create_new_mediafile(link: str, db: Session) -> MediaFile:
logger.info(f"created {media_file}")
return media_file
def delete_mediafile(db: Session, media_file_id: str):
logger.info(f"delete MediaFile with id {media_file_id}")
media_file = db.get(MediaFile, media_file_id)
db.delete(media_file)
db.commit()
def create_new_mediaactor(new_actor: Actor, db: Session) -> MediaActor:
logger.info(f"create MediaActor with url {new_actor.url}")
media_actor: MediaActor = MediaActor()
@@ -53,6 +59,12 @@ def create_new_mediaactor(new_actor: Actor, db: Session) -> MediaActor:
logger.info(f"created {media_actor}")
return media_actor
def delete_mediaactor(db: Session, actor_id: str):
logger.info(f"delete MediaActor with id {actor_id}")
media_actor = db.get(MediaActor, actor_id)
db.delete(media_actor)
db.commit()
def create_new_mediaactorfile(db: Session, actor_id: str, file_id: str) -> MediaActorFile:
logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}")
media_actor_file: MediaActorFile = MediaActorFile()
+18 -8
View File
@@ -1,17 +1,18 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from src.apis.base import api_router
from src.apis.version1.healthcheck import health_router
from src.apis.version1.login import login_router
from src.core.config import settings
from src.core.log_conf import logger
from src.db.models.base import Base
from src.db.session import engine
from src.db.utils import check_db_connected, check_db_disconnected
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
@@ -20,33 +21,42 @@ async def lifespan(app: FastAPI):
yield
await check_db_disconnected()
def include_router(app: FastAPI):
app.include_router(api_router)
app.include_router(web_app_router)
app.include_router(health_router)
app.include_router(login_router)
def configure_static(app: FastAPI):
app.mount("/static", StaticFiles(directory="src/static"), name="static")
def add_middle_ware(app: FastAPI):
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
allow_methods=["*"],
allow_headers=["*"],
)
def create_tables():
Base.metadata.create_all(bind=engine)
def start_application(log):
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)
configure_static(app)
add_middle_ware(app)
create_tables()
return app
kontor = start_application(logger)
+54
View File
@@ -0,0 +1,54 @@
package main
import (
"kontor-api-echo/pkg/handler"
"kontor-api-echo/pkg/schema"
"log"
"os"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
log.SetOutput(os.Stdout)
log.Println("Kontor started")
if _, err := schema.GetDatabase(); 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,
LogError: true,
HandleError: true,
Skipper: skipper,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
if v.Error == nil {
log.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
} else {
log.Printf("REQUEST-ERROR: uri: %v, status: %v, err: %v\n", v.URI, v.Status, v.Error.Error())
}
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=
+78
View File
@@ -0,0 +1,78 @@
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
}
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func Login(c echo.Context) error {
// user := c.FormValue("email")
// pass := c.FormValue("password")
loginRequest := new(LoginRequest)
if err := c.Bind(loginRequest); err != nil {
return err
}
email := loginRequest.Email
password := loginRequest.Password
var profile schema.Profile
var err error
var db *bun.DB
ctx := context.Background()
db, _ = schema.GetDatabase()
err = db.NewSelect().Model(&profile).Where("email = ?", email).Scan(ctx)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
if !utils.ComparePassword(profile.Password, password) {
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{"access_token": t, "token_type": "bearer"})
}
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+"!")
}
+81
View File
@@ -0,0 +1,81 @@
package handler
import (
"context"
"fmt"
"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()
fmt.Printf("REQUEST: uri: %v, status: %v\n", c.Request().URL, c.Response().Status)
log.Printf("REQUEST: uri: %v, status: %v\n", c.Request().URL, c.Response().Status)
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")
}
+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"`
}
+100
View File
@@ -0,0 +1,100 @@
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/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))
}
+31
View File
@@ -0,0 +1,31 @@
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 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
}
+116
View File
@@ -0,0 +1,116 @@
package schema
import (
"time"
"github.com/uptrace/bun"
)
type Sport struct {
bun.BaseModel `bun:"table:sport"`
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"`
Teams []Team `bun:"rel:has-many,join:id=sport_id"`
Positions []FieldPosition `bun:"rel:has-many,join:id=sport_id"`
}
type Team struct {
bun.BaseModel `bun:"table:team"`
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"`
Shortname string `bun:"short_name"`
Roosters []Rooster `bun:"rel:has-many,join:id=team_id"`
SportID *string `bun:"sport_id"`
Sport *Sport `bun:"rel:belongs-to,join:sport_id=id"`
}
type FieldPosition struct {
bun.BaseModel `bun:"table:field_position"`
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"`
Shortname string `bun:"short_name"`
SportID *string `bun:"sport_id"`
Sport *Sport `bun:"rel:belongs-to,join:sport_id=id"`
Roosters []Rooster `bun:"rel:has-many,join:id=position_id"`
}
type Player struct {
bun.BaseModel `bun:"table:player"`
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"`
Roosters []Rooster `bun:"rel:has-many,join:id=player_id"`
}
type Rooster struct {
bun.BaseModel `bun:"table:rooster"`
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"`
Year int `bun:"year"`
TeamID *string `bun:"team_id"`
Team *Team `bun:"rel:belongs-to,join:team_id=id"`
PlayerID *string `bun:"player_id"`
Player *Player `bun:"rel:belongs-to,join:player_id=id"`
PositionID *string `bun:"position_id"`
Position *FieldPosition `bun:"rel:belongs-to,join:position_id=id"`
}
type CardSet struct {
bun.BaseModel `bun:"table:card_set"`
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"`
ParallelSet bool `bun:"parallel_set"`
InsertSet bool `bun:"insert_set"`
VendorID *string `bun:"vendor_id"`
Vendor *Vendor `bun:"rel:belongs-to,join:vendor_id=id"`
}
type Vendor struct {
bun.BaseModel `bun:"table:vendor"`
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"`
CardSets []CardSet `bun:"rel:has-many,join:id=vendor_id"`
}
type Card struct {
bun.BaseModel `bun:"table:card"`
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"`
CardNumber int `bun:"card_number"`
Year int `bun:"year"`
VendorID *string `bun:"vendor_id"`
Vendor *Vendor `bun:"rel:belongs-to,join:vendor_id=id"`
CardSetID *string `bun:"card_set_id"`
CardSet *CardSet `bun:"rel:belongs-to,join:card_set_id=id"`
RoosterID *string `bun:"rooster_id"`
Rooster *Rooster `bun:"rel:belongs-to,join:rooster_id=id"`
}
+155
View File
@@ -0,0 +1,155 @@
package schema
import (
"context"
"log"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
)
func TestSelectSports(t *testing.T) {
var sports []Sport
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&sports).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 4, len(sports))
}
func TestSelectTeams(t *testing.T) {
var teams []Team
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&teams).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 122, len(teams))
}
func TestSelectFieldPositions(t *testing.T) {
var positions []FieldPosition
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&positions).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 44, len(positions))
}
func TestSelectPlayers(t *testing.T) {
var players []Player
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&players).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 38, len(players))
}
func TestSelectRoosters(t *testing.T) {
var roosters []Rooster
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&roosters).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 11, len(roosters))
}
func TestSelectVendors(t *testing.T) {
var vendors []Vendor
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&vendors).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 9, len(vendors))
}
func TestSelectCardSets(t *testing.T) {
var cardSets []CardSet
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&cardSets).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 15, len(cardSets))
}
func TestSelectCards(t *testing.T) {
var cards []Card
var err error
var db *bun.DB
ctx := context.Background()
db, err = GetTestDatabase()
require.NoError(t, err)
err = db.NewSelect().Model(&cards).Scan(ctx)
if err != nil {
log.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, 10, len(cards))
}
@@ -1,7 +1,7 @@
package utils
import (
"kontor-api-go/pkg/schema"
"kontor-api-echo/pkg/schema"
"time"
"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
import (
"kontor-api-go/pkg/handler"
"kontor-api-go/pkg/schema"
"kontor-api-fiber/pkg/handler"
"kontor-api-fiber/pkg/schema"
"log"
jwtware "github.com/gofiber/contrib/jwt"
@@ -23,17 +23,19 @@ func main() {
// SigningKey: jwtware.SigningKey{Key: []byte("secret")},
// }))
// app.Use(logger.New())
//app.Use(logger.New())
app.Get("/health", handler.GetHealth)
app.Post("/login", handler.Login)
api := app.Group("/api", logger.New(), jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
}))
api.Use(logger.New())
handler.SetupComicRoutes(api)
handler.SetupMediaRoutes(api)
// Listen on port 8900
app.Listen(":8900")
app.Listen(":8600")
log.Println("Kontor finished")
}
+8 -2
View File
@@ -1,4 +1,4 @@
module kontor-api-go
module kontor-api-fiber
go 1.24.2
@@ -7,6 +7,8 @@ require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gofiber/contrib/jwt v1.1.2 // indirect
github.com/gofiber/fiber/v2 v2.52.10 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
@@ -17,12 +19,15 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/rivo/uniseg v0.4.7 // 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/fasthttp v1.68.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
@@ -31,6 +36,7 @@ require (
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/sys v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
mellium.im/sasl v0.3.2 // indirect
)
@@ -7,6 +7,10 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
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/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs=
github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk=
github.com/gofiber/fiber v1.14.6 h1:QRUPvPmr8ijQuGo1MgupHBn8E+wW0IKqiOvIZPtV70o=
@@ -38,10 +42,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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=
@@ -50,6 +58,8 @@ github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI
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/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
@@ -78,6 +88,11 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
@@ -2,8 +2,8 @@ package handler
import (
"context"
"kontor-api-go/pkg/schema"
"kontor-api-go/pkg/utils"
"kontor-api-fiber/pkg/schema"
"kontor-api-fiber/pkg/utils"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
+77
View File
@@ -0,0 +1,77 @@
package handler
import (
"context"
"kontor-api-fiber/pkg/schema"
"log"
"github.com/gofiber/fiber/v2"
"github.com/uptrace/bun"
)
func SetupComicRoutes(api fiber.Router) {
comics := api.Group("/comics")
comics.Get("/comics", GetAllComics)
comics.Get("/publishers", GetAllPublishers)
comics.Get("/comicworks", GetAllComicWorks)
}
func GetAllComics(c *fiber.Ctx) 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 fiber.NewError(fiber.StatusInternalServerError)
}
return c.JSON(comics)
}
func GetAllPublishers(c *fiber.Ctx) 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 fiber.NewError(fiber.StatusInternalServerError)
}
return c.JSON(publishers)
}
func GetAllComicWorks(c *fiber.Ctx) 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 fiber.NewError(fiber.StatusInternalServerError)
}
return c.JSON(comic_works)
}
@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"kontor-api-go/pkg/schema"
"kontor-api-fiber/pkg/schema"
"log"
"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))
}
+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
}
+17 -87
View File
@@ -1,10 +1,5 @@
plugins {
id 'java'
//id 'application'
id 'jacoco'
id 'test-report-aggregation'
id 'jacoco-report-aggregation'
alias(libs.plugins.lombok)
}
repositories {
@@ -12,89 +7,27 @@ repositories {
}
dependencies {
implementation libs.javalin.bundle
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation libs.postgresql
implementation libs.hibernate
annotationProcessor libs.hibernate.modelgen
testImplementation( platform( libs.junit.bom.get().toString() ))
testImplementation "org.junit.jupiter:junit-jupiter"
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation libs.javalin // Pulling in Javalin
implementation libs.jackson // For JSON serialization of persons
implementation libs.slf4j // To see some Javalin logging
compileOnly libs.lombok
annotationProcessor libs.lombok
implementation libs.postgresql
implementation libs.hibernate
implementation libs.hibernate.validator
implementation libs.hypersistence
implementation libs.validation.api
annotationProcessor libs.hibernate.jpamodelgen
implementation libs.el
annotationProcessor libs.openapi.annotation
implementation libs.javalin.openapi
implementation libs.javalin.swagger
implementation libs.javalin.redoc
testImplementation(platform(libs.junit.bom))
testImplementation "org.junit.jupiter:junit-jupiter"
}
testing {
suites {
configureEach {
useJUnitJupiter()
dependencies {
implementation project()
implementation libs.hsqldb
implementation libs.sqlite.jdbc
runtimeOnly 'org.junit.platform:junit-platform-launcher'
}
}
test(JvmTestSuite) {
targets {
all {
testTask.configure {
reports {
junitXml {
outputPerTestCase = true // defaults to false
mergeReruns = true // defaults to false
}
}
finalizedBy(jacocoTestReport)
}
}
}
}
integrationTest(JvmTestSuite) {
targets {
all {
testTask.configure {
shouldRunAfter(test)
finalizedBy(jacocoTestReport)
}
}
}
}
}
}
tasks.named('check') {
dependsOn(testing.suites.integrationTest)
dependsOn(testing.suites.test)
dependsOn tasks.named('testAggregateTestReport', TestReport)
dependsOn tasks.named('integrationTestAggregateTestReport', TestReport)
}
jacocoTestReport {
dependsOn test, integrationTest
reports {
xml.required = true
csv.required = false
}
}
reporting {
reports {
testAggregateTestReport(AggregateTestReport) {
}
integrationTestAggregateTestReport(AggregateTestReport) {
}
integrationTestCodeCoverageReport(JacocoCoverageReport) {
}
}
}
// application {
// mainClass = 'de.thpeetz.kontor.api.Main'
// }
task fatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'de.thpeetz.kontor.Main'
@@ -105,9 +38,6 @@ task fatJar(type: Jar) {
}
build.dependsOn fatJar
// distZip.dependsOn fatJar
// distTar.dependsOn fatJar
// startScripts.dependsOn fatJar
wrapper {
gradleVersion = "9.2.1"
+1 -3
View File
@@ -1,5 +1,3 @@
description='Kontor with Spring Boot'
description='Kontor with Javalin'
version=0.2.0-SNAPSHOT
group=de.thpeetz
org.gradle.warning.mode=none
+30 -22
View File
@@ -1,32 +1,40 @@
[versions]
gradle = "8.6"
hibernate = "6.4.4.Final"
hsqldb = "2.7.1"
jackson = "2.16.1"
javalin = "6.7.0"
junit = "5.8.2"
lombok = "8.6"
postgresql = "42.6.2"
#junit = "5.8.2"
junit = "5.10.3"
#slf4j = "1.7.36"
slf4j = "2.0.16"
sonarqube = "3.3"
spotbugs = "6.0.7"
hsqldb = "2.7.1"
sqlite = "3.25.2"
jackson = "2.16.1"
#javalin = "4.6.4"
javalin = "6.7.0"
lombok = "1.18.34"
postgresql = "42.7.3"
hibernate = "7.0.5.Final"
validation = "2.0.1.Final"
hypersistence = "3.14.1"
el = "4.0.1"
openapi = "6.7.0-3"
[libraries]
hibernate = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate" }
hibernate-modelgen = { module = "org.hibernate.orm:hibernate-jpamodelgen", version.ref = "hibernate" }
hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" }
jackson = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
javalin = { module = "io.javalin:javalin", version.ref = "javalin" }
javalin-bundle = { module = "io.javalin:javalin-bundle", version.ref = "javalin" }
junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
slf4j = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" }
jackson = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
javalin = { module = "io.javalin:javalin", version.ref = "javalin" }
javalin-bundle = { module = "io.javalin:javalin-bundle", version.ref = "javalin" }
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
slf4j = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
[plugins]
spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
lombok = { id = "io.freefair.lombok", version.ref = "lombok" }
hibernate = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate" }
hibernate-jpamodelgen = { module = "org.hibernate.orm:hibernate-jpamodelgen", version.ref = "hibernate" }
hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernate" }
validation-api = { module = "javax.validation:validation-api", version.ref = "validation" }
el = { module = "org.glassfish:jakarta.el", version.ref = "el" }
hypersistence = { module = "io.hypersistence:hypersistence-utils-hibernate-70", version.ref = "hypersistence" }
javalin-openapi = { module = "io.javalin.community.openapi:javalin-openapi-plugin", version.ref = "openapi" }
javalin-swagger = { module = "io.javalin.community.openapi:javalin-swagger-plugin", version.ref = "openapi" }
javalin-redoc = { module = "io.javalin.community.openapi:javalin-redoc-plugin", version.ref = "openapi" }
openapi-annotation = { module = "io.javalin.community.openapi:openapi-annotation-processor", version.ref = "openapi" }
@@ -1,26 +0,0 @@
package de.thpeetz.kontor;
import io.javalin.Javalin;
import static io.javalin.apibuilder.ApiBuilder.get;
import static io.javalin.apibuilder.ApiBuilder.path;
import java.util.HashMap;
import de.thpeetz.kontor.web.PersonHandler;
public class JavalinApp {
public static Javalin create() {
return Javalin.create((var config) -> config.router.apiBuilder(() -> {
path("/health", () -> get(ctx -> {
HashMap<String, String> status = new HashMap<>();
status.put("status", "ok");
ctx.json(status);
}));
path("/persons", () -> {
get(PersonHandler.listAll);
});
}));
}
}
@@ -1,15 +1,89 @@
package de.thpeetz.kontor;
import java.util.HashMap;
import java.util.LinkedList;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
import de.thpeetz.kontor.infrastructure.AppHibernateSessionFactory;
import de.thpeetz.kontor.models.comics.Comic;
import de.thpeetz.kontor.models.media.MediaFile;
import io.javalin.Javalin;
import io.javalin.config.Key;
//import jakarta.persistence.EntityManager;
public class Main {
private static Logger logger = LoggerFactory.getLogger(Main.class);
private static short port = 8400;
// private static Key<EntityManagerFactory> emf = new
// Key<EntityManagerFactory>("entityManagerFactory");
private static Key<SessionFactory> sf = new Key<>("SessionFactory");
public static void main(String[] args) {
JavalinApp.create().start(port);
logger.info("API's alive for real :-)))");
Javalin app = Javalin.create(config -> {
config.requestLogger.http((ctx, ms) -> {
var path = ctx.path();
if (!path.equals("/health")) {
logger.info(ctx.path());
}
});
config.appData(sf, AppHibernateSessionFactory.getSessionFactory());
});
app.get("/", ctx -> ctx.json("Ok"));
app.get("/health", ctx -> {
HashMap<String, String> status = new HashMap<>();
status.put("status", "ok");
ctx.json(status);
});
app.get("/api/v1/comics", ctx -> {
var result = new LinkedList<Comic>();
ctx.appData(sf).inStatelessTransaction(session -> {
session.createSelectionQuery("from Comic", Comic.class).getResultList().forEach(comic -> {
result.add(comic);
});
});
// var em = ctx.appData(emf).createEntityManager();
// logger.info(em.toString());
// EntityManager entityManager = ctx.attribute("entityManager");
// entityManager.createQuery("select * from Comic",
// Comic.class).getResultList().forEach(comic -> {
// result.add(comic);
// });
ctx.json(result);
});
app.get("/api/v1/media/files", ctx -> {
var result = new LinkedList<>();
// ctx.appData(sf).inStatelessTransaction(session -> {
// var files = session.createSelectionQuery("from MediaFile",
// MediaFile.class).getResultList();
// result.addAll(files);
// });
var list = ctx.appData(sf).openStatelessSession().createSelectionQuery("from MediaFile", MediaFile.class)
.getResultList();
result.addAll(list);
ctx.json(result);
});
// app.before(ctx -> {
// ctx.attribute("configuration", AppHibernateConfig.configuration());
// ctx.attribute("sessionFactory",
// AppHibernateSessionFactory.getSessionFactory());
// });
app.after(ctx -> {
// SessionFactory sessionFactory = ctx.attribute("sessionFactory");
// if (sessionFactory != null) {
// logger.info("close SessionFactory");
// sessionFactory.close();
// }
// EntityManager entityManager = ctx.attribute("entityManager");
// if (entityManager != null) {
// logger.info("close EntityManager");
// entityManager.close();
// }
});
app.start(port);
logger.info("API's alive for real :-)");
}
}
@@ -0,0 +1,17 @@
package de.thpeetz.kontor.infrastructure;
import java.util.function.Consumer;
import java.util.function.Function;
import org.hibernate.StatelessSession;
public class AppHibernate {
public static void inTransaction(Consumer<StatelessSession> consumer) {
AppHibernateSessionFactory.getSessionFactory().inStatelessTransaction(consumer);
}
public static <R> R fromTransaction(Function<StatelessSession, R> function) {
return AppHibernateSessionFactory.getSessionFactory().fromStatelessTransaction(function);
}
}
@@ -0,0 +1,54 @@
package de.thpeetz.kontor.infrastructure;
import java.util.Properties;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.tool.schema.Action;
import de.thpeetz.kontor.models.comics.Artist;
import de.thpeetz.kontor.models.comics.Comic;
import de.thpeetz.kontor.models.comics.ComicWork;
import de.thpeetz.kontor.models.comics.Issue;
import de.thpeetz.kontor.models.comics.IssueWork;
import de.thpeetz.kontor.models.comics.Publisher;
import de.thpeetz.kontor.models.comics.StoryArc;
import de.thpeetz.kontor.models.comics.TradePaperback;
import de.thpeetz.kontor.models.comics.Volume;
import de.thpeetz.kontor.models.comics.Worktype;
import de.thpeetz.kontor.models.media.MediaActor;
import de.thpeetz.kontor.models.media.MediaActorFile;
import de.thpeetz.kontor.models.media.MediaArticle;
import de.thpeetz.kontor.models.media.MediaFile;
import de.thpeetz.kontor.models.media.MediaVideo;
public class AppHibernateConfig {
public static Configuration configuration() {
var configuration = new Configuration();
var settings = new Properties();
settings.put(AvailableSettings.JAKARTA_JDBC_DRIVER, "org.postgresql.Driver");
settings.put(AvailableSettings.JAKARTA_JDBC_URL, "jdbc:postgresql://postgres:5432/kontor");
settings.put(AvailableSettings.JAKARTA_JDBC_USER, "kontor");
settings.put(AvailableSettings.JAKARTA_JDBC_PASSWORD, "kontor");
settings.put(AvailableSettings.HIGHLIGHT_SQL, true);
settings.put(AvailableSettings.HBM2DDL_AUTO, Action.ACTION_UPDATE);
configuration.setProperties(settings);
configuration.addAnnotatedClass(MediaFile.class);
configuration.addAnnotatedClass(MediaActorFile.class);
configuration.addAnnotatedClass(MediaActor.class);
configuration.addAnnotatedClass(MediaArticle.class);
configuration.addAnnotatedClass(MediaVideo.class);
configuration.addAnnotatedClass(Comic.class);
configuration.addAnnotatedClass(Publisher.class);
configuration.addAnnotatedClass(Artist.class);
configuration.addAnnotatedClass(ComicWork.class);
configuration.addAnnotatedClass(Issue.class);
configuration.addAnnotatedClass(IssueWork.class);
configuration.addAnnotatedClass(StoryArc.class);
configuration.addAnnotatedClass(TradePaperback.class);
configuration.addAnnotatedClass(Volume.class);
configuration.addAnnotatedClass(Worktype.class);
return configuration;
}
}
@@ -0,0 +1,33 @@
package de.thpeetz.kontor.infrastructure;
import java.util.Objects;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AppHibernateSessionFactory {
private static final Logger logger = LoggerFactory.getLogger(AppHibernateSessionFactory.class);
private static SessionFactory sessionFactory;
public static SessionFactory getSessionFactory() {
if (Objects.isNull(sessionFactory)) {
var configuration = AppHibernateConfig.configuration();
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySettings(configuration.getProperties())
.build();
try {
sessionFactory = configuration.buildSessionFactory(registry);
logger.info("SessionFactory created");
} catch (Throwable ex) {
logger.error("Failed to create session factory", ex);
StandardServiceRegistryBuilder.destroy(registry);
}
}
return sessionFactory;
}
}
@@ -1,27 +0,0 @@
package de.thpeetz.kontor.models;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@@ -0,0 +1,52 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table
@Getter
@Setter
public class Artist {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(unique = true, nullable = false)
private String name;
@Column(nullable = true)
private String weblink;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<ComicWork> comicWorks = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<IssueWork> issueWorks = new LinkedList<>();
}
@@ -0,0 +1,93 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table
@Getter
@Setter
public class Comic {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(unique = true, nullable = false)
private String title;
@ManyToOne
@JoinColumn(name = "publisher_id", nullable = false)
@JsonIgnoreProperties({ "comics" })
private Publisher publisher;
@Column(name = "current_order")
private Boolean currentOrder = false;
private Boolean completed = false;
@Column(nullable = true)
private String weblink;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<ComicWork> comicWorks;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true)
private List<Issue> issues = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true)
private List<StoryArc> storyArcs = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true)
private List<TradePaperback> tradePaperbacks = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true)
private List<Volume> volumes = new LinkedList<>();
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Comic{");
sb.append("title='").append(title).append('\'');
sb.append(", publisher=").append(publisher);
sb.append(", currentOrder=").append(currentOrder);
sb.append(", completed=").append(completed);
sb.append('}');
return sb.toString();
}
public String getPublisherName() {
return this.publisher.getName();
}
}
@@ -0,0 +1,12 @@
package de.thpeetz.kontor.models.comics;
import java.util.List;
import org.hibernate.StatelessSession;
import org.hibernate.annotations.processing.Find;
public interface ComicQueries {
@Find
List<Comic> getAllComics(StatelessSession session);
}
@@ -0,0 +1,53 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
@Entity
@Table(name = "comic_work")
public class ComicWork {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@ManyToOne
@JoinColumn(name = "comic_id", nullable = false)
private Comic comic;
@ManyToOne
@JoinColumn(name = "artist_id", nullable = false)
private Artist artist;
@ManyToOne
@JoinColumn(name = "work_type_id", nullable = false)
private Worktype workType;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ComicWork{");
sb.append("comic=").append(comic);
sb.append(", artist=").append(artist);
sb.append(", workType=").append(workType);
sb.append('}');
return sb.toString();
}
}
@@ -0,0 +1,126 @@
package de.thpeetz.kontor.models.comics;
import java.time.YearMonth;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.hypersistence.utils.hibernate.type.basic.YearMonthDateType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Type;
@Entity
@Table
@Getter
@Setter
public class Issue {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@ManyToOne
@JoinColumn(name = "comic_id", nullable = false)
@JsonIgnoreProperties({ "issues" })
private Comic comic;
@ManyToOne
@JoinColumn(name = "volume_id", nullable = true)
@JsonIgnoreProperties({ "issues" })
private Volume volume;
@ManyToOne
@JoinColumn(name = "story_arc_id", nullable = true)
@JsonIgnoreProperties({ "issues" })
private StoryArc storyArc;
@Column(name = "issue_number", nullable = false)
private String issueNumber;
@Column(nullable = true)
private String title;
@Type(YearMonthDateType.class)
@Column(name = "published_on", columnDefinition = "date", nullable = true)
private YearMonth publishedOn;
@Column(name = "is_read")
private Boolean isRead;
@Column(name = "in_stock")
private Boolean inStock;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "issue", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<IssueWork> issueWorks;
public String getComicTitle() {
return comic.getTitle();
}
public String getVolumeName() {
if (volume != null) {
return volume.getName();
}
return null;
}
public String getFullTitle() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(this.getComic().getTitle());
stringBuilder.append(" #");
stringBuilder.append(this.getIssueNumber());
if (this.title != null && !this.title.isEmpty()) {
stringBuilder.append(": ");
stringBuilder.append(this.title);
}
return stringBuilder.toString();
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append("Issue{");
result.append("comic=");
result.append(comic);
result.append(", volume=");
result.append(volume);
result.append(", storyArc=");
result.append(storyArc);
result.append(", issueNumber='");
result.append(issueNumber);
result.append(", title='");
result.append(title);
result.append(", publishedOn=");
result.append(publishedOn);
result.append(", isRead=");
result.append(isRead);
result.append(", inStock=");
result.append(inStock);
result.append("}");
return result.toString();
}
}
@@ -0,0 +1,57 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "issue_work")
@Getter
@Setter
public class IssueWork {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@ManyToOne
@JoinColumn(name = "issue_id", nullable = false)
private Issue issue;
@ManyToOne
@JoinColumn(name = "artist_id", nullable = false)
private Artist artist;
@ManyToOne
@JoinColumn(name = "work_type_id", nullable = false)
private Worktype workType;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("IssueWork{");
sb.append("issue=").append(issue);
sb.append(", artist=").append(artist);
sb.append(", workType=").append(workType);
sb.append('}');
return sb.toString();
}
}
@@ -0,0 +1,77 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table
@Getter
@Setter
public class Publisher {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(unique = true, nullable = false)
private String name;
private String weblink;
@JsonBackReference
@ManyToOne
@JoinColumn(name = "parent_publisher_id", nullable = true)
@JsonIgnoreProperties({ "comics" })
private Publisher parentCompany;
@Column(nullable = true)
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parentCompany", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Publisher> imprints = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comic> comics = new LinkedList<>();
public String getParentCompanyName() {
if (parentCompany != null) {
return parentCompany.name;
}
return null;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Publisher{");
sb.append("name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
@@ -0,0 +1,59 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "story_arc")
@Getter
@Setter
public class StoryArc {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(nullable = false)
private String name;
@ManyToOne
@JoinColumn(name = "comic_id", nullable = false)
@JsonIgnoreProperties({ "storyArcs" })
private Comic comic;
@ManyToOne
@JoinColumn(name = "volume_id", nullable = true)
@JsonIgnoreProperties({ "storyArcs" })
private Volume volume;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "storyArc", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Issue> issues = new LinkedList<>();
}
@@ -0,0 +1,48 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "trade_paperback")
@Getter
@Setter
public class TradePaperback {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(nullable = false)
private String name;
@ManyToOne
@JoinColumn(name = "comic_id", nullable = false)
private Comic comic;
@Column
private Integer issueStart;
@Column
private Integer issueEnd;
}
@@ -0,0 +1,69 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table
@Getter
@Setter
public class Volume {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(nullable = false)
private String name;
@ManyToOne
@JoinColumn(name = "comic_id", nullable = false)
@JsonIgnoreProperties({ "volumes" })
private Comic comic;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "volume", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<StoryArc> storyArcs = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "volume", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Issue> issues = new LinkedList<>();
@Override
public String toString() {
StringBuilder result = new StringBuilder("Volume{");
result.append("name=");
result.append(name);
result.append("comic=Comic{title=");
result.append(comic.getTitle());
result.append("}}");
return result.toString();
}
}
@@ -0,0 +1,53 @@
package de.thpeetz.kontor.models.comics;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
@Entity
@Table
public class Worktype {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(unique = true, nullable = false)
private String name;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "workType", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<ComicWork> comicWorks = new LinkedList<>();
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "workType", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<IssueWork> issueWorks = new LinkedList<>();
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Worktype{");
sb.append("name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
@@ -0,0 +1,51 @@
package de.thpeetz.kontor.models.media;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotEmpty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "media_actor")
public class MediaActor {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@NotEmpty
@Column(unique = true)
private String name;
@Column(nullable = true)
private String url;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "media_actor", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<MediaActorFile> mediaActorFiles = new LinkedList<>();
}
@@ -0,0 +1,56 @@
package de.thpeetz.kontor.models.media;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "media_actor_file", indexes = {
@Index(columnList = "media_file_id, media_actor_id") }, uniqueConstraints = @UniqueConstraint(columnNames = {
"media_file_id", "media_actor_id" }))
public class MediaActorFile {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@ManyToOne
@JoinColumn(name = "media_file_id")
@NotNull
private MediaFile media_file;
@ManyToOne
@JoinColumn(name = "media_actor_id")
@NotNull
private MediaActor media_actor;
public String getTitle() {
return media_file.getTitle();
}
}
@@ -0,0 +1,46 @@
package de.thpeetz.kontor.models.media;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "media_article", indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = {
"url" }))
public class MediaArticle {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date", nullable = true)
private Date lastModifiedDate;
@NotEmpty
private String url;
@Column
private boolean review;
@Column(nullable = true)
private String title;
}
@@ -0,0 +1,71 @@
package de.thpeetz.kontor.models.media;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "media_file")
public class MediaFile {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(nullable = true)
private String url;
@Column
private boolean review;
@Column(name = "should_download")
private boolean shouldDownload;
@Column(nullable = true)
private String title;
@Column(name = "cloud_link", nullable = true)
private String cloudLink;
@Column(name = "file_name", nullable = true)
private String fileName;
@Column(nullable = true)
private String path;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "media_file", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<MediaActorFile> mediaActorFiles = new LinkedList<>();
public static MediaFile newMediaFile(String url) {
var mediaFile = new MediaFile();
mediaFile.setUrl(url);
return mediaFile;
}
}
@@ -0,0 +1,11 @@
package de.thpeetz.kontor.models.media;
import java.util.List;
import org.hibernate.annotations.processing.Find;
public interface MediaFileQueries {
@Find
List<MediaFile> getAllMediaFiles();
}
@@ -0,0 +1,57 @@
package de.thpeetz.kontor.models.media;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "media_video", uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) })
public class MediaVideo {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date", nullable = true)
private Date lastModifiedDate;
@Column
private String url;
@Column
private boolean review;
@Column(name = "should_download")
private boolean shouldDownload;
@Column(nullable = true)
private String title;
@Column(name = "cloud_link", nullable = true)
private String cloudLink;
@Column(name = "file_name", nullable = true)
private String fileName;
@Column(nullable = true)
private String path;
}
@@ -1,8 +0,0 @@
package de.thpeetz.kontor.services.api;
import de.thpeetz.kontor.models.Person;
import java.util.List;
public interface PersonReader {
List<Person> getAll();
}
@@ -1,16 +0,0 @@
package de.thpeetz.kontor.services.inmemory;
import de.thpeetz.kontor.models.Person;
import java.util.List;
import de.thpeetz.kontor.services.api.PersonReader;
public class InMemoryPersonReader implements PersonReader {
@Override
public List<Person> getAll() {
return List.of(
new Person("Vincent Vega", 73),
new Person("Jules Winnfield", 12));
}
}
@@ -0,0 +1,17 @@
package de.thpeetz.kontor.web;
import java.util.List;
import de.thpeetz.kontor.infrastructure.AppHibernate;
import de.thpeetz.kontor.models.comics.Comic;
import de.thpeetz.kontor.models.comics.ComicQueries_;
import de.thpeetz.kontor.web.model.ResultComic;
import io.javalin.http.Handler;
public class ComicHandler {
public static Handler listAll = (context) -> {
List<Comic> result = AppHibernate.fromTransaction(ComicQueries_::getAllComics);
context.json(new ResultComic(result));
};
}
@@ -0,0 +1,29 @@
package de.thpeetz.kontor.web;
import java.util.LinkedList;
import java.util.List;
import de.thpeetz.kontor.infrastructure.AppHibernate;
import io.javalin.http.Handler;
import io.javalin.http.HttpStatus;
import de.thpeetz.kontor.models.media.MediaFile;
import de.thpeetz.kontor.web.model.NewMediaFile;
import de.thpeetz.kontor.web.model.ResultMediaFile;
public class MediaFileHandler {
public static Handler listAll = (context) -> {
List<MediaFile> result = new LinkedList<>();
context.json(new ResultMediaFile(result));
};
public static Handler save = (context) -> {
var newMediaFile = context.bodyAsClass(NewMediaFile.class);
var result = AppHibernate.fromTransaction(session -> {
var insertedId = session.insert(MediaFile.newMediaFile(newMediaFile.url()));
return session.get(MediaFile.class, insertedId);
});
context.json(result).status(HttpStatus.CREATED);
};
}
@@ -1,16 +0,0 @@
package de.thpeetz.kontor.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.thpeetz.kontor.services.inmemory.InMemoryPersonReader;
import io.javalin.http.Handler;
public class PersonHandler {
public static Handler listAll = (context) -> {
var personReader = new InMemoryPersonReader();
var objMapper = new ObjectMapper();
var result = objMapper.valueToTree(personReader.getAll());
context.json(result);
};
}
@@ -0,0 +1,4 @@
package de.thpeetz.kontor.web.model;
public record NewMediaFile(String url) {
}
@@ -0,0 +1,8 @@
package de.thpeetz.kontor.web.model;
import java.util.List;
import de.thpeetz.kontor.models.comics.Comic;
public record ResultComic(List<Comic> comics) {
}
@@ -0,0 +1,9 @@
package de.thpeetz.kontor.web.model;
import java.util.List;
import de.thpeetz.kontor.models.media.MediaFile;
public record ResultMediaFile(List<MediaFile> mediaFiles) {
}
@@ -1,24 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.hibernate.SQL" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<!-- <logger name="org.hibernate.orm.jdbc.bind" level="trace" additivity="false">-->
<!-- <appender-ref ref="STDOUT" />-->
<!-- </logger>-->
<!-- <logger name="org.hibernate.orm.jdbc.extract" level="trace" additivity="false">-->
<!-- <appender-ref ref="STDOUT" />-->
<!-- </logger>-->
</configuration>
+9
View File
@@ -0,0 +1,9 @@
*
!build.gradle.kts
!gradle.properties
!settings.gradle.kts
!src/*
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*
+42
View File
@@ -0,0 +1,42 @@
# Gradle
.gradle/
.kotlin/
build/
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
# Plugin directory
/.quarkus/cli/plugins/
# TLS Certificates
.certs/
+37
View File
@@ -0,0 +1,37 @@
# ----------------------------------------------------------------------- #
FROM gradle:9.2-jdk AS builder
WORKDIR /
COPY ./src/ ./src/
COPY ./build.gradle.kts ./
COPY ./gradle.properties ./
COPY ./settings.gradle.kts ./
RUN gradle build --no-daemon
# ----------------------------------------------------------------------- #
#FROM alpine/java:21-jdk AS run
#RUN adduser --system appuser
#COPY --from=builder --chown=appuser:appuser /build/libs/kontor-quarkus-0.2.0-SNAPSHOT.jar app.jar
#EXPOSE 8800
#USER appuser
#CMD ["java", "-jar", "-Dquarkus.http.host=0.0.0.0", "app.jar"]
# ----------------------------------------------------------------------- #
FROM registry.access.redhat.com/ubi9/openjdk-21:1.23
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --from=builder --chown=185 build/quarkus-app/lib/ /deployments/lib/
COPY --from=builder --chown=185 build/quarkus-app/*.jar /deployments/
COPY --from=builder --chown=185 build/quarkus-app/app/ /deployments/app/
COPY --from=builder --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8800
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
+67
View File
@@ -0,0 +1,67 @@
# kontor-quarkus
This project uses Quarkus, the Supersonic Subatomic Java Framework.
If you want to learn more about Quarkus, please visit its website: <https://quarkus.io/>.
## Running the application in dev mode
You can run your application in dev mode that enables live coding using:
```shell script
./gradlew quarkusDev
```
> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at <http://localhost:8080/q/dev/>.
## Packaging and running the application
The application can be packaged using:
```shell script
./gradlew build
```
It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory.
Be aware that its not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory.
The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`.
If you want to build an _über-jar_, execute the following command:
```shell script
./gradlew build -Dquarkus.package.jar.type=uber-jar
```
The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`.
## Creating a native executable
You can create a native executable using:
```shell script
./gradlew build -Dquarkus.native.enabled=true
```
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
```shell script
./gradlew build -Dquarkus.native.enabled=true -Dquarkus.native.container-build=true
```
You can then execute your native executable with: `./build/kontor-quarkus-1.0.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/gradle-tooling>.
## Related Guides
- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it
- Kotlin ([guide](https://quarkus.io/guides/kotlin)): Write your services in Kotlin
## Provided Code
### REST
Easily start your REST Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
+57
View File
@@ -0,0 +1,57 @@
plugins {
kotlin("jvm") version "2.2.21"
kotlin("plugin.allopen") version "2.2.21"
id("io.quarkus")
}
repositories {
mavenCentral()
mavenLocal()
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-container-image-docker")
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-kotlin")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-smallrye-openapi")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin")
implementation("io.quarkus:quarkus-jdbc-h2")
implementation("io.quarkus:quarkus-jdbc-postgresql")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
}
group = "de.thpeetz"
version = "0.2.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
tasks.withType<Test> {
systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
}
allOpen {
annotation("jakarta.ws.rs.Path")
annotation("jakarta.enterprise.context.ApplicationScoped")
annotation("jakarta.persistence.Entity")
annotation("io.quarkus.test.junit.QuarkusTest")
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
javaParameters = true
}
}
+7
View File
@@ -0,0 +1,7 @@
# Gradle properties
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.30.6
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.30.6
Binary file not shown.
@@ -0,0 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
# https://gradle.org/release-checksums/
distributionSha256Sum=f86344275d1b194688dd330abf9f6f2344cd02872ffee035f2d1ea2fd60cf7f3
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+245
View File
@@ -0,0 +1,245 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+13
View File
@@ -0,0 +1,13 @@
pluginManagement {
val quarkusPluginVersion: String by settings
val quarkusPluginId: String by settings
repositories {
mavenCentral()
gradlePluginPortal()
mavenLocal()
}
plugins {
id(quarkusPluginId) version quarkusPluginVersion
}
}
rootProject.name="kontor-quarkus"
@@ -0,0 +1,98 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-quarkus-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi9/openjdk-21:1.23
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 build/quarkus-app/*.jar /deployments/
COPY --chown=185 build/quarkus-app/app/ /deployments/app/
COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -0,0 +1,94 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.package.jar.type=legacy-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-quarkus-legacy-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi9/openjdk-21:1.23
ENV LANGUAGE='en_US:en'
COPY build/lib/* /deployments/lib/
COPY build/*-runner.jar /deployments/quarkus-run.jar
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -0,0 +1,29 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor-quarkus .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus
#
# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
###
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -0,0 +1,32 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
# It uses a micro base image, tuned for Quarkus native executables.
# It reduces the size of the resulting container image.
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor-quarkus .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus
#
# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`.
###
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -0,0 +1,14 @@
package de.thpeetz
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@Path("/hello")
class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
fun hello() = "Hello from Quarkus REST"
}
@@ -0,0 +1,14 @@
package de.thpeetz.kontor
import jakarta.ws.rs.core.Application
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition
import org.eclipse.microprofile.openapi.annotations.info.Info
import org.eclipse.microprofile.openapi.annotations.tags.Tag
@OpenAPIDefinition(
info = Info(
title = "Kontor",
version = "0.2.0"
)
)
class KontorApplication: Application()
@@ -0,0 +1,25 @@
package de.thpeetz.kontor.comics.domain
import io.quarkus.hibernate.orm.panache.kotlin.PanacheEntityBase
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.eclipse.microprofile.openapi.annotations.media.Schema
import java.time.LocalDate
import java.time.LocalDateTime
import kotlin.properties.Delegates
@Entity
@Table(name = "comic")
class Comic: PanacheEntityBase {
@Id
lateinit var id: String
lateinit var createdDate: LocalDateTime
lateinit var lastModifiedDate: LocalDateTime
lateinit var version: Integer
lateinit var title: String
lateinit var webLink: String
var completed: Boolean = false
var currentOrder: Boolean = false
lateinit var publisherId: String
}

Some files were not shown because too many files have changed in this diff Show More