KB49 Add private news feeds.
Change-Id: Ib9488351b9734795e02ddaeb26aa81eeb79c0b4d
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java
index 2553a11..05e4bb5 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/PageKey.java
@@ -26,6 +26,6 @@
public BigInteger key;
@ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "user", referencedColumnName = "id", nullable = false)
+ @JoinColumn(name = "\"user\"", referencedColumnName = "id", nullable = false)
public User user;
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
index 6dc76b0..495e511 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
@@ -16,6 +16,7 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.util.Objects;
import java.util.Set;
@@ -42,7 +43,7 @@
@Inject
Template newBookmark;
- public BookmarkResource() {
+ public BookmarkResource() throws NoSuchAlgorithmException {
super(PostFilter.BOOKMARKS_ONLY, "Bookmarks");
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
index 4f9a9fb..cbdbe76 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
@@ -10,6 +10,7 @@
import io.quarkus.security.Authenticated;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.util.Objects;
import javax.transaction.Transactional;
@@ -27,7 +28,7 @@
@Path("/lazychat")
public class LazychatResource extends PostResource {
- public LazychatResource() {
+ public LazychatResource() throws NoSuchAlgorithmException {
super(PostFilter.LAZYCHAT_MESSAGES_ONLY, "Lazy Chat");
}
@@ -76,7 +77,7 @@
throw new NotFoundException();
}
- if (!Objects.equals(message.owner.id, user.id)) {
+ if (message.owner == null || !Objects.equals(message.owner.id, user.id)) {
throw new ForbiddenException();
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java
index 47c644c..4047b9e 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/AllPostsResource.java
@@ -1,11 +1,12 @@
package eu.mulk.mulkcms2.benki.posts;
+import java.security.NoSuchAlgorithmException;
import javax.ws.rs.Path;
@Path("/posts")
public class AllPostsResource extends PostResource {
- public AllPostsResource() {
+ public AllPostsResource() throws NoSuchAlgorithmException {
super(PostFilter.ALL, "All Posts");
}
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
index fc0f76f..3a02e4e 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
@@ -6,7 +6,6 @@
import eu.mulk.mulkcms2.benki.users.User;
import eu.mulk.mulkcms2.benki.users.User_;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
-import io.quarkus.security.identity.SecurityIdentity;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -60,6 +59,7 @@
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner", referencedColumnName = "id")
+ @CheckForNull
@JsonbTransient
public User owner;
@@ -108,7 +108,7 @@
protected static <T extends Post> CriteriaQuery<T> queryViewable(
Class<T> entityClass,
- SecurityIdentity readerIdentity,
+ @CheckForNull User reader,
@CheckForNull User owner,
@CheckForNull Integer cursor,
CriteriaBuilder cb,
@@ -118,16 +118,13 @@
var conditions = new ArrayList<Predicate>();
From<?, T> post;
- if (readerIdentity.isAnonymous()) {
+ if (reader == null) {
post = query.from(entityClass);
var target = post.join(Post_.targets);
conditions.add(cb.equal(target, Role.getWorld()));
} else {
- var userName = readerIdentity.getPrincipal().getName();
- var user = User.findByNickname(userName);
-
var root = query.from(User.class);
- conditions.add(cb.equal(root, user));
+ conditions.add(cb.equal(root, reader));
if (entityClass.isAssignableFrom(Post.class)) {
post = (From<?, T>) root.join(User_.visiblePosts);
} else if (entityClass.isAssignableFrom(Bookmark.class)) {
@@ -189,14 +186,14 @@
}
public static List<Post> findViewable(
- PostFilter postFilter, Session session, SecurityIdentity viewer, @CheckForNull User owner) {
+ PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) {
return findViewable(postFilter, session, viewer, owner, null, null).posts;
}
public static PostPage<Post> findViewable(
PostFilter postFilter,
Session session,
- SecurityIdentity viewer,
+ @CheckForNull User viewer,
@CheckForNull User owner,
@CheckForNull Integer cursor,
@CheckForNull Integer count) {
@@ -217,7 +214,7 @@
protected static <T extends Post> PostPage<T> findViewable(
Class<? extends T> entityClass,
Session session,
- SecurityIdentity viewer,
+ @CheckForNull User viewer,
@CheckForNull User owner,
@CheckForNull Integer cursor,
@CheckForNull Integer count) {
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
index c718bbc..c100e55 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
@@ -11,6 +11,7 @@
import com.rometools.rome.feed.synd.SyndPersonImpl;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.WireFeedOutput;
+import eu.mulk.mulkcms2.benki.accesscontrol.PageKey;
import eu.mulk.mulkcms2.benki.accesscontrol.Role;
import eu.mulk.mulkcms2.benki.users.User;
import io.quarkus.qute.Template;
@@ -18,7 +19,10 @@
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.api.ResourcePath;
import io.quarkus.security.identity.SecurityIdentity;
+import java.math.BigInteger;
import java.net.URI;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -29,6 +33,7 @@
import java.util.Date;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
@@ -37,6 +42,7 @@
import javax.json.spi.JsonProvider;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
+import javax.transaction.Transactional;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
@@ -59,6 +65,8 @@
private static final DateTimeFormatter humanDateFormatter =
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
+ private static final int pageKeyBytes = 32;
+
protected static final JsonProvider jsonProvider = JsonProvider.provider();
@ConfigProperty(name = "mulkcms.posts.default-max-results")
@@ -78,12 +86,15 @@
@PersistenceContext protected EntityManager entityManager;
+ private final SecureRandom secureRandom;
+
private final PostFilter postFilter;
private final String pageTitle;
- public PostResource(PostFilter postFilter, String pageTitle) {
+ public PostResource(PostFilter postFilter, String pageTitle) throws NoSuchAlgorithmException {
this.postFilter = postFilter;
this.pageTitle = pageTitle;
+ secureRandom = SecureRandom.getInstanceStrong();
}
@GET
@@ -94,12 +105,19 @@
maxResults = maxResults == null ? defaultMaxResults : maxResults;
+ @CheckForNull var reader = getCurrentUser();
var session = entityManager.unwrap(Session.class);
- var q = Post.findViewable(postFilter, session, identity, null, cursor, maxResults);
+ var q = Post.findViewable(postFilter, session, reader, null, cursor, maxResults);
+
+ var feedUri = "/posts/feed";
+ if (reader != null) {
+ var pageKey = ensurePageKey(reader, feedUri);
+ feedUri += "?page-key=" + pageKey.key.toString(36);
+ }
return postList
.data("posts", q.posts)
- .data("feedUri", "/posts/feed")
+ .data("feedUri", feedUri)
.data("pageTitle", pageTitle)
.data("showBookmarkForm", showBookmarkForm())
.data("showLazychatForm", showLazychatForm())
@@ -120,13 +138,20 @@
maxResults = maxResults == null ? defaultMaxResults : maxResults;
+ @CheckForNull var reader = getCurrentUser();
var owner = User.findByNickname(ownerName);
var session = entityManager.unwrap(Session.class);
- var q = Post.findViewable(postFilter, session, identity, owner, cursor, maxResults);
+ var q = Post.findViewable(postFilter, session, reader, owner, cursor, maxResults);
+
+ var feedUri = String.format("/posts/~%s/feed", ownerName);
+ if (reader != null) {
+ var pageKey = ensurePageKey(reader, feedUri);
+ feedUri += "?page-key=" + pageKey.key.toString(36);
+ }
return postList
.data("posts", q.posts)
- .data("feedUri", String.format("/posts/~%s/feed", ownerName))
+ .data("feedUri", feedUri)
.data("pageTitle", pageTitle)
.data("showBookmarkForm", showBookmarkForm())
.data("showLazychatForm", showLazychatForm())
@@ -137,23 +162,63 @@
.data("pageSize", maxResults);
}
+ @Transactional
+ protected final PageKey ensurePageKey(User reader, String pagePath) {
+ PageKey pageKey = PageKey.find("page = ?1 AND user = ?2", pagePath, reader).firstResult();
+ if (pageKey == null) {
+ pageKey = new PageKey();
+ byte[] keyBytes = new byte[pageKeyBytes];
+ secureRandom.nextBytes(keyBytes);
+ pageKey.key = new BigInteger(keyBytes);
+ pageKey.page = pagePath;
+ pageKey.user = reader;
+ pageKey.persist();
+ }
+ return pageKey;
+ }
+
@GET
@Path("feed")
@Produces(APPLICATION_ATOM_XML)
- public String getFeed() throws FeedException {
- return makeFeed(null, null);
+ public String getFeed(@QueryParam("page-key") @CheckForNull String pageKeyBase36)
+ throws FeedException {
+ @CheckForNull var pageKey = pageKeyBase36 == null ? null : new BigInteger(pageKeyBase36, 36);
+ return makeFeed(pageKey, null, null);
}
@GET
@Path("~{ownerName}/feed")
@Produces(APPLICATION_ATOM_XML)
- public String getUserFeed(@PathParam("ownerName") String ownerName) throws FeedException {
+ public String getUserFeed(
+ @QueryParam("page-key") @CheckForNull String pageKeyBase36,
+ @PathParam("ownerName") String ownerName)
+ throws FeedException {
var owner = User.findByNickname(ownerName);
- return makeFeed(owner, ownerName);
+ @CheckForNull var pageKey = pageKeyBase36 == null ? null : new BigInteger(pageKeyBase36, 36);
+ return makeFeed(pageKey, ownerName, owner);
}
- private String makeFeed(@Nullable User owner, @Nullable String ownerName) throws FeedException {
- var posts = Post.findViewable(postFilter, entityManager.unwrap(Session.class), identity, owner);
+ private String makeFeed(
+ @CheckForNull BigInteger pageKey, @CheckForNull String ownerName, @CheckForNull User owner)
+ throws FeedException {
+ if (pageKey == null) {
+ return makeFeed(getCurrentUser(), owner, ownerName);
+ }
+
+ Optional<PageKey> pageKeyInfo =
+ PageKey.find("page = ?1 AND key = ?2", uri.getPath(), pageKey).singleResultOptional();
+ if (pageKeyInfo.isEmpty()) {
+ throw new ForbiddenException();
+ }
+
+ var pageKeyOwner = pageKeyInfo.get().user;
+ return makeFeed(pageKeyOwner, owner, ownerName);
+ }
+
+ private String makeFeed(
+ @CheckForNull User reader, @Nullable User owner, @Nullable String ownerName)
+ throws FeedException {
+ var posts = Post.findViewable(postFilter, entityManager.unwrap(Session.class), reader, owner);
var feed = new Feed("atom_1.0");
var feedSubId = owner == null ? "" : String.format("/%d", owner.id);
@@ -181,7 +246,7 @@
feed.setOtherLinks(List.of(selfLink));
var htmlAltLink = new Link();
- var htmlAltPath = owner == null ? "/posts" : String.format("~%s/posts", ownerName);
+ var htmlAltPath = ownerName == null ? "/posts" : String.format("~%s/posts", ownerName);
htmlAltLink.setHref(uri.resolve(URI.create(htmlAltPath)).toString());
htmlAltLink.setRel("alternate");
htmlAltLink.setType("text/html");
@@ -199,9 +264,11 @@
entry.setUpdated(Date.from(post.date.toInstant()));
}
- var author = new SyndPersonImpl();
- author.setName(post.owner.getFirstAndLastName());
- entry.setAuthors(List.of(author));
+ if (post.owner != null) {
+ var author = new SyndPersonImpl();
+ author.setName(post.owner.getFirstAndLastName());
+ entry.setAuthors(List.of(author));
+ }
if (post.getTitle() != null) {
var title = new Content();
@@ -294,16 +361,6 @@
}
}
- @CheckForNull
- protected final User getCurrentUser() {
- if (identity.isAnonymous()) {
- return null;
- }
-
- var userName = identity.getPrincipal().getName();
- return User.findByNickname(userName);
- }
-
protected final Post getPostIfVisible(int id) {
@CheckForNull var user = getCurrentUser();
var message = getSession().byId(Post.class).load(id);
@@ -315,6 +372,11 @@
return message;
}
+ @CheckForNull
+ protected final User getCurrentUser() {
+ return identity.isAnonymous() ? null : User.findByNickname(identity.getPrincipal().getName());
+ }
+
@GET
@Produces(APPLICATION_JSON)
@Path("{id}")
diff --git a/src/main/resources/hibernate.properties b/src/main/resources/hibernate.properties
new file mode 100644
index 0000000..52cfc53
--- /dev/null
+++ b/src/main/resources/hibernate.properties
@@ -0,0 +1 @@
+hibernate.dialect = io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect