1 Commits

Author SHA1 Message Date
mbremer
fd28303eb0 get holidays from external service 2021-07-31 14:48:09 +02:00
32 changed files with 153 additions and 606 deletions

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Matthias Bremer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -44,9 +44,4 @@ target/buerokalender-1.1.0-runner
Push to docker.io:
```shell script
./mvnw clean package -DskipTests -Dquarkus.container-image.push=true
```
## Set up
Initial Admin-User $BASE_URL/user/init
```

View File

@@ -1,7 +0,0 @@
# TODO
- [ ] AJAX via unpoly
- [ ] Feiertage-WS mit Cache
- [ ] REST-API via API-Key
- [ ] Verkehrsmodul via Google-Maps: E-Mail, Telegram
- [ ] CalDAV-Sync

12
pom.xml
View File

@@ -14,7 +14,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>2.2.2.Final</quarkus.platform.version>
<quarkus.platform.version>2.0.0.Final</quarkus.platform.version>
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
</properties>
<dependencyManagement>
@@ -75,7 +75,15 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
<artifactId>quarkus-rest-client-jsonb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonp</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>

View File

@@ -1,7 +1,5 @@
package de.mbremer.extension;
import de.mbremer.room.Room;
import de.mbremer.secutity.User;
import io.quarkus.qute.TemplateExtension;
import java.time.LocalDate;
@@ -26,8 +24,4 @@ public class CommonExtensions {
public static String rightPad(String str, int length) {
return String.format("%1$-" + length + "s", str);
}
public static String selectedIfIn(User user, Room room) {
return user !=null && user.getRoom() != null && user.getRoom().getName().equals(room.getName()) ? "selected" : "";
}
}

View File

@@ -0,0 +1,23 @@
package de.mbremer.holiday;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.json.JsonObject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
/**
* curl -H "X-DFA-Token: dfa" -X POST https://deutsche-feiertage-api.de/api/v1/2018-10-03
*/
@Path("/api/v1")
@RegisterRestClient
public interface HolidayRestClient {
/**
* @param date yyyy-MM-DD
*/
@POST
@Path("/{date}")
@Produces(MediaType.APPLICATION_JSON)
JsonObject getHoliday(@PathParam("date") String date, @HeaderParam("X-DFA-Token") String token);
}

View File

@@ -0,0 +1,32 @@
package de.mbremer.holiday;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.JsonObject;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@ApplicationScoped
public class HolidayService {
private static final String TOKEN = "dfa";
@Inject
@RestClient
HolidayRestClient holidayRestClient;
/**
* @return Holiday name or null if it's not a holiday;
*/
public String getHolidayNiedersachsen(LocalDate date) {
// TODO Token via Filter
// TODO Fehlertolerant
// TODO Caching
// TODO Test via Wiremock
JsonObject holiday = holidayRestClient.getHoliday(date.format(DateTimeFormatter.ISO_DATE), TOKEN);
return holiday.getBoolean("result", false) && holiday.getJsonObject("holiday").getJsonObject("regions").getBoolean("ni", false) ?
holiday.getJsonObject("holiday").getString("name") : null;
}
}

View File

@@ -1,27 +0,0 @@
package de.mbremer.image;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Image extends PanacheEntity {
@NotBlank
@Column(nullable = false)
private String name;
@NotNull
@Column(nullable = false)
private byte[] data;
}

View File

@@ -1,57 +0,0 @@
package de.mbremer.image;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.NoResultException;
import javax.transaction.Transactional;
import javax.validation.ValidationException;
@ApplicationScoped
public class ImageService {
private static final String ROOMPLAN_NAME = "roomplan";
private static final int MAX_PLANSIZE_IN_BYTES = 1000 * 1024;
@Inject
Logger log;
public byte[] getRoomplan() {
try {
Image image = Image.find("name", ROOMPLAN_NAME).singleResult();
return image.getData();
} catch (NoResultException e) {
return new byte[0];
}
}
/**
*
* @throws ValidationException
*/
private void validate(byte[] data) {
if (data.length > MAX_PLANSIZE_IN_BYTES) {
throw new ValidationException("Bild ist zu groß: " + data.length + " Bytes. Maximal " + MAX_PLANSIZE_IN_BYTES + " Bytes " +
"erlaubt.");
}
}
/**
*
* @throws ValidationException
*/
@Transactional
public void persistRoomplan(byte[] data) {
validate(data);
boolean exists = Image.count("name", ROOMPLAN_NAME) > 0;
log.info("Persist Roomplan:" + data.length + " Bytes" );
if (exists) {
Image.update("data=?1 where name=?2", data, ROOMPLAN_NAME);
} else {
Image roomplan = new Image(ROOMPLAN_NAME, data);
roomplan.persist();
}
}
}

View File

@@ -1,6 +1,5 @@
package de.mbremer.kalender;
import de.mbremer.room.Room;
import de.mbremer.secutity.User;
import de.mbremer.secutity.UserService;
import io.quarkus.qute.Location;
@@ -10,6 +9,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.NoResultException;
import javax.transaction.Transactional;
@@ -22,6 +22,7 @@ import java.util.stream.Collectors;
@Path("/kalender")
@RolesAllowed({"USER", "ADMIN"})
@ApplicationScoped
public class KalenderResource {
@Inject
@@ -42,10 +43,12 @@ public class KalenderResource {
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance kalender(@QueryParam("offset") @DefaultValue("0") int offsetInWeeks) {
List<KalenderTag> week = kalenderService.getWeek(offsetInWeeks);
return kalenderpage
.data("today", LocalDate.now())
.data("offset", offsetInWeeks)
.data("week", kalenderService.getWeek(offsetInWeeks));
.data("week", week);
}
private List<Event> getEvents(int offsetInWeeks) {
@@ -65,24 +68,24 @@ public class KalenderResource {
LocalDate dayParsed = LocalDate.parse(day);
User currentUser = userService.getCurrentUser();
Room room = currentUser.getRoom();
try {
KalenderTag tag = KalenderTag.findByDayAndRoomId(dayParsed, room == null ? null : room.id);
KalenderTag tag = KalenderTag.find("day", dayParsed).singleResult();
if (kalenderForm.isInOffice() && tag.getInOffice() == null) {
tag.setInOffice(currentUser);
} else if (!kalenderForm.isInOffice() && currentUser.equals(tag.getInOffice())) {
tag.setInOffice(null);
}
} catch (NoResultException e) {
KalenderTag tag = new KalenderTag(dayParsed, room);
KalenderTag tag = new KalenderTag(dayParsed);
tag.setInOffice(currentUser);
tag.setRoom(room);
tag.persist();
}
return kalender(offsetInWeeks);
}
// Exports
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("export/txt")

View File

@@ -1,12 +1,11 @@
package de.mbremer.kalender;
import de.mbremer.room.Room;
import de.mbremer.holiday.HolidayService;
import de.mbremer.secutity.User;
import de.mbremer.secutity.UserService;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.NoResultException;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
@@ -14,25 +13,24 @@ import java.util.stream.Stream;
@ApplicationScoped
public class KalenderService {
@Inject
UserService userService;
@Inject
HolidayService holidayService;
public List<KalenderTag> getWeek(int offsetInWeeks) {
List<KalenderTag> getWeek(int offsetInWeeks) {
User currentUser = userService.getCurrentUser();
Room room = currentUser.getRoom();
LocalDate today = LocalDate.now();
LocalDate montag = today.minusDays(today.getDayOfWeek().getValue() - 1).plusDays(7 * offsetInWeeks);
return Stream.iterate(0, i -> i < 5, i -> ++i)
.map(d -> {
LocalDate day = montag.plusDays(d);
KalenderTag tag;
try {
tag = KalenderTag.findByDayAndRoomId(day, room == null ? null : room.id);
} catch (NoResultException e) {
tag = new KalenderTag(day, room);
}
KalenderTag tag = (KalenderTag) KalenderTag.find("day", day).singleResultOptional().orElse(new KalenderTag(day));
tag.setCurrentUserInOffice(currentUser.equals(tag.getInOffice()));
tag.setToday(LocalDate.now().equals(day));
tag.setHolidayname(holidayService.getHolidayNiedersachsen(day));
return tag;
})
.collect(Collectors.toList());

View File

@@ -1,10 +1,9 @@
package de.mbremer.kalender;
import de.mbremer.room.Room;
import de.mbremer.secutity.User;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
@@ -14,31 +13,29 @@ import java.time.LocalDate;
@Getter
@Setter
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
public class KalenderTag extends PanacheEntity {
private LocalDate day;
@OneToOne
private User inOffice;
@OneToOne
private Room room;
@Transient
private boolean currentUserInOffice;
@Transient
private boolean today;
@Transient
private String holidayname;
public KalenderTag(LocalDate day, Room room) {
public KalenderTag(LocalDate day) {
this.day = day;
this.room = room;
}
/**
* @throws javax.persistence.NoResultException
*/
public static KalenderTag findByDayAndRoomId(LocalDate day, Long roomId) {
return roomId == null ?
(KalenderTag) KalenderTag.find("day = ?1 and room_id is null", day).singleResult() :
(KalenderTag) KalenderTag.find("day = ?1 and room_id = ?2", day, roomId).singleResult();
public boolean istHoliday() {
return holidayname != null;
}
public boolean isPast() {
return day.isBefore(LocalDate.now());
}
}

View File

@@ -1,22 +0,0 @@
package de.mbremer.room;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Room extends PanacheEntity {
@NotBlank
@Column(nullable = false)
private String name;
}

View File

@@ -1,13 +0,0 @@
package de.mbremer.room;
import org.jboss.resteasy.annotations.providers.multipart.PartType;
import javax.ws.rs.FormParam;
import javax.ws.rs.core.MediaType;
import java.io.InputStream;
public class RoomFileForm {
@FormParam("file")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public InputStream file;
}

View File

@@ -1,11 +0,0 @@
package de.mbremer.room;
import javax.ws.rs.FormParam;
public class RoomForm {
public @FormParam("name") String name;
public Room getRoom() {
return new Room(name);
}
}

View File

@@ -1,120 +0,0 @@
package de.mbremer.room;
import de.mbremer.image.ImageService;
import de.mbremer.kalender.KalenderTag;
import de.mbremer.secutity.User;
import de.mbremer.secutity.UserService;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.security.identity.SecurityIdentity;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.Cache;
import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.NoResultException;
import javax.transaction.Transactional;
import javax.validation.ValidationException;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import static io.quarkus.panache.common.Sort.ascending;
@Path("/room")
@ApplicationScoped
@RolesAllowed({"USER", "ADMIN"})
public class RoomResource {
@Inject
Logger log;
@Inject
SecurityIdentity identity;
@Inject
ImageService imageService;
@Inject
UserService userService;
@Inject
Template room;
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getRoom() {
Room currentRoom = userService.getCurrentUser().getRoom();
return room
.data("is_admin", identity.hasRole("ADMIN"))
.data("rooms", Room.listAll(ascending("name")))
.data("current_room", currentRoom == null ? "" : currentRoom.getName());
}
@POST
@Path("/plan")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
@RolesAllowed({"ADMIN"})
public TemplateInstance uploadRoomplan(@MultipartForm RoomFileForm roomFileForm) throws IOException {
byte[] newRoomPlan = roomFileForm.file.readAllBytes();
TemplateInstance room = getRoom();
try {
imageService.persistRoomplan(newRoomPlan);
} catch (ValidationException e) {
room.data("error", e.getMessage());
}
return room;
}
@GET
@Path("/plan")
@Cache(maxAge = 3600) // 1H
@Produces("image/*")
public Response getImage(@PathParam("image") String image) {
byte[] roomplan = imageService.getRoomplan();
return roomplan.length == 0 ? Response.noContent().build() : Response.ok(roomplan).build();
}
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
@Transactional
@Path("/new")
public TemplateInstance add(@MultipartForm RoomForm roomForm) {
Room room = roomForm.getRoom();
if (Room.count("name", room.getName()) > 0) {
return getRoom().data("error", "Der Raum " + room.getName() + " ist bereits vorhanden");
}
room.persist();
if (Room.count() == 1) {
log.info("Migriere alle KalenderTage ohne Rooom zu " + room.getName());
KalenderTag.find("room is null").stream()
.map(k -> (KalenderTag) k).forEach(k -> k.setRoom(room));
}
return getRoom().data("info", "Raum " + room.getName() + " angelegt.");
}
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
@Transactional
@Path("/user")
public TemplateInstance setRoom(@FormParam("room") String roomName) {
Room room;
try {
room = Room.find("name", roomName).singleResult();
} catch (NoResultException e) {
return getRoom().data("error", "Raum " + roomName + " existiert nicht.");
}
User user = userService.getCurrentUser();
user.setRoom(room);
log.info("Setze Raum " + roomName + " für User " + user.getUsername());
return getRoom();
}
}

View File

@@ -1,10 +1,14 @@
package de.mbremer.secutity;
import de.mbremer.room.Room;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.security.jpa.*;
import lombok.*;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
@@ -27,36 +31,27 @@ public class User extends PanacheEntity {
@Column(nullable = false)
private String password;
@OneToOne
@Getter
@Setter
private Room room;
/**
* ADMIN or USER.
*/
@Roles
@Getter
@Setter
@Column(nullable = false)
private String role = "USER";
public void setPassword(String password) {
public User setPassword(String password) {
this.password = BcryptUtil.bcryptHash(password);
return this;
}
public void setUsername(String username) {
public User setUsername(String username) {
this.username = username == null ? null : username.trim();
return this;
}
public void setRole(String role) {
public User setRole(String role) {
this.role = role == null ? role : role.toUpperCase();
}
public boolean hasRoleAdmin() {
return "ADMIN".equals(role);
}
public boolean hasRoleUser() {
return "USER".equals(role);
return this;
}
}

View File

@@ -1,28 +1,15 @@
package de.mbremer.secutity;
import de.mbremer.room.Room;
import javax.ws.rs.FormParam;
public class UserForm {
public @FormParam("username")
String username;
public @FormParam("password")
String password;
public @FormParam("passwordVerify")
String passwordVerify;
public @FormParam("room")
String room;
public @FormParam("role")
String role;
public @FormParam("username") String username;
public @FormParam("password") String password;
public @FormParam("passwordVerify") String passwordVerify;
public @FormParam("role") String role;
public User getUser() {
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setRole(role);
user.setRoom((Room) Room.find("name", room).singleResultOptional().orElse(null));
return user;
return new User().setUsername(username).setPassword(password).setRole(role);
}
public boolean verifyPassword() {

View File

@@ -1,7 +1,5 @@
package de.mbremer.secutity;
import de.mbremer.room.Room;
import io.quarkus.panache.common.Sort;
import io.quarkus.qute.Location;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
@@ -11,6 +9,7 @@ import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.*;
@@ -23,6 +22,7 @@ import static de.mbremer.secutity.Role.ADMIN;
@Path("/user")
@RolesAllowed("ADMIN")
@ApplicationScoped
public class UserResource {
@Inject
@@ -30,8 +30,6 @@ public class UserResource {
@Inject
SecurityIdentity identity;
@Inject
UserService userService;
@Inject
Template userinit;
@Inject
@Location("user.html")
@@ -41,17 +39,10 @@ public class UserResource {
@Produces(MediaType.TEXT_HTML)
@RolesAllowed({"USER", "ADMIN"})
public TemplateInstance getUser() {
TemplateInstance templateInstance = userTemplate
.data("current_user", User.find("username", identity.getPrincipal().getName()).singleResult());
if (identity.hasRole("ADMIN")) {
templateInstance
.data("is_admin", true)
.data("users", User.listAll(Sort.by("username")))
.data("rooms", Room.listAll(Sort.by("name")));
}
return templateInstance;
return userTemplate
.data("user_count", User.count())
.data("current_username", identity.getPrincipal().getName())
.data("is_admin", identity.hasRole("ADMIN"));
}
@POST
@@ -110,38 +101,6 @@ public class UserResource {
User user = userForm.getUser();
user.persist();
return getUser().data("info", "User angelegt");
}
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
@Transactional
@Path("/password")
@RolesAllowed({"USER", "ADMIN"})
public TemplateInstance changePassword(@MultipartForm UserForm userForm) {
if (!userForm.verifyPassword()) {
return getUser().data("error", "Das Passwort ist zu kurz oder stimmt nicht mit der Wiederholung überein.");
}
userService.getCurrentUser().setPassword(userForm.password);
return getUser().data("info", "Passwort aktualisiert");
}
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
@Transactional
@Path("/update")
public TemplateInstance update(@MultipartForm UserForm userForm) {
log.info("update");
Room room = Room.find("name", userForm.room).singleResult();
log.info("set room " + room.getName());
User user = User.find("username", userForm.username).singleResult();
user.setRoom(room);
return getUser();
return getUser().data("info", "User angelegt.");
}
}

View File

@@ -2,15 +2,17 @@ package de.mbremer.secutity;
import io.quarkus.security.identity.SecurityIdentity;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
@ApplicationScoped
@RequestScoped
public class UserService {
@Inject
SecurityIdentity identity;
/**
* Returns the user currently logged in.
*/
public User getCurrentUser() {
return User.find("username", identity.getPrincipal().getName()).singleResult();
}

View File

@@ -2,23 +2,25 @@
%prod.quarkus.datasource.jdbc.url = jdbc:postgresql://${POSTGRES_HOST:db}:${POSTGRES_PORT:5432}/buerokalender
%prod.quarkus.datasource.username = ${POSTGRES_USER:buerokalender}
%prod.quarkus.datasource.password = ${POSTGRES_PASSWORD:buerokalender}
# max JDBC pool size
%prod.quarkus.datasource.jdbc.max-size=16
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQL95Dialect
%dev.quarkus.hibernate-orm.log.sql=true
quarkus.flyway.migrate-at-start=true
# theoretische soll auch 'filesystem:' funktionieren
%dev.quarkus.flyway.locations=db/migration,db/dev/migration
# Security
quarkus.http.auth.form.enabled=true
#quarkus.http.auth.session.encryption-key=zHId14V+uiyxmbzhEPCyi7VvbaI80UeEO5yu0H/hVLs=
# 24h
quarkus.http.auth.form.timeout=24H
quarkus.http.auth.form.timeout=86400
# REST-CLient
de.mbremer.holiday.HolidayRestClient/mp-rest/url=https://deutsche-feiertage-api.de
de.mbremer.holiday.HolidayRestClient/mp-rest/scope=javax.inject.Singleton
# Deployment
quarkus.container-image.additional-tags= 1
quarkus.container-image.group = mattbremer
quarkus.container-image.name = buerokalender
quarkus.container-image.name = buerokalender

View File

@@ -1,3 +0,0 @@
-- passwd: admin
insert into users(id, username, password, role) values (NEXTVAL('hibernate_sequence'), 'admin', '$2a$10$VG/tPFFYl6jc7NKpK/kc3eqVFHdjuW7dZ9Ry39s4Re2lUi0xeuzqC', 'ADMIN');
insert into users(id, username, password, role) values (NEXTVAL('hibernate_sequence'), 'user', '$2a$10$VG/tPFFYl6jc7NKpK/kc3eqVFHdjuW7dZ9Ry39s4Re2lUi0xeuzqC', 'USER');

View File

@@ -1,5 +0,0 @@
create table image (
id bigint not null primary key,
name varchar not null unique,
data bytea not null
);

View File

@@ -1,10 +0,0 @@
create table room (
id bigint not null primary key,
name varchar not null unique
);
alter table users add column room_id bigint references room;
alter table kalendertag add column room_id bigint references room;
alter table kalendertag drop constraint kalendertag_day_key;
alter table kalendertag add constraint kalendertag_day_room_unique unique(day,room_id);

View File

@@ -19,14 +19,13 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<div class="navbar-nav mr-auto">
<a class="nav-item nav-link {#insert kalender_active}{/}" href="/kalender">Kalender</a>
<a class="nav-item nav-link {#insert room_active}{/}" href="/room">Raum</a>
<a class="nav-item nav-link {#insert user_active}{/}" href="/user">Benutzer</a>
</div>
</div>
<a class="btn btn-outline-primary me-2" href="/user/logout">Logout</a>
</nav>
{#if error??}<div class="alert alert-danger" role="alert">{error}</div>{/if}
{#if info??}<div class="alert alert-primary" role="alert">{info}</div>{/if}
{#if error}<div class="alert alert-danger" role="alert">{error}</div>{/if}
{#if info}<div class="alert alert-primary" role="alert">{info}</div>{/if}
{#insert contents}No contents!{/}
</div>
<script src="/webjars/popper.js/2.9.2/umd/popper.min.js"></script>

View File

@@ -40,6 +40,7 @@
<form action="/kalender/inoffice/{day.day}?offset={offset}" method="POST" name="kalenderForm" enctype="multipart/form-data">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="inoffice" onchange="this.form.submit()"
{#if day.isPast or day.istHoliday}disabled=""{/if}"
{#if day.isCurrentUserInOffice}checked{/if} >
</div>
</form>
@@ -48,6 +49,7 @@
{day.day.formatCommon}
</td>
<td>
{#if day.istHoliday}{day.holidayname}{/if}
{#if day.inOffice}{day.inOffice.username}{/if}
</td>

View File

@@ -1,25 +0,0 @@
<div class="modal fade" id="roomModal" tabindex="-1" aria-labelledby="roomModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="roomModalLabel">Raum anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/room/new" method="POST" name="roomForm" enctype="multipart/form-data">
<div class="modal-body row mb-3">
<div class="align-items-center col-md-10 mx-auto col-lg-11">
<div class="form-floating mb-3">
<input type="text" name="name" class="form-control" id="name" placeholder="Raumname" required>
<label for="name">Name</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,46 +0,0 @@
{#include base.html}
{#room_active}active{/}
{#contents}
<div class="mt-2">
{#if rooms.size == 0}
Kein Raum vorhanden
{#else}
<h2>Raum wählen</h2>
{/if}
<div class="btn-group" role="group">
{#for room in rooms}
<form action="/room/user" method="POST" name="chooseRoomForm" enctype="application/x-www-form-urlencoded">
<input type="radio" class="btn-check" name="room" value="{room.name}" id="radio{room.name}" autocomplete="off"
onchange="this.form.submit()" {#if room.name == current_room}checked{/if} >
<label class="btn btn-outline-primary" for="radio{room.name}">{room.name}</label>
</form>
{/for}
</div>
</div>
<div class="mt-2">
<img src="/room/plan" class="img-fluid" alt="kein Raumplan vorhanden">
</div>
{#if is_admin}
<div class="mt-2">
<form action="/room/plan" method="POST" enctype="multipart/form-data">
<label id="filelabel" class="btn btn-secondary" for="fileinput">
neuen Raumplan auswählen...
</label>
<input id="fileinput" type="file" name="file" accept="image/*" hidden
onchange="document.getElementById('filelabel').innerHTML = 'Datei: ' + this.files[0].name;" />
<input class="btn btn-primary" type="submit" value="Upload" />
</form>
</div>
<div class="mt-2">
<a class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#roomModal" role="button">neuer Raum</a>
</div>
{#include room-modal.html}{/include}
{/if}
{/contents}
{/include}

View File

@@ -1,4 +1,4 @@
<div class="modal fade" id="userModal{#if id??}{id}{/if}" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -6,37 +6,25 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/user/{#if update??}update{#else}new{/if}" method="POST" name="userForm" enctype="multipart/form-data">
<form action="/user/new" method="POST" name="userForm" enctype="multipart/form-data">
<div class="modal-body row mb-3">
<div class="align-items-center col-md-10 mx-auto col-lg-11">
<div class="form-floating mb-3">
<input type="text" name="username" class="form-control" id="name" placeholder="Benutzername" required
{#if update??}readonly value="{user.username}"{/if}>
<input type="text" name="username" class="form-control" id="name" placeholder="Benutzername" required>
<label for="name">Benutzername</label>
</div>
<div class="form-floating mb-3">
<input type="password" name="password" class="form-control" id="pwd" placeholder="Password" required
{#if update??}disabled value="xxxxx"{/if}>
<input type="password" name="password" class="form-control" id="pwd" placeholder="Password" required>
<label for="pwd">Passwort</label>
</div>
<div class="form-floating mb-3">
<input type="password" name="passwordVerify" class="form-control" id="pwdv" placeholder="Passwort wiederholen" required
{#if update??}disabled value="xxxxx"{/if}>
<input type="password" name="passwordVerify" class="form-control" id="pwdv" placeholder="Passwort wiederholen" required>
<label for="pwd">Passwort wiederholen</label>
</div>
<div class="form-floating mb-3">
<select id="room" name="room" class="form-select" required>
<option value="">Bitte wählen...</option>
{#for room in rooms}
<option {user.selectedIfIn(room)}>{room.name}</option>
{/for}
</select>
<label class="col-sm-3 col-form-label1" for="room">Raum</label>
</div>
<div class="form-floating mb-3">
<select id="role" name="role" class="form-select" required {#if update??}disabled{/if}>
<option {#if update?? and user.hasRoleUser}selected{/if}>USER</option>
<option {#if update?? and user.hasRoleAdmin}selected{/if}>ADMIN</option>
<select id="role" name="role" class="form-select" required>
<option selected>USER</option>
<option>ADMIN</option>
</select>
<label class="col-sm-3 col-form-label1" for="role">Rolle</label>
</div>

View File

@@ -3,43 +3,17 @@
{#contents}
<div class="mt-2">
<h2>Hallo {current_user.username}</h2>
{#if is_admin??}
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Raum</th>
<th scope="col"/>
</tr>
</thead>
<tbody>
{#for user in users}
<tr>
<td>
{user.username}
</td>
<td>
{user.room.name??}
</td>
<td style="width:1px; white-space:nowrap;">
<a class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#userModal{count}" role="button">Edit</a>
{#include user-modal.html rooms=rooms update=true id=count user=user}{/include}
</td>
</tr>
{/for}
</tbody>
</table>
{/if}
<h2>Hallo {current_username}</h2>
{#if is_admin}User: {user_count}{/if}
</div>
<div class="mt-2" >
{#if is_admin??}
<a class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal" role="button">neuer Benutzer</a>
{#if is_admin}
<a class="btn btn-primary btn" data-bs-toggle="modal" data-bs-target="#userModal" role="button">neuer Benutzer</a>
{/if}
<a class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#passwordModal" role="button">Passwort ändern</a>
{#if is_admin??}
{#include user-modal.html rooms=rooms user=current_user}{/include}
<a class="btn btn-primary btn" data-bs-toggle="modal" data-bs-target="#passwordModal" role="button">Passwort ändern</a>
{#if is_admin}
{#include user-modal.html}{/include}
{/if}
{#include password-modal.html}{/include}
</div>

View File

@@ -22,6 +22,8 @@ class KalenderResourceTest {
@InjectMock
SecurityIdentity identity;
@InjectMock
KalenderService kalenderService;
@Test
@TestSecurity(authorizationEnabled = false)

View File

@@ -1,46 +0,0 @@
package de.mbremer.secutity;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.mock.PanacheMock;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
import org.junit.jupiter.api.Test;
import java.security.Principal;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@QuarkusTest
class UserResourceTest {
@InjectMock
SecurityIdentity identity;
@Test
@TestSecurity(authorizationEnabled = false)
void testUser() {
Principal principal = mock(Principal.class);
when(principal.getName()).thenReturn("user");
when(identity.getPrincipal()).thenReturn(principal);
PanacheMock.mock(User.class);
PanacheQuery panacheQuery = mock(PanacheQuery.class);
User user = new User();
user.setUsername("testuser");
when(panacheQuery.singleResult()).thenReturn(user);
when(User.find("username", "user")).thenReturn(panacheQuery);
given()
.when().get("/user")
.then()
.statusCode(200)
.body(containsString("Hallo testuser"));
}
}