not yet completed migration to jakarta - still keycloak roles missing

This commit is contained in:
Jörg Henke
2023-07-27 19:35:36 +02:00
parent 059dcadb01
commit f07f8f3c06
33 changed files with 310 additions and 453 deletions

View File

@ -19,7 +19,12 @@
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.web.container"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer">
<attributes>
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@ -10,11 +10,6 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
@ -25,6 +20,11 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>

View File

@ -1,11 +1,14 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=11
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=11
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17

View File

@ -1,48 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="timetrack">
<property name="context-root" value="timetrack"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/resources"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/webapp"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/java"/>
<wb-resource deploy-path="/" source-path="src/main/webapp"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/java"/>
</wb-module>
<?xml version="1.0" encoding="UTF-8"?>
<project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="timetrack">
<property name="context-root" value="timetrack"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/resources"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="src/main/java"/>
</wb-module>
</project-modules>

View File

@ -3,5 +3,5 @@
<fixed facet="jst.java"/>
<fixed facet="jst.web"/>
<installed facet="jst.web" version="2.4"/>
<installed facet="jst.java" version="11"/>
<installed facet="jst.java" version="17"/>
</faceted-project>

View File

@ -1,19 +1,18 @@
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'org.springframework.boot' version '3.1.1'
id 'java'
id 'war'
}
group = 'de.jottyfan'
version = '1.2.8'
sourceCompatibility = '11'
apply plugin: 'io.spring.dependency-management'
ext['spring-framework.version'] = '5.3.18'
group = 'de.jottyfan'
version = '1.2.9'
description = """timetrack"""
sourceCompatibility = 11
targetCompatibility = 11
sourceCompatibility = 17
targetCompatibility = 17
repositories {
mavenLocal()
@ -23,12 +22,6 @@ repositories {
// maven { url "https://gitlab.com/jottyfan/libs/-/raw/main" }
}
dependencyManagement {
imports {
mavenBom 'org.keycloak.bom:keycloak-adapter-bom:21.1.1'
}
}
dependencies {
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
@ -46,14 +39,14 @@ dependencies {
implementation 'org.webjars.bowergithub.datatables:datatables:1.10.21'
implementation 'org.keycloak:keycloak-spring-boot-starter:17.0.1'
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:1.1.1'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'de.jottyfan:timetrackjooq:0.1.1'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0'
@ -64,6 +57,18 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
}
war {
doFirst {
manifest {
attributes("Implementation-Title": project.name,
"Implementation-Version": version,
"Implementation-Timestamp": new Date())
}
}
baseName = project.name
version = version
}
test {
useJUnitPlatform()
}

View File

@ -2,14 +2,21 @@ package de.jottyfan.timetrack;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
public class TimetrackApplication {
public class TimetrackApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder application) {
return application.sources(TimetrackApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(TimetrackApplication.class, args);
}
}

View File

@ -1,56 +0,0 @@
package de.jottyfan.timetrack.config;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@KeycloakConfiguration
@ComponentScan(basePackageClasses = KeycloakSpringBootConfigResolver.class)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/public/**").permitAll();
http.csrf().disable();
}
}

View File

@ -0,0 +1,58 @@
package de.jottyfan.timetrack.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import de.jottyfan.timetrack.config.converter.KeycloakRealmRoleConverter;
/**
*
* @author henkej
*
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {
@Value("${spring.security.oauth2.client.provider.keycloak.jwk-set-uri}")
private String jwtSetUri;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity sec, InMemoryClientRegistrationRepository crr)
throws Exception {
sec.csrf(o -> o.disable()).anonymous(o -> o.disable())
// @formatter:off
.oauth2Login(o -> o.defaultSuccessUrl("/"))
.logout(o -> o.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(crr)))
.authorizeHttpRequests(o -> o.requestMatchers("/public/**").permitAll().anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(getConverter())));
// @formatter:on
return sec.build();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> getConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());
return jwtConverter;
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwtSetUri).build();
}
}

View File

@ -0,0 +1,29 @@
package de.jottyfan.timetrack.config.converter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
/**
*
* @author henkej
*
*/
public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Object o = jwt.getClaims().get("realm_access");
@SuppressWarnings("unchecked")
final Map<String, Object> realmAccess = (Map<String, Object>) o;
Object o2 = realmAccess.get("roles");
@SuppressWarnings("unchecked")
List<String> l = (List<String>) o2;
return l.stream().map(roleName -> "ROLE_" + roleName).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
}

View File

@ -4,13 +4,10 @@ import java.time.Duration;
import java.time.LocalDate;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -19,8 +16,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
import de.jottyfan.timetrack.modules.done.DoneBean;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.done.IDoneService;
import de.jottyfan.timetrack.modules.done.DoneService;
import de.jottyfan.timetrack.modules.done.SummaryBean;
import jakarta.annotation.security.RolesAllowed;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
/**
*
@ -31,15 +31,8 @@ import de.jottyfan.timetrack.modules.done.SummaryBean;
public class IndexController {
private static final Logger LOGGER = LogManager.getLogger(IndexController.class);
private final HttpServletRequest request;
@Autowired
private IDoneService doneService;
@Autowired
public IndexController(HttpServletRequest request) {
this.request = request;
}
private DoneService doneService;
@GetMapping("/logout")
public String getLogout(HttpServletRequest request) throws ServletException {
@ -49,8 +42,8 @@ public class IndexController {
@RolesAllowed("timetrack_user")
@RequestMapping("/")
public String getIndex(@ModelAttribute DoneModel doneModel, Model model) {
String username = doneService.getCurrentUser(request);
public String getIndex(@ModelAttribute DoneModel doneModel, Model model, OAuth2AuthenticationToken token) {
String username = doneService.getCurrentUser(token);
Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file
LocalDate day = LocalDate.now();
List<DoneBean> list = doneService.getList(day, username);

View File

@ -3,13 +3,8 @@ package de.jottyfan.timetrack.modules.contact;
import java.util.Arrays;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -20,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import de.jottyfan.timetrack.db.contact.enums.EnumContacttype;
import jakarta.annotation.security.RolesAllowed;
/**
*
@ -28,22 +24,14 @@ import de.jottyfan.timetrack.db.contact.enums.EnumContacttype;
*/
@Controller
public class ContactController {
private static final Logger LOGGER = LogManager.getLogger(ContactController.class);
private final HttpServletRequest request;
@Autowired
private IContactService contactService;
@Autowired
public ContactController(HttpServletRequest request) {
this.request = request;
}
private ContactService contactService;
@ModelAttribute("currentUser")
@ResponseBody
public String getCurrentUser() {
return contactService.getCurrentUser(request);
public String getCurrentUser(OAuth2AuthenticationToken token) {
return contactService.getCurrentUser(token);
}
@RolesAllowed("timetrack_user")

View File

@ -31,11 +31,11 @@ import de.jottyfan.timetrack.db.contact.tables.records.TContactRecord;
*
*/
@Repository
public class ContactGateway {
private static final Logger LOGGER = LogManager.getLogger(ContactGateway.class);
public class ContactRepository {
private static final Logger LOGGER = LogManager.getLogger(ContactRepository.class);
private final DSLContext jooq;
public ContactGateway(@Autowired DSLContext jooq) throws Exception {
public ContactRepository(@Autowired DSLContext jooq) throws Exception {
this.jooq = jooq;
}

View File

@ -3,13 +3,13 @@ package de.jottyfan.timetrack.modules.contact;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.keycloak.KeycloakSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -20,32 +20,41 @@ import org.springframework.transaction.annotation.Transactional;
*/
@Service
@Transactional(transactionManager = "transactionManager")
public class ContactService implements IContactService {
public class ContactService {
private static final Logger LOGGER = LogManager.getLogger(ContactService.class);
@Autowired
private DSLContext dsl;
@Override
public String getCurrentUser(HttpServletRequest request) {
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
return ksc == null ? "" : ksc.getIdToken().getPreferredUsername();
@Autowired
private OAuth2AuthorizedClientService security;
public String getCurrentUser(OAuth2AuthenticationToken token) {
if (token != null) {
OAuth2AuthorizedClient client = security.loadAuthorizedClient(token.getAuthorizedClientRegistrationId(),
token.getName());
if (client != null) {
return client.getPrincipalName();
} else {
return "client is null";
}
} else {
return "oauth token is null";
}
}
@Override
public List<ContactBean> getList() {
try {
return new ContactGateway(dsl).getAll();
return new ContactRepository(dsl).getAll();
} catch (Exception e) {
LOGGER.error(e);
return new ArrayList<>();
}
}
@Override
public Integer doUpsert(ContactBean bean) {
try {
ContactGateway gw = new ContactGateway(dsl);
ContactRepository gw = new ContactRepository(dsl);
return bean.getPk() == null ? gw.add(bean) : gw.update(bean);
} catch (Exception e) {
LOGGER.error(e);
@ -53,25 +62,22 @@ public class ContactService implements IContactService {
}
}
@Override
public Integer doDelete(Integer pk) {
try {
return new ContactGateway(dsl).delete(pk);
return new ContactRepository(dsl).delete(pk);
} catch (Exception e) {
LOGGER.error(e);
return 0;
}
}
@Override
public Integer getAmount() {
return getList().size();
}
@Override
public ContactBean getBean(Integer id) {
try {
return new ContactGateway(dsl).getBean(id);
return new ContactRepository(dsl).getBean(id);
} catch (Exception e) {
LOGGER.error(e);
return null;

View File

@ -1,19 +0,0 @@
package de.jottyfan.timetrack.modules.contact;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
*
* @author henkej
*
*/
public interface IContactService {
public List<ContactBean> getList();
public Integer doUpsert(ContactBean bean);
public Integer doDelete(Integer pk);
public Integer getAmount();
public String getCurrentUser(HttpServletRequest request);
public ContactBean getBean(Integer id);
}

View File

@ -4,13 +4,9 @@ import java.time.Duration;
import java.time.LocalDate;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -19,6 +15,8 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.annotation.security.RolesAllowed;
/**
*
* @author henkej
@ -26,22 +24,14 @@ import org.springframework.web.bind.annotation.RequestMethod;
*/
@Controller
public class DoneController {
private static final Logger LOGGER = LogManager.getLogger(DoneController.class);
private final HttpServletRequest request;
@Autowired
private IDoneService doneService;
@Autowired
public DoneController(HttpServletRequest request) {
this.request = request;
}
private DoneService doneService;
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/list")
public String getList(@ModelAttribute DoneModel doneModel, Model model) {
String username = doneService.getCurrentUser(request);
public String getList(@ModelAttribute DoneModel doneModel, Model model, OAuth2AuthenticationToken token) {
String username = doneService.getCurrentUser(token);
Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file
LocalDate day = doneModel.getDay();
List<DoneBean> list = doneService.getList(day, username);
@ -61,10 +51,10 @@ public class DoneController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/abort/{day}")
public String abort(@PathVariable String day, Model model) {
public String abort(@PathVariable String day, Model model, OAuth2AuthenticationToken token) {
DoneModel doneModel = new DoneModel();
doneModel.setDayString(day);
return getList(doneModel, model);
return getList(doneModel, model, token);
}
@RolesAllowed("timetrack_user")
@ -99,21 +89,21 @@ public class DoneController {
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute DoneBean bean) {
String username = doneService.getCurrentUser(request);
public String doUpsert(Model model, @ModelAttribute DoneBean bean, OAuth2AuthenticationToken token) {
String username = doneService.getCurrentUser(token);
Integer amount = doneService.doUpsert(bean, username);
DoneModel doneModel = new DoneModel();
doneModel.setDay(bean.getLocalDate());
return amount.equals(1) ? getList(doneModel, model) : toItem(bean.getPk(), model);
return amount.equals(1) ? getList(doneModel, model, token) : toItem(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model) {
public String doDelete(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) {
DoneBean bean = doneService.getBean(id);
Integer amount = doneService.doDelete(id);
DoneModel doneModel = new DoneModel();
doneModel.setDay(bean.getLocalDate());
return amount.equals(1) ? getList(doneModel, model) : toItem(id, model);
return amount.equals(1) ? getList(doneModel, model, token) : toItem(id, model);
}
}

View File

@ -4,13 +4,13 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.keycloak.KeycloakSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -27,20 +27,29 @@ import de.jottyfan.timetrack.modules.note.NoteService;
*/
@Service
@Transactional(transactionManager = "transactionManager")
public class DoneService implements IDoneService {
public class DoneService {
private static final Logger LOGGER = LogManager.getLogger(NoteService.class);
@Autowired
private DSLContext dsl;
@Override
public String getCurrentUser(HttpServletRequest request) {
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request
.getAttribute(KeycloakSecurityContext.class.getName());
return ksc == null ? "" : ksc.getIdToken().getPreferredUsername();
@Autowired
private OAuth2AuthorizedClientService security;
public String getCurrentUser(OAuth2AuthenticationToken token) {
if (token != null) {
OAuth2AuthorizedClient client = security.loadAuthorizedClient(token.getAuthorizedClientRegistrationId(),
token.getName());
if (client != null) {
return client.getPrincipalName();
} else {
return "client is null";
}
} else {
return "oauth token is null";
}
}
@Override
public List<DoneBean> getList(LocalDate day, String username) {
try {
DoneGateway gw = new DoneGateway(dsl);
@ -55,7 +64,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public List<DoneBean> getWeek(LocalDate day, String username) {
try {
DoneGateway gw = new DoneGateway(dsl);
@ -70,8 +78,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public DoneBean getBean(Integer id) {
try {
return new DoneGateway(dsl).getBean(id);
@ -81,7 +87,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public List<VProjectRecord> getProjects(boolean includeNull) {
try {
return new DoneGateway(dsl).getAllProjects(includeNull);
@ -91,7 +96,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public List<VModuleRecord> getModules(boolean includeNull) {
try {
return new DoneGateway(dsl).getAllModules(includeNull);
@ -101,7 +105,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public List<VJobRecord> getJobs(boolean includeNull) {
try {
return new DoneGateway(dsl).getAllJobs(includeNull);
@ -111,7 +114,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public List<VBillingRecord> getBillings(boolean includeNull) {
try {
return new DoneGateway(dsl).getAllBillings(includeNull);
@ -121,7 +123,6 @@ public class DoneService implements IDoneService {
}
}
@Override
public Integer doUpsert(DoneBean bean, String username) {
try {
DoneGateway gw = new DoneGateway(dsl);

View File

@ -1,29 +0,0 @@
package de.jottyfan.timetrack.modules.done;
import java.time.LocalDate;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import de.jottyfan.timetrack.db.done.tables.records.VBillingRecord;
import de.jottyfan.timetrack.db.done.tables.records.VJobRecord;
import de.jottyfan.timetrack.db.done.tables.records.VModuleRecord;
import de.jottyfan.timetrack.db.done.tables.records.VProjectRecord;
/**
*
* @author henkej
*
*/
public interface IDoneService {
public List<DoneBean> getList(LocalDate day, String username);
public List<DoneBean> getWeek(LocalDate day, String username);
public DoneBean getBean(Integer id);
public String getCurrentUser(HttpServletRequest request);
public List<VProjectRecord> getProjects(boolean includeNull);
public List<VModuleRecord> getModules(boolean includeNull);
public List<VJobRecord> getJobs(boolean includeNull);
public List<VBillingRecord> getBillings(boolean includeNull);
public Integer doUpsert(DoneBean bean, String username);
public Integer doDelete(Integer id);
}

View File

@ -1,14 +0,0 @@
package de.jottyfan.timetrack.modules.done.job;
import de.jottyfan.timetrack.db.done.tables.records.TJobRecord;
/**
*
* @author henkej
*
*/
public interface IJobService {
public TJobRecord get(Integer id);
public Integer doUpsert(TJobRecord bean);
public Integer doDelete(Integer id);
}

View File

@ -1,10 +1,7 @@
package de.jottyfan.timetrack.modules.done.job;
import javax.annotation.security.RolesAllowed;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -16,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.db.done.tables.records.TJobRecord;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.DoneModel;
import jakarta.annotation.security.RolesAllowed;
/**
*
@ -24,10 +22,8 @@ import de.jottyfan.timetrack.modules.done.DoneModel;
*/
@Controller
public class JobController {
private static final Logger LOGGER = LogManager.getLogger(JobController.class);
@Autowired
private IJobService jobService;
private JobService jobService;
@Autowired
private DoneController doneController;
@ -42,9 +38,9 @@ public class JobController {
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/job", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute TJobRecord bean) {
public String doUpsert(Model model, @ModelAttribute TJobRecord bean, OAuth2AuthenticationToken token) {
Integer amount = jobService.doUpsert(bean);
return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toJob(bean.getPk(), model);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toJob(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@ -55,8 +51,8 @@ public class JobController {
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/job/{id}")
public String doDeleteJob(@PathVariable Integer id, Model model) {
public String doDeleteJob(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) {
Integer amount = jobService.doDelete(id);
return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toJob(id, model);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toJob(id, model);
}
}

View File

@ -8,18 +8,15 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import de.jottyfan.timetrack.db.done.tables.records.TJobRecord;
import de.jottyfan.timetrack.db.done.tables.records.TModuleRecord;
@Service
@Transactional(transactionManager = "transactionManager")
public class JobService implements IJobService {
public class JobService {
private static final Logger LOGGER = LogManager.getLogger(JobService.class);
@Autowired
private DSLContext dsl;
@Override
public TJobRecord get(Integer id) {
try {
return id == null ? new TJobRecord() : new JobGateway(dsl).get(id);
@ -29,7 +26,6 @@ public class JobService implements IJobService {
}
}
@Override
public Integer doUpsert(TJobRecord bean) {
try {
return new JobGateway(dsl).upsert(bean);
@ -39,7 +35,6 @@ public class JobService implements IJobService {
}
}
@Override
public Integer doDelete(Integer id) {
try {
return new JobGateway(dsl).delete(id);

View File

@ -1,14 +0,0 @@
package de.jottyfan.timetrack.modules.done.module;
import de.jottyfan.timetrack.db.done.tables.records.TModuleRecord;
/**
*
* @author henkej
*
*/
public interface IModuleService {
public TModuleRecord get(Integer id);
public Integer doUpsert(TModuleRecord bean);
public Integer doDelete(Integer id);
}

View File

@ -1,10 +1,11 @@
package de.jottyfan.timetrack.modules.done.module;
import javax.annotation.security.RolesAllowed;
import jakarta.annotation.security.RolesAllowed;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -24,10 +25,9 @@ import de.jottyfan.timetrack.modules.done.DoneModel;
*/
@Controller
public class ModuleController {
private static final Logger LOGGER = LogManager.getLogger(ModuleController.class);
@Autowired
private IModuleService moduleService;
private ModuleService moduleService;
@Autowired
private DoneController doneController;
@ -42,9 +42,9 @@ public class ModuleController {
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/module", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute TModuleRecord bean) {
public String doUpsert(Model model, @ModelAttribute TModuleRecord bean, OAuth2AuthenticationToken token) {
Integer amount = moduleService.doUpsert(bean);
return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toModule(bean.getPk(), model);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toModule(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@ -55,8 +55,8 @@ public class ModuleController {
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/module/{id}")
public String doDeleteModule(@PathVariable Integer id, Model model) {
public String doDeleteModule(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) {
Integer amount = moduleService.doDelete(id);
return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toModule(id, model);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toModule(id, model);
}
}

View File

@ -8,19 +8,15 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import de.jottyfan.timetrack.db.done.tables.records.TModuleRecord;
import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord;
import de.jottyfan.timetrack.modules.done.project.IProjectService;
@Service
@Transactional(transactionManager = "transactionManager")
public class ModuleService implements IModuleService {
public class ModuleService {
private static final Logger LOGGER = LogManager.getLogger(ModuleService.class);
@Autowired
private DSLContext dsl;
@Override
public TModuleRecord get(Integer id) {
try {
return id == null ? new TModuleRecord() : new ModuleGateway(dsl).getModule(id);
@ -30,7 +26,6 @@ public class ModuleService implements IModuleService {
}
}
@Override
public Integer doUpsert(TModuleRecord bean) {
try {
return new ModuleGateway(dsl).upsert(bean);
@ -40,7 +35,6 @@ public class ModuleService implements IModuleService {
}
}
@Override
public Integer doDelete(Integer id) {
try {
return new ModuleGateway(dsl).delete(id);

View File

@ -1,14 +0,0 @@
package de.jottyfan.timetrack.modules.done.project;
import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord;
/**
*
* @author henkej
*
*/
public interface IProjectService {
public TProjectRecord get(Integer id);
public Integer doUpsert(TProjectRecord bean);
public Integer doDelete(Integer id);
}

View File

@ -1,10 +1,7 @@
package de.jottyfan.timetrack.modules.done.project;
import javax.annotation.security.RolesAllowed;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -16,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.DoneModel;
import jakarta.annotation.security.RolesAllowed;
/**
*
@ -24,10 +22,8 @@ import de.jottyfan.timetrack.modules.done.DoneModel;
*/
@Controller
public class ProjectController {
private static final Logger LOGGER = LogManager.getLogger(ProjectController.class);
@Autowired
private IProjectService projectService;
private ProjectService projectService;
@Autowired
private DoneController doneController;
@ -42,9 +38,9 @@ public class ProjectController {
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/project", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute TProjectRecord bean) {
public String doUpsert(Model model, @ModelAttribute TProjectRecord bean, OAuth2AuthenticationToken token) {
Integer amount = projectService.doUpsert(bean);
return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toProject(bean.getPk(), model);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toProject(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@ -55,8 +51,8 @@ public class ProjectController {
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/project/{id}")
public String doDeleteProject(@PathVariable Integer id, Model model) {
public String doDeleteProject(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) {
Integer amount = projectService.doDelete(id);
return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toProject(id, model);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toProject(id, model);
}
}

View File

@ -11,14 +11,12 @@ import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord;
@Service
@Transactional(transactionManager = "transactionManager")
public class ProjectService implements IProjectService {
public class ProjectService {
private static final Logger LOGGER = LogManager.getLogger(ProjectService.class);
@Autowired
private DSLContext dsl;
@Override
public TProjectRecord get(Integer id) {
try {
return id == null ? new TProjectRecord() : new ProjectGateway(dsl).getProject(id);
@ -28,7 +26,6 @@ public class ProjectService implements IProjectService {
}
}
@Override
public Integer doUpsert(TProjectRecord bean) {
try {
return new ProjectGateway(dsl).upsert(bean);
@ -38,7 +35,6 @@ public class ProjectService implements IProjectService {
}
}
@Override
public Integer doDelete(Integer id) {
try {
return new ProjectGateway(dsl).delete(id);

View File

@ -1,19 +0,0 @@
package de.jottyfan.timetrack.modules.note;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
*
* @author henkej
*
*/
public interface INoteService {
public List<NoteBean> getList();
public Integer doUpsert(NoteBean bean);
public Integer doDelete(Integer pk);
public Integer getAmount();
public String getCurrentUser(HttpServletRequest request);
public NoteBean getBean(Integer id);
}

View File

@ -3,11 +3,6 @@ package de.jottyfan.timetrack.modules.note;
import java.util.Arrays;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -19,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.db.note.enums.EnumCategory;
import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
import jakarta.annotation.security.RolesAllowed;
/**
*
@ -27,17 +23,9 @@ import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
*/
@Controller
public class NoteController {
private static final Logger LOGGER = LogManager.getLogger(NoteController.class);
private final HttpServletRequest request;
@Autowired
private INoteService noteService;
@Autowired
public NoteController(HttpServletRequest request) {
this.request = request;
}
private NoteService noteService;
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/list")
@ -79,5 +67,4 @@ public class NoteController {
Integer amount = noteService.doDelete(id);
return amount.equals(1) ? getList(model) : toItem(id, model);
}
}

View File

@ -3,13 +3,13 @@ package de.jottyfan.timetrack.modules.note;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.keycloak.KeycloakSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -20,19 +20,29 @@ import org.springframework.transaction.annotation.Transactional;
*/
@Service
@Transactional(transactionManager = "transactionManager")
public class NoteService implements INoteService {
public class NoteService {
private static final Logger LOGGER = LogManager.getLogger(NoteService.class);
@Autowired
private DSLContext dsl;
@Override
public String getCurrentUser(HttpServletRequest request) {
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
return ksc == null ? "" : ksc.getIdToken().getPreferredUsername();
@Autowired
private OAuth2AuthorizedClientService security;
public String getCurrentUser(OAuth2AuthenticationToken token) {
if (token != null) {
OAuth2AuthorizedClient client = security.loadAuthorizedClient(token.getAuthorizedClientRegistrationId(),
token.getName());
if (client != null) {
return client.getPrincipalName();
} else {
return "client is null";
}
} else {
return "oauth token is null";
}
}
@Override
public List<NoteBean> getList() {
try {
return new NoteGateway(dsl).getAll();
@ -42,7 +52,6 @@ public class NoteService implements INoteService {
}
}
@Override
public Integer doUpsert(NoteBean bean) {
try {
NoteGateway gw = new NoteGateway(dsl);
@ -53,7 +62,6 @@ public class NoteService implements INoteService {
}
}
@Override
public Integer doDelete(Integer pk) {
try {
return new NoteGateway(dsl).delete(pk);
@ -63,12 +71,10 @@ public class NoteService implements INoteService {
}
}
@Override
public Integer getAmount() {
return getList().size();
}
@Override
public NoteBean getBean(Integer id) {
try {
return new NoteGateway(dsl).getBean(id);

View File

@ -1,21 +1,25 @@
# jooq
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/timetrack
spring.datasource.username=timetrack
spring.datasource.password=timetrack
spring.datasource.driver-class-name = org.postgresql.Driver
# todo: export to /etc/timetrack
spring.datasource.url = jdbc:postgresql://localhost:5432/timetrack
spring.datasource.username = timetrack
spring.datasource.password = timetrack
# security
keycloak.url = http://localhost:8080/realms/jottyfan
keycloak.openid.url = ${keycloak.url}/protocol/openid-connect
spring.security.oauth2.client.registration.keycloak.client-id = timetrack
spring.security.oauth2.client.registration.keycloak.scope = openid
spring.security.oauth2.client.registration.keycloak.authorization-grant-type = authorization_code
# todo: export to /etc/timetrack
spring.security.oauth2.client.registration.keycloak.redirect-uri = http://localhost:8888/timetrack/login/oauth2/code/timetrack
spring.security.oauth2.client.provider.keycloak.issuer-uri = ${keycloak.url}
spring.security.oauth2.client.provider.keycloak.authorization-uri = ${keycloak.openid.url}/auth
spring.security.oauth2.client.provider.keycloak.token-uri = ${keycloak.openid.url}/token
spring.security.oauth2.client.provider.keycloak.user-info-uri = ${keycloak.openid.url}/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri = ${keycloak.openid.url}/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username
# application
server.port = 8083
server.servlet.context-path=/timetrack
# keycloak
keycloak.auth-server-url = https://www.jottyfan.de/auth
keycloak.realm = jottyfan
keycloak.resource = timetrack
keycloak.public-client = true
keycloak.security-constraints[0].authRoles[0] = timetrack_user
keycloak.security-constraints[0].securityCollections[0].patterns[0] = /*
#keycloak.credentia
keycloak.use-resource-role-mappings=true
#keycloak.bearer-only=true
server:.port = 8083
server.servlet.context-path = /timetrack

View File

@ -259,8 +259,6 @@ body {
.spanlabel {
display: inline-block;
min-width: 128px;
max-width: 128px;
margin-top: 4px;
margin-bottom: 4px;
}

View File

@ -1,42 +1,47 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/main.html}">
<head>
<title>Timetrack</title>
<title>Timetrack</title>
</head>
<body>
<ul layout:fragment="menu">
</ul>
<main layout:fragment="content">
<div class="card text-dark bg-light" style="width: 256px; margin: 24px">
<div class="card-header"><a class="btn btn-seondary btn-bordered btn-secondaryhover" style="width: 100%" th:href="@{/done/list}">heutige Arbeitszeiten</a></div>
<div class="card text-dark bg-light" style="width: 312px; margin: 24px">
<div class="card-header"><a class="btn btn-seondary btn-bordered btn-secondaryhover" style="width: 100%"
th:href="@{/done/list}">heutige Arbeitszeiten</a></div>
<div class="card-body">
<div>
<span class="spanlabel">Start:</span> <span class="emphgreen" th:text="${sum.start}"></span>
</div>
<div>
<span class="spanlabel">Ende:</span> <span class="emphgreen" th:text="${sum.end}"></span>
</div>
<div>
<span class="spanlabel">Arbeitszeit total:</span> <span class="emphblue" th:text="${sum.total}"></span>
</div>
<div>
<span class="spanlabel">Pausezeit total:</span> <span class="emphorange" th:text="${sum.pause}"></span>
</div>
<div>
<span class="spanlabel">Überstunden:</span> <span class="emphred" th:text="${sum.overdue}"></span>
<div class="container">
<div class="row">
<div class="col-8"><span class="spanlabel">Start:</span></div>
<div class="col-4"><span class="emphgreen" th:text="${sum.start}"></span></div>
<div class="col-8"><span class="spanlabel">Ende:</span></div>
<div class="col-4"><span class="emphgreen" th:text="${sum.end}"></span></div>
<div class="col-8"><span class="spanlabel">Arbeitszeit total:</span></div>
<div class="col-4"><span class="emphblue" th:text="${sum.total}"></span></div>
<div class="col-8"><span class="spanlabel">Pausezeit total:</span></div>
<div class="col-4"><span class="emphorange" th:text="${sum.pause}"></span></div>
<div class="col-8"><span class="spanlabel">Überstunden:</span></div>
<div class="col-4"><span class="emphred" th:text="${sum.overdue}"></span></div>
</div>
</div>
</div>
<div class="card-footer">
<span th:if="${sum.getBillingTime('WP2') != '0,0 h'}"><span class="billing WP2">WP2</span><span
th:text="${sum.getBillingTime('WP2')}" class="distfat"></span></span> <span th:if="${sum.getBillingTime('WP4') != '0,0 h'}"><span
class="billing WP4">WP4</span><span th:text="${sum.getBillingTime('WP4')}" class="distfat"></span></span> <span
th:text="${sum.getBillingTime('WP2')}" class="distfat"></span></span> <span
th:if="${sum.getBillingTime('WP4') != '0,0 h'}"><span class="billing WP4">WP4</span><span
th:text="${sum.getBillingTime('WP4')}" class="distfat"></span></span> <span
th:if="${sum.getBillingTime('WP5') != '0,0 h'}"><span class="billing WP5">WP5</span><span
th:text="${sum.getBillingTime('WP5')}" class="distfat"></span></span> <span th:if="${sum.getBillingTime('TA3') != '0,0 h'}"><span
class="billing TA3">TA3</span><span th:text="${sum.getBillingTime('TA3')}" class="distfat"></span></span> <span class="billing">X</span><span
th:text="${sum.getBillingTime('WP5')}" class="distfat"></span></span> <span
th:if="${sum.getBillingTime('TA3') != '0,0 h'}"><span class="billing TA3">TA3</span><span
th:text="${sum.getBillingTime('TA3')}" class="distfat"></span></span> <span class="billing">X</span><span
th:text="${sum.getBillingTime(null)}" class="distfat"></span>
</div>
</div>
</main>
</body>
</html>