first commit

This commit is contained in:
Булат Хайруллин 2024-06-25 13:45:54 +03:00
commit 9c03bc4475
901 changed files with 140656 additions and 0 deletions

67
.gitignore vendored Normal file
View file

@ -0,0 +1,67 @@
#ignore target dir
target*/
#gradle files
.gradle*/
*.orig
#
# Eclipse project files
#
#.classpath
#.project
#.settings*/
.springBeans
.metadata/
war*/
#
# IntelliJ IDEA project files
#
.idea*/
.classes*/
*.ipr
*.iml
*.iws
*.ids
atlassian-ide-plugin.xml
#ignore NetBeans project files
nb-configuration.xml
profiles.xml
catalog.xml
nbactions.xml
#ignore some temporary files
*.vpp~*
# os meta files
Thumbs.db
.DS_Store
pom.xml.versionsBackup
*.jasper
#studio
.studio*/
resources/src/main/generated-resources*/
resources/src/main/resources/database/database_structure.xml
frontend/build*/
frontend/tmp*/
frontend/.angular*/
frontend/build_dev*/
frontend/dist*/
frontend/node_modules*/
frontend/src/ts/**/*.js
frontend/src/ts/**/*.js.map
frontend/src/ts/**/*.ngsummary.json
frontend/src/ts/aot*/
frontend/src/ts/generated*/
npm-debug.log
#Sublime project files
*.sublime-project
*.sublime-workspace

29
.studioignore Normal file
View file

@ -0,0 +1,29 @@
#Files for Webbpm-Studio to ignore
frontend/build/
frontend/build_dev/
frontend/dist/
frontend/node_modules/
frontend/src/ts/page.routing.ts
frontend/src/ts/generated-sources/
frontend/src/ts/generated/
frontend/target/
backend/target/
backend/src/main/generated-sources/
distribution/target/
resources/target/
test/
extensions/
config/
target/
themes/
.studio/
.git/
.idea/
.studioignore
**.js

224
README.md Normal file
View file

@ -0,0 +1,224 @@
# Создание БД проекта
Создание роли для основной схемы БД проекта
```
CREATE ROLE "<your-project-main-role>" WITH
LOGIN
NOSUPERUSER
INHERIT
NOCREATEDB
NOCREATEROLE
NOREPLICATION
PASSWORD '<your password>';
```
Создание роли для схемы безопасности БД проекта
```
CREATE ROLE "<your-project-security-role>" WITH
LOGIN
NOSUPERUSER
INHERIT
NOCREATEDB
NOCREATEROLE
NOREPLICATION
PASSWORD '<your password>';
```
Создание БД проекта
```
CREATE DATABASE "<your-project-db>"
WITH
OWNER = "<your-project-main-role>";
```
ВНИМАНИЕ: в общем случае, отдельную БД для безопасности создавать не нужно. В конфигурации источника данных security-ds в файле standalone.xml в качестве имени базы данных используйте базу данных приложения.
Предоставление необходимых прав для роли &lt;your-project-security-role&gt;
```
GRANT CREATE ON DATABASE "<your-project-db>" TO "<your-project-security-role>";
```
Создание таблицы shedlock для автосинхронизации
```
CREATE TABLE shedlock
(
name varchar not null
constraint tasks_lock_pkey
primary key,
lock_until timestamp,
locked_at timestamp,
locked_by varchar
);
comment on table shedlock is 'Таблица для синхронизации выполнения запланированных задач между нодами.';
ALTER TABLE shedlock
OWNER to "owner";
```
## Дополнительные ограничения базы секьюрити
Логин пользователя &lt;user_account.username&gt; и имена ролей &lt;user_role.name&gt; не должны совпадать, так как в ходе работы jbpm-а они сохраняются в одну и ту же таблицу.
Пример ошибки при совпадении: username = 'qa_test' и role_name = 'qa_test' (роль привязана к этому пользователю). Ошибка возникает при запуске любого процесса под этим пользователем.
```
ERROR [errorhandling.ExceptionHandlerController] (default task-5) [19usm9-bgyi63]
Organizational entity already exists with [GroupImpl:'qa_test'] id,
please check that there is no group and user with same id:
java.lang.RuntimeException: Organizational entity already exists with [GroupImpl:'qa_test'] id,
please check that there is no group and user with same id
```
## Создание нового администратора
Создайте группу &lt;your-admin-group&gt; и предоставьте ей права в модуль администрирования. Для этого выполните в БД проекта
```
INSERT INTO security.user_group(
user_group_id, name, access_level_id)
(SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring),
'<your-admin-group>', access_level_id FROM security.access_level where level=999);
```
```
INSERT INTO security.link_user_group_user_role(
link_user_group_user_role_id, user_group_id, user_role_id)
(SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring),
(SELECT user_group_id FROM security.user_group WHERE name = '<your-admin-group>'),
(SELECT user_role_id FROM security.user_role WHERE name = 'Security - User Admin'));
```
```
INSERT INTO security.link_user_group_user_role(
link_user_group_user_role_id, user_group_id, user_role_id)
(SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring),
(SELECT user_group_id FROM security.user_group WHERE name = '<your-admin-group>'),
(SELECT user_role_id FROM security.user_role WHERE name = 'Security - Group Admin'));
```
```
INSERT INTO security.link_user_group_user_role(
link_user_group_user_role_id, user_group_id, user_role_id)
(SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring),
(SELECT user_group_id FROM security.user_group WHERE name = '<your-admin-group>'),
(SELECT user_role_id FROM security.user_role WHERE name = 'Security - Role Admin'));
```
# Настройка браузера для входа в систему с помощью Kerberos
1. Запустите браузер firefox.
2. В адресной строке введите about:config, нажать кнопку "я принимаю на себя риск"
3. С помощью поиска найдите параметр network.negotiate-auth.trusted-uris и в качестве значения ввести домен(например для домена example.com надо ввести .example.com)
4. Откройте в браузере приложение. Пример http://app.example.com/ . Приложение должно открыться без запроса логина/пароля
# Восстановление структуры БД
На основе БД проекта с помощью jOOQ генерируются Java классы для каждого объекта БД. Это происходит по нажатию кнопки Обновить на панели БД в студии. При необходимости можно сформировать DDL на основе данных классов. Пример класса для генерации DDL
```
package ru.cg.webbpm.test_project.db_beans;
import org.jooq.*;
import org.jooq.impl.*;
public class Main {
public static void main (String args []) {
DefaultConfiguration defaultConfiguration = new DefaultConfiguration();
defaultConfiguration.setSQLDialect(SQLDialect.POSTGRES);
Queries ddl = DSL.using(defaultConfiguration).ddl(DefaultCatalog.DEFAULT_CATALOG);
for (Query query : ddl.queries()) {
System.out.println(query);
}
}
}
```
** ВНИМАНИЕ: **
- этим способом нельзя восстановить функции/процедуры БД
см. также [https://www.jooq.org/doc/latest/manual/sql-building/ddl-statements/generating-ddl/](https://www.jooq.org/doc/latest/manual/sql-building/ddl-statements/generating-ddl/)
# Сборка проекта
## В dev режиме
```bash
mvn clean && mvn package
```
## В prod режиме
```bash
mvn clean && mvn package -Pprod -DngcCoreCount=4 -DpagePackSizeMb=24
```
ngcCoreCount - количество ядер, выделяемых процессу компиляции ngc. По умолчанию - количество ядер - 1
pagePackSizeMb - размер пачки в МБ. По умолчанию - количество ядер - 24 МБ.
## С обновлением database beans
```bash
mvn clean && mvn package -Dwebbpm.generate-db-beans
```
# Версия проекта
Если версия проекта содержит SNAPSHOT (например 1.0-SNAPSHOT), то при установке такой версии на сервере приложений будет запущена процедура остановки запущенных процессов данной версии. Этот режим удобен при отладке процесса на рабочем месте аналитика.
На боевом и тестовом стенде необходимо передавать дистрибутив проекта, с версией, которая не содержит SNAPSHOT. Например - 1.0
# Обновление платформы
## Обновления версии платформы
### С помощью студии
1. Откройте проект в студии. Версия платформы обновится автоматически
### Вручную
1. Обновите значение webbpm-platform.version в pom.xml. Пример
```xml
<webbpm-platform.version>3.164.0-SNAPSHOT</webbpm-platform.version>
```
## Обновление базового пакета компонент
### С помощью студии
1. Откройте проект в студии.
2. Откройте меню "Проект - Пакеты"
3. Нажмите обновить.
### Вручную
#### Из удаленного репозитория
```bash
mvn webbpm:update-package -DpackageVersion="3.158.8"
```
#### Из файла
```bash
mvn webbpm:update-package -DexecuteNpmInstall=false -Dpath=resources-<your-version>.jar
```
#### Руками
1. Измените версию платформы и backend модуля в файле [pom.xml](pom.xml) вашего проекта на нужную версию
2. Скопируйте ресурсы
```
из директории: webbpm-platform\components\resources\target\classes\
в директорию: {your-project}\packages\ru.cg.webbpm.packages.base.resources\
```
3. Скопируйте фронт
```
из директории: webbpm-platform\components\frontend\dist
в директорию: {your-project}\frontend\node_modules\@webbpm\base-package\
```
4. Запретите выполнение npm install при запуске студии. Для этого добавьте параметр `-DexecuteNpmInstall=false` в настройках Run/Debug Configurations студии

353
backend/pom.xml Normal file
View file

@ -0,0 +1,353 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ervu_dashboard</groupId>
<artifactId>ervu_dashboard</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>backend</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>resources</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting.reporting-jasper</groupId>
<artifactId>reporting-jasper-fonts</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.ocpsoft.prettytime</groupId>
<artifactId>prettytime</artifactId>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.santuario</groupId>
<artifactId>xmlsec</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules</groupId>
<artifactId>inject</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.bpmn</groupId>
<artifactId>bpmn-workflow-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.bpmn</groupId>
<artifactId>bpmn-workflow-jbpm-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.bpmn</groupId>
<artifactId>bpmn-workflow-jbpm</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.bpmn</groupId>
<artifactId>bpmn-beans</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.bpmn</groupId>
<artifactId>bpmn-variable-condition-adapter</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.bpmn</groupId>
<artifactId>bpmn-deploy</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules</groupId>
<artifactId>webkit-rpc</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules</groupId>
<artifactId>webkit-beans</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.core</groupId>
<artifactId>core-runtime-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.resources</groupId>
<artifactId>resources-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.core</groupId>
<artifactId>error-handling-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.database</groupId>
<artifactId>database-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.database</groupId>
<artifactId>database-impl</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.jndi</groupId>
<artifactId>jndi-beans</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.jndi</groupId>
<artifactId>jndi-inject</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.database</groupId>
<artifactId>database-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules</groupId>
<artifactId>standard-annotations</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.core</groupId>
<artifactId>metrics</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.webkit</groupId>
<artifactId>active-users-tracker</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.security</groupId>
<artifactId>security-beans</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.security</groupId>
<artifactId>security-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.security</groupId>
<artifactId>security-esia</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting</groupId>
<artifactId>reporting-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting</groupId>
<artifactId>reporting-runtime-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting</groupId>
<artifactId>reporting-runtime-impl</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting.reporting-jasper</groupId>
<artifactId>reporting-jasper-impl</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting.reporting-jasper</groupId>
<artifactId>reporting-jasper-runtime-impl</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting.reporting-xdoc</groupId>
<artifactId>reporting-xdoc-impl</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.reporting.reporting-xdoc</groupId>
<artifactId>reporting-xdoc-runtime-impl</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules</groupId>
<artifactId>webkit-base</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.security</groupId>
<artifactId>security-db-synchronization-api</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.modules.security</groupId>
<artifactId>security-db-synchronization-ldap-impl</artifactId>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>ru.micord.fias</groupId>
<artifactId>client</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.kerberos</groupId>
<artifactId>spring-security-kerberos-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.kerberos</groupId>
<artifactId>spring-security-kerberos-web</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.mnode.ical4j</groupId>
<artifactId>ical4j</artifactId>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
</dependency>
<dependency>
<groupId>ru.cg.webbpm.packages.base</groupId>
<artifactId>backend</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<useIncrementalCompilation>false</useIncrementalCompilation>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.basedir}/target/generated-sources/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>studio</id>
<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<warName>${project.artifactId}</warName>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>ru.cg.webbpm.modules.resources</groupId>
<artifactId>resources-impl-development</artifactId>
</dependency>
</dependencies>
</profile>
<profile>
<id>dev</id>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

View file

@ -0,0 +1,65 @@
import java.time.Duration;
import javax.sql.DataSource;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.ScheduledLockConfiguration;
import net.javacrumbs.shedlock.spring.ScheduledLockConfigurationBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* Root application context
* This context imports XML configs from all the other jars, and is created by {@link WebAppInitializer}
* NB: modules are excluded from component scan since spring-context.xml sometimes holds important parameters and / or annotations
* @author krylov
*/
@Configuration
@ComponentScan(basePackages = {
"service",
"dao",
"bpmn",
"i18n",
"errorhandling",
"database",
"security",
"component.addresses",
"gen",
"ru.cg",
"ru.micord"
})
@EnableAspectJAutoProxy(proxyTargetClass = true)
@EnableWebMvc
@EnableScheduling
public class AppConfig {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public ScheduledLockConfiguration taskScheduler(LockProvider lockProvider) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(12);
scheduler.initialize();
return ScheduledLockConfigurationBuilder
.withLockProvider(lockProvider)
.withTaskScheduler(scheduler)
.withDefaultLockAtMostFor(Duration.ofHours(4))
.build();
}
@Bean
public LockProvider lockProvider(@Qualifier("datasource") DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}

View file

@ -0,0 +1,31 @@
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import org.springframework.web.util.IntrospectorCleanupListener;
/**
* This initializer creates root context and registers dispatcher servlet
* Spring scans for initializers automatically
*/
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
servletContext.addListener(new IntrospectorCleanupListener());
}
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{AppConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[0];
}
}

View file

@ -0,0 +1,34 @@
package controller;
import dto.jivoprofile.JivoProfileDto;
import org.hsqldb.lib.StringUtil;
import org.springframework.web.bind.annotation.*;
import ru.cg.webbpm.modules.security.api.model.User;
import ru.cg.webbpm.modules.security.api.service.UserService;
@RestController
public class ProfileController {
private UserService userService;
public ProfileController(UserService userService) {
this.userService = userService;
}
@RequestMapping(value = "/profile/jivo/{userId}", method = RequestMethod.GET)
public JivoProfileDto profile(@PathVariable("userId") String userId) {
if (StringUtil.isEmpty(userId)) {
return new JivoProfileDto();
}
User user = userService.get(userId);
JivoProfileDto profileDto = new JivoProfileDto();
profileDto.setUsername(user.firstName());
profileDto.setEmail(user.email());
profileDto.setPhone(user.phone());
return profileDto;
}
}

View file

@ -0,0 +1,35 @@
package dto.jivoprofile;
import ru.cg.webbpm.modules.webkit.annotations.Model;
@Model
public class JivoProfileDto {
public String username;
public String email;
public String phone;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}

View file

749
config.md Normal file
View file

