diff --git a/pom.xml b/pom.xml
index 398bad29e..42e55e769 100644
--- a/pom.xml
+++ b/pom.xml
@@ -632,6 +632,11 @@
org.springframeworkspring-websocket
+
+ org.springframework
+ spring-jdbc
+ jar
+ org.springframework.securityspring-security-web
diff --git a/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java b/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java
index fb6abcf19..f754a2aab 100644
--- a/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java
+++ b/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java
@@ -89,7 +89,8 @@ public class BeanConfiguration implements WebMvcConfigurer {
/**
* https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
*/
- private void initDataSource() {
+ @Bean
+ public HikariDataSource dataSource() {
SteveConfiguration.DB dbConfig = CONFIG.getDb();
HikariConfig hc = new HikariConfig();
@@ -111,7 +112,7 @@ private void initDataSource() {
// https://github.com/steve-community/steve/issues/736
hc.setMaxLifetime(580_000);
- dataSource = new HikariDataSource(hc);
+ return new HikariDataSource(hc);
}
/**
@@ -128,7 +129,9 @@ private void initDataSource() {
*/
@Bean
public DSLContext dslContext() {
- initDataSource();
+ if (dataSource == null) {
+ dataSource = dataSource();
+ }
Settings settings = new Settings()
// Normally, the records are "attached" to the Configuration that created (i.e. fetch/insert) them.
diff --git a/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java b/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java
index 31da890be..253003bf7 100644
--- a/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java
+++ b/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java
@@ -18,8 +18,10 @@
*/
package de.rwth.idsg.steve.config;
+import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
+import com.zaxxer.hikari.HikariDataSource;
import de.rwth.idsg.steve.SteveProdCondition;
import de.rwth.idsg.steve.web.api.ApiControllerAdvice;
import lombok.extern.slf4j.Slf4j;
@@ -33,17 +35,10 @@
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.userdetails.User;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.crypto.factory.PasswordEncoderFactories;
-import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
@@ -53,7 +48,11 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.provisioning.JdbcUserDetailsManager;
+import org.springframework.security.provisioning.UserDetailsManager;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* @author Sevket Goekay
@@ -65,6 +64,10 @@
@Conditional(SteveProdCondition.class)
public class SecurityConfiguration {
+ @Autowired
+ private HikariDataSource dataSource;
+
+
/**
* Password encoding changed with spring-security 5.0.0. We either have to use a prefix before the password to
* indicate which actual encoder {@link DelegatingPasswordEncoder} should use [1, 2] or specify the encoder as we do.
@@ -77,17 +80,6 @@ public PasswordEncoder passwordEncoder() {
return CONFIG.getAuth().getPasswordEncoder();
}
- @Bean
- public UserDetailsService userDetailsService() {
- UserDetails webPageUser = User.builder()
- .username(CONFIG.getAuth().getUserName())
- .password(CONFIG.getAuth().getEncodedPassword())
- .roles("ADMIN")
- .build();
-
- return new InMemoryUserDetailsManager(webPageUser);
- }
-
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
final String prefix = CONFIG.getSpringManagerMapping();
@@ -95,12 +87,35 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
return http
.authorizeHttpRequests(
req -> req
- .requestMatchers(
- "/static/**",
- CONFIG.getCxfMapping() + "/**",
- "/WEB-INF/views/**" // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065
- ).permitAll()
+ // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065
+ .requestMatchers(new AntPathRequestMatcher("/static/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher(CONFIG.getCxfMapping() + "/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/WEB-INF/views/**")).permitAll()
+
.requestMatchers(prefix + "/**").hasRole("ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/home")).hasAnyRole("USER", "ADMIN")
+ // webuser
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/webusers")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/webusers" + "/details/**")).hasAnyRole("USER", "ADMIN")
+ // users
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/users")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/users" + "/details/**")).hasAnyRole("USER", "ADMIN")
+ //ocppTags
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/ocppTags")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/ocppTags" + "/details/**")).hasAnyRole("USER", "ADMIN")
+ // chargepoints
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/chargepoints")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/chargepoints" + "/details/**")).hasAnyRole("USER", "ADMIN")
+ // transactions and reservations
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/transactions")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/transactions" + "/details/**")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/reservations")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/reservations" + "/**")).hasRole("ADMIN")
+ // singout and noAccess
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/signout/" + "**")).hasAnyRole("USER", "ADMIN")
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/noAccess/" + "**")).hasAnyRole("USER", "ADMIN")
+ // any other site
+ .requestMatchers(new AntPathRequestMatcher(prefix + "/**")).hasRole("ADMIN")
)
.sessionManagement(
req -> req.invalidSessionUrl(prefix + "/signin")
@@ -111,9 +126,65 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logout(
req -> req.logoutUrl(prefix + "/signout")
)
+ .exceptionHandling(
+ req -> req.accessDeniedPage(prefix + "/noAccess")
+ )
.build();
}
+ /**
+ *
+ * @param auth
+ * @throws Exception
+ */
+ @Autowired
+ public void configureGlobal(AuthenticationManagerBuilder auth)
+ throws Exception {
+ auth.jdbcAuthentication()
+ .passwordEncoder(CONFIG.getAuth().getPasswordEncoder())
+ .dataSource(dataSource)
+ .usersByUsernameQuery("select username,password,enabled from webusers where username = ?")
+ .authoritiesByUsernameQuery("select username,authority from webauthorities where username = ?");
+ }
+
+
+ /**
+ *
+ * @return
+ */
+ @Bean
+ public UserDetailsManager authenticateUsers() {
+ JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
+ // Adapt the SQL-Commands to the correct table names (user -> webuser; authorities -> webauthorities)
+ users.setAuthoritiesByUsernameQuery("select username,authority from webauthorities where username=?");
+ users.setUsersByUsernameQuery("select username,password,enabled from webusers where username=?");
+
+ /* Adding the admin from config-file to the database/webusers
+ --> changing the name in the file will not delete the corresponding webuser in the database!
+ users.setCreateUserSql("insert into webusers (username, password, enabled) values (?,?,?)");
+ users.setCreateAuthoritySql("insert into webauthorities (username, authority) values (?,?)");
+ users.setUserExistsSql("select username from webusers where username = ?");
+ users.setUpdateUserSql("update webusers set password = ?, enabled = ? where username = ?");
+ users.setDeleteUserAuthoritiesSql("delete from webauthorities where username = ?");
+
+ UserDetails localUser = User.builder()
+ .username(CONFIG.getAuth().getUserName())
+ .password(CONFIG.getAuth().getEncodedPassword())
+ .roles("USER")
+ .build();
+
+ if (users.userExists(CONFIG.getAuth().getUserName()))
+ {
+ users.updateUser(localUser);
+ }
+ else
+ {
+ users.createUser(localUser);
+ }
+ */
+ return users;
+ }
+
@Bean
@Order(1)
public SecurityFilterChain apiKeyFilterChain(HttpSecurity http, ObjectMapper objectMapper) throws Exception {
diff --git a/src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java b/src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java
new file mode 100644
index 000000000..a74abb511
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java
@@ -0,0 +1,39 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.repository;
+
+import de.rwth.idsg.steve.repository.dto.WebUser;
+import de.rwth.idsg.steve.web.dto.WebUserForm;
+import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
+
+import java.util.List;
+
+ /**
+ * @author fnkbsi
+ * @since 21.03.2022
+ */
+public interface WebUserRepository {
+ List getOverview(WebUserQueryForm form);
+ WebUser.Details getDetails(String webusername);
+
+ void add(WebUserForm form);
+ void update(WebUserForm form);
+ void delete(String webusername, String role);
+ void delete(String webusername);
+}
diff --git a/src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java b/src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java
new file mode 100644
index 000000000..ae459f43a
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java
@@ -0,0 +1,46 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.repository.dto;
+
+import java.util.List;
+import jooq.steve.db.tables.records.WebauthoritiesRecord;
+import jooq.steve.db.tables.records.WebusersRecord;
+import lombok.Builder;
+import lombok.Getter;
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+public class WebUser {
+
+ @Getter
+ @Builder
+ public static final class Overview {
+ private final Boolean enabled;
+ private final String webusername, roles;
+ }
+
+ @Getter
+ @Builder
+ public static final class Details {
+ private final WebusersRecord webusersRecord;
+ private final List webauthoritiesRecordList;
+ }
+}
diff --git a/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java b/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java
index 1c458b059..81ca960ee 100644
--- a/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java
+++ b/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java
@@ -28,7 +28,7 @@
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.Record2;
-import org.jooq.Record8;
+import org.jooq.Record9;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@@ -39,6 +39,7 @@
import static jooq.steve.db.tables.OcppTag.OCPP_TAG;
import static jooq.steve.db.tables.SchemaVersion.SCHEMA_VERSION;
import static jooq.steve.db.tables.User.USER;
+import static jooq.steve.db.tables.Webusers.WEBUSERS;
import static org.jooq.impl.DSL.max;
import static org.jooq.impl.DSL.select;
@@ -103,7 +104,12 @@ public Statistics getStats() {
.where(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).lessThan(date(yesterdaysNow)))
.asField("heartbeats_earlier");
- Record8 gs =
+ Field numWebUsers =
+ ctx.selectCount()
+ .from(WEBUSERS)
+ .asField("num_webusers");
+
+ Record9 gs =
ctx.select(
numChargeBoxes,
numOcppTags,
@@ -112,7 +118,8 @@ public Statistics getStats() {
numTransactions,
heartbeatsToday,
heartbeatsYesterday,
- heartbeatsEarlier
+ heartbeatsEarlier,
+ numWebUsers
).fetchOne();
return Statistics.builder()
@@ -124,6 +131,7 @@ public Statistics getStats() {
.heartbeatToday(gs.value6())
.heartbeatYesterday(gs.value7())
.heartbeatEarlier(gs.value8())
+ .numWebUsers(gs.value9())
.build();
}
diff --git a/src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java b/src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java
new file mode 100644
index 000000000..0dff010aa
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java
@@ -0,0 +1,279 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.repository.impl;
+
+import de.rwth.idsg.steve.SteveException;
+import de.rwth.idsg.steve.repository.WebUserRepository;
+import de.rwth.idsg.steve.repository.dto.WebUser;
+import de.rwth.idsg.steve.web.dto.WebUserForm;
+import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
+import jooq.steve.db.tables.records.WebauthoritiesRecord;
+import jooq.steve.db.tables.records.WebusersRecord;
+import lombok.extern.slf4j.Slf4j;
+import org.jooq.DSLContext;
+import org.jooq.JoinType;
+import org.jooq.Record3;
+import org.jooq.Result;
+import org.jooq.SelectQuery;
+import org.jooq.exception.DataAccessException;
+import org.jooq.impl.DSL;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+import static de.rwth.idsg.steve.utils.CustomDSL.includes;
+import static jooq.steve.db.tables.Webusers.WEBUSERS;
+import static jooq.steve.db.tables.Webauthorities.WEBAUTHORITIES;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+@Slf4j
+@Repository
+public class WebUserRepositoryImpl implements WebUserRepository {
+
+ @Autowired private DSLContext ctx;
+
+ private PasswordEncoder encoder = new BCryptPasswordEncoder();
+
+ @Override
+ public List getOverview(WebUserQueryForm form) {
+ return getOverviewInternal(form)
+ .map(r -> WebUser.Overview.builder()
+ .webusername(r.value1())
+ .enabled(r.value2())
+ .roles(r.value3())
+ .build()
+ );
+ }
+
+
+ @Override
+ public WebUser.Details getDetails(String webusername) {
+
+ // -------------------------------------------------------------------------
+ // 1. user table
+ // -------------------------------------------------------------------------
+
+ WebusersRecord ur = ctx.selectFrom(WEBUSERS)
+ .where(WEBUSERS.USERNAME.equal(webusername))
+ .fetchOne();
+
+ if (ur == null) {
+ throw new SteveException("There is no user with id '%s'", webusername);
+ }
+
+ // -------------------------------------------------------------------------
+ // 2. address table
+ // -------------------------------------------------------------------------
+
+ List lar = ctx.selectFrom(WEBAUTHORITIES)
+ .where(WEBAUTHORITIES.USERNAME.eq(webusername))
+ .fetch();
+
+ if (lar == null) {
+ throw new SteveException("There is no role for user '%s' defined", webusername);
+ }
+
+ // -------------------------------------------------------------------------
+ // 3. ocpp_tag table
+ // -------------------------------------------------------------------------
+
+ return WebUser.Details.builder()
+ .webusersRecord(ur)
+ .webauthoritiesRecordList(lar)
+ .build();
+ }
+
+ @Override
+ public void add(WebUserForm form) {
+ ctx.transaction(configuration -> {
+ DSLContext ctx = DSL.using(configuration);
+ try {
+ addInternal(ctx, form);
+ } catch (DataAccessException e) {
+ throw new SteveException("Failed to add the user", e);
+ }
+ });
+ }
+
+ @Override
+ public void update(WebUserForm form) {
+ ctx.transaction(configuration -> {
+ DSLContext ctx = DSL.using(configuration);
+ try {
+ updateInternal(ctx, form);
+
+ } catch (DataAccessException e) {
+ throw new SteveException("Failed to update the webuser", e);
+ }
+ });
+ }
+
+ @Override
+ public void delete(String webusername, String role) {
+ ctx.transaction(configuration -> {
+ DSLContext ctx = DSL.using(configuration);
+ try {
+ deleteInternal(ctx, webusername, role);
+
+ } catch (DataAccessException e) {
+ throw new SteveException("Failed to delete the webuser", e);
+ }
+ });
+ }
+
+ @Override
+ public void delete(String webusername) {
+ ctx.transaction(configuration -> {
+ DSLContext ctx = DSL.using(configuration);
+ try {
+ deleteInternal(ctx, webusername);
+ } catch (DataAccessException e) {
+ throw new SteveException("Failed to delete the webuser", e);
+ }
+ });
+ }
+
+ // -------------------------------------------------------------------------
+ // Private helpers
+ // -------------------------------------------------------------------------
+
+ @SuppressWarnings("unchecked")
+ private Result> getOverviewInternal(WebUserQueryForm form) {
+ SelectQuery selectQuery = ctx.selectQuery();
+ selectQuery.addFrom(WEBUSERS);
+ selectQuery.addJoin(WEBAUTHORITIES, JoinType.LEFT_OUTER_JOIN, WEBUSERS.USERNAME.eq(WEBAUTHORITIES.USERNAME));
+ selectQuery.addSelect(
+ WEBUSERS.USERNAME,
+ WEBUSERS.ENABLED,
+ WEBAUTHORITIES.AUTHORITY
+ );
+
+ if (form.isSetWebusername()) {
+ selectQuery.addConditions(WEBUSERS.USERNAME.eq(form.getWebusername()));
+ }
+
+ if (form.isSetEnabled()) {
+ selectQuery.addConditions(WEBUSERS.ENABLED.eq(form.getEnabled()));
+ }
+
+ if (form.isSetRoles()) {
+ String[] roles = form.getRoles().split(";"); //Semicolon seperated String to StringArray
+ for (String role : roles) {
+ selectQuery.addConditions(includes(WEBAUTHORITIES.AUTHORITY, role.strip())); // strip--> No Withspace
+ }
+ }
+
+ return selectQuery.fetch();
+ }
+
+
+ private void addInternal(DSLContext ctx, WebUserForm form) {
+ int count = 0;
+
+ count = ctx.insertInto(WEBUSERS)
+ .set(WEBUSERS.USERNAME, form.getWebusername())
+ .set(WEBUSERS.ENABLED, form.getEnabled())
+ .set(WEBUSERS.PASSWORD, encoder.encode(form.getPassword()))
+ .execute();
+
+ String[] roles = form.getRoles().split(";"); //Semicolon seperated String to StringArray
+ for (String role : roles) {
+ count += ctx.insertInto(WEBAUTHORITIES)
+ .set(WEBAUTHORITIES.USERNAME, form.getWebusername())
+ .set(WEBAUTHORITIES.AUTHORITY, role.strip())
+ .execute();
+ }
+
+ if (count == 0) {
+ throw new SteveException("Failed to insert the user");
+ }
+ }
+
+ private void updateInternal(DSLContext ctx, WebUserForm form) {
+ if (form.getPassword() == null) {
+ ctx.update(WEBUSERS)
+ .set(WEBUSERS.USERNAME, form.getWebusername())
+ .set(WEBUSERS.ENABLED, form.getEnabled())
+ //set Username unnessary until WebUserForm has oldName or the table and the form uses a primary key
+ .where(WEBUSERS.USERNAME.eq(form.getWebusername()))
+ .execute();
+ } else {
+ ctx.update(WEBUSERS)
+ .set(WEBUSERS.USERNAME, form.getWebusername())
+ .set(WEBUSERS.ENABLED, form.getEnabled())
+ .set(WEBUSERS.PASSWORD, encoder.encode(form.getPassword()))
+ //set Username unnessary until WebUserForm has oldName or the table and the form uses a primary key
+ .where(WEBUSERS.USERNAME.eq(form.getWebusername()))
+ .execute();
+ }
+ // delete all Authority entries for this webuser
+ ctx.delete(WEBAUTHORITIES)
+ .where(WEBAUTHORITIES.USERNAME.equal(form.getWebusername()))
+ .execute();
+
+ String[] roles = form.getRoles().split(";"); //Semicolon seperated String to StringArray
+ for (String role : roles) {
+ ctx.insertInto(WEBAUTHORITIES)
+ .set(WEBAUTHORITIES.USERNAME, form.getWebusername())
+ .set(WEBAUTHORITIES.AUTHORITY, role.strip())
+ .execute();
+ }
+ }
+
+ private void deleteInternal(DSLContext ctx, String webusername, String role) {
+
+ ctx.delete(WEBAUTHORITIES)
+ .where(WEBAUTHORITIES.USERNAME.equal(webusername))
+ .and(WEBAUTHORITIES.AUTHORITY.eq(role))
+ .execute();
+
+ boolean isEmpty = ctx.selectFrom(WEBAUTHORITIES)
+ .where(WEBAUTHORITIES.USERNAME.equal(webusername))
+ .fetch().isEmpty();
+
+ if (isEmpty) {
+ ctx.delete(WEBUSERS)
+ .where(WEBUSERS.USERNAME.equal(webusername))
+ .execute();
+ }
+
+ }
+
+ private void deleteInternal(DSLContext ctx, String webusername) {
+
+ Integer count = ctx.selectCount().from(WEBUSERS).execute();
+
+ if (count > 1) { // don't delete the last webuser!
+ ctx.delete(WEBAUTHORITIES)
+ .where(WEBAUTHORITIES.USERNAME.equal(webusername))
+ .execute();
+
+ ctx.delete(WEBUSERS)
+ .where(WEBUSERS.USERNAME.equal(webusername))
+ .execute();
+ }
+ }
+}
diff --git a/src/main/java/de/rwth/idsg/steve/utils/mapper/WebUserFormMapper.java b/src/main/java/de/rwth/idsg/steve/utils/mapper/WebUserFormMapper.java
new file mode 100644
index 000000000..172bb71dc
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/utils/mapper/WebUserFormMapper.java
@@ -0,0 +1,62 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.utils.mapper;
+
+import de.rwth.idsg.steve.repository.dto.WebUser;
+import de.rwth.idsg.steve.web.dto.WebUserForm;
+import java.util.List;
+import jooq.steve.db.tables.records.WebusersRecord;
+import jooq.steve.db.tables.records.WebauthoritiesRecord;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class WebUserFormMapper {
+
+ public static WebUserForm toForm(WebUser.Details details) {
+ WebusersRecord webuserRecord = details.getWebusersRecord();
+ List authRecords = details.getWebauthoritiesRecordList();
+
+ WebUserForm form = new WebUserForm();
+ form.setWebusername(webuserRecord.getUsername());
+ form.setEnabled(webuserRecord.getEnabled());
+
+ form.setRoles(rolesStr(authRecords));
+
+ return form;
+ }
+
+ private static String rolesStr(List authRecords) {
+ String roles = "";
+
+ for (WebauthoritiesRecord ar : authRecords) {
+ roles = roles + ar.getAuthority() + "; ";
+ }
+ roles = roles.strip();
+ if (!roles.isBlank()) { //(roles.endsWith(";"))
+ roles = roles.substring(0, roles.length() - 1);
+ }
+
+ return roles;
+ }
+}
diff --git a/src/main/java/de/rwth/idsg/steve/web/controller/NoAccessController.java b/src/main/java/de/rwth/idsg/steve/web/controller/NoAccessController.java
new file mode 100644
index 000000000..3cecc0906
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/web/controller/NoAccessController.java
@@ -0,0 +1,45 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.web.controller;
+
+
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+@Controller
+@RequestMapping(value = "/manager/noAccess")
+public class NoAccessController {
+
+ // -------------------------------------------------------------------------
+ // HTTP methods
+ // -------------------------------------------------------------------------
+
+ @RequestMapping()
+ public String accessDenied() {
+ return "noAccess";
+ }
+
+}
diff --git a/src/main/java/de/rwth/idsg/steve/web/controller/WebUsersController.java b/src/main/java/de/rwth/idsg/steve/web/controller/WebUsersController.java
new file mode 100644
index 000000000..2345a8bed
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/web/controller/WebUsersController.java
@@ -0,0 +1,183 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.web.controller;
+
+
+import de.rwth.idsg.steve.repository.WebUserRepository;
+import de.rwth.idsg.steve.repository.dto.WebUser;
+import de.rwth.idsg.steve.utils.mapper.WebUserFormMapper;
+import de.rwth.idsg.steve.web.dto.WebUserForm;
+import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import jakarta.validation.Valid;
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+@Controller
+@RequestMapping(value = "/manager/webusers")
+public class WebUsersController {
+
+ @Autowired private WebUserRepository webuserRepository;
+
+ private static final String PARAMS = "params";
+
+ // -------------------------------------------------------------------------
+ // Paths
+ // -------------------------------------------------------------------------
+
+ private static final String QUERY_PATH = "/query";
+
+ private static final String DETAILS_PATH = "/details/{webusername}";
+ private static final String DELETE_PATH = "/delete/{webusername}/{role}";
+ private static final String DELETE_ALL_PATH = "/delete/{webusername}";
+ private static final String UPDATE_PATH = "/update";
+ private static final String ADD_PATH = "/add";
+
+ // -------------------------------------------------------------------------
+ // HTTP methods
+ // -------------------------------------------------------------------------
+
+ @RequestMapping(method = RequestMethod.GET)
+ public String getOverview(Model model) {
+ initList(model, new WebUserQueryForm());
+ return "data-man/webusers";
+ }
+
+ @RequestMapping(value = QUERY_PATH, method = RequestMethod.GET)
+ public String getQuery(@ModelAttribute(PARAMS) WebUserQueryForm params, Model model) {
+ initList(model, params);
+ return "data-man/webusers";
+ }
+
+ private void initList(Model model, WebUserQueryForm params) {
+ model.addAttribute(PARAMS, params);
+ model.addAttribute("webuserList", webuserRepository.getOverview(params));
+ }
+
+ @RequestMapping(value = DETAILS_PATH, method = RequestMethod.GET)
+ public String getDetails(@PathVariable("webusername") String webusername, Model model) {
+ WebUser.Details details = webuserRepository.getDetails(webusername);
+ WebUserForm form = WebUserFormMapper.toForm(details);
+
+ model.addAttribute("webuserForm", form);
+ return "data-man/webuserDetails";
+ }
+
+ @RequestMapping(value = ADD_PATH, method = RequestMethod.GET)
+ public String addGet(Model model) {
+ model.addAttribute("webuserForm", new WebUserForm());
+ return "data-man/webuserAdd";
+ }
+
+ @RequestMapping(params = "add", value = ADD_PATH, method = RequestMethod.POST)
+ public String addPost(@Valid @ModelAttribute("webuserForm") WebUserForm webuserForm,
+ BindingResult result, Model model) {
+ if (result.hasErrors()) {
+ return "data-man/webuserAdd";
+ }
+
+ // password is Null, Blank, Empty or less than 8 Characters then don't add and show an Error
+ if (webuserForm.getPassword() == null) {
+ webuserForm.setPwerror(Boolean.TRUE);
+ return "data-man/webuserAdd";
+
+ }
+
+ if ((webuserForm.getPassword().length() < 8) | webuserForm.getPassword().isBlank())
+ /* | webuserForm.getPassword().isEmpty() in isBlank included */ {
+ webuserForm.setPwerror(Boolean.TRUE);
+ return "data-man/webuserAdd";
+ }
+
+ // Compare both the password inputs
+ if (!webuserForm.getPassword().equals(webuserForm.getPasswordComparison())) {
+ webuserForm.setPwerror(Boolean.TRUE);
+ return "data-man/webuserAdd";
+ }
+
+ webuserForm.setPwerror(Boolean.FALSE);
+
+ webuserRepository.add(webuserForm);
+ return toOverview();
+ }
+
+ @RequestMapping(params = "update", value = UPDATE_PATH, method = RequestMethod.POST)
+ public String update(@Valid @ModelAttribute("webuserForm") WebUserForm webuserForm,
+ BindingResult result, Model model) {
+ if (result.hasErrors()) {
+ return "data-man/webuserDetails";
+ }
+
+ if (webuserForm.getPassword() != null) {
+ if (!webuserForm.getPassword().equals(webuserForm.getPasswordComparison())) {
+ webuserForm.setPwerror(Boolean.TRUE);
+ return "data-man/webuserDetails";
+ }
+ // password is Blank or less than 8 Characters then don't update and show an Error
+ // --> WebUserRepositoryImpl: Null and Empty update without updating the password
+ if (webuserForm.getPassword().isBlank() | webuserForm.getPassword().length() < 8) {
+ webuserForm.setPwerror(Boolean.TRUE);
+ return "data-man/webuserDetails";
+ }
+ }
+
+ webuserRepository.update(webuserForm);
+ return toOverview();
+ }
+
+ @RequestMapping(value = DELETE_PATH, method = RequestMethod.POST)
+ public String delete(@PathVariable("webusername") String webusername, @PathVariable("role") String role) {
+ webuserRepository.delete(webusername, role);
+ return toOverview();
+ }
+
+ @RequestMapping(value = DELETE_ALL_PATH, method = RequestMethod.POST)
+ public String delete(@PathVariable("webusername") String webusername) {
+ webuserRepository.delete(webusername);
+ return toOverview();
+ }
+
+ // -------------------------------------------------------------------------
+ // Back to Overview
+ // -------------------------------------------------------------------------
+
+ @RequestMapping(params = "backToOverview", value = ADD_PATH, method = RequestMethod.POST)
+ public String addBackToOverview() {
+ return toOverview();
+ }
+
+ @RequestMapping(params = "backToOverview", value = UPDATE_PATH, method = RequestMethod.POST)
+ public String updateBackToOverview() {
+ return toOverview();
+ }
+
+ private String toOverview() {
+ return "redirect:/manager/webusers";
+ }
+}
diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java b/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java
index 9f2293c89..48eef3408 100644
--- a/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java
+++ b/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java
@@ -37,7 +37,9 @@ public final class Statistics {
// Number of chargeboxes, ocppTags, users, reservations, transactions
private final Integer numChargeBoxes, numOcppTags, numUsers, numReservations, numTransactions,
// Received heartbeats
- heartbeatToday, heartbeatYesterday, heartbeatEarlier;
+ heartbeatToday, heartbeatYesterday, heartbeatEarlier,
+ //WebUser
+ numWebUsers;
// Number of connected WebSocket/JSON chargeboxes
@Setter private int numOcpp12JChargeBoxes, numOcpp15JChargeBoxes, numOcpp16JChargeBoxes;
diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/WebUserForm.java b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserForm.java
new file mode 100644
index 000000000..5f2beb974
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserForm.java
@@ -0,0 +1,45 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.web.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+@Getter
+@Setter
+public class WebUserForm {
+
+ // Internal database id
+ private Boolean enabled;
+
+ private String webusername;
+
+ private String password;
+
+ private String passwordComparison;
+
+ private String roles;
+
+ private Boolean pwerror;
+}
diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/WebUserQueryForm.java b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserQueryForm.java
new file mode 100644
index 000000000..d8e3b5cbb
--- /dev/null
+++ b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserQueryForm.java
@@ -0,0 +1,53 @@
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2023 SteVe Community Team
+/*
+ * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ * Copyright (C) 2013-2024 SteVe Community Team
+ * All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package de.rwth.idsg.steve.web.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+ /**
+ * @author fnkbsi
+ * @since 01.04.2022
+ */
+@Getter
+@Setter
+public class WebUserQueryForm {
+
+ private Boolean enabled;
+
+ // Free text input
+ private String webusername;
+ private String roles;
+
+ public boolean isSetWebusername() {
+ return webusername != null;
+ }
+
+ public boolean isSetRoles() {
+ return roles != null;
+ }
+
+ public boolean isSetEnabled() {
+ return enabled != null;
+ }
+
+}
diff --git a/src/main/resources/db/migration/V1_0_6__update.sql b/src/main/resources/db/migration/V1_0_6__update.sql
new file mode 100644
index 000000000..1a5cf08df
--- /dev/null
+++ b/src/main/resources/db/migration/V1_0_6__update.sql
@@ -0,0 +1,29 @@
+-- adding tables for multi users access
+
+CREATE TABLE `webusers` (
+ `username` varchar(50) COLLATE utf8mb3_unicode_ci NOT NULL,
+ `password` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
+ `enabled` tinyint(1) NOT NULL,
+ PRIMARY KEY (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
+
+CREATE TABLE `webauthorities` (
+ `username` varchar(50) COLLATE utf8mb3_unicode_ci NOT NULL,
+ `authority` varchar(50) COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT 'ROLE_USER',
+ UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
+ CONSTRAINT `authorities_ibfk_1` FOREIGN KEY (`username`) REFERENCES `webusers` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
+
+CREATE UNIQUE INDEX ix_auth_username
+ on webauthorities (username,authority);
+
+-- Insert a user = admin with the password = pass. Change password after installing!
+INSERT INTO webusers (username, password, enabled)
+ values ('admin',
+ '$2a$10$.Rxx4JnuX8OGJTIOCXn76euuB3dIGHHrkX9tswYt9ECKjAGyms30W',
+ 1);
+
+INSERT INTO webauthorities (username, authority)
+ values ('admin', 'ROLE_ADMIN');
+
+
\ No newline at end of file
diff --git a/src/main/resources/webapp/WEB-INF/views/00-header.jsp b/src/main/resources/webapp/WEB-INF/views/00-header.jsp
index fcccd7e38..b64be4f46 100644
--- a/src/main/resources/webapp/WEB-INF/views/00-header.jsp
+++ b/src/main/resources/webapp/WEB-INF/views/00-header.jsp
@@ -27,8 +27,10 @@
<%@ include file="00-context.jsp" %>
-
+
+
+
@@ -45,7 +47,7 @@
diff --git a/src/main/resources/webapp/WEB-INF/views/data-man/webuserAdd.jsp b/src/main/resources/webapp/WEB-INF/views/data-man/webuserAdd.jsp
new file mode 100644
index 000000000..a86b60dd1
--- /dev/null
+++ b/src/main/resources/webapp/WEB-INF/views/data-man/webuserAdd.jsp
@@ -0,0 +1,72 @@
+<%--
+
+ SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ Copyright (C) 2013-2023 SteVe Community Team
+ All Rights Reserved.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+--%>
+<%@ include file="../00-header.jsp" %>
+
+
+
+ Error while trying to add a webuser:
+
+
+
${error.defaultMessage}
+
+
+
+
+
+
The password is to short or the password input is not identical.
+
+
+Add Webuser
+
+
+
New Webuser
+
+
Webusername:
+
Password:
+
Password confirmation:
+
Roles:
+
+
+
+
+
Enabled:
true
+
+
+
+
+
+
+
+
+
+
+
+
+<%@ include file="../00-footer.jsp" %>
\ No newline at end of file
diff --git a/src/main/resources/webapp/WEB-INF/views/data-man/webuserDetails.jsp b/src/main/resources/webapp/WEB-INF/views/data-man/webuserDetails.jsp
new file mode 100644
index 000000000..68b5f515e
--- /dev/null
+++ b/src/main/resources/webapp/WEB-INF/views/data-man/webuserDetails.jsp
@@ -0,0 +1,80 @@
+<%--
+
+ SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ Copyright (C) 2013-2023 SteVe Community Team
+ All Rights Reserved.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+--%>
+<%@ include file="../00-header.jsp" %>
+
+
+ Error while trying to update a charge point:
+
+
+
${error.defaultMessage}
+
+
+
+
+
+
The password is to short or the password input is not identical.
+
+
+ Webuser Details
+
+
+
Webuser
+
+
Username:
${webusername}
+
Password:
+
Password confirmation:
+
Roles:
+
+
+
+
+
+
Enabled:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<%@ include file="../00-footer.jsp" %>
+
diff --git a/src/main/resources/webapp/WEB-INF/views/data-man/webusers.jsp b/src/main/resources/webapp/WEB-INF/views/data-man/webusers.jsp
new file mode 100644
index 000000000..eedf69bbd
--- /dev/null
+++ b/src/main/resources/webapp/WEB-INF/views/data-man/webusers.jsp
@@ -0,0 +1,72 @@
+<%--
+
+ SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
+ Copyright (C) 2013-2023 SteVe Community Team
+ All Rights Reserved.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+--%>
+<%@ include file="../00-header.jsp" %>
+
+
+<%@ include file="00-footer.jsp" %>
\ No newline at end of file
diff --git a/src/main/resources/webapp/WEB-INF/views/signin.jsp b/src/main/resources/webapp/WEB-INF/views/signin.jsp
index 27aa43993..5e41a3563 100644
--- a/src/main/resources/webapp/WEB-INF/views/signin.jsp
+++ b/src/main/resources/webapp/WEB-INF/views/signin.jsp
@@ -24,16 +24,18 @@
<%@ include file="00-context.jsp" %>
-
+
+
+
-
+
SteVe - Steckdosenverwaltung