initial commit

This commit is contained in:
_Bastler 2021-10-03 17:17:00 +02:00
commit 3fffe798cd
45 changed files with 3846 additions and 0 deletions

8
.gitignore vendored Executable file
View File

@ -0,0 +1,8 @@
bin/
target/
.settings/
.project
.classpath
hs_err*.log
application.properties
usernames.txt

183
pom.xml Normal file
View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.bstly.board</groupId>
<artifactId>bstlboard</artifactId>
<version>${revision}</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<revision>0.2.2-SNAPSHOT</revision>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath />
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
<!-- Query DSL -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<!-- Utils -->
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20200713.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.68</version>
</dependency>
</dependencies>
<!-- Database -->
<profiles>
<profile>
<id>db-inmemory</id>
<dependencies>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
</dependency>
</dependencies>
</profile>
<profile>
<id>db-mariadb</id>
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>db-mysql</id>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>db-postgresql</id>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>de.bstly.board.Application</mainClass>
<finalName>bstlboard</finalName>
<executable>true</executable>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor
</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,25 @@
/**
*
*/
package de.bstly.board;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
*
* @author spring-cachet-monitoring@champonthis.de
*
*/
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
/**
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,31 @@
/**
*
*/
package de.bstly.board;
import javax.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import com.querydsl.jpa.impl.JPAQueryFactory;
/**
* @author monitoring@bstly.de
*
*/
@Configuration
@EnableJpaAuditing()
public class JPAConfig {
@Autowired
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}

View File

@ -0,0 +1,39 @@
/**
*
*/
package de.bstly.board;
import java.util.Properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
/**
* @author monitoring@bstly.de
*
*/
@Configuration
public class MessageSourceConfig {
@Value("${bstly.board.title:bstlboard}")
private String title;
@Value("${bstly.board.url:http://localhost:8080}")
private String baseUrl;
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
Properties commonMessages = new Properties();
commonMessages.put("title", title);
commonMessages.put("baseUrl", baseUrl);
messageSource.setCommonMessages(commonMessages);
return messageSource;
}
}

View File

@ -0,0 +1,185 @@
/**
*
*/
package de.bstly.board.businesslogic;
import java.time.Instant;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.stereotype.Component;
import de.bstly.board.model.Comment;
import de.bstly.board.model.QComment;
import de.bstly.board.model.QVote;
import de.bstly.board.model.Types;
import de.bstly.board.model.VoteType;
import de.bstly.board.repository.CommentRepository;
import de.bstly.board.repository.VoteRepository;
/**
* @author Lurkars
*
*/
@Component
public class CommentManager {
@Autowired
private CommentRepository commentRepository;
@Autowired
private VoteRepository voteRepository;
@Autowired
private VoteManager voteManager;
private QComment qComment = QComment.comment;
private QVote qVote = QVote.vote;
/**
*
* @param parent
* @param target
* @param page
* @param size
* @return
*/
public Page<Comment> fetchByRanking(Long target, Long parent, Instant date, double gravity,
int page, int size) {
if (parent == null) {
return commentRepository.findAllByRankingAndParent(target, date, gravity,
PageRequest.of(page, size));
}
return commentRepository.findAllByRankingAndParent(target, parent, date, gravity,
PageRequest.of(page, size));
}
/**
*
* @param page
* @param size
* @param order
* @return
*/
public Page<Comment> fetchByDate(Long target, Long parent, Instant date, int page, int size) {
if (parent == null) {
return commentRepository.findAll(
qComment.target.eq(target).and(qComment.parent.isNull())
.and(qComment.created.before(date)),
PageRequest.of(page, size, Sort.by(Order.asc("created"))));
}
return commentRepository.findAll(
qComment.target.eq(target).and(qComment.parent.eq(parent))
.and(qComment.created.before(date)),
PageRequest.of(page, size, Sort.by(Order.asc("created"))));
}
/**
*
* @param target
* @param parent
* @return
*/
public Long count(Long target, Long parent) {
if (parent == null) {
return count(target);
}
return commentRepository.count(qComment.target.eq(target).and(qComment.parent.eq(parent)));
}
/**
*
* @param target
* @return
*/
public Long count(Long target) {
return commentRepository.count(qComment.target.eq(target));
}
/**
*
* @param username
* @param comment
*/
public void applyMetadata(String username, Comment comment) {
if (!comment.getMetadata().containsKey("comments")) {
comment.getMetadata().put("comments", count(comment.getTarget(), comment.getId()));
}
if (!comment.getMetadata().containsKey("points")) {
comment.getMetadata().put("points",
voteManager.getPoints(comment.getId(), Types.entry));
}
if (!comment.getMetadata().containsKey("vote")) {
comment.getMetadata().put("vote",
!voteRepository.exists(qVote.target.eq(comment.getId())
.and(qVote.targetType.eq(Types.comment)).and(qVote.type.eq(VoteType.up))
.and(qVote.author.eq(username))));
}
if (!comment.getMetadata().containsKey("unvote")) {
comment.getMetadata().put("unvote",
voteRepository.exists(qVote.target.eq(comment.getId())
.and(qVote.targetType.eq(Types.comment))
.and(qVote.type.eq(VoteType.up).and(qVote.author.eq(username)))));
}
if (!comment.getMetadata().containsKey("downvote")) {
comment.getMetadata()
.put("downvote",
!voteRepository.exists(qVote.target.eq(comment.getId())
.and(qVote.targetType.eq(Types.comment))
.and(qVote.author.eq(username))));
}
}
/**
*
* @param entries
*/
public void applyMetadata(String username, List<Comment> entries) {
for (Comment comment : entries) {
applyMetadata(username, comment);
}
}
/**
* @param id
* @return
*/
public boolean exists(Long id) {
return commentRepository.existsById(id);
}
/**
* @param id
* @return
*/
public Comment get(Long id) {
return commentRepository.findById(id).orElse(null);
}
/**
* @param comment
* @return
*/
public Comment save(Comment comment) {
return commentRepository.save(comment);
}
/**
*
* @param commentId
* @return
*/
public long getPoints(Long commentId) {
long upvotes = voteRepository.count(qVote.targetType.eq(Types.comment)
.and(qVote.type.eq(VoteType.up)).and(qVote.target.eq(commentId)));
long downvotes = voteRepository.count(qVote.targetType.eq(Types.comment)
.and(qVote.type.eq(VoteType.down)).and(qVote.target.eq(commentId)));
return upvotes - downvotes;
}
}

View File

@ -0,0 +1,158 @@
/**
*
*/
package de.bstly.board.businesslogic;
import java.time.Instant;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.stereotype.Component;
import de.bstly.board.model.Entry;
import de.bstly.board.model.QEntry;
import de.bstly.board.model.QVote;
import de.bstly.board.model.RankedEntry;
import de.bstly.board.model.Types;
import de.bstly.board.model.VoteType;
import de.bstly.board.repository.EntryRepository;
import de.bstly.board.repository.VoteRepository;
/**
* @author Lurkars
*
*/
@Component
public class EntryManager {
@Autowired
private EntryRepository entryRepository;
@Autowired
private CommentManager commentManager;
@Autowired
private VoteManager voteManager;
@Autowired
private VoteRepository voteRepository;
private QEntry qEntry = QEntry.entry;
private QVote qVote = QVote.vote;
@Value("${bstly.board.ranking.gravity:1.8}")
private double GRAVITY;
/**
*
* @param page
* @param size
* @return
*/
public Page<Entry> fetchByRanking(int page, int size) {
return entryRepository.findAll(
PageRequest.of(page, size, Sort.by(Order.desc("ranking"), Order.desc("created"))));
}
/**
*
* @param page
* @param size
* @param order
* @return
*/
public Page<Entry> fetchByDate(Instant date, int page, int size) {
return entryRepository.findAll(qEntry.created.before(date),
PageRequest.of(page, size, Sort.by(Order.desc("created"))));
}
/**
*
* @param page
* @param size
* @return
*/
public Page<RankedEntry> directFetchByRanking(Instant date, double gravity, int page,
int size) {
return entryRepository.findAllByRanking(date, gravity, PageRequest.of(page, size));
}
/**
*
* @param entry
*/
public void applyMetadata(String username, Entry entry) {
if (!entry.getMetadata().containsKey("comments")) {
entry.getMetadata().put("comments", commentManager.count(entry.getId()));
}
if (!entry.getMetadata().containsKey("points")) {
entry.getMetadata().put("points", voteManager.getPoints(entry.getId(), Types.entry));
}
if (!entry.getMetadata().containsKey("vote")) {
entry.getMetadata().put("vote",
!voteRepository.exists(qVote.target.eq(entry.getId())
.and(qVote.targetType.eq(Types.entry)).and(qVote.type.eq(VoteType.up))
.and(qVote.author.eq(username))));
}
if (!entry.getMetadata().containsKey("unvote")) {
entry.getMetadata().put("unvote",
voteRepository.exists(qVote.target.eq(entry.getId())
.and(qVote.targetType.eq(Types.entry))
.and(qVote.type.eq(VoteType.up).and(qVote.author.eq(username)))));
}
if (!entry.getMetadata().containsKey("downvote")) {
entry.getMetadata().put("downvote",
!voteRepository.exists(qVote.target.eq(entry.getId())
.and(qVote.targetType.eq(Types.entry)).and(qVote.author.eq(username))));
}
}
/**
*
* @param entries
*/
public void applyMetadata(String username, List<Entry> entries) {
for (Entry entry : entries) {
applyMetadata(username, entry);
}
}
/**
* @param id
* @return
*/
public boolean exists(Long id) {
return entryRepository.existsById(id);
}
/**
* @param id
* @return
*/
public Entry get(Long id) {
return entryRepository.findById(id).orElse(null);
}
/**
* @param entry
* @return
*/
public Entry save(Entry entry) {
return entryRepository.save(entry);
}
/**
*
* @param entryId
* @return
*/
public long getPoints(Long entryId) {
long upvotes = voteRepository.count(qVote.targetType.eq(Types.entry)
.and(qVote.type.eq(VoteType.up)).and(qVote.target.eq(entryId)));
long downvotes = voteRepository.count(qVote.targetType.eq(Types.entry)
.and(qVote.type.eq(VoteType.down)).and(qVote.target.eq(entryId)));
return upvotes - downvotes;
}
}