@ -0,0 +1,749 @@
# Описание параметров конфигурации сервера приложений
Файл /standalone/configuration/standalone.xml
## Общие
- `webbpm.security.login.case_sensitive`. Default value = true. Параметр отвечающий за чувствительность к регистру при обработке логина пользователя.
- true - Login и login, обрабатываются как разные логины.
- false - Login и login, обрабатывается как один и тот же логин.
- `webbpm.db.pool_size`. Нужен только для `webbpm.mode` = development.
- `webbpm.mode`. Режим работы приложения. Значения - development, production.
- development - в этом режиме используется соединение к БД проекта, заданное в Studio.
- production - в этом режиме используется соединение к БД проекта, заданное с помощью jndi имени.
## jBPM
- `com.arjuna.ats.arjuna.allowMultipleLastResources`
- `webbpm.jbpm.audit-log.disabled` - флаг, отвечающий за включение/отключение аудита jBPM
- `webbpm.jbpm.cleaner_cron` - cron расписание автоматической очистки БД JBPM, по умолчанию "-" т.е. выключен. Очищаются незавершенные процессы.
- `webbpm.jbpm.cleaner_timeout` - время, спустя которое процесс считается устаревшим. По умолчанию - 10 часов.
- `webbpm.jbpm.finished_process_cleaner_cron` - cron расписание автоматической очистки аудита процессов в БД JBPM, по умолчанию "-"
т.е. выключен
- `webbpm.jbpm.finished_process_cleaner_timeout` - время, спустя которое процесс считается устаревшим. По умолчанию - 10 часов.
Пример:
```xml
<property name="com.arjuna.ats.arjuna.allowMultipleLastResources" value="true"/>
<property name="webbpm.db.pool_size" value="5"/>
<property name="webbpm.mode" value="development"/>
```
### jBPM Runtime Strategy
Возможные варианты runtime strategy:
- SINGLETON
- PER_REQUEST (значение по умолчанию)
- PER_PROCESS_INSTANCE
- PER_CASE
Пример:
```xml
<property name="webbpm.jbpm.runtime_strategy" value="PER_PROCESS_INSTANCE"/>
```
## Способ аутентификации
- authentication.method - способ аутентификации. Поддерживаемые способы аутентификации: form, kerberos, cert_over_db, cert_over_ldap
### По логину и паролю
Пример конфигурации:
```xml
<property name="authentication.method" value="form"/>
```
### По сертификату
- cert_over_db - проверка наличия пользователя в базе данных безопасности
- cert_over_ldap - проверка наличия пользователя в базе данных безопасности и в LDAP
Примеры:
```xml
<property name="authentication.method" value="cert_over_db"/>
<property name="authentication.method" value="cert_over_ldap"/>
```
Параметр способа аутентификации authentication.method должен быть также установлен на клиентской части приложения в app-config.json
Также для аутентификации по сертификату нужны свойства для хранилища сертификатов:
- certificate.keystore.location - путь до java key store. Key store - это хранилище доверенных сертификатов, с помощью которых можно проверить корневой сертификат. Сертификат устанавливается с помощью команды:
```text
keytool -importcert -alias myAlias -file Example.cer -keystore exampleKeyStore
```
- certificate.keystore.password - пароль для keystore, установленный при импорте сертификата
Примеры:
```xml
<property name="certificate.keystore.location" value="${jboss.home.dir}/mfc"/>
<property name="certificate.keystore.password" value="mfc_auth"/>
```
### Kerberos
Получите от администратора Kerberos .keytab файл, из которого командой `klist -k http.keytab` можно получить список principal-ов
Проверить успешность авторизации principal-а можно командой `kinit -t -i http.keytab %principal%`.
В
случае успешной авторизации команда `klist` в качестве default principal которым проводилась авторизация.
После этого в standalone.xml поправить параметр `app.service-principal` на principal, которым успешно авторизовались. principal имеет формат: `HTTP/%hostname%@%REALM%`
Пример конфигурации:
```xml
<property name="app.service-principal" value="HTTP/oleg-rxserver.alt.dom@ALT.DOM"/>
<property name="app.keytab-location" value="${jboss.home.dir}/http.keytab"/>
<property name="authentication.method" value="kerberos"/>
```
- app.service-principal. Пример - HTTP/oleg-rxserver.alt.dom@ALT.DOM
- app.keytab-location - расположение keytab файла. Пример - ${jboss.home.dir}/
- http.keytab.
Также необходимо в настройках браузера на клиенте задать параметр `network.negotiate-auth.trusted-uris` задать значение `.%domain%`.
Пример:
```xml
.ALT.DOM
```
### Ldap
Аутентификация происходит посредством логина и пароля синхронизированного пользователя Ldap.
Логин и пароль введенные в форму входа, будут проверены сервисом Ldap.
Пример конфигурации:
```xml
<property name="ldap.url" value="ldap://localhost:389"/>
<property name="ldap.base" value="dc=example,dc=org"/>
<property name="ldap.username" value="cn=admin,dc=example,dc=org"/>
<property name="ldap.password" value="admin"/>
<property name="authentication.method" value="form"/>
```
#### Настройка сервера приложений для работы с Kerberos
1. создать учетные записи в домене:
- тестовые для проверки работоспособности функционала (пользователи домена, почтовые ящики):
`User1 pass1 user1@example.com`
`User2 pass2 user2@example.com`
- сервисную для доступа сервиса(приложения) к MS AD - (пользователь домена - снять устаревание пароля, ограничение по времени действия, почтовый ящик):
`serviceUser servicePass serviceuser@example.com`
---
2. завести учетную запись машины `appserver.machine.name` (fqdn-имя сервиса) в AD (вручную, как запись в computer) и в DNS (A-запись)
`appserver.machine.name 10.250.216.91`
3. сгенерировать keytab (утилита ktpass) для аутентификации сервисов(приложения) для единой точки входа - `serviceUser` - с именем test.file.name.keytab - привязав к ней пользователя serviceUser
например, так:
```
ktpass -princ http/appserverMachineName.example.com@example.com -mapuser example.com\serviceUser -pass "пароль_уз_serviceUser" -crypto All -ptype KRB5_NT_PRINCIPAL -out "путь_к_директории_выгрузки_файла\test.file.name.keytab"
```
##### Kerberos FAQ
- В случае, если авторизация не проходит и в логах сервера приложений присутствует следующий вывод:
```
2019-05-14 05:33:36,588 INFO [security.controller.KerberosAuthenticationController] (default task-3) Authentication request header Authorization not exists
2019-05-14 05:33:36,588 INFO [security.controller.KerberosAuthenticationController] (default task-3) Authentication object is not presented
```
необходимо проверить настройку браузера firefox `network.negotiate-auth.trusted-uris`, она должна соответствовать домену из principal-а.
Для этого в поисковую строку браузера вводим "about:config", в открывшемся окне нажимаем "accept with risk and continue", в поисковой строке открывшейся страницы ввести `network.negotiate-auth.trusted-uris`.
Пример: для principal-а `HTTP/oleg-rxserver.alt.dom@ALT.DOM` настройка в браузере должна быть `.alt.dom`, приложение в браузере должно открываться по `http:\\oleg-rxserver.alt.dom:8080\...`
- если в логах сервера приложений есть ошибка:
```xml
2019-05-13 14:13:07,095 WARN [org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter] (default task-1) Negotiate Header was invalid: Negotiate TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKAGNFAAAADw==: org.springframework.security.authentication.BadCredentialsException: Kerberos validation not successful
...
Caused by: java.security.PrivilegedActionException: GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)
at java.security.AccessController.doPrivileged(Native Method) [rt.jar:1.8.0_211]
at javax.security.auth.Subject.doAs(Subject.java:422) [rt.jar:1.8.0_211]
at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator.validateTicket(SunJaasKerberosTicketValidator.java:68) [spring-security-kerberos-core-1.0.1.RELEASE.jar:1.0.1.RELEASE]
... 66 more
Caused by: GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)
at sun.security.jgss.GSSHeader.(GSSHeader.java:97) [rt.jar:1.8.0_211]
at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:306) [rt.jar:1.8.0_211]
at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285) [rt.jar:1.8.0_211]
at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidatorKerberosValidateAction.run(SunJaasKerberosTicketValidator.java:170) [spring-security-kerberos-core-1.0.1.RELEASE.jar:1.0.1.RELEASE]
at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidatorKerberosValidateAction.run(SunJaasKerberosTicketValidator.java:153) [spring-security-kerberos-core-1.0.1.RELEASE.jar:1.0.1.RELEASE]
... 69 more
```
необходимо проверить правильность указанного в standalone.xml principal-а.
### Комбинации нескольких способов аутентификации
Приложение может обрабатывать запросы на несколько способов аутентификации. Для этого необходимо переичислить нужные профили через запятую.
Примеры:
```xml
<property name="authentication.method" value="form,cert_over_db"/>
<property name="authentication.method" value="form,cert_over_ldap"/>
<property name="authentication.method" value="kerberos,cert_over_db"/>
<property name="authentication.method" value="kerberos,cert_over_ldap"/>
```
### Время жизни токенов аутентификации
```xml
<property name="webbpm.security.access_token.duration.minutes" value="60"/>
<property name="webbpm.security.refresh_token.duration.days" value="30"/>
<property name="webbpm.security.session.active.count" value="2"/>
```
`webbpm.security.access_token.duration.minutes` - опциональный параметр (значение по умолчанию 60), время жизни в минутах, сколько будет действителен токен, после истечения этого времени токен будет обновлён `webbpm.security.refresh_token.duration.days` - опциональный параметр (значение по умолчанию 30), время жизни в днях, после истечения этого времени с последнего обновления, пользователю будет необходимо повторно войти `webbpm.security.session.active.count` - опциональный параметр (значение по умолчанию 1), количество сохраняемых в базу токенов обновления (количество активных сессий)
Примечания.
- Нельзя использовать одновременно профили cert_over_db c cert_over_ldap и kerberos с form.
## Синхронизация пользователей с LDAP
```xml
<property name="ldap.auto.sync.enabled" value="true"/>
<property name="ldap.synchronizer.cron" value="0 0 * * * *"/>
<property name="ldap.url" value="ldap://localhost:389"/>
<property name="ldap.base" value="dc=alt,dc=dom"/>
<property name="ldap.username" value="uid=test,ou=People,dc=alt,dc=dom"/>
<property name="ldap.password" value="password"/>
<property name="webbpm.ldap.implementation" value="open-ldap"/>
```
Настройки подключения к LDAP:
- `ldap.auto.sync.enabled` - включает/отключает автоматическую синхронизацию с LDAP
- `ldap.synchronizer.cron` - cron расписание автоматической синхронизации с LDAP
- `ldap.url`. Пример - ldap://localhost:389
- `ldap.base`. Пример - dc=alt,dc=dom
- `ldap.username`. Пример - uid=test,ou=People,dc=alt,dc=dom
- `ldap.password`
- `webbpm.ldap.implementation`. Допускается два значения: open-ldap и active-directory.
## WEBGUARD
Для синхронизации пользователей в WEBGUARD и для корректной работы админки необходимо указать настройки соединения к REST API WEBGUARD
```xml
<property name="webguard.url" value="http://wg-host:8081/security-manager"/>
<property name="webguard.user.login" value="wg-user"/>
<property name="webguard.user.password" value="wg-password"/>
```
## Статистика
### jbpm hibernate statistics
Статистика hibernate jbpm доступна по jmx по пути `org.hibernate:type=Stats,name=jbpm`.
Полный список параметров можно посмотреть через jconsole.
По умолчанию включена, отключить можно настройкой `webbpm.jbpm.hibernate_statistics.enabled`:
```xml
<property name="webbpm.jbpm.hibernate_statistics.enabled" value="false"/>
```
## Schedulers по очистке базы *jbpm*.
- Очистка таблиц аудита от завершенных процессов по истечении таймаута
- `webbpm.jbpm.finished_process_cleaner_cron` - задает расписание (*spring cron*), если настройка не задана - джоб не запускается
- `webbpm.jbpm.finished_process_cleaner_timeout` - таймаут в часах (*целое числовое значение*)
Пример:
```xml
<property name="webbpm.jbpm.finished_process_cleaner_cron" value="0 * * * * *"/>
<property name="webbpm.jbpm.finished_process_cleaner_timeout" value="2"/>
```
## Ограничения для запросов БД.
- `webbpm.db.query_limit_enabled` - флаг, отвечающий за вывод сообщений о превышении лимитов на количество возвращаемых записей. По умолчанию - false.
- `webbpm.db.select_records_max_limit` - максимальный лимит возвращаемых строк для запросов в БД, при превышении/равенстве данного лимита будет выброшена ошибка (*целое числовое значение*). По умолчанию - 100000
- `webbpm.db.select_records_min_limit` - минимальный лимит возвращаемых строк для запросов в БД, при превышении/равенстве данного лимита в логи будет выведен warning (*целое числовое значение*). По умолчанию - 1000
- `webbpm.db.execution_time_threshold`. The threshold for time of executing a statement. Система выводит сообщение в логи при превышении. Действует для запросов, созданных в jOOQ. По умолчанию - 1000 миллисекунд
- `webbpm.db.result_read_time_threshold`. The threshold for time of fetching a set of records from a ResultSet.
. Система выводит сообщение в логи при превышении. Действует для запросов, созданных в jOOQ. По умолчанию - 50 миллисекунд
- `webbpm.db.results_count_threshold`. Система выводит сообщение в логи при превышении. Действует для запросов, созданных в jOOQ. По умолчанию - 1000 записей
- `webbpm.db.full_time_threshold`. Ограничение на полное время выполнения запроса. Система выводит сообщение в логи при превышении. Действует для запросов, созданных в jOOQ. По умолчанию sum(webbpm.db.result_read_time_threshold, webbpm.db.execution_time_threshold) миллисекунд
- `webbpm.db.query_timeout`. Ограничение на время выполнения запроса. При превышении запрос будет отклонен. Действует для запросов, созданных в jOOQ в dev режиме. По умолчанию 120 секунд.
Пример:
```xml
<property name="webbpm.db.query_limit_enabled" value="false"/>
<property name="webbpm.db.select_records_max_limit" value="100000"/>
<property name="webbpm.db.select_records_min_limit" value="1000"/>
<property name="webbpm.db.execution_time_threshold" value="1000"/>
<property name="webbpm.db.results_count_threhold" value="50"/>
<property name="webbpm.db.result_read_time_threshold" value="50"/>
<property name="webbpm.db.full_time_threshold" value="300"/>
<property name="webbpm.db.query_timeout" value="120"/>
```
## Добавление версии приложения в URL при запросах к backend-у
При сборке приложения с профилем enable-version-in-url в URL будет добавляться версия приложения, указанная в pom.xml.
Шаблон URL:
```
<protocol>//<hostname>:<port>/backend<app-version>/<some-controller>
```
## Включение регистрации пользователя
1. Укажите конфигурацию почтового сервера для отправки писем с подтверждением регистрации.
Для этого в файле проекта *jndi-resources.xml* добавьте ресурс SmtpConfiguration.
Шаблон:
```
<jndi-resource name="java:comp/env/webbpm/testResource" type="bpmn.handler.common.SmtpConfiguration">{"host":"host","port":1234,"login":"user","password":"password","from":"email_from","senderName":"sender_name","isSecured":true}</jndi-resource>
```
Почтовый сервер - зарегистрированный актуальный почтовый адрес. В поле password нужно указывать не пароль для входа в почту, а создать пароль для приложений в учетке почты и указать его.
2. Для включения регистрации добавьте в *standalone.xml* свойство
```
<property name="registration.enabled" value="true"/>
```
3. Также в *standalone.xml* укажите ресурс для отправки писем для подтверждения регистрации (из п.1)
```
<property name="mail.jndi.resource.name" value="java:comp/env/webbpm/testResource"/>
```
4. При необходимости, отредактируйте шаблон письма для подтверждения регистрации
(resources/src/main/resources/mail/confirmation.html)
5. При необходимости, отредактируйте шаблон письма для восстановления пароля
(resources/src/main/resources/mail/reset_password.html)
#### Настройка браузера для входа в систему с помощью Kerberos
1. Запустите браузер firefox.
2. В адресной строке введите about:config, нажать кнопку "я принимаю на себя риск"
3. С помощью поиска найдите параметр network.negotiate-auth.trusted-uris и в качестве значения ввести домен(например для домена example.com надо ввести .example.com)
4. Откройте в браузере приложение. Пример [http://app.example.com/](http://app.example.com/) . Приложение должно открыться без запроса логина/пароля
## Восстановление структуры БД
На основе БД проекта с помощью jOOQ генерируются Java классы для каждого объекта БД. Это происходит по нажатию кнопки Обновить на панели БД в студии. При необходимости можно сформировать DDL на основе данных классов. Пример класса для генерации DDL
```
package ru.cg.webbpm.test_project.db_beans;
import org.jooq.*;
import org.jooq.impl.*;
public class Main {
public static void main (String args []) {
DefaultConfiguration defaultConfiguration = new DefaultConfiguration();
defaultConfiguration.setSQLDialect(SQLDialect.POSTGRES);
Queries ddl = DSL.using(defaultConfiguration).ddl(DefaultCatalog.DEFAULT_CATALOG);
for (Query query : ddl.queries()) {
System.out.println(query);
}
}
}
```
** ВНИМАНИЕ: **
- этим способом нельзя восстановить функции/процедуры БД
см. также [Generating DDL from objects](https://www.jooq.org/doc/latest/manual/sql-building/ddl-statements/generating-ddl/)
## Распределенный кэш (Hazelcast)
В платформе подключен кэш. Он используется для подсчёта количества пользователей и кэширования подсистемы безопасности
- `webbpm.cache.hazelcast.port` - входящий порт hazelcast. по дефолту 5701.
- Обязательное задать одно из двух следующих параметров
- `webbpm.cache.hazelcast.hosts` - список хостов серверов приложений.
webbpm.cache.hazelcast.hosts = hostname1,hostname2,hostname3
- `webbpm.cache.hazelcast.kubernetes.service_name` - имя сервиса в среде kubernetes,
используемый для обнаружения других подов. Подходит как стратегия обнаружения, если используется
kubernetes
- `webbpm.cache.hazelcast.outbound_port_definitions` - исходящие порты hazelcast. по дефолту не задано, система сама выбирает свободные порты. Задать диапазон 5801 - 5820
- `webbpm.cache.hazelcast.backup_count`. Нужны чтобы когда сервер выключается был доступен бекап. Если предполагается выключать несколько серверов за раз, то нужно увеличить.
Данный бекап делает копии синхронно с основной записью и операция записи ждет пока будет записана везде. Можно делать бекап асинхронно, настраивается через async_backup_count
подробнее про бекапы [Hazelcast IMDG Reference Manual](https://docs.hazelcast.org/docs/3.11/manual/html-single/index.html#backing-up-maps) . по дефолту 1
- `webbpm.cache.hazelcast.async_backup_count`
- `webbpm.cache.hazelcast.public_address`
- `webbpm.cache.hazelcast.interfaces`
Пример конфигурации:
```xml
<property name="webbpm.cache.hazelcast.hosts" value="app1,app2"/>
<property name="webbpm.cache.hazelcast.outbound_port_definitions" value="5801-5820"/>`
```
## Подключение компоненты адреса в режиме ГАР (Государственный адресный реестр)
Необходимо задать параметры:
- `gar.enable` - флаг, который включает/отключает сервис для работы с ГАР. Должен быть задан для работы компоненты в режиме ГАР. По умолчанию true, для отключения задать false.
- `gar.elastic.url.host` - хост на котором развернут elasticsearch.
- `gar.elastic.password` - пароль для аутентификации elasticsearch.
Дополнительные параметры:
- `gar.elastic.url.port` - порт на котором развернут elasticsearch.
- `gar.elastic.username` - логин для аутентификации elasticsearch.
Пример конфигурации:
```xml
<property name="gar.enable" value="true"/>
<property name="gar.elastic.url.host" value="localhost"/>
<property name="gar.elastic.password" value="password"/>
```
## Метрики
Отчет
Отчет собирается раз в 30 секунд по дефолту, меняется параметром `webbpm.metrics.report_period_ms`.
Все метрики идут за отчетный период, после сбора отчета они сбрасываются.
Получить json со всеми метриками можно по урлу `backend/metrics/v1/all` - метрики будут с последнего собранного отчета, запрос не триггерит сбор отчета - это независимые операции. Отчет содержит только метрики по которым был совершен хотя бы один вызов в отчетный период. То есть, если какая-то операция не была совершена в отчетный период, то соответствующая ей метрика не попадет в отчет - ее не будет в json.
### Значения метрик
Все метрики идут за отчетный период, после сбора отчета они сбрасываются.
Нас в основном интересуют `callsCountSum`, `latencyMin`, `latencyAvg`, `latencyMax`
- `callsCountSum` - количество завершенных вызовов
- `latencyMin` - минимальное время выполнения
- `latencyAvg` - среднее время выполнения
- `latencyMax` - максимальное время выполнения
- `activeCallsCountMax` - количество начатых, но еще не завершенных вызовов.
- `activeCallsLatencyMax` - длительность самого долгого еще не завершенного вызова
### Текущие метрики приложения
- Получение коннекта из пула
- `webbpm.jbpm.db.connection.acquire`
- `webbpm.security.db.connection.acquire`
- `webbpm.db.connection.acquire`
- Время с момента получения коннекта до возврата его в пул
- `webbpm.jbpm.db.connection.in_use`
- `webbpm.security.db.connection.in_use`
- `webbpm.db.connection.in_use`
- Время выполнения запроса на бд проекта
- `webbpm.db.query.success.execution_time`
- Время выполнения запроса на бд проекта + время получения коннекта из пула
- `webbpm.db.query.success.full_time`
- active-users-count.indicatorMax
- active-users-count-ttl.indicatorMax
# Количество пользователей
- `webbpm.active_users_counter.enabled` - включает подсчет пользователей, нужно чтобы не запускать hazelcast на дев машинах. по дефолту false. На боевых серверах необходимо установить в true.
- `webbpm.active_users_counter.max_time_between_operations_in_seconds` - время, которое пользователь считается активным после действия. по дефолту 15 минут.
- `webbpm.active_users_counter.hazelcast.app_pool_size`. Запись в hazelcast производится асинхронно в отдельном пуле, не блокируя обработку http запроса. Это размер этого пула. по дефолту 4. Можно пока оставить 4 и последить за метриками pool.hazelcast-executor.queue.indicatorMax и pool.hazelcast-executor.activeThreads.indicatorMax. Если очередь будет сильно копиться, то увеличить.
## Настройка логов
Все настройки делаются в файле `standalone.xml`, если не указано иначе.
### Общие настройки
Платформа Web-bpm использует корневую категорию логирования `ru.cg.webbpm`, рекомендуется выставлять ее в уровень `info`. todo check prod config
```xml
<logger category="ru.cg.webbpm">
<level name="INFO"/>
</logger>
```
При этом компоненты используемые в проекте могут использовать другие категории.
### Параметры конфигурации
**Рекомендованное использование:** всегда в `info`.
Все параметры конфигурации загружаемые платформой web-bpm и пользовательским приложением через api webbpm логируются категорией `ru.cg.webbpm.modules.core.app_info.api.property.BaseProperty`. Она всегда должна быть выставлена в `info`.
Пример вывода:
```
2017-12-04 16:02:19,074 INFO [ru.cg.webbpm.modules.core.app_info.api.property.BaseProperty] (EclipseGeminiBlueprintExtenderThread-1) System property [webbpm.active_users_counter.enabled] not set. Using default value [false]
2017-12-04 16:02:19,074 INFO [ru.cg.webbpm.modules.core.app_info.api.property.BaseProperty] (EclipseGeminiBlueprintExtenderThread-1) System property [webbpm.active_users_counter.hazelcast.hosts] set to [127.0.0.1]
```
### БД проекта
#### Логирование запросов в бд security и бд проекта
**Рекомендованное использование:** только при разработке.
Использовать только при разработке. Категория `org.jooq.tools.LoggerListener` в `debug` уровень.
```xml
<logger category="org.jooq.tools.LoggerListener">
<level name="DEBUG"/>
</logger>
```
Пример вывода:
```
2017-12-04 18:21:10,391 DEBUG [org.jooq.tools.LoggerListener] (default task-19) Executing query : select "department"."department_name", "department"."department_id", "department"."parent_department_id", "department"."parent_department_id", "department"."department_id", (select (count(*) <> ?) from "public"."department" as "$$child" where "$$child"."parent_department_id" = "department"."department_id") as "$$hasChildren" from "public"."department" as "department" limit ?
2017-12-04 18:21:10,395 DEBUG [org.jooq.tools.LoggerListener] (default task-19) -> with bind values : select "department"."department_name", "department"."department_id", "department"."parent_department_id", "department"."parent_department_id", "department"."department_id", (select (count(*) <> 0) from "public"."department" as "$$child" where "$$child"."parent_department_id" = "department"."department_id") as "$$hasChildren" from "public"."department" as "department" limit 2147483647
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) Fetched result : +-----------------+-------------+--------------------+--------------------+-------------+-------------+
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |department_name |department_id|parent_department_id|parent_department_id|department_id|$$hasChildren|
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : +-----------------+-------------+--------------------+--------------------+-------------+-------------+
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |Головной офис 1 | 8| {null}| {null}| 8|true |
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |Головной офис 2 | 9| {null}| {null}| 9|true |
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |Головной офис 3 | 10| {null}| {null}| 10|true |
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |Подразделение 1.1| 11| 8| 8| 11|true |
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |Подразделение 1.2| 12| 8| 8| 12|true |
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : +-----------------+-------------+--------------------+--------------------+-------------+-------------+
2017-12-04 18:21:10,568 DEBUG [org.jooq.tools.LoggerListener] (default task-19) : |...55 record(s) truncated...
```
#### Логирование больших запросов
**Рекомендованное использование:** всегда в `info`, с подобранными для проекта значениями. В проде значения должны быть проверены что они не вызывают излишнее логирование.
1. Для отслеживания больших запросов в пользовательскую базу нужно задать параметры что считать большими запросами, логироваться будут запросы больше заданных показателей.
Чтобы логировать все запросы можно задать значение `-1`. Добавляются как property в раздел `system-properties`:
```xml
<property name="webbpm.db.full_time_threshold" value="1050"/>
<property name="webbpm.db.results_count_threshold" value="1000"/>
```
В `info` режиме работают:
- `webbpm.db.full_time_threshold` - полное время выполнения запроса в миллисекундах. Включает построение запроса библиотекой, выполнение и загрузку результатов. Пример сообщения <br>`2023-01-11 13:09:38,361 WARN [ru.cg.webbpm.modules.database.impl.analytics.PerformanceListener] (default task-33) Query full time threshold exceeded. full_time=[6565ms] execution_time=[6565ms] read_time=[0ms] results_count=[4] query=[<your query>]`
- `webbpm.db.results_count_threshold` - количество записей, которое вернул запрос. Пример сообщения <br>`2023-01-11 13:10:34,088 WARN [ru.cg.webbpm.modules.database.impl.analytics.PerformanceListener] (default task-41) Query results count threshold exceeded. results_count=[11177] query=[<your query>]`
В `debug` режиме дополнительно к работающим в info:
- `webbpm.db.execution_time_threshold` - время выполнения запроса + построения запроса библиотекой.
- `webbpm.db.result_read_time_threshold` - время чтения результатов запроса
2. После задания настроек нужно настроить логирование - категория `ru.cg.webbpm.modules.database.impl.analytics.PerformanceListener`, уровень `info` или `debug`.
```xml
<logger category="ru.cg.webbpm.modules.database.impl.analytics.PerformanceListener">
<level name="INFO"/>
</logger>
```
### JBPM
#### Основные настройки
JBPM использует 3 корневых категории логирования `org.jbpm`, `org.drools`, `org.kie`. Все они должны быть выставлены в `warn`.
```xml
<logger category="org.jbpm">
<level name="WARN"/>
</logger>
<logger category="org.drools">
<level name="WARN"/>
</logger>
<logger category="org.kie">
<level name="WARN"/>
</logger>
```
#### Логирование запросов в БД
**Рекомендованное использование:** только при разработке.
Для логирования sql запросов нужно включить категорию `org.hibernate.SQL` в `debug`:
```xml
<logger category="org.hibernate.SQL">
<level name="DEBUG"/>
</logger>
```
Чтобы вывести параметры запросов и результат выполнения категорию `org.hibernate.type.descriptor.sql` в `trace`:
```xml
<logger category="org.hibernate.type.descriptor.sql">
<level name="TRACE"/>
</logger>
```
Пример вывода:
```
18:21:06,938 DEBUG [org.hibernate.SQL] (default task-47) select names0_.Task_Names_Id as Task_Nam7_16_0_, names0_.id as id1_16_0_, names0_.id as id1_16_1_, names0_.language as language2_16_1_, names0_.shortText as shortTex3_16_1_, names0_.text as text4_16_1_ from I18NText names0_ where names0_.Task_Names_Id=?
18:21:06,939 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] (default task-47) binding parameter [1] as [BIGINT] - [180]
18:21:06,940 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (default task-47) extracted value ([id1_16_1_] : [BIGINT]) - [539]
18:21:06,940 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (default task-47) extracted value ([language2_16_1_] : [VARCHAR]) - [en-UK]
18:21:06,940 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (default task-47) extracted value ([shortTex3_16_1_] : [VARCHAR]) - [Список записей]
18:21:06,941 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (default task-47) extracted value ([text4_16_1_] : [CLOB]) - [Список записей]
18:21:06,941 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (default task-47) extracted value ([Task_Nam7_16_0_] : [BIGINT]) - [180]
18:21:06,942 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (default task-47) extracted value ([id1_16_0_] : [BIGINT]) - [539]
```
#### Дополнительные логи hibernate
**Рекомендованное использование:** только при разработке в случае необходимости.
1. Время выполнения запроса и количество результатов.
Включаются категорией `org.hibernate.stat` в `debug`. При этом в hibernate должен быть включен сбор статистики.
Похоже что логируется только hql select запросов.
```xml
<logger category="org.hibernate.stat">
<level name="DEBUG"/>
</logger>
```
Пример вывода:
```
18:21:06,858 DEBUG [org.hibernate.stat.internal.ConcurrentStatisticsImpl] (default task-41) HHH000117: HQL: select t from AuditTaskImpl t where t.taskId = :taskId, time: 6ms, rows: 1
```
2. Показатели hibernate сессий
Включаются категорией `org.hibernate.engine.internal.StatisticalLoggingSessionEventListener` в `info`.
При этом в hibernate должен быть включен сбор статистики. Тут может быть интересно количество запросов, флашей и общее время на все запросы сессией.
Пример вывода:
```
2017-12-04 17:25:58,493 INFO [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] (default task-21) Session Metrics {
63408365 nanoseconds spent acquiring 17 JDBC connections;
521509 nanoseconds spent releasing 17 JDBC connections;
65732621 nanoseconds spent preparing 17 JDBC statements;
31471897 nanoseconds spent executing 17 JDBC statements;
0 nanoseconds spent executing 0 JDBC batches;
0 nanoseconds spent performing 0 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
0 nanoseconds spent performing 0 L2C misses;
297556 nanoseconds spent executing 1 flushes (flushing a total of 16 entities and 0 collections);
430168 nanoseconds spent executing 1 partial-flushes (flushing a total of 16 entities and 16 collections)
}
```
#### Логирование hbm2ddl
**Рекомендованное использование:** всегда в `trace`.
Должно быть включено всегда. Позволяет убедиться что hibernate не накатывал никакие миграции на базу. Этот функционал отключен у нас в коде.
```xml
<logger category="org.hibernate.tool.hbm2ddl">
<level name="TRACE"/>
</logger>
```
# Описание параметров конфигурации клиентской части
Свойства задаются в файле frontend/src/resources/app-config.json или frontend.war/src/resources/app-config.json
## Общие
- `dev_mode` - настройка задающая dev_mode для просмотра логов (true/false). При отсутствие оставляет значение при сборке
- `guard.confirm_exit` - выводить или нет диалог подтверждения, если обнаружены несохраненные данные в форме. Значение по умолчанию - false.
- `password.pattern` - Регулярное выражение для валидации пароля.
- `password_pattern_error` - Сообщение об ошибке валидации.
- `show.client.errors` - отвечает за отображение ошибок javascript-a пользователю (должна использоваться только в тестовых контурах) по умолчанию выключена
- 'available_task.single_fetch' - Отвечает за количество запросов available_task при завершении процесса. true - одиночный запрос, false/не указано - 10 запросов(старая реализация).
## Вывод сообщений
- `message_service_error_timeout` время в мс, в течение которого будет отображено сообщение об ошибке. Значение по умолчанию - таймаут не задан (окно не закрывается).
- `message_service_warning_timeout` время в мс, в течение которого будет отображено предупреждающее сообщение. Значение по умолчанию - таймаут не задан (окно не закрывается).
- `message_service_success_timeout` время в мс, в течение которого будет отображено сообщение об успехе. Значение по умолчанию - таймаут не задан (окно не закрывается).
- `message_service_info_timeout` время в мс, в течение которого будет отображено информационное сообщение. Значение по умолчанию - таймаут не задан (окно не закрывается).
## Электронная подпись
### Esmart
- `electronic_sign.esmart_extension_url` - url для создания расширенной подписи. Подробная информация по ссылке [http://demo.esmart.ru](http://demo.esmart.ru)
- `electronic_sign.tsp_address` - адрес сервера службы штампов времени
Пример:
```text
"electronic_sign.esmart_extension_url": "http://dsig.ibsdemo.ru/ibs_dsig/ibs_dSig.asmx"
```
## Способ аутентификации
- `auth_method` - способ аутентификации. Может принимать одно значение из списка: form, kerberos, cert_over_db, cert_over_ldap
## Таймер очистки закешированных значений фильтров
- `filter_cleanup_interval_hours` - время жизни закешированного значения фильтра в часах. По умолчанию - 720 часов,
- `filter_cleanup_check_period_minutes` - период проверки наличия просроченных закешированных значений в минутах. По умолчанию - 30 минут
## Добавление версии приложения в URL при запросах к frontend-у
В модуле frontend в src/resources/app-config.json добавлены 2 переменные
- `"enable.version.in.url": "%enable.version.in.url%"` - подставлять ли версию в URL приложения. По умолчанию false. Если сборка произведена
- `"app.version": "%project.version%"` - версия приложения.
с профилем `enable-version-in-url`, то значение будет true.
## Добавление Jivo чат в проект
Свойства задаются в файле frontend/src/resources/app-config.json или frontend.war/src/resources/app-config.json
- `jivo_chat_widget_api_url` - API url для работы Jivo чата. Необходимо заменить {WIDGET_ID} на реальный Widget API ID
- `jivo_chat_widget_enabled` - параметр отвечающий за активацию Jivo чата. По дефолту false, для активации задать true.
Пример:
```json
"jivo_chat_widget_api_url": "https://code.jivo.ru/widget/{WIDGET_ID}",
"jivo_chat_widget_enabled": false
```
# Прочее
## Смена удалённого репозитория
1. Смените адрес NPM registry в файле frontend.npmrc. Пример - registry=https://repo.example.com/repository/npm-all/
2. Поменяйте ссылки в блоке , файла pom.xml

2
config/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/*.ear
/*.jar

41
config/Dockerfile Normal file
View file

@ -0,0 +1,41 @@
FROM quay.io/wildfly/wildfly:26.1.3.Final-jdk17
USER root
ARG POSTGRES_DRIVER_VERSION=42.7.3
RUN yum -y install sudo && chown -R jboss: /opt/jboss/
USER jboss
WORKDIR $JBOSS_HOME
ENV JAVA_ARGS=-Xmx3g
RUN echo 'JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=*:8787,server=y,suspend=n"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=1g"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:+ExplicitGCInvokesConcurrent"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS -XX:+UnlockDiagnosticVMOptions -XX:G1SummarizeRSetStatsPeriod=1"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS -XX:-OmitStackTraceInFastThrow"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS -Dorg.jboss.logmanager.nocolor=true"' >> bin/standalone.conf && \
echo 'JAVA_OPTS="$JAVA_OPTS $JAVA_ARGS"' >> bin/standalone.conf
### Locale support ru_RU ###
USER root
RUN localedef -i ru_RU -f UTF-8 ru_RU.UTF-8
RUN echo "LANG=\"ru_RU.UTF-8\"" > /etc/locale.conf
USER jboss
ENV LANG ru_RU.UTF-8
ENV LANGUAGE ru_RU.UTF-8
ENV LC_ALL ru_RU.UTF-8
### Locale Support END ###
COPY --chown=jboss entrypoint.sh .
COPY --chown=jboss patches patches
RUN (cd patches && wget https://repo.micord.ru/repository/libs-releases-local/org/jboss/ironjacamar/ironjacamar-core-impl/1.5.3.Final/ironjacamar-core-impl-1.5.3.Final.jar)
RUN (cd patches/system && wget https://repo1.maven.org/maven2/org/postgresql/postgresql/$POSTGRES_DRIVER_VERSION/postgresql-$POSTGRES_DRIVER_VERSION.jar -O postgresql-driver.jar)
RUN chmod -R +x patches && \
chmod +x entrypoint.sh && \
./entrypoint.sh && \
rm -rf patches
ENV SERVER_START=true
COPY --chown=jboss *.ear $JBOSS_HOME/standalone/deployments/
HEALTHCHECK --timeout=3s --start-period=3600s CMD curl --fail 127.0.0.1:8080/backend/version || exit 1

10
config/Dockerfile.build Normal file
View file

@ -0,0 +1,10 @@
FROM maven:3-jdk-11-slim
RUN apt update \
&& apt upgrade -y \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt install -y git nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY ../ .
RUN mvn clean -T4C && mvn package -T4C

5
config/Dockerfile.secdb Normal file
View file

@ -0,0 +1,5 @@
FROM postgres:11-alpine
COPY secdb.sql .
CMD psql "postgresql://${DB_SEC_USERNAME:-security}:${DB_SEC_PASSWORD:-secpassword}@${DB_SEC_HOST:-db}/${DB_SEC_NAME:-security}" < secdb.sql

1
config/JDK_version Normal file
View file

@ -0,0 +1 @@
17

31
config/browsers.json Normal file
View file

@ -0,0 +1,31 @@
{
"firefox": {
"default": "112.0",
"versions": {
"112.0": {
"image": "selenoid/firefox:112.0",
"port": "4444",
"path": "/wd/hub",
"tmpfs": {
"/tmp": "size=512m"
},
"env" : ["LANG=ru_RU.UTF-8", "LANGUAGE=ru:en", "LC_ALL=ru_RU.UTF-8"],
"shmSize": 1073741824
}
}
},
"chrome": {
"default": "121.0",
"versions": {
"121.0": {
"image": "selenoid/chrome:121.0",
"port": "4444",
"tmpfs": {
"/tmp": "size=512m"
},
"env" : ["LANG=ru_RU.UTF-8", "LANGUAGE=ru:en", "LC_ALL=ru_RU.UTF-8"],
"shmSize": 1073741824
}
}
}
}

View file

@ -0,0 +1,12 @@
#!/bin/bash
set -e
IFS=','
for databases in $WILDFLY_DATABASES
do
IFS=':' read name user password <<< $databases
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER $user WITH PASSWORD '$password';
CREATE DATABASE $name WITH OWNER $user;
EOSQL
done

View file

@ -0,0 +1,12 @@
version: "3"
services:
secdb:
build:
context: .
dockerfile: Dockerfile.secdb
depends_on:
- db
env_file:
- testing.env
labels:
- "tmp=true"

View file

@ -0,0 +1,28 @@
version: "3"
services:
selenoid:
network_mode: bridge
image: aerokube/selenoid:latest-release
container_name: "selenoid"
environment:
- OVERRIDE_VIDEO_OUTPUT_DIR=$PWD/video
volumes:
- ".:/etc/selenoid/:ro"
- "/var/run/docker.sock:/var/run/docker.sock"
- "./logs:/opt/selenoid/logs/"
- "./video/:/opt/selenoid/video/"
command: -session-attempt-timeout 1m -retry-count 3 -limit ${LIMIT:-4} -save-all-logs -log-output-dir /opt/selenoid/logs -video-output-dir /opt/selenoid/video
ports:
- "4444:4444"
selenoid-ui:
image: aerokube/selenoid-ui
container_name: "selenoid-ui"
network_mode: bridge
depends_on:
- selenoid
links:
- "selenoid:selenoid"
ports:
- "8080:8080"
command: --selenoid-uri http://selenoid:4444`

View file

@ -0,0 +1,41 @@
version: "3"
services:
db:
ports:
- 5432:5432
environment:
- WILDFLY_DATABASES=app:app_user:apppassword,security:security_user:secpassword,jbpm:jbpm:jbpmpassword
webbpm-app:
env_file:
- testing.env
selenoid:
network_mode: bridge
image: aerokube/selenoid:latest-release
container_name: "selenoid"
environment:
- OVERRIDE_VIDEO_OUTPUT_DIR=$PWD/video
volumes:
- "$PWD:/etc/selenoid/:ro"
- "/var/run/docker.sock:/var/run/docker.sock"
- "$PWD/logs:/opt/selenoid/logs/"
- "$PWD/video/:/opt/selenoid/video/"
command: -session-attempt-timeout 1m -retry-count 3 -limit ${LIMIT:-4} -save-all-logs -log-output-dir /opt/selenoid/logs -video-output-dir /opt/selenoid/video
ports:
- "4444:4444"
depends_on:
- webbpm-app
selenoid-ui:
image: aerokube/selenoid-ui
container_name: "selenoid-ui"
network_mode: bridge
depends_on:
- selenoid
links:
- "selenoid:selenoid"
ports:
- "8080:8080"
command: --selenoid-uri http://selenoid:4444

View file

@ -0,0 +1,31 @@
version: "3"
services:
db:
image: postgres:15-bullseye
volumes:
- ./create-databases.sh:/docker-entrypoint-initdb.d/create-databases.sh
command:
- "--max_prepared_transactions=100"
ports:
- 5432
environment:
- WILDFLY_DATABASES=security:security_user:secpassword,jbpm:jbpm:jbpmpassword
- POSTGRES_PASSWORD=supersecretpassword
labels:
- "tmp=true"
webbpm-app:
build:
context: .
dockerfile: Dockerfile
depends_on:
- db
ports:
- 9990
- 8080
- 8787
- 12345
env_file:
- testing.env
labels:
- "tmp=true"

48
config/entrypoint.sh Normal file
View file

@ -0,0 +1,48 @@
#! /bin/bash
set -e
function wait_for_server() {
until `$JBOSS_HOME/bin/jboss-cli.sh -c ":read-attribute(name=server-state)" 2> /dev/null | grep -q running`; do
echo "Retry ..."
done
}
echo "dump environment variables to env.properties file"
printenv > env.properties
echo "starting JBoss"
nohup $JBOSS_HOME/bin/standalone.sh --admin-only 1>&2 2>/dev/null &
# running system patches
wait_for_server
$JBOSS_HOME/bin/jboss-cli.sh --connect --file="./patches/system/init.cli" --properties=env.properties
$JBOSS_HOME/bin/jboss-cli.sh --connect --file="./patches/system/add-postgresql-driver.cli" --properties=env.properties
bash "./patches/system/add-demo-user.sh"
# running project patches
find ./patches/ -type f -name '*.cli' -not -path './patches/system/*' -print0 |
while IFS= read -r -d '' f; do
wait_for_server
echo "running $f"
$JBOSS_HOME/bin/jboss-cli.sh --connect --file="$f" --properties=env.properties
done;
find ./patches/ -type f -name '*.sh' -not -path './patches/system/*' -print0 |
while IFS= read -r -d '' f; do
wait_for_server
echo "running $f"
bash "$f"
done
echo "stopping JBoss"
wait_for_server
$JBOSS_HOME/bin/jboss-cli.sh --connect --command=:shutdown
if ! [[ -z $SERVER_START ]]; then
echo "starting JBoss in standalone"
sleep 10 # without this occurs error "address already in use"
/opt/jboss/wildfly/bin/standalone.sh -c standalone.xml -b 0.0.0.0 -bmanagement 0.0.0.0
else
echo "cleaning up JBoss logs"
rm -rf $JBOSS_HOME/standalone/log
fi

View file

@ -0,0 +1,3 @@
/subsystem=logging/logger=org.jooq.tools:add()
/subsystem=logging/logger=org.jooq.tools:write-attribute(name=level, value=DEBUG)
/subsystem=logging/logger=org.jooq.tools:add-handler(name=CONSOLE)

View file

@ -0,0 +1,64 @@
xa-data-source add \
--name=AppDS \
--enabled=true \
--driver-name=postgresql \
--jndi-name=java:/webbpm/AppDS \
--user-name=${env.DB_APP_USERNAME:app_user} \
--password=${env.DB_APP_PASSWORD:apppassword} \
--use-ccm=true \
--valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker \
--validate-on-match=false \
--background-validation=true \
--background-validation-millis=5000 \
--exception-sorter-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter \
--statistics-enabled=true \
--max-pool-size=50 \
--query-timeout=300 \
--xa-datasource-properties=ServerName=${env.DB_APP_HOST:db},PortNumber=${env.DB_APP_PORT:5432},DatabaseName=${env.DB_APP_NAME:app}
xa-data-source add \
--name=JBPMDS \
--enabled=true \
--driver-name=postgresql \
--jndi-name=java:jboss/datasources/jbpmDS \
--user-name=${env.DB_JBPM_USERNAME:jbpm} \
--password=${env.DB_JBPM_PASSWORD:jbpmpassword} \
--use-ccm=true \
--valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker \
--validate-on-match=false \
--background-validation=true \
--background-validation-millis=5000 \
--exception-sorter-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter \
--statistics-enabled=true \
--max-pool-size=50 \
--query-timeout=300 \
--xa-datasource-properties=ServerName=${env.DB_JBPM_HOST:db},PortNumber=${env.DB_JBPM_PORT:5432},DatabaseName=${env.DB_JBPM_NAME:jbpm}
xa-data-source add \
--name=SECURITYDS \
--enabled=true \
--driver-name=postgresql \
--jndi-name=java:/webbpm/security-ds \
--user-name=${env.DB_SEC_USERNAME:security_user} \
--password=${env.DB_SEC_PASSWORD:secpassword} \
--max-pool-size=70 \
--valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker \
--validate-on-match=false \
--background-validation=true \
--background-validation-millis=5000 \
--exception-sorter-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter \
--statistics-enabled=true \
--query-timeout=300 \
--xa-datasource-properties=ServerName=${env.DB_SEC_HOST:db},PortNumber=${env.DB_SEC_PORT:5432},DatabaseName=${env.DB_SEC_NAME:app}
/system-property=ldap.mapping.login.param:add(value=${env.WEBBPM_LDAP_LOGIN_ATTR:uid})
/system-property=ldap.mapping.org.code.param:add(value=${env.WEBBPM_LDAP_ORGANIZATION_ATTR:ou})
/system-property=jboss.as.management.blocking.timeout:add(value=900)
/subsystem=undertow/server=default-server/http-listener=default/:write-attribute(name=record-request-start-time,value=true)
/subsystem=undertow/server=default-server/host=default-host/setting=access-log:add(pattern=%h %t "%r" %s %b %D)
/system-property=webbpm.cache.hazelcast.hosts:add(value="127.0.0.1")
/system-property=webbpm.cache.hazelcast.outbound_port_definitions:add(value="5801-5820")
/system-property=webbpm.security.session.active.count:add(value="20")
/system-property=gar.enable:add(value=false)
/system-property=security.password.regex:add(value="^((?=(.*\\d){1,})(?=.*[a-zа-яё])(?=.*[A-ZА-ЯЁ]).{8,})$")
/system-property=fias.enable:add(value=false)

View file

@ -0,0 +1 @@
$JBOSS_HOME/bin/add-user.sh demo@example.com demo

View file

@ -0,0 +1,5 @@
/subsystem=datasources/jdbc-driver=postgresql:add( \
driver-name="postgresql", \
driver-module-name="org.postgresql", \
driver-xa-datasource-class-name="org.postgresql.xa.PGXADataSource" \
)

View file

@ -0,0 +1,14 @@
/system-property=webbpm.mode:add(value=production)
/system-property=authentication.method:add(value=form)
/subsystem=undertow/configuration=filter/gzip=gzipFilter:add()
/subsystem=undertow/server=default-server/host=default-host/\
filter-ref=gzipFilter:add(predicate="exists('%{o,Content-Type}') and regex(pattern='(?:application/javascript|text/css|text/html|text/xml|application/json)(;.*)?', value=%{o,Content-Type}, full-match=true)")
/subsystem=undertow/configuration=filter/response-header=vary-header:add(header-name="Vary", header-value="Accept-Encoding")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=vary-header:add()
/subsystem=undertow/server=default-server/http-listener=default/:write-attribute(name=max-post-size,value=${env.MAX_POST_SIZE:104857600})
data-source remove --name=ExampleDS
/subsystem=ee/service=default-bindings:remove
/system-property=jboss.bind.address.management:add(value=0.0.0.0)
/system-property=jboss.bind.address:add(value=0.0.0.0)
module add --name=org.postgresql --resources=./patches/system/postgresql-driver.jar --dependencies=javax.api,javax.transaction.api
shutdown --restart

25
config/secdb.sql Normal file
View file

@ -0,0 +1,25 @@
INSERT INTO security.user_group(user_group_id, name, created, updated, access_level_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), 'Example', '2019-07-04 11:12:01.263+03', '2019-08-26 16:40:11.953+03', (SELECT access_level_id FROM security.access_level where level = 0)) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_group_user_role(link_user_group_user_role_id, user_group_id, user_role_id) SELECT uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_group_id FROM security.user_group where name = 'Example'), user_role_id FROM security.user_role WHERE name = 'Example' ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_group_user_role(link_user_group_user_role_id, user_group_id, user_role_id) SELECT uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_group_id FROM security.user_group where name = 'Demo'), user_role_id FROM security.user_role WHERE name = 'Example' ON CONFLICT DO NOTHING;
INSERT INTO security.user_group(user_group_id, name, created, updated, access_level_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), 'Example', '2019-07-04 11:12:01.263+03', '2019-08-26 16:40:11.953+03', (SELECT access_level_id FROM security.access_level where level = 0)) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_group_user_role(link_user_group_user_role_id, user_group_id, user_role_id) SELECT uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_group_id FROM security.user_group where name = 'Example'), user_role_id FROM security.user_role WHERE name = 'Example' ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_group_user_role(link_user_group_user_role_id, user_group_id, user_role_id) SELECT uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_group_id FROM security.user_group where name = 'Demo'), user_role_id FROM security.user_role WHERE name = 'Example' ON CONFLICT DO NOTHING;
INSERT INTO security.user_account(user_account_id, email, first_name, last_name, middle_name, created, updated, locked, org_unit_id, username) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), 'qa_test@micord.ru', 'Пользователь', 'Основной-Тестовый', NULL, '2020-01-20 08:41:25.526+03', '2020-01-20 09:57:07.25+03', false, (SELECT id from security.org_unit where code = 'DEFAULT'), 'qa_test') ON CONFLICT DO NOTHING;
INSERT INTO security.simple_credentials( user_account_id, password, password_expires, password_updated) VALUES ((SELECT user_account_id from security.user_account where username = 'qa_test'), '$2a$11$uTzaeewDp2NehwjmfRe/euxP.cy.4ecl5nYA.E5TV9AGjjDKdJwI2', NULL, '2020-01-20 05:41:25.746') ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_test'), (SELECT user_group_id FROM security.user_group where name = 'Security Admin')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_test'), (SELECT user_group_id FROM security.user_group where name = 'BPMN Superuser')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_test'), (SELECT user_group_id FROM security.user_group where name = 'BPMN Admin')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_test'), (SELECT user_group_id FROM security.user_group where name = 'Example')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_test'), (SELECT user_group_id FROM security.user_group where name = 'BPMN User')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_test'), (SELECT user_group_id FROM security.user_group where name = 'Demo')) ON CONFLICT DO NOTHING;
INSERT INTO security.user_account(user_account_id, email, first_name, last_name, middle_name, created, updated, locked, org_unit_id, username) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), 'qa_admin@micord.ru', 'qa_admin', 'qa_admin', NULL, '2020-01-20 08:41:25.526+03', '2020-01-20 09:57:07.25+03', false, (SELECT id from security.org_unit where code = 'DEFAULT'), 'qa_admin') ON CONFLICT DO NOTHING;
INSERT INTO security.simple_credentials( user_account_id, password, password_expires, password_updated) VALUES ((SELECT user_account_id from security.user_account where username = 'qa_admin'), '$2a$11$Zzmuga/xw1c1UnUe9nP9c.aqJ2O.OAI/AbmphOm2jurxMI0S6VAcy', NULL, '2020-01-20 05:41:25.746') ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_admin'), (SELECT user_group_id FROM security.user_group where name = 'Security Admin')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_admin'), (SELECT user_group_id FROM security.user_group where name = 'BPMN Superuser')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_admin'), (SELECT user_group_id FROM security.user_group where name = 'BPMN Admin')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_admin'), (SELECT user_group_id FROM security.user_group where name = 'Example')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_admin'), (SELECT user_group_id FROM security.user_group where name = 'BPMN User')) ON CONFLICT DO NOTHING;
INSERT INTO security.link_user_account_user_group(link_user_account_user_group_id, user_account_id, user_group_id) VALUES (uuid_in(md5(random()::text || now()::text)::cstring), (SELECT user_account_id from security.user_account where username = 'qa_admin'), (SELECT user_group_id FROM security.user_group where name = 'Demo')) ON CONFLICT DO NOTHING;

