huge refactoring
fixed everything except for complicated hibernate stuff
This commit is contained in:
parent
20d0e142c0
commit
9ef47b2833
29 changed files with 434 additions and 355 deletions
3
TODO.md
Normal file
3
TODO.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
Review:
|
||||
- `@Transactional`
|
||||
- `.persist()` (especially `.persistAndFlush()`)
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("/")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||
|
||||
public Uni<SecurityIdentity> 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();
|
||||
|
|
|
@ -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<PageRevision> revisions = new ArrayList<>();
|
||||
|
||||
@OneToOne
|
||||
public RegisteredUser user;
|
||||
}
|
|
@ -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<PageRevision> revisions = new ArrayList<>();
|
||||
|
||||
public static Page findByTitle(String title) {
|
||||
return find("title", title).firstResult();
|
||||
}
|
||||
|
||||
public static List<Page> findByTitleIgnoreCase(String title) {
|
||||
return find("lower(title) = ?1", title.toLowerCase()).list();
|
||||
}
|
||||
|
||||
public void setLatestRevision(PageRevision pageRevision) {
|
||||
revisions.add(pageRevision);
|
||||
latestRevision = pageRevision;
|
||||
}
|
||||
}
|
|
@ -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<br>
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -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<String> roles;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = false)
|
||||
public Set<Session> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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}<br>
|
||||
* 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;
|
||||
}
|
||||
}
|
75
src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java
Normal file
75
src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java
Normal file
|
@ -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.<br>
|
||||
* 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.<br>
|
||||
* System accounts don't have passwords, so can't be logged into.<br>
|
||||
* 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<String> roles = new HashSet<>();
|
||||
|
||||
@OneToMany(orphanRemoval = true, cascade = CascadeType.ALL)
|
||||
private Set<Session> sessions = new HashSet<>();
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = false, fetch = FetchType.EAGER)
|
||||
private List<PageRevision> revisions = new ArrayList<>();
|
||||
|
||||
|
||||
// Getters
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getPassword() { return password; }
|
||||
public Set<String> getRoles() { return roles; }
|
||||
public Set<Session> getSessions() { return sessions; }
|
||||
public List<PageRevision> getRevisions() { return revisions; }
|
||||
|
||||
public boolean isSystemAccount() { return password == null; }
|
||||
}
|
65
src/main/java/eu/m724/talkpages/orm/entity/auth/Session.java
Normal file
65
src/main/java/eu/m724/talkpages/orm/entity/auth/Session.java
Normal file
|
@ -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}<br>
|
||||
* A token is randomly generated. It's 64 bytes and is encoded in Base64.<br>
|
||||
* 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}<br>
|
||||
* A token is randomly generated. It's 64 bytes and is encoded in Base64.<br>
|
||||
*
|
||||
* @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; }
|
||||
|
||||
}
|
63
src/main/java/eu/m724/talkpages/orm/entity/content/Page.java
Normal file
63
src/main/java/eu/m724/talkpages/orm/entity/content/Page.java
Normal file
|
@ -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<PageRevision> revisions = new ArrayList<>();
|
||||
|
||||
|
||||
// Getters
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public PageRevision getLatestRevision() { return latestRevision; }
|
||||
public List<PageRevision> 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<Page> findByTitleIgnoreCase(String title) {
|
||||
return find("lower(title) = ?1", title.toLowerCase()).list();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<PageRevision> revisions = page.revisions.reversed();
|
||||
List<PageRevision> revisions = page.getRevisions().reversed();
|
||||
|
||||
return Response.ok().entity(Templates.history(page, revisions)).build();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String, String> 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<String, String> 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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<CurrentIdentityAssociation> handle = Arc.container().instance(CurrentIdentityAssociation.class)) {
|
||||
SecurityIdentity identity = handle.get().getIdentity();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h1>Creating a page</h1>
|
||||
|
||||
<p>Editing as {name}</p>
|
||||
<p>Editing as {user:name}</p>
|
||||
|
||||
<form action="/action/create" method="post">
|
||||
<label for="title">Title</label>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h1>Editing {page.title}</h1>
|
||||
|
||||
<p>Editing as {name}</p>
|
||||
<p>Editing as {user:name}</p>
|
||||
|
||||
<form action="/action/edit" method="post">
|
||||
<input type="hidden" name="title" value="{page.title}">
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
<h1>History of {page.title}</h1>
|
||||
<h1>History of {page.getTitle}</h1>
|
||||
|
||||
{#for revision in revisions}
|
||||
{#if page.latestRevision == revision}
|
||||
<span><strong><a href="/page/{page.title}?revision={revision.index}">#{revision.index}</a> ({revision.delta}) {revision.timestamp.toString()} by <a href="/user/{revision.author.id}">{revision.author.name}</a></strong></span>
|
||||
<span>
|
||||
<strong>
|
||||
<a href="/page/{page.getTitle}?revision={revision.getIndex}">#{revision.getIndex}</a>
|
||||
({revision.getDelta}) {revision.getTimestamp.toString()} by
|
||||
<a href="/user/{revision.getAuthor.getSlug}">{revision.getAuthor.getName}</a>
|
||||
</strong>
|
||||
</span>
|
||||
{#else}
|
||||
<span><a href="/page/{page.title}?revision={revision.index}">#{revision.index}</a> ({revision.delta}) {revision.timestamp.toString()} by <a href="/user/{revision.author.id}">{revision.author.name}</a></span>
|
||||
<span>
|
||||
<a href="/page/{page.getTitle}?revision={revision.getIndex}">#{revision.getIndex}</a>
|
||||
({revision.getDelta}) {revision.getTimestamp.toString()} by
|
||||
<a href="/user/{revision.getAuthor.getSlug}">{revision.getAuthor.getName}</a>
|
||||
</span>
|
||||
{/if}
|
||||
<br>
|
||||
{/for}
|
||||
|
||||
<ul>
|
||||
<li><a href="/page/{page.path}">Back to {page.title}</a></li>
|
||||
<li><a href="/page/Talk:{page.path}">Talk:{page.title}</a></li>
|
||||
<li><a href="/edit/{page.path}">Edit page</a></li>
|
||||
<li><a href="/page/{page.getSlug}">Back to {page.getTitle}</a></li>
|
||||
<li><a href="/page/Talk:{page.getSlug}">Talk:{page.getTitle}</a></li>
|
||||
<li><a href="/edit/{page.getSlug}">Edit page</a></li>
|
||||
</ul>
|
|
@ -6,7 +6,7 @@
|
|||
<p>Are you looking for:</p>
|
||||
<ul>
|
||||
{#for suggestion in suggestions}
|
||||
<li><a href="/page/{suggestion.path}">{suggestion.title}</a></li>
|
||||
<li><a href="/page/{suggestion.getSlug}">{suggestion.getTitle}</a></li>
|
||||
{/for}
|
||||
</ul>
|
||||
<p>Actions:</p>
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
{#if old}
|
||||
{#if page.latestRevision != revision}
|
||||
<h3>
|
||||
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}.
|
||||
<br>
|
||||
<a href="/page/{page.title}">See current version</a>
|
||||
<a href="/page/{page.getTitle}">See current version</a>
|
||||
</h3>
|
||||
{#else}
|
||||
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
|
||||
|
@ -20,5 +20,5 @@
|
|||
|
||||
<br><br>
|
||||
|
||||
<small>Modified {revision.timestamp.toString()} | <a href="/history/{page.path}">Full history</a> | <a href="/edit/{page.path}">Edit</a>
|
||||
{#if !page.title.startsWith("Talk:")} | <a href="/page/Talk:{page.path}">Talk</a>{/if}</small>
|
||||
<small>Modified {revision.timestamp.toString()} | <a href="/history/{page.getSlug}">Full history</a> | <a href="/edit/{page.getSlug}">Edit</a>
|
||||
{#if !page.title.startsWith("Talk:")} | <a href="/page/Talk:{page.getSlug}">Talk</a>{/if}</small>
|
|
@ -1,7 +1,7 @@
|
|||
<h1>{page.title}</h1>
|
||||
<h1>{page.getTitle}</h1>
|
||||
|
||||
There is no revision #{revisionId}.
|
||||
<ul>
|
||||
<li><a href="/history/{page.path}">View history</a></li>
|
||||
<li><a href="/page/{page.path}">Back to {page.title}</a></li>
|
||||
<li><a href="/history/{page.getSlug}">View history</a></li>
|
||||
<li><a href="/page/{page.getSlug}">Back to {page.getTitle}</a></li>
|
||||
</ul>
|
Loading…
Reference in a new issue