View File

@ -0,0 +1,83 @@
/**
*
*/
package de.bstly.board.businesslogic;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
/**
* @author _bastler@bstly.de
*
*/
public class InstantHelper {
/**
*
* @param instant
* @param amount
* @return
*/
public static Instant plus(Instant instant, TemporalAmount amount) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).plus(amount).toInstant();
}
/**
*
* @param instant
* @param amountToAdd
* @param unit
* @return
*/
public static Instant plus(Instant instant, long amountToAdd, TemporalUnit unit) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).plus(amountToAdd, unit).toInstant();
}
/**
*
* @param instant
* @param amount
* @return
*/
public static Instant minus(Instant instant, TemporalAmount amount) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).minus(amount).toInstant();
}
/**
*
* @param instant
* @param amountToAdd
* @param unit
* @return
*/
public static Instant minus(Instant instant, long amountToAdd, TemporalUnit unit) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).minus(amountToAdd, unit)
.toInstant();
}
/**
*
* @param instant
* @param unit
* @return
*/
public static Instant truncate(Instant instant, TemporalUnit unit) {
if (ChronoUnit.YEARS.equals(unit)) {
instant = instant.truncatedTo(ChronoUnit.DAYS);
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
.with(ChronoField.DAY_OF_YEAR, 1L).toInstant();
} else if (ChronoUnit.MONTHS.equals(unit)) {
instant = instant.truncatedTo(ChronoUnit.DAYS);
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
.with(ChronoField.DAY_OF_MONTH, 1L).toInstant();
}
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).truncatedTo(unit).toInstant();
}
}

View File

@ -0,0 +1,217 @@
/**
*
*/
package de.bstly.board.businesslogic;
import java.util.List;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.google.common.collect.Lists;
import de.bstly.board.model.Entry;
import de.bstly.board.model.LocalUser;
import de.bstly.board.model.QEntry;
import de.bstly.board.model.QLocalUser;
import de.bstly.board.repository.EntryRepository;
import de.bstly.board.repository.LocalUserRepository;
/**
* @author monitoring@bstly.de
*
*/
@Service
public class UserManager implements UserDetailsService, SmartInitializingSingleton {
private Logger logger = LoggerFactory.getLogger(UserManager.class);
@Autowired
private LocalUserRepository localUserRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private EntryManager entryManager;
@Autowired
private EntryRepository entryRepository;
private QLocalUser qLocalUser = QLocalUser.localUser;
private QEntry qEntry = QEntry.entry;
@Value("${admin.password:}")
private String adminPassword;
/*
* @see
* de.bstly.board.businesslogic.LocalUserManager#loadUserByUsername(java.lang.
* String)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LocalUser localUser = getByUsername(username);
if (localUser == null) {
throw new UsernameNotFoundException(username);
}
List<GrantedAuthority> authorities = Lists.newArrayList();
if (localUser.getRoles() != null) {
for (String role : localUser.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
return new User(username, localUser.getPasswordHash(), authorities);
}
/*
*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
@Override
public void afterSingletonsInstantiated() {
if (!localUserRepository.exists(qLocalUser.roles.contains("ROLE_ADMIN"))) {
if (!StringUtils.hasText(adminPassword)) {
adminPassword = RandomStringUtils.random(24, true, true);
logger.error("password for 'admin': "
+ adminPassword);
}
LocalUser admin = new LocalUser();
admin.setUsername("admin");
admin.setRoles(Lists.newArrayList("ROLE_ADMIN"));
admin.setPasswordHash(passwordEncoder.encode(adminPassword));
localUserRepository.save(admin);
}
}
/**
*
* @param username
* @return
*/
public LocalUser getByUsername(String username) {
return localUserRepository.findById(username).orElse(null);
}
/**
*
* @param externalId
* @return
*/
public LocalUser getByExternalId(String externalId) {
return localUserRepository.findOne(qLocalUser.externalId.eq(externalId)).orElse(null);
}
/**
*
* @param authentication
* @return
*/
public LocalUser getByAuth(Authentication authentication) {
if (authentication != null) {
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
return getByUsername(token.getName());
} else if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
String externalId = token.getAuthorizedClientRegistrationId()
+ "-"
+ token.getName();
LocalUser localUser = getByExternalId(externalId);
if (localUser == null) {
localUser = new LocalUser();
localUser.setExternalId(externalId);
String tmpUsername = token.getPrincipal().getAttribute("preferred_username");
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = token.getPrincipal().getAttribute("username");
}
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = token.getPrincipal().getAttribute("name");
}
if (!StringUtils.hasText(tmpUsername)) {
tmpUsername = token.getName();
}
int count = 1;
String username = tmpUsername;
while (localUserRepository.existsById(username)) {
username = tmpUsername
+ "-"
+ count;
count++;
}
localUser.setUsername(username);
localUser.setEmail(token.getPrincipal().getAttribute("email"));
String locale = token.getPrincipal().getAttribute("locale");
if (!StringUtils.hasText(locale)) {
locale = "en";
}
localUser.setLocale(locale);
String darkTheme = token.getPrincipal().getAttribute("darkTheme");
if (!StringUtils.hasText(darkTheme)) {
darkTheme = "false";
}
localUser.setDarkTheme(Boolean.valueOf(darkTheme));
localUser = localUserRepository.save(localUser);
}
return localUser;
}
}
return null;
}
/**
*
* @param user
*/
public void applyMetadata(String username, LocalUser user) {
if (user.getUsername().equals(username) && !user.getMetadata().containsKey("self")) {
user.getMetadata().put("self", true);
}
if (!user.getMetadata().containsKey("points")) {
long points = 0;
for (Entry entry : entryRepository.findAll(qEntry.author.eq(username))) {
points += entryManager.getPoints(entry.getId());
}
user.getMetadata().put("points", points);
}
}
/**
*
* @param localUser
* @return
*/
public LocalUser save(LocalUser localUser) {
return localUserRepository.save(localUser);
}
/**
* @param username
* @return
*/
public boolean exists(String username) {
return localUserRepository.existsById(username);
}
}

View File

@ -0,0 +1,69 @@
/**
*
*/
package de.bstly.board.businesslogic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import de.bstly.board.model.QVote;
import de.bstly.board.model.Types;
import de.bstly.board.model.Vote;
import de.bstly.board.model.VoteType;
import de.bstly.board.repository.VoteRepository;
/**
* @author Lurkars
*
*/
@Component
public class VoteManager {
@Autowired
private VoteRepository voteRepository;
private QVote qVote = QVote.vote;
/**
*
* @param author
* @param targetType
* @param target
* @return
*/
public Vote get(String author, Types targetType, Long target) {
return voteRepository.findOne(qVote.author.eq(author).and(qVote.targetType.eq(targetType))
.and(qVote.target.eq(target))).orElse(null);
}
/**
*
* @param vote
* @return
*/
public Vote save(Vote vote) {
return voteRepository.save(vote);
}
/**
*
* @param vote
*/
public void delete(Vote vote) {
voteRepository.delete(vote);
}
/**
*
* @param target
* @param targetType
* @return
*/
public Long getPoints(Long target, Types targetType) {
return voteRepository
.count(qVote.target.eq(target)
.and(qVote.targetType.eq(targetType).and(qVote.type.eq(VoteType.up))))
- voteRepository.count(qVote.target.eq(target)
.and(qVote.targetType.eq(targetType).and(qVote.type.eq(VoteType.down))));
}
}

View File

@ -0,0 +1,83 @@
/**
*
*/
package de.bstly.board.businesslogic.support;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
/**
* @author _bastler@bstly.de
*
*/
public class InstantHelper {
/**
*
* @param instant
* @param amount
* @return
*/
public static Instant plus(Instant instant, TemporalAmount amount) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).plus(amount).toInstant();
}
/**
*
* @param instant
* @param amountToAdd
* @param unit
* @return
*/
public static Instant plus(Instant instant, long amountToAdd, TemporalUnit unit) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).plus(amountToAdd, unit).toInstant();
}
/**
*
* @param instant
* @param amount
* @return
*/
public static Instant minus(Instant instant, TemporalAmount amount) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).minus(amount).toInstant();
}
/**
*
* @param instant
* @param amountToAdd
* @param unit
* @return
*/
public static Instant minus(Instant instant, long amountToAdd, TemporalUnit unit) {
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).minus(amountToAdd, unit)
.toInstant();
}
/**
*
* @param instant
* @param unit
* @return
*/
public static Instant truncate(Instant instant, TemporalUnit unit) {
if (ChronoUnit.YEARS.equals(unit)) {
instant = instant.truncatedTo(ChronoUnit.DAYS);
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
.with(ChronoField.DAY_OF_YEAR, 1L).toInstant();
} else if (ChronoUnit.MONTHS.equals(unit)) {
instant = instant.truncatedTo(ChronoUnit.DAYS);
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
.with(ChronoField.DAY_OF_MONTH, 1L).toInstant();
}
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC).truncatedTo(unit).toInstant();
}
}

