19 Commits

Author SHA1 Message Date
Thomas Peetz 85b5168445 fix typo in workflow demo.yaml
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2026-05-05 18:05:19 +02:00
Thomas Peetz e8763fe635 test gitea actions
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 13s
2026-05-05 18:02:08 +02:00
Thomas Peetz 69e58e6dfb Add environment variable for DB server 2026-05-04 15:21:18 +02:00
Thomas Peetz d8914627a3 move *.service files to subdirectory container and add service file for standalone operations in standalone 2026-05-04 15:02:41 +02:00
Thomas Peetz b256e20491 renamed systemd unit files 2026-04-27 07:08:41 +02:00
tpeetz a8df860752 Merge branch 'feature/44-evaluate-typst-for-documentation' into 'develop/0.3.0'
Resolve "Evaluate Typst for documentation"

Closes #44

See merge request tpeetz/kontor!42
2026-04-26 21:09:54 +02:00
tpeetz 4e88b52dc3 use separate files for chapters 2026-04-26 21:09:06 +02:00
tpeetz 9772c9f554 add subprojekt kontor-doc 2026-04-26 16:08:17 +02:00
tpeetz e81d473a69 Einrichtung Build 2026-04-23 23:14:26 +02:00
tpeetz aca9a1aa4a integrate kontor-angular 2026-04-19 20:05:50 +02:00
tpeetz 57129c2d37 integration von kontor-angular 2026-04-18 00:29:25 +02:00
tpeetz e09f7cddf3 rename Dockerfile to Containerfile and integrate kontor-angular in pod kontor 2026-04-17 00:18:50 +02:00
Thomas Peetz a68dd05794 update kontor-api to reflect api changes 2026-04-15 14:38:38 +02:00
Thomas Peetz 62e22c56c4 update usage of api for login by email or username 2026-04-15 11:32:18 +02:00
Thomas Peetz 7aa6a12044 improve konto-vue/Containerfile by explicitly stating images 2026-04-15 10:26:35 +02:00
tpeetz 39a0759b0b fix OAuth authentication 2026-04-12 20:08:42 +02:00
tpeetz e82f2825ae update systemd service files and add script to copy these files 2026-04-12 17:51:19 +02:00
tpeetz 74d29ae8ef Merge branch 'feature/43-typeerror-not-all-arguments-converted-during-string-formatting' into 'develop/0.3.0'
Resolve "TypeError: not all arguments converted during string formatting"

Closes #43