37
config/testing.env Normal file
View file

@ -0,0 +1,37 @@
# Security datasource
DB_SEC_USERNAME=security_user
DB_SEC_PASSWORD=secpassword
DB_SEC_HOST=db
DB_SEC_PORT=5432
DB_SEC_NAME=security
# App datasource
DB_APP_USERNAME=app_user
DB_APP_PASSWORD=apppassword
DB_APP_HOST=db
DB_APP_PORT=5432
DB_APP_NAME=app
# jBPM datasorce
DB_JBPM_USERNAME=jbpm
DB_JBPM_PASSWORD=jbpmpassword
DB_JBPM_HOST=db
DB_JBPM_PORT=5432
DB_JBPM_NAME=jbpm
# Elasticsearch
WEBBPM_ELASTIC_HOST=
WEBBPM_ELASTIC_USER_PASSWORD=
# LDAP
WEBBPM_LDAP_URL=
WEBBPM_LDAP_BASE=
WEBBPM_LDAP_USER=
WEBBPM_LDAP_PASSWORD=
WEBBPM_LDAP_SYNC_ENABLED=false
WEBBPM_LDAP_SYNC_CRON=0 0 * * * *
WEBBPM_LDAP_LOGIN_ATTR=uid
WEBBPM_LDAP_ORGANIZATION_ATTR=ou
# JIRA
WEBBPM_JIRA_URL=https:/jira.com
WEBBPM_JIRA_USER=jiraUser
WEBBPM_JIRA_PASSWORD=jiraPass
TZ=Europe/Moscow
WEBBPM_TELEGRAM_BOT_TOKEN=
WEBBPM_TELEGRAM_BOT_NAME=

