diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..cae4dc7 --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +Review: +- `@Transactional` +- `.persist()` (especially `.persistAndFlush()`) \ No newline at end of file diff --git a/src/main/java/eu/m724/talkpages/RedirectService.java b/src/main/java/eu/m724/talkpages/RedirectService.java index 33bf05f..e49e3b3 100644 --- a/src/main/java/eu/m724/talkpages/RedirectService.java +++ b/src/main/java/eu/m724/talkpages/RedirectService.java @@ -1,6 +1,6 @@ package eu.m724.talkpages; -import eu.m724.talkpages.orm.entity.Page; +import eu.m724.talkpages.orm.entity.content.Page; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -19,7 +19,7 @@ public class RedirectService { } public Response.ResponseBuilder page(Page page) { - return redirect("/page/" + page.path); + return redirect("/page/" + page.getSlug()); } public Response.ResponseBuilder pageTitle(String title) { diff --git a/src/main/java/eu/m724/talkpages/Startup.java b/src/main/java/eu/m724/talkpages/Startup.java index 2e2ccc6..687a9a9 100644 --- a/src/main/java/eu/m724/talkpages/Startup.java +++ b/src/main/java/eu/m724/talkpages/Startup.java @@ -1,8 +1,8 @@ package eu.m724.talkpages; -import eu.m724.talkpages.orm.entity.Account; -import eu.m724.talkpages.orm.entity.Page; -import eu.m724.talkpages.orm.entity.PageRevision; +import eu.m724.talkpages.orm.entity.auth.Account; +import eu.m724.talkpages.orm.entity.content.Page; +import eu.m724.talkpages.orm.entity.content.PageRevision; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.event.Observes; @@ -33,7 +33,6 @@ public class Startup { System.out.println("Performing first run setup"); Account account = new Account(username); - //account.id = 0L; account.persistAndFlush(); if (createExamplePage) { @@ -46,15 +45,12 @@ public class Startup { @Transactional public void addPage(Account account, String title, String content) { Page page = new Page(title); - - PageRevision revision = new PageRevision(page); - revision.author = account; - revision.content = content; - revision.delta = revision.content.length(); + PageRevision revision = new PageRevision(page, account, content); page.setLatestRevision(revision); - account.revisions.add(revision); + page.persistAndFlush(); + + account.getRevisions().add(revision); - revision.persistAndFlush(); } } diff --git a/src/main/java/eu/m724/talkpages/auth/AuthResource.java b/src/main/java/eu/m724/talkpages/auth/AuthResource.java index 3bf7e00..7678426 100644 --- a/src/main/java/eu/m724/talkpages/auth/AuthResource.java +++ b/src/main/java/eu/m724/talkpages/auth/AuthResource.java @@ -1,6 +1,6 @@ package eu.m724.talkpages.auth; -import eu.m724.talkpages.orm.entity.Session; +import eu.m724.talkpages.orm.entity.auth.Session; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.security.Authenticated; @@ -12,7 +12,6 @@ import jakarta.ws.rs.core.*; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.time.LocalDateTime; @Path("/auth") @Produces(MediaType.TEXT_HTML) @@ -73,7 +72,7 @@ public class AuthResource { Session session = authService.authenticate(username, password); NewCookie cookie = new NewCookie.Builder("session-token") - .value(session.token) + .value(session.getToken()) .maxAge(604800)// TODO make the age respect session's expires .httpOnly(true) .path("/") @@ -124,7 +123,7 @@ public class AuthResource { Session session = authService.register(username, password); NewCookie cookie = new NewCookie.Builder("session-token") - .value(session.token) + .value(session.getToken()) .maxAge(604800)// TODO make the age respect session's expires .httpOnly(true) .path("/") diff --git a/src/main/java/eu/m724/talkpages/auth/AuthService.java b/src/main/java/eu/m724/talkpages/auth/AuthService.java index d49f4ff..7c3548b 100644 --- a/src/main/java/eu/m724/talkpages/auth/AuthService.java +++ b/src/main/java/eu/m724/talkpages/auth/AuthService.java @@ -2,8 +2,8 @@ package eu.m724.talkpages.auth; import de.mkammerer.argon2.Argon2; import de.mkammerer.argon2.Argon2Factory; -import eu.m724.talkpages.orm.entity.Session; -import eu.m724.talkpages.orm.entity.RegisteredUser; +import eu.m724.talkpages.orm.entity.auth.Account; +import eu.m724.talkpages.orm.entity.auth.Session; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; @@ -16,7 +16,7 @@ public class AuthService { private final SecureRandom random = new SecureRandom(); /** - * Register a new {@link RegisteredUser} + * Register a new {@link Account} * * @param username username * @param password password @@ -25,16 +25,20 @@ public class AuthService { */ @Transactional Session register(String username, String password) throws UsernameExistsException { - RegisteredUser user = RegisteredUser.find("username", username).firstResult(); + Account account = Account.findById(username); - if (user != null) { + if (account != null) { throw new UsernameExistsException(); } String hashedPassword = argon2.hash(10, 65536, 1, password); - user = RegisteredUser.add(username, hashedPassword, "user"); + account = new Account(username, hashedPassword); + account.persistAndFlush(); - return Session.createForUser(user); + Session session = new Session(account); + session.persist(); + + return session; } @Transactional @@ -47,7 +51,7 @@ public class AuthService { Session session = Session.find("token", sessionToken).firstResult(); if (session != null) { - if (session.expires.isAfter(LocalDateTime.now())) { + if (session.getExpires().isAfter(LocalDateTime.now())) { return session; } else { session.delete(); @@ -59,17 +63,20 @@ public class AuthService { @Transactional Session authenticate(String username, String password) throws InvalidCredentialsException { - RegisteredUser user = RegisteredUser.find("username", username).firstResult(); + Account account = Account.findById(username); - if (user == null) { + if (account == null) { throw new InvalidCredentialsException(false); } - if (!argon2.verify(user.password, password)) { + if (!argon2.verify(account.getPassword(), password)) { throw new InvalidCredentialsException(true); } - return Session.createForUser(user); + Session session = new Session(account); + session.persist(); + + return session; } public static class InvalidCredentialsException extends Exception { diff --git a/src/main/java/eu/m724/talkpages/auth/MyHttpAuthenticationMechanism.java b/src/main/java/eu/m724/talkpages/auth/MyHttpAuthenticationMechanism.java index 8d9dfb9..69547be 100644 --- a/src/main/java/eu/m724/talkpages/auth/MyHttpAuthenticationMechanism.java +++ b/src/main/java/eu/m724/talkpages/auth/MyHttpAuthenticationMechanism.java @@ -1,7 +1,7 @@ package eu.m724.talkpages.auth; -import eu.m724.talkpages.orm.entity.Session; -import eu.m724.talkpages.orm.entity.RegisteredUser; +import eu.m724.talkpages.orm.entity.auth.Account; +import eu.m724.talkpages.orm.entity.auth.Session; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusPrincipal; @@ -25,27 +25,25 @@ public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanis AuthService authService; @Override - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { // TODO handle ip accounts return Uni.createFrom().item(() -> { Cookie cookie = context.request().getCookie("session-token"); - if (cookie == null) - return (SecurityIdentity) QuarkusSecurityIdentity.builder().setAnonymous(true).build(); + if (cookie != null) { + String sessionToken = context.request().getCookie("session-token").getValue(); + Session session = authService.validateSessionToken(sessionToken); - String sessionToken = context.request().getCookie("session-token").getValue(); - Session session = authService.validateSessionToken(sessionToken); + if (session != null) { + Account account = session.getAccount(); - if (session != null) { - RegisteredUser user = session.user; + QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(account.getName())) + .addRoles(account.getRoles()) + .addAttribute("session", session) + .build(); - QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder() - .setPrincipal(new QuarkusPrincipal(user.username)) - .addRoles(user.roles) - .addAttribute("session", session) - .build(); - - return (SecurityIdentity) identity; + return (SecurityIdentity) identity; + } } return (SecurityIdentity) QuarkusSecurityIdentity.builder().setAnonymous(true).build(); diff --git a/src/main/java/eu/m724/talkpages/orm/entity/Account.java b/src/main/java/eu/m724/talkpages/orm/entity/Account.java deleted file mode 100644 index fc5cd11..0000000 --- a/src/main/java/eu/m724/talkpages/orm/entity/Account.java +++ /dev/null @@ -1,31 +0,0 @@ -package eu.m724.talkpages.orm.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.*; - -import java.util.ArrayList; -import java.util.List; - -@Entity -public class Account extends PanacheEntity { - public Account() {} - - public Account(String name) { - this.name = name; - } - - /** - * the name of this account - */ - @Column(unique = true) - public String name; - - /** - * This user's edits - */ - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = false) - public List revisions = new ArrayList<>(); - - @OneToOne - public RegisteredUser user; -} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/Page.java b/src/main/java/eu/m724/talkpages/orm/entity/Page.java deleted file mode 100644 index a19b4d7..0000000 --- a/src/main/java/eu/m724/talkpages/orm/entity/Page.java +++ /dev/null @@ -1,50 +0,0 @@ -package eu.m724.talkpages.orm.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntityBase; -import jakarta.persistence.*; -import org.hibernate.annotations.GenericGenerator; - -import java.util.ArrayList; -import java.util.List; - -@Entity -public class Page extends PanacheEntityBase { - public Page() {} - - public Page(String title) { - this.title = title; - } - - @Id - @GeneratedValue(generator = "title-path") - @GenericGenerator(name = "title-path", strategy = "eu.m724.talkpages.orm.generator.PathGenerator") - public String path; - - /** - * The title of this page, derives path - */ - @Column(unique = true) - public String title; - - @OneToOne - public PageRevision latestRevision; - - /** - * Revisions of this page - */ - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - public List revisions = new ArrayList<>(); - - public static Page findByTitle(String title) { - return find("title", title).firstResult(); - } - - public static List findByTitleIgnoreCase(String title) { - return find("lower(title) = ?1", title.toLowerCase()).list(); - } - - public void setLatestRevision(PageRevision pageRevision) { - revisions.add(pageRevision); - latestRevision = pageRevision; - } -} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java b/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java deleted file mode 100644 index a834a90..0000000 --- a/src/main/java/eu/m724/talkpages/orm/entity/PageRevision.java +++ /dev/null @@ -1,62 +0,0 @@ -package eu.m724.talkpages.orm.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table( - uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"}) -) -public class PageRevision extends PanacheEntity { - public PageRevision() {} - - public PageRevision(Page page) { - long index = 1; - if (page.latestRevision != null) { - index = page.latestRevision.index + 1; - } - - this.index = index; - this.page = page; - } - - @ManyToOne(cascade = CascadeType.ALL) - public Page page; - - /** - * the index of the revision for a page, first revision is 1 - * TODO make generated - */ - public long index = 1; - - /** - * The creation time of the revision - */ - @CreationTimestamp - public LocalDateTime timestamp; - - /** - * The author of the revision - */ - @ManyToOne(cascade = CascadeType.ALL) - public Account author; - - /** - * The full page content of the revision. - */ - @Column(columnDefinition="text") - public String content; - - /** - * Change of content size compared to previous revision
- * If this is the first revision, total bytes - */ - public int delta; - - public static PageRevision findByIndex(Page page, long index) { - return find("page = ?1 and index = ?2", page, index).firstResult(); - } -} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/RegisteredUser.java b/src/main/java/eu/m724/talkpages/orm/entity/RegisteredUser.java deleted file mode 100644 index 99add3f..0000000 --- a/src/main/java/eu/m724/talkpages/orm/entity/RegisteredUser.java +++ /dev/null @@ -1,42 +0,0 @@ -package eu.m724.talkpages.orm.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntityBase; -import jakarta.persistence.*; -import jakarta.transaction.Transactional; - -import java.util.HashSet; -import java.util.Set; - -@Entity -public class RegisteredUser extends PanacheEntityBase { - @Id - public String username; - - public String password; - - public Set roles; - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = false) - public Set sessions = new HashSet<>(); - - @OneToOne - public Account account; - - /** - * Adds a new user to the database - * @param username the username - * @param password HASHED password - * @param roles roles - */ - public static RegisteredUser add(String username, String password, String... roles) { - RegisteredUser user = new RegisteredUser(); - - user.username = username; - user.password = password; - user.roles = Set.of(roles); - user.persistAndFlush(); - - return user; - } - -} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/Session.java b/src/main/java/eu/m724/talkpages/orm/entity/Session.java deleted file mode 100644 index e821e46..0000000 --- a/src/main/java/eu/m724/talkpages/orm/entity/Session.java +++ /dev/null @@ -1,43 +0,0 @@ -package eu.m724.talkpages.orm.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.ManyToOne; -import jakarta.transaction.Transactional; - -import java.security.SecureRandom; -import java.time.LocalDateTime; -import java.util.Base64; - -@Entity -public class Session extends PanacheEntity { - @ManyToOne(cascade = CascadeType.ALL) - public RegisteredUser user; - - public String token; // TODO make this generated - - public LocalDateTime expires; - - /** - * create a {@link Session} for a {@link RegisteredUser}
- * randomly generated token, expiring in 7 days - * - * @param user the user - * @return the created session - */ - public static Session createForUser(RegisteredUser user) { - byte[] tokenBytes = new byte[64]; - new SecureRandom().nextBytes(tokenBytes); - String token = Base64.getEncoder().encodeToString(tokenBytes); - - Session session = new Session(); - session.user = user; - session.token = token; - session.expires = LocalDateTime.now().plusDays(7); - user.sessions.add(session); - - session.persist(); - return session; - } -} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java b/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java new file mode 100644 index 0000000..8ed040d --- /dev/null +++ b/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java @@ -0,0 +1,75 @@ +package eu.m724.talkpages.orm.entity.auth; + +import eu.m724.talkpages.orm.entity.content.PageRevision; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.persistence.*; +import jakarta.transaction.Transactional; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +// TODO consider moving away authentication to allow like multiple profiles on one account +@Entity +public class Account extends PanacheEntityBase { + private Account() {} + + /** + * Creates a user account.
+ * No default roles. + * + * @param name username + * @param password hashed password + */ + public Account(String name, String password) { + this.name = name; + this.slug = URLEncoder.encode(name, StandardCharsets.UTF_8); + this.password = password; + } + + + /** + * Creates a system account.
+ * System accounts don't have passwords, so can't be logged into.
+ * No default roles. + * + * @param name username + */ + public Account(String name) { + this(name, null); + } + + + // Columns + + @Id + private String name; + + private String slug; + + private String password; + + private Set roles = new HashSet<>(); + + @OneToMany(orphanRemoval = true, cascade = CascadeType.ALL) + private Set sessions = new HashSet<>(); + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = false, fetch = FetchType.EAGER) + private List revisions = new ArrayList<>(); + + + // Getters + + public String getName() { return name; } + public String getSlug() { return slug; } + public String getPassword() { return password; } + public Set getRoles() { return roles; } + public Set getSessions() { return sessions; } + public List getRevisions() { return revisions; } + + public boolean isSystemAccount() { return password == null; } +} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/auth/Session.java b/src/main/java/eu/m724/talkpages/orm/entity/auth/Session.java new file mode 100644 index 0000000..9303158 --- /dev/null +++ b/src/main/java/eu/m724/talkpages/orm/entity/auth/Session.java @@ -0,0 +1,65 @@ +package eu.m724.talkpages.orm.entity.auth; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.transaction.Transactional; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.Base64; + +// TODO clean old sessions +@Entity +public class Session extends PanacheEntity { + private Session() {} + + /** + * Create a {@link Session} for the given {@link Account}
+ * A token is randomly generated. It's 64 bytes and is encoded in Base64.
+ * Expiration is set to 7 days in the future. + * + * @param account the account + */ + public Session(Account account) { + this(account, java.time.LocalDateTime.now().plusDays(7)); + } + + /** + * Create a {@link Session} for the given {@link Account}
+ * A token is randomly generated. It's 64 bytes and is encoded in Base64.
+ * + * @param account the account + * @param expires expiration time + */ + public Session(Account account, LocalDateTime expires) { + byte[] tokenBytes = new byte[64]; + new SecureRandom().nextBytes(tokenBytes); + String token = Base64.getEncoder().encodeToString(tokenBytes); + + this.account = account; + this.token = token; + this.expires = expires; + } + + + // Columns + + @ManyToOne(cascade = CascadeType.ALL) + private Account account; + + // TODO make a generator for this if possible + private String token; + + // TODO rename? + private LocalDateTime expires; + + + // Getters + + public Account getAccount() { return account; } + public String getToken() { return token; } + public LocalDateTime getExpires() { return expires; } + +} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java b/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java new file mode 100644 index 0000000..487cdef --- /dev/null +++ b/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java @@ -0,0 +1,63 @@ +package eu.m724.talkpages.orm.entity.content; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table( + indexes = @Index(name = "idx_title", columnList = "title"), + uniqueConstraints = @UniqueConstraint(columnNames = "title") +) +public class Page extends PanacheEntityBase { + private Page() {} + + public Page(String title) { + // TODO maybe map space to _ + this.slug = URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20"); + this.title = title; + } + + // Columns + + @Id + private String slug; + + // TODO is this really necessary + private String title; + + @OneToOne + private PageRevision latestRevision; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List revisions = new ArrayList<>(); + + + // Getters + + public String getSlug() { return slug; } + public String getTitle() { return title; } + public PageRevision getLatestRevision() { return latestRevision; } + public List getRevisions() { return revisions; } + + public void setLatestRevision(PageRevision pageRevision) { + revisions.add(pageRevision); + latestRevision = pageRevision; + } + + // Operations + + public static Page findByTitle(String title) { + return find("title", title).firstResult(); + } + + public static List findByTitleIgnoreCase(String title) { + return find("lower(title) = ?1", title.toLowerCase()).list(); + } +} diff --git a/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java b/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java new file mode 100644 index 0000000..44fc68a --- /dev/null +++ b/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java @@ -0,0 +1,73 @@ +package eu.m724.talkpages.orm.entity.content; + +import eu.m724.talkpages.orm.entity.auth.Account; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table( + indexes = @Index(columnList = "page_slug"), + uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"}) +) +public class PageRevision extends PanacheEntity { + private PageRevision() {} + + public PageRevision(Page page, Account author, String content) { + long index = 1; + int delta = content.length(); + + if (page.getLatestRevision() != null) { + index = page.getLatestRevision().index + 1; + } + + if (!page.getRevisions().isEmpty()) { + delta -= page.getRevisions().getLast().content.length(); + } // TODO don't + + this.page = page; + this.index = index; + this.author = author; + this.content = content; + this.delta = delta; + } + + + // Columns + + @ManyToOne(cascade = CascadeType.DETACH) + private Page page; + + private long index = 1; + + @CreationTimestamp + private LocalDateTime timestamp; + + @ManyToOne(cascade = CascadeType.DETACH) + private Account author; + + // TODO wondering about a table only for content and meta and title perhaps + @Column(columnDefinition="text") + private String content; + + private int delta; + + + // Getters + + public Page getPage() { return page; } + public long getIndex() { return index; } + public LocalDateTime getTimestamp() { return timestamp; } + public Account getAuthor() { return author; } + public String getContent() { return content; } + public int getDelta() { return delta; } + + + // Operations + + public static PageRevision findByIndex(Page page, long index) { + return find("page = ?1 and index = ?2", page, index).firstResult(); + } +} diff --git a/src/main/java/eu/m724/talkpages/orm/generator/PathGenerator.java b/src/main/java/eu/m724/talkpages/orm/generator/PathGenerator.java deleted file mode 100644 index 206be2b..0000000 --- a/src/main/java/eu/m724/talkpages/orm/generator/PathGenerator.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.m724.talkpages.orm.generator; - -import eu.m724.talkpages.orm.entity.Page; -import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.id.IdentifierGenerator; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -public class PathGenerator implements IdentifierGenerator { - @Override - public Object generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) { - if (!(o instanceof Page page)) { - throw new HibernateException("The entity must be a Page"); - } - - return URLEncoder.encode(page.title, StandardCharsets.UTF_8).replace("+", "%20"); - } -} diff --git a/src/main/java/eu/m724/talkpages/page/EditResource.java b/src/main/java/eu/m724/talkpages/page/EditResource.java index 1d602d6..d8d8073 100644 --- a/src/main/java/eu/m724/talkpages/page/EditResource.java +++ b/src/main/java/eu/m724/talkpages/page/EditResource.java @@ -1,6 +1,6 @@ package eu.m724.talkpages.page; -import eu.m724.talkpages.orm.entity.Page; +import eu.m724.talkpages.orm.entity.content.Page; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.ws.rs.*; @@ -12,14 +12,14 @@ import jakarta.ws.rs.core.Response; public class EditResource { @CheckedTemplate public static class Templates { - public static native TemplateInstance edit(Page page, String content, String name); - public static native TemplateInstance create(String title, String name); + public static native TemplateInstance edit(Page page, String content); + public static native TemplateInstance create(String title); } @GET @Path("/") public Response editPageBlank() { - return Response.ok().entity(Templates.create("", "198.51.100.42")).build(); + return Response.ok().entity(Templates.create("")).build(); } @GET @@ -28,14 +28,14 @@ public class EditResource { Page page = Page.findByTitle(title); if (page == null) - return Response.ok().entity(Templates.create(title, "198.51.100.42")).build(); + return Response.ok().entity(Templates.create(title)).build(); if (prefilledContent == null || prefilledContent.isBlank()) { - prefilledContent = page.latestRevision.content; + prefilledContent = page.getLatestRevision().getContent(); } // TODO check for permissions - return Response.ok().entity(Templates.edit(page, prefilledContent, "198.51.100.42")).build(); + return Response.ok().entity(Templates.edit(page, prefilledContent)).build(); } } diff --git a/src/main/java/eu/m724/talkpages/page/HistoryResource.java b/src/main/java/eu/m724/talkpages/page/HistoryResource.java index 5d727f5..7149d46 100644 --- a/src/main/java/eu/m724/talkpages/page/HistoryResource.java +++ b/src/main/java/eu/m724/talkpages/page/HistoryResource.java @@ -1,8 +1,8 @@ package eu.m724.talkpages.page; import eu.m724.talkpages.RedirectService; -import eu.m724.talkpages.orm.entity.Page; -import eu.m724.talkpages.orm.entity.PageRevision; +import eu.m724.talkpages.orm.entity.content.Page; +import eu.m724.talkpages.orm.entity.content.PageRevision; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.inject.Inject; @@ -41,7 +41,7 @@ public class HistoryResource { if (page == null) return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(title, redirectService.titleEncoded(title))).build(); // TODO replace with own template - List revisions = page.revisions.reversed(); + List revisions = page.getRevisions().reversed(); return Response.ok().entity(Templates.history(page, revisions)).build(); } diff --git a/src/main/java/eu/m724/talkpages/page/PageResource.java b/src/main/java/eu/m724/talkpages/page/PageResource.java index 6e4eb59..16c6da9 100644 --- a/src/main/java/eu/m724/talkpages/page/PageResource.java +++ b/src/main/java/eu/m724/talkpages/page/PageResource.java @@ -1,8 +1,8 @@ package eu.m724.talkpages.page; import eu.m724.talkpages.RedirectService; -import eu.m724.talkpages.orm.entity.Page; -import eu.m724.talkpages.orm.entity.PageRevision; +import eu.m724.talkpages.orm.entity.content.Page; +import eu.m724.talkpages.orm.entity.content.PageRevision; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import jakarta.inject.Inject; @@ -48,13 +48,14 @@ public class PageResource { } if (revisionId == null) { - if (page.latestRevision.content.startsWith("@")) { - String target = page.latestRevision.content.substring(1); + PageRevision revision = page.getLatestRevision(); + if (revision.getContent().startsWith("@")) { + String target = revision.getContent().substring(1); if (redirectFrom == null) redirectFrom = pageId; - return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build(); // TODO I could really reduce some of this + return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build(); } - return Response.ok().entity(Templates.page(page, page.latestRevision, false)).build(); + return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build(); } else { PageRevision revision = PageRevision.findByIndex(page, revisionId); if (revision != null) { diff --git a/src/main/java/eu/m724/talkpages/page/action/AccountService.java b/src/main/java/eu/m724/talkpages/page/action/AccountService.java new file mode 100644 index 0000000..f30f55d --- /dev/null +++ b/src/main/java/eu/m724/talkpages/page/action/AccountService.java @@ -0,0 +1,24 @@ +package eu.m724.talkpages.page.action; + +import eu.m724.talkpages.orm.entity.auth.Account; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +@ApplicationScoped +public class AccountService { + + // TODO I think it would be better to accept InetAddress + @Transactional + public Account addressAccount(String address) { + Account account = Account.findById(address); + + if (account != null) + return account; + + account = new Account(address); + account.persistAndFlush(); + + return account; + } + +} diff --git a/src/main/java/eu/m724/talkpages/page/action/ActionResource.java b/src/main/java/eu/m724/talkpages/page/action/ActionResource.java index aa120a1..d16dccc 100644 --- a/src/main/java/eu/m724/talkpages/page/action/ActionResource.java +++ b/src/main/java/eu/m724/talkpages/page/action/ActionResource.java @@ -1,8 +1,11 @@ package eu.m724.talkpages.page.action; import eu.m724.talkpages.RedirectService; -import eu.m724.talkpages.orm.entity.Account; -import eu.m724.talkpages.orm.entity.Page; +import eu.m724.talkpages.orm.entity.auth.Account; +import eu.m724.talkpages.orm.entity.content.Page; +import eu.m724.talkpages.orm.entity.auth.Session; +import io.quarkus.security.identity.SecurityIdentity; +import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; @@ -18,27 +21,39 @@ import org.jboss.resteasy.reactive.RestResponse; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.Random; @Path("/action") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_PLAIN) public class ActionResource { + @Inject + SecurityIdentity identity; + + @Inject + AccountService accountService; + @Inject ActionService actionService; @Inject RedirectService redirectService; + @Inject + HttpServerRequest request; + @POST @Path("/create") - @Transactional // TODO make this in a service or whatever is it called public Response create(MultivaluedMap formData) { String title = formData.getFirst("title"); String content = formData.getFirst("content"); + Account account; - Account account = new Account("test user #" + new Random().nextInt(10000)); - account.persistAndFlush(); + if (identity.isAnonymous()) { + account = accountService.addressAccount(request.remoteAddress().hostAddress()); + } else { + Session session = identity.getAttribute("session"); + account = session.getAccount(); + } try { Page page = actionService.createPage(title, content, account); @@ -60,11 +75,18 @@ public class ActionResource { @POST @Path("/edit") - @Transactional // TODO make this in a service or whatever is it called + @Transactional // TODO move to service or something public Response edit(MultivaluedMap formData) { String title = formData.getFirst("title"); String content = formData.getFirst("content"); - Account account = new Account("test user #" + new Random().nextInt(0, 1000)); + Account account; + + if (identity.isAnonymous()) { + account = accountService.addressAccount(request.remoteAddress().hostAddress()); + } else { + Session session = identity.getAttribute("session"); + account = session.getAccount(); + } Page page = Page.findByTitle(title); diff --git a/src/main/java/eu/m724/talkpages/page/action/ActionService.java b/src/main/java/eu/m724/talkpages/page/action/ActionService.java index 884c614..ec37806 100644 --- a/src/main/java/eu/m724/talkpages/page/action/ActionService.java +++ b/src/main/java/eu/m724/talkpages/page/action/ActionService.java @@ -1,8 +1,8 @@ package eu.m724.talkpages.page.action; -import eu.m724.talkpages.orm.entity.Account; -import eu.m724.talkpages.orm.entity.Page; -import eu.m724.talkpages.orm.entity.PageRevision; +import eu.m724.talkpages.orm.entity.auth.Account; +import eu.m724.talkpages.orm.entity.content.Page; +import eu.m724.talkpages.orm.entity.content.PageRevision; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; @@ -10,7 +10,9 @@ import jakarta.transaction.Transactional; public class ActionService { @Transactional Page createPage(String title, String content, Account account) { - // title and content is sanitized so only prohibit if necessary + //account = Account.findById(account.getName()); + + // title and content are sanitized so only prohibit if necessary if (title.contains("/")) { throw new UnacceptableDataException("Title cannot contain slashes (/). Those are used for sub-pages."); } else if (Page.findByTitle(title) != null) { @@ -18,17 +20,12 @@ public class ActionService { } Page page = new Page(title); - page.persist(); - - PageRevision revision = new PageRevision(page); - revision.author = account; - revision.content = content; - revision.delta = content.length(); + PageRevision revision = new PageRevision(page, account, content); page.setLatestRevision(revision); - account.revisions.add(revision); + page.persistAndFlush(); - revision.persistAndFlush(); + account.getRevisions().add(revision); return page; } @@ -37,15 +34,12 @@ public class ActionService { Page editPage(Page page, String newContent, Account account) { // constraints are not checked - PageRevision revision = new PageRevision(page); - revision.author = account; - revision.content = newContent; - revision.delta = newContent.length() - page.latestRevision.content.length(); // TODO optimize + PageRevision revision = new PageRevision(page, account, newContent); page.setLatestRevision(revision); - account.revisions.add(revision); + account.getRevisions().add(revision); - revision.persistAndFlush(); + revision.persist(); return page; } diff --git a/src/main/java/eu/m724/talkpages/template/AuthExtension.java b/src/main/java/eu/m724/talkpages/template/AuthExtension.java index e01e53d..40809e8 100644 --- a/src/main/java/eu/m724/talkpages/template/AuthExtension.java +++ b/src/main/java/eu/m724/talkpages/template/AuthExtension.java @@ -6,11 +6,8 @@ import io.quarkus.qute.TemplateExtension; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.security.identity.SecurityIdentity; -import java.util.Set; - @TemplateExtension(namespace = "user") public class AuthExtension { - public static String name() { try (InstanceHandle handle = Arc.container().instance(CurrentIdentityAssociation.class)) { SecurityIdentity identity = handle.get().getIdentity(); diff --git a/src/main/resources/templates/EditResource/create.html b/src/main/resources/templates/EditResource/create.html index b543206..92cd5ae 100644 --- a/src/main/resources/templates/EditResource/create.html +++ b/src/main/resources/templates/EditResource/create.html @@ -1,6 +1,6 @@

Creating a page

-

Editing as {name}

+

Editing as {user:name}

diff --git a/src/main/resources/templates/EditResource/edit.html b/src/main/resources/templates/EditResource/edit.html index 1155b54..90ff250 100644 --- a/src/main/resources/templates/EditResource/edit.html +++ b/src/main/resources/templates/EditResource/edit.html @@ -1,6 +1,6 @@

Editing {page.title}

-

Editing as {name}

+

Editing as {user:name}

diff --git a/src/main/resources/templates/HistoryResource/history.html b/src/main/resources/templates/HistoryResource/history.html index 5b92066..7e24020 100644 --- a/src/main/resources/templates/HistoryResource/history.html +++ b/src/main/resources/templates/HistoryResource/history.html @@ -1,16 +1,26 @@ -

History of {page.title}

+

History of {page.getTitle}

{#for revision in revisions} {#if page.latestRevision == revision} -#{revision.index} ({revision.delta}) {revision.timestamp.toString()} by {revision.author.name} + + + #{revision.getIndex} + ({revision.getDelta}) {revision.getTimestamp.toString()} by + {revision.getAuthor.getName} + + {#else} -#{revision.index} ({revision.delta}) {revision.timestamp.toString()} by {revision.author.name} + + #{revision.getIndex} + ({revision.getDelta}) {revision.getTimestamp.toString()} by + {revision.getAuthor.getName} + {/if}
{/for} \ No newline at end of file diff --git a/src/main/resources/templates/PageResource/notFound.html b/src/main/resources/templates/PageResource/notFound.html index 1e2970d..2751302 100644 --- a/src/main/resources/templates/PageResource/notFound.html +++ b/src/main/resources/templates/PageResource/notFound.html @@ -6,7 +6,7 @@

Are you looking for:

Actions:

diff --git a/src/main/resources/templates/PageResource/page.html b/src/main/resources/templates/PageResource/page.html index 031c88a..57cef6e 100644 --- a/src/main/resources/templates/PageResource/page.html +++ b/src/main/resources/templates/PageResource/page.html @@ -5,9 +5,9 @@ {#if old} {#if page.latestRevision != revision}

- You are viewing an outdated revision #{revision.index} of this page from {revision.timestamp.toString()}, authored by {revision.author.name}. + You are viewing an outdated revision #{revision.getIndex} of this page from {revision.getTimestamp.toString()}, authored by {revision.getAuthor.getName}.
- See current version + See current version

{#else}

This is the current revision #{revision.index} authored by {revision.author.name}.

@@ -20,5 +20,5 @@

-Modified {revision.timestamp.toString()} | Full history | Edit - {#if !page.title.startsWith("Talk:")} | Talk{/if} \ No newline at end of file +Modified {revision.timestamp.toString()} | Full history | Edit + {#if !page.title.startsWith("Talk:")} | Talk{/if} \ No newline at end of file diff --git a/src/main/resources/templates/PageResource/revisionNotFound.html b/src/main/resources/templates/PageResource/revisionNotFound.html index 2526ccd..9069e2b 100644 --- a/src/main/resources/templates/PageResource/revisionNotFound.html +++ b/src/main/resources/templates/PageResource/revisionNotFound.html @@ -1,7 +1,7 @@ -

{page.title}

+

{page.getTitle}

There is no revision #{revisionId}. \ No newline at end of file