View File

@ -0,0 +1,113 @@
/**
*
*/
package de.bstly.board.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.common.collect.Lists;
/**
*
* @author _bastler@bstly.de
*
*/
@RestController
@RequestMapping("/auth")
public class AuthenticationController extends BaseController {
private static String authorizationRequestBaseUri = "oauth2/authorization";
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
/**
*
* @return
*/
@GetMapping
public Authentication me() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth;
}
/**
*
* @return
*/
@SuppressWarnings("unchecked")
@GetMapping("external")
public List<Client> getExternalLoginUrls() {
List<Client> clients = Lists.newArrayList();
Iterable<ClientRegistration> clientRegistrations = null;
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
.as(Iterable.class);
if (type != ResolvableType.NONE
&& ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
}
clientRegistrations
.forEach(registration -> clients.add(new Client(registration.getRegistrationId(),
authorizationRequestBaseUri
+ "/"
+ registration.getRegistrationId())));
return clients;
}
protected static class Client {
private String id;
private String loginUrl;
/**
* @param id
* @param loginUrl
*/
public Client(String id, String loginUrl) {
super();
this.id = id;
this.loginUrl = loginUrl;
}
/**
* @return the id
*/
public String getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
}
/**
* @return the loginUrl
*/
public String getLoginUrl() {
return loginUrl;
}
/**
* @param loginUrl the loginUrl to set
*/
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
}
}

View File

@ -0,0 +1,48 @@
/**
*
*/
package de.bstly.board.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import de.bstly.board.businesslogic.UserManager;
import de.bstly.board.model.LocalUser;
/**
* @author monitoring@bstly.de
*
*/
public class BaseController {
@Autowired
private UserManager localUserManager;
/**
*
* @return
*/
protected boolean authenticated() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null && auth.isAuthenticated();
}
/**
*
* @return
*/
protected String getCurrentUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : null;
}
/**
*
* @return
*/
protected LocalUser getLocalUser() {
return localUserManager.getByAuth(SecurityContextHolder.getContext().getAuthentication());
}
}

View File