66
distribution/pom.xml Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ervu_dashboard</groupId>
<artifactId>ervu_dashboard</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>distribution</artifactId>
<packaging>ear</packaging>
<properties>
<backendContext>/backend</backendContext>
</properties>
<dependencies>
<dependency>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>backend</artifactId>
<type>war</type>
</dependency>
<dependency>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>frontend</artifactId>
<type>war</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ear-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<modules>
<webModule>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>frontend</artifactId>
<contextRoot>/</contextRoot>
<bundleFileName>frontend.war</bundleFileName>
</webModule>
<webModule>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>backend</artifactId>
<contextRoot>${backendContext}</contextRoot>
<bundleFileName>backend.war</bundleFileName>
</webModule>
</modules>
</configuration>
</plugin>
</plugins>
<finalName>${project.parent.artifactId}</finalName>
</build>
<profiles>
<profile>
<id>enable-version-in-url</id>
<properties>
<backendContext>/backend-${project.version}</backendContext>
</properties>
</profile>
</profiles>
</project>

1
frontend/.npmrc Normal file
View file

@ -0,0 +1 @@
registry=https://repo.micord.ru/repository/npm-all/

71
frontend/angular.json Normal file
View file

@ -0,0 +1,71 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"webbpm-frontend": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/ts/main.ts",
"tsConfig": "src/tsconfig.json",
"polyfills": "src/ts/polyfills.ts",
"assets": [
"src/resources"
],
"styles": [
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/moment/min/moment-with-locales.js",
"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
"node_modules/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js",
"node_modules/selectize/dist/js/standalone/selectize.min.js",
"node_modules/downloadjs/download.min.js"
]
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "webbpm-frontend:build"
},
"configurations": {}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "webbpm-frontend:build"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [],
"exclude": []
}
}
}
}
},
"defaultProject": "webbpm-frontend"
}

10
frontend/bs-config.json Normal file
View file

@ -0,0 +1,10 @@
{
"port": 8000,
"open": false,
"files": [
"./**/*.{html,htm,css,js}"
],
"server": {
"baseDir": "./"
}
}

