accounts
This commit is contained in:
parent
5c87e3d86d
commit
20d0e142c0
14 changed files with 455 additions and 5 deletions
11
pom.xml
11
pom.xml
|
@ -62,6 +62,17 @@
|
||||||
<artifactId>quarkus-junit5</artifactId>
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- custom auth -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.mkammerer</groupId>
|
||||||
|
<artifactId>argon2-jvm-nolibs</artifactId>
|
||||||
|
<version>2.11</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
145
src/main/java/eu/m724/talkpages/auth/AuthResource.java
Normal file
145
src/main/java/eu/m724/talkpages/auth/AuthResource.java
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package eu.m724.talkpages.auth;
|
||||||
|
|
||||||
|
import eu.m724.talkpages.orm.entity.Session;
|
||||||
|
import io.quarkus.qute.CheckedTemplate;
|
||||||
|
import io.quarkus.qute.TemplateInstance;
|
||||||
|
import io.quarkus.security.Authenticated;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
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)
|
||||||
|
public class AuthResource {
|
||||||
|
@Inject
|
||||||
|
AuthService authService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity identity;
|
||||||
|
|
||||||
|
@CheckedTemplate
|
||||||
|
public static class Templates {
|
||||||
|
public static native TemplateInstance auth(String message);
|
||||||
|
public static native TemplateInstance authenticated(String username);
|
||||||
|
public static native TemplateInstance register(String username, String message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/")
|
||||||
|
public TemplateInstance auth(@QueryParam("message") String message) {
|
||||||
|
if (identity.isAnonymous()) {
|
||||||
|
return Templates.auth(message);
|
||||||
|
} else {
|
||||||
|
String username = identity.getPrincipal().getName();
|
||||||
|
return Templates.authenticated(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/register")
|
||||||
|
public TemplateInstance register(@QueryParam("username") String username, @QueryParam("message") String message) {
|
||||||
|
return Templates.register(username, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/logout")
|
||||||
|
@Authenticated
|
||||||
|
public Response logout() {
|
||||||
|
Session session = identity.getAttribute("session");
|
||||||
|
authService.logout(session);
|
||||||
|
|
||||||
|
NewCookie cookie = new NewCookie.Builder("session-token")
|
||||||
|
.value("")
|
||||||
|
.maxAge(1)
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Response.temporaryRedirect(URI.create("/")).cookie(cookie).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/action/login")
|
||||||
|
public Response actionLogin(MultivaluedMap<String, String> formData) {
|
||||||
|
String username = formData.getFirst("username");
|
||||||
|
String password = formData.getFirst("password");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Session session = authService.authenticate(username, password);
|
||||||
|
|
||||||
|
NewCookie cookie = new NewCookie.Builder("session-token")
|
||||||
|
.value(session.token)
|
||||||
|
.maxAge(604800)// TODO make the age respect session's expires
|
||||||
|
.httpOnly(true)
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Response.temporaryRedirect(URI.create("/auth")).cookie(cookie).build();
|
||||||
|
} catch (AuthService.InvalidCredentialsException e) {
|
||||||
|
String usernameEncoded = URLEncoder.encode(username, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (e.password) {
|
||||||
|
return Response
|
||||||
|
.temporaryRedirect(URI.create("/auth?message=Incorrect+password&username=" + usernameEncoded))
|
||||||
|
.status(Response.Status.SEE_OTHER)
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
NewCookie cookie = new NewCookie.Builder("password")
|
||||||
|
.value(password) // TODO don't do that
|
||||||
|
.maxAge(3600)
|
||||||
|
.httpOnly(true)
|
||||||
|
.path("/auth/action/register")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Response
|
||||||
|
.temporaryRedirect(URI.create("/auth/register?username=" + usernameEncoded))
|
||||||
|
.status(Response.Status.SEE_OTHER)
|
||||||
|
.cookie(cookie)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/action/register")
|
||||||
|
public Response actionRegister(@Context HttpHeaders headers, MultivaluedMap<String, String> formData) {
|
||||||
|
String username = formData.getFirst("username");
|
||||||
|
String password = formData.getFirst("password");
|
||||||
|
String comparedPassword = headers.getCookies().get("password").getValue();
|
||||||
|
String usernameEncoded = URLEncoder.encode(username, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (!password.equals(comparedPassword)) {
|
||||||
|
return Response
|
||||||
|
.temporaryRedirect(URI.create("/auth/register?message=Password+does+not+match&username=" + usernameEncoded))
|
||||||
|
.status(Response.Status.SEE_OTHER)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Session session = authService.register(username, password);
|
||||||
|
|
||||||
|
NewCookie cookie = new NewCookie.Builder("session-token")
|
||||||
|
.value(session.token)
|
||||||
|
.maxAge(604800)// TODO make the age respect session's expires
|
||||||
|
.httpOnly(true)
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Response
|
||||||
|
.temporaryRedirect(URI.create("/"))
|
||||||
|
.status(Response.Status.SEE_OTHER)
|
||||||
|
.cookie(cookie)
|
||||||
|
.build();
|
||||||
|
} catch (AuthService.UsernameExistsException e) {
|
||||||
|
return Response
|
||||||
|
.temporaryRedirect(URI.create("/auth/register?message=Username+is+taken&username=" + usernameEncoded))
|
||||||
|
.status(Response.Status.SEE_OTHER)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
src/main/java/eu/m724/talkpages/auth/AuthService.java
Normal file
84
src/main/java/eu/m724/talkpages/auth/AuthService.java
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
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 jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuthService {
|
||||||
|
private final Argon2 argon2 = Argon2Factory.create();
|
||||||
|
private final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new {@link RegisteredUser}
|
||||||
|
*
|
||||||
|
* @param username username
|
||||||
|
* @param password password
|
||||||
|
* @return a new {@link Session} for the new user
|
||||||
|
* @throws UsernameExistsException if user with same username already exists
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
Session register(String username, String password) throws UsernameExistsException {
|
||||||
|
RegisteredUser user = RegisteredUser.find("username", username).firstResult();
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
throw new UsernameExistsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
String hashedPassword = argon2.hash(10, 65536, 1, password);
|
||||||
|
user = RegisteredUser.add(username, hashedPassword, "user");
|
||||||
|
|
||||||
|
return Session.createForUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void logout(Session session) {
|
||||||
|
session.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
Session validateSessionToken(String sessionToken) {
|
||||||
|
Session session = Session.find("token", sessionToken).firstResult();
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
if (session.expires.isAfter(LocalDateTime.now())) {
|
||||||
|
return session;
|
||||||
|
} else {
|
||||||
|
session.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
Session authenticate(String username, String password) throws InvalidCredentialsException {
|
||||||
|
RegisteredUser user = RegisteredUser.find("username", username).firstResult();
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new InvalidCredentialsException(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!argon2.verify(user.password, password)) {
|
||||||
|
throw new InvalidCredentialsException(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Session.createForUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InvalidCredentialsException extends Exception {
|
||||||
|
public final boolean password;
|
||||||
|
|
||||||
|
public InvalidCredentialsException(boolean password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UsernameExistsException extends Exception {}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package eu.m724.talkpages.auth;
|
||||||
|
|
||||||
|
import eu.m724.talkpages.orm.entity.Session;
|
||||||
|
import eu.m724.talkpages.orm.entity.RegisteredUser;
|
||||||
|
import io.quarkus.security.identity.IdentityProviderManager;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import io.quarkus.security.runtime.QuarkusPrincipal;
|
||||||
|
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||||
|
import io.quarkus.vertx.http.runtime.security.ChallengeData;
|
||||||
|
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import io.smallrye.mutiny.infrastructure.Infrastructure;
|
||||||
|
import io.vertx.core.http.Cookie;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import jakarta.annotation.Priority;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.inject.Alternative;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
@Alternative
|
||||||
|
@Priority(1)
|
||||||
|
@ApplicationScoped
|
||||||
|
public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||||
|
@Inject
|
||||||
|
AuthService authService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||||
|
|
||||||
|
return Uni.createFrom().item(() -> {
|
||||||
|
Cookie cookie = context.request().getCookie("session-token");
|
||||||
|
|
||||||
|
if (cookie == null)
|
||||||
|
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setAnonymous(true).build();
|
||||||
|
|
||||||
|
String sessionToken = context.request().getCookie("session-token").getValue();
|
||||||
|
Session session = authService.validateSessionToken(sessionToken);
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
RegisteredUser user = session.user;
|
||||||
|
|
||||||
|
QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder()
|
||||||
|
.setPrincipal(new QuarkusPrincipal(user.username))
|
||||||
|
.addRoles(user.roles)
|
||||||
|
.addAttribute("session", session)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return (SecurityIdentity) identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setAnonymous(true).build();
|
||||||
|
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uni<ChallengeData> getChallenge(RoutingContext context) {
|
||||||
|
return Uni.createFrom().nullItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uni<Boolean> sendChallenge(RoutingContext context) {
|
||||||
|
// Always return false to indicate that no challenge was sent
|
||||||
|
return Uni.createFrom().item(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,7 @@
|
||||||
package eu.m724.talkpages.orm.entity;
|
package eu.m724.talkpages.orm.entity;
|
||||||
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||||
import jakarta.persistence.CascadeType;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.OneToMany;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -28,4 +25,7 @@ public class Account extends PanacheEntity {
|
||||||
*/
|
*/
|
||||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = false)
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = false)
|
||||||
public List<PageRevision> revisions = new ArrayList<>();
|
public List<PageRevision> revisions = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToOne
|
||||||
|
public RegisteredUser user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
src/main/java/eu/m724/talkpages/orm/entity/Session.java
Normal file
43
src/main/java/eu/m724/talkpages/orm/entity/Session.java
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
20
src/main/java/eu/m724/talkpages/template/AuthExtension.java
Normal file
20
src/main/java/eu/m724/talkpages/template/AuthExtension.java
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package eu.m724.talkpages.template;
|
||||||
|
|
||||||
|
import io.quarkus.arc.Arc;
|
||||||
|
import io.quarkus.arc.InstanceHandle;
|
||||||
|
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();
|
||||||
|
return identity.isAnonymous() ? null : identity.getPrincipal().getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.talkpages;
|
package eu.m724.talkpages.template;
|
||||||
|
|
||||||
import io.quarkus.qute.TemplateExtension;
|
import io.quarkus.qute.TemplateExtension;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
@ -2,6 +2,8 @@ talkpages.homePage=/
|
||||||
talkpages.systemUser.name=System
|
talkpages.systemUser.name=System
|
||||||
talkpages.createExamplePage=true
|
talkpages.createExamplePage=true
|
||||||
|
|
||||||
|
quarkus.http.auth.basic=true
|
||||||
|
|
||||||
quarkus.hibernate-orm.database.generation=drop-and-create
|
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||||
|
|
||||||
quarkus.datasource.db-kind=h2
|
quarkus.datasource.db-kind=h2
|
||||||
|
|
18
src/main/resources/templates/AuthResource/auth.html
Normal file
18
src/main/resources/templates/AuthResource/auth.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
<p>To log in, submit your credentials.</p>
|
||||||
|
<p>To register, submit desired credentials.</p>
|
||||||
|
|
||||||
|
{#if message != null}
|
||||||
|
<h3>{message}</h3>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form method="post" action="/auth/action/login">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username">
|
||||||
|
<br>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
<br>
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
|
@ -0,0 +1,2 @@
|
||||||
|
<p>Logged in as {username}</p>
|
||||||
|
<p><a href="/auth/logout">Log out</a></p>
|
13
src/main/resources/templates/AuthResource/register.html
Normal file
13
src/main/resources/templates/AuthResource/register.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<p>Registering as <strong>{username}</strong></p>
|
||||||
|
|
||||||
|
{#if message != null}
|
||||||
|
<h3>{message}</h3>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form method="post" action="/auth/action/register">
|
||||||
|
<input type="hidden" name="username" value="{username}">
|
||||||
|
<label for="password">Confirm password</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
<br>
|
||||||
|
<input type="submit" value="Register">
|
||||||
|
</form>
|
|
@ -5,6 +5,11 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/edit">Create a page</a></li>
|
<li><a href="/edit">Create a page</a></li>
|
||||||
|
{#if user:name == null}
|
||||||
|
<li><a href="/auth">Login or register</a></li>
|
||||||
|
{#else}
|
||||||
|
<li><a href="/auth/logout">Logout ({user:name})</a></li>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<small>Running <a href="/page/TalkPages">TalkPages</a> version {config:["quarkus.application.version"]}</small>
|
<small>Running <a href="/page/TalkPages">TalkPages</a> version {config:["quarkus.application.version"]}</small>
|
Loading…
Reference in a new issue