commit
5c00a3cfbe
39 changed files with 2051 additions and 1101 deletions
22
build.gradle
22
build.gradle
|
@ -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
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
133
src/main/java/com/faendir/acra/model/MailSettings.java
Normal file
133
src/main/java/com/faendir/acra/model/MailSettings.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
|
|
160
src/main/java/com/faendir/acra/service/MailService.java
Normal file
160
src/main/java/com/faendir/acra/service/MailService.java
Normal 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);
|
||||
}
|
||||
}
|
37
src/main/java/com/faendir/acra/service/NewReportEvent.java
Normal file
37
src/main/java/com/faendir/acra/service/NewReportEvent.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
137
src/main/java/com/faendir/acra/ui/component/UserEditor.java
Normal file
137
src/main/java/com/faendir/acra/ui/component/UserEditor.java
Normal 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() {
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
77
src/main/java/com/faendir/acra/ui/view/user/AccountView.java
Normal file
77
src/main/java/com/faendir/acra/ui/view/user/AccountView.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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
|
@ -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:
|
|
@ -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:
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue