Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde4987c8a | ||
|
|
bd35eff00b | ||
|
|
87e38dbb54 | ||
|
|
871cce4265 | ||
|
|
8768de4241 | ||
|
|
055e46449a | ||
|
|
f455c42b65 | ||
|
|
74870afae3 | ||
| abb7994d84 | |||
| d002542575 | |||
|
|
d73404af30 | ||
|
|
0c98992cf9 | ||
|
|
a3db8c67d0 | ||
|
|
0d4dcd4df4 | ||
|
|
6f02239bdc | ||
|
|
78bb174be6 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
@@ -44,4 +44,9 @@ 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
|
||||
|
||||
|
||||
7
TODO.md
Normal file
7
TODO.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# TODO
|
||||
|
||||
- [ ] AJAX via unpoly
|
||||
- [ ] Feiertage-WS mit Cache
|
||||
- [ ] REST-API via API-Key
|
||||
- [ ] Verkehrsmodul via Google-Maps: E-Mail, Telegram
|
||||
- [ ] CalDAV-Sync
|
||||
6
pom.xml
6
pom.xml
@@ -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.0.0.Final</quarkus.platform.version>
|
||||
<quarkus.platform.version>2.2.2.Final</quarkus.platform.version>
|
||||
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
@@ -73,6 +73,10 @@
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-container-image-docker</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.mbremer.extension;
|
||||
|
||||
import de.mbremer.room.Room;
|
||||
import de.mbremer.secutity.User;
|
||||
import io.quarkus.qute.TemplateExtension;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -24,4 +26,8 @@ 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" : "";
|
||||
}
|
||||
}
|
||||
|
||||
27
src/main/java/de/mbremer/image/Image.java
Normal file
27
src/main/java/de/mbremer/image/Image.java
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
57
src/main/java/de/mbremer/image/ImageService.java
Normal file
57
src/main/java/de/mbremer/image/ImageService.java
Normal file
@@ -0,0 +1,57 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package de.mbremer.kalender;
|
||||
|
||||
import de.mbremer.room.Room;
|
||||
import de.mbremer.secutity.User;
|
||||
import de.mbremer.secutity.UserService;
|
||||
import io.quarkus.qute.Location;
|
||||
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.providers.multipart.MultipartForm;
|
||||
|
||||
@@ -18,7 +19,6 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Path("/kalender")
|
||||
@RolesAllowed({"USER", "ADMIN"})
|
||||
@@ -27,7 +27,9 @@ public class KalenderResource {
|
||||
@Inject
|
||||
Logger log;
|
||||
@Inject
|
||||
SecurityIdentity identity;
|
||||
UserService userService;
|
||||
@Inject
|
||||
KalenderService kalenderService;
|
||||
@Inject
|
||||
Template kalenderpage;
|
||||
@Inject
|
||||
@@ -40,32 +42,14 @@ public class KalenderResource {
|
||||
@GET
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public TemplateInstance kalender(@QueryParam("offset") @DefaultValue("0") int offsetInWeeks) {
|
||||
List<KalenderTag> week = getWeek(offsetInWeeks);
|
||||
|
||||
return kalenderpage
|
||||
.data("today", LocalDate.now())
|
||||
.data("offset", offsetInWeeks)
|
||||
.data("week", week);
|
||||
}
|
||||
|
||||
private List<KalenderTag> getWeek(int offsetInWeeks) {
|
||||
User currentUser = getCurrentUser();
|
||||
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 = (KalenderTag) KalenderTag.find("day", day).singleResultOptional().orElse(new KalenderTag(day));
|
||||
tag.setCurrentUserInOffice(currentUser.equals(tag.getInOffice()));
|
||||
tag.setToday(LocalDate.now().equals(day));
|
||||
return tag;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
.data("week", kalenderService.getWeek(offsetInWeeks));
|
||||
}
|
||||
|
||||
private List<Event> getEvents(int offsetInWeeks) {
|
||||
List<KalenderTag> week = getWeek(offsetInWeeks);
|
||||
List<KalenderTag> week = kalenderService.getWeek(offsetInWeeks);
|
||||
return week.stream()
|
||||
.filter(d -> d.isCurrentUserInOffice())
|
||||
.map(d-> new Event(LocalDateTime.now(), d.getDay()))
|
||||
@@ -80,32 +64,30 @@ public class KalenderResource {
|
||||
@QueryParam("offset") int offsetInWeeks, @MultipartForm KalenderTagForm kalenderForm) {
|
||||
LocalDate dayParsed = LocalDate.parse(day);
|
||||
|
||||
User currentUser = getCurrentUser();
|
||||
User currentUser = userService.getCurrentUser();
|
||||
Room room = currentUser.getRoom();
|
||||
try {
|
||||
KalenderTag tag = KalenderTag.find("day", dayParsed).singleResult();
|
||||
KalenderTag tag = KalenderTag.findByDayAndRoomId(dayParsed, room == null ? null : room.id);
|
||||
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);
|
||||
KalenderTag tag = new KalenderTag(dayParsed, room);
|
||||
tag.setInOffice(currentUser);
|
||||
tag.setRoom(room);
|
||||
tag.persist();
|
||||
}
|
||||
|
||||
return kalender(offsetInWeeks);
|
||||
}
|
||||
|
||||
private User getCurrentUser() {
|
||||
return User.find("username", identity.getPrincipal().getName()).singleResult();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Path("export/txt")
|
||||
public TemplateInstance exportTxt(@QueryParam("offset") int offset) {
|
||||
return textExport.data("week", getWeek(offset));
|
||||
return textExport.data("week", kalenderService.getWeek(offset));
|
||||
}
|
||||
|
||||
@GET
|
||||
|
||||
40
src/main/java/de/mbremer/kalender/KalenderService.java
Normal file
40
src/main/java/de/mbremer/kalender/KalenderService.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package de.mbremer.kalender;
|
||||
|
||||
import de.mbremer.room.Room;
|
||||
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;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@ApplicationScoped
|
||||
public class KalenderService {
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
public 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);
|
||||
}
|
||||
tag.setCurrentUserInOffice(currentUser.equals(tag.getInOffice()));
|
||||
tag.setToday(LocalDate.now().equals(day));
|
||||
return tag;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
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.RequiredArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
@@ -13,19 +14,31 @@ import java.time.LocalDate;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@RequiredArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@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;
|
||||
|
||||
public KalenderTag(LocalDate day) {
|
||||
public KalenderTag(LocalDate day, Room room) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
22
src/main/java/de/mbremer/room/Room.java
Normal file
22
src/main/java/de/mbremer/room/Room.java
Normal file
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
13
src/main/java/de/mbremer/room/RoomFileForm.java
Normal file
13
src/main/java/de/mbremer/room/RoomFileForm.java
Normal file
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
11
src/main/java/de/mbremer/room/RoomForm.java
Normal file
11
src/main/java/de/mbremer/room/RoomForm.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package de.mbremer.room;
|
||||
|
||||
import javax.ws.rs.FormParam;
|
||||
|
||||
public class RoomForm {
|
||||
public @FormParam("name") String name;
|
||||
|
||||
public Room getRoom() {
|
||||
return new Room(name);
|
||||
}
|
||||
}
|
||||
120
src/main/java/de/mbremer/room/RoomResource.java
Normal file
120
src/main/java/de/mbremer/room/RoomResource.java
Normal file
@@ -0,0 +1,120 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
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.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 io.quarkus.security.jpa.*;
|
||||
import lombok.*;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
@@ -31,27 +27,36 @@ 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 User setPassword(String password) {
|
||||
public void setPassword(String password) {
|
||||
this.password = BcryptUtil.bcryptHash(password);
|
||||
return this;
|
||||
}
|
||||
|
||||
public User setUsername(String username) {
|
||||
public void setUsername(String username) {
|
||||
this.username = username == null ? null : username.trim();
|
||||
return this;
|
||||
}
|
||||
|
||||
public User setRole(String role) {
|
||||
public void setRole(String role) {
|
||||
this.role = role == null ? role : role.toUpperCase();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasRoleAdmin() {
|
||||
return "ADMIN".equals(role);
|
||||
}
|
||||
|
||||
public boolean hasRoleUser() {
|
||||
return "USER".equals(role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
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("role") String role;
|
||||
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 User getUser() {
|
||||
return new User().setUsername(username).setPassword(password).setRole(role);
|
||||
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;
|
||||
}
|
||||
|
||||
public boolean verifyPassword() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -28,6 +30,8 @@ public class UserResource {
|
||||
@Inject
|
||||
SecurityIdentity identity;
|
||||
@Inject
|
||||
UserService userService;
|
||||
@Inject
|
||||
Template userinit;
|
||||
@Inject
|
||||
@Location("user.html")
|
||||
@@ -37,10 +41,17 @@ public class UserResource {
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
@RolesAllowed({"USER", "ADMIN"})
|
||||
public TemplateInstance getUser() {
|
||||
return userTemplate
|
||||
.data("user_count", User.count())
|
||||
.data("current_username", identity.getPrincipal().getName())
|
||||
.data("is_admin", identity.hasRole("ADMIN"));
|
||||
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;
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -99,6 +110,38 @@ public class UserResource {
|
||||
User user = userForm.getUser();
|
||||
user.persist();
|
||||
|
||||
return getUser().data("info", "User angelegt.");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
17
src/main/java/de/mbremer/secutity/UserService.java
Normal file
17
src/main/java/de/mbremer/secutity/UserService.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package de.mbremer.secutity;
|
||||
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ApplicationScoped
|
||||
public class UserService {
|
||||
|
||||
@Inject
|
||||
SecurityIdentity identity;
|
||||
|
||||
public User getCurrentUser() {
|
||||
return User.find("username", identity.getPrincipal().getName()).singleResult();
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,19 @@
|
||||
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=86400
|
||||
quarkus.http.auth.form.timeout=24H
|
||||
|
||||
# Deployment
|
||||
quarkus.container-image.additional-tags= 1
|
||||
quarkus.container-image.group = mattbremer
|
||||
quarkus.container-image.name = buerokalender
|
||||
quarkus.container-image.name = buerokalender
|
||||
|
||||
3
src/main/resources/db/dev/migration/V9001__users.sql
Normal file
3
src/main/resources/db/dev/migration/V9001__users.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- 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');
|
||||
5
src/main/resources/db/migration/V0002__image.sql
Normal file
5
src/main/resources/db/migration/V0002__image.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
create table image (
|
||||
id bigint not null primary key,
|
||||
name varchar not null unique,
|
||||
data bytea not null
|
||||
);
|
||||
10
src/main/resources/db/migration/V0003__room.sql
Normal file
10
src/main/resources/db/migration/V0003__room.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
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);
|
||||
@@ -19,13 +19,14 @@
|
||||
<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>
|
||||
|
||||
25
src/main/resources/templates/room-modal.html
Normal file
25
src/main/resources/templates/room-modal.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
46
src/main/resources/templates/room.html
Normal file
46
src/main/resources/templates/room.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{#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}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
|
||||
<div class="modal fade" id="userModal{#if id??}{id}{/if}" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -6,25 +6,37 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form action="/user/new" method="POST" name="userForm" enctype="multipart/form-data">
|
||||
<form action="/user/{#if update??}update{#else}new{/if}" 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>
|
||||
<input type="text" name="username" class="form-control" id="name" placeholder="Benutzername" required
|
||||
{#if update??}readonly value="{user.username}"{/if}>
|
||||
<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>
|
||||
<input type="password" name="password" class="form-control" id="pwd" placeholder="Password" required
|
||||
{#if update??}disabled value="xxxxx"{/if}>
|
||||
<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>
|
||||
<input type="password" name="passwordVerify" class="form-control" id="pwdv" placeholder="Passwort wiederholen" required
|
||||
{#if update??}disabled value="xxxxx"{/if}>
|
||||
<label for="pwd">Passwort wiederholen</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<select id="role" name="role" class="form-select" required>
|
||||
<option selected>USER</option>
|
||||
<option>ADMIN</option>
|
||||
<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>
|
||||
<label class="col-sm-3 col-form-label1" for="role">Rolle</label>
|
||||
</div>
|
||||
|
||||
@@ -3,17 +3,43 @@
|
||||
{#contents}
|
||||
|
||||
<div class="mt-2">
|
||||
<h2>Hallo {current_username}</h2>
|
||||
{#if is_admin}User: {user_count}{/if}
|
||||
<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}
|
||||
</div>
|
||||
|
||||
<div class="mt-2" >
|
||||
{#if is_admin}
|
||||
<a class="btn btn-primary btn" data-bs-toggle="modal" data-bs-target="#userModal" role="button">neuer Benutzer</a>
|
||||
{#if is_admin??}
|
||||
<a class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal" role="button">neuer Benutzer</a>
|
||||
{/if}
|
||||
<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}
|
||||
<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}
|
||||
{/if}
|
||||
{#include password-modal.html}{/include}
|
||||
</div>
|
||||
|
||||
46
src/test/java/de/mbremer/secutity/UserResourceTest.java
Normal file
46
src/test/java/de/mbremer/secutity/UserResourceTest.java
Normal file
@@ -0,0 +1,46 @@
|
||||
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"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user