@ -0,0 +1,132 @@
/**
*
*/
package de.bstly.board.controller;
import java.time.Instant;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.board.businesslogic.CommentManager;
import de.bstly.board.businesslogic.VoteManager;
import de.bstly.board.controller.support.EntityResponseStatusException;
import de.bstly.board.controller.support.RequestBodyErrors;
import de.bstly.board.controller.validation.CommentValidator;
import de.bstly.board.model.Comment;
import de.bstly.board.model.Types;
import de.bstly.board.model.Vote;
import de.bstly.board.model.VoteType;
/**
* @author Lurkars
*
*/
@RestController
@RequestMapping("/c")
public class CommentController extends BaseController {
@Autowired
private CommentManager commentManager;
@Autowired
private CommentValidator commentValidator;
@Autowired
private VoteManager voteManager;
@Value("${bstly.board.size:30}")
private int SIZE;
@Value("${bstly.board.ranking.gravity:1.8}")
private double GRAVITY;
@PreAuthorize("isAuthenticated()")
@GetMapping({ "/e/{target}", "/e/{target}/{parent}" })
public Page<Comment> rankedComments(@PathVariable("target") Long target,
@PathVariable("parent") Optional<Long> parent,
@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter,
@RequestParam("date") Optional<Instant> dateParameter,
@RequestParam("gravity") Optional<Double> gravityParameter) {
Page<Comment> comments = newComments(target, parent, pageParameter, sizeParameter,
dateParameter);
commentManager.applyMetadata(getCurrentUsername(), comments.getContent());
return comments;
// Page<Comment> comments = commentManager.fetchByRanking(target, parent.orElse(null),
// dateParameter.orElse(Instant.now()), gravityParameter.orElse(GRAVITY),
// pageParameter.orElse(0), sizeParameter.orElse(SIZE));
// return comments;
}
@PreAuthorize("isAuthenticated()")
@GetMapping({ "/c/{target}", "/c/{target}/{parent}" })
public Long countComments(@PathVariable("target") Long target,
@PathVariable("parent") Optional<Long> parent) {
return commentManager.count(target, parent.orElse(null));
}
@PreAuthorize("isAuthenticated()")
@GetMapping({ "/e/new/{target}", "/e/new/{target}/{parent}" })
public Page<Comment> newComments(@PathVariable("target") Long target,
@PathVariable("parent") Optional<Long> parent,
@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter,
@RequestParam("date") Optional<Instant> dateParameter) {
Page<Comment> comments = commentManager.fetchByDate(target, parent.orElse(null),
dateParameter.orElse(Instant.now()), pageParameter.orElse(0),
sizeParameter.orElse(SIZE));
commentManager.applyMetadata(getCurrentUsername(), comments.getContent());
return comments;
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
public Comment getComment(@PathVariable("id") Long id) {
Comment comment = commentManager.get(id);
if (comment == null) {
throw new EntityResponseStatusException(HttpStatus.NOT_FOUND);
}
commentManager.applyMetadata(getCurrentUsername(), comment);
return comment;
}
@PreAuthorize("isAuthenticated()")
@PostMapping()
public Comment createComment(@RequestBody Comment comment) {
RequestBodyErrors bindingResult = new RequestBodyErrors(comment);
commentValidator.validate(comment, bindingResult);
if (bindingResult.hasErrors()) {
throw new EntityResponseStatusException(bindingResult.getAllErrors(),
HttpStatus.UNPROCESSABLE_ENTITY);
}
comment.setCreated(Instant.now());
comment.setAuthor(getCurrentUsername());
comment = commentManager.save(comment);
Vote vote = new Vote();
vote.setTarget(comment.getId());
vote.setTargetType(Types.comment);
vote.setType(VoteType.up);
vote.setAuthor(getCurrentUsername());
voteManager.save(vote);
return comment;
}
}

View File

@ -0,0 +1,202 @@
/**
*
*/
package de.bstly.board.controller;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.board.businesslogic.EntryManager;
import de.bstly.board.model.Comment;
import de.bstly.board.model.Entry;
import de.bstly.board.model.EntryStatus;
import de.bstly.board.model.EntryType;
import de.bstly.board.model.LocalUser;
import de.bstly.board.model.QLocalUser;
import de.bstly.board.model.Types;
import de.bstly.board.model.Vote;
import de.bstly.board.model.VoteType;
import de.bstly.board.repository.CommentRepository;
import de.bstly.board.repository.LocalUserRepository;
import de.bstly.board.repository.VoteRepository;
/**
*
* @author _bastler@bstly.de
*
*/
@RestController
@RequestMapping("/debug")
public class DebugController extends BaseController {
/**
* logger
*/
private Logger logger = LogManager.getLogger(DebugController.class);
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private LocalUserRepository localUserRepository;
@Autowired
private CommentRepository commentRepository;
@Autowired
private VoteRepository voteRepository;
@Autowired
private EntryManager entryManager;
@Value("${debug.random.users:0}")
private int users;
@Value("${debug.random.minEntries:0}")
private int minEntries;
@Value("${debug.random.maxEntries:10}")
private int maxEntries;
@Value("${debug.random.entryAge:63115200}")
private long entryAge;
@Value("${debug.random.minComments:0}")
private int minComments;
@Value("${debug.random.maxComments:10}")
private int maxComments;
@Value("${debug.random.subCommentsFactor:0.5}")
private double subCommentsFactor;
@Value("${debug.random.subCommentsThresh:0.3}")
private double subCommentsThresh;
@Value("${debug.random.subCommentsDepth:2}")
private int subCommentsDepth;
@Value("${debug.random.minUpvotes:5}")
private int minUpvotes;
@Value("${debug.random.maxUpvotes:10}")
private int maxUpvotes;
@Value("${debug.random.minDownvotes:0}")
private int minDownvotes;
@Value("${debug.random.maxDownvotes:10}")
private int maxDownvotes;
/**
*
* @return
*/
@GetMapping("/random")
public void random() {
logger.warn("start random generation");
long userCount = localUserRepository
.count(QLocalUser.localUser.username.startsWith("user"));
for (long i = userCount; i < userCount + users; i++) {
LocalUser localUser = new LocalUser();
String username = "user" + i;
localUser.setUsername(username);
localUser.setPasswordHash(passwordEncoder.encode(username));
localUserRepository.save(localUser);
logger.trace("Created user: '" + username + "'");
}
logger.info("Created " + users + " users");
for (long id = 0; id <= userCount; id++) {
entries("user" + id, userCount);
}
logger.warn("finished random generation");
}
protected void entries(String username, long userCount) {
long numEntries = RandomUtils.nextLong(minEntries, maxEntries);
for (int i = 0; i < numEntries; i++) {
Entry entry = new Entry();
entry.setEntryType(EntryType.INTERN);
entry.setAuthor(username);
entry.setCreated(
Instant.now().minus(RandomUtils.nextLong(0, entryAge), ChronoUnit.SECONDS));
entry.setTitle(RandomStringUtils.randomAscii(RandomUtils.nextInt(10, 250)));
entry.setText(RandomStringUtils.randomAscii(RandomUtils.nextInt(0, 2500)));
entry.setEntryStatus(EntryStatus.NORMAL);
entry = entryManager.save(entry);
logger.trace("Created entry: '" + entry.getId() + "'");
comments(entry.getId(), entry.getCreated(), userCount);
votes(entry.getId(), Types.entry, userCount);
}
logger.info("Created " + numEntries + " entries of '" + username + "'");
}
protected void comments(Long target, Instant date, long userCount) {
long numComments = RandomUtils.nextLong(minComments, maxComments);
logger.debug("Create " + numComments + " comments for '" + target + "'");
for (int i = 0; i < numComments; i++) {
Comment comment = new Comment();
comment.setTarget(target);
comment.setAuthor("user" + RandomUtils.nextLong(0, userCount));
comment.setText(RandomStringUtils.randomAscii(RandomUtils.nextInt(0, 2500)));
comment.setCreated(Instant.now()
.minus(RandomUtils.nextLong(0,
(Instant.now().toEpochMilli() - date.toEpochMilli()) / 1000),
ChronoUnit.SECONDS));
comment = commentRepository.save(comment);
logger.trace("Created comment: '" + comment.getId() + "'");
subComments(target, comment.getId(), comment.getCreated(), subCommentsFactor,
subCommentsThresh, 0, userCount);
}
}
protected void subComments(Long target, Long parent, Instant date, double factor, double thresh,
int depth, long userCount) {
if (depth < subCommentsDepth && RandomUtils.nextDouble(0, 1) < thresh) {
long numSubComments = RandomUtils.nextLong(0, Math.round(maxComments * factor));
logger.debug("Create " + numSubComments + " subComments for '" + parent + "'");
for (int i = 0; i < numSubComments; i++) {
Comment comment = new Comment();
comment.setTarget(target);
comment.setParent(parent);
comment.setAuthor("user" + RandomUtils.nextLong(0, userCount));
comment.setText(RandomStringUtils.randomAscii(RandomUtils.nextInt(0, 2500)));
comment.setCreated(Instant.now()
.minus(RandomUtils.nextLong(0,
(Instant.now().toEpochMilli() - date.toEpochMilli()) / 1000),
ChronoUnit.SECONDS));
comment = commentRepository.save(comment);
logger.trace("Created subComment: '" + comment.getId() + "'");
subComments(target, comment.getId(), comment.getCreated(), factor * 0.5,
thresh * 0.5, depth++, userCount);
}
}
}
protected void votes(Long target, Types targetType, long userCount) {
long numUpvotes = RandomUtils.nextLong(minUpvotes, maxUpvotes);
logger.debug("Create " + numUpvotes + " upvotes for '" + target + "'");
for (int i = 0; i < numUpvotes; i++) {
Vote upvote = new Vote();
upvote.setTarget(target);
upvote.setType(VoteType.up);
upvote.setTargetType(targetType);
upvote.setAuthor("user" + RandomUtils.nextLong(0, userCount));
upvote = voteRepository.save(upvote);
logger.trace("Created upvote: '" + upvote.getId() + "'");
}
long numDownvotes = RandomUtils.nextLong(minDownvotes, maxDownvotes);
logger.debug("Create " + numDownvotes + " downvotes for '" + target + "'");
for (int i = 0; i < numDownvotes; i++) {
Vote downvote = new Vote();
downvote.setTarget(target);
downvote.setType(VoteType.down);
downvote.setTargetType(targetType);
downvote.setAuthor("user" + RandomUtils.nextLong(0, userCount));
downvote = voteRepository.save(downvote);
logger.trace("Created downvote: '" + downvote.getId() + "'");
}
}
}

View File

@ -0,0 +1,152 @@
/**
*
*/
package de.bstly.board.controller;
import java.time.Instant;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.board.businesslogic.EntryManager;
import de.bstly.board.businesslogic.VoteManager;
import de.bstly.board.controller.support.EntityResponseStatusException;
import de.bstly.board.controller.support.RequestBodyErrors;
import de.bstly.board.controller.validation.EntryValidator;
import de.bstly.board.model.Entry;
import de.bstly.board.model.EntryStatus;
import de.bstly.board.model.RankedEntry;
import de.bstly.board.model.Types;
import de.bstly.board.model.Vote;
import de.bstly.board.model.VoteType;
/**
* @author Lurkars
*
*/
@RestController
@RequestMapping("/e")
public class EntryController extends BaseController {
@Autowired
private EntryManager entryManager;
@Autowired
private EntryValidator entryValidator;
@Autowired
private VoteManager voteManager;
@Value("${bstly.board.size:30}")
private int SIZE;
@Value("${bstly.board.ranking.gravity:1.8}")
private double GRAVITY;
@PreAuthorize("isAuthenticated()")
@GetMapping()
public Page<Entry> rankedEntries(@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter,
@RequestParam("date") Optional<Instant> dateParameter,
@RequestParam("gravity") Optional<Double> gravityParameter) {
if (sizeParameter.isPresent() && sizeParameter.get() > 100) {
sizeParameter = Optional.of(100);
}
Page<RankedEntry> entries = entryManager.directFetchByRanking(
dateParameter.orElse(Instant.now()), gravityParameter.orElse(GRAVITY),
pageParameter.orElse(0), sizeParameter.orElse(SIZE));
Page<Entry> transformed = new PageImpl<Entry>(
entries.getContent().stream().map(rankedEntry -> Entry.fromRankedEntry(rankedEntry))
.collect(Collectors.toList()),
entries.getPageable(), entries.getTotalElements());
entryManager.applyMetadata(getCurrentUsername(), transformed.getContent());
return transformed;
}
@PreAuthorize("isAuthenticated()")
@GetMapping("ranked")
public Page<Entry> rankedEntries(@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter) {
if (sizeParameter.isPresent() && sizeParameter.get() > 100) {
sizeParameter = Optional.of(100);
}
Page<Entry> entries = entryManager.fetchByRanking(pageParameter.orElse(0),
sizeParameter.orElse(SIZE));
entryManager.applyMetadata(getCurrentUsername(), entries.getContent());
return entries;
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/new")
public Page<Entry> newEntries(@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter,
@RequestParam("date") Optional<Instant> dateParameter) {
if (sizeParameter.isPresent() && sizeParameter.get() > 100) {
sizeParameter = Optional.of(100);
}
Page<Entry> entries = entryManager.fetchByDate(dateParameter.orElse(Instant.now()),
pageParameter.orElse(0), sizeParameter.orElse(SIZE));
entryManager.applyMetadata(getCurrentUsername(), entries.getContent());
return entries;
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
public Entry getEntry(@PathVariable("id") Long id) {
Entry entry = entryManager.get(id);
if (entry == null) {
throw new EntityResponseStatusException(HttpStatus.NOT_FOUND);
}
entryManager.applyMetadata(getCurrentUsername(), entry);
return entry;
}
@PreAuthorize("isAuthenticated()")
@PostMapping()
public Entry createEntry(@RequestBody Entry entry) {
RequestBodyErrors bindingResult = new RequestBodyErrors(entry);
entryValidator.validate(entry, bindingResult);
if (bindingResult.hasErrors()) {
throw new EntityResponseStatusException(bindingResult.getAllErrors(),
HttpStatus.UNPROCESSABLE_ENTITY);
}
entry.setCreated(Instant.now());
entry.setAuthor(getCurrentUsername());
entry.setEntryStatus(EntryStatus.NORMAL);
entry = entryManager.save(entry);
Vote vote = new Vote();
vote.setTarget(entry.getId());
vote.setType(VoteType.up);
vote.setTargetType(Types.entry);
vote.setAuthor(getCurrentUsername());
voteManager.save(vote);
return entry;
}
}

View File

@ -0,0 +1,79 @@
/**
*
*/
package de.bstly.board.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.board.businesslogic.UserManager;
import de.bstly.board.controller.support.EntityResponseStatusException;
import de.bstly.board.model.LocalUser;
/**
* @author monitoring@bstly.de
*
*/
@RestController
@RequestMapping("/u")
public class UserController extends BaseController {
@Autowired
private UserManager userManager;
@PreAuthorize("isAuthenticated()")
@GetMapping({ "", "/{username}" })
public LocalUser getUser(@PathVariable("username") Optional<String> usernameParameter) {
String username = usernameParameter.orElse(getCurrentUsername());
LocalUser user = userManager.getByUsername(username);
if (user == null) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
if (user.getUsername() != getCurrentUsername()) {
LocalUser otherUser = new LocalUser();
otherUser.setUsername(user.getUsername());
otherUser.setAbout(user.getAbout());
}
user.setPasswordHash(null);
userManager.applyMetadata(getCurrentUsername(), user);
return user;
}
@PreAuthorize("isAuthenticated()")
@PostMapping()
public LocalUser updateUser(@RequestBody LocalUser user) {
if (!getCurrentUsername().equals(user.getUsername())) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
LocalUser orgUser = userManager.getByUsername(user.getUsername());
orgUser.setAbout(user.getAbout());
orgUser.setDarkTheme(user.isDarkTheme());
orgUser.setEmail(user.getEmail());
orgUser.setLocale(user.getLocale());
orgUser.setSettings(user.getSettings());
user = userManager.save(orgUser);
user.setPasswordHash(null);
return user;
}
}

View File

@ -0,0 +1,143 @@
/**
*
*/
package de.bstly.board.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.board.businesslogic.CommentManager;
import de.bstly.board.businesslogic.EntryManager;
import de.bstly.board.businesslogic.VoteManager;
import de.bstly.board.controller.support.EntityResponseStatusException;
import de.bstly.board.model.Types;
import de.bstly.board.model.Vote;
import de.bstly.board.model.VoteType;
/**
* @author monitoring@bstly.de
*
*/
@RestController
@RequestMapping("/v")
public class VoteController extends BaseController {
@Autowired
private VoteManager voteManager;
@Autowired
private EntryManager entryManager;
@Autowired
private CommentManager commentManager;
@PreAuthorize("isAuthenticated()")
@GetMapping("/e/{id}")
public long getEntryPoints(@PathVariable("id") Long id) {
if (!entryManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
return voteManager.getPoints(id, Types.entry);
}
@PreAuthorize("isAuthenticated()")
@PutMapping("/e/{id}")
public void voteEntryUp(@PathVariable("id") Long id) {
if (!entryManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
Vote vote = voteManager.get(getCurrentUsername(), Types.entry, id);
if (vote == null) {
vote = new Vote();
vote.setTarget(id);
vote.setAuthor(getCurrentUsername());
vote.setType(VoteType.up);
vote.setTargetType(Types.entry);
voteManager.save(vote);
throw new EntityResponseStatusException(HttpStatus.CREATED);
} else if (!VoteType.up.equals(vote.getType())) {
voteManager.delete(vote);
}
}
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/e/{id}")
public void voteEntryDown(@PathVariable("id") Long id) {
if (!entryManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
Vote vote = voteManager.get(getCurrentUsername(), Types.entry, id);
if (vote == null) {
vote = new Vote();
vote.setTarget(id);
vote.setAuthor(getCurrentUsername());
vote.setType(VoteType.down);
vote.setTargetType(Types.entry);
voteManager.save(vote);
throw new EntityResponseStatusException(HttpStatus.CREATED);
} else if (!VoteType.down.equals(vote.getType())) {
voteManager.delete(vote);
}
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/c/{id}")
public long getCommentPoints(@PathVariable("id") Long id) {
if (!commentManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
return voteManager.getPoints(id, Types.comment);
}
@PreAuthorize("isAuthenticated()")
@PutMapping("/c/{id}")
public void voteCommentUp(@PathVariable("id") Long id) {
if (!commentManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
Vote vote = voteManager.get(getCurrentUsername(), Types.comment, id);
if (vote == null) {
vote = new Vote();
vote.setTarget(id);
vote.setAuthor(getCurrentUsername());
vote.setType(VoteType.up);
vote.setTargetType(Types.comment);
voteManager.save(vote);
throw new EntityResponseStatusException(HttpStatus.CREATED);
} else if (!VoteType.up.equals(vote.getType())) {
voteManager.delete(vote);
}
}
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/c/{id}")
public void voteCommentDown(@PathVariable("id") Long id) {
if (!commentManager.exists(id)) {
throw new EntityResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
}
Vote vote = voteManager.get(getCurrentUsername(), Types.comment, id);
if (vote == null) {
vote = new Vote();
vote.setTarget(id);
vote.setAuthor(getCurrentUsername());
vote.setType(VoteType.down);
vote.setTargetType(Types.comment);
voteManager.save(vote);
throw new EntityResponseStatusException(HttpStatus.CREATED);
} else if (!VoteType.down.equals(vote.getType())) {
voteManager.delete(vote);
}
}
}

View File

@ -0,0 +1,35 @@
/**
*
*/
package de.bstly.board.controller.support;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
/**
*
* @author monitoring@bstly.de
*
*/
@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
/**
*
* @param exception
* @param request
* @return
*/
@ExceptionHandler(value = { EntityResponseStatusException.class })
protected ResponseEntity<Object> handleResponseEntityStatusException(RuntimeException exception,
WebRequest request) {
EntityResponseStatusException entityResponseStatusException = (EntityResponseStatusException) exception;
return handleExceptionInternal(exception, entityResponseStatusException.getBody(),
new HttpHeaders(), entityResponseStatusException.getStatus(), request);
}
}

View File

@ -0,0 +1,87 @@
/**
*
*/
package de.bstly.board.controller.support;
import javax.annotation.Nullable;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
/**
*
* @author monitoring@bstly.de
*
*/
public class EntityResponseStatusException extends NestedRuntimeException {
/**
* default serialVersionUID
*/
private static final long serialVersionUID = 1L;
private final HttpStatus status;
@Nullable
private final Object body;
/**
*
* @param status
*/
public EntityResponseStatusException(HttpStatus status) {
this(null, status);
}
/**
*
* @param body
* @param status
*/
public EntityResponseStatusException(@Nullable Object body, HttpStatus status) {
this(body, status, null);
}
/**
*
* @param body
* @param status
* @param cause
*/
public EntityResponseStatusException(@Nullable Object body, HttpStatus status, @Nullable Throwable cause) {
super(null, cause);
Assert.notNull(status, "HttpStatus is required");
this.status = status;
this.body = body;
}
/**
*
* @return
*/
public HttpStatus getStatus() {
return this.status;
}
/**
*
* @return
*/
@Nullable
public Object getBody() {
return this.body;
}
/**
*
* @return
*/
@Override
public String getMessage() {
String msg = this.status + (this.body != null ? " \"" + this.body + "\"" : "");
return NestedExceptionUtils.buildMessage(msg, getCause());
}
}

View File

@ -0,0 +1,49 @@
/**
*
*/
package de.bstly.board.controller.support;
import org.springframework.lang.Nullable;
import org.springframework.validation.AbstractBindingResult;
/**
*
* @author _bastler@bstly.de
*
*/
@SuppressWarnings("serial")
public class RequestBodyErrors extends AbstractBindingResult {
@Nullable
private final Object target;
/**
*
* @param target
* @param objectName
*/
public RequestBodyErrors(@Nullable Object target) {
super("request-body");
this.target = target;
}
/*
* @see org.springframework.validation.AbstractBindingResult#getTarget()
*/
@Override
public Object getTarget() {
return target;
}
/*
* @see
* org.springframework.validation.AbstractBindingResult#getActualFieldValue(java
* .lang.String)
*/
@Override
protected Object getActualFieldValue(String field) {
// Not necessary
return null;
}
}

View File

@ -0,0 +1,61 @@
/**
*
*/
package de.bstly.board.controller.validation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import de.bstly.board.businesslogic.CommentManager;
import de.bstly.board.businesslogic.EntryManager;
import de.bstly.board.model.Comment;
/**
* @author monitoring@bstly.de
*
*/
@Component
public class CommentValidator implements Validator {
@Autowired
private CommentManager commentManager;
@Autowired
private EntryManager entryManager;
/*
* @see org.springframework.validation.Validator#supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Comment.class);
}
/*
* @see org.springframework.validation.Validator#validate(java.lang.Object,
* org.springframework.validation.Errors)
*/
@Override
public void validate(Object target, Errors errors) {
Comment comment = (Comment) target;
if (comment.getTarget() == null) {
errors.rejectValue("target", "REQUIRED");
} else if (!entryManager.exists(comment.getTarget())) {
errors.rejectValue("target", "INVALID");
}
if (comment.getParent() != null) {
Comment parent = commentManager.get(comment.getParent());
if (parent == null || !parent.getTarget().equals(comment.getTarget())) {
errors.rejectValue("parent", "INVALID");
}
}
if (!StringUtils.hasText(comment.getText())) {
errors.rejectValue("text", "REQUIRED");
}
}
}

View File

@ -0,0 +1,57 @@
/**
*
*/
package de.bstly.board.controller.validation;
import org.apache.commons.validator.routines.UrlValidator;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import de.bstly.board.model.Entry;
import de.bstly.board.model.EntryType;
/**
* @author monitoring@bstly.de
*
*/
@Component
public class EntryValidator implements Validator {
private UrlValidator urlValidator = new UrlValidator();
/*
* @see org.springframework.validation.Validator#supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Entry.class);
}
/*
* @see org.springframework.validation.Validator#validate(java.lang.Object,
* org.springframework.validation.Errors)
*/
@Override
public void validate(Object target, Errors errors) {
Entry entry = (Entry) target;
if (!StringUtils.hasText(entry.getTitle())) {
errors.rejectValue("title", "REQUIRED");
}
if (entry.getEntryType() == null) {
errors.rejectValue("entrytype", "REQUIRED");
} else if (EntryType.LINK.equals(entry.getEntryType())) {
entry.setText(null);
if (!StringUtils.hasText(entry.getUrl())) {
errors.rejectValue("url", "REQUIRED");
}
}
if (StringUtils.hasText(entry.getUrl()) && !urlValidator.isValid(entry.getUrl())) {
errors.rejectValue("url", "INVALID");
}
}
}

View File

@ -0,0 +1,28 @@
/**
*
*/
package de.bstly.board.events;
import org.springframework.context.ApplicationEvent;
import de.bstly.board.model.Vote;
/**
* @author Lurkars
*
*/
public class VotedEvent extends ApplicationEvent {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* @param source
*/
public VotedEvent(Vote vote) {
super(vote);
}
}

View File

@ -0,0 +1,176 @@
/**
*
*/
package de.bstly.board.i18n.businesslogic;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.bstly.board.i18n.model.I18n;
import de.bstly.board.i18n.repository.I18nRepository;
/**
* @author _bastler@bstly.de
*
*/
@Component
public class I18nManager implements SmartInitializingSingleton {
private Logger logger = LoggerFactory.getLogger(I18nManager.class);
@Autowired
private I18nRepository i18nRepository;
@Autowired
private ResourceLoader resourceLoader;
private Gson gson = new Gson();
/**
*
* @param locale
* @return
*/
public I18n get(String locale) {
return i18nRepository.findById(locale).orElse(null);
}
/**
*
* @param locale
* @return
*/
public JsonObject getLabel(String locale) {
I18n i18n = get(locale);
if (i18n != null && StringUtils.hasText(i18n.getLabel())) {
JsonElement element = JsonParser.parseString(i18n.getLabel());
if (element != null && element.isJsonObject()) {
return element.getAsJsonObject();
}
}
return null;
}
/**
*
* @return
*/
public List<String> getLocales() {
return i18nRepository.findAll().stream().map(I18n::getLocale).collect(Collectors.toList());
}
/**
*
* @param dest
* @param src
*/
protected void extendJsonObject(JsonObject dest, JsonObject src) {
for (Entry<String, JsonElement> srcEntry : src.entrySet()) {
String srcKey = srcEntry.getKey();
JsonElement srcValue = srcEntry.getValue();
if (dest.has(srcKey)) {
JsonElement destValue = dest.get(srcKey);
if (destValue.isJsonObject() && srcValue.isJsonObject()) {
extendJsonObject(destValue.getAsJsonObject(), srcValue.getAsJsonObject());
} else {
dest.add(srcKey, srcValue);
}
} else {
dest.add(srcKey, srcValue);
}
}
}
/**
*
* @param locale
* @param newLabel
* @return
*/
public I18n addLabel(String locale, JsonObject newLabel) {
JsonObject label = getLabel(locale);
if (label == null || label.size() == 0 || label.entrySet().isEmpty()) {
label = newLabel;
} else {
extendJsonObject(label, newLabel);
}
I18n i18n = new I18n();
i18n.setLocale(locale);
i18n.setLabel(gson.toJson(label));
return i18nRepository.save(i18n);
}
/**
*
* @param locale
* @param label
* @return
*/
public I18n setLabel(String locale, JsonObject label) {
I18n i18n = new I18n();
i18n.setLocale(locale);
i18n.setLabel(gson.toJson(label));
return i18nRepository.save(i18n);
}
/**
*
* @param locale
*/
public void deleteLabel(String locale) {
if (i18nRepository.existsById(locale)) {
i18nRepository.deleteById(locale);
}
}
/*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
@Override
public void afterSingletonsInstantiated() {
try {
Resource resource = resourceLoader.getResource("classpath:label");
if (resource.exists()) {
File labelFolder = resource.getFile();
if (labelFolder.exists() && labelFolder.isDirectory()) {
for (File labelFile : labelFolder.listFiles()) {
JsonObject label = JsonParser
.parseReader(new FileReader(labelFile, StandardCharsets.UTF_8))
.getAsJsonObject();
String locale = labelFile.getName().replace(".json", "");
addLabel(locale, label);
}
}
}
} catch (IOException e) {
logger.warn("cannot read in label folder", e.getMessage());
}
}
}

View File

@ -0,0 +1,108 @@
/**
*
*/
package de.bstly.board.i18n.controller;
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import de.bstly.board.controller.BaseController;
import de.bstly.board.i18n.businesslogic.I18nManager;
/**
* @author _bastler@bstly.de
*
*/
@RestController
@RequestMapping("/i18n")
public class I18nController extends BaseController {
@Autowired
private I18nManager i18nManager;
private Gson gson = new Gson();
/**
*
* @return
*/
@GetMapping
public List<String> getLocales() {
return i18nManager.getLocales();
}
/**
*
* @param locale
* @param response
* @throws JsonIOException
* @throws IOException
*/
@GetMapping("/{locale}")
public void getLabel(@PathVariable("locale") String locale, HttpServletResponse response)
throws JsonIOException, IOException {
JsonObject label = i18nManager.getLabel(locale);
if (label != null) {
response.setCharacterEncoding("utf-8");
gson.toJson(label, response.getWriter());
}
}
/**
*
* @param locale
* @param label
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/{locale}")
public void setLabel(@PathVariable("locale") String locale, @RequestBody Object label) {
JsonElement element = gson.toJsonTree(label);
if (element != null && element.isJsonObject()) {
i18nManager.setLabel(locale, element.getAsJsonObject());
}
}
/**
*
* @param locale
* @param label
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PutMapping("/{locale}")
public void addLabel(@PathVariable("locale") String locale, @RequestBody Object label) {
JsonElement element = gson.toJsonTree(label);
if (element != null && element.isJsonObject()) {
i18nManager.addLabel(locale, element.getAsJsonObject());
}
}
/**
*
* @param locale
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{locale}")
public void deleteLocale(@PathVariable("locale") String locale) {
i18nManager.deleteLabel(locale);
}
}

View File

@ -0,0 +1,64 @@
/**
*
*/
package de.bstly.board.i18n.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
/**
*
* @author _bastler@bstly.de
*
*/
@Entity
@Table(name = "i18n", uniqueConstraints = @UniqueConstraint(columnNames = { "locale" }))
public class I18n {
/**
*
*/
@Id
@Column(name = "locale", unique = true, nullable = false)
private String locale;
/**
*
*/
@Lob
@Column(name = "label")
private String label;
/**
* @return the locale
*/
public String getLocale() {
return locale;
}
/**
* @param locale the locale to set
*/
public void setLocale(String locale) {
this.locale = locale;
}
/**
* @return the label
*/
public String getLabel() {
return label;
}
/**
* @param label the label to set
*/
public void setLabel(String label) {
this.label = label;
}
}

View File

@ -0,0 +1,21 @@
/**
*
*/
package de.bstly.board.i18n.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.bstly.board.i18n.model.I18n;
/**
*
* @author _bastler@bstly.de
*
*/
@Repository
public interface I18nRepository
extends JpaRepository<I18n, String>, QuerydslPredicateExecutor<I18n> {
}

View File

@ -0,0 +1,152 @@
/**
*
*/
package de.bstly.board.model;
import java.time.Instant;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import com.google.common.collect.Maps;
/**
* @author Lurkars
*
*/
@Entity
@Table(name = "comments")
@EntityListeners({ AuditingEntityListener.class })
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "author", nullable = false)
private String author;
@Column(name = "created", nullable = false)
private Instant created;
@Column(name = "target", nullable = false)
private Long target;
@Column(name = "parent", nullable = true)
private Long parent;
@Lob
@Column(name = "text", nullable = false)
private String text;
@Transient
private Map<String, Object> metadata;
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the author
*/
public String getAuthor() {
return author;
}
/**
* @param author the author to set
*/
public void setAuthor(String author) {
this.author = author;
}
/**
* @return the created
*/
public Instant getCreated() {
return created;
}
/**
* @param created the created to set
*/
public void setCreated(Instant created) {
this.created = created;
}
/**
* @return the target
*/
public Long getTarget() {
return target;
}
/**
* @param target the target to set
*/
public void setTarget(Long target) {
this.target = target;
}
/**
* @return the parent
*/
public Long getParent() {
return parent;
}
/**
* @param parent the parent to set
*/
public void setParent(Long parent) {
this.parent = parent;
}
/**
* @return the text
*/
public String getText() {
return text;
}
/**
* @param text the text to set
*/
public void setText(String text) {
this.text = text;
}
/**
* @return the metadata
*/
public Map<String, Object> getMetadata() {
if (metadata == null) {
metadata = Maps.newHashMap();
}
return metadata;
}
/**
* @param metadata the metadata to set
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
}

View File

@ -0,0 +1,222 @@
/**
*
*/
package de.bstly.board.model;
import java.time.Instant;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import com.google.common.collect.Maps;
/**
* @author Lurkars
*
*/
@Entity
@Table(name = "entries")
@EntityListeners({ AuditingEntityListener.class })
public class Entry {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "author", nullable = false)
private String author;
@Column(name = "created", nullable = false)
private Instant created;
@Enumerated(EnumType.STRING)
@Column(name = "entry_type", nullable = false)
private EntryType entryType;
@Enumerated(EnumType.STRING)
@Column(name = "entry_status", nullable = false, columnDefinition = "varchar(255) default 'NORMAL'")
private EntryStatus entryStatus;
@Column(name = "url")
private String url;
@Column(name = "title", nullable = false)
private String title;
@Lob
@Column(name = "text")
private String text;
@Transient
private Double ranking;
@Transient
private Map<String, Object> metadata;
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the author
*/
public String getAuthor() {
return author;
}
/**
* @param author the author to set
*/
public void setAuthor(String author) {
this.author = author;
}
/**
* @return the created
*/
public Instant getCreated() {
return created;
}
/**
* @param created the created to set
*/
public void setCreated(Instant created) {
this.created = created;
}
/**
* @return the entryType
*/
public EntryType getEntryType() {
return entryType;
}
/**
* @param entryType the entryType to set
*/
public void setEntryType(EntryType entryType) {
this.entryType = entryType;
}
/**
* @return the entryStatus
*/
public EntryStatus getEntryStatus() {
return entryStatus;
}
/**
* @param entryStatus the entryStatus to set
*/
public void setEntryStatus(EntryStatus entryStatus) {
this.entryStatus = entryStatus;
}
/**
* @return the url
*/
public String getUrl() {
return url;
}
/**
* @param url the url to set
*/
public void setUrl(String url) {
this.url = url;
}
/**
* @return the title
*/
public String getTitle() {
return title;
}
/**
* @param title the title to set
*/
public void setTitle(String title) {
this.title = title;
}
/**
* @return the text
*/
public String getText() {
return text;
}
/**
* @param text the text to set
*/
public void setText(String text) {
this.text = text;
}
/**
* @return the ranking
*/
public Double getRanking() {
return ranking;
}
/**
* @param ranking the ranking to set
*/
public void setRanking(Double ranking) {
this.ranking = ranking;
}
/**
* @return the metadata
*/
public Map<String, Object> getMetadata() {
if (metadata == null) {
metadata = Maps.newHashMap();
}
return metadata;
}
/**
* @param metadata the metadata to set
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
/**
*
* @param rankedEntry
* @return
*/
public static Entry fromRankedEntry(RankedEntry rankedEntry) {
Entry entry = new Entry();
entry.setId(rankedEntry.getId());
entry.setAuthor(rankedEntry.getAuthor());
entry.setCreated(rankedEntry.getCreated());
entry.setEntryType(rankedEntry.getEntry_Type());
entry.setUrl(rankedEntry.getUrl());
entry.setTitle(rankedEntry.getTitle());
entry.setText(rankedEntry.getText());
entry.setRanking(rankedEntry.getRanking());
entry.getMetadata().put("points", rankedEntry.getPoints());
return entry;
}
}

View File

@ -0,0 +1,14 @@
/**
*
*/
package de.bstly.board.model;
/**
* @author Lurkars
*
*/
public enum EntryStatus {
NORMAL, ARCHIVED, PINNED
}

View File

@ -0,0 +1,14 @@
/**
*
*/
package de.bstly.board.model;
/**
* @author Lurkars
*
*/
public enum EntryType {
DISCUSSION, INTERN, LINK, QUESTION
}

View File

@ -0,0 +1,206 @@
/**
*
*/
package de.bstly.board.model;
import java.util.List;
import java.util.Map;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.google.common.collect.Maps;
/**
* @author monitoring@bstly.de
*
*/
@Entity
@Table(name = "users")
@JsonInclude(Include.NON_EMPTY)
public class LocalUser {
@Id
@Column(name = "username", nullable = false)
private String username;
@Column(name = "external_id", nullable = true)
private String externalId;
@JsonIgnore
@Column(name = "password_hash", nullable = true)
private String passwordHash;
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
@CollectionTable(name = "users_roles")
private List<String> roles;
@Lob
@Column(name = "about", nullable = true)
private String about;
@Column(name = "email", nullable = true)
private String email;
@Column(name = "locale", nullable = false, columnDefinition = "varchar(255) default 'en'")
private String locale;
@Column(name = "dark_theme", columnDefinition = "boolean default false")
private boolean darkTheme;
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
@CollectionTable(name = "users_settings")
private Map<String, String> settings;
@Transient
private Map<String, Object> metadata;
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the externalId
*/
public String getExternalId() {
return externalId;
}
/**
* @param externalId the externalId to set
*/
public void setExternalId(String externalId) {
this.externalId = externalId;
}
/**
* @return the passwordHash
*/
public String getPasswordHash() {
return passwordHash;
}
/**
* @param passwordHash the passwordHash to set
*/
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
/**
* @return the roles
*/
public List<String> getRoles() {
return roles;
}
/**
* @param roles the roles to set
*/
public void setRoles(List<String> roles) {
this.roles = roles;
}
/**
* @return the about
*/
public String getAbout() {
return about;
}
/**
* @param about the about to set
*/
public void setAbout(String about) {
this.about = about;
}
/**
* @return the email
*/
public String getEmail() {
return email;
}
/**
* @param email the email to set
*/
public void setEmail(String email) {
this.email = email;
}
/**
* @return the locale
*/
public String getLocale() {
return locale;
}
/**
* @param locale the locale to set
*/
public void setLocale(String locale) {
this.locale = locale;
}
/**
* @return the darkTheme
*/
public boolean isDarkTheme() {
return darkTheme;
}
/**
* @param darkTheme the darkTheme to set
*/
public void setDarkTheme(boolean darkTheme) {
this.darkTheme = darkTheme;
}
/**
* @return the settings
*/
public Map<String, String> getSettings() {
return settings;
}
/**
* @param settings the settings to set
*/
public void setSettings(Map<String, String> settings) {
this.settings = settings;
}
/**
* @return the metadata
*/
public Map<String, Object> getMetadata() {
if (metadata == null) {
metadata = Maps.newHashMap();
}
return metadata;
}
/**
* @param metadata the metadata to set
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
}

View File

@ -0,0 +1,87 @@
/**
*
*/
package de.bstly.board.model;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* @author monitoring@bstly.de
*
*/
@Entity
@Table(name = "persistent_logins")
public class PersistentLogin {
@Column(name = "username", length = 64, nullable = false)
private String username;
@Id
@Column(name = "series", length = 64)
private String series;
@Column(name = "token", length = 64, nullable = false)
private String token;
@Column(name = "last_used", nullable = false)
private Instant last_used;
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the series
*/
public String getSeries() {
return series;
}
/**
* @param series the series to set
*/
public void setSeries(String series) {
this.series = series;
}
/**
* @return the token
*/
public String getToken() {
return token;
}
/**
* @param token the token to set
*/
public void setToken(String token) {
this.token = token;
}
/**
* @return the last_used
*/
public Instant getLast_used() {
return last_used;
}
/**
* @param last_used the last_used to set
*/
public void setLast_used(Instant last_used) {
this.last_used = last_used;
}
}

View File

@ -0,0 +1,31 @@
/**
*
*/
package de.bstly.board.model;
import java.time.Instant;
/**
* @author Lurkars
*
*/
public interface RankedEntry {
Long getId();
String getAuthor();
Instant getCreated();
EntryType getEntry_Type();
String getUrl();
String getTitle();
String getText();
Double getRanking();
Long getPoints();
}

View File

@ -0,0 +1,13 @@
/**
*
*/
package de.bstly.board.model;
/**
* @author Lurkars
*
*/
public enum Types {
comment, entry, user
}

View File

@ -0,0 +1,108 @@
/**
*
*/
package de.bstly.board.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* @author Lurkars
*
*/
@Entity
@Table(name = "votes")
@EntityListeners({ AuditingEntityListener.class })
public class Vote {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
@Column(name = "target", nullable = false)
private Long target;
@Column(name = "target_type", nullable = false)
private Types targetType;
@Column(name = "author", nullable = false)
private String author;
@Column(name = "type", nullable = false)
private VoteType type;
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the target
*/
public Long getTarget() {
return target;
}
/**
* @param target the target to set
*/
public void setTarget(Long target) {
this.target = target;
}
/**
* @return the targetType
*/
public Types getTargetType() {
return targetType;
}
/**
* @param targetType the targetType to set
*/
public void setTargetType(Types targetType) {
this.targetType = targetType;
}
/**
* @return the author
*/
public String getAuthor() {
return author;
}
/**
* @param author the author to set
*/
public void setAuthor(String author) {
this.author = author;
}
/**
* @return the type
*/
public VoteType getType() {
return type;
}
/**
* @param type the type to set
*/
public void setType(VoteType type) {
this.type = type;
}
}

View File

@ -0,0 +1,12 @@
/**
*
*/
package de.bstly.board.model;
/**
* @author Lurkars
*
*/
public enum VoteType {
up, down
}

View File

@ -0,0 +1,45 @@
/**
*
*/
package de.bstly.board.repository;
import java.time.Instant;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import de.bstly.board.model.Comment;
/**
*
* @author monitoring@bstly.de
*
*/
@Repository
public interface CommentRepository
extends JpaRepository<Comment, Long>, QuerydslPredicateExecutor<Comment> {
@Query(value = "SELECT comment.*, ranked.ranking FROM comments AS comment LEFT JOIN (SELECT comment.id, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) / POW(TIMESTAMPDIFF(HOUR, comment.created, :date)+2,:gravity) AS ranking FROM comments AS comment LEFT JOIN (SELECT upvote.target,COUNT(upvote.id) AS count FROM votes as upvote WHERE upvote.type = 0 AND upvote.target_type = 0 GROUP BY upvote.target) AS upvote ON upvote.target = comment.id LEFT JOIN (SELECT downvote.target,COUNT(downvote.id) AS count FROM votes as downvote WHERE downvote.type = 1 GROUP BY downvote.target) AS downvote ON downvote.target = comment.id) as ranked on ranked.id = comment.id WHERE comment.target = :target AND parent IS NULL AND comment.created < :date ORDER BY ranked.ranking DESC, comment.created DESC", countQuery = "SELECT count(*) FROM comments as comment WHERE comment.target = :target AND parent IS NULL AND comment.created < :date", nativeQuery = true)
List<Comment> findAllByRankingAndParent(@Param("target") Long target,
@Param("date") Instant date, @Param("gravity") double gravity);
@Query(value = "SELECT comment.*, ranked.ranking FROM comments AS comment LEFT JOIN (SELECT comment.id, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) / POW(TIMESTAMPDIFF(HOUR, comment.created, :date)+2,:gravity) AS ranking FROM comments AS comment LEFT JOIN (SELECT upvote.target,COUNT(upvote.id) AS count FROM votes as upvote WHERE upvote.type = 0 AND upvote.target_type = 0 GROUP BY upvote.target) AS upvote ON upvote.target = comment.id LEFT JOIN (SELECT downvote.target,COUNT(downvote.id) AS count FROM votes as downvote WHERE downvote.type = 1 GROUP BY downvote.target) AS downvote ON downvote.target = comment.id) as ranked on ranked.id = comment.id WHERE comment.target = :target AND parent = :parent AND comment.created < :date ORDER BY ranked.ranking DESC, comment.created DESC", countQuery = "SELECT count(*) FROM comments as comment WHERE comment.target = :target AND parent = :parent AND comment.created < :date", nativeQuery = true)
List<Comment> findAllByRankingAndParent(@Param("target") Long target,
@Param("parent") Long parent, @Param("date") Instant date,
@Param("gravity") double gravity);
@Query(value = "SELECT comment.*, ranked.ranking FROM comments AS comment LEFT JOIN (SELECT comment.id, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) / POW(TIMESTAMPDIFF(HOUR, comment.created, :date)+2,:gravity) AS ranking FROM comments AS comment LEFT JOIN (SELECT upvote.target,COUNT(upvote.id) AS count FROM votes as upvote WHERE upvote.type = 0 AND upvote.target_type = 0 GROUP BY upvote.target) AS upvote ON upvote.target = comment.id LEFT JOIN (SELECT downvote.target,COUNT(downvote.id) AS count FROM votes as downvote WHERE downvote.type = 1 GROUP BY downvote.target) AS downvote ON downvote.target = comment.id) as ranked on ranked.id = comment.id WHERE comment.target = :target AND parent IS NULL AND comment.created < :date ORDER BY ranked.ranking DESC, comment.created DESC", countQuery = "SELECT count(*) FROM comments as comment WHERE comment.target = :target AND parent IS NULL AND comment.created < :date", nativeQuery = true)
Page<Comment> findAllByRankingAndParent(@Param("target") Long target,
@Param("date") Instant date, @Param("gravity") double gravity, Pageable pageable);
@Query(value = "SELECT comment.*, ranked.ranking FROM comments AS comment LEFT JOIN (SELECT comment.id, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) / POW(TIMESTAMPDIFF(HOUR, comment.created, :date)+2,:gravity) AS ranking FROM comments AS comment LEFT JOIN (SELECT upvote.target,COUNT(upvote.id) AS count FROM votes as upvote WHERE upvote.type = 0 AND upvote.target_type = 0 GROUP BY upvote.target) AS upvote ON upvote.target = comment.id LEFT JOIN (SELECT downvote.target,COUNT(downvote.id) AS count FROM votes as downvote WHERE downvote.type = 1 GROUP BY downvote.target) AS downvote ON downvote.target = comment.id) as ranked on ranked.id = comment.id WHERE comment.target = :target AND parent = :parent AND comment.created < :date ORDER BY ranked.ranking DESC, comment.created DESC", countQuery = "SELECT count(*) FROM comments as comment WHERE comment.target = :target AND parent = :parent AND comment.created < :date", nativeQuery = true)
Page<Comment> findAllByRankingAndParent(@Param("target") Long target,
@Param("parent") Long parent, @Param("date") Instant date,
@Param("gravity") double gravity, Pageable pageable);
}

View File

@ -0,0 +1,58 @@
/**
*
*/
package de.bstly.board.repository;
import java.time.Instant;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import de.bstly.board.model.Entry;
import de.bstly.board.model.RankedEntry;
/**
*
* @author monitoring@bstly.de
*
*/
@Repository
public interface EntryRepository
extends JpaRepository<Entry, Long>, QuerydslPredicateExecutor<Entry> {
static final String UPVOTES_QUERY = "SELECT upvote.target,COUNT(upvote.id) AS count FROM votes as upvote WHERE upvote.type = 0 AND upvote.target_type = 1 GROUP BY upvote.target";
static final String DOWNVOTES_QUERY = "SELECT downvote.target,COUNT(downvote.id) AS count FROM votes as downvote WHERE downvote.type = 1 AND downvote.target_type = 1 GROUP BY downvote.target";
static final String CALCULATION_QUERY = "SELECT entry.*, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) as points, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) / POW(TIMESTAMPDIFF(HOUR, entry.created, :before)+2,:gravity) AS ranking FROM entries AS entry LEFT JOIN ("
+ UPVOTES_QUERY
+ ") AS upvote ON upvote.target = entry.id LEFT JOIN ("
+ DOWNVOTES_QUERY
+ ") AS downvote ON downvote.target = entry.id WHERE entry.created < :before AND entry.entry_status = 'NORMAL' ORDER BY ranking DESC, entry.created DESC";
static final String ARCHIVE_CALCULATION_QUERY = "SELECT entry.*, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) as points, (IFNULL(upvote.count,0) - IFNULL(downvote.count,0)) / POW(TIMESTAMPDIFF(HOUR, entry.created, :before)+2,:gravity) AS ranking FROM entries AS entry LEFT JOIN ("
+ UPVOTES_QUERY
+ ") AS upvote ON upvote.target = entry.id LEFT JOIN ("
+ DOWNVOTES_QUERY
+ ") AS downvote ON downvote.target = entry.id WHERE entry.created < :before ORDER BY ranking DESC, entry.created DESC";
static final String ADDITIONAL_QUERY = "SELECT entry.*, calculation.ranking, calculation.points FROM entries AS entry LEFT JOIN ("
+ CALCULATION_QUERY
+ ") as calculation on calculation.id = entry.id WHERE entry.created < :before ORDER BY calculation.ranking DESC, entry.created DESC";
static final String COUNT_QUERY = "SELECT count(*) FROM entries as entry WHERE entry.created < :before";
@Query(value = CALCULATION_QUERY, countQuery = COUNT_QUERY, nativeQuery = true)
Page<RankedEntry> findAllByRanking(@Param("before") Instant before,
@Param("gravity") double gravity, Pageable pageable);
@Query(value = ARCHIVE_CALCULATION_QUERY, countQuery = COUNT_QUERY, nativeQuery = true)
Page<RankedEntry> findAllByRankingArchive(@Param("before") Instant before,
@Param("gravity") double gravity, Pageable pageable);
}

View File

@ -0,0 +1,21 @@
/**
*
*/
package de.bstly.board.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.bstly.board.model.LocalUser;
/**
*
* @author monitoring@bstly.de
*
*/
@Repository
public interface LocalUserRepository
extends JpaRepository<LocalUser, String>, QuerydslPredicateExecutor<LocalUser> {
}

View File

@ -0,0 +1,20 @@
/**
*
*/
package de.bstly.board.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.bstly.board.model.Vote;
/**
*
* @author monitoring@bstly.de
*
*/
@Repository
public interface VoteRepository extends JpaRepository<Vote, Long>, QuerydslPredicateExecutor<Vote> {
}

View File

@ -0,0 +1,50 @@
/**
*
*/
package de.bstly.board.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import de.bstly.board.businesslogic.UserManager;
import de.bstly.board.model.LocalUser;
/**
* @author Lurkars
*
*/
@Component
public class OAuth2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private UserManager localUserManager;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
LocalUser localUser = localUserManager.getByAuth(authentication);
User user = new User(localUser.getUsername(), "", authentication.getAuthorities());
UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(
user, null, authentication.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(newAuthentication);
handle(request, response, newAuthentication);
clearAuthenticationAttributes(request);
}
}

View File

@ -0,0 +1,111 @@
/**
*
*/
package de.bstly.board.security;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import de.bstly.board.businesslogic.UserManager;
/**
*
* @author monitoring@bstly.de
*
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserManager localUserManager;
@Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Value("${loginUrl:/login}")
private String loginUrl;
@Value("${loginTargetUrl:/}")
private String loginTargetUrl;
/*
*
* @see org.springframework.security.config.annotation.web.configuration.
* WebSecurityConfigurerAdapter#configure(org.springframework.security.config.
* annotation.web.builders.HttpSecurity)
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
oAuth2AuthenticationSuccessHandler.setDefaultTargetUrl(loginTargetUrl);
http
// crsf
.csrf().disable()
// anonymous
.anonymous().disable()
// login
.formLogin().loginPage("/login").defaultSuccessUrl(loginTargetUrl)
.failureHandler(new SimpleUrlAuthenticationFailureHandler(loginUrl
+ "?error"))
.and()
// remember me
.rememberMe().rememberMeServices(rememberMeServices()).and()
// logout
.logout().logoutUrl("/logout")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
.and()
// exception
.exceptionHandling()
.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/api/**"))
.and()
// oidc
.oauth2Login().successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(new SimpleUrlAuthenticationFailureHandler(loginUrl
+ "?externalError"))
.loginPage("/login");
}
/**
*
* @return
*/
@Bean(name = "passwordEncoder")
public Argon2PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Bean
public RememberMeServices rememberMeServices() {
PersistentTokenBasedRememberMeServices rememberMeServices = new PersistentTokenBasedRememberMeServices(
"remember-me", localUserManager, persistentTokenRepository());
return rememberMeServices;
}
}

View File

@ -0,0 +1,46 @@
# index
login=Login
login.error=Credentials not valid.
login.external=Login with <strong>{0}</strong>
logout=Logout
pagination.first=First
pagination.prev=Previous
pagination.next=Next
pagination.last=Last
username=Username
password=Password
rememberMe=Remember Me
settings=Settings
submit=Submit
top=Top
new=New
comments={0} comments
entry.title=title
entry.url=url
entry.text=text
entry.type=type
entry.vote=vote
entry.unvote=unvote
entry.votes={0} points
entry.createdBy=by <a href="/u/{0}">{0}</a>
entryType.LINK=link
entryType.DISCUSSION=discussion
entryType.QUESTION=question
entryType.INTERN=intern
entryType.LINK.bicon=bi-link-45deg
entryType.DISCUSSION.bicon=bi-chat-text
entryType.QUESTION.bicon=bi-question-square
entryType.INTERN.bicon=bi-shield-lock
REQUIRED.entry.title=title is required
REQUIRED.entry.type=type is required
REQUIRED.entry.url=url is required
INVALID.entry.url=url is invalid
entries.empty=No entries found.
submit.info=Create a new entry.