23
frontend/index.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>ervu_dashboard</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="icon" type="image/png" href="src/resources/img/logo.png"/>
<link rel="stylesheet" href="src/resources/css/style.css"/>
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('webbpm').catch(function (err) {
console.error(err);
});
</script>
</head>
<body webbpm class="webbpm ervu_dashboard">
<div class="progress"></div>
</body>
</html>

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>ervu_dashboard</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="icon" type="image/png" href="src/resources/img/logo.png"/>
</head>
<body webbpm class="webbpm ervu_dashboard">
<div class="progress"></div>
</body>
</html>

10696
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

101
frontend/package.json Normal file
View file

@ -0,0 +1,101 @@
{
"name": "webbpm-frontend",
"version": "1.0.0",
"scripts": {
"lite": "node ./node_modules/lite-server/bin/lite-server",
"cleanup": "npm run cleanup-ngc && node ./node_modules/rimraf/bin ./build ./dist",
"cleanup-ngc": "node ./node_modules/rimraf/bin ./src/ts/**/*.js ./src/ts/**/*.json ./src/ts/page.routing.ts",
"cleanup-and-ngc": "npm run cleanup && npm run ngc",
"ngc": "node --max-old-space-size=14336 ./node_modules/@angular/compiler-cli/src/main -p tsconfig.aot.json",
"build-webpack": "node --max-old-space-size=14336 ./node_modules/webpack/bin/webpack --config webpack.aot.config.js --progress --profile",
"save-ts-metadata": "node save.ts.metadata.js",
"tsc": "node ./node_modules/typescript/bin/tsc",
"tsc-watch": "node ./node_modules/typescript/bin/tsc --watch",
"ts-watch": "node node_modules/cross-env/dist/bin/cross-env.js TSC_NONPOLLING_WATCHER=true npm run tsc-watch",
"ts": "npm install && npm run tsc",
"compile": "npm run ts-watch",
"install-compile": "npm install && npm run ts-watch"
},
"dependencies": {
"@angular/animations": "7.2.15",
"@angular/common": "7.2.15",
"@angular/compiler": "7.2.15",
"@angular/core": "7.2.15",
"@angular/forms": "7.2.15",
"@angular/http": "7.2.15",
"@angular/platform-browser": "7.2.15",
"@angular/platform-browser-dynamic": "7.2.15",
"@angular/router": "7.2.15",
"@ng-bootstrap/ng-bootstrap": "4.1.1",
"@webbpm/base-package": "3.173.1",
"ag-grid-angular": "29.0.0-micord.4",
"ag-grid-community": "29.0.0-micord.4",
"angular-calendar": "0.28.28",
"autonumeric": "4.5.10-cg",
"bootstrap": "4.3.1",
"bootstrap-icons": "1.10.3",
"cadesplugin_api": "2.0.4-micord.1",
"chart.js": "3.8.0-cg.1",
"chartjs-adapter-moment": "1.0.0",
"core-js": "2.4.1",
"date-fns": "2.29.3",
"downloadjs": "1.4.8",
"eonasdan-bootstrap-datetimepicker": "4.17.47-micord.4",
"esmarttokenjs": "2.2.1-cg",
"font-awesome": "4.7.0",
"google-libphonenumber": "3.0.9",
"inputmask": "5.0.5-cg.2",
"jquery": "3.3.1",
"js-year-calendar": "1.0.0-cg.2",
"jsgantt-improved": "2.0.10-cg",
"moment": "2.17.1",
"moment-timezone": "0.5.11",
"ngx-cookie": "3.0.1",
"ngx-international-phone-number": "1.0.6",
"ngx-toastr": "10.2.0-cg",
"popper.js": "1.14.7",
"reflect-metadata": "0.1.13",
"rxjs": "6.4.0",
"rxjs-compat": "6.4.0",
"selectize": "0.12.4-cg.10",
"systemjs": "0.21.4",
"systemjs-plugin-babel": "0.0.25",
"tslib": "1.9.3",
"zone.js": "0.8.29"
},
"devDependencies": {
"@angular-devkit/build-optimizer": "0.13.9",
"@angular-devkit/core": "7.3.9",
"@angular/cli": "7.3.9",
"@angular/compiler-cli": "7.2.15",
"@angular/platform-server": "7.2.15",
"@babel/core": "7.9.6",
"@babel/preset-env": "7.9.6",
"@types/bootstrap": "3.3.39",
"@types/jquery": "2.0.49",
"@types/node": "7.0.5",
"@types/selectize": "0.12.33",
"angular-router-loader": "0.8.5",
"angular2-template-loader": "0.6.2",
"babel-loader": "8.1.0",
"codelyzer": "5.2.1",
"copy-webpack-plugin": "5.0.3",
"cross-env": "5.2.1",
"css-loader": "2.1.0",
"del": "2.2.2",
"file-loader": "3.0.1",
"html-webpack-plugin": "4.5.2",
"lite-server": "2.3.0",
"mini-css-extract-plugin": "0.6.0",
"mkdirp": "0.5.1",
"raw-loader": "1.0.0",
"style-loader": "0.23.1",
"terser-webpack-plugin": "1.2.4",
"tslint": "5.13.1",
"typescript": "3.2.4",
"typescript-parser": "2.6.1-cg-fork",
"webpack": "4.32.2",
"webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "3.3.2"
}
}

113
frontend/pom.xml Normal file
View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ervu_dashboard</groupId>
<artifactId>ervu_dashboard</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>ervu_dashboard.ervu_dashboard</groupId>
<artifactId>frontend</artifactId>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>1.5.3</version>
<executions>
<execution>
<id>replace-version-in-url</id>
<phase>process-resources</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>${basedir}/src/resources/app-config.json</include>
<include>${basedir}/dist/src/resources/app-config.json</include>
<include>${basedir}/src/resources/app.version</include>
<include>${basedir}/dist/src/resources/app.version</include>
</includes>
<replacements>
<replacement>
<token>%project.version%</token>
<value>${project.version}</value>
</replacement>
<replacement>
<token>%enable.version.in.url%</token>
<value>${enable.version.in.url}</value>
</replacement>
</replacements>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<copyWebResources>false</copyWebResources>
<webResources>
<resource>
<directory>${basedir}</directory>
<includes>
<include>src/resources/**/*</include>
<include>build_dev/**/*</include>
<include>node_modules/**/*</include>
<include>index.html</include>
<include>systemjs.config.js</include>
</includes>
</resource>
</webResources>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>compile-ts</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<copyWebResources>false</copyWebResources>
<webResources>
<resource>
<directory>${basedir}/dist</directory>
</resource>
</webResources>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>enable-version-in-url</id>
<properties>
<enable.version.in.url>true</enable.version.in.url>
</properties>
</profile>
</profiles>
</project>

23
frontend/preview.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Web BPM</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="icon" type="image/png" href="src/resources/img/logo.png"/>
<link rel="stylesheet" href="src/resources/css/style.css"/>
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.preview.config.js"></script>
<script>
System.import('preview').catch(function (err) {
console.error(err);
});
</script>
</head>
<body preview-container id="webbpm-angular-application-container ervu_dashboard" class="webbpm">
<div class="progress"></div>
</body>
</html>

View file

@ -0,0 +1,72 @@
#!/usr/bin/env node
var tsp = require("typescript-parser");
var fs = require('fs');
var path = require('path');
var ts = require("typescript");
var parser = new tsp.TypescriptParser();
var excludedDirs = [
'generated-sources'
];
var walkFileTree = function (dir, action) {
if (typeof action !== "function") {
return;
}
fs.readdirSync(dir).forEach(function (file) {
var path = dir + "/" + file;
var stat = fs.statSync(path);
var extension = ".ts";
if (stat && stat.isDirectory() && excludedDirs.indexOf(file) === -1) {
walkFileTree(path, action);
}
else if (path.indexOf(extension, path.length - extension.length) !== -1) {
action(null, path);
}
});
};
var dateInLong = Date.now();
var arr = [];
var basePath = path.resolve(__dirname, "src/ts/");
walkFileTree(basePath, function (err, file) {
var content = fs.readFileSync(file).toString();
var jsonStructure = parser.parseTypescript(ts.createSourceFile(
file,
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
),
'/');
jsonStructure['packageName'] = path.relative(path.resolve(__dirname, "src/ts/"),jsonStructure['filePath']);
jsonStructure['imports'].forEach( function (val) {
if (val.libraryName.startsWith(".")) {
val['libraryName'] = path.resolve(path.dirname(jsonStructure['filePath']), val['libraryName']);
val['libraryName'] = path.relative(path.resolve(__dirname, "src/ts/"), val['libraryName']);
val['libraryName'] = path.dirname(val['libraryName']).split(path.sep).join(".");
}
});
delete jsonStructure['filePath'];
jsonStructure['packageName'] = path.dirname(jsonStructure['packageName']).split(path.sep).join( ".");
arr.push(jsonStructure);
});
var cache = [];
fs.writeFileSync("./../.studio/typescript.metadata.json",
JSON.stringify(arr, function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
// Circular reference found, discard key
return;
}
// Store value in our collection
cache.push(value);
}
return value;
}));
cache = null;
console.log("typescript parse time = " + (Date.now() - dateInLong));

View file

@ -0,0 +1,19 @@
{
"electronic_sign.esmart_extension_url": "",
"electronic_sign.tsp_address": "",
"filter_cleanup_interval_hours": 720,
"filter_cleanup_check_period_minutes": 30,
"auth_method": "form",
"enable.version.in.url": "%enable.version.in.url%",
"guard.confirm_exit": false,
"message_service_error_timeout": "",
"message_service_warning_timeout": "",
"message_service_success_timeout": "",
"message_service_info_timeout": "",
"jivo_chat_widget_api_url": "https://code.jivo.ru/widget/{ID}",
"jivo_chat_widget_enabled": false,
"password_pattern": "^((?=(.*\\d){1,})(?=.*[a-zа-яё])(?=.*[A-ZА-ЯЁ]).{8,})$",
"password_pattern_error": "Пароль должен содержать заглавные или прописные буквы и как минимум 1 цифру",
"show.client.errors": false,
"available_task.single_fetch": true
}

View file

