16 Commits

Author SHA1 Message Date
mbremer
fde4987c8a User etwas überarbeitet 2021-09-24 14:32:16 +02:00
mbremer
bd35eff00b sql für dev-mode 2021-09-16 12:41:03 +02:00
mbremer
87e38dbb54 initial Users in Dev-Mode 2021-09-13 11:35:18 +02:00
mbremer
871cce4265 Upgrade to quarkus 2.2.2 2021-09-13 11:34:36 +02:00
mbremer
8768de4241 small improovements 2021-08-31 13:56:32 +02:00
mbremer
055e46449a Raumplan im Client-Cache 2021-08-31 13:55:18 +02:00
mbremer
f455c42b65 Merge branch 'master' of ssh://bremer.rocks:222/matt/buerokalender 2021-08-22 21:58:40 +02:00
mbremer
74870afae3 upgrade to quarkus 2.1.3 2021-08-22 21:58:27 +02:00
abb7994d84 set up in readme 2021-08-03 21:51:38 +02:00
d002542575 TODO with checkboxes 2021-08-03 21:39:24 +02:00
mbremer
d73404af30 add MIT License 2021-08-03 21:26:24 +02:00
mbremer
0c98992cf9 Fix change-password dialog, user-edit for admin 2021-08-02 18:40:18 +02:00
mbremer
a3db8c67d0 Fix Kalender-Page for multiple rooms 2021-08-02 16:57:01 +02:00
mbremer
0d4dcd4df4 Room per User and KalenderTag 2021-08-01 11:49:32 +02:00
mbremer
6f02239bdc roomplan persistence 2021-08-01 09:48:31 +02:00
mbremer
78bb174be6 upload roomplan 2021-07-31 16:18:51 +02:00
28 changed files with 662 additions and 79 deletions

21
LICENSE Normal file
View 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.

View File

@@ -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
View 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

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.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>

View File

@@ -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" : "";
}
}

View 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;
}

View 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();
}
}
}

View File

@@ -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

View 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());
}
}

View File

@@ -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();
}
}

View 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;
}

View 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;
}

View 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);
}
}

View 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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();
}
}

View 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();
}
}

View File

@@ -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

View 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');

View File

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

View 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);

View File

@@ -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>

View 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>

View 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}

View File

@@ -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>

View File

@@ -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>

View 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"));
}
}