huge refactoring

fixed everything except for complicated hibernate stuff
This commit is contained in:
Minecon724 2024-09-13 14:56:33 +02:00
parent 20d0e142c0
commit 9ef47b2833
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
29 changed files with 434 additions and 355 deletions

3
TODO.md Normal file
View file

@ -0,0 +1,3 @@
Review:
- `@Transactional`
- `.persist()` (especially `.persistAndFlush()`)

View file

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

View file

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

View file

@ -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("/")

View file

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

View file

@ -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,28 +25,26 @@ 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);
if (session != null) {
RegisteredUser user = session.user;
Account account = session.getAccount();
QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal(user.username))
.addRoles(user.roles)
.setPrincipal(new QuarkusPrincipal(account.getName()))
.addRoles(account.getRoles())
.addAttribute("session", session)
.build();
return (SecurityIdentity) identity;
}
}
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setAnonymous(true).build();
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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