@ -0,0 +1 @@
%project.version%

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,342 @@
@font-face {
font-family: 'Segoe';
src: url('../fonts/Segoe.ttf');
}
@font-face {
font-family: 'SegoeSL';
src: url('../fonts/SegoeSL.ttf');
}
@font-face {
font-family: 'SegoeSB';
src: url('../fonts/SegoeSB.ttf');
}
@font-face {
font-family: 'SegoeB';
src: url('../fonts/SegoeB.ttf');
}
@font-face {
font-family: 'SegoeBL';
src: url('../fonts/SegoeBL.ttf');
}
.webbpm a {
color: var(--color-link);
&:hover,
&:focus,
&:active {
color: var(--color-link-hover);
text-decoration: none;
}
}
body.webbpm {
display: flex;
flex-direction: column;
color: var(--color-text-primary);
font-family: 'Segoe';
background-color: var(--white);
}
.webbpm .container {
padding: 70px 0 0;
}
body.webbpm [id="page"],
.webbpm .container .container-inside {
font-family: 'Segoe';
font-size: var(--size-text-primary);
}
.webbpm .logo {
height: auto;
width: auto;
}
.webbpm .header-logo {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 40px;
.logo a {
background: url('../../../src/resources/img/logo-full.png') no-repeat 0 50%;
}
}
.webbpm .header-menu {
display: flex;
flex-direction: row;
margin-left: auto;
margin-right: 40px;
& > * {
margin-right: 20px;
&:last-child {
margin-right: 0;
}
}
.nav-link {
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-size: var(--size-text-primary);
width: 30px;
height: 30px;
padding: 0;
border: 0;
border-radius: 15px;
background-color: var(--color-text-primary);
outline: transparent;
&::after {
display: none;
}
&:hover,
&:focus,
&:active {
background-color: var(--color-link);
}
}
.logout .user-info {
display: flex;
flex-direction: column;
color: var(--black);
padding: 4px 20px;
background: transparent;
cursor: default;
& > * {
display: flex;
padding-bottom: 10px;
margin: 0 0 10px 0;
border-bottom: 1px solid #f1f5f9;
&:last-child {
margin-bottom: 0;
}
}
.user-fio {
padding-bottom: 0;
margin-bottom: 0;
border-bottom: 0;
}
.user-department {
color: #a0b1bc;
line-height: 1.2;
}
}
}
.webbpm .header {
display: flex;
font-family: 'Segoe';
width: 100%;
height: auto;
min-height: 70px;
border-bottom: 1px solid var(--bg-light);
background: var(--white);
box-shadow: 0px 15px 20px 0px rgb(0 0 0 / 4%);
& > div > * {
position: relative;
display: flex;
align-items: center;
}
.dropdown-menu.show {
top: 69px !important;
right: 0px !important;
left: auto !important;
transform: none !important;
margin: 0;
border: 0;
border-radius: 0 0 10px 10px;
background-color: var(--white);
box-shadow: 0 8px 12px rgb(77 72 91 / 5%), 0 6px 10px rgb(77 72 91 / 0%);
.dropdown-menu-inner {
max-height: calc(100vh - 140px);
overflow-y: auto;
}
}
:is(process, admin-menu) .dropdown-menu.show {
top: 49px !important;
}
.logout .dropdown-menu.show {
width: 300px;
}
}
.webbpm .dropdown-menu-inner:hover {
background-color: transparent;
}
.webbpm .dropdown-item {
padding: 4px 20px;
&:hover,
&:focus,
&:active {
color: var(--color-link);
background-color: transparent;
outline: transparent;
}
}
.webbpm footer {
color: var(--color-text-primary);
font-size: var(--size-text-secondary);
left: 0;
right: 0;
padding: 15px 40px;
border-top: 1px solid var(--border-light);
a {
color: var(--color-text-primary);
&:hover {
color: var(--color-link);
}
}
}
/*-------------- Menu tasks -------------- */
.webbpm .task-list-tree-panel {
padding: 0 40px;
.task-list-filter {
font-family: 'Segoe';
box-shadow: none;
li:first-of-type {
font-family: 'SegoeSB';
font-weight: normal;
padding-left: 0;
}
li.ontime label div {
background-color: #31c980;
}
li.overdue label div {
background-color: var(--color-link);
}
}
}
.webbpm .task-list-workplace {
padding: 20px 40px 0 40px;
}
.webbpm .task-tbl :is(.tr.task-ontime, .tr.task-overdue) > .td.task::before {
top: 24px;
background-color: #31c980;
}
.webbpm .task-tbl .tr.task-overdue > .td.task::before {
background-color: var(--color-link);
}
.webbpm .task-tbl .thead {
display: table-header-group;
border: 0;
background: transparent;
}
.webbpm .task-tbl .th {
color: var(--color-text-primary);
font-family: 'SegoeSB';
font-size: var(--size-text-secondary);
font-weight: normal;
padding: 9px 12px;
border: 0;
background: var(--bg-light);
}
.webbpm .task-tbl .th:first-child {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.webbpm .task-tbl .th:last-child {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
.webbpm .task-tbl .td {
color: var(--color-text-primary);
font-size: var(--size-text-primary);
padding: 16px 12px;
border: 0;
}
.webbpm .task-tbl .thead ~ .tr {
border-color: var(--border-light);
border-width: 1px 0 0 0;
border-style: solid;
}
.webbpm .task-tbl .thead + .tr {
border-top-color: transparent;
}
.webbpm .task-tbl .thead ~ .tr:hover {
border-top-color: transparent;
border-bottom-color: transparent;
}
.webbpm .task-tbl .thead ~ .tr:hover +.tr {
border-top-color: transparent;
}
.webbpm .task-tbl .thead ~ .tr:hover .td {
background-color: #f9f9f9;
}
.webbpm .task-tbl .thead ~ .tr:hover .td:first-child {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.webbpm .task-tbl .thead ~ .tr:hover .td:last-child {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
/*------------- end Menu tasks ----------- */
/*----------------- Login ---------------- */
.webbpm :is(.form-signin, .form-signup, .confirm) {
color: var(--color-text-primary);
width: 560px;
padding: 60px 80px;
margin: 30px auto;
border: 0;
border-radius: 15px;
box-shadow: 0px 0px 30px 2px rgb(77 72 91 / 12%);
background-color: var(--white);
}
.webbpm :is(.form-signin, .form-signup) .row.title {
position: relative;
padding: 0;
}
.webbpm .form-signin h1,
.webbpm .form-signin h2,
.webbpm .form-signup h2,
.webbpm .confirm h2 {
font-family: 'SegoeB';
font-size: 32px;
text-align: left;
margin-bottom: 20px;
}
.webbpm :is(.form-signin, .form-signup, .confirm) .logo {
position: absolute;
top: -10px;
right: 0;
width: 145px;
height: 40px;
background: none;
}
.webbpm .form-signin .row.registration > * + *,
.webbpm .form-signin .login-btn-box .password,
.webbpm .form-signin .login-btn-box .btn + .btn {
margin-left: 20px;
}
/*--------------- end Login -------------- */

View file

@ -0,0 +1,846 @@
:root {
--white: #ffffff;
--black: #000000;
--color-text-primary: #404954;
--color-link: #1c92ea;
--color-link-hover: #1b84d2;
--bg-light: #f5f7fa;
--bg-secondary: #4c5969;
--border-light: #e3e6ed;
--size-text-primary: 16px;
--size-text-secondary: 14px;
}
* {
margin: 0;
padding: 0;
}
*, *:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body.webbpm .form-signin label {
width: 160px;
margin-right: 0;
}
.webbpm .progress {
position: absolute;
width: 64px;
height: 64px;
margin-bottom: 0 !important;
background: url("../img/progress.gif") no-repeat 0 0;
}
.webbpm > .progress {
top: 50%;
left: 50%;
margin-top: -30px;
margin-left: -30px;
}
.webbpm .search-task-progress-bar {
text-align: center;
}
.webbpm .modal-body .progress,
.webbpm .search-task-progress-bar .progress {
position: relative;
top: 0;
margin-top: 0;
}
/*-- common class --*/
.webbpm .fl-left {
float: left;
}
.webbpm .fl-right {
float: right;
}
.webbpm .anchor {
float: none;
clear: both;
}
.webbpm :is(ul, ol) li {
list-style: none;
}
.webbpm :is(h1, h2, h3) {
margin: 0;
font-weight: normal;
}
.webbpm h1 {
font-size: 2.33em;
}
.webbpm h2 {
font-size: 2em;
}
.webbpm h3 {
font-size: 1.5em;
}
.webbpm .table {
display: table;
}
.webbpm .tr {
display: table-row;
}
.webbpm .td, .webbpm .th {
display: table-cell;
}
/*-- layout --*/
html, body.webbpm {
width: 100%;
height: 100%;
display: block;
}
body.webbpm {
background-color: #f9f9fa;
font-family: Arial;
font-size: var(--size-text-secondary);
min-height: 0;
padding: 0;
}
.webbpm .wrapper {
height: 100%;
min-height: 100%;
position: relative;
}
.webbpm .container {
width: 100%;
max-width: 100%;
height: auto;
margin: 0;
padding: 67px 0 0;
position: absolute;
top: 0px;
left: 0;
right: 0;
bottom: 50px;
border: 0;
overflow: hidden;
[ng-include="taskPageFile"] {
position: relative;
min-height: 100%;
height: 100%;
overflow: auto;
}
.container-inside {
font-family: Arial;
font-size: var(--size-text-secondary);
position: relative;
height: 100%;
overflow: auto;
}
}
.webbpm footer {
position: absolute;
color: var(--black);
font-size: 12px;
bottom: 0;
left: 15px;
right: 15px;
height: 50px;
padding: 15px 0;
border-top: 1px solid #c1c1c1;
background: transparent;
span + span {
padding-left: 20px;
}
}
/*--------- TOP MENU ----------*/
.webbpm .logo {
display: inline-block;
margin: 0;
float: left;
a {
width: 200px;
height: 67px;
position: absolute;
background: url("../img/logo.png") no-repeat 0 0;
}
}
.webbpm .header {
position: absolute;
color: var(--white);
font-family: Corbel;
font-size: var(--size-text-secondary);
top: 0;
left: 0;
right: 0;
height: 67px;
min-height: 67px;
line-height: normal;
border: 0;
padding: 0;
background: #b9c0ca;
z-index: 997;
.nav .nav-link {
color: var(--white);
float: none;
display: block;
line-height: 60px;
padding: 0 15px 0 60px;
text-shadow: none;
cursor: pointer;
}
.nav .nav-link:hover {
text-decoration: none;
}
}
.webbpm .dropdown-menu {
background-color: #eee;
}
.webbpm .nav .nav-item .dropdown-menu:after {
border-bottom: 6px solid #eee;
}
.webbpm .inner {
overflow-y: auto;
}
@media (min-width: 768px) {
.webbpm .navbar .nav > * {
float: left;
}
}
@media (max-width: 767px) {
.webbpm .dropdown-menu > div > button {
color: #d1dbe5;
}
}
/*--------- end - TOP MENU ----------*/
.webbpm .user-department,
.webbpm .user-info {
color: #5a6473;
}
.webbpm .user-info > * {
display: inline-block;
margin-left: 5px;
}
.webbpm [log-out] {
max-width: 40%;
margin-right: 15px;
}
.webbpm .content {
padding: 0 20px;
}
.webbpm .inner {
min-height: 100%;
height: 100%;
overflow-y : scroll;
}
/*--------------task-list------------------*/
.task-list {
font-size: 0;
height: 100%;
min-height: 100%;
}
.task-list > div {
display: block;
float: left;
height: 100%;
min-height: 100%;
font-size: var(--size-text-secondary);
}
.task-list-tree-panel {
width: 20%;
background: #e9edf2;
border-right: 1px solid #b9c1ca;
}
.task-list-tree-panel .task-list-filter {
position: relative;
margin-top: 15px;
z-index: 10;
}
.task-list-tree-panel .task-list-filter li {
position: relative;
padding: 8px 10px;
margin: 2px 10px;
}
.task-list-tree-panel .task-list-filter li::before {
content: "";
position: absolute;
left: 0px;
bottom: 0px;
right: 0;
top: 0;
border: 1px solid rgb(206, 212, 219);
border-radius: 4px;
background-color: var(--white);
opacity: 0.6;
}
.task-list-tree-panel .task-list-filter li label:hover {
cursor: pointer;
}
.task-list-tree-panel .task-list-filter li label {
position: relative;
width: 100%;
margin: 0;
z-index: 11;
}
.task-list-tree-panel .task-list-filter li label input[type="radio"] {
float: left;
margin-top: 2px;
margin-right: 4px;
}
.task-list-tree-panel .task-list-filter li label span {
float: right;
background-color: #bbb;
padding: 0px 4px;
border-radius: 3px;
min-width: 25px;
text-align: center;
color: var(--white);
}
.task-list-tree-panel .task-list-filter li.ontime label span {
background-color: #a0c367;
}
.task-list-tree-panel .task-list-filter li.overdue label span {
background-color: #fc2d2d;
}
.task-list-header {
border-bottom: 1px solid #b9c1ca;
background-color: #ccd6e0;
height: 30px;
line-height: 30px;
color: #565968;
font-size: var(--size-text-secondary);
padding: 0 12px;
}
.structure-box {
padding: 15px;
}
.task-list-workplace {
width: 80%;
padding: 15px 15px 0;
overflow-y: auto;
}
/*--------------task-list end--------------*/
/*---------------table-list----------------*/
.task-tbl {
background: var(--white);
width: 100%;
border-collapse: collapse;
}
.task-tbl .td, .task-tbl .th {
border: 1px solid #b9c1ca;
padding: 10px 15px;
}
.task-tbl .th {
color: #565968;
}
.task-tbl .td {
color: #333;
}
.task-tbl .thead {
background: #ccd6e0;
}
.task-tbl > .tr:hover {
cursor: pointer;
background: #e9edf2;
}
.task-tbl .tr.task-ontime > .td.task,
.task-tbl .tr.task-overdue > .td.task {
position: relative;
padding-left: 20px;
}
.task-tbl .tr.task-ontime > .td.task::before,
.task-tbl .tr.task-overdue > .td.task::before {
content: "";
position: absolute;
top: 12px;
left: 5px;
width: 10px;
height: 10px;
background-color: #a0c367;
border-radius: 10px;
z-index: 1;
}
.task-tbl .tr.task-overdue > .td.task::before {
background-color: #ff0000;
}
/*----------------table-list end----------------*/
/*--------------Окно сообщения об ошибке--------------*/
.webbpm #toast-container {
font-size: 12px;
bottom: auto;
overflow-y: auto;
overflow-x: hidden;
max-height: 95%;
}
.webbpm #toast-container .toast:hover {
box-shadow: 0 0 12px #999999;
}
.webbpm #toast-container .ngx-toastr {
min-width: 540px;
opacity: 0.9;
}
.webbpm #toast-container .toast-error {
background-color: #d9534f;
}
.webbpm #toast-container .toast-close-button {
text-shadow: none;
}
.webbpm .toast-message > div {
display: none;
}
.webbpm .toast-message > .active {
display: block;
}
.webbpm .toast-message > .active::after {
display: block;
content: "";
float: none;
clear: both;
}
.webbpm .toast-message > .active a {
float: right;
margin: 5px 0;
}
.webbpm .toast-message > .toast-msg-close.active ~ .toast-msg-text {
display: block;
font-size: 0.8em;
}
.webbpm .toast-message a:not([href]):not([tabindex]) {
text-decoration: underline;
}
/*------------Окно сообщения об ошибке end------------*/
/*----------------- Ошибка 404 -------------------*/
.webbpm .container .task-not-found-page {
position: relative;
display: table;
width: 100%;
height: 100%;
}
.webbpm .container .task-not-found-container {
display: table-cell;
color: var(--black);
font-size: 1.8em;
background-color: #c9d4e0;
text-align: center;
vertical-align: middle;
}
.webbpm .container .task-not-found-container > div {
display: inline-block;
}
.webbpm .container .task-not-found-container > div:first-child {
color: var(--white);
font-size: 7.8em;
font-weight: bold;
}
.webbpm .container .task-not-found-container > div:last-child {
text-align: left;
margin-left: 40px;
}
.webbpm .container .task-not-found-container h2 {
font-size: 2em;
margin-bottom: 10px;
}
.webbpm .container .task-not-found-container a {
cursor: pointer;
}
/*--------------- end Ошибка 404 -----------------*/
/*-------------- MOBILE --------------*/
.webbpm.mobile .task-list-tree-panel,
.webbpm.mobile footer {
display: none;
}
.webbpm.mobile .task-list-workplace {
margin-left: 0;
}
.webbpm.mobile .container {
bottom: 0;
}
.webbpm.mobile form {
overflow: hidden;
}
.webbpm.mobile .form-signin {
width: auto;
padding: 20px;
margin: 40px 20px;
border-radius: 4px;
}
.webbpm.mobile .form-signin h1,
.webbpm.mobile .form-signin h2 {
margin-left: 0;
}
.webbpm.mobile .form-signin label {
text-align: left;
}
.webbpm.mobile .form-signin input[type="text"],
.webbpm.mobile .form-signin input[type="password"] {
width: 100%;
}
.webbpm.mobile .form-signin .login-btn-box {
width: auto;
margin-right: 0;
}
/*-------------- end MOBILE --------------*/
/*-------------- НОВЫЙ ДИЗАЙН --------------*/
/*------------------ Фильтры ------------------*/
.webbpm .task-list {
height: auto;
min-height: auto;
}
.webbpm .task-list > .task-list-tree-panel {
background: var(--white);
border-right: 0;
}
.webbpm .task-list > .task-list-tree-panel,
.webbpm .task-list > .task-list-workplace {
font-size: var(--size-text-secondary);
float: none;
width: 100%;
height: auto;
min-height: auto;
}
.webbpm .task-list-tree-panel .task-list-filter {
font-family: Corbel;
margin: 0;
box-shadow: 0px 4px 10px -5px rgba(40, 40, 40, 0.3);
ul {
margin: 0;
}
li {
display: inline-block;
padding: 12px 0px;
margin: 0;
&:first-of-type {
font-size: 1.4em;
font-weight: bold;
width: 197px;
padding: 0 15px;
}
&::before {
display: none;
}
label {
font-size: 1.3em;
font-weight: normal;
}
label div {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 10px;
margin-right: 8px;
}
&.ontime label div {
background-color: #2da6a1;
}
&.overdue label div {
background-color: #9c5d7a;
}
label input[type="radio"] {
float: none;
display: none;
}
label span {
float: none;
color: var(--black);
font-weight: normal;
text-align: left;
min-width: auto;
padding: 5px 15px;
border-radius: 0;
background-color: transparent;
}
label input[type="radio"].ng-valid-parse ~ span {
background-color: #eaedf2;
}
}
}
/*-------------- end НОВЫЙ ДИЗАЙН --------------*/
.webbpm .dialog-stack-trace {
overflow-x: auto;
overflow-y: auto;
max-height: 300px;
}
.webbpm .dialog-show-button {
text-decoration: underline;
cursor: pointer;
}
.webbpm .dialog-error-number {
font-size: 1.25rem;
}
.webbpm .dialog-error-title {
font-size: 1rem;
}
/*-- login --*/
.webbpm :is(.form-signin, .form-signup, .confirm) {
color: #333;
width: 580px;
padding: 80px 100px;
margin: 20px auto;
border: 1px solid var(--bg-light);
background: var(--white);
}
.webbpm .form-signin.esia {
width: 450px;
text-align: center;
padding: 45px 55px 35px 55px;
}
.webbpm .form-signin h1,
.webbpm .form-signin h2,
.webbpm .form-signup h2,
.webbpm .confirm h2 {
text-align: center;
margin-bottom: 20px;
}
.webbpm .form-signin.esia :is(h1, h2) {
margin-left: 0;
}
.webbpm .form-signin label,
.webbpm .form-signup label {
margin-bottom: 0;
}
.webbpm .form-signin input {
width: 240px;
font-size: var(--size-text-secondary);
display: inline-block;
}
.webbpm .form-signin.esia input {
width: 160px;
margin-top: 40px;
}
.webbpm :is(.form-signin, .form-signup) .row {
display: flex;
margin: 0 0 20px;
}
.webbpm .registration-link,
.webbpm .login-link {
margin-right: 20px;
font-size: var(--size-text-secondary);
}
.webbpm .form-signin .row.registration {
flex-direction: row;
}
.webbpm .form-signin .login-btn-box {
display: flex;
flex-direction: row;
align-items: center;
}
.webbpm .form-signin .row.registration > * + *,
.webbpm .form-signin .login-btn-box .password,
.webbpm .form-signin .login-btn-box .btn + .btn {
margin-left: 10px;
}
.webbpm .input-group > .input-group-append > .input-group-text {
border-radius: 0;
}
.webbpm .form-signin .register-btn-box,
.webbpm .form-signup .register-btn-box,
.webbpm .form-signup .reset-password-btn-box {
width: 220px;
}
.webbpm .form-signup .row international-phone-number .flagInput .btn {
border-left: 1px solid #c6cdd3;
}
.webbpm .form-signup .row international-phone-number .flagInput ~ input {
border-left: none;
}
.webbpm .form-signin .has-error .help-block {
padding-left: 125px;
font-size: var(--size-text-secondary);
}
.webbpm .form-signup .has-account a {
margin-left: 10px;
}
/*------------------ Формы регистрации и подтверждения ------------------*/
.form-signup .has-account {
flex-direction: row;
}
.form-signup .has-account a span,
.confirm a span {
margin-right: 4px;
}
.form-signup .dropbtn.btn {
margin-bottom: 0;
}
.form-signup .input-group-text {
border-left-width: 0;
}
.form-signup input.ng-invalid.ng-touched,
.form-signup input.ng-invalid.ng-dirty {
border-color: red !important;
}
.form-signup .msg-alert {
color: red;
font-size: 11px;
padding: 3px 0 0;
}
.form-signup .consent {
color: #929292;
font-size: 13px;
margin-top: 20px;
}
/*------------------ End - Формы регистрации и подтверждения ------------------*/
/*------------------ Сообщения об ошибке ------------------*/
.webbpm .error_message {
width: 650px;
margin: 0 auto;
margin-top: 10%;
}
.webbpm .error_title {
position: relative;
color: #9c5d7a;
font-size: 5.5em;
font-weight: bold;
margin-left: 100px;
line-height: 1;
}
.webbpm .error_title::before {
position: absolute;
content: "";
left: -100px;
width: 75px;
height: 75px;
border-radius: 40px;
background-color: #9c5d7a;
background-image: url("../img/access_denied.png");
background-repeat: no-repeat;
background-position: 50% 50%;
}
.webbpm .error_title_long {
margin-bottom: 20px;
font-size: 2.5em;
}
.webbpm .error_body {
font-size: 2em;
line-height: 1.2;
margin: 5px 0 0 100px;
}
/*---------------- end Сообщения об ошибке ---------------*/
/*-------------- Поле телефона ------------ */
.flag {
background-image: url('./../img/country-flags.jpg') !important;
}
/*-------------- end Поле телефона ------------ */

View file

@ -0,0 +1,10 @@
@import "../../../node_modules/angular-calendar/css/angular-calendar.css";
@import "../../../node_modules/bootstrap/dist/css/bootstrap-grid.css";
@import "../../../node_modules/bootstrap/dist/css/bootstrap-reboot.css";
@import "../../../node_modules/bootstrap/dist/css/bootstrap.css";
@import "../../../node_modules/bootstrap-icons/font/bootstrap-icons.css";
@import "../../../node_modules/font-awesome/css/font-awesome.css";
@import "../../../node_modules/@webbpm/base-package/css/style.css";
@import "structure.css";
@import "inbox-app.css";
@import "components-app.css";

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

View file

@ -0,0 +1,4 @@
<div class="error_message">
<div class="error_title">403</div>
<div class="error_body">Доступ запрещен</div>
</div>

View file

@ -0,0 +1,34 @@
<div *ngIf="(currentSession | async)?.authorities.includes('BPMN.ADMIN.PROCESS_INSTANCE.LIST') ||
(currentSession | async)?.authorities.includes('USER_MANAGEMENT.USER.LIST') ||
(currentSession | async)?.authorities.includes('USER_MANAGEMENT.GROUP.LIST') ||
(currentSession | async)?.authorities.includes('USER_MANAGEMENT.ROLE.LIST') ||
(currentSession | async)?.authorities.includes('USER_MANAGEMENT.ORG_UNIT.LIST') ||
(currentSession | async)?.authorities.includes('USER_MANAGEMENT.AUTHORITY.LIST')" ngbDropdown class="nav-item" [placement]="placement">
<button class="nav-link bi bi-gear-fill" id="adminDropdownMenu" ngbDropdownToggle title="Администрирование"></button>
<div ngbDropdownMenu aria-labelledby="adminDropdownMenu">
<button *ngIf="(currentSession | async)?.authorities.includes('BPMN.ADMIN.PROCESS_INSTANCE.LIST')"
routerLink="/process/instance" ngbDropdownItem>
Экземпляры процессов
</button>
<button *ngIf="(currentSession | async)?.authorities.includes('USER_MANAGEMENT.USER.LIST')"
routerLink="/user-management/users" ngbDropdownItem>
Пользователи
</button>
<button *ngIf="(currentSession | async)?.authorities.includes('USER_MANAGEMENT.GROUP.LIST')"
routerLink="/user-management/groups" ngbDropdownItem>
Группы
</button>
<button *ngIf="(currentSession | async)?.authorities.includes('USER_MANAGEMENT.ROLE.LIST')"
routerLink="/user-management/roles" ngbDropdownItem>
Роли
</button>
<button *ngIf="(currentSession | async)?.authorities.includes('USER_MANAGEMENT.ORG_UNIT.LIST')"
routerLink="/user-management/org-units" ngbDropdownItem>
Организации
</button>
<button *ngIf="(currentSession | async)?.authorities.includes('USER_MANAGEMENT.AUTHORITY.LIST')"
routerLink="/user-management/authorities" ngbDropdownItem>
Безопасность действий
</button>
</div>
</div>

View file

@ -0,0 +1,10 @@
<footer>
<div id="webbpm-footer">
<div class="fl-left"><span>Web-BPM</span></div>
<div class="fl-right">
<span><a href="mailto:info@micord.ru">Поддержка</a></span>
<span><a href="http://www.micord.ru" target="_blank">Компания "Микорд"</a></span>
<span><application-version></application-version></span>
</div>
</div>
</footer>

