Merge pull request #33 from F43nd1r/mail

WIP: Mail notifications
This commit is contained in:
F43nd1r 2019-03-06 15:23:03 +01:00 committed by GitHub
commit 5c00a3cfbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2051 additions and 1101 deletions

View file

@ -17,15 +17,15 @@ plugins {
id 'java'
id 'idea'
id 'war'
id 'org.springframework.boot' version '2.1.0.RELEASE'
id 'com.devsoap.vaadin-flow' version '1.0.0.M6'
id 'io.spring.dependency-management' version '1.0.6.RELEASE'
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'com.devsoap.vaadin-flow' version '1.0.0.RC8'
id 'io.spring.dependency-management' version '1.0.7.RELEASE'
id 'cn.bestwu.propdeps' version '0.0.10'
id 'net.researchgate.release' version '2.7.0'
id 'net.researchgate.release' version '2.8.0'
}
vaadin {
version '12.0.0.beta2'
version '13.0.0'
submitStatistics false
}
@ -70,6 +70,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.liquibase:liquibase-core'
implementation 'org.yaml:snakeyaml'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
@ -86,21 +87,20 @@ dependencies {
implementation vaadin.platform()
implementation vaadin.dependency('icons-flow')
implementation vaadin.dependency('spring-boot-starter')
implementation 'com.vaadin:vaadin-grid-flow:2.1.2'
implementation 'org.jfree:jfreechart:1.5.0'
implementation 'org.apache.xmlgraphics:batik-svggen:1.7'
implementation 'org.apache.xmlgraphics:batik-svggen:1.10'
implementation 'javax.servlet:javax.servlet-api:4.0.1'
implementation 'org.webjars.bowergithub.simpleelements:simple-dropdown:1.0.0'
implementation 'com.faendir.vaadin:jfreechart-flow:1.1.6'
//utility
implementation 'org.codeartisans:org.json:20161124'
implementation 'org.apache.commons:commons-text:1.4'
implementation 'org.apache.commons:commons-text:1.6'
implementation 'org.xbib:time:1.0.0'
implementation 'ch.acra:acra-javacore:5.1.3'
implementation 'ch.acra:acra-javacore:5.3.0-rc01'
implementation 'com.faendir.proguard:retrace:1.3'
implementation 'javax.xml.bind:jaxb-api:2.3.0'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'com.github.ziplet:ziplet:2.3.0'
implementation 'me.xdrop:fuzzywuzzy:1.1.10'
implementation 'me.xdrop:fuzzywuzzy:1.2.0'
implementation 'com.talanlabs:avatar-generator:1.1.0'
implementation 'org.ektorp:org.ektorp.spring:1.5.0'
//testing

View file

@ -16,6 +16,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -16,13 +16,13 @@
package com.faendir.acra;
import com.faendir.acra.config.AcraConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.lang.NonNull;
@ -30,7 +30,7 @@ import org.springframework.lang.NonNull;
@PropertySource("classpath:default.properties")
@PropertySource(value = "file:${user.home}/.config/acrarium/application.properties", ignoreResourceNotFound = true)
@PropertySource(value = "file:${user.home}/.acra/application.properties", ignoreResourceNotFound = true)
@EnableConfigurationProperties(AcraConfiguration.class)
@Import(MailSenderAutoConfiguration.class)
public class BackendApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);

View file

@ -1,66 +0,0 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Lukas
* @since 15.12.2017
*/
@ConfigurationProperties(prefix = "acra")
public class AcraConfiguration {
private User user;
private int paginationSize;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public int getPaginationSize() {
return paginationSize;
}
public void setPaginationSize(int paginationSize) {
this.paginationSize = paginationSize;
}
public static class User {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
}

View file

@ -0,0 +1,73 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.liquibase.change;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author lukas
* @since 09.12.18
*/
@Component
public class VersionChange extends BaseChange {
@NonNull
private final EntityManager entityManager;
@Autowired
public VersionChange(@NonNull @Lazy EntityManager entityManager) {
super("2018-12-9-version-entity", entityManager);
this.entityManager = entityManager;
}
@Override
protected void afterChange() {
iterate(() -> entityManager.createNativeQuery("SELECT " + quote("version_code") + ", " + quote("version_name") + ", " + quote("app_id") + ", GROUP_CONCAT(" + quote("stacktrace") + "." + quote("id") + " SEPARATOR ',') FROM " + quote("stacktrace") + " JOIN " + quote("bug") + " ON " + quote("stacktrace") + "." + quote("bug_id") + " = " + quote("bug") + "." + quote("id") + " GROUP BY " + quote("version_code") + ", " + quote("version_name") + ", " + quote("app_id")), o -> {
Object[] result = (Object[]) o;
int versionCode = (int) result[0];
String versioName = (String) result[1];
int appId = (int) result[2];
List<Integer> stacktraces = Stream.of((String) result[3]).map(Integer::parseInt).collect(Collectors.toList());
List<Object> list = entityManager.createNativeQuery("SELECT " + quote("mappings") + " FROM " + quote("proguard_mapping") + " WHERE " + quote("version_code") + " = ?1 AND " + quote("app_id") + " = ?2")
.setParameter(1, versionCode)
.setParameter(2, appId)
.getResultList();
String mappings = list.isEmpty() ? null : (String) list.get(0);
entityManager.createNativeQuery("INSERT INTO " + quote("version") + "(" + quote("code") + ", " + quote("name") + ", " + quote("app_id") + ", " + quote("mappings") + ") VALUES (?1, ?2, ?3, ?4)")
.setParameter(1, versionCode)
.setParameter(2, versioName)
.setParameter(3, appId)
.setParameter(4, mappings)
.executeUpdate();
int id = (int) entityManager.createNativeQuery("SELECT " + quote("id") + " FROM " + quote("version") + " WHERE " + quote("code") + " = ?1 AND " + quote("app_id") + " = ?2")
.setParameter(1, versionCode)
.setParameter(2, appId)
.getSingleResult();
entityManager.createNativeQuery("UPDATE " + quote("stacktrace") + " SET " + quote("version_id") + " = ?1 WHERE " + quote("id") + " IN ?2")
.setParameter(1, id)
.setParameter(2, stacktraces)
.executeUpdate();
});
}
}

View file

@ -23,6 +23,7 @@ import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.Type;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
@ -30,6 +31,7 @@ import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
/**
@ -44,7 +46,10 @@ public class Bug {
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false, fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)
private App app;
private boolean solved;
@Nullable
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, fetch = FetchType.EAGER)
@JoinColumn(name = "solved_version")
private Version solvedVersion;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@ -67,14 +72,6 @@ public class Bug {
return app;
}
public boolean isSolved() {
return solved;
}
public void setSolved(boolean solved) {
this.solved = solved;
}
@NonNull
public String getTitle() {
return title;
@ -84,6 +81,15 @@ public class Bug {
this.title = title;
}
@Nullable
public Version getSolvedVersion() {
return solvedVersion;
}
public void setSolvedVersion(@Nullable Version solvedVersion) {
this.solvedVersion = solvedVersion;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -0,0 +1,133 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.model;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.lang.NonNull;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.io.Serializable;
import java.util.Objects;
/**
* @author lukas
* @since 07.12.18
*/
@Entity
@IdClass(MailSettings.ID.class)
public class MailSettings {
@Id
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
private App app;
@Id
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "username")
private User user;
private boolean newBug;
private boolean regression;
private boolean spike;
private boolean summary;
@PersistenceConstructor
MailSettings() {
}
public MailSettings(@NonNull App app, @NonNull User user) {
this.app = app;
this.user = user;
}
public App getApp() {
return app;
}
public User getUser() {
return user;
}
public boolean getNewBug() {
return newBug;
}
public void setNewBug(boolean newBug) {
this.newBug = newBug;
}
public boolean getRegression() {
return regression;
}
public void setRegression(boolean regression) {
this.regression = regression;
}
public boolean getSpike() {
return spike;
}
public void setSpike(boolean spike) {
this.spike = spike;
}
public boolean getSummary() {
return summary;
}
public void setSummary(boolean summary) {
this.summary = summary;
}
static class ID implements Serializable {
private int app;
private String user;
@PersistenceConstructor
ID() {
}
public ID(App app, User user) {
this.app = app.getId();
this.user = user.getUsername();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ID id = (ID) o;
return app == id.app &&
Objects.equals(user, id.user);
}
@Override
public int hashCode() {
return Objects.hash(app, user);
}
}
}

View file

@ -1,91 +0,0 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.model;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.Type;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.lang.NonNull;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.ManyToOne;
import java.io.Serializable;
import java.util.Objects;
/**
* @author Lukas
* @since 11.12.2017
*/
@Entity
@IdClass(ProguardMapping.MetaData.class)
public class ProguardMapping {
@Id
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
private App app;
@Id private int versionCode;
@Type(type = "text") private String mappings;
@PersistenceConstructor
ProguardMapping() {
}
public ProguardMapping(App app, int versionCode, @NonNull String mappings) {
this.app = app;
this.versionCode = versionCode;
this.mappings = mappings;
}
@NonNull
public String getMappings() {
return mappings;
}
public int getVersionCode() {
return versionCode;
}
public static class MetaData implements Serializable {
private int app;
private int versionCode;
@PersistenceConstructor
MetaData() {
}
public MetaData(App app, int versionCode) {
this.app = app.getId();
this.versionCode = versionCode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MetaData metaData = (MetaData) o;
return versionCode == metaData.versionCode && Objects.equals(app, metaData.app);
}
@Override
public int hashCode() {
return Objects.hash(app, versionCode);
}
}
}

View file

@ -48,6 +48,8 @@ public class Stacktrace {
@OnDelete(action = OnDeleteAction.CASCADE)
private Bug bug;
@Type(type = "text") private String stacktrace;
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, optional = false, fetch = FetchType.EAGER)
@OnDelete(action = OnDeleteAction.CASCADE)
private Version version;
@PersistenceConstructor

View file