See merge request tpeetz/kontor!41
2026-04-12 17:14:46 +02:00
tpeetz 614c5f20aa Resolve "TypeError: not all arguments converted during string formatting" 2026-04-12 17:14:46 +02:00
97 changed files with 4098 additions and 4117 deletions
+19
View File
@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} ist testing out Gitea Actions
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: inky
steps:
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "The workflow is now ready to test your code on the runner."
- name: List files in the directory
run: |
ls ${{ gitea.workspace }}
- run: echo "The job's status is ${{ job.status }}."
@@ -1,5 +1,5 @@
### STAGE 1: Build ###
FROM node:22.15-alpine AS build
FROM docker.io/node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
@@ -7,8 +7,9 @@ COPY . .
RUN npm run build
### STAGE 2: Run ###
FROM nginx:1.17.1-alpine
FROM docker.io/library/nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+1
View File
@@ -1,4 +1,5 @@
server {
listen 8800;
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
root /usr/share/nginx/html;
+5 -3
View File
@@ -5,7 +5,8 @@ from fastapi import APIRouter, Body, HTTPException, status, Depends, Response
from fastapi.security import OAuth2PasswordRequestForm
from src.core.config import settings
from src.core.security import create_access_token, authenticate_user, get_current_active_user
from src.core.log_conf import logger
from src.core.security import create_access_token, authenticate_user_by_email, authenticate_user_by_username, get_current_active_user
from src.db.models.admin import Profile
from src.schema.admin import Token, ProfileModel
from src.webapps.auth.forms import LoginForm
@@ -15,7 +16,8 @@ router = APIRouter()
@router.post("/token")
def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
user = authenticate_user(form_data.username, form_data.password)
user = authenticate_user_by_username(form_data.username, form_data.password)
logger.info(f"Request /token: login with {form_data.username}")
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -31,7 +33,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) # type: ignore
user = authenticate_user_by_email(form_data.username, form_data.password) # type: ignore
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
+19 -4
View File
@@ -1,11 +1,12 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer, SecurityScopes
from pydantic import BaseModel
from typing import Annotated
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.core.security import authenticate_user_by_email, authenticate_user_by_username, create_access_token
from src.schema.admin import Token
login_router = APIRouter()
@@ -25,7 +26,7 @@ class LoginRequest(BaseModel):
)
def login(request: LoginRequest) -> Token:
logger.info(f"login with {request.email}")
user = authenticate_user(request.email, request.password)
user = authenticate_user_by_email(request.email, request.password)
scopes = ["admin", "read"]
if not user:
raise HTTPException(
@@ -39,3 +40,17 @@ def login(request: LoginRequest) -> Token:
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
@login_router.post("/token", tags=["login"], summary="Login for access token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
user = authenticate_user_by_username(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.user_name, "scope": " ".join(form_data.scopes)},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
+57 -37
View File
@@ -13,48 +13,47 @@ from pydantic import ValidationError
from src.core.config import settings
from src.core.log_conf import logger
from src.db.models.admin import Profile
from src.db.repository.admin import get_profile, is_database_empty
from src.db.repository.admin import get_profile_by_username, get_profile_by_email, is_database_empty
from src.db.session import SessionLocal
from src.schema.admin import ProfileModel, TokenData
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/api/login/token",
scopes={"me": "read", "admin": "read"},
tokenUrl="/token",
scopes={"me": "read", "admin": "read", "ROLE_ADMIN": "admin", "ROLE_MEDIA": "media", "ROLE_USER": "user"},
)
class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__(
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}) # type: ignore
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
# class OAuth2PasswordBearerWithCookie(OAuth2):
# def __init__(
# 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}) # 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
# async def __call__(self, request: Request) -> Optional[str]:
# authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
return param
# scheme, param = get_authorization_scheme_param(authorization)
# if not authorization or scheme.lower() != "bearer":
# if self.auto_error:
# raise HTTPException(
# status_code=status.HTTP_401_UNAUTHORIZED,
# detail="Not authenticated",
# headers={"WWW-Authenticate": "Bearer"},
# )
# else:
# return None
# return param
def authenticate_user(username: str, password: str) -> Optional[Profile]:
def authenticate_user_by_email(email: str, password: str) -> Optional[Profile]:
with SessionLocal() as db:
user = get_profile(username=username, db=db)
user = get_profile_by_email(email=email, db=db)
logger.debug(user)
if not user:
if is_database_empty(db):
@@ -65,7 +64,26 @@ def authenticate_user(username: str, password: str) -> Optional[Profile]:
return None
else:
if bcrypt.checkpw(password.encode(), user.password.encode()):
print("User successful authenticated")
logger.info("User successful authenticated")
else:
logger.info("Authentication failed!")
return user
def authenticate_user_by_username(username: str, password: str) -> Optional[Profile]:
with SessionLocal() as db:
user = get_profile_by_username(username=username, db=db)
logger.debug(user)
if not user:
if is_database_empty(db):
logger.info("database is empty, use temporary access")
user = Profile()
user.email = "init_user@thpeetz.de"
return user
return None
else:
if bcrypt.checkpw(password.encode(), user.password.encode()):
logger.info("User successful authenticated")
else:
logger.info("Authentication failed!")
return user
@@ -103,17 +121,19 @@ async def get_current_user(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username)
logger.info("username/email extracted is %s", username)
if username is None:
raise credentials_exception
scope: str = payload.get("scope", "")
token_scopes: List[str] = scope.split(" ")
token_data = TokenData(scopes=token_scopes, username=username)
except (JWTError, ValidationError):
logger.info("Exception raised", exc_info=True)
raise credentials_exception
with SessionLocal() as db:
user = get_profile(username=token_data.username, db=db) # type: ignore
user = get_profile_by_username(username=token_data.username, db=db)
if user is None:
logger.info("user not found")
raise credentials_exception
for scope in security_scopes.scopes:
if scope not in token_scopes:
@@ -128,7 +148,7 @@ async def get_current_user(
async def get_current_active_user(
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
) -> ProfileModel:
if not current_user.enabled: # type: ignore
if not current_user.enabled:
raise HTTPException(status_code=400, detail="Inactive user")
user_model = ProfileModel(
username=current_user.user_name,
@@ -150,13 +170,13 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username)
logger.info("username/email extracted is %s", username)
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
with SessionLocal() as db:
user = get_profile(username=username, db=db)
user = get_profile_by_email(email=username, db=db)
if user is None:
raise credentials_exception
return user
+7 -3
View File
@@ -1,12 +1,16 @@
from typing import AnyStr, Optional
from typing import Optional
from sqlalchemy.orm import Session
from src.db.models.admin import Profile
def get_profile(username: AnyStr, db: Session) -> Optional[Profile]:
profile = db.query(Profile).filter(Profile.email == username).first()
def get_profile_by_username(username: str, db: Session) -> Optional[Profile]:
profile = db.query(Profile).filter(Profile.user_name == username).first()
return profile
def get_profile_by_email(email: str, db: Session) -> Optional[Profile]:
profile = db.query(Profile).filter(Profile.email == email).first()
return profile
def is_database_empty(db: Session) -> bool:
+12
View File
@@ -0,0 +1,12 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
# Binary files should be left untouched
*.jar binary
+8
View File
@@ -0,0 +1,8 @@
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build
# Ignore Kotlin plugin data
.kotlin
+21
View File
@@ -0,0 +1,21 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This is a general purpose Gradle build.
* Learn more about Gradle by exploring our Samples at https://docs.gradle.org/9.4.1/samples
*/
plugins {
id("base")
id("de.infolektuell.typst") version "0.8.0"
}
typst.version = "v0.14.2"
typst.sourceSets {
// Sources in src/main
val main by registering {
// The files to compile (without .typ extension) in src/main/typst
documents = listOf("kontor") // src/main/typst/document.typst
}
}
+5
View File
@@ -0,0 +1,5 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=true
+2
View File
@@ -0,0 +1,2 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/version_catalogs.html#sec::toml-dependencies-format
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# 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/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/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##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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
# 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
if ! command -v java >/dev/null 2>&1
then
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
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=SC2039,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=SC2039,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" )
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 optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# 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" "$@"
+93
View File
@@ -0,0 +1,93 @@
@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
@rem SPDX-License-Identifier: Apache-2.0
@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. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
: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
+8
View File
@@ -0,0 +1,8 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/9.4.1/userguide/multi_project_builds.html in the Gradle documentation.
*/
rootProject.name = "kontor-doc"
+25
View File
@@ -0,0 +1,25 @@
= Anforderungen der Domäne
== Systemfunktionen
=== Anwendungsfälle
=== Akteure
=== Zielgruppen
== Anforderungen
=== Anforderungen an externe Schnittstellen
=== Funktionale Anforderungen
=== Qualitätsanforderungen
=== Randbedingungen
=== Weitere Anforderungen
=== Wartungs- und Supportinformationen
== Verifikation
+99
View File
@@ -0,0 +1,99 @@
//#set page("a4")
#import "@preview/basic-report:0.4.0": *
#import "@preview/in-dexter:0.7.2": *
#import "@preview/pintorita:0.1.4"
#set text(fill: white)
#show raw.where(lang: "pintora"): it => pintorita.render(it.text)
#show: it => basic-report(
doc-category: "Entwicklungs- und Projekthandbuch",
doc-title: "Projekt kontor",
author: "Thomas Peetz",
//affiliation: "MouseTec, Entenhausen",
//logo: image("assets/aerospace-engineering.png", width: 2cm),
// <a href="https://www.flaticon.com/free-icons/aerospace" title="aerospace icons">Aerospace icons created by gravisio - Flaticon</a>
language: "de",
compact-mode: false,
it
)
= Allgemeines
#figure(
table(
columns: 4,
[Version], [Datum], [Autor], [Änderungsgrund / Bemerkungen],
[1.0.0], [16.05.2022], [Thomas Peetz], [Ersterstellung],
),
caption: [Dokumenthistorie],
)
== Zweck des Dokumentes
Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung
im Projekt kontor-spring und der Erstellung der Dokumentation.
== Verwendete Tools
=== Gitea
Für die Verwaltung des Sourcecode kommt Gitea #index[Gitea]@gitea zum Einsatz.
Mit Gitea werden auch die Projektaufgaben verwaltet.
Das Projekt und das dazugehörige Git Repository sind unter der Adresse @gitea-kontor zu finden.
= Erstellung der Dokumentation
Die Dokumentation des Projektes wird mit Asciidoctor #index[Asciidoctor]@asciidoctor geschrieben.
Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument.
== Quellcode Verwaltung
Die Asciidoctor-Dateien haben die Endung `.adoc`.
== Buildsystem
Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem Gradle #index[Gradle]@gradle verwendet.
Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`.
Der Gradle Build wird über die Datei `build.gradle` definiert.
#pagebreak()
= Einführung
== Zweck
== Stakeholder des Systems
== Systemumfang
=== Zielsetzung des Systems
#pagebreak()
#include "system.typ"
#include "domain.typ"
#include "projekt.typ"
= Verschiedenes
== Erreichbarkeiten
= Referenzen
#bibliography("reference.yaml", title: none, full: true)
= Glossar
= Index
#make-index(title: none, outlined: true, use-page-counter: true)
= Verzeichnisse
== Abbildungsverzeichnis
#outline(title: none, target: figure.where(kind: image))
== Tabellenverzeichnis
#outline(title: none, target: figure.where(kind: table))
+46
View File
@@ -0,0 +1,46 @@
= Projektbeschreibung
== Ausgangslage
//==== Rechtliche Vorgaben und Rahmenbedingungen
//=== Rahmenbedingungen
//==== Vorhandene Regelungen
== Projektziele
== Projektabgrenzung
//=== Voraussichtliche Kosten
//=== Projektrisiken
//==== Produktivität
//==== Finanzielle Risiken
//==== Akzeptanz
= Projektorganisation
== Projekt-Aufbauorganisation
== Rollendefinition
//==== Projektauftraggeber
//==== Projektausschuss
//==== Beratung / Qualitätssicherung
=== Projekteiter
=== Projektteam
=== Liste der Stakeholder
== Projektablauforganisation
=== Projekt-Phasen
== Erstellung der Projektdokumentation
+15
View File
@@ -0,0 +1,15 @@
asciidoctor:
type: web
url: http://asciidoctor.org
gitea:
type: web
url: http://www.gitea.org
gradle:
type: web
url: http://www.gradle.org
jenkins:
type: web
url: http://jenkins-ci.org
gitea-kontor:
type: web
url: https://gitea.thpeetz.de/kontor/kontor-spring
+336
View File
@@ -0,0 +1,336 @@
= Systemübersicht
== Systemkontext
== Systemarchitektur
== Systemschnittstellen
=== Realisierte Schnittstellen
=== Verwendete Schnittstellen
== Logisches Datenmodell
#figure(
kind: image,
```pintora
erDiagram
USER {
string id PK
datetime created_date
datetime last_modified_date
int version
string email
boolean enabled
string firstName
string lastName
string password
string token
boolean tokenExpired
string userName UNIQUE
}
USER ||--o{ AUTHORIZATION_MATRIX : "matrix"
ROLE {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
ROLE ||--o{ AUTHORIZATION_MATRIX : "matrix"
AUTHORIZATION_MATRIX {
string id PK
datetime created_date
datetime last_modified_date
int version
string user_id FK
string role_id FK
}
```,
caption: [Benutzer ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
comic {
string id PK
datetime created_date
datetime last_modified_date
int version
boolean completed
boolean currentOrder
string title
string publisher_id FK
}
comic ||--o{ comic_work : "1"
comic ||--o{ issue : "1"
comic ||--o{ volume : "1"
comic ||--o{ story_arc : "1"
comic ||--o{ trade_paperback : "1"
volume {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string comic_id FK
}
volume ||--o{ issue : "1"
issue {
string id PK
datetime created_date
datetime last_modified_date
int version
boolean in_stock
boolean is_read
string issue_number
string comic_id FK
string volume_id FK
}
publisher {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
publisher ||--o{ comic : "1"
artist {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
artist ||--o{ comic_work : "1"
story_arc {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string comic_id FK
}
trade_paperback {
string id PK
datetime created_date
datetime last_modified_date
int version
int issueStart
int issueEnd
string name
string comic_id FK
}
worktype {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
worktype ||--o{ comic-work : "1"
comic_work {
string id PK
datetime created_date
datetime last_modified_date
int version
string artist_id FK
string comic_id FK
string worktype_id FK
}
```,
caption: [Comics ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
sport {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
team {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string short_name
string sport_id FK
}
field_position {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
string short_name
string sport_id FK
}
rooster {
string id PK
datetime created_date
datetime last_modified_date
int version
int year
string player_id FK
string position_id FK
string team_id FK
}
player {
string id PK
datetime created_date
datetime last_modified_date
int version
string first_name
string last_name
}
vendor {
string id PK
datetime created_date
datetime last_modified_date
int version
string name
}
card_set {
string id PK
datetime created_date
datetime last_modified_date
int version
boolean insert_set
string name
boolean parallel_set
string vendor_id FK
}
card {
string id PK
datetime created_date
datetime last_modified_date
int version
int cardNumber
int year
string card_set FK
string rooster_id FK
string vendor_id FK
}
sport ||--o{ team : "1"
sport ||--o{ field_position : "1"
field_position ||--o{ rooster : "1"
player ||--o{ rooster : "1"
team ||--o{ rooster : "1"
vendor ||--o{ card : "1"
card_set ||--o{ card : "1"
rooster ||--o{ card : "1"
```,
caption: [TYSC ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
book {
string id PK
datetime created_date
datetime last_modified_date
int version
string isbn UNIQUE
string title
int year
string publisher_id FK
}
bookshelf_publisher {
string id PK
datetime created_date
datetime last_modified_date
int version
string name UNIQUE
}
author {
string id PK
datetime created_date
datetime last_modified_date
int version
string first_name
string last_name
}
article {
string id PK
datetime created_date
datetime last_modified_date
int version
string title
}
article_author {
string id PK
datetime created_date
datetime last_modified_date
int version
string article_id FK
string author_id FK
}
book_author {
string id PK
datetime created_date
datetime last_modified_date
int version
string book_id FK
string author_id FK
}
bookshelf_publisher ||--o{ book : "1"
article ||--o{ article_author : "1"
author ||--o{ article_author : "1"
book ||--o{ book_author : "1"
author ||--o{ book_author : "1"
```,
caption: [Bookshelf ER-Diagramm]
)
#figure(
kind: image,
```pintora
erDiagram
mail {
string id PK
datetime created_date
datetime last_modified_date
int version
string subject
string content
datetime received_date
datetime sent_date
}
mail_account {
string id PK
datetime created_date
datetime last_modified_date
int version
string host
string password
int port
string protocol
boolean start_tls
string user_name
}
mail_address {
string id PK
datetime created_date
datetime last_modified_date
int version
string internet_address UNIQUE
string personal
string user_id FK
}
user ||--o{ mail_address : "1"
```,
caption: [Mail ER-Diagramm]
)
=== Einschränkungen
+1
View File
@@ -30,6 +30,7 @@ plugins {
alias(libs.plugins.asciidoctorPdf)
alias(libs.plugins.asciidoctorConvert)
alias(libs.plugins.asciidoctorGems)
id "de.infolektuell.typst" version "0.8.0"
}
repositories {
-1
View File
@@ -1 +0,0 @@
VITE_API_URL=http://localhost:4000
-12
View File
@@ -1,12 +0,0 @@
/* eslint-env node */
module.exports = {
"root": true,
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"env": {
"vue/setup-compiler-macros": true,
"vue/multi-word-component-names": false
}
}
+1 -5
View File
@@ -8,19 +8,15 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
+3 -2
View File
@@ -1,4 +1,4 @@
FROM node:22.18.0-bookworm-slim AS build
FROM docker.io/node:20-alpine AS build
WORKDIR /app
ENV PATH=/app/node_modules/.bin:$PATH
COPY package.json /app/package.json
@@ -7,7 +7,8 @@ RUN npm install -g @vue/cli
COPY . /app
RUN npm run build
FROM nginx:1.29.0-alpine
FROM docker.io/library/nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+3 -3
View File
@@ -1,5 +1,5 @@
# vue-3-pinia-registration-login-example
# Vue 3 + Vite
Vue 3 + Pinia - User Registration and Login Example & Tutorial
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Documentation at https://jasonwatmore.com/post/2022/07/25/vue-3-pinia-user-registration-and-login-example-tutorial
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
+3 -19
View File
@@ -1,29 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 3 + Pinia - User Registration and Login Example & Tutorial</title>
<!-- bootstrap css -->
<link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
<title>kontor-vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- credits -->
<div class="text-center mt-4">
<p>
<a href="https://jasonwatmore.com/post/2022/07/25/vue-3-pinia-user-registration-and-login-example-tutorial">Vue 3 + Pinia - User Registration and Login Example & Tutorial</a>
</p>
<p>
<a href="https://jasonwatmore.com">JasonWatmore.com</a>
</p>
</div>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
server {
listen 8700;
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
root /usr/share/nginx/html;
# Definieren der Standard-Indexdatei (Angular erstellt die Datei index.html für uns und sie befindet sich im oben genannten Verzeichnis)
index index.html;
# Cache-Header für Medien-ASsets
location ~* \.(?:cur|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ {
access_log off;
add_header Pragma "must-revalidate, public";
add_header Cache-Control "must-revalidate, public";
expires max;
tcp_nodelay off;
}
# Cache-Header für HTML, CSS und JS-Dateien
location ~* \.(?:css|js|html)$ {
access_log off;
add_header Pragma "must-revalidate, public";
add_header Cache-Control "must-revalidate, public";
expires 2d;
tcp_nodelay off;
}
# Konfiguration für den /-Pfad
location / {
# Zunächst versuchen wir die angeforderte URI auzuliefern
# Klappt das nicht, versuchen wir es mit einem abschließenden Slash
# Klappt auch das nicht, liefern wir die index.html aus.
# Das ist nötig, damit Angular-Routen korrekt augeflöst und ausgeliefert werden
try_files $uri $uri/ /index.html;
}
}
+1294 -2764
View File
File diff suppressed because it is too large Load Diff
+18 -12
View File
@@ -1,23 +1,29 @@
{
"name": "vue-3-pinia-registration-login-example",
"name": "kontor-vue",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 5050",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
"preview": "vite preview"
},
"dependencies": {
"pinia": "^2.0.13",
"vee-validate": "^4.5.11",
"vue": "^3.2.33",
"vue-router": "^4.0.14",
"yup": "^0.32.11"
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/vue-fontawesome": "^3.1.3",
"axios": "^1.14.0",
"bootstrap": "^5.3.8",
"jquery": "^4.0.0",
"popper.js": "^1.16.1",
"vee-validate": "^4.15.1",
"vue": "^3.5.30",
"vue-router": "^4.6.4",
"vuex": "^4.1.0",
"yup": "^1.7.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^8.2.0",
"vite": "^2.9.5"
"@vitejs/plugin-vue": "^6.0.5",
"vite": "^8.0.1"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+78 -14
View File
@@ -1,20 +1,84 @@
<script setup>
import { Nav, Alert } from '@/components';
import { useAuthStore } from '@/stores';
const authStore = useAuthStore();
</script>
<template>
<div class="app-container" :class="authStore.user && 'bg-light'">
<Nav />
<Alert />
<div class="container pt-4 pb-4">
<div id="app">
<nav class="navbar navbar-expand navbar-dark bg-dark">
<a href="/" class="navbar-brand">Kontor</a>
<div class="navbar-nav mr-auto">
<li class="nav-item">
<router-link to="/home" class="nav-link">
<font-awesome-icon icon="home" /> Home
</router-link>
</li>
<li v-if="showAdminBoard" class="nav-item">
<router-link to="/admin" class="nav-link">Admin Board</router-link>
</li>
<li v-if="showModeratorBoard" class="nav-item">
<router-link to="/mod" class="nav-link">Moderator Board</router-link>
</li>
<li class="nav-item">
<router-link v-if="currentUser" to="/user" class="nav-link">User</router-link>
</li>
</div>
<div v-if="!currentUser" class="navbar-nav ml-auto">
<li class="nav-item">
<router-link to="/register" class="nav-link">
<font-awesome-icon icon="user-plus" /> Sign Up
</router-link>
</li>
<li class="nav-item">
<router-link to="/login" class="nav-link">
<font-awesome-icon icon="sign-in-alt" /> Login
</router-link>
</li>
</div>
<div v-if="currentUser" class="navbar-nav ml-auto">
<li class="nav-item">
<router-link to="/profile" class="nav-link">
<font-awesome-icon icon="user" />
{{ currentUser.username }}
</router-link>
</li>
<li class="nav-item">
<a class="nav-link" @click.prevent="logOut">
<font-awesome-icon icon="sign-out-alt" /> LogOut
</a>
</li>
</div>
</nav>
<div class="container">
<router-view />
</div>
</div>
</template>
<style>
@import '@/assets/base.css';
</style>
<script>
export default {
computed: {
currentUser() {
return this.$store.state.auth.user;
},
showAdminBoard() {
if (this.currentUser && this.currentUser['roles']) {
return this.currentUser['roles'].includes('ROLE_ADMIN');
}
return false;
},
showModeratorBoard() {
if (this.currentUser && this.currentUser['roles']) {
return this.currentUser['roles'].includes('ROLE_MODERATOR');
}
return false;
}
},
methods: {
logOut() {
this.$store.dispatch('auth/logout');
this.$router.push('/login');
}
}
};
</script>
-9
View File
@@ -1,9 +0,0 @@
.app-container {
min-height: 350px;
}
.btn-delete-user {
width: 40px;
text-align: center;
box-sizing: content-box;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

-19
View File
@@ -1,19 +0,0 @@
<script setup>
import { storeToRefs } from 'pinia';
import { useAlertStore } from '@/stores';
const alertStore = useAlertStore();
const { alert } = storeToRefs(alertStore);
</script>
<template>
<div v-if="alert" class="container">
<div class="m-3">
<div class="alert alert-dismissable" :class="alert.type">
<button @click="alertStore.clear()" class="btn btn-link close">&times;</button>
{{alert.message}}
</div>
</div>
</div>
</template>
+36
View File
@@ -0,0 +1,36 @@
<template>
<div class="container">
<header class="jumbotron">
<h3>{{ content }}</h3>
</header>
</div>
</template>
<script>
import UserService from "../services/user.service";
export default {
name: "Admin",
data() {
return {
content: "",
};
},
mounted() {
UserService.getAdminBoard().then(
(response) => {
this.content = response.data;
},
(error) => {
this.content =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
}
);
},
};
</script>
@@ -0,0 +1,36 @@
<template>
<div class="container">
<header class="jumbotron">
<h3>{{ content }}</h3>
</header>
</div>
</template>
<script>
import UserService from "../services/user.service";
export default {
name: "Moderator",
data() {
return {
content: "",
};
},
mounted() {
UserService.getModeratorBoard().then(
(response) => {
this.content = response.data;
},
(error) => {
this.content =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
}
);
},
};
</script>
+35
View File
@@ -0,0 +1,35 @@
<template>
<div class="container">
<header class="jumbotron">
<h3>{{ content }}</h3>
</header>
</div>
</template>
<script>
import UserService from "../services/user.service";
export default {
name: "User",
data() {
return {
content: "",
};
},
mounted() {
UserService.getUserBoard().then(
(response) => {
this.content = response.data;
},
(error) => {
this.content =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
}
);
},
};
</script>
+85
View File
@@ -0,0 +1,85 @@
<script setup>
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button class="counter" @click="count++">Count is {{ count }}</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>
+35
View File
@@ -0,0 +1,35 @@
<template>
<div class="container">
<header class="jumbotron">
<h3>{{ content }}</h3>
</header>
</div>
</template>
<script>
import UserService from "../services/user.service";
export default {
name: "Home",
data() {
return {
content: "",
};
},
mounted() {
UserService.getPublicContent().then(
(response) => {
this.content = response.data;
},
(error) => {
this.content =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
}
);
},
};
</script>
+96
View File
@@ -0,0 +1,96 @@
<template>
<div class="col-md-12">
<div class="card card-container">
<img
id="profile-img"
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
class="profile-img-card"
/>
<Form @submit="handleLogin" :validation-schema="schema">
<div class="form-group">
<label for="username">Username</label>
<Field name="username" type="text" class="form-control" />
<ErrorMessage name="username" class="error-feedback" />
</div>
<div class="form-group">
<label for="password">Password</label>
<Field name="password" type="password" class="form-control" />
<ErrorMessage name="password" class="error-feedback" />
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" :disabled="loading">
<span
v-show="loading"
class="spinner-border spinner-border-sm"
></span>
<span>Login</span>
</button>
</div>
<div class="form-group">
<div v-if="message" class="alert alert-danger" role="alert">
{{ message }}
</div>
</div>
</Form>
</div>
</div>
</template>
<script>
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
export default {
name: "Login",
components: {
Form,
Field,
ErrorMessage,
},
data() {
const schema = yup.object().shape({
username: yup.string().required("Username is required!"),
password: yup.string().required("Password is required!"),
});
return {
loading: false,
message: "",
schema,
};
},
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn;
},
},
created() {
if (this.loggedIn) {
this.$router.push("/profile");
}
},
methods: {
handleLogin(user) {
this.loading = true;
this.$store.dispatch("auth/login", user).then(
() => {
this.$router.push("/profile");
},
(error) => {
this.loading = false;
this.message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
}
);
},
},
};
</script>
-17
View File
@@ -1,17 +0,0 @@
<script setup>
import { useAuthStore } from '@/stores';
const authStore = useAuthStore();
</script>
<template>
<nav v-show="authStore.user" class="navbar navbar-expand navbar-dark bg-dark">
<div class="navbar-nav">
<router-link to="/" class="nav-item nav-link">Home</router-link>
<router-link to="/comics" class="nav-item nav-link">Comics</router-link>
<router-link to="/Media" class="nav-item nav-link">Media</router-link>
<router-link to="/users" class="nav-item nav-link">Users</router-link>
<button @click="authStore.logout()" class="btn btn-link nav-item nav-link">Logout</button>
</div>
</nav>
</template>
+41
View File
@@ -0,0 +1,41 @@
<template>
<div class="container">
<header class="jumbotron">
<h3>
<strong>{{currentUser.username}}</strong> Profile
</h3>
</header>
<p>
<strong>Token:</strong>
{{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
</p>
<p>
<strong>Id:</strong>
{{currentUser.id}}
</p>
<p>
<strong>Email:</strong>
{{currentUser.email}}
</p>
<strong>Authorities:</strong>
<ul>
<li v-for="role in currentUser.roles" :key="role">{{role}}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Profile',
computed: {
currentUser() {
return this.$store.state.auth.user;
}
},
mounted() {
if (!this.currentUser) {
this.$router.push('/login');
}
}
};
</script>
+123
View File
@@ -0,0 +1,123 @@
<template>
<div class="col-md-12">
<div class="card card-container">
<img
id="profile-img"
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
class="profile-img-card"
/>
<Form @submit="handleRegister" :validation-schema="schema">
<div v-if="!successful">
<div class="form-group">
<label for="username">Username</label>
<Field name="username" type="text" class="form-control" />
<ErrorMessage name="username" class="error-feedback" />
</div>
<div class="form-group">
<label for="email">Email</label>
<Field name="email" type="email" class="form-control" />
<ErrorMessage name="email" class="error-feedback" />
</div>
<div class="form-group">
<label for="password">Password</label>
<Field name="password" type="password" class="form-control" />
<ErrorMessage name="password" class="error-feedback" />
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" :disabled="loading">
<span
v-show="loading"
class="spinner-border spinner-border-sm"
></span>
Sign Up
</button>
</div>
</div>
</Form>
<div
v-if="message"
class="alert"
:class="successful ? 'alert-success' : 'alert-danger'"
>
{{ message }}
</div>
</div>
</div>
</template>
<script>
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
export default {
name: "Register",
components: {
Form,
Field,
ErrorMessage,
},
data() {
const schema = yup.object().shape({
username: yup
.string()
.required("Username is required!")
.min(3, "Must be at least 3 characters!")
.max(20, "Must be maximum 20 characters!"),
email: yup
.string()
.required("Email is required!")
.email("Email is invalid!")
.max(50, "Must be maximum 50 characters!"),
password: yup
.string()
.required("Password is required!")
.min(6, "Must be at least 6 characters!")
.max(40, "Must be maximum 40 characters!"),
});
return {
successful: false,
loading: false,
message: "",
schema,
};
},
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn;
},
},
mounted() {
if (this.loggedIn) {
this.$router.push("/profile");
}
},
methods: {
handleRegister(user) {
this.message = "";
this.successful = false;
this.loading = true;
this.$store.dispatch("auth/register", user).then(
(data) => {
this.message = data.message;
this.successful = true;
this.loading = false;
},
(error) => {
this.message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
this.successful = false;
this.loading = false;
}
);
},
},
};
</script>
-2
View File
@@ -1,2 +0,0 @@
export { default as Alert } from './Alert.vue';
export { default as Nav } from './Nav.vue';
-149
View File
@@ -1,149 +0,0 @@
export { fakeBackend };
// array in local storage for registered users
const usersKey = 'vue-3-pinia-registration-login-example-users';
let users = JSON.parse(localStorage.getItem(usersKey)) || [];
function fakeBackend() {
let realFetch = window.fetch;
window.fetch = function (url, opts) {
return new Promise((resolve, reject) => {
// wrap in timeout to simulate server api call
setTimeout(handleRoute, 500);
function handleRoute() {
switch (true) {
case url.endsWith('/users/authenticate') && opts.method === 'POST':
return authenticate();
case url.endsWith('/users/register') && opts.method === 'POST':
return register();
case url.endsWith('/users') && opts.method === 'GET':
return getUsers();
case url.match(/\/users\/\d+$/) && opts.method === 'GET':
return getUserById();
case url.match(/\/users\/\d+$/) && opts.method === 'PUT':
return updateUser();
case url.match(/\/users\/\d+$/) && opts.method === 'DELETE':
return deleteUser();
default:
// pass through any requests not handled above
return realFetch(url, opts)
.then(response => resolve(response))
.catch(error => reject(error));
}
}
// route functions
function authenticate() {
const { username, password } = body();
const user = users.find(x => x.username === username && x.password === password);
if (!user) return error('Username or password is incorrect');
return ok({
...basicDetails(user),
token: 'fake-jwt-token'
});
}
function register() {
const user = body();
if (users.find(x => x.username === user.username)) {
return error('Username "' + user.username + '" is already taken')
}
user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
users.push(user);
localStorage.setItem(usersKey, JSON.stringify(users));
return ok();
}
function getUsers() {
if (!isAuthenticated()) return unauthorized();
return ok(users.map(x => basicDetails(x)));
}
function getUserById() {
if (!isAuthenticated()) return unauthorized();
const user = users.find(x => x.id === idFromUrl());
return ok(basicDetails(user));
}
function updateUser() {
if (!isAuthenticated()) return unauthorized();
let params = body();
let user = users.find(x => x.id === idFromUrl());
// only update password if entered
if (!params.password) {
delete params.password;
}
// if username changed check if taken
if (params.username !== user.username && users.find(x => x.username === params.username)) {
return error('Username "' + params.username + '" is already taken')
}
// update and save user
Object.assign(user, params);
localStorage.setItem(usersKey, JSON.stringify(users));
return ok();
}
function deleteUser() {
if (!isAuthenticated()) return unauthorized();
users = users.filter(x => x.id !== idFromUrl());
localStorage.setItem(usersKey, JSON.stringify(users));
return ok();
}
// helper functions
function ok(body) {
resolve({ ok: true, ...headers(), json: () => Promise.resolve(body) })
}
function unauthorized() {
resolve({ status: 401, ...headers(), json: () => Promise.resolve({ message: 'Unauthorized' }) })
}
function error(message) {
resolve({ status: 400, ...headers(), json: () => Promise.resolve({ message }) })
}
function basicDetails(user) {
const { id, username, firstName, lastName } = user;
return { id, username, firstName, lastName };
}
function isAuthenticated() {
return opts.headers['Authorization'] === 'Bearer fake-jwt-token';
}
function body() {
return opts.body && JSON.parse(opts.body);
}
function idFromUrl() {
const urlParts = url.split('/');
return parseInt(urlParts[urlParts.length - 1]);
}
function headers() {
return {
headers: {
get(key) {
return ['application/json'];
}
}
}
}
});
}
}
-56
View File
@@ -1,56 +0,0 @@
import { useAuthStore } from '@/stores';
export const fetchWrapper = {
get: request('GET'),
post: request('POST'),
put: request('PUT'),
delete: request('DELETE')
};
function request(method) {
return (url, body) => {
const requestOptions = {
method,
headers: authHeader(url)
};
if (body) {
requestOptions.headers['Content-Type'] = 'application/json';
requestOptions.body = JSON.stringify(body);
}
return fetch(url, requestOptions).then(handleResponse);
}
}
// helper functions
function authHeader(url) {
// return auth header with jwt if user is logged in and request is to the api url
const { user } = useAuthStore();
const isLoggedIn = !!user?.token;
const isApiUrl = url.startsWith(import.meta.env.VITE_API_URL);
if (isLoggedIn && isApiUrl) {
return { Authorization: `Bearer ${user.token}` };
} else {
return {};
}
}
async function handleResponse(response) {
const isJson = response.headers?.get('content-type')?.includes('application/json');
const data = isJson ? await response.json() : null;
// check for error response
if (!response.ok) {
const { user, logout } = useAuthStore();
if ([401, 403].includes(response.status) && user) {
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
logout();
}
// get error message from body or default to response status
const error = (data && data.message) || response.status;
return Promise.reject(error);
}
return data;
}
-2
View File
@@ -1,2 +0,0 @@
export * from './fake-backend';
export * from './fetch-wrapper';
+13 -15
View File
@@ -1,16 +1,14 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import App from './App.vue';
import { router } from './router';
// setup fake backend
import { fakeBackend } from './helpers';
fakeBackend();
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');
createApp(App)
.use(router)
.use(store)
.component("font-awesome-icon", FontAwesomeIcon)
.mount("#app");
+60
View File
@@ -0,0 +1,60 @@
import { createWebHistory, createRouter } from "vue-router";
import Home from "./components/Home.vue";
import Login from "./components/Login.vue";
import Register from "./components/Register.vue";
// lazy-loaded
const Profile = () => import("./components/Profile.vue");
const BoardAdmin = () => import("./components/BoardAdmin.vue");
const BoardModerator = () => import("./components/BoardModerator.vue");
const BoardUser = () => import("./components/BoardUser.vue");
const routes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/home",
component: Home,
},
{
path: "/login",
component: Login,
},
{
path: "/register",
component: Register,
},
{
path: "/profile",
name: "profile",
// lazy-loaded
component: Profile,
},
{
path: "/admin",
name: "admin",
// lazy-loaded
component: BoardAdmin,
},
{
path: "/mod",
name: "moderator",
// lazy-loaded
component: BoardModerator,
},
{
path: "/user",
name: "user",
// lazy-loaded
component: BoardUser,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
-11
View File
@@ -1,11 +0,0 @@
import { Layout, Login, Register } from '@/views/account';
export default {
path: '/account',
component: Layout,
children: [
{ path: '', redirect: 'login' },
{ path: 'login', component: Login },
{ path: 'register', component: Register }
]
};
-34
View File
@@ -1,34 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore, useAlertStore } from '@/stores';
import { Home } from '@/views';
import accountRoutes from './account.routes';
import usersRoutes from './users.routes';
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
linkActiveClass: 'active',
routes: [
{ path: '/', component: Home },
{ ...accountRoutes },
{ ...usersRoutes },
// catch all redirect to home page
{ path: '/:pathMatch(.*)*', redirect: '/' }
]
});
router.beforeEach(async (to) => {
// clear alert on route change
const alertStore = useAlertStore();
alertStore.clear();
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/account/login', '/account/register'];
const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore();
if (authRequired && !authStore.user) {
authStore.returnUrl = to.fullPath;
return '/account/login';
}
});
-11
View File
@@ -1,11 +0,0 @@
import { Layout, List, AddEdit } from '@/views/users';
export default {
path: '/users',
component: Layout,
children: [
{ path: '', component: List },
{ path: 'add', component: AddEdit },
{ path: 'edit/:id', component: AddEdit }
]
};
+9
View File
@@ -0,0 +1,9 @@
export default function authHeader() {
let user = JSON.parse(localStorage.getItem("user"));
if (user && user.accessToken) {
return { Authorization: "Bearer " + user.accessToken };
} else {
return {};
}
}
+33
View File
@@ -0,0 +1,33 @@
import axios from "axios";
const API_URL = "http://localhost:8200/";
class AuthService {
login(user) {
return axios
.post(API_URL + "api/login/token", {
username: user.username,
password: user.password,
})
.then((response) => {
if (response.data.access_token) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
});
}
logout() {
localStorage.removeItem("user");
}
register(user) {
return axios.post(API_URL + "signup", {
username: user.username,
email: user.email,
password: user.password,
});
}
}
export default new AuthService();
+24
View File
@@ -0,0 +1,24 @@
import axios from "axios";
import authHeader from "./auth-header";
const API_URL = "http://localhost:8200/api/user/";
class UserService {
getPublicContent() {
return axios.get(API_URL + "profiles");
}
getUserBoard() {
return axios.get(API_URL + "user", { headers: authHeader() });
}
getModeratorBoard() {
return axios.get(API_URL + "mod", { headers: authHeader() });
}
getAdminBoard() {
return axios.get(API_URL + "admin", { headers: authHeader() });
}
}
export default new UserService();
+61
View File
@@ -0,0 +1,61 @@
import AuthService from "../services/auth.service";
const user = JSON.parse(localStorage.getItem("user"));
const initialState = user
? { status: { loggedIn: true }, user }
: { status: { loggedIn: false }, user: null };
export const auth = {
namespaced: true,
state: initialState,
actions: {
login({ commit }, user) {
return AuthService.login(user).then(
(user) => {
commit("loginSuccess", user);
return Promise.resolve(user);
},
(error) => {
commit("loginFailure");
return Promise.reject(error);
}
);
},
logout({ commit }) {
AuthService.logout();
commit("logout");
},
register({ commit }, user) {
return AuthService.register(user).then(
(response) => {
commit("registerSuccess");
return Promise.resolve(response.data);
},
(error) => {
commit("registerFailure");
return Promise.reject(error);
}
);
},
},
mutations: {
loginSuccess(state, user) {
state.status.loggedIn = true;
state.user = user;
},
loginFailure(state) {
state.status.loggedIn = false;
state.user = null;
},
logout(state) {
state.status.loggedIn = false;
state.user = null;
},
registerSuccess(state) {
state.status.loggedIn = false;
},
registerFailure(state) {
state.status.loggedIn = false;
},
},
};
+10
View File
@@ -0,0 +1,10 @@
import { createStore } from "vuex";
import { auth } from "./auth.module";
const store = createStore({
modules: {
auth,
},
});
export default store;
-19
View File
@@ -1,19 +0,0 @@
import { defineStore } from 'pinia';
export const useAlertStore = defineStore({
id: 'alert',
state: () => ({
alert: null
}),
actions: {
success(message) {
this.alert = { message, type: 'alert-success' };
},
error(message) {
this.alert = { message, type: 'alert-danger' };
},
clear() {
this.alert = null;
}
}
});
-40
View File
@@ -1,40 +0,0 @@
import { defineStore } from 'pinia';
import { fetchWrapper } from '@/helpers';
import { router } from '@/router';
import { useAlertStore } from '@/stores';
const baseUrl = `${import.meta.env.VITE_API_URL}/users`;
export const useAuthStore = defineStore({
id: 'auth',
state: () => ({
// initialize state from local storage to enable user to stay logged in
user: JSON.parse(localStorage.getItem('user')),
returnUrl: null
}),
actions: {
async login(username, password) {
try {
const user = await fetchWrapper.post(`${baseUrl}/authenticate`, { username, password });
// update pinia state
this.user = user;
// store user details and jwt in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
// redirect to previous url or default to home page
router.push(this.returnUrl || '/');
} catch (error) {
const alertStore = useAlertStore();
alertStore.error(error);
}
},
logout() {
this.user = null;
localStorage.removeItem('user');
router.push('/account/login');
}
}
});
-3
View File
@@ -1,3 +0,0 @@
export * from './alert.store';
export * from './auth.store';
export * from './users.store';
-64
View File
@@ -1,64 +0,0 @@
import { defineStore } from 'pinia';
import { fetchWrapper } from '@/helpers';
import { useAuthStore } from '@/stores';
const baseUrl = `${import.meta.env.VITE_API_URL}/users`;
export const useUsersStore = defineStore({
id: 'users',
state: () => ({
users: {},
user: {}
}),
actions: {
async register(user) {
await fetchWrapper.post(`${baseUrl}/register`, user);
},
async getAll() {
this.users = { loading: true };
try {
this.users = await fetchWrapper.get(baseUrl);
} catch (error) {
this.users = { error };
}
},
async getById(id) {
this.user = { loading: true };
try {
this.user = await fetchWrapper.get(`${baseUrl}/${id}`);
} catch (error) {
this.user = { error };
}
},
async update(id, params) {
await fetchWrapper.put(`${baseUrl}/${id}`, params);
// update stored user if the logged in user updated their own record
const authStore = useAuthStore();
if (id === authStore.user.id) {
// update local storage
const user = { ...authStore.user, ...params };
localStorage.setItem('user', JSON.stringify(user));
// update auth user in pinia state
authStore.user = user;
}
},
async delete(id) {
// add isDeleting prop to user being deleted
this.users.find(x => x.id === id).isDeleting = true;
await fetchWrapper.delete(`${baseUrl}/${id}`);
// remove user from list after deleted
this.users = this.users.filter(x => x.id !== id);
// auto logout if the logged in user deleted their own record
const authStore = useAuthStore();
if (id === authStore.user.id) {
authStore.logout();
}
}
}
});
+296
View File
@@ -0,0 +1,296 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
-16
View File
@@ -1,16 +0,0 @@
<script setup>
import { storeToRefs } from 'pinia';
import { useAuthStore } from '@/stores';
const authStore = useAuthStore();
const { user } = storeToRefs(authStore);
</script>
<template>
<div v-if="user">
<h1>Hi {{user.firstName}}!</h1>
<p>You're logged in with Vue 3 + Pinia & JWT!!</p>
<p><router-link to="/users">Manage Users</router-link></p>
</div>
</template>
-20
View File
@@ -1,20 +0,0 @@
<script setup>
import { useAuthStore } from '@/stores';
import { router } from '@/router';
// redirect home if already logged in
const authStore = useAuthStore();
if (authStore.user) {
router.push('/');
}
</script>
<template>
<div class="container">
<div class="row">
<div class="col-sm-8 offset-sm-2 mt-5">
<router-view />
</div>
</div>
</div>
</template>
-44
View File
@@ -1,44 +0,0 @@
<script setup>
import { Form, Field } from 'vee-validate';
import * as Yup from 'yup';
import { useAuthStore } from '@/stores';
const schema = Yup.object().shape({
username: Yup.string().required('Username is required'),
password: Yup.string().required('Password is required')
});
async function onSubmit(values) {
const authStore = useAuthStore();
const { username, password } = values;
await authStore.login(username, password);
}
</script>
<template>
<div class="card m-3">
<h4 class="card-header">Login</h4>
<div class="card-body">
<Form @submit="onSubmit" :validation-schema="schema" v-slot="{ errors, isSubmitting }">
<div class="form-group">
<label>Username</label>
<Field name="username" type="text" class="form-control" :class="{ 'is-invalid': errors.username }" />
<div class="invalid-feedback">{{ errors.username }}</div>
</div>
<div class="form-group">
<label>Password</label>
<Field name="password" type="password" class="form-control" :class="{ 'is-invalid': errors.password }" />
<div class="invalid-feedback">{{ errors.password }}</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="isSubmitting">
<span v-show="isSubmitting" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
<router-link to="register" class="btn btn-link">Register</router-link>
</div>
</Form>
</div>
</div>
</template>
-68
View File
@@ -1,68 +0,0 @@
<script setup>
import { Form, Field } from 'vee-validate';
import * as Yup from 'yup';
import { useUsersStore, useAlertStore } from '@/stores';
import { router } from '@/router';
const schema = Yup.object().shape({
firstName: Yup.string()
.required('First Name is required'),
lastName: Yup.string()
.required('Last Name is required'),
username: Yup.string()
.required('Username is required'),
password: Yup.string()
.required('Password is required')
.min(6, 'Password must be at least 6 characters')
});
async function onSubmit(values) {
const usersStore = useUsersStore();
const alertStore = useAlertStore();
try {
await usersStore.register(values);
await router.push('/account/login');
alertStore.success('Registration successful');
} catch (error) {
alertStore.error(error);
}
}
</script>
<template>
<div class="card m-3">
<h4 class="card-header">Register</h4>
<div class="card-body">
<Form @submit="onSubmit" :validation-schema="schema" v-slot="{ errors, isSubmitting }">
<div class="form-group">
<label>First Name</label>
<Field name="firstName" type="text" class="form-control" :class="{ 'is-invalid': errors.firstName }" />
<div class="invalid-feedback">{{ errors.firstName }}</div>
</div>
<div class="form-group">
<label>Last Name</label>
<Field name="lastName" type="text" class="form-control" :class="{ 'is-invalid': errors.lastName }" />
<div class="invalid-feedback">{{ errors.lastName }}</div>
</div>
<div class="form-group">
<label>Username</label>
<Field name="username" type="text" class="form-control" :class="{ 'is-invalid': errors.username }" />
<div class="invalid-feedback">{{ errors.username }}</div>
</div>
<div class="form-group">
<label>Password</label>
<Field name="password" type="password" class="form-control" :class="{ 'is-invalid': errors.password }" />
<div class="invalid-feedback">{{ errors.password }}</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="isSubmitting">
<span v-show="isSubmitting" class="spinner-border spinner-border-sm mr-1"></span>
Register
</button>
<router-link to="login" class="btn btn-link">Cancel</router-link>
</div>
</Form>
</div>
</div>
</template>
-3
View File
@@ -1,3 +0,0 @@
export { default as Layout } from './Layout.vue';
export { default as Login } from './Login.vue';
export { default as Register } from './Register.vue';
-1
View File
@@ -1 +0,0 @@
export { default as Home } from './Home.vue';
-106
View File
@@ -1,106 +0,0 @@
<script setup>
import { Form, Field } from 'vee-validate';
import * as Yup from 'yup';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useUsersStore, useAlertStore } from '@/stores';
import { router } from '@/router';
const usersStore = useUsersStore();
const alertStore = useAlertStore();
const route = useRoute();
const id = route.params.id;
let title = 'Add User';
let user = null;
if (id) {
// edit mode
title = 'Edit User';
({ user } = storeToRefs(usersStore));
usersStore.getById(id);
}
const schema = Yup.object().shape({
firstName: Yup.string()
.required('First Name is required'),
lastName: Yup.string()
.required('Last Name is required'),
username: Yup.string()
.required('Username is required'),
password: Yup.string()
.transform(x => x === '' ? undefined : x)
// password optional in edit mode
.concat(user ? null : Yup.string().required('Password is required'))
.min(6, 'Password must be at least 6 characters')
});
async function onSubmit(values) {
try {
let message;
if (user) {
await usersStore.update(user.value.id, values)
message = 'User updated';
} else {
await usersStore.register(values);
message = 'User added';
}
await router.push('/users');
alertStore.success(message);
} catch (error) {
alertStore.error(error);
}
}
</script>
<template>
<h1>{{title}}</h1>
<template v-if="!(user?.loading || user?.error)">
<Form @submit="onSubmit" :validation-schema="schema" :initial-values="user" v-slot="{ errors, isSubmitting }">
<div class="form-row">
<div class="form-group col">
<label>First Name</label>
<Field name="firstName" type="text" class="form-control" :class="{ 'is-invalid': errors.firstName }" />
<div class="invalid-feedback">{{ errors.firstName }}</div>
</div>
<div class="form-group col">
<label>Last Name</label>
<Field name="lastName" type="text" class="form-control" :class="{ 'is-invalid': errors.lastName }" />
<div class="invalid-feedback">{{ errors.lastName }}</div>
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label>Username</label>
<Field name="username" type="text" class="form-control" :class="{ 'is-invalid': errors.username }" />
<div class="invalid-feedback">{{ errors.username }}</div>
</div>
<div class="form-group col">
<label>
Password
<em v-if="user">(Leave blank to keep the same password)</em>
</label>
<Field name="password" type="password" class="form-control" :class="{ 'is-invalid': errors.password }" />
<div class="invalid-feedback">{{ errors.password }}</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="isSubmitting">
<span v-show="isSubmitting" class="spinner-border spinner-border-sm mr-1"></span>
Save
</button>
<router-link to="/users" class="btn btn-link">Cancel</router-link>
</div>
</Form>
</template>
<template v-if="user?.loading">
<div class="text-center m-5">
<span class="spinner-border spinner-border-lg align-center"></span>
</div>
</template>
<template v-if="user?.error">
<div class="text-center m-5">
<div class="text-danger">Error loading user: {{user.error}}</div>
</div>
</template>
</template>
-8
View File
@@ -1,8 +0,0 @@
<template>
<div class="p-4">
<div class="container">
<router-view />
</div>
</div>
</template>
-51
View File
@@ -1,51 +0,0 @@
<script setup>
import { storeToRefs } from 'pinia';
import { useUsersStore } from '@/stores';
const usersStore = useUsersStore();
const { users } = storeToRefs(usersStore);
usersStore.getAll();
</script>
<template>
<h1>Users</h1>
<router-link to="/users/add" class="btn btn-sm btn-success mb-2">Add User</router-link>
<table class="table table-striped">
<thead>
<tr>
<th style="width: 30%">First Name</th>
<th style="width: 30%">Last Name</th>
<th style="width: 30%">Username</th>
<th style="width: 10%"></th>
</tr>
</thead>
<tbody>
<template v-if="users.length">
<tr v-for="user in users" :key="user.id">
<td>{{ user.firstName }}</td>
<td>{{ user.lastName }}</td>
<td>{{ user.username }}</td>
<td style="white-space: nowrap">
<router-link :to="`/users/edit/${user.id}`" class="btn btn-sm btn-primary mr-1">Edit</router-link>
<button @click="usersStore.delete(user.id)" class="btn btn-sm btn-danger btn-delete-user" :disabled="user.isDeleting">
<span v-if="user.isDeleting" class="spinner-border spinner-border-sm"></span>
<span v-else>Delete</span>
</button>
</td>
</tr>
</template>
<tr v-if="users.loading">
<td colspan="4" class="text-center">
<span class="spinner-border spinner-border-lg align-center"></span>
</td>
</tr>
<tr v-if="users.error">
<td colspan="4">
<div class="text-danger">Error loading users: {{users.error}}</div>
</td>
</tr>
</tbody>
</table>
</template>
-3
View File
@@ -1,3 +0,0 @@
export { default as AddEdit } from './AddEdit.vue';
export { default as Layout } from './Layout.vue';
export { default as List } from './List.vue';
+4 -11
View File
@@ -1,14 +1,7 @@
import { fileURLToPath, URL } from 'url';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
});
})
+2
View File
@@ -20,4 +20,6 @@ echo " => Baue Image localhost/kontor-javalin:0.3.0"
buildah build -t kontor-javalin:0.3.0 kontor-javalin
echo " => Baue Image localhost/kontor-vue:0.3.0"
buildah build -t kontor-vue:0.3.0 kontor-vue
echo " => Baue Image localhost/kontor-angular:0.3.0"
buildah build -t kontor-angular:0.3.0 kontor-angular
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# script/service: Setup user systemd scripts
set -e
cd "$(dirname "$0")/.."
echo "==> Setting up systemd user services"
cp service/container/*.service $HOME/.config/systemd/user/
+21 -2
View File
@@ -8,7 +8,7 @@ cd "$(dirname "$0")/.."
script/bootstrap
script/cibuild
#script/cibuild
echo "==> Setting up pod kontor"
@@ -29,7 +29,8 @@ else
-p 8400:8400 \
-p 8500:8500 \
-p 8600:8600 \
-p 8700:80 \
-p 8700:8700 \
-p 8800:8800 \
-p 8900:8080 \
-p 61616:61616 \
-p 8161:8161 \
@@ -235,3 +236,21 @@ else
localhost/kontor-vue:0.3.0
fi
echo "==> Setting up container kontor-angular"
if podman image exists localhost/kontor-angular:0.3.0; then
echo " => Image localhost/kontor-angular:0.3.0 available"
fi
if podman container exists kontor-angular; then
if podman ps -q --filter "name=kontor-angular"; then
echo " => kontor-angular is running"
fi
else
podman run -d \
--replace \
--pod kontor \
--name kontor-angular \
--label "io.containers.autoupdate=local" \
--label "PODMAN_SYSTEMD_UNIT=container-kontor-angular.service" \
localhost/kontor-angular:0.3.0
fi
+3 -1
View File
@@ -9,7 +9,9 @@ cd "$(dirname "$0")/.."
echo "==> Stopping and removing pod kontor"
if podman pod exists kontor; then
podman pod stop kontor
podman pod rm kontor
fi
script/cibuild
script/setup
@@ -1,12 +1,11 @@
# container-activemq.service
# kontor-activemq.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-activemq.service
Description=Podman kontor-activemq.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
@@ -1,12 +1,11 @@
# container-adminer.service
# kontor-adminer.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-adminer.service
Description=Podman kontor-adminer.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
@@ -23,7 +22,8 @@ ExecStart=/usr/bin/podman run \
--sdnotify=conmon \
-d \
--replace \
--name adminer docker.io/library/adminer
--name kontor-adminer \
docker.io/library/adminer
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
+41
View File
@@ -0,0 +1,41 @@
# kontor-angular.service
# autogenerated by Podman 4.9.3
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman kontor-angular.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--pod-id-file %t/pod-kontor.pod-id \
--sdnotify=conmon \
-d \
--replace \
--name kontor-angular \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=kontor-angular.service \
localhost/kontor-angular:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
@@ -1,12 +1,11 @@
# container-kontor-api.service
# kontor-api.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-kontor-api.service
Description=Podmanco kontor-api.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
@@ -25,10 +24,12 @@ ExecStart=/usr/bin/podman run \
--replace \
--name kontor-api \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-api.service "--health-cmd=curl -f http://kontor-api:8200/health || exit 1" \
--label PODMAN_SYSTEMD_UNIT=kontor-api.service \
--health-cmd="curl -f http://kontor-api:8200/health || exit 1" \
--health-interval=1s \
--health-timeout=5s \
--health-retries=10 localhost/kontor-api:0.3.0
--health-retries=10 \
localhost/kontor-api:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-echo.service
# kontor-echo.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-kontor-echo.service
Description=Podman kontor-echo.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \
--name kontor-echo \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-echo.service "--health-cmd=curl -f http://kontor-echo:8400/health || exit 1" \
--label PODMAN_SYSTEMD_UNIT=kontor-echo.service \
--health-cmd="curl -f http://kontor-echo:8400/health || exit 1" \
--health-interval=1s \
--health-timeout=5s \
--health-retries=10 localhost/kontor-echo:0.3.0
--health-retries=10 \
localhost/kontor-echo:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-fiber.service
# kontor-fiber.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-kontor-fiber.service
Description=Podman kontor-fiber.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \
--name kontor-fiber \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-fiber.service "--health-cmd=curl -f http://kontor-echo:8500/health || exit 1" \
--label PODMAN_SYSTEMD_UNIT=kontor-fiber.service \
--health-cmd="curl -f http://kontor-echo:8500/health || exit 1" \
--health-interval=1s \
--health-timeout=5s \
--health-retries=10 localhost/kontor-fiber:0.3.0
--health-retries=10 \
localhost/kontor-fiber:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-javalin.service
# kontor-javalin.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-kontor-javalin.service
Description=Podman kontor-javalin.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \
--name kontor-javalin \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-javalin.service "--health-cmd=curl -f http://kontor-echo:8600/health || exit 1" \
--label PODMAN_SYSTEMD_UNIT=kontor-javalin.service \
--health-cmd="curl -f http://kontor-echo:8600/health || exit 1" \
--health-interval=1s \
--health-timeout=5s \
--health-retries=10 localhost/kontor-javalin:0.3.0
--health-retries=10 \
localhost/kontor-javalin:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-postgres.service
# kontor-postgres.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-postgres.service
Description=Podman kontor-postgres.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
@@ -27,10 +27,12 @@ ExecStart=/usr/bin/podman run \
-e POSTGRES_PASSWORD=kontor \
-e POSTGRES_DB=kontor \
-e POSTGRES_USER=kontor \
-v kontor-db:/var/lib/postgresql/data "--health-cmd=pg_isready -U kontor || exit 1" \
-v kontor-db:/var/lib/postgresql/data \
--health-cmd="pg_isready -U kontor || exit 1" \
--health-interval=1s \
--health-timeout=5s \
--health-retries=10 docker.io/library/postgres:17
--health-retries=10 \
docker.io/library/postgres:17
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-quarkus.service
# kontor-quarkus.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-kontor-quarkus.service
Description=Podman kontor-quarkus.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
@@ -25,10 +25,12 @@ ExecStart=/usr/bin/podman run \
--replace \
--name kontor-quarkus \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-quarkus.service "--health-cmd=curl -f http://kontor-quarkus:8300/q/health || exit 1" \
--label PODMAN_SYSTEMD_UNIT=kontor-quarkus.service \
--health-cmd="curl -f http://kontor-quarkus:8300/q/health || exit 1" \
--health-interval=1s \
--health-timeout=5s \
--health-retries=10 localhost/kontor-quarkus:0.3.0
--health-retries=10 \
localhost/kontor-quarkus:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
@@ -1,9 +1,9 @@
# container-kontor-spring.service
# kontor-spring.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman container-kontor-spring.service
Description=Podman kontor-spring.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
@@ -25,7 +25,8 @@ ExecStart=/usr/bin/podman run \
--replace \
--name kontor-spring \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=container-kontor-spring.service localhost/kontor-spring:0.3.0
--label PODMAN_SYSTEMD_UNIT=kontor-spring.service \
localhost/kontor-spring:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
+41
View File
@@ -0,0 +1,41 @@
# kontor-vue.service
# autogenerated by Podman 4.9.3
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman kontor-vue.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-kontor.service
After=pod-kontor.service
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--pod-id-file %t/pod-kontor.pod-id \
--sdnotify=conmon \
-d \
--replace \
--name kontor-vue \
--label io.containers.autoupdate=local \
--label PODMAN_SYSTEMD_UNIT=kontor-vue.service \
localhost/kontor-vue:0.3.0
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
@@ -1,6 +1,6 @@
# pod-kontor.service
# autogenerated by Podman 4.9.3
# Fri Feb 6 17:23:12 CET 2026
# Sun Apr 19 16:28:14 CEST 2026
[Unit]
Description=Podman pod-kontor.service
@@ -8,8 +8,8 @@ Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/run/user/1000/containers
Wants=container-activemq.service container-adminer.service container-kontor-api.service container-kontor-echo.service container-kontor-fiber.service container-kontor-javalin.service container-kontor-quarkus.service container-kontor-spring.service container-postgres.service
Before=container-activemq.service container-adminer.service container-kontor-api.service container-kontor-echo.service container-kontor-fiber.service container-kontor-javalin.service container-kontor-quarkus.service container-kontor-spring.service container-postgres.service
Wants=kontor-activemq.service kontor-adminer.service kontor-angular.service kontor-api.service kontor-echo.service kontor-fiber.service kontor-javalin.service kontor-quarkus.service kontor-spring.service kontor-vue.service kontor-postgres.service
Before=kontor-activemq.service kontor-adminer.service kontor-angular.service kontor-api.service kontor-echo.service kontor-fiber.service kontor-javalin.service kontor-quarkus.service kontor-spring.service kontor-vue.service kontor-postgres.service
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
@@ -28,6 +28,8 @@ ExecStartPre=/usr/bin/podman pod create \
-p 8400:8400 \
-p 8500:8500 \
-p 8600:8600 \
-p 8700:8700 \
-p 8800:8800 \
-p 8900:8080 \
-p 61616:61616 \
-p 8161:8161 \
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=kontor-api via UV
After=network.target
[Service]
Type=simple
User=tpeetz
Group=tpeetz
Environment="DB_SERVER=localhost"
WorkingDirectory=/home/tpeetz/projects/kontor/kontor-api
ExecStart=/home/tpeetz/projects/kontor/kontor-api/.venv/bin/uvicorn src.main:kontor --log-level info --host 127.0.0.1 --port 8200
Restart=on-failure
[Install]
WantedBy=multi-user.target