View file

@ -0,0 +1,13 @@
<nav class="header" *ngIf="currentSession | async as session" id="webbpm-header">
<div class="header-logo">
<div class="logo"><a routerLink="/"></a></div>
</div>
<div class="header-menu">
<process *ngIf="session.authorities.includes('BPMN.USER.START_PROCESS')"></process>
<div *ngIf="session.authorities.includes('BPMN.USER.TASK_LIST')">
<button class="nav-link bi bi-file-text-fill" (click)="openTaskList()" title="Задачи"></button>
</div>
<admin-menu [placement]="'bottom'"></admin-menu>
<div ngbDropdown class="logout" log-out></div>
</div>
</nav>

View file

@ -0,0 +1 @@
<span id="version-footer">Версия: {{applicationVersion}}</span>

View file

@ -0,0 +1,27 @@
<div class="confirm">
<div class="form-logo">
<div></div>
</div>
<div class="info">
<div>
<h2>Подтверждение почты</h2>
<div *ngIf="verificationStatus.toString() === 'VERIFYING'">
Подтверждение...
</div>
<div *ngIf="verificationStatus.toString() === 'VERIFIED'">
<div class="alert alert-success">
Адрес электронной почты успешно подтвержден
</div>
</div>
<div *ngIf="verificationStatus.toString() === 'FAILED'">
<div class="alert alert-danger">{{ errorMessage }}</div>
</div>
<div *ngIf="(currentSession | async) == null">
<a href="#/login"><span class="fa fa-lock"></span>Войти</a><br/>
<a href="#/registration">Зарегистрироваться</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,9 @@
<button class="nav-link bi bi-person-fill" ngbDropdownToggle title="Пользователь"></button>
<div ngbDropdownMenu *ngIf="currentSession | async as session">
<div class="user-info">
<div class="user-fio">{{session.fullUserName}}</div>
<div class="user-login">{{session.username}}</div>
<div class="user-department">{{getOrgUnitName()}}</div>
</div>
<button ngbDropdownItem *ngIf="isLogoutButtonVisible()" (click)="logout()">Выход</button>
</div>

View file

@ -0,0 +1,51 @@
<div class="form-signin">
<form #formComponent="ngForm">
<div class="alert alert-success" [hidden]="!confirmationSent">На ваш почтовый адрес было
отправлено письмо. Подтвердите почту, чтобы войти в систему
</div>
<div class="alert alert-danger" [hidden]="!errorMessage">{{errorMessage}}</div>
<h2>Вход</h2>
<div class="row registration">
<span>Еще нет аккаунта?</span><a href="#/registration">Зарегистрироваться</a>
</div>
<div class="row">
<label>Пользователь</label>
<div class="input-group">
<input type="text" name="username" class="form-control" placeholder="Пользователь" required autofocus
[(ngModel)]="username" maxlength="100">
</div>
</div>
<div class="row">
<label>Пароль</label>
<div class="input-group">
<input
[type]="passwordType ? 'text' : 'password'"
name="password"
class="form-control field-password-view"
placeholder="" required
[(ngModel)]="password"
maxlength="100"
>
<div class="input-group-append">
<span class="input-group-text">
<i
(click)="togglePasswordType()"
class="fa"
[ngClass]="{
'fa-eye': passwordType,
'fa-eye-slash': !passwordType
}"
></i>
</span>
</div>
</div>
</div>
<div class="login-btn-box">
<!--<esia-login-button></esia-login-button>-->
<button type="submit" class="btn btn-primary" (click)="formComponent.form.valid && login()">Войти</button>
<div class="password"><a href="#/reset-password">Забыли пароль?</a></div>
</div>
</form>
</div>

View file

@ -0,0 +1,103 @@
<div class="form-signup">
<div class="form-logo">
<div></div>
</div>
<div class="form-new-password">
<form #formComponent="ngForm">
<div [hidden]="!errorMessage" class="alert alert-danger">{{ errorMessage }}</div>
<p class="has-account">Вспомнили пароль?
<a href="#/login"><span class="fa fa-lock"></span>Войти</a></p>
<p class="has-account">Задайте новый пароль</p>
<div class="row">
<label>Пароль</label>
<div class="input-group">
<input
#passwordInput="ngModel"
[(ngModel)]="password"
[type]="passwordType ? 'text' : 'password'"
class="form-control"
maxlength="32"
minlength="6"
name="password"
pattern="^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$"
required
(change)="validPasswords()"
>
<div class="input-group-append">
<span class="input-group-text">
<i
(click)="togglePasswordType()"
class="fa"
[ngClass]="{
'fa-eye': passwordType,
'fa-eye-slash': !passwordType
}"
></i>
</span>
</div>
</div>
<div *ngIf="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
<div *ngIf="passwordInput.errors.required" class="msg-alert">Поле обязательно
</div>
<div *ngIf="passwordInput.errors.minlength" class="msg-alert">Пароль должен
содержать как минимум 6 символов
</div>
<div *ngIf="passwordInput.errors.pattern" class="msg-alert">Пароль должен
содержать заглавные и прописные буквы и как минимум 1 цифру
</div>
</div>
</div>
<div class="row">
<label>Подтверждение пароля</label>
<div class="input-group">
<input
#confirmPasswordInput="ngModel"
[(ngModel)]="confirmPassword"
[type]="confirmPasswordType ? 'text' : 'password'"
class="form-control"
maxlength="32"
minlength="6"
name="confirmPassword"
pattern="^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$"
required
(change)="validPasswords()"
>
<div class="input-group-append">
<span class="input-group-text">
<i
(click)="toggleConfirmPasswordType()"
class="fa"
[ngClass]="{
'fa-eye': confirmPasswordType,
'fa-eye-slash': !confirmPasswordType
}"
></i>
</span>
</div>
</div>
<div *ngIf="confirmPasswordInput.invalid && (confirmPasswordInput.dirty || confirmPasswordInput.touched)">
<div *ngIf="confirmPasswordInput.errors.required" class="msg-alert">Поле обязательно
</div>
<div *ngIf="confirmPasswordInput.errors.minlength" class="msg-alert">Пароль должен
содержать как минимум 6 символов
</div>
<div *ngIf="confirmPasswordInput.errors.pattern" class="msg-alert">Пароль должен
содержать заглавные и прописные буквы и как минимум 1 цифру
</div>
</div>
</div>
<div class="reset-password-btn-box">
<button
(click)="formComponent.form.valid && validPasswords() && changePassword()"
[disabled]="!formComponent.form.valid && !validPasswords()"
class="btn btn-primary"
type="submit"
>
Изменить пароль
</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
<div ngbDropdown class="nav-item">
<button class="nav-link bi bi-clipboard-plus-fill" id="startProcessDropdownMenu" ngbDropdownToggle title="Создать"></button>
<div ngbDropdownMenu aria-labelledby="startProcessDropdownMenu">
<div class="dropdown-menu-inner">
<div *ngFor="let process of processList">
<button (click)="startProcess(process.processDefId)" ngbDropdownItem>
{{ process.name }}
</button>
</div>
</div>
<div>
</div><!-- TODO: move to directive or something else -->

View file

@ -0,0 +1,3 @@
<div class="modal-body">
<div class="progress"></div>
</div>

View file

@ -0,0 +1,121 @@
<div class="form-signup">
<div class="form-logo">
<div></div>
</div>
<div class="form-register">
<form #formComponent="ngForm">
<div [hidden]="!errorMessage" class="alert alert-danger">{{ errorMessage }}</div>
<h2>Регистрация</h2>
<p class="has-account">Уже зарегистрированы?
<a href="#/login"><span class="fa fa-lock"></span>Войти</a></p>
<div class="row">
<label>Имя</label>
<input
#name="ngModel"
[(ngModel)]="username"
class="form-control"
maxlength="100"
name="username"
required
type="text"
>
<div *ngIf="name.invalid && (name.dirty || name.touched)">
<div *ngIf="name.errors.required" class="msg-alert">Поле обязательно</div>
</div>
</div>
<div class="row">
<label>Адрес эл. почты</label>
<input
#emailInput="ngModel"
[(ngModel)]="email"
class="form-control"
email
maxlength="100"
name="email"
required
type="email"
>
<div *ngIf="emailInput.invalid && (emailInput.dirty || emailInput.touched)">
<div *ngIf="emailInput.errors.required" class="msg-alert">Поле обязательно</div>
<div *ngIf="emailInput.errors.email" class="msg-alert">Неверный формат адреса
эл. почты
</div>
</div>
</div>
<div class="row">
<label>Номер телефона</label>
<international-phone-number
#phoneInput="ngModel"
[(ngModel)]="phoneNumber"
[defaultCountry]="'ru'"
[pattern]="'^\\+(?!7 ?\\d{11})[0-9 ]+$'"
maxlength="20"
minlength="8"
name="phoneNumber"
placeholder="+79991112233"
(focusout)="phoneInputFocusOut()"
required
></international-phone-number>
<div *ngIf="phoneInput.invalid && (phoneInput.dirty || phoneIsTouched)">
<div *ngIf="phone.selectedCountry">
<div *ngIf="phoneHasOnlyDialCode()" class="msg-alert">Поле обязательно</div>
<div *ngIf="!phoneHasOnlyDialCode()" class="msg-alert">Введите корректный номер</div>
</div>
<div *ngIf="!phone.selectedCountry">
<div *ngIf="phoneInput.errors.required" class="msg-alert">Поле обязательно</div>
<div *ngIf="!phoneInput.errors.required" class="msg-alert">Введите код страны</div>
</div>
</div>
</div>
<div class="row">
<label>Пароль</label>
<div class="input-group">
<input
#passwordInput="ngModel"
[(ngModel)]="password"
[type]="fieldType ? 'text' : 'password'"
class="form-control"
maxlength="32"
minlength="8"
name="password"
[pattern]="passwordPattern"
required
>
<div class="input-group-append">
<span class="input-group-text">
<i
(click)="toggleFieldType()"
class="fa"
[ngClass]="{
'fa-eye': fieldType,
'fa-eye-slash': !fieldType
}"
></i>
</span>
</div>
</div>
<div *ngIf="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
<div *ngIf="passwordInput.errors.required" class="msg-alert">Поле обязательно
</div>
<div *ngIf="passwordInput.errors.minlength" class="msg-alert">Пароль должен
содержать как минимум 8 символов
</div>
<div *ngIf="passwordInput.errors.pattern" class="msg-alert" [innerText]="passwordPatternErrorMessage">
</div>
</div>
</div>
<div class="register-btn-box">
<button
(click)="formComponent.form.valid && register()"
[disabled]="!formComponent.form.valid"
class="btn btn-primary"
type="submit"
>
Зарегистрироваться
</button>
</div>
<div *ngIf="consent" [innerHTML]="consent" class="consent"></div>
</form>
</div>
</div>

View file

@ -0,0 +1,45 @@
<div class="form-signup">
<div class="form-logo">
<div></div>
</div>
<div class="form-reset-password">
<form #formComponent="ngForm">
<div [hidden]="!errorMessage" class="alert alert-danger">{{ errorMessage }}</div>
<p class="has-account">Вспомнили пароль?
<a href="#/login"><span class="fa fa-lock"></span>Войти</a></p>
<p class="has-account">Укажите адрес эл. почты, который был указан при регистрации,
на него пришлем временный пароль. Пароль сможете поменять в личном кабинете.
</p>
<div class="row">
<label>Адрес эл. почты</label>
<input
#emailInput="ngModel"
[(ngModel)]="email"
class="form-control"
email
maxlength="100"
name="email"
required
type="email"
>
<div *ngIf="emailInput.invalid && (emailInput.dirty || emailInput.touched)">
<div *ngIf="emailInput.errors.required" class="msg-alert">Поле обязательно</div>
<div *ngIf="emailInput.errors.email" class="msg-alert">Неверный формат адреса эл. почты
</div>
</div>
</div>
<div class="reset-password-btn-box">
<button
(click)="formComponent.form.valid && resetPassword()"
[disabled]="!formComponent.form.valid"
class="btn btn-primary"
type="submit"
>
Восстановить
</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div id="page">
<router-outlet></router-outlet>
</div>

View file

@ -0,0 +1,49 @@
<div class="inner">
<div class="task-list">
<div class="task-list-tree-panel">
<div class="task-list-filter">
<ul>
<li>Фильтры</li>
<li>
<label>
<input type="radio" name="task-filter" value="All" [(ngModel)]="showMode" (ngModelChange)="filterVisibleTasks()">
<span>Все [{{tasks.length}}]</span>
</label></li>
<li class="ontime">
<label>
<input type="radio" name="task-filter" value="OnTime" [(ngModel)]="showMode" (ngModelChange)="filterVisibleTasks()">
<span><div></div>Без превышения срока [{{onTimeTasks.length}}]</span>
</label></li>
<li class="overdue">
<label>
<input type="radio" name="task-filter" value="Overdue" [(ngModel)]="showMode" (ngModelChange)="filterVisibleTasks()">
<span><div></div>С превышением срока [{{overdueTasks.length}}]</span>
</label></li>
</ul>
</div>
</div>
<div class="task-list-workplace">
<div class="alert alert-danger" [hidden]="!errorMessage">{{errorMessage}}</div>
<div class="table task-tbl">
<div class="thead">
<div class="tr">
<div class="th">Процесс</div>
<div class="th">Версия</div>
<div class="th">Задача</div>
<div class="th">Дата создания</div>
<div class="th">Срок</div>
</div>
</div>
<div *ngFor="let task of visibleTasks"
class="tr" [ngClass]="{'task-overdue': isOverdue(task), 'task-ontime': isOnTime(task)}"
(click)="startTask(task)">
<div class="td">{{ task.processName }}</div>
<div class="td">{{ task.processVersion }}</div>
<div class="td task">{{ task.name }}</div>
<div class="td">{{ task.createdOn | date:'dd.MM.yyyy HH:mm:ss' }}</div>
<div class="td">{{ task.expirationTime | date:'dd.MM.yyyy HH:mm:ss' }}</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,8 @@
<div class="task-not-found-page">
<div class="task-not-found-container">
<div>:(</div>
<div><h2>Ошибка</h2>
Данная задача не существует.<br/>
Перейти к <a (click)="goToTaskList()" tabindex>списку задач</a></div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div id="page">
<router-outlet></router-outlet>
</div>

View file

@ -0,0 +1 @@
<router-outlet></router-outlet>

View file

@ -0,0 +1,6 @@
<div id="home">
<div class="inner">
<div class="task-list">
</div>
</div>
</div>

View file

@ -0,0 +1 @@
<div #pageContent></div>

View file

@ -0,0 +1,12 @@
<div class="wrapper">
<app-header *ngIf="headerVisible">
</app-header>
<div class="container">
<div class="container-inside" id="webbpm-angular-application-container">
<router-outlet></router-outlet>
</div>
</div>
<app-footer *ngIf="footerVisible">
</app-footer>
</div>

View file

@ -0,0 +1,8 @@
import "../../src/resources/css/style.css";
import {platformBrowser} from '@angular/platform-browser';
import {enableProdMode} from "@angular/core";
import {WebbpmModuleNgFactory} from "./modules/webbpm/webbpm.module.ngfactory";
window['dev_mode'] = false;
enableProdMode();
platformBrowser().bootstrapModuleFactory(WebbpmModuleNgFactory);

7
frontend/src/ts/main.ts Normal file
View file

@ -0,0 +1,7 @@
import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";
import {enableProdMode} from "@angular/core";
import {WebbpmModule} from "./modules/webbpm/webbpm.module";
window['dev_mode'] = true;
enableProdMode();
platformBrowserDynamic().bootstrapModule(WebbpmModule);

View file

@ -0,0 +1,72 @@
import {NgModule} from "@angular/core";
import {RouterModule, Routes} from "@angular/router";
import {AccessDeniedComponent} from "./component/access-denied.component";
import {LoginComponent} from "./component/login.component";
import {AuthenticationGuard, ConfirmExitGuard, SignedInGuard, ProcessInstanceRouteResolver} from "@webbpm/base-package";
import {RegisterComponent} from "./component/register.component";
import {ConfirmUserEmailComponent} from "./component/confirm-user-email.component";
import {ResetPasswordComponent} from "./component/reset-password.component";
import {NewPasswordComponent} from "./component/new-password.component";
import {TaskListComponent} from "./component/task-list.component";
const appRoutes: Routes = [
{
path: 'login',
component: LoginComponent,
canActivate: [SignedInGuard]
},
{
path: 'access-denied',
component: AccessDeniedComponent,
canActivate: [AuthenticationGuard, ConfirmExitGuard]
},
{
path: 'registration',
component: RegisterComponent,
canActivate: [SignedInGuard]
},
{
path: 'confirm',
component: ConfirmUserEmailComponent
},
{
path: 'reset-password',
component: ResetPasswordComponent
},
{
path: 'new-password',
component: NewPasswordComponent
},
{
path: 'process',
canActivate: [AuthenticationGuard, ConfirmExitGuard],
children: [
{
path: 'instance',
loadChildren: 'generated-sources/page-process-instance-list.module#PageprocessinstancelistModule',
resolve: {
processInstanceId: ProcessInstanceRouteResolver
}
},
{
path: 'instance/:processInstanceId',
loadChildren: 'generated-sources/page-process-instance.module#PageprocessinstanceModule',
resolve: {
processInstanceId: ProcessInstanceRouteResolver
}
},
{
path: 'tasks',
component: TaskListComponent,
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View file

@ -0,0 +1,80 @@
import {forwardRef, NgModule} from "@angular/core";
import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
import {CommonModule, registerLocaleData} from "@angular/common";
import localeRu from '@angular/common/locales/ru';
import {FormsModule} from "@angular/forms";
import {AgGridModule} from "ag-grid-angular";
import {
BpmnModule,
ComponentsModule,
CoreModule,
ProgressIndicationService,
SecurityModule
} from "@webbpm/base-package";
import {AdminMenuComponent} from "./component/admin-menu.component";
import {AppHeaderComponent} from "./component/app-header.component";
import {AppFooterComponent} from "./component/app-footer.component";
import {LogOutComponent} from "./component/logout.component";
import {LoginComponent} from "./component/login.component";
import {AccessDeniedComponent} from "./component/access-denied.component";
import {ApplicationVersionComponent} from "./component/application-version.component";
import {RouterModule} from "@angular/router";
import {RegisterComponent} from "./component/register.component";
import {ConfirmUserEmailComponent} from "./component/confirm-user-email.component";
import {InternationalPhoneNumberModule} from "ngx-international-phone-number";
import {ResetPasswordComponent} from "./component/reset-password.component";
import {NewPasswordComponent} from "./component/new-password.component";
import {AppProgressIndicationComponent} from "./component/app-progress-indication.component";
import {AppProgressIndicationService} from "./service/app-progress-indication.service";
import {TaskListComponent} from "./component/task-list.component";
import {ProcessListComponent} from "./component/process-list.component";
import {TaskComponent} from "./component/task.component";
import {TaskNotFoundComponent} from "./component/task-not-found.component";
registerLocaleData(localeRu);
export const DIRECTIVES = [
forwardRef(() => AppHeaderComponent),
forwardRef(() => AppFooterComponent),
forwardRef(() => AdminMenuComponent),
forwardRef(() => ApplicationVersionComponent),
forwardRef(() => LogOutComponent),
forwardRef(() => LoginComponent),
forwardRef(() => AccessDeniedComponent),
forwardRef(() => RegisterComponent),
forwardRef(() => ConfirmUserEmailComponent),
forwardRef(() => ResetPasswordComponent),
forwardRef(() => NewPasswordComponent),
forwardRef(() => AppProgressIndicationComponent),
forwardRef(() => TaskListComponent),
forwardRef(() => ProcessListComponent),
forwardRef(() => TaskComponent),
forwardRef(() => TaskNotFoundComponent)
];
@NgModule({
imports: [
CommonModule,
FormsModule,
CoreModule,
NgbModule,
BpmnModule,
SecurityModule,
ComponentsModule,
AgGridModule,
RouterModule,
InternationalPhoneNumberModule
],
declarations: [
DIRECTIVES
],
exports: [
DIRECTIVES
],
providers: [
{ provide: ProgressIndicationService, useClass: AppProgressIndicationService }
],
bootstrap: [],
entryComponents: [AppProgressIndicationComponent, TaskNotFoundComponent]
})
export class AppModule {
}

View file

@ -0,0 +1,11 @@
import {ChangeDetectionStrategy, Component} from "@angular/core";
@Component({
moduleId: module.id,
selector: "access-denied",
templateUrl: "../../../../../src/resources/template/app/component/access_denied.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AccessDeniedComponent {
}

View file

@ -0,0 +1,23 @@
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
import {UserService, Session} from "@webbpm/base-package";
import {NgbDropdownConfig, Placement} from "@ng-bootstrap/ng-bootstrap";
import {Observable} from "rxjs";
@Component({
moduleId: module.id,
selector: 'admin-menu',
templateUrl: '../../../../../src/resources/template/app/component/admin_menu.html',
providers: [NgbDropdownConfig],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AdminMenuComponent {
@Input()
public placement: Placement = 'bottom';
public currentSession: Observable<Session>;
constructor(protected userService: UserService, public config: NgbDropdownConfig) {
this.config.placement = this.placement;
this.currentSession = this.userService.getCurrentSession();
}
}

View file

@ -0,0 +1,9 @@
import {Component} from "@angular/core";
@Component({
moduleId: module.id,
selector: "app-footer",
templateUrl: "../../../../../src/resources/template/app/component/app_footer.html"
})
export class AppFooterComponent {
}

View file

@ -0,0 +1,24 @@
import {ChangeDetectionStrategy, Component} from "@angular/core";
import {Router} from "@angular/router";
import {UserService, Session} from "@webbpm/base-package";
import {Observable} from "rxjs";
@Component({
moduleId: module.id,
selector: "app-header",
templateUrl: "../../../../../src/resources/template/app/component/app_header.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppHeaderComponent {
public currentSession: Observable<Session>;
constructor(protected userService: UserService,
protected router: Router) {
this.currentSession = this.userService.getCurrentSession();
}
public openTaskList(): void {
this.router.navigateByUrl("/process/tasks");
}
}

View file

@ -0,0 +1,11 @@
import {ChangeDetectionStrategy, Component} from "@angular/core";
@Component({
moduleId: module.id,
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'progress-indication-dialog-content',
templateUrl: '../../../../../src/resources/template/app/component/progress-indication.html'
})
export class AppProgressIndicationComponent {
}

View file

@ -0,0 +1,24 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input} from "@angular/core";
import {HttpClient} from "@angular/common/http";
@Component({
moduleId: module.id,
selector: "application-version",
templateUrl: "../../../../../src/resources/template/app/component/application_version.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApplicationVersionComponent {
@Input()
public applicationVersion: string;
constructor(private httpClient: HttpClient, private cd: ChangeDetectorRef) {
this.loadAppVersion(); //TODO: check version url
}
private loadAppVersion() {
this.httpClient.get("version").toPromise().then((version: any) => {
this.applicationVersion = version.number;
this.cd.markForCheck();
})
}
}

View file

@ -0,0 +1,51 @@
import {ActivatedRoute, Router} from "@angular/router";
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input} from "@angular/core";
import {Session, UserService} from "@webbpm/base-package";
import {Observable} from "rxjs";
enum VerificationStatus {
VERIFYING = "VERIFYING",
VERIFIED = "VERIFIED",
FAILED = "FAILED"
}
@Component({
moduleId: module.id,
selector: "confirm",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "../../../../../src/resources/template/app/component/confirm-user-email.html"
})
export class ConfirmUserEmailComponent {
public verificationStatus = VerificationStatus.VERIFYING;
public currentSession: Observable<Session>;
@Input()
public errorMessage: string;
constructor(private router: Router, private userService: UserService,
private route: ActivatedRoute, private cd: ChangeDetectorRef) {
this.currentSession = this.userService.getCurrentSession();
}
ngOnInit() {
const link: string = this.route.snapshot.queryParamMap.get("link");
// remove link from url to prevent http referer leakage
this.router.navigate([], { relativeTo: this.route, replaceUrl: true });
this.userService.confirm(
link,
(reason) => {
this.verificationStatus = VerificationStatus.FAILED;
if (reason.status === 404) {
this.errorMessage = 'Ссылка недействительна. Требуется повторная регистрация.';
}
else {
this.errorMessage = 'Произошла ошибка, обратитесь в службу технической поддержки!';
}
this.cd.markForCheck();
})
.then(() => {
this.verificationStatus = VerificationStatus.VERIFIED;
this.cd.markForCheck();
});
}
}

View file

@ -0,0 +1,68 @@
import {Component, Input} from "@angular/core";
import {ActivatedRoute, Router} from "@angular/router";
import {UserService, Credentials} from "@webbpm/base-package";
@Component({
moduleId: module.id,
selector: "login",
templateUrl: "../../../../../src/resources/template/app/component/login.html"
})
export class LoginComponent {
@Input()
public username: string;
@Input()
public password: string;
public passwordType: boolean;
@Input()
public errorMessage: string;
@Input()
public confirmationSent: boolean;
constructor(private router: Router, private userService: UserService,
private route: ActivatedRoute) {
}
ngOnInit() {
this.confirmationSent = this.route.snapshot.queryParamMap.get('confirmationSent') === 'true';
this.router.navigate([], { relativeTo: this.route, replaceUrl: true });
}
public login(): void {
let credentials: Credentials = new Credentials();
credentials.username = this.username;
credentials.password = this.password;
this.userService.login(credentials, "Password")
.then(() => this.router.navigateByUrl("/"),
(reason: any) => {
switch (reason.status) {
case 401: {
this.errorMessage = "Неправильный логин или пароль";
break;
}
case 404: {
this.errorMessage = "Приложение стартует. Пожалуйста, подождите...";
break;
}
default: {
this.errorMessage =
"Произошла неизвестная ошибка, обратитесь в службу технической поддержки!";
break;
}
}
}
);
}
public goToRegister(): void {
this.router.navigateByUrl("/register");
}
togglePasswordType(): void {
this.passwordType = !this.passwordType;
}
}

View file

@ -0,0 +1,37 @@
import {Component} from "@angular/core";
import {UserService, Session, AuthenticationMethodService} from "@webbpm/base-package";
import {Observable} from "rxjs";
@Component({
moduleId: module.id,
selector: "[log-out]",
templateUrl: "../../../../../src/resources/template/app/component/log_out.html"
})
export class LogOutComponent {
public currentSession: Observable<Session>;
constructor(private userService: UserService, private authenticationMethodService: AuthenticationMethodService) {
this.currentSession = userService.getCurrentSession();
}
public logout(): void {
this.userService.logout();
}
public getCurrentUserName(): string {
return this.userService.getCurrentUserName();
}
public getFullUserName(): string {
return this.userService.getFullUserName();
}
public isLogoutButtonVisible(): boolean {
return this.authenticationMethodService.isFormAuth();
}
public getOrgUnitName(): string {
return this.userService.getOrgUnitName();
}
}

View file

@ -0,0 +1,79 @@
import {ActivatedRoute, Router} from "@angular/router";
import {Component, Input} from "@angular/core";
import {Session, UserPasswordResetRequestDto, UserService} from "@webbpm/base-package";
import {Observable} from "rxjs";
@Component({
moduleId: module.id,
selector: "newPassword",
templateUrl: "../../../../../src/resources/template/app/component/new_password.html"
})
export class NewPasswordComponent {
public currentSession: Observable<Session>;
private token: string;
@Input()
public password: string;
public passwordType: boolean;
@Input()
public confirmPassword: string;
public confirmPasswordType: boolean;
@Input()
public errorMessage: string;
constructor(private router: Router, private userService: UserService,
private route: ActivatedRoute) {
this.currentSession = this.userService.getCurrentSession();
}
ngOnInit() {
this.token = this.route.snapshot.queryParamMap.get("token");
this.router.navigate([], {relativeTo: this.route, replaceUrl: true});
if (this.token == undefined || this.token === '') {
this.errorMessage = 'Ссылка недействительна. Требуется повторить восстановление пароля.';
return;
}
}
public changePassword(): void {
let dto: UserPasswordResetRequestDto = new UserPasswordResetRequestDto();
dto.password = this.password;
dto.passwordConfirm = this.confirmPassword;
this.userService.changePassword(dto, this.token)
.then(() => this.router.navigateByUrl("/"),
() => {
this.errorMessage =
'Произошла неизвестная ошибка, обратитесь в службу технической поддержки!';
});
}
togglePasswordType(): void {
this.passwordType = !this.passwordType;
}
toggleConfirmPasswordType(): void {
this.confirmPasswordType = !this.confirmPasswordType;
}
validPasswords(): boolean {
if (this.password === undefined || this.confirmPassword === undefined) {
return false;
}
let eq = this.password === this.confirmPassword;
if (!eq) {
this.errorMessage = 'Введенные пароли не совпадают. Убедитесь, что данные, ' +
'введенные в поле "Подтверждение пароля", совпадают с теми, ' +
'которые указаны в поле "Пароль".';
}
else {
this.errorMessage = '';
}
return eq;
}
}

View file

@ -0,0 +1,44 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input} from "@angular/core";
import {Process, TaskService, ProcessDefinitionResource, ProcessService} from "@webbpm/base-package";
@Component({
moduleId: module.id,
selector: 'process',
templateUrl: '../../../../../src/resources/template/app/component/process_list.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProcessListComponent {
@Input()
public processList: Process[];
@Input()
public errorMessage: string;
constructor(private processDefinitionResource: ProcessDefinitionResource,
private taskService: TaskService,
private cd: ChangeDetectorRef,
private processService: ProcessService) {
this.processList = [];
this.loadProcessList();
}
loadProcessList() {
this.processDefinitionResource.list().then(
(processList) => {
this.processList = processList;
this.cd.markForCheck();
},
() => {
this.errorMessage = "Error load process list";
this.cd.markForCheck();
})
}
startProcess(processDefinitionId: string) {
this.processService.start(processDefinitionId, null).then(
(processInstanceId: number) => {
this.taskService.startAndOpenNextTask(processInstanceId);
}
);
}
}

View file

@ -0,0 +1,71 @@
import {Component, Input, ViewChild} from "@angular/core";
import {AppConfigService, UserDto, UserService} from "@webbpm/base-package";
import {Router} from "@angular/router";
import {PhoneNumberComponent} from "ngx-international-phone-number";
@Component({
moduleId: module.id,
selector: "register",
templateUrl: "../../../../../src/resources/template/app/component/register.html"
})
export class RegisterComponent {
public passwordPattern: string;
public passwordPatternErrorMessage: string;
public errorMessage: string;
@Input()
public username: string;
@Input()
public email: string;
@ViewChild(PhoneNumberComponent)
public phone: PhoneNumberComponent;
public phoneNumber: string;
public phoneIsTouched: boolean = false;
@Input()
public password: string;
public fieldType: boolean;
@Input()
public consent: string;
constructor(private router: Router, private userService: UserService,private appConfigService: AppConfigService) {
this.passwordPattern = appConfigService.getParamValue("password_pattern");
this.passwordPatternErrorMessage = appConfigService.getParamValue("password_pattern_error");
}
public register(): void {
let user: UserDto = new UserDto();
user.username = this.username;
user.email = this.email;
user.name = this.username;
user.phone = this.phone.value;
user.password = this.password;
this.userService.register(user)
.then(() => this.router.navigateByUrl("/login?confirmationSent=true"),
(reason: any) => {
if (reason.status === 409) {
this.errorMessage = 'Пользователь с данным почтовым адресом уже существует';
}
else {
this.errorMessage = 'Произошла неизвестная ошибка, обратитесь в службу технической поддержки!';
}
});
}
toggleFieldType(): void {
this.fieldType = !this.fieldType;
}
phoneHasOnlyDialCode(): boolean {
return this.phone.phoneNumber.trim() === this.phone.getSelectedCountryDialCode().trim()
}
phoneInputFocusOut(): void {
this.phoneIsTouched = true;
}
}

View file

@ -0,0 +1,30 @@
import {Component, Input} from "@angular/core";
import {UserService} from "@webbpm/base-package";
import {Router} from "@angular/router";
@Component({
moduleId: module.id,
selector: "resetPassword",
templateUrl: "../../../../../src/resources/template/app/component/reset_password.html"
})
export class ResetPasswordComponent {
@Input()
public email: string;
@Input()
public errorMessage: string;
constructor(private router: Router, private userService: UserService) {
}
resetPassword(): void {
this.userService.resetPassword(this.email)
.then(() => this.router.navigateByUrl("/"),
(reason: any) => {
this.errorMessage =
'Произошла неизвестная ошибка, обратитесь в службу технической поддержки!';
});
}
}

View file

@ -0,0 +1,121 @@
import {Component, Input} from "@angular/core";
import {Task, TaskService, TaskStatus, TaskReference, TaskResource, ProcessVariable, UserService, RolesService, ProcessService} from "@webbpm/base-package";
@Component({
moduleId: module.id,
selector: "task-list",
templateUrl: "../../../../../src/resources/template/app/component/task_list.html"
})
export class TaskListComponent {
@Input()
public tasks: Task[];
@Input()
public onTimeTasks: Task[];
@Input()
public overdueTasks: Task[];
@Input()
public visibleTasks: Task[];
@Input()
public showMode: string;
@Input()
public errorMessage: string;
@Input()
public roles: string[];
constructor(private taskService: TaskService,
private $taskResource: TaskResource,
private userService: UserService,
private rolesService: RolesService,
private processService: ProcessService) {
this.tasks = [];
this.onTimeTasks = [];
this.overdueTasks = [];
this.visibleTasks = [];
this.roles = [];
this.showMode = 'All';
this.rolesService.getRoles().then((roles: string[]) => {
this.roles = roles;
});
this.loadTasks();
}
private loadTasks(): void {
this.$taskResource.list()
.then(
(tasks: Task[]) => this.initTasks(tasks),
() => this.errorMessage = "Error load tasks"
);
}
public startTask(task: Task): Promise<any> {
let taskRef: TaskReference = new TaskReference();
taskRef.processInstanceId = task.processInstanceId;
taskRef.taskId = task.id;
if (task.status == TaskStatus.InProgress) {
return this.taskService.openTask(taskRef);
}
else if (task.status == TaskStatus.Reserved || task.status == TaskStatus.Ready) {
return this.taskService.startAndOpenTask(taskRef);
}
}
private initTasks(tasks: Task[]): void {
this.tasks = tasks;
this.filterOnTimeTasks();
this.filterOverdueTasks();
this.filterVisibleTasks();
}
public isVisible(task: Task): boolean {
return this.showMode == 'All' ||
this.showMode == 'OnTime' && !this.isOverdue(task) ||
this.showMode == 'Overdue' && this.isOverdue(task);
}
public isOverdue(task: Task): boolean {
if (!task.expirationTime) {
return false;
}
return new Date(task.expirationTime).getTime() <= new Date().getTime();
}
public isOnTime(task: Task): boolean {
return !this.isOverdue(task);
}
public startProcess(processDefinitionId: string, processVars?: Array<ProcessVariable | any>) {
this.processService.start(processDefinitionId, processVars).then(
(processInstanceId: number) => {
this.taskService.startAndOpenNextTask(processInstanceId);
}
);
}
public filterOnTimeTasks() {
this.onTimeTasks = this.tasks
.filter((task: Task) => this.isOnTime(task));
}
public filterOverdueTasks() {
this.overdueTasks = this.tasks
.filter((task: Task) => this.isOverdue(task));
}
public filterVisibleTasks() {
this.visibleTasks = this.tasks
.filter((task: Task) => this.isVisible(task));
}
public hasRole(role: string) {
return this.roles.includes(role);
}
}

View file

@ -0,0 +1,18 @@
import {ChangeDetectionStrategy, Component} from "@angular/core";
import {Router} from "@angular/router";
@Component({
moduleId: module.id,
selector: 'task-not-found',
templateUrl: '../../../../../src/resources/template/app/component/task_not_found.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TaskNotFoundComponent {
constructor(private router: Router) {
}
goToTaskList() {
return this.router.navigateByUrl("/process/tasks");
}
}

Some files were not shown because too many files have changed in this diff Show more