Namespaces,
finally. Creation of them still TODO Also brought back account IDs.
This commit is contained in:
parent
fa3fdc263b
commit
6197b4ec9a
12 changed files with 153 additions and 40 deletions
|
@ -22,9 +22,6 @@ public class Startup {
|
||||||
@ConfigProperty(name = "talkpages.systemUser.name")
|
@ConfigProperty(name = "talkpages.systemUser.name")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@ConfigProperty(name = "talkpages.createExamplePage")
|
|
||||||
private boolean createExamplePage;
|
|
||||||
|
|
||||||
void installStaticRoute(@Observes StartupEvent startupEvent, Router router) {
|
void installStaticRoute(@Observes StartupEvent startupEvent, Router router) {
|
||||||
router.route()
|
router.route()
|
||||||
.path("/static/*")
|
.path("/static/*")
|
||||||
|
@ -33,7 +30,7 @@ public class Startup {
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void examplePage(@Observes StartupEvent ignoredEvent) {
|
public void examplePage(@Observes StartupEvent ignoredEvent) {
|
||||||
if (Account.findById(1) != null) {
|
if (Account.findByName(username) != null) {
|
||||||
// system user exists so assuming this is not the first run
|
// system user exists so assuming this is not the first run
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -43,15 +40,18 @@ public class Startup {
|
||||||
Account account = new Account(username);
|
Account account = new Account(username);
|
||||||
account.persistAndFlush();
|
account.persistAndFlush();
|
||||||
|
|
||||||
if (createExamplePage) {
|
Page talkPagesPage = addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
|
||||||
addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
|
|
||||||
addPage(account, "Talkpages", "ambiguous for [TalkPages]");
|
addPage(account, "Talkpages", "ambiguous for [TalkPages]");
|
||||||
addPage(account, "TP", "@TalkPages");
|
addPage(account, "TP", "@TalkPages");
|
||||||
}
|
|
||||||
|
Page tosPage = addPage(account, "Terms of Service", "TODO");
|
||||||
|
addPage(account, "ToS", "@TalkPages/Terms of Service");
|
||||||
|
|
||||||
|
tosPage.setParentPage(talkPagesPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void addPage(Account account, String title, String content) {
|
public Page addPage(Account account, String title, String content) {
|
||||||
Page page = new Page(title);
|
Page page = new Page(title);
|
||||||
PageRevision revision = new PageRevision(page, account, content);
|
PageRevision revision = new PageRevision(page, account, content);
|
||||||
|
|
||||||
|
@ -59,6 +59,6 @@ public class Startup {
|
||||||
page.persistAndFlush();
|
page.persistAndFlush();
|
||||||
|
|
||||||
account.getRevisions().add(revision);
|
account.getRevisions().add(revision);
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ public class AuthService {
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
Session register(String username, String password) throws UsernameExistsException {
|
Session register(String username, String password) throws UsernameExistsException {
|
||||||
Account account = Account.findById(username);
|
Account account = Account.findByName(username);
|
||||||
|
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
throw new UsernameExistsException();
|
throw new UsernameExistsException();
|
||||||
|
@ -63,7 +63,7 @@ public class AuthService {
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
Session authenticate(String username, String password) throws InvalidCredentialsException {
|
Session authenticate(String username, String password) throws InvalidCredentialsException {
|
||||||
Account account = Account.findById(username);
|
Account account = Account.findByName(username);
|
||||||
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
throw new InvalidCredentialsException(false);
|
throw new InvalidCredentialsException(false);
|
||||||
|
|
|
@ -14,8 +14,13 @@ import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
// TODO consider moving away authentication to allow like multiple profiles on one account
|
// TODO consider moving away authentication to allow like multiple profiles on one account
|
||||||
|
// TODO also consider giving different, namespaced ids to system and addressed accounts
|
||||||
@Entity
|
@Entity
|
||||||
public class Account extends PanacheEntityBase {
|
@Table(
|
||||||
|
indexes = @Index(name = "idx_name", columnList = "name"),
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = "name")
|
||||||
|
)
|
||||||
|
public class Account extends PanacheEntity {
|
||||||
private Account() {}
|
private Account() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,7 +51,6 @@ public class Account extends PanacheEntityBase {
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
|
|
||||||
@Id
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
private String slug;
|
private String slug;
|
||||||
|
@ -72,4 +76,11 @@ public class Account extends PanacheEntityBase {
|
||||||
public List<PageRevision> getRevisions() { return revisions; }
|
public List<PageRevision> getRevisions() { return revisions; }
|
||||||
|
|
||||||
public boolean isSystemAccount() { return password == null; }
|
public boolean isSystemAccount() { return password == null; }
|
||||||
|
|
||||||
|
|
||||||
|
// Operations
|
||||||
|
|
||||||
|
public static Account findByName(String name) {
|
||||||
|
return Account.find("name", name).firstResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,29 @@
|
||||||
package eu.m724.talkpages.orm.entity.content;
|
package eu.m724.talkpages.orm.entity.content;
|
||||||
|
|
||||||
|
import eu.m724.talkpages.page.action.NoSuchNamespaceException;
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import org.hibernate.annotations.GenericGenerator;
|
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
indexes = @Index(name = "idx_title", columnList = "title"),
|
indexes = @Index(name = "idx_title", columnList = "title"),
|
||||||
uniqueConstraints = @UniqueConstraint(columnNames = "title")
|
uniqueConstraints = @UniqueConstraint(columnNames = {"title", "parentPage"})
|
||||||
)
|
)
|
||||||
public class Page extends PanacheEntityBase {
|
public class Page extends PanacheEntity {
|
||||||
private Page() {}
|
private Page() {}
|
||||||
|
|
||||||
public Page(String title) {
|
public Page(String title) {
|
||||||
// TODO maybe map space to _
|
|
||||||
this.slug = URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
|
|
||||||
this.title = title;
|
this.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
|
|
||||||
@Id
|
|
||||||
private String slug;
|
|
||||||
|
|
||||||
// TODO is this really necessary
|
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
|
@ -38,21 +32,103 @@ public class Page extends PanacheEntityBase {
|
||||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<PageRevision> revisions = new ArrayList<>();
|
private List<PageRevision> revisions = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, mappedBy = "parentPage")
|
||||||
|
private List<Page> subpages = new ArrayList<>();
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "parentPage_id")
|
||||||
|
private Page parentPage = null;
|
||||||
|
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
|
||||||
|
@PreRemove
|
||||||
|
private void preRemove() {
|
||||||
|
subpages.forEach(page -> page.parentPage = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
public String getSlug() { return slug; }
|
|
||||||
public String getTitle() { return title; }
|
public String getTitle() { return title; }
|
||||||
public PageRevision getLatestRevision() { return latestRevision; }
|
public PageRevision getLatestRevision() { return latestRevision; }
|
||||||
public List<PageRevision> getRevisions() { return revisions; }
|
public List<PageRevision> getRevisions() { return revisions; }
|
||||||
|
public List<Page> getSubpages() { return subpages; }
|
||||||
|
public Page getParentPage() { return parentPage; }
|
||||||
|
|
||||||
public void setLatestRevision(PageRevision pageRevision) {
|
public void setLatestRevision(PageRevision pageRevision) {
|
||||||
revisions.add(pageRevision);
|
revisions.add(pageRevision);
|
||||||
latestRevision = pageRevision;
|
latestRevision = pageRevision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setParentPage(Page parentPage) {
|
||||||
|
// Remove from current parent, if any
|
||||||
|
if (this.parentPage != null)
|
||||||
|
parentPage.subpages.remove(this);
|
||||||
|
|
||||||
|
// Set the parent page and add as a child
|
||||||
|
this.parentPage = parentPage;
|
||||||
|
if (parentPage != null)
|
||||||
|
parentPage.getSubpages().add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSlug() {
|
||||||
|
// TODO maybe map space to _
|
||||||
|
return URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chain of parents. The broadest (root) one is the first one.<br>
|
||||||
|
* The list is empty if there's no parents.
|
||||||
|
*
|
||||||
|
* @return chain of parents
|
||||||
|
*/
|
||||||
|
public List<Page> getChain() {
|
||||||
|
List<Page> parents = new ArrayList<>();
|
||||||
|
|
||||||
|
Page parent = this.getParentPage();
|
||||||
|
while (parent != null) {
|
||||||
|
parents.add(parent);
|
||||||
|
parent = parent.getParentPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parents.reversed();
|
||||||
|
}
|
||||||
|
|
||||||
// Operations
|
// Operations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a {@link Page} by title. The title must not be encoded.<br>
|
||||||
|
* Separate namespaces with a slash (<code>/</code>)<br>
|
||||||
|
*
|
||||||
|
* @param title the title, not encoded
|
||||||
|
* @return page if found, else null
|
||||||
|
* @throws IllegalStateException if
|
||||||
|
*/
|
||||||
|
public static Page findByPath(String title) {
|
||||||
|
String[] parts = title.split("/", -1);
|
||||||
|
|
||||||
|
Page page = findByTitle(parts[0]);
|
||||||
|
|
||||||
|
for (int i=1; i<parts.length; i++) {
|
||||||
|
String part = parts[i];
|
||||||
|
|
||||||
|
// those checks apply to the previous iteration, but won't run on the last one
|
||||||
|
if (page == null)
|
||||||
|
break; // TODO maybe raise an error here
|
||||||
|
|
||||||
|
if (page.title.isBlank())
|
||||||
|
throw new IllegalStateException("Can't have empty title in the middle"); // TODO also enforce
|
||||||
|
|
||||||
|
|
||||||
|
page = page.getSubpages().stream()
|
||||||
|
.filter(p -> p.getTitle().equals(part))
|
||||||
|
.findFirst().orElse(null); // TODO optimize
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
public static Page findByTitle(String title) {
|
public static Page findByTitle(String title) {
|
||||||
return find("title", title).firstResult();
|
return find("title", title).firstResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
indexes = @Index(columnList = "page_slug"),
|
indexes = @Index(columnList = "page_id"),
|
||||||
uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"})
|
uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"})
|
||||||
)
|
)
|
||||||
public class PageRevision extends PanacheEntity {
|
public class PageRevision extends PanacheEntity {
|
||||||
|
@ -49,7 +49,7 @@ public class PageRevision extends PanacheEntity {
|
||||||
private Account author;
|
private Account author;
|
||||||
|
|
||||||
// TODO wondering about a table only for content and meta and title perhaps
|
// TODO wondering about a table only for content and meta and title perhaps
|
||||||
@Column(columnDefinition="text")
|
@Column(columnDefinition = "text")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
private int delta;
|
private int delta;
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class HistoryResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{title}")
|
@Path("/{title:.+}")
|
||||||
public Response pageHistory(@PathParam("title") String title) {
|
public Response pageHistory(@PathParam("title") String title) {
|
||||||
Page page = Page.findByTitle(title);
|
Page page = Page.findByTitle(title);
|
||||||
|
|
||||||
|
|
|
@ -35,16 +35,16 @@ public class PageResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{pageId}")
|
@Path("/{title:.+}")
|
||||||
public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
|
public Response viewPage(@PathParam("title") String title, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
|
||||||
Page page = Page.findByTitle(pageId);
|
Page page = Page.findByPath(title);
|
||||||
|
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
List<Page> suggestions = Page.findByTitleIgnoreCase(pageId);
|
List<Page> suggestions = Page.findByTitleIgnoreCase(title);
|
||||||
if (suggestions.size() == 1) {
|
if (suggestions.size() == 1) {
|
||||||
return redirectService.page(suggestions.getFirst()).build();
|
return redirectService.page(suggestions.getFirst()).build();
|
||||||
}
|
}
|
||||||
return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(pageId, suggestions)).build();
|
return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(title, suggestions)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (revisionId == null) {
|
if (revisionId == null) {
|
||||||
|
@ -52,7 +52,7 @@ public class PageResource {
|
||||||
if (revision.getContent().startsWith("@")) {
|
if (revision.getContent().startsWith("@")) {
|
||||||
String target = revision.getContent().substring(1);
|
String target = revision.getContent().substring(1);
|
||||||
if (redirectFrom == null)
|
if (redirectFrom == null)
|
||||||
redirectFrom = pageId;
|
redirectFrom = title;
|
||||||
return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build();
|
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.getLatestRevision(), false)).build();
|
return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build();
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package eu.m724.talkpages.page.action;
|
||||||
|
|
||||||
|
public class NoSuchNamespaceException extends RuntimeException {
|
||||||
|
public final String namespace;
|
||||||
|
|
||||||
|
public NoSuchNamespaceException(String namespace) {
|
||||||
|
this.namespace = namespace;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
talkpages.homePage=/
|
talkpages.homePage=/
|
||||||
talkpages.systemUser.name=System
|
talkpages.systemUser.name=System
|
||||||
talkpages.createExamplePage=true
|
|
||||||
|
|
||||||
quarkus.http.auth.basic=true
|
quarkus.http.auth.basic=true
|
||||||
|
|
||||||
quarkus.hibernate-orm.database.generation=drop-and-create
|
quarkus.hibernate-orm.database.generation=update
|
||||||
|
|
||||||
quarkus.datasource.db-kind=h2
|
quarkus.datasource.db-kind=h2
|
||||||
quarkus.datasource.username=username-default
|
quarkus.datasource.username=username-default
|
||||||
|
|
0
src/main/resources/static/style.css
Normal file
0
src/main/resources/static/style.css
Normal file
|
@ -1,4 +1,4 @@
|
||||||
{#include layout}
|
{#include layout customTitle=true noHeading=true}
|
||||||
{#header}
|
{#header}
|
||||||
{#if http:param("redirectFrom") != null}
|
{#if http:param("redirectFrom") != null}
|
||||||
<p>Redirected from {http:param("redirectFrom")}</p>
|
<p>Redirected from {http:param("redirectFrom")}</p>
|
||||||
|
@ -15,9 +15,23 @@
|
||||||
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
|
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{/header}
|
{/header}
|
||||||
|
|
||||||
{#pageTitle}{page.title}{/pageTitle}
|
{#pageTitle}{page.getTitle} - TalkPages{/pageTitle}
|
||||||
|
|
||||||
|
{#if page.getChain.isEmpty} <!-- TODO don't repeat that -->
|
||||||
|
<h1>{page.getTitle}</h1>
|
||||||
|
{#else}
|
||||||
|
<h1>
|
||||||
|
<small>
|
||||||
|
{#for parent in page.getChain}
|
||||||
|
<a href="/page/{parent.getSlug}">{parent.getTitle}</a> /
|
||||||
|
{/for}
|
||||||
|
</small>
|
||||||
|
{page.getTitle}
|
||||||
|
</h1>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{revision.content.sanitizeContent.raw}
|
{revision.content.sanitizeContent.raw}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here -->
|
<!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here -->
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
{#if theme:darkTheme}
|
{#if theme:darkTheme}
|
||||||
<link rel="stylesheet" href="/static/dark.css">
|
<link rel="stylesheet" href="/static/dark.css">
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -11,7 +12,10 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{#insert header}{/}
|
{#insert header}{/}
|
||||||
|
|
||||||
|
{#if !noHeading??}
|
||||||
<h1>{#insert pageTitle /}</h1>
|
<h1>{#insert pageTitle /}</h1>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#insert}{/}
|
{#insert}{/}
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in a new issue