@ -27,6 +27,8 @@ import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.Transient;
import javax.validation.constraints.Email;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@ -48,6 +50,10 @@ public class User implements UserDetails {
@ElementCollection(fetch = FetchType.EAGER)
private Set<Permission> permissions;
private String password;
@Transient
private String plainTextPassword;
@Email
private String mail;
@PersistenceConstructor
User() {
@ -60,6 +66,20 @@ public class User implements UserDetails {
this.permissions = new HashSet<>();
}
public User(@NonNull String username, @NonNull String plainTextPassword, @NonNull String password, @NonNull Collection<Role> roles) {
this(username, password, roles);
this.plainTextPassword = plainTextPassword;
}
public User(User other) {
this.username = other.username;
this.roles = other.roles;
this.permissions = other.permissions;
this.password = other.password;
this.plainTextPassword = other.plainTextPassword;
this.mail = other.mail;
}
@NonNull
public Set<Permission> getPermissions() {
return permissions;
@ -86,12 +106,32 @@ public class User implements UserDetails {
this.password = password;
}
public boolean hasPlainTextPassword() {
return plainTextPassword != null;
}
@NonNull
public String getPlainTextPassword() {
if(plainTextPassword == null) {
throw new IllegalStateException("Trying to access plain text password of persisted entity");
}
return plainTextPassword;
}
public void setPlainTextPassword(@NonNull String plainTextPassword) {
this.plainTextPassword = plainTextPassword;
}
@NonNull
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public boolean isAccountNonExpired() {
return true;
@ -112,6 +152,15 @@ public class User implements UserDetails {
return true;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public enum Role implements GrantedAuthority {
ADMIN,
USER,

View file

@ -16,31 +16,52 @@
package com.faendir.acra.model;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.Type;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
/**
* @author lukas
* @since 26.07.18
*/
@Embeddable
public class Version {
@Column(name = "version_code")
@Entity
public class Version implements Comparable<Version>{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
private App app;
private int code;
@Column(name = "version_name")
private String name;
@Type(type = "text")
@Nullable
private String mappings;
@PersistenceConstructor
Version() {
}
public Version(int code, String name) {
public Version(App app, int code, String name) {
this.app = app;
this.code = code;
this.name = name;
}
public Version(App app, int code, String name, String mappings) {
this(app, code, name);
this.mappings = mappings;
}
public int getCode() {
return code;
}
@ -48,4 +69,14 @@ public class Version {
public String getName() {
return name;
}
@Nullable
public String getMappings() {
return mappings;
}
@Override
public int compareTo(@NonNull Version o) {
return Integer.compare(code, o.code);
}
}

View file

@ -29,9 +29,11 @@ import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAUpdateClause;
import me.xdrop.fuzzywuzzy.FuzzySearch;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -54,16 +56,18 @@ import static com.faendir.acra.model.QStacktraceMatch.stacktraceMatch;
@EnableAsync
@Service
public class BugMerger {
@NonNull private final EntityManager entityManager;
@NonNull
private final EntityManager entityManager;
@Autowired
public BugMerger(@NonNull EntityManager entityManager) {
this.entityManager = entityManager;
}
@Async
@EventListener
@Transactional
void checkAutoMerge(@NonNull Stacktrace stacktrace) {
public void checkAutoMerge(@NonNull NewReportEvent event) {
Stacktrace stacktrace = event.getReport().getStacktrace();
Bug b = new JPAQuery<>(entityManager).from(stacktrace1).where(stacktrace1.eq(stacktrace)).join(stacktrace1.bug, bug).join(bug.app).fetchJoin().select(bug).fetchOne();
if (b != null) {
CloseableIterator<Stacktrace> iterator = new JPAQuery<>(entityManager).from(stacktrace1)
@ -90,11 +94,11 @@ public class BugMerger {
}
}
@EventListener
@Async
@Transactional
public void changeConfiguration(@NonNull App app, @NonNull App.Configuration configuration) {
app.setConfiguration(configuration);
app = entityManager.merge(app);
public void changeConfiguration(@NonNull ConfigurationUpdateEvent event) {
App app = event.getApp();
QStacktrace stacktrace2 = new QStacktrace("stacktrace2");
QBug bug2 = new QBug("bug2");
CloseableIterator<Tuple> traceIterator = new JPAQuery<>(entityManager).from(stacktrace1)
@ -110,7 +114,7 @@ public class BugMerger {
.notExists()))
.select(stacktrace1, stacktrace2)
.iterate();
while (traceIterator.hasNext()){
while (traceIterator.hasNext()) {
Tuple tuple = traceIterator.next();
Stacktrace left = tuple.get(stacktrace1);
Stacktrace right = tuple.get(stacktrace2);
@ -133,7 +137,7 @@ public class BugMerger {
.iterate();
while (matchIterator.hasNext()) {
StacktraceMatch match = matchIterator.next();
if (match.getScore() >= configuration.getMinScore() && !match.getLeft().getBug().equals(match.getRight().getBug())) {
if (match.getScore() >= app.getConfiguration().getMinScore() && !match.getLeft().getBug().equals(match.getRight().getBug())) {
new JPAUpdateClause(entityManager, stacktrace1).set(stacktrace1.bug, match.getLeft().getBug()).where(stacktrace1.bug.eq(match.getRight().getBug())).execute();
}
}
@ -142,6 +146,7 @@ public class BugMerger {
deleteOrphanBugs();
}
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#bugs[0].app, T(com.faendir.acra.model.Permission$Level).EDIT)")
@Transactional
public Bug mergeBugs(@NonNull @Size(min = 2) Collection<Bug> bugs, @NonNull String title) {
List<Bug> list = new ArrayList<>(bugs);
@ -153,6 +158,12 @@ public class BugMerger {
return bug;
}
@EventListener
@Transactional
public void onReportsDeleted(ReportsDeleteEvent event) {
deleteOrphanBugs();
}
@Transactional
public void deleteOrphanBugs() {
new JPADeleteClause(entityManager, bug).where(bug.notIn(JPAExpressions.select(stacktrace1.bug).from(stacktrace1).distinct())).execute();

View file

@ -14,25 +14,24 @@
* limitations under the License.
*/
package com.faendir.acra.util;
package com.faendir.acra.service;
import com.faendir.acra.model.User;
import java.util.Collection;
import com.faendir.acra.model.App;
import org.springframework.context.ApplicationEvent;
/**
* @author lukas
* @since 21.05.18
* @since 07.12.18
*/
public class PlainTextUser extends User {
private final String plaintextPassword;
public class ConfigurationUpdateEvent extends ApplicationEvent {
private final App app;
public PlainTextUser(String username, String plaintextPassword, String encodedPassword, Collection<Role> roles) {
super(username, encodedPassword, roles);
this.plaintextPassword = plaintextPassword;
public ConfigurationUpdateEvent(Object source, App app) {
super(source);
this.app = app;
}
public String getPlaintextPassword() {
return plaintextPassword;
public App getApp() {
return app;
}
}

View file

@ -19,17 +19,17 @@ import com.faendir.acra.dataprovider.QueryDslDataProvider;
import com.faendir.acra.model.App;
import com.faendir.acra.model.Attachment;
import com.faendir.acra.model.Bug;
import com.faendir.acra.model.ProguardMapping;
import com.faendir.acra.model.MailSettings;
import com.faendir.acra.model.QApp;
import com.faendir.acra.model.Report;
import com.faendir.acra.model.Stacktrace;
import com.faendir.acra.model.User;
import com.faendir.acra.model.Version;
import com.faendir.acra.model.view.Queries;
import com.faendir.acra.model.view.VApp;
import com.faendir.acra.model.view.VBug;
import com.faendir.acra.model.view.WhereExpressions;
import com.faendir.acra.util.ImportResult;
import com.faendir.acra.util.PlainTextUser;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Predicate;
@ -50,6 +50,7 @@ import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.access.prepost.PostAuthorize;
@ -60,13 +61,11 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityManager;
import javax.validation.constraints.Size;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -79,9 +78,10 @@ import java.util.stream.StreamSupport;
import static com.faendir.acra.model.QApp.app;
import static com.faendir.acra.model.QAttachment.attachment;
import static com.faendir.acra.model.QBug.bug;
import static com.faendir.acra.model.QProguardMapping.proguardMapping;
import static com.faendir.acra.model.QMailSettings.mailSettings;
import static com.faendir.acra.model.QReport.report;
import static com.faendir.acra.model.QStacktrace.stacktrace1;
import static com.faendir.acra.model.QVersion.version;
/**
* @author lukas
@ -96,14 +96,14 @@ public class DataService implements Serializable {
@NonNull
private final EntityManager entityManager;
@NonNull
private final BugMerger bugMerger;
private final ApplicationEventPublisher applicationEventPublisher;
private final Object stacktraceLock = new Object();
@Autowired
public DataService(@NonNull UserService userService, @NonNull EntityManager entityManager, @NonNull BugMerger bugMerger) {
public DataService(@NonNull UserService userService, @NonNull EntityManager entityManager, @NonNull ApplicationEventPublisher applicationEventPublisher) {
this.userService = userService;
this.entityManager = entityManager;
this.bugMerger = bugMerger;
this.applicationEventPublisher = applicationEventPublisher;
}
@NonNull
@ -120,7 +120,7 @@ public class DataService implements Serializable {
@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public QueryDslDataProvider<VBug> getBugProvider(@NonNull App app, BooleanSupplier onlyNonSolvedProvider) {
Supplier<BooleanExpression> whereSupplier = () -> onlyNonSolvedProvider.getAsBoolean() ? bug.app.eq(app).and(bug.solved.eq(false)) : bug.app.eq(app);
Supplier<BooleanExpression> whereSupplier = () -> onlyNonSolvedProvider.getAsBoolean() ? bug.app.eq(app).and(bug.solvedVersion.isNull()) : bug.app.eq(app);
return new QueryDslDataProvider<>(() -> Queries.selectVBug(entityManager).where(whereSupplier.get()),
() -> new JPAQuery<>(entityManager).from(bug).where(whereSupplier.get()));
}
@ -179,8 +179,8 @@ public class DataService implements Serializable {
@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public QueryDslDataProvider<ProguardMapping> getMappingProvider(@NonNull App app) {
return new QueryDslDataProvider<>(new JPAQuery<>(entityManager).from(proguardMapping).where(proguardMapping.app.eq(app)).select(proguardMapping));
public QueryDslDataProvider<Version> getVersionProvider(@NonNull App app) {
return new QueryDslDataProvider<>(new JPAQuery<>(entityManager).from(version).where(version.app.eq(app)).select(version));
}
@Transactional
@ -202,8 +202,8 @@ public class DataService implements Serializable {
@Transactional
@NonNull
@PreAuthorize("hasRole(T(com.faendir.acra.model.User$Role).ADMIN)")
public PlainTextUser createNewApp(@NonNull String name) {
PlainTextUser user = userService.createReporterUser();
public User createNewApp(@NonNull String name) {
User user = userService.createReporterUser();
store(new App(name, user));
return user;
}
@ -211,18 +211,13 @@ public class DataService implements Serializable {
@Transactional
@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).ADMIN)")
public PlainTextUser recreateReporterUser(@NonNull App app) {
PlainTextUser user = userService.createReporterUser();
public User recreateReporterUser(@NonNull App app) {
User user = userService.createReporterUser();
app.setReporter(user);
store(app);
return user;
}
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#bugs[0].app, T(com.faendir.acra.model.Permission$Level).EDIT)")
public void mergeBugs(@NonNull @Size(min = 2) Collection<Bug> bugs, @NonNull String title) {
bugMerger.mergeBugs(bugs, title);
}
@Transactional
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#bug.app, T(com.faendir.acra.model.Permission$Level).EDIT)")
public void unmergeBug(@NonNull Bug bug) {
@ -232,19 +227,19 @@ public class DataService implements Serializable {
@Transactional
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#bug.app, T(com.faendir.acra.model.Permission$Level).EDIT)")
public void setBugSolved(@NonNull Bug bug, boolean solved) {
bug.setSolved(solved);
public void setBugSolved(@NonNull Bug bug, Version solved) {
bug.setSolvedVersion(solved);
store(bug);
}
@NonNull
/*@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public Optional<ProguardMapping> findMapping(@NonNull App app, int versionCode) {
return Optional.ofNullable(new JPAQuery<>(entityManager).from(proguardMapping)
.where(proguardMapping.app.eq(app).and(proguardMapping.versionCode.eq(versionCode)))
.select(proguardMapping)
.fetchOne());
}
}*/
@NonNull
@PostAuthorize("!returnObject.isPresent() || T(com.faendir.acra.security.SecurityUtils).hasPermission(returnObject.get().stacktrace.bug.app, T(com.faendir.acra.model.Permission$Level).VIEW)")
@ -261,16 +256,6 @@ public class DataService implements Serializable {
.fetchOne());
}
@NonNull
@PostAuthorize("!returnObject.isPresent() || T(com.faendir.acra.security.SecurityUtils).hasPermission(returnObject.get().app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public Optional<Bug> findBug(@NonNull String encodedId) {
try {
return findBug(Integer.parseInt(encodedId));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
@NonNull
@PostAuthorize("!returnObject.isPresent() || T(com.faendir.acra.security.SecurityUtils).hasPermission(returnObject.get().app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public Optional<Bug> findBug(@NonNull int id) {
@ -327,10 +312,26 @@ public class DataService implements Serializable {
.fetchFirst());
}
@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public List<Version> findAllVersions(@NonNull App app) {
return new JPAQuery<>(entityManager).from(version).where(version.app.eq(app)).select(version).fetch();
}
@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public Optional<MailSettings> findMailSettings(@NonNull App app, @NonNull User user) {
return Optional.ofNullable(new JPAQuery<>(entityManager).from(mailSettings).where(mailSettings.app.eq(app).and(mailSettings.user.eq(user))).select(mailSettings).fetchOne());
}
@Transactional
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).EDIT)")
public void changeConfiguration(@NonNull App app, @NonNull App.Configuration configuration) {
bugMerger.changeConfiguration(app, configuration);
app.setConfiguration(configuration);
app = store(app);
entityManager.flush();
applicationEventPublisher.publishEvent(new ConfigurationUpdateEvent(this, app));
}
@Transactional
@ -338,7 +339,7 @@ public class DataService implements Serializable {
public void deleteReportsOlderThanDays(@NonNull App app, @NonNull int days) {
new JPADeleteClause(entityManager, report).where(report.stacktrace.bug.app.eq(app).and(report.date.before(ZonedDateTime.now().minus(days, ChronoUnit.DAYS))));
entityManager.flush();
bugMerger.deleteOrphanBugs();
applicationEventPublisher.publishEvent(new ReportsDeleteEvent(this));
}
@Transactional
@ -346,7 +347,7 @@ public class DataService implements Serializable {
public void deleteReportsBeforeVersion(@NonNull App app, int versionCode) {
new JPADeleteClause(entityManager, report).where(report.stacktrace.bug.app.eq(app).and(report.stacktrace.version.code.lt(versionCode)));
entityManager.flush();
bugMerger.deleteOrphanBugs();
applicationEventPublisher.publishEvent(new ReportsDeleteEvent(this));
}
@Transactional
@ -356,7 +357,7 @@ public class DataService implements Serializable {
if (app != null) {
JSONObject jsonObject = new JSONObject(content);
String trace = jsonObject.optString(ReportField.STACK_TRACE.name());
Version version = getVersion(jsonObject);
Version version = getVersion(app, jsonObject);
Stacktrace stacktrace = findStacktrace(app, trace, version.getCode()).orElseGet(() -> {
synchronized (stacktraceLock) {
return findStacktrace(app, trace, version.getCode()).orElseGet(() -> store(new Stacktrace(findBug(app, trace).orElseGet(() -> new Bug(app, trace)), trace, version)));
@ -372,11 +373,12 @@ public class DataService implements Serializable {
log.warn("Failed to load attachment with name " + multipartFile.getName(), e);
}
});
bugMerger.checkAutoMerge(report.getStacktrace());
entityManager.flush();
applicationEventPublisher.publishEvent(new NewReportEvent(this, report));
}
}
private Version getVersion(JSONObject jsonObject) {
private Version getVersion(App app, JSONObject jsonObject) {
JSONObject buildConfig = jsonObject.optJSONObject(ReportField.BUILD_CONFIG.name());
Integer versionCode = null;
String versionName = null;
@ -396,7 +398,7 @@ public class DataService implements Serializable {
if (versionName == null) {
versionName = jsonObject.optString(ReportField.APP_VERSION_NAME.name(), "N/A");
}
return new Version(versionCode, versionName);
return new Version(app, versionCode, versionName);
}
@NonNull
@ -435,8 +437,8 @@ public class DataService implements Serializable {
@NonNull
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasPermission(#app, T(com.faendir.acra.model.Permission$Level).VIEW)")
public Optional<Integer> getMaximumMappingVersion(@NonNull App app) {
return Optional.ofNullable(new JPAQuery<>(entityManager).from(proguardMapping).where(proguardMapping.app.eq(app)).select(proguardMapping.versionCode.max()).fetchOne());
public Optional<Integer> getMaxVersion(@NonNull App app) {
return Optional.ofNullable(new JPAQuery<>(entityManager).from(version).where(version.app.eq(app)).select(version.code.max()).fetchOne());
}
@Transactional
@ -444,7 +446,7 @@ public class DataService implements Serializable {
public ImportResult importFromAcraStorage(String host, int port, boolean ssl, String database) {
HttpClient httpClient = new StdHttpClient.Builder().host(host).port(port).enableSSL(ssl).build();
CouchDbConnector db = new StdCouchDbConnector(database, new StdCouchDbInstance(httpClient));
PlainTextUser user = createNewApp(database.replaceFirst("acra-", ""));
User user = createNewApp(database.replaceFirst("acra-", ""));
int total = 0;
int success = 0;
for (String id : db.getAllDocIds()) {

View file

@ -0,0 +1,160 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.service;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.App;
import com.faendir.acra.model.Bug;
import com.faendir.acra.model.MailSettings;
import com.faendir.acra.model.QBug;
import com.faendir.acra.model.Report;
import com.faendir.acra.model.Stacktrace;
import com.faendir.acra.model.User;
import com.faendir.acra.ui.view.bug.tabs.ReportTab;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQuery;
import com.vaadin.flow.i18n.I18NProvider;
import com.vaadin.flow.router.RouteConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.event.EventListener;
import org.springframework.lang.NonNull;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.persistence.EntityManager;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import static com.faendir.acra.model.QBug.bug;
import static com.faendir.acra.model.QMailSettings.mailSettings;
import static com.faendir.acra.model.QReport.report;
import static com.faendir.acra.model.QStacktrace.stacktrace1;
/**
* @author lukas
* @since 07.12.18
*/
@Service
@EnableScheduling
@EnableAsync
@ConditionalOnProperty(prefix = "spring.mail", name = "host")
public class MailService {
private final EntityManager entityManager;
@NonNull
private final I18NProvider i18nProvider;
@NonNull
private final JavaMailSender mailSender;
public MailService(@NonNull EntityManager entityManager, @NonNull I18NProvider i18nProvider, @NonNull JavaMailSender mailSender) {
this.entityManager = entityManager;
this.i18nProvider = i18nProvider;
this.mailSender = mailSender;
}
@Transactional
@EventListener
@Async
public void onNewReport(NewReportEvent event) {
Report r = event.getReport();
Stacktrace stacktrace = r.getStacktrace();
Bug bug = stacktrace.getBug();
App app = bug.getApp();
List<MailSettings> settings = new JPAQuery<>(entityManager).select(mailSettings).from(mailSettings).where(mailSettings.app.eq(app).and(mailSettings.user.mail.isNotNull())).fetch();
List<User> newBugReceiver = getUserBy(settings, MailSettings::getNewBug);
List<User> regressionReceiver = getUserBy(settings, MailSettings::getRegression);
List<User> spikeReceiver = getUserBy(settings, MailSettings::getSpike);
RouteConfiguration configuration = RouteConfiguration.forApplicationScope();
if (!newBugReceiver.isEmpty() && new JPAQuery<>(entityManager).from(report).where(report.stacktrace.bug.eq(bug)).limit(2).select(report.count()).fetchCount() == 1) {
sendMessage(newBugReceiver, getTranslation(Messages.NEW_BUG_MAIL_TEMPLATE, configuration.getUrl(ReportTab.class, bug.getId()), bug.getTitle(), r.getBrand(), r.getPhoneModel(), r.getAndroidVersion(), app.getName(), stacktrace.getVersion().getName()), getTranslation(Messages.NEW_BUG_MAIL_SUBJECT, app.getName()));
} else if (!regressionReceiver.isEmpty() && bug.getSolvedVersion() != null && bug.getSolvedVersion().getCode() <= stacktrace.getVersion().getCode()) {
sendMessage(regressionReceiver, getTranslation(Messages.REGRESSION_MAIL_TEMPLATE, configuration.getUrl(ReportTab.class, bug.getId()), bug.getTitle(), r.getBrand(), bug.getSolvedVersion().getName(), r.getPhoneModel(), r.getAndroidVersion(), app.getName(), stacktrace.getVersion().getName()), getTranslation(Messages.REGRESSION_MAIL_SUBJECT, app.getName()));
bug.setSolvedVersion(null);
entityManager.merge(bug);
} else if(!spikeReceiver.isEmpty()){
long reportCount = fetchReportCountOnDay(0);
double averageCount = LongStream.range(1, 3).map(this::fetchReportCountOnDay).average().orElse(Double.MAX_VALUE);
if (reportCount > 1.2 * averageCount && reportCount - 1 <= 1.2 * averageCount) {
sendMessage(regressionReceiver, getTranslation(Messages.SPIKE_MAIL_TEMPLATE, configuration.getUrl(ReportTab.class, bug.getId()), bug.getTitle(), stacktrace.getVersion().getName(), reportCount), getTranslation(Messages.SPIKE_MAIL_SUBJECT, app.getName()));
}
}
}
private List<User> getUserBy(List<MailSettings> list, Predicate<MailSettings> predicate) {
return list.stream().filter(predicate).map(MailSettings::getUser).collect(Collectors.toList());
}
private long fetchReportCountOnDay(long subtractDays) {
ZonedDateTime today = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS);
return new JPAQuery<>(entityManager).from(report).where(report.stacktrace.bug.eq(bug).and(report.date.between(today.minus(subtractDays, ChronoUnit.DAYS), today.minus(subtractDays - 1, ChronoUnit.DAYS)))).select(report.count()).fetchCount();
}
@Scheduled(cron = "0 0 0 * * SUN")
public void weeklyReport() {
Map<App, List<MailSettings>> settings = new JPAQuery<>(entityManager).select(mailSettings).from(mailSettings).where(mailSettings.summary.isTrue()).fetch()
.stream()
.collect(Collectors.groupingBy(MailSettings::getApp, Collectors.toList()));
RouteConfiguration configuration = RouteConfiguration.forApplicationScope();
for (Map.Entry<App, List<MailSettings>> entry : settings.entrySet()) {
List<Tuple> tuples = new JPAQuery<>(entityManager).from(bug).join(stacktrace1).on(bug.eq(stacktrace1.bug)).join(report).on(stacktrace1.eq(report.stacktrace)).where(bug.app.eq(entry.getKey()).and(report.date.after(ZonedDateTime.now().minus(1, ChronoUnit.WEEKS))))
.groupBy(bug)
.select(bug, report.count(), report.installationId.countDistinct())
.fetch();
String body = tuples.stream().map(tuple -> {
Bug bug = tuple.get(QBug.bug);
return getTranslation(Messages.WEEKLY_MAIL_BUG_TEMPLATE, configuration.getUrl(ReportTab.class, bug.getId()), bug.getTitle(), tuple.get(report.count()), tuple.get(report.installationId.countDistinct()));
}).collect(Collectors.joining("\n"));
if (body.isEmpty()) {
body = getTranslation(Messages.WEEKLY_MAIL_NO_REPORTS);
}
sendMessage(entry.getValue().stream().map(MailSettings::getUser).collect(Collectors.toList()), body, getTranslation(Messages.WEEKLY_MAIL_SUBJECT, entry.getKey().getName()));
}
}
private void sendMessage(@NonNull List<User> users, @NonNull String body, @NonNull String subject) {
try {
MimeMessage template = mailSender.createMimeMessage();
template.setContent(body, "text/html");
template.setSubject(subject);
for (User user : users) {
MimeMessage message = new MimeMessage(template);
message.setRecipients(Message.RecipientType.TO, user.getMail());
mailSender.send(message);
}
} catch (MessagingException e) {
e.printStackTrace();
}
}
private String getTranslation(String messageId, Object... params) {
//TODO use user locale
return i18nProvider.getTranslation(messageId, Locale.ENGLISH, params);
}
}

View file

@ -0,0 +1,37 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.service;
import com.faendir.acra.model.Report;
import org.springframework.context.ApplicationEvent;
/**
* @author lukas
* @since 07.12.18
*/
public class NewReportEvent extends ApplicationEvent {
private final Report report;
public NewReportEvent(Object source, Report report) {
super(source);
this.report = report;
}
public Report getReport() {
return report;
}
}

View file

@ -0,0 +1,29 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.service;
import org.springframework.context.ApplicationEvent;
/**
* @author lukas
* @since 07.12.18
*/
public class ReportsDeleteEvent extends ApplicationEvent {
public ReportsDeleteEvent(Object source) {
super(source);
}
}

View file

@ -15,13 +15,12 @@
*/
package com.faendir.acra.service;
import com.faendir.acra.config.AcraConfiguration;
import com.faendir.acra.dataprovider.QueryDslDataProvider;
import com.faendir.acra.model.App;
import com.faendir.acra.model.Permission;
import com.faendir.acra.model.QUser;
import com.faendir.acra.model.User;
import com.faendir.acra.util.PlainTextUser;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQuery;
import org.apache.commons.text.RandomStringGenerator;
import org.springframework.beans.factory.annotation.Autowired;
@ -33,8 +32,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.validation.Validator;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
@ -45,27 +44,27 @@ import java.util.Optional;
@Service
public class UserService implements Serializable {
private static final QUser USER = QUser.user;
@NonNull private final AcraConfiguration acraConfiguration;
@NonNull private final PasswordEncoder passwordEncoder;
@NonNull private final RandomStringGenerator randomStringGenerator;
@NonNull private final EntityManager entityManager;
@NonNull
private final PasswordEncoder passwordEncoder;
@NonNull
private final RandomStringGenerator randomStringGenerator;
@NonNull
private final EntityManager entityManager;
@NonNull
private final Validator validator;
@Autowired
public UserService(@NonNull PasswordEncoder passwordEncoder, @NonNull AcraConfiguration acraConfiguration, @NonNull RandomStringGenerator randomStringGenerator,
@NonNull EntityManager entityManager) {
public UserService(@NonNull PasswordEncoder passwordEncoder, @NonNull RandomStringGenerator randomStringGenerator,
@NonNull EntityManager entityManager, @NonNull Validator validator) {
this.passwordEncoder = passwordEncoder;
this.acraConfiguration = acraConfiguration;
this.randomStringGenerator = randomStringGenerator;
this.entityManager = entityManager;
this.validator = validator;
}
@Nullable
public User getUser(@NonNull String username) {
User user = new JPAQuery<>(entityManager).from(USER).where(USER.username.eq(username)).select(USER).fetchOne();
if (user == null && acraConfiguration.getUser().getName().equals(username)) {
user = getDefaultUser();
}
return user;
return new JPAQuery<>(entityManager).from(USER).where(USER.username.eq(username)).select(USER).fetchOne();
}
@Transactional
@ -76,21 +75,33 @@ public class UserService implements Serializable {
}
entityManager.persist(new User(username, passwordEncoder.encode(password), Collections.singleton(User.Role.USER)));
}
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasRole(T(com.faendir.acra.model.User$Role).ADMIN)")
public PlainTextUser createReporterUser() {
public User createReporterUser() {
String username;
do {
username = randomStringGenerator.generate(16);
} while (new JPAQuery<>(entityManager).from(USER).where(USER.username.eq(username)).fetchFirst() != null);
String password = randomStringGenerator.generate(16);
return new PlainTextUser(username, password, passwordEncoder.encode(password), Collections.singleton(User.Role.REPORTER));
return new User(username, password, passwordEncoder.encode(password), Collections.singleton(User.Role.REPORTER));
}
public boolean checkPassword(@Nullable User user, @NonNull String password) {
return user != null && passwordEncoder.matches(password, user.getPassword());
}
@Transactional
public User store(@NonNull User user) {
if(user.hasPlainTextPassword()) {
user.setPassword(passwordEncoder.encode(user.getPlainTextPassword()));
}
return entityManager.merge(user);
}
public boolean hasAdmin() {
return new JPAQuery<>(entityManager).from(USER).where(USER.roles.contains(User.Role.ADMIN)).select(Expressions.ONE).fetchOne() != null;
}
@Transactional
@PreAuthorize("authentication.name == #user.username")
public boolean changePassword(@NonNull User user, @NonNull String oldPassword, @NonNull String newPassword) {
@ -102,6 +113,20 @@ public class UserService implements Serializable {
return false;
}
@Transactional
@PreAuthorize("authentication.name == #user.username")
public boolean changeMail(@NonNull User user, @Nullable String mail) {
String oldMail = user.getMail();
user.setMail(mail);
if (!validator.validate(user).isEmpty()) {
user.setMail(oldMail);
return false;
}
entityManager.merge(user);
return true;
}
@Transactional
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasRole(T(com.faendir.acra.model.User$Role).ADMIN)")
public void setAdmin(@NonNull User user, boolean admin) {
@ -140,13 +165,6 @@ public class UserService implements Serializable {
entityManager.merge(user);
}
@NonNull
private User getDefaultUser() {
return new User(acraConfiguration.getUser().getName(),
passwordEncoder.encode(acraConfiguration.getUser().getPassword()),
Arrays.asList(User.Role.USER, User.Role.ADMIN));
}
@PreAuthorize("T(com.faendir.acra.security.SecurityUtils).hasRole(T(com.faendir.acra.model.User$Role).ADMIN)")
public QueryDslDataProvider<User> getUserProvider() {
return new QueryDslDataProvider<>(new JPAQuery<>(entityManager).from(USER).where(USER.roles.any().eq(User.Role.USER)).select(USER));

View file

@ -17,10 +17,10 @@
package com.faendir.acra.ui.base;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.User;
import com.faendir.acra.rest.RestReportInterface;
import com.faendir.acra.ui.component.Label;
import com.faendir.acra.ui.view.Overview;
import com.faendir.acra.util.PlainTextUser;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.i18n.LocaleChangeEvent;
import com.vaadin.flow.i18n.LocaleChangeObserver;
@ -31,15 +31,15 @@ import org.springframework.lang.NonNull;
* @since 09.11.18
*/
public class ConfigurationLabel extends Label implements LocaleChangeObserver {
private final PlainTextUser user;
private final User user;
public ConfigurationLabel(@NonNull PlainTextUser user) {
public ConfigurationLabel(@NonNull User user) {
super("");
this.user = user;
}
@Override
public void localeChange(LocaleChangeEvent event) {
getElement().setProperty("innerHTML", getTranslation(Messages.CONFIGURATION_LABEL, UI.getCurrent().getRouter().getUrl(Overview.class), RestReportInterface.REPORT_PATH, user.getUsername(), user.getPlaintextPassword()));
getElement().setProperty("innerHTML", getTranslation(Messages.CONFIGURATION_LABEL, UI.getCurrent().getRouter().getUrl(Overview.class), RestReportInterface.REPORT_PATH, user.getUsername(), user.getPlainTextPassword()));
}
}

View file

@ -17,7 +17,6 @@
package com.faendir.acra.ui.base.popup;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.HasValidation;
import com.vaadin.flow.component.HasValue;
import org.springframework.lang.NonNull;
@ -56,10 +55,6 @@ public class ValidatedField<V, T extends Component> {
return new ValidatedField<>(field, field::getValue, vConsumer -> field.addValueChangeListener(event -> vConsumer.accept(event.getValue())), field::setErrorMessage);
}
public static <V, C extends Composite<T>, T extends Component & HasValue<?, V> & HasValidation> ValidatedField<V, C> of(C field) {
return new ValidatedField<>(field, () -> field.getContent().getValue(), vConsumer -> field.getContent().addValueChangeListener(event -> vConsumer.accept(event.getValue())), m -> field.getContent().setErrorMessage(m));
}
public static <V, T extends Component & HasValue<?, V> & HasValidation> ValidatedField<V, T> of(T field, Supplier<V> valueSupplier, Consumer<Consumer<V>> listenerRegistration) {
return new ValidatedField<>(field, valueSupplier, listenerRegistration, field::setErrorMessage);
}

View file

@ -34,7 +34,23 @@ public interface HasStyle extends com.vaadin.flow.component.HasStyle {
getStyle().set("color","inherit");
}
default void setPadding(int value, HasSize.Unit unit) {
default void setPadding(double value, HasSize.Unit unit) {
getStyle().set("padding", value + unit.getText());
}
default void setPaddingLeft(double value, HasSize.Unit unit) {
getStyle().set("padding-left", value + unit.getText());
}
default void setPaddingTop(double value, HasSize.Unit unit) {
getStyle().set("padding-top", value + unit.getText());
}
default void setPaddingRight(double value, HasSize.Unit unit) {
getStyle().set("padding-right", value + unit.getText());
}
default void setPaddingBottom(double value, HasSize.Unit unit) {
getStyle().set("padding-bottom", value + unit.getText());
}
}

View file

@ -0,0 +1,39 @@
/*
* (C) Copyright 2019 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.ui.component;
import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValue;
/**
* @author lukas
* @since 01.03.19
*/
public interface HasValidation<T extends Component> extends com.vaadin.flow.component.HasValidation, HasValue<AbstractField.ComponentValueChangeEvent<T, String>, String> {
default void invalidateWithMessage(String errorMessageId, Object... params) {
setErrorMessage(getTranslation(errorMessageId, params));
setInvalid(true);
addValueChangeListener(event -> {
setInvalid(false);
event.unregisterListener();
});
}
String getTranslation(String key, Object... params);
}

View file

@ -16,11 +16,13 @@
package com.faendir.acra.ui.component;
import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.HasText;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
@ -34,9 +36,12 @@ import com.vaadin.flow.i18n.LocaleChangeEvent;
import com.vaadin.flow.i18n.LocaleChangeObserver;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.RouterLink;
import com.vaadin.flow.shared.Registration;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
/**
@ -87,13 +92,13 @@ public class Translatable<T extends Component> extends Composite<T> implements L
}
@NonNull
public static Translatable<TextField> createTextField(@NonNull String initialValue, @NonNull String captionId, @NonNull Object... params) {
return new Translatable<>(new TextField("", initialValue, ""), textField -> textField.setLabel(textField.getTranslation(captionId, params)));
public static Value<TextField> createTextField(@Nullable String initialValue, @NonNull String captionId, @NonNull Object... params) {
return new Value<>(new TextField("", initialValue == null ? "" : initialValue, ""), textField -> textField.setLabel(textField.getTranslation(captionId, params)));
}
@NonNull
public static Translatable<PasswordField> createPasswordField(@NonNull String captionId, @NonNull Object... params) {
return new Translatable<>(new PasswordField(), textField -> textField.setLabel(textField.getTranslation(captionId, params)));
public static Value<PasswordField> createPasswordField(@NonNull String captionId, @NonNull Object... params) {
return new Value<>(new PasswordField(), textField -> textField.setLabel(textField.getTranslation(captionId, params)));
}
@NonNull
@ -132,4 +137,71 @@ public class Translatable<T extends Component> extends Composite<T> implements L
div.setText(translation);
});
}
public static class Value<T extends Component & HasValue<AbstractField.ComponentValueChangeEvent<T, String>, String> & com.vaadin.flow.component.HasValidation> extends Translatable<T> implements HasValidation<T> {
protected Value(T t, Consumer<T> setter) {
super(t, setter);
}
public void setValue(String value) {
getContent().setValue(value);
}
public String getValue() {
return getContent().getValue();
}
public Registration addValueChangeListener(ValueChangeListener<? super AbstractField.ComponentValueChangeEvent<T, String>> listener) {
return getContent().addValueChangeListener(listener);
}
public String getEmptyValue() {
return getContent().getEmptyValue();
}
public Optional<String> getOptionalValue() {
return getContent().getOptionalValue();
}
public boolean isEmpty() {
return getContent().isEmpty();
}
public void clear() {
getContent().clear();
}
public void setReadOnly(boolean readOnly) {
getContent().setReadOnly(readOnly);
}
public boolean isReadOnly() {
return getContent().isReadOnly();
}
public void setRequiredIndicatorVisible(boolean requiredIndicatorVisible) {
getContent().setRequiredIndicatorVisible(requiredIndicatorVisible);
}
public boolean isRequiredIndicatorVisible() {
return getContent().isRequiredIndicatorVisible();
}
public void setErrorMessage(String errorMessage) {
getContent().setErrorMessage(errorMessage);
}
public String getErrorMessage() {
return getContent().getErrorMessage();
}
public void setInvalid(boolean invalid) {
getContent().setInvalid(invalid);
}
public boolean isInvalid() {
return getContent().isInvalid();
}
}
}

View file

@ -0,0 +1,137 @@
/*
* (C) Copyright 2019 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.ui.component;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.User;
import com.faendir.acra.service.UserService;
import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationResult;
import com.vaadin.flow.data.validator.EmailValidator;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementFactory;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.util.Arrays;
/**
* @author lukas
* @since 28.02.19
*/
public class UserEditor extends Composite<FlexLayout> {
@NonNull
private User user;
public UserEditor(@NonNull UserService userService, @Nullable User u, Runnable onSuccess) {
boolean newUser = u == null;
this.user = newUser ? new User("", "", Arrays.asList(User.Role.ADMIN, User.Role.USER)) : u;
Binder<User> binder = new Binder<>();
Translatable.Value<TextField> username = Translatable.createTextField(user.getUsername(), Messages.USERNAME);
exposeInput(username);
username.setWidthFull();
Binder.BindingBuilder<User, String> usernameBindingBuilder = binder.forField(username);
if (newUser) {
usernameBindingBuilder.asRequired(getTranslation(Messages.USERNAME_REQUIRED));
}
usernameBindingBuilder.withValidator(uName -> uName.equals(user.getUsername()) || userService.getUser(uName) == null, getTranslation(Messages.USERNAME_TAKEN))
.bind(User::getUsername, newUser ? User::setUsername : null);
getContent().add(username);
Translatable.Value<TextField> mail = Translatable.createTextField(user.getMail(), Messages.EMAIL);
mail.setWidthFull();
EmailValidator emailValidator = new EmailValidator(getTranslation(Messages.INVALID_MAIL));
binder.forField(mail).withValidator((m, c) -> {
if (m.isEmpty()) {
return ValidationResult.ok();
}
return emailValidator.apply(m, c);
}).bind(User::getMail, User::setMail);
getContent().add(mail);
Translatable.Value<PasswordField> newPassword = Translatable.createPasswordField(Messages.NEW_PASSWORD);
exposeInput(newPassword);
newPassword.setWidthFull();
Translatable.Value<PasswordField> repeatPassword = Translatable.createPasswordField(Messages.REPEAT_PASSWORD);
exposeInput(repeatPassword);
repeatPassword.setWidthFull();
if (!newUser) {
Translatable.Value<PasswordField> oldPassword = Translatable.createPasswordField(Messages.OLD_PASSWORD);
exposeInput(oldPassword);
oldPassword.setWidthFull();
Binder.Binding<User, String> oldPasswordBinding = binder.forField(oldPassword).withValidator(p -> {
if (!newPassword.getValue().isEmpty() || !oldPassword.getValue().isEmpty()) {
return userService.checkPassword(user, p);
}
return true;
}, getTranslation(Messages.INCORRECT_PASSWORD))
.bind(user1 -> "", (user1, s) -> doNothing());
getContent().add(oldPassword);
newPassword.addValueChangeListener(e -> oldPasswordBinding.validate());
repeatPassword.addValueChangeListener(e -> oldPasswordBinding.validate());
}
Binder.BindingBuilder<User, String> newPasswordBindingBuilder = binder.forField(newPassword);
Binder.BindingBuilder<User, String> repeatPasswordBindingBuilder = binder.forField(repeatPassword);
if (newUser) {
newPasswordBindingBuilder.asRequired(getTranslation(Messages.PASSWORD_REQUIRED));
repeatPasswordBindingBuilder.asRequired(getTranslation(Messages.PASSWORD_REQUIRED));
}
newPasswordBindingBuilder.bind(user1 -> "", User::setPlainTextPassword);
getContent().add(newPassword);
Binder.Binding<User, String> repeatPasswordBinding = repeatPasswordBindingBuilder.withValidator(p -> p.equals(newPassword.getValue()), getTranslation(Messages.PASSWORDS_NOT_MATCHING)).bind(user1 -> "", (user1, s) -> doNothing());
newPassword.addValueChangeListener(e -> {
if (!repeatPassword.getValue().isEmpty()) {
repeatPasswordBinding.validate();
}
});
getContent().add(repeatPassword);
binder.readBean(this.user);
Translatable<Button> button = Translatable.createButton(e -> {
if (binder.writeBeanIfValid(user)) {
user = userService.store(user);
binder.readBean(user);
onSuccess.run();
}
}, Messages.CONFIRM);
button.setWidthFull();
button.getContent().setEnabled(false);
binder.addStatusChangeListener(e -> button.getContent().setEnabled(e.getBinder().hasChanges()));
getContent().add(button);
getContent().setFlexDirection(FlexLayout.FlexDirection.COLUMN);
getContent().setMaxWidthFull();
getContent().setWidth("calc(var(--lumo-size-m) * 10)");
}
/**
* password managers need an input outside the shadow dom, which we add here.
* @param field the field which should be visible to password managers
* @param <T> the type of the field
*/
private <T extends Component & HasValue<AbstractField.ComponentValueChangeEvent<T, String>, String> & com.vaadin.flow.component.HasValidation> void exposeInput(Translatable.Value<T> field) {
Element input = ElementFactory.createInput();
input.setAttribute("slot", "input");
field.getElement().appendChild(input);
}
private void doNothing() {
}
}

View file

@ -19,13 +19,16 @@ package com.faendir.acra.ui.view;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.User;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.service.UserService;
import com.faendir.acra.ui.base.ParentLayout;
import com.faendir.acra.ui.base.Path;
import com.faendir.acra.ui.base.popup.Popup;
import com.faendir.acra.ui.component.DropdownMenu;
import com.faendir.acra.ui.component.FlexLayout;
import com.faendir.acra.ui.component.Label;
import com.faendir.acra.ui.component.Translatable;
import com.faendir.acra.ui.view.user.ChangePasswordView;
import com.faendir.acra.ui.component.UserEditor;
import com.faendir.acra.ui.view.user.AccountView;
import com.faendir.acra.ui.view.user.UserManager;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
@ -34,9 +37,8 @@ import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.dependency.HtmlImport;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.login.LoginForm;
import com.vaadin.flow.component.login.LoginI18n;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
@ -61,12 +63,14 @@ import org.springframework.security.core.context.SecurityContextHolder;
public class MainView extends ParentLayout {
private final AuthenticationManager authenticationManager;
private final ApplicationContext applicationContext;
private final UserService userService;
private ParentLayout layout;
@Autowired
public MainView(AuthenticationManager authenticationManager, ApplicationContext applicationContext) {
public MainView(AuthenticationManager authenticationManager, ApplicationContext applicationContext, UserService userService) {
this.authenticationManager = authenticationManager;
this.applicationContext = applicationContext;
this.userService = userService;
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
setSizeFull();
@ -78,8 +82,10 @@ public class MainView extends ParentLayout {
setRouterRoot(layout);
if (SecurityUtils.isLoggedIn()) {
showMain();
} else {
} else if (userService.hasAdmin()) {
showLogin();
} else {
showFirstTimeSetup();
}
}
@ -89,7 +95,7 @@ public class MainView extends ParentLayout {
Translatable<Button> userManager = Translatable.createButton(e -> UI.getCurrent().navigate(UserManager.class), Messages.USER_MANAGER);
userManager.setDefaultTextStyle();
userManager.getContent().addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Translatable<Button> changePassword = Translatable.createButton(e -> UI.getCurrent().navigate(ChangePasswordView.class), Messages.CHANGE_PASSWORD);
Translatable<Button> changePassword = Translatable.createButton(e -> UI.getCurrent().navigate(AccountView.class), Messages.ACCOUNT);
changePassword.setDefaultTextStyle();
changePassword.getContent().addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Translatable<Button> logout = Translatable.createButton(e -> logout(), Messages.LOGOUT);
@ -105,8 +111,8 @@ public class MainView extends ParentLayout {
FlexLayout menuLayout = new FlexLayout(darkTheme, userManager, changePassword, logout, about);
menuLayout.setFlexDirection(FlexDirection.COLUMN);
//menuLayout.getChildren().forEach(c -> c.getElement().getStyle().set("margin", "0"));
menuLayout.getStyle().set("background","var(--lumo-contrast-5pct");
menuLayout.getStyle().set("padding","1rem");
menuLayout.getStyle().set("background", "var(--lumo-contrast-5pct");
menuLayout.getStyle().set("padding", "1rem");
DropdownMenu menu = new DropdownMenu(menuLayout);
menu.getStyle().set("padding", "1rem");
menu.setLabel(SecurityUtils.getUsername());
@ -129,17 +135,43 @@ public class MainView extends ParentLayout {
logo.setPadding(1, Unit.REM);
FlexLayout logoWrapper = new FlexLayout(logo);
logoWrapper.expand(logo);
TextField username = new TextField();
PasswordField password = new PasswordField();
Translatable<Button> login = Translatable.createButton(event -> login(username.getValue(), password.getValue()), Messages.LOGIN);
login.setWidthFull();
FlexLayout loginForm = new FlexLayout(logoWrapper, username, password, login);
loginForm.setFlexDirection(FlexDirection.COLUMN);
loginForm.setSizeUndefined();
setContent(loginForm);
LoginI18n loginI18n = LoginI18n.createDefault();
loginI18n.getForm().setTitle("");
LoginForm loginForm = new LoginForm(loginI18n);
loginForm.setForgotPasswordButtonVisible(false);
loginForm.getElement().getStyle().set("padding", "0");
loginForm.addLoginListener(event -> {
if (!login(event.getUsername(), event.getPassword())) {
event.getSource().setError(true);
}
});
FlexLayout layout = new FlexLayout(logoWrapper, loginForm);
layout.setFlexDirection(FlexDirection.COLUMN);
layout.setSizeUndefined();
setContent(layout);
}
private void login(@NonNull String username, @NonNull String password) {
private void showFirstTimeSetup() {
Translatable<Image> logo = Translatable.createImage("frontend/logo.png", Messages.ACRARIUM);
logo.setWidthFull();
logo.setPaddingTop(0.5, Unit.REM);
logo.setPaddingBottom(1, Unit.REM);
Translatable<Label> welcomeLabel = Translatable.createLabel(Messages.WELCOME);
welcomeLabel.getStyle().set("font-size", "var(--lumo-font-size-xxl");
FlexLayout header = new FlexLayout(welcomeLabel, logo, Translatable.createLabel(Messages.CREATE_ADMIN));
header.setFlexDirection(FlexDirection.COLUMN);
header.setAlignSelf(Alignment.CENTER, welcomeLabel);
header.setWidth(0, Unit.PIXEL);
FlexLayout wrapper = new FlexLayout(header);
wrapper.expand(header);
UserEditor userEditor = new UserEditor(userService, null, () -> UI.getCurrent().getPage().reload());
FlexLayout layout = new FlexLayout(wrapper, userEditor);
layout.setFlexDirection(FlexDirection.COLUMN);
layout.setSizeUndefined();
setContent(layout);
}
private boolean login(@NonNull String username, @NonNull String password) {
try {
Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username.toLowerCase(), password));
if (!token.getAuthorities().contains(User.Role.USER)) {
@ -148,12 +180,13 @@ public class MainView extends ParentLayout {
VaadinService.reinitializeSession(VaadinService.getCurrentRequest());
SecurityContextHolder.getContext().setAuthentication(token);
UI.getCurrent().getPage().reload();
return true;
} catch (AuthenticationException ex) {
Notification.show(getTranslation(Messages.LOGIN_FAILED), 5000, Notification.Position.MIDDLE);
return false;
}
}
public void logout() {
private void logout() {
SecurityContextHolder.clearContext();
getUI().ifPresent(ui -> {
ui.getPage().reload();

View file

@ -83,7 +83,7 @@ public class Overview extends VerticalLayout implements ComponentEventListener<A
Translatable<TextField> host = Translatable.createTextField("localhost", Messages.HOST);
NumberInput port = new NumberInput(5984, 0, 65535);
Translatable<Checkbox> ssl = Translatable.createCheckbox(false, Messages.SSL);
Translatable<TextField> databaseName = Translatable.createTextField("acra-myapp", Messages.DATABASE_NAME);
Translatable.Value<TextField> databaseName = Translatable.createTextField("acra-myapp", Messages.DATABASE_NAME);
new Popup().setTitle(Messages.IMPORT_ACRALYZER)
.addComponent(host)
.addComponent(port)

View file

@ -18,16 +18,20 @@ package com.faendir.acra.ui.view.app.tabs;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.App;
import com.faendir.acra.model.MailSettings;
import com.faendir.acra.model.Permission;
import com.faendir.acra.model.ProguardMapping;
import com.faendir.acra.model.QProguardMapping;
import com.faendir.acra.model.QReport;
import com.faendir.acra.model.QVersion;
import com.faendir.acra.model.User;
import com.faendir.acra.model.Version;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.service.DataService;
import com.faendir.acra.service.UserService;
import com.faendir.acra.ui.base.ConfigurationLabel;
import com.faendir.acra.ui.base.MyGrid;
import com.faendir.acra.ui.base.popup.Popup;
import com.faendir.acra.ui.component.Card;
import com.faendir.acra.ui.component.CssGrid;
import com.faendir.acra.ui.component.DownloadButton;
import com.faendir.acra.ui.component.FlexLayout;
import com.faendir.acra.ui.component.HasSize;
@ -37,13 +41,16 @@ import com.faendir.acra.ui.component.Translatable;
import com.faendir.acra.ui.view.Overview;
import com.faendir.acra.ui.view.app.AppView;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.data.renderer.ComponentRenderer;
@ -52,6 +59,7 @@ import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.util.StreamUtils;
import java.io.ByteArrayInputStream;
@ -70,51 +78,88 @@ import static com.faendir.acra.model.QReport.report;
@SpringComponent
@Route(value = "admin", layout = AppView.class)
public class AdminTab extends AppTab<Div> {
@NonNull
private final UserService userService;
private final FlexLayout layout;
@Autowired
public AdminTab(DataService dataService) {
public AdminTab(DataService dataService, @NonNull UserService userService) {
super(dataService);
this.userService = userService;
getContent().setSizeFull();
layout = new FlexLayout();
layout.setWidthFull();
layout.setFlexWrap(FlexLayout.FlexWrap.WRAP);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
getContent().add(layout);
}
@Override
protected void init(App app) {
getContent().removeAll();
FlexLayout layout = new FlexLayout();
layout.setFlexWrap(FlexLayout.FlexWrap.WRAP);
layout.setWidthFull();
MyGrid<ProguardMapping> mappingGrid = new MyGrid<>(getDataService().getMappingProvider(app));
mappingGrid.setHeightToRows();
mappingGrid.addColumn(ProguardMapping::getVersionCode, QProguardMapping.proguardMapping.versionCode, Messages.VERSION).setFlexGrow(1);
mappingGrid.setHeight("");
Card mappingCard = new Card(mappingGrid);
mappingCard.setHeader(Translatable.createText(Messages.DE_OBFUSCATION));
mappingCard.setWidth(500, HasSize.Unit.PIXEL);
layout.removeAll();
MyGrid<Version> versionGrid = new MyGrid<>(getDataService().getVersionProvider(app));
versionGrid.setHeightToRows();
versionGrid.addColumn(Version::getCode, QVersion.version.code, Messages.VERSION_CODE).setFlexGrow(1);
versionGrid.addColumn(Version::getName, QVersion.version.name, Messages.VERSION).setFlexGrow(1);
versionGrid.setHeight("");
Card versionCard = createCard(versionGrid);
versionCard.setHeader(Translatable.createText(Messages.VERSIONS));
if (SecurityUtils.hasPermission(app, Permission.Level.EDIT)) {
mappingGrid.addColumn(new ComponentRenderer<>(mapping -> new Button(new Icon(VaadinIcon.TRASH), e -> new Popup().addComponent(Translatable.createText(Messages.DELETE_MAPPING_CONFIRM, mapping.getVersionCode())).addYesNoButtons(p -> {
getDataService().delete(mapping);
mappingGrid.getDataProvider().refreshAll();
versionGrid.addColumn(new ComponentRenderer<>(v -> new Button(new Icon(VaadinIcon.TRASH), e -> new Popup().addComponent(Translatable.createText(Messages.DELETE_MAPPING_CONFIRM, v.getCode())).addYesNoButtons(p -> {
getDataService().delete(v);
versionGrid.getDataProvider().refreshAll();
}, true).show())));
mappingGrid.appendFooterRow().getCell(mappingGrid.getColumns().get(0)).setComponent(Translatable.createButton(e -> {
NumberInput version = new NumberInput(getDataService().getMaximumMappingVersion(app).map(i -> i + 1).orElse(1));//, Messages.VERSION_CODE);
versionGrid.appendFooterRow().getCell(versionGrid.getColumns().get(0)).setComponent(Translatable.createButton(e -> {
TextField name = new TextField();
NumberInput version = new NumberInput(getDataService().getMaxVersion(app).map(i -> i + 1).orElse(1));//, Messages.VERSION_CODE);
MemoryBuffer buffer = new MemoryBuffer();
Upload upload = new Upload(buffer);
new Popup()
.setTitle(Messages.NEW_MAPPING)
new Popup().setTitle(Messages.NEW_MAPPING)
.addComponent(name)
.addComponent(version)
.addComponent(upload)
.addCreateButton(popup -> {
try {
getDataService().store(new ProguardMapping(app, version.getValue().intValue(), StreamUtils.copyToString(buffer.getInputStream(), Charset.defaultCharset())));
getDataService().store(new Version(app, version.getValue().intValue(), name.getValue(), StreamUtils.copyToString(buffer.getInputStream(), Charset.defaultCharset())));
} catch (Exception ex) {
//TODO
}
mappingGrid.getDataProvider().refreshAll();
}, true)
.show();
}, Messages.NEW_FILE));
versionGrid.getDataProvider().refreshAll();
}, true).show();
}, Messages.NEW_VERSION));
}
layout.add(mappingCard);
layout.expand(mappingCard);
CssGrid notificationLayout = new CssGrid();
notificationLayout.setTemplateColumns("auto max-content");
notificationLayout.setWidthFull();
User user = userService.getUser(SecurityUtils.getUsername());
MailSettings settings = getDataService().findMailSettings(app, user).orElse(new MailSettings(app, user));
notificationLayout.add(Translatable.createLabel(Messages.NEW_BUG_MAIL_LABEL), new Checkbox("", event -> {
settings.setNewBug(event.getValue());
getDataService().store(settings);
}));
notificationLayout.add(Translatable.createLabel(Messages.REGRESSION_MAIL_LABEL), new Checkbox("", event -> {
settings.setRegression(event.getValue());
getDataService().store(settings);
}));
notificationLayout.add(Translatable.createLabel(Messages.SPIKE_MAIL_LABEL), new Checkbox("", event -> {
settings.setSpike(event.getValue());
getDataService().store(settings);
}));
notificationLayout.add(Translatable.createLabel(Messages.WEEKLY_MAIL_LABEL), new Checkbox("", event -> {
settings.setSummary(event.getValue());
getDataService().store(settings);
}));
if(user.getMail() == null) {
Icon icon = VaadinIcon.WARNING.create();
icon.getStyle().set("height", "var(--lumo-font-size-m)");
Div div = new Div(icon, Translatable.createText(Messages.NO_MAIL_SET));
div.getStyle().set("color","var(--lumo-error-color)");
div.getStyle().set("font-style", "italic");
notificationLayout.add(div);
}
Card notificationCard = createCard(notificationLayout);
notificationCard.setHeader(Translatable.createText(Messages.NOTIFICATIONS));
Translatable<ComboBox<String>> mailBox = Translatable.createComboBox(getDataService().getFromReports(app, null, QReport.report.userEmail), Messages.BY_MAIL);
mailBox.setWidthFull();
@ -136,11 +181,8 @@ public class AdminTab extends AppTab<Div> {
return new ByteArrayInputStream(getDataService().getFromReports(app, where, report.content, report.id).stream().collect(Collectors.joining(", ", "[", "]")).getBytes(StandardCharsets.UTF_8));
}), Messages.DOWNLOAD);
download.setSizeFull();
Card exportCard = new Card(mailBox, idBox, download);
Card exportCard = createCard(mailBox, idBox, download);
exportCard.setHeader(Translatable.createText(Messages.EXPORT));
exportCard.setWidth(500, HasSize.Unit.PIXEL);
layout.add(exportCard);
layout.expand(exportCard);
Translatable<Button> configButton = Translatable.createButton(e -> new Popup().setTitle(Messages.NEW_ACRA_CONFIG_CONFIRM)
.addYesNoButtons(popup -> popup.clear().addComponent(new ConfigurationLabel(getDataService().recreateReporterUser(app))).addCloseButton().show())
@ -182,12 +224,17 @@ public class AdminTab extends AppTab<Div> {
UI.getCurrent().navigate(Overview.class);
}, true).show(), Messages.DELETE_APP);
deleteButton.setWidthFull();
Card dangerCard = new Card(configButton, matchingButton, purgeAge, purgeVersion, deleteButton);
Card dangerCard = createCard(configButton, matchingButton, purgeAge, purgeVersion, deleteButton);
dangerCard.setHeader(Translatable.createText(Messages.DANGER_ZONE));
dangerCard.setHeaderColor("var(----lumo-error-text-color)", "var(--lumo-error-color)");
dangerCard.setWidth(500, HasSize.Unit.PIXEL);
layout.add(dangerCard);
layout.expand(dangerCard);
getContent().add(layout);
}
private Card createCard(Component... content) {
Card card = new Card(content);
card.setWidth(500, HasSize.Unit.PIXEL);
card.setMaxWidth(1000, HasSize.Unit.PIXEL);
layout.add(card);
layout.expand(card);
return card;
}
}

View file

@ -21,8 +21,10 @@ import com.faendir.acra.model.App;
import com.faendir.acra.model.Permission;
import com.faendir.acra.model.QBug;
import com.faendir.acra.model.QReport;
import com.faendir.acra.model.Version;
import com.faendir.acra.model.view.VBug;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.service.BugMerger;
import com.faendir.acra.service.DataService;
import com.faendir.acra.ui.base.MyGrid;
import com.faendir.acra.ui.base.popup.Popup;
@ -32,6 +34,7 @@ import com.faendir.acra.ui.view.bug.tabs.ReportTab;
import com.faendir.acra.util.TimeSpanRenderer;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.grid.FooterRow;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.notification.Notification;
@ -56,9 +59,12 @@ import java.util.stream.Collectors;
@SpringComponent
@Route(value = "bug", layout = AppView.class)
public class BugTab extends AppTab<VerticalLayout> {
private final BugMerger bugMerger;
@Autowired
public BugTab(DataService dataService) {
public BugTab(DataService dataService, BugMerger bugMerger) {
super(dataService);
this.bugMerger = bugMerger;
}
@Override
@ -78,7 +84,7 @@ public class BugTab extends AppTab<VerticalLayout> {
titles.setItems(selectedItems.stream().map(bug -> bug.getBug().getTitle()).collect(Collectors.toList()));
titles.setValue(selectedItems.get(0).getBug().getTitle());
new Popup().setTitle(Messages.CHOOSE_BUG_GROUP_TITLE).addComponent(titles).addCreateButton(p -> {
getDataService().mergeBugs(selectedItems.stream().map(VBug::getBug).collect(Collectors.toList()), titles.getValue());
bugMerger.mergeBugs(selectedItems.stream().map(VBug::getBug).collect(Collectors.toList()), titles.getValue());
bugs.deselectAll();
bugs.getDataProvider().refreshAll();
}, true).show();
@ -91,12 +97,15 @@ public class BugTab extends AppTab<VerticalLayout> {
bugs.addColumn(VBug::getHighestVersionCode, QReport.report.stacktrace.version.code.max(), Messages.LATEST_VERSION);
bugs.addColumn(VBug::getUserCount, QReport.report.installationId.countDistinct(), Messages.AFFECTED_USERS);
bugs.addColumn(bug -> bug.getBug().getTitle(), QBug.bug.title, Messages.TITLE).setFlexGrow(1);
Grid.Column<VBug> solvedColumn = bugs.addColumn(new ComponentRenderer<>(bug -> {
Checkbox checkbox = new Checkbox(bug.getBug().isSolved());
checkbox.setEnabled(SecurityUtils.hasPermission(app, Permission.Level.EDIT));
checkbox.addValueChangeListener(e -> getDataService().setBugSolved(bug.getBug(), e.getValue()));
return checkbox;
}), QBug.bug.solved, Messages.SOLVED);
List<Version> versions = getDataService().findAllVersions(app);
Grid.Column<VBug> solvedColumn = bugs.addColumn(new ComponentRenderer<>((VBug bug) -> {
ComboBox<Version> comboBox = new ComboBox<>("", versions);
comboBox.setItemLabelGenerator(Version::getName);
comboBox.setValue(bug.getBug().getSolvedVersion());
comboBox.setEnabled(SecurityUtils.hasPermission(app, Permission.Level.EDIT));
comboBox.addValueChangeListener(e -> getDataService().setBugSolved(bug.getBug(), e.getValue()));
return comboBox;
}), QBug.bug.solvedVersion, Messages.SOLVED);
bugs.addOnClickNavigation(ReportTab.class, bug -> bug.getBug().getId());
FooterRow footerRow = bugs.appendFooterRow();
footerRow.getCell(countColumn).setComponent(merge);

View file

@ -18,7 +18,6 @@ package com.faendir.acra.ui.view.bug.tabs;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.Bug;
import com.faendir.acra.model.ProguardMapping;
import com.faendir.acra.model.Stacktrace;
import com.faendir.acra.service.DataService;
import com.faendir.acra.ui.component.Card;
@ -32,8 +31,6 @@ import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import java.util.Optional;
/**
* @author lukas
* @since 19.11.18
@ -52,10 +49,10 @@ public class StacktraceTab extends BugTab<Div> implements HasSize {
protected void init(Bug bug) {
getContent().removeAll();
for (Stacktrace stacktrace : getDataService().getStacktraces(bug)) {
Optional<ProguardMapping> mapping = getDataService().findMapping(bug.getApp(), stacktrace.getVersion().getCode());
String mapping = stacktrace.getVersion().getMappings();
String trace = stacktrace.getStacktrace();
if (mapping.isPresent()) {
trace = Utils.retrace(trace, mapping.get().getMappings());
if (mapping != null) {
trace = Utils.retrace(trace, mapping);
}
Card card = new Card(new Label(trace).honorWhitespaces());
card.setAllowCollapse(true);

View file

@ -17,7 +17,6 @@
package com.faendir.acra.ui.view.report;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.ProguardMapping;
import com.faendir.acra.model.Report;
import com.faendir.acra.service.AvatarService;
import com.faendir.acra.service.DataService;
@ -102,8 +101,8 @@ public class ReportView extends Composite<Div> implements HasSecureStringParamet
summaryLayout.add(userLabel, userLayout);
summaryLayout.add(Translatable.createLabel(Messages.EMAIL).with(Label::secondary), new Label(report.getUserEmail()));
summaryLayout.add(Translatable.createLabel(Messages.COMMENT).with(Label::secondary), new Label(report.getUserComment()));
Optional<ProguardMapping> mapping = dataService.findMapping(report.getStacktrace().getBug().getApp(), report.getStacktrace().getVersion().getCode());
Label stacktrace = new Label(mapping.map(m -> Utils.retrace(report.getStacktrace().getStacktrace(), m.getMappings())).orElse(report.getStacktrace().getStacktrace()));
Optional<String> mapping = Optional.ofNullable(report.getStacktrace().getVersion().getMappings());
Label stacktrace = new Label(mapping.map(m -> Utils.retrace(report.getStacktrace().getStacktrace(), m)).orElse(report.getStacktrace().getStacktrace()));
stacktrace.honorWhitespaces();
summaryLayout.add(Translatable.createLabel(mapping.isPresent() ? Messages.DE_OBFUSCATED_STACKTRACE : Messages.NO_MAPPING_STACKTRACE).with(Label::secondary), stacktrace);
summaryLayout.add(Translatable.createLabel(Messages.ATTACHMENTS).with(Label::secondary), new Div(dataService.findAttachments(report).stream().map(attachment -> {

View file

@ -0,0 +1,77 @@
/*
* (C) Copyright 2019 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.ui.view.user;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.User;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.service.UserService;
import com.faendir.acra.ui.base.HasRoute;
import com.faendir.acra.ui.base.Path;
import com.faendir.acra.ui.component.FlexLayout;
import com.faendir.acra.ui.component.UserEditor;
import com.faendir.acra.ui.view.MainView;
import com.faendir.acra.ui.view.Overview;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
/**
* @author lukas
* @since 26.02.19
*/
@UIScope
@SpringComponent
@Route(value = "account", layout = MainView.class)
public class AccountView extends Composite<FlexLayout> implements HasRoute {
@NonNull
private final UserService userService;
@Autowired
public AccountView(@NonNull UserService userService) {
this.userService = userService;
}
@Override
protected void onAttach(AttachEvent attachEvent) {
getContent().removeAll();
User user = userService.getUser(SecurityUtils.getUsername());
assert user != null;
UserEditor userEditor = new UserEditor(userService, user, () -> Notification.show(getTranslation(Messages.SUCCESS)));
getContent().add(userEditor);
getContent().setSizeFull();
getContent().setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
getContent().setAlignItems(FlexComponent.Alignment.CENTER);
}
@NonNull
@Override
public Path.Element<?> getPathElement() {
return new Path.Element<>(getClass(), Messages.ACCOUNT);
}
@Override
public Parent<?> getLogicalParent() {
return new Parent<>(Overview.class);
}
}

View file

@ -1,97 +0,0 @@
/*
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.faendir.acra.ui.view.user;
import com.faendir.acra.i18n.Messages;
import com.faendir.acra.model.User;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.service.UserService;
import com.faendir.acra.ui.base.HasRoute;
import com.faendir.acra.ui.base.Path;
import com.faendir.acra.ui.component.FlexLayout;
import com.faendir.acra.ui.component.Translatable;
import com.faendir.acra.ui.view.MainView;
import com.faendir.acra.ui.view.Overview;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
/**
* @author lukas
* @since 16.11.18
*/
@UIScope
@SpringComponent
@Route(value = "password", layout = MainView.class)
public class ChangePasswordView extends Composite<FlexLayout> implements HasRoute {
@Autowired
public ChangePasswordView(UserService userService) {
Translatable<PasswordField> oldPassword = Translatable.createPasswordField(Messages.OLD_PASSWORD);
getContent().add(oldPassword);
Translatable<PasswordField> newPassword = Translatable.createPasswordField(Messages.NEW_PASSWORD);
getContent().add(newPassword);
Translatable<PasswordField> repeatPassword = Translatable.createPasswordField(Messages.REPEAT_PASSWORD);
getContent().add(repeatPassword);
FlexLayout layout = new FlexLayout(oldPassword, newPassword, repeatPassword, Translatable.createButton(e -> {
User user = userService.getUser(SecurityUtils.getUsername());
assert user != null;
if (newPassword.getContent().getValue().equals(repeatPassword.getContent().getValue())) {
if (userService.changePassword(user, oldPassword.getContent().getValue(), newPassword.getContent().getValue())) {
Notification.show(getTranslation(Messages.SUCCESS));
UI.getCurrent().navigate(Overview.class);
} else {
oldPassword.getContent().setErrorMessage(getTranslation(Messages.INCORRECT_PASSWORD));
oldPassword.getContent().setInvalid(true);
oldPassword.getContent().addValueChangeListener(event -> {
oldPassword.getContent().setInvalid(false);
event.unregisterListener();
});
}
} else {
repeatPassword.getContent().setErrorMessage(getTranslation(Messages.PASSWORDS_NOT_MATCHING));
repeatPassword.getContent().setInvalid(true);
repeatPassword.getContent().addValueChangeListener(event -> {
repeatPassword.getContent().setInvalid(false);
event.unregisterListener();
});
}
}, Messages.CONFIRM));
layout.setFlexDirection(FlexLayout.FlexDirection.COLUMN);
getContent().add(layout);
getContent().setSizeFull();
getContent().setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
getContent().setAlignItems(FlexComponent.Alignment.CENTER);
}
@NonNull
@Override
public Path.Element<?> getPathElement() {
return new Path.Element<>(getClass(), Messages.CHANGE_PASSWORD);
}
@Override
public Parent<?> getLogicalParent() {
return new Parent<>(Overview.class);
}
}

View file

@ -106,8 +106,8 @@ public class UserManager extends Composite<FlexLayout> implements HasRoute {
}), Messages.ACCESS_PERMISSION, app.getName());
}
Translatable<Button> newUser = Translatable.createButton(e -> {
Translatable<TextField> name = Translatable.createTextField("", Messages.USERNAME);
Translatable<PasswordField> password = Translatable.createPasswordField(Messages.PASSWORD);
Translatable.Value<TextField> name = Translatable.createTextField("", Messages.USERNAME);
Translatable.Value<PasswordField> password = Translatable.createPasswordField(Messages.PASSWORD);
new Popup().setTitle(Messages.NEW_USER)
.addValidatedField(ValidatedField.of(name).addValidator(s -> !s.isEmpty(), Messages.USERNAME_EMPTY))
.addValidatedField(ValidatedField.of(password).addValidator(s -> !s.isEmpty(), Messages.PASSWORD_EMPTY))

View file

@ -16,22 +16,24 @@
package com.faendir.acra.util;
import com.faendir.acra.model.User;
/**
* @author lukas
* @since 15.08.18
*/
public class ImportResult {
private final PlainTextUser user;
private final User user;
private final int totalCount;
private final int successCount;
public ImportResult(PlainTextUser user, int totalCount, int successCount) {
public ImportResult(User user, int totalCount, int successCount) {
this.user = user;
this.totalCount = totalCount;
this.successCount = successCount;
}
public PlainTextUser getUser() {
public User getUser() {
return user;
}

File diff suppressed because it is too large Load diff

View file

@ -131,4 +131,28 @@ login=Login
oneArg=%s
api=API-Zugriff
about=Info
overview=Übersicht
overview=Übersicht
versions=Versionen
newVersion=Neue Version
weeklyMailSubject=Wöchentliche Zusammenfassung für %s
weeklyMailBugTemplate=- <a href='%s'>%s</a>: %d Berichte von %d Nutzern
weeklyMailNoReports=Glückwunsch, es wurden keine Bugs gemeldet!
regressionMailTemplate=<a href='%s'>%s</a> wurde in Version %s als gelöst markiert, aber wurde von %s %s mit Android %s und %s Version %s erneut gemeldet
regressionMailSubject=Regression für %s gemeldet
newBugMailSubject=Neuer Bug für %s gemeldet
newBugMailTemplate=<a href='%s'>%s</a> wurde von %s %s mit Android %s und %s Version %s gemeldet
notifications=Benachrichtigungen
newBugMailLabel=Neuer Bug
regressionMailLabel=Regression
spikeMailLabel=Große Anzahl an Berichten
weeklyMailLabel=Wöchentliche Zusammenfassung
noMailSet=Sie haben keine Emailaddresse gesetzt. Benachrichtigungen können nicht versendet werden.
account=Account
invalidMail=Ungültige Emailadresse
spikeMailSubject=Große Anzahl an Berichten für %s
spikeMailTemplate=<a href='%s'>%s</a> hat stark erhöhtes Berichtaufkommen in Version %s mit %d Berichten heute
usernameRequired=Sie müssen einen Nutzernamen wählen
usernameTaken=Dieser Nutzername ist bereits vergeben
passwordRequired=Ein Passwort muss gesetzt werden
welcome=Willkommen zu
createAdmin=Zuerst müssen Sie einen Adminstrator anlegen:

View file

@ -131,4 +131,28 @@ login=Login
oneArg=%s
api=API Access
about=About
overview=Overview
overview=Overview
versions=Versions
newVersion=New Version
weeklyMailBugTemplate=- <a href='%s'>%s</a>: %d reports by %d users
weeklyMailSubject=Weekly summary for %s
weeklyMailNoReports=Congratulations, there were no bugs reported!
newBugMailSubject=New Bug reported for %s
newBugMailTemplate=<a href='%s'>%s</a> reported from %s %s running android %s and %s version %s
regressionMailSubject=Regression reported for %s
regressionMailTemplate=<a href='%s'>%s</a> was marked solved in version %s, but was reported again from %s %s running android %s and %s version %s
spikeMailSubject=Report spike for %s
spikeMailTemplate=<a href='%s'>%s</a> is spiking in version %s with %d reports today
notifications=Notifications
newBugMailLabel=New Bug
regressionMailLabel=Regression
spikeMailLabel=Report spikes
weeklyMailLabel=Weekly Summary
noMailSet=You have not set an email address. Notifications cannot be sent.
account=Account
invalidMail=Invalid email
usernameRequired=A username is required
usernameTaken=That username is already taken
passwordRequired=A password is required
welcome=Welcome to
createAdmin=First, you have to create an administrator:

View file

@ -27,3 +27,13 @@
</style>
</custom-style>
<dom-module id="my-login-form" theme-for="vaadin-login-form-wrapper">
<template>
<style>
[part="form"] {
padding: 0 !important;
}
</style>
</template>
</dom-module>

View file

@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfigurati
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@ -36,6 +37,8 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import javax.sql.DataSource;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.List;
/**
@ -63,5 +66,9 @@ public abstract class LiquibaseTest {
@Configuration
@ComponentScan("com.faendir.acra.liquibase.change")
public static class Config {
@Bean
public Validator validator(){
return Validation.buildDefaultValidatorFactory().getValidator();
}
}
}