Compare commits
No commits in common. "c05f9e8fbd0a828675a7420dc7149e2ac24aa346" and "64162a1c580229c7fe1d56bc47a20d02bd6918e0" have entirely different histories.
c05f9e8fbd
...
64162a1c58
|
@ -0,0 +1,38 @@
|
||||||
|
####################################### Build stage #######################################
|
||||||
|
FROM maven:3.9-eclipse-temurin-21-alpine AS build-stage
|
||||||
|
|
||||||
|
ARG MAVEN_ACCOUNT_USR
|
||||||
|
ARG MAVEN_ACCOUNT_PSW
|
||||||
|
ARG REVISION
|
||||||
|
ARG PROFILE
|
||||||
|
ENV server_username=$MAVEN_ACCOUNT_USR
|
||||||
|
ENV server_password=$MAVEN_ACCOUNT_PSW
|
||||||
|
ARG CITE_MAVEN_REPO_URL
|
||||||
|
|
||||||
|
COPY pom.xml /build/
|
||||||
|
COPY notification /build/notification/
|
||||||
|
COPY notification-web /build/notification-web/
|
||||||
|
COPY settings.xml /root/.m2/settings.xml
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/config/app.env
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/config/*-devel.yml
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/logging/*.xml
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/certificates/*.crt
|
||||||
|
|
||||||
|
WORKDIR /build/
|
||||||
|
|
||||||
|
RUN mvn -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} clean
|
||||||
|
RUN mvn -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} install
|
||||||
|
# Build project
|
||||||
|
RUN mvn -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} package
|
||||||
|
|
||||||
|
######################################## Run Stage ########################################
|
||||||
|
FROM eclipse-temurin:21-jre-ubi9-minimal
|
||||||
|
|
||||||
|
ARG PROFILE
|
||||||
|
ARG REVISION
|
||||||
|
ENV SERVER_PORT=8080
|
||||||
|
EXPOSE ${SERVER_PORT}
|
||||||
|
|
||||||
|
COPY --from=build-stage /build/notification-web/target/notification-web-${REVISION}.jar /app/notification-web.jar
|
||||||
|
|
||||||
|
ENTRYPOINT ["java","-Dspring.config.additional-location=file:/config/","-Dspring.profiles.active=${PROFILE}","-Djava.security.egd=file:/dev/./urandom","-jar","/app/notification-web.jar"]
|
|
@ -0,0 +1,30 @@
|
||||||
|
####################################### Build stage #######################################
|
||||||
|
FROM maven:3.9-eclipse-temurin-21-alpine
|
||||||
|
|
||||||
|
ARG MAVEN_ACCOUNT_USR
|
||||||
|
ARG MAVEN_ACCOUNT_PSW
|
||||||
|
ARG REVISION
|
||||||
|
ARG PROFILE
|
||||||
|
ARG ORACLE_URL
|
||||||
|
ARG ORACLE_TOKEN
|
||||||
|
ENV server_username=$MAVEN_ACCOUNT_USR
|
||||||
|
ENV server_password=$MAVEN_ACCOUNT_PSW
|
||||||
|
ARG CITE_MAVEN_REPO_URL
|
||||||
|
|
||||||
|
COPY pom.xml /build/
|
||||||
|
COPY notification /build/notification/
|
||||||
|
COPY notification-web /build/notification-web/
|
||||||
|
COPY settings.xml /root/.m2/settings.xml
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/config/app.env
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/config/*-devel.yml
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/logging/*.xml
|
||||||
|
RUN rm -f /build/notification-web/src/main/resources/certificates/*.crt
|
||||||
|
|
||||||
|
COPY oracle.local.cite.gr.crt $JAVA_HOME/conf/security
|
||||||
|
RUN cd "$JAVA_HOME"/conf/security && keytool -cacerts -storepass changeit -noprompt -trustcacerts -importcert -alias oraclecert -file oracle.local.cite.gr.crt
|
||||||
|
|
||||||
|
WORKDIR /build/
|
||||||
|
RUN mvn -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} clean
|
||||||
|
RUN mvn -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} install
|
||||||
|
RUN mvn -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} package
|
||||||
|
RUN mvn sonar:sonar -Drevision=${REVISION} -DciteMavenRepoUrl=${CITE_MAVEN_REPO_URL} -P${PROFILE} -Dsonar.projectKey=OpenDMP:notification-api -Dsonar.login=${ORACLE_TOKEN} -Dsonar.host.url=${ORACLE_URL} -Dsonar.projectName='OpenDMP Notification API'
|
|
@ -0,0 +1,33 @@
|
||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?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>gr.cite</groupId>
|
||||||
|
<artifactId>notification-service-parent</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>notification-web</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<revision>1.0.0-SNAPSHOT</revision>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.orm</groupId>
|
||||||
|
<artifactId>hibernate-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>gr.cite</groupId>
|
||||||
|
<artifactId>notification</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>gr.cite</groupId>
|
||||||
|
<artifactId>oidc-authz</artifactId>
|
||||||
|
<version>2.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>gr.cite</groupId>
|
||||||
|
<artifactId>exceptions-web</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>gr.cite</groupId>
|
||||||
|
<artifactId>cors-web</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,26 @@
|
||||||
|
package gr.cite.notification.web;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
|
@SpringBootApplication(
|
||||||
|
scanBasePackages = {
|
||||||
|
"gr.cite.notification.*",
|
||||||
|
"gr.cite",
|
||||||
|
"gr.cite.queueoutbox",
|
||||||
|
"gr.cite.queueinbox",
|
||||||
|
"gr.cite.notification.integrationevent",
|
||||||
|
"gr.cite.tools",
|
||||||
|
"gr.cite.commons"})
|
||||||
|
@EntityScan({
|
||||||
|
"gr.cite.notification.data"})
|
||||||
|
@EnableAsync
|
||||||
|
public class NotificationApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(NotificationApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package gr.cite.notification.web;
|
||||||
|
|
||||||
|
import gr.cite.notification.web.scope.tenant.TenantInterceptor;
|
||||||
|
import gr.cite.notification.web.scope.tenant.TenantScopeClaimInterceptor;
|
||||||
|
import gr.cite.notification.web.scope.tenant.TenantScopeHeaderInterceptor;
|
||||||
|
import gr.cite.notification.web.scope.user.UserInterceptor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfiguration implements WebMvcConfigurer {
|
||||||
|
private final TenantInterceptor tenantInterceptor;
|
||||||
|
private final TenantScopeHeaderInterceptor scopeHeaderInterceptor;
|
||||||
|
private final TenantScopeClaimInterceptor scopeClaimInterceptor;
|
||||||
|
private final UserInterceptor userInterceptor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public WebConfiguration(
|
||||||
|
TenantInterceptor tenantInterceptor,
|
||||||
|
TenantScopeHeaderInterceptor scopeHeaderInterceptor,
|
||||||
|
TenantScopeClaimInterceptor scopeClaimInterceptor,
|
||||||
|
UserInterceptor userInterceptor
|
||||||
|
) {
|
||||||
|
this.tenantInterceptor = tenantInterceptor;
|
||||||
|
this.scopeHeaderInterceptor = scopeHeaderInterceptor;
|
||||||
|
this.scopeClaimInterceptor = scopeClaimInterceptor;
|
||||||
|
this.userInterceptor = userInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
int order = 1;
|
||||||
|
registry.addWebRequestInterceptor(scopeHeaderInterceptor).order(order++);
|
||||||
|
registry.addWebRequestInterceptor(scopeClaimInterceptor).order(order++);
|
||||||
|
registry.addWebRequestInterceptor(userInterceptor).order(order++);
|
||||||
|
registry.addWebRequestInterceptor(tenantInterceptor).order(order++);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package gr.cite.notification.web.authorization;
|
||||||
|
|
||||||
|
import gr.cite.commons.web.authz.handler.AuthorizationHandler;
|
||||||
|
import gr.cite.commons.web.authz.handler.AuthorizationHandlerContext;
|
||||||
|
import gr.cite.commons.web.authz.policy.AuthorizationRequirement;
|
||||||
|
import gr.cite.commons.web.oidc.principal.MyPrincipal;
|
||||||
|
import gr.cite.notification.authorization.OwnedAuthorizationRequirement;
|
||||||
|
import gr.cite.notification.authorization.OwnedResource;
|
||||||
|
import gr.cite.notification.common.scope.user.UserScope;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component("ownedAuthorizationHandler")
|
||||||
|
public class OwnedAuthorizationHandler extends AuthorizationHandler<OwnedAuthorizationRequirement> {
|
||||||
|
|
||||||
|
private final UserScope userScope;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public OwnedAuthorizationHandler(UserScope userScope) {
|
||||||
|
this.userScope = userScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int handleRequirement(AuthorizationHandlerContext context, Object resource, AuthorizationRequirement requirement) {
|
||||||
|
OwnedAuthorizationRequirement req = (OwnedAuthorizationRequirement) requirement;
|
||||||
|
|
||||||
|
OwnedResource rs = (OwnedResource) resource;
|
||||||
|
|
||||||
|
boolean isAuthenticated = ((MyPrincipal) context.getPrincipal()).isAuthenticated();
|
||||||
|
if (!isAuthenticated) return ACCESS_NOT_DETERMINED;
|
||||||
|
|
||||||
|
if (this.userScope.getUserIdSafe() == null) return ACCESS_NOT_DETERMINED;
|
||||||
|
|
||||||
|
if (rs != null && rs.getUserIds() != null && rs.getUserIds().contains(this.userScope.getUserIdSafe())) return ACCESS_GRANTED;
|
||||||
|
|
||||||
|
return ACCESS_NOT_DETERMINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends AuthorizationRequirement> supporting() {
|
||||||
|
return OwnedAuthorizationRequirement.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package gr.cite.notification.web.config;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
||||||
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AppMessageSourceConfiguration {
|
||||||
|
@Bean
|
||||||
|
public MessageSource messageSource() {
|
||||||
|
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||||
|
messageSource.setBasename("classpath:messages/messages");
|
||||||
|
messageSource.setDefaultEncoding("UTF-8");
|
||||||
|
return messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocalValidatorFactoryBean getValidator() {
|
||||||
|
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
|
||||||
|
bean.setValidationMessageSource(messageSource());
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package gr.cite.notification.web.config;
|
||||||
|
|
||||||
|
import gr.cite.commons.web.authz.handler.AuthorizationHandler;
|
||||||
|
import gr.cite.commons.web.authz.handler.PermissionClientAuthorizationHandler;
|
||||||
|
import gr.cite.commons.web.authz.policy.AuthorizationRequirement;
|
||||||
|
import gr.cite.commons.web.authz.policy.AuthorizationRequirementMapper;
|
||||||
|
import gr.cite.commons.web.authz.policy.AuthorizationResource;
|
||||||
|
import gr.cite.commons.web.authz.policy.resolver.AuthorizationPolicyConfigurer;
|
||||||
|
import gr.cite.commons.web.authz.policy.resolver.AuthorizationPolicyResolverStrategy;
|
||||||
|
import gr.cite.commons.web.oidc.configuration.WebSecurityProperties;
|
||||||
|
import gr.cite.notification.authorization.OwnedAuthorizationRequirement;
|
||||||
|
import gr.cite.notification.authorization.OwnedResource;
|
||||||
|
import gr.cite.notification.web.authorization.OwnedAuthorizationHandler;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
private final WebSecurityProperties webSecurityProperties;
|
||||||
|
private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
|
||||||
|
private final Filter apiKeyFilter;
|
||||||
|
private final OwnedAuthorizationHandler ownedAuthorizationHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public SecurityConfiguration(WebSecurityProperties webSecurityProperties,
|
||||||
|
@Qualifier("tokenAuthenticationResolver") AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver,
|
||||||
|
@Qualifier("apiKeyFilter") Filter apiKeyFilter,
|
||||||
|
@Qualifier("ownedAuthorizationHandler") OwnedAuthorizationHandler ownedAuthorizationHandler) {
|
||||||
|
this.webSecurityProperties = webSecurityProperties;
|
||||||
|
this.authenticationManagerResolver = authenticationManagerResolver;
|
||||||
|
this.apiKeyFilter = apiKeyFilter;
|
||||||
|
this.ownedAuthorizationHandler = ownedAuthorizationHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
HttpSecurity tempHttp = http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.cors(httpSecurityCorsConfigurer -> {})
|
||||||
|
.headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||||
|
.addFilterBefore(apiKeyFilter, AbstractPreAuthenticatedProcessingFilter.class)
|
||||||
|
.authorizeHttpRequests(authRequest ->
|
||||||
|
authRequest.requestMatchers(buildAntPatterns(webSecurityProperties.getAllowedEndpoints())).anonymous()
|
||||||
|
.requestMatchers(buildAntPatterns(webSecurityProperties.getAuthorizedEndpoints())).authenticated())
|
||||||
|
.sessionManagement( sessionManagementConfigurer-> sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.NEVER))
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
|
||||||
|
return tempHttp.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
AuthorizationPolicyConfigurer authorizationPolicyConfigurer() {
|
||||||
|
return new AuthorizationPolicyConfigurer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthorizationPolicyResolverStrategy strategy() {
|
||||||
|
return AuthorizationPolicyResolverStrategy.STRICT_CONSENSUS_BASED;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Here you can register your custom authorization handlers, which will get used as well as the existing ones
|
||||||
|
//This is optional and can be omitted
|
||||||
|
//If not set / set to null, only the default authorization handlers will be used
|
||||||
|
@Override
|
||||||
|
public List<AuthorizationHandler<? extends AuthorizationRequirement>> addCustomHandlers() {
|
||||||
|
return List.of(ownedAuthorizationHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Here you can register your custom authorization requirements (if any)
|
||||||
|
//This is optional and can be omitted
|
||||||
|
//If not set / set to null, only the default authorization requirements will be used
|
||||||
|
@Override
|
||||||
|
public List<? extends AuthorizationRequirement> extendRequirements() {
|
||||||
|
return List.of(
|
||||||
|
// new TimeOfDayAuthorizationRequirement(new TimeOfDay("08:00","16:00"), true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Here you can select handlers you want to disable by providing the classes they are implemented by
|
||||||
|
//You can disable any handler (including any custom one)
|
||||||
|
//This is optional and can be omitted
|
||||||
|
//If not set / set to null, all the handlers will be invoked, based on their requirement support
|
||||||
|
//In the example below, the default client handler will be ignored by the resolver
|
||||||
|
@Override
|
||||||
|
public List<Class<? extends AuthorizationHandler<? extends AuthorizationRequirement>>> disableHandlers() {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
AuthorizationRequirementMapper authorizationRequirementMapper() {
|
||||||
|
return new AuthorizationRequirementMapper() {
|
||||||
|
@Override
|
||||||
|
public AuthorizationRequirement map(AuthorizationResource resource, boolean matchAll, String[] permissions) {
|
||||||
|
Class<?> type = resource.getClass();
|
||||||
|
if (!AuthorizationResource.class.isAssignableFrom(type)) throw new IllegalArgumentException("resource");
|
||||||
|
|
||||||
|
if (OwnedResource.class.equals(type)) {
|
||||||
|
return new OwnedAuthorizationRequirement();
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("resource");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] buildAntPatterns(Set<String> endpoints) {
|
||||||
|
if (endpoints == null) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
return endpoints.stream()
|
||||||
|
.filter(endpoint -> endpoint != null && !endpoint.isBlank())
|
||||||
|
.map(endpoint -> "/" + stripUnnecessaryCharacters(endpoint) + "/**")
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripUnnecessaryCharacters(String endpoint) {
|
||||||
|
endpoint = endpoint.strip();
|
||||||
|
if (endpoint.startsWith("/")) {
|
||||||
|
endpoint = endpoint.substring(1);
|
||||||
|
}
|
||||||
|
if (endpoint.endsWith("/")) {
|
||||||
|
endpoint = endpoint.substring(0, endpoint.length() - 1);
|
||||||
|
}
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
package gr.cite.notification.web.controllerhandler;
|
||||||
|
|
||||||
|
import gr.cite.notification.common.JsonHandlingService;
|
||||||
|
import gr.cite.tools.exception.*;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(GlobalExceptionHandler.class));
|
||||||
|
|
||||||
|
private final JsonHandlingService jsonHandlingService;
|
||||||
|
|
||||||
|
public GlobalExceptionHandler(JsonHandlingService jsonHandlingService) {
|
||||||
|
this.jsonHandlingService = jsonHandlingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<?> handleUnexpectedErrors(Exception exception, WebRequest request) throws Exception {
|
||||||
|
HandledException handled = this.handleException(exception, request);
|
||||||
|
this.log(handled.getLevel(), exception, MessageFormat.format("returning code {0} and payload {1}", handled.getStatusCode(), handled.getMessage()));
|
||||||
|
return new ResponseEntity<>(handled.getMessage(), handled.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void log(System.Logger.Level level, Exception e, String message) {
|
||||||
|
if (level != null) {
|
||||||
|
switch (level) {
|
||||||
|
case TRACE:
|
||||||
|
logger.trace(message, e);
|
||||||
|
break;
|
||||||
|
case DEBUG:
|
||||||
|
logger.debug(message, e);
|
||||||
|
break;
|
||||||
|
case INFO:
|
||||||
|
logger.info(message, e);
|
||||||
|
break;
|
||||||
|
case WARNING:
|
||||||
|
logger.warn(message, e);
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
logger.error(message, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HandledException handleException(Exception exception, WebRequest request) throws Exception {
|
||||||
|
HttpStatus statusCode;
|
||||||
|
Map<String, Object> result;
|
||||||
|
System.Logger.Level logLevel;
|
||||||
|
|
||||||
|
switch (exception){
|
||||||
|
case MyNotFoundException myNotFoundException -> {
|
||||||
|
logLevel = System.Logger.Level.DEBUG;
|
||||||
|
statusCode = HttpStatus.NOT_FOUND;
|
||||||
|
int code = myNotFoundException.getCode();
|
||||||
|
if (code > 0) {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("code", code),
|
||||||
|
Map.entry("error", myNotFoundException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("error", myNotFoundException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case MyUnauthorizedException myUnauthorizedException -> {
|
||||||
|
logLevel = System.Logger.Level.DEBUG;
|
||||||
|
statusCode = HttpStatus.UNAUTHORIZED;
|
||||||
|
int code = myUnauthorizedException.getCode();
|
||||||
|
if (code > 0) {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("code", code),
|
||||||
|
Map.entry("error", myUnauthorizedException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("error", myUnauthorizedException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case MyForbiddenException myForbiddenException -> {
|
||||||
|
logLevel = System.Logger.Level.DEBUG;
|
||||||
|
statusCode = HttpStatus.FORBIDDEN;
|
||||||
|
int code = myForbiddenException.getCode();
|
||||||
|
if (code > 0) {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("code", code),
|
||||||
|
Map.entry("error", myForbiddenException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("error", myForbiddenException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case MyValidationException myValidationException -> {
|
||||||
|
logLevel = System.Logger.Level.DEBUG;
|
||||||
|
statusCode = HttpStatus.BAD_REQUEST;
|
||||||
|
int code = myValidationException.getCode();
|
||||||
|
if (code > 0) {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("code", code),
|
||||||
|
Map.entry("error", myValidationException.getMessage()),
|
||||||
|
Map.entry("message", myValidationException.getErrors())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("error", myValidationException.getMessage()),
|
||||||
|
Map.entry("message", myValidationException.getErrors())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case MyApplicationException myApplicationException -> {
|
||||||
|
logLevel = System.Logger.Level.ERROR;
|
||||||
|
statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
int code = myApplicationException.getCode();
|
||||||
|
if (code > 0) {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("code", code),
|
||||||
|
Map.entry("error", myApplicationException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("error", myApplicationException.getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
logLevel = System.Logger.Level.ERROR;
|
||||||
|
statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
result = Map.ofEntries(
|
||||||
|
Map.entry("error", "System error")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String serialization = this.jsonHandlingService.toJsonSafe(result);
|
||||||
|
return new HandledException(statusCode, serialization, logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HandledException{
|
||||||
|
public HttpStatus statusCode;
|
||||||
|
public String message;
|
||||||
|
public System.Logger.Level level;
|
||||||
|
|
||||||
|
public HandledException(HttpStatus statusCode, String message, System.Logger.Level level) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.message = message;
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpStatus getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusCode(HttpStatus statusCode) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public System.Logger.Level getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLevel(System.Logger.Level level) {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
package gr.cite.notification.web.controllers;
|
||||||
|
|
||||||
|
import gr.cite.notification.audit.AuditableAction;
|
||||||
|
import gr.cite.notification.authorization.AuthorizationFlags;
|
||||||
|
import gr.cite.notification.common.enums.IsActive;
|
||||||
|
import gr.cite.notification.common.enums.NotificationInAppTracking;
|
||||||
|
import gr.cite.notification.common.scope.user.UserScope;
|
||||||
|
import gr.cite.notification.data.InAppNotificationEntity;
|
||||||
|
import gr.cite.notification.data.TenantEntityManager;
|
||||||
|
import gr.cite.notification.errorcode.ErrorThesaurusProperties;
|
||||||
|
import gr.cite.notification.model.InAppNotification;
|
||||||
|
import gr.cite.notification.model.Notification;
|
||||||
|
import gr.cite.notification.model.builder.InAppNotificationBuilder;
|
||||||
|
import gr.cite.notification.model.censorship.InAppNotificationCensor;
|
||||||
|
import gr.cite.notification.query.InAppNotificationQuery;
|
||||||
|
import gr.cite.notification.query.lookup.InAppNotificationLookup;
|
||||||
|
import gr.cite.notification.service.inappnotification.InAppNotificationService;
|
||||||
|
import gr.cite.notification.service.notification.NotificationService;
|
||||||
|
import gr.cite.notification.web.model.QueryResult;
|
||||||
|
import gr.cite.tools.auditing.AuditService;
|
||||||
|
import gr.cite.tools.data.builder.BuilderFactory;
|
||||||
|
import gr.cite.tools.data.censor.CensorFactory;
|
||||||
|
import gr.cite.tools.data.query.QueryFactory;
|
||||||
|
import gr.cite.tools.exception.MyApplicationException;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.exception.MyNotFoundException;
|
||||||
|
import gr.cite.tools.fieldset.BaseFieldSet;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "api/notification/inapp-notification")
|
||||||
|
public class InAppNotificationController {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(InAppNotificationController.class));
|
||||||
|
|
||||||
|
private final BuilderFactory builderFactory;
|
||||||
|
private final AuditService auditService;
|
||||||
|
private final CensorFactory censorFactory;
|
||||||
|
private final QueryFactory queryFactory;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final InAppNotificationService inAppNotificationService;
|
||||||
|
private final UserScope userScope;
|
||||||
|
private final ErrorThesaurusProperties errors;
|
||||||
|
private final TenantEntityManager tenantEntityManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public InAppNotificationController(BuilderFactory builderFactory,
|
||||||
|
AuditService auditService,
|
||||||
|
NotificationService notificationService, CensorFactory censorFactory,
|
||||||
|
QueryFactory queryFactory,
|
||||||
|
MessageSource messageSource,
|
||||||
|
InAppNotificationService inAppNotificationService, UserScope userScope, ErrorThesaurusProperties errors, TenantEntityManager tenantEntityManager) {
|
||||||
|
this.builderFactory = builderFactory;
|
||||||
|
this.auditService = auditService;
|
||||||
|
this.censorFactory = censorFactory;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.inAppNotificationService = inAppNotificationService;
|
||||||
|
this.userScope = userScope;
|
||||||
|
this.errors = errors;
|
||||||
|
this.tenantEntityManager = tenantEntityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("query")
|
||||||
|
public QueryResult<InAppNotification> Query(@RequestBody InAppNotificationLookup lookup) throws MyApplicationException, MyForbiddenException, InvalidApplicationException {
|
||||||
|
logger.debug("querying {}", InAppNotification.class.getSimpleName());
|
||||||
|
UUID userId = this.userScope.getUserId();
|
||||||
|
this.censorFactory.censor(InAppNotificationCensor.class).censor(lookup.getProject(), userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.tenantEntityManager.loadExplictTenantFilters();
|
||||||
|
|
||||||
|
if (userId == null) throw new MyForbiddenException(this.errors.getNonPersonPrincipal().getCode(), this.errors.getNonPersonPrincipal().getMessage());
|
||||||
|
InAppNotificationQuery query = lookup.enrich(this.queryFactory).disableTracking().userId(userId);
|
||||||
|
List<InAppNotificationEntity> data = query.collectAs(lookup.getProject());
|
||||||
|
List<InAppNotification> models = this.builderFactory.builder(InAppNotificationBuilder.class).build(lookup.getProject(), data);
|
||||||
|
long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size();
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.InApp_Notification_Query, "lookup", lookup);
|
||||||
|
|
||||||
|
return new QueryResult<>(models, count);
|
||||||
|
} finally {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public InAppNotification Get(@PathVariable UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + InAppNotification.class.getSimpleName()).And("id", id).And("fields", fieldSet));
|
||||||
|
UUID userId = this.userScope.getUserId();
|
||||||
|
this.censorFactory.censor(InAppNotificationCensor.class).censor(fieldSet, userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.tenantEntityManager.loadExplictTenantFilters();
|
||||||
|
|
||||||
|
InAppNotificationQuery query = this.queryFactory.query(InAppNotificationQuery.class).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission).userId(userId).ids(id);
|
||||||
|
InAppNotification model = this.builderFactory.builder(InAppNotificationBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.firstAs(fieldSet));
|
||||||
|
if (model == null)
|
||||||
|
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{id, Notification.class.getSimpleName()}, LocaleContextHolder.getLocale()));
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.InApp_Notification_Lookup, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("id", id),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
} finally {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/read")
|
||||||
|
@Transactional
|
||||||
|
public Boolean persist(@PathVariable UUID id) throws InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("marking as read").And("id", id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.tenantEntityManager.loadExplictTenantFilters();
|
||||||
|
|
||||||
|
this.inAppNotificationService.markAsRead(id);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.InApp_Notification_Read, Map.of("id", id));
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("read-all")
|
||||||
|
@Transactional
|
||||||
|
public Boolean MarkAsReadAllUserInAppNotification() throws InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("marking as read all"));
|
||||||
|
|
||||||
|
UUID userId = this.userScope.getUserId();
|
||||||
|
if (userId == null) throw new MyForbiddenException(this.errors.getNonPersonPrincipal().getCode(), this.errors.getNonPersonPrincipal().getMessage());
|
||||||
|
try {
|
||||||
|
this.tenantEntityManager.loadExplictTenantFilters();
|
||||||
|
this.inAppNotificationService.markAsReadAllUserNotification(userId);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.InApp_Notification_Read_All, Map.of("userId", userId));
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("count-unread")
|
||||||
|
@Transactional
|
||||||
|
public Integer CountUnread() throws InvalidApplicationException {
|
||||||
|
logger.debug("count-unread");
|
||||||
|
|
||||||
|
UUID userId = this.userScope.getUserId();
|
||||||
|
if (userId == null) throw new MyForbiddenException(this.errors.getNonPersonPrincipal().getCode(), this.errors.getNonPersonPrincipal().getMessage());
|
||||||
|
|
||||||
|
this.censorFactory.censor(InAppNotificationCensor.class).censor(new BaseFieldSet(InAppNotification.Field.ID), userId);
|
||||||
|
try {
|
||||||
|
this.tenantEntityManager.loadExplictTenantFilters();
|
||||||
|
InAppNotificationQuery query = this.queryFactory.query(InAppNotificationQuery.class).disableTracking().isActive(IsActive.Active).trackingState(NotificationInAppTracking.STORED).userId(userId);
|
||||||
|
int count = Math.toIntExact(query.count());
|
||||||
|
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
} finally {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public void Delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("deleting" + Notification.class.getSimpleName()).And("id", id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.tenantEntityManager.loadExplictTenantFilters();
|
||||||
|
this.inAppNotificationService.deleteAndSave(id);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.InApp_Notification_Delete, "id", id);
|
||||||
|
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
} finally {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package gr.cite.notification.web.controllers;
|
||||||
|
|
||||||
|
import gr.cite.notification.audit.AuditableAction;
|
||||||
|
import gr.cite.notification.authorization.AuthorizationFlags;
|
||||||
|
import gr.cite.notification.data.NotificationEntity;
|
||||||
|
import gr.cite.notification.model.Notification;
|
||||||
|
import gr.cite.notification.model.builder.NotificationBuilder;
|
||||||
|
import gr.cite.notification.model.censorship.NotificationCensor;
|
||||||
|
import gr.cite.notification.model.persist.NotificationPersist;
|
||||||
|
import gr.cite.notification.query.NotificationQuery;
|
||||||
|
import gr.cite.notification.query.lookup.NotificationLookup;
|
||||||
|
import gr.cite.notification.service.notification.NotificationService;
|
||||||
|
import gr.cite.notification.web.model.QueryResult;
|
||||||
|
import gr.cite.tools.auditing.AuditService;
|
||||||
|
import gr.cite.tools.data.builder.BuilderFactory;
|
||||||
|
import gr.cite.tools.data.censor.CensorFactory;
|
||||||
|
import gr.cite.tools.data.query.QueryFactory;
|
||||||
|
import gr.cite.tools.exception.MyApplicationException;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.exception.MyNotFoundException;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
|
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "api/notification")
|
||||||
|
public class NotificationController {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(NotificationController.class));
|
||||||
|
|
||||||
|
private final BuilderFactory builderFactory;
|
||||||
|
private final AuditService auditService;
|
||||||
|
private final NotificationService notificationService;
|
||||||
|
private final CensorFactory censorFactory;
|
||||||
|
private final QueryFactory queryFactory;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public NotificationController(BuilderFactory builderFactory,
|
||||||
|
AuditService auditService,
|
||||||
|
NotificationService notificationService, CensorFactory censorFactory,
|
||||||
|
QueryFactory queryFactory,
|
||||||
|
MessageSource messageSource) {
|
||||||
|
this.builderFactory = builderFactory;
|
||||||
|
this.auditService = auditService;
|
||||||
|
this.notificationService = notificationService;
|
||||||
|
this.censorFactory = censorFactory;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("query")
|
||||||
|
public QueryResult<Notification> Query(@RequestBody NotificationLookup lookup) throws MyApplicationException, MyForbiddenException {
|
||||||
|
logger.debug("querying {}", Notification.class.getSimpleName());
|
||||||
|
|
||||||
|
this.censorFactory.censor(NotificationCensor.class).censor(lookup.getProject());
|
||||||
|
|
||||||
|
NotificationQuery query = lookup.enrich(this.queryFactory).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission);
|
||||||
|
List<NotificationEntity> data = query.collectAs(lookup.getProject());
|
||||||
|
List<Notification> models = this.builderFactory.builder(NotificationBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(lookup.getProject(), data);
|
||||||
|
long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size();
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Query, "lookup", lookup);
|
||||||
|
|
||||||
|
return new QueryResult<>(models, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public Notification Get(@PathVariable UUID id, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + Notification.class.getSimpleName()).And("id", id).And("fields", fieldSet));
|
||||||
|
|
||||||
|
this.censorFactory.censor(NotificationCensor.class).censor(fieldSet);
|
||||||
|
|
||||||
|
NotificationQuery query = this.queryFactory.query(NotificationQuery.class).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission).ids(id);
|
||||||
|
Notification model = this.builderFactory.builder(NotificationBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.firstAs(fieldSet));
|
||||||
|
if (model == null)
|
||||||
|
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{id, Notification.class.getSimpleName()}, LocaleContextHolder.getLocale()));
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Lookup, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("id", id),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("persist")
|
||||||
|
@Transactional
|
||||||
|
@ValidationFilterAnnotation(validator = NotificationPersist.NotificationPersistValidator.ValidatorName, argumentName = "model")
|
||||||
|
public Notification Persist(@RequestBody NotificationPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("persisting" + Notification.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
|
||||||
|
|
||||||
|
Notification persisted = this.notificationService.persist(model, fieldSet);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Persist, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("model", model),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public void Delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("deleting" + Notification.class.getSimpleName()).And("id", id));
|
||||||
|
|
||||||
|
this.notificationService.deleteAndSave(id);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.InApp_Notification_Delete, "id", id);
|
||||||
|
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package gr.cite.notification.web.controllers;
|
||||||
|
|
||||||
|
import gr.cite.notification.audit.AuditableAction;
|
||||||
|
import gr.cite.notification.authorization.AuthorizationFlags;
|
||||||
|
import gr.cite.notification.data.NotificationTemplateEntity;
|
||||||
|
import gr.cite.notification.model.NotificationTemplate;
|
||||||
|
import gr.cite.notification.model.builder.NotificationTemplateBuilder;
|
||||||
|
import gr.cite.notification.model.censorship.NotificationTemplateCensor;
|
||||||
|
import gr.cite.notification.model.persist.NotificationTemplatePersist;
|
||||||
|
import gr.cite.notification.query.NotificationTemplateQuery;
|
||||||
|
import gr.cite.notification.query.lookup.NotificationTemplateLookup;
|
||||||
|
import gr.cite.notification.service.notificationtemplate.NotificationTemplateService;
|
||||||
|
import gr.cite.notification.web.model.QueryResult;
|
||||||
|
import gr.cite.tools.auditing.AuditService;
|
||||||
|
import gr.cite.tools.data.builder.BuilderFactory;
|
||||||
|
import gr.cite.tools.data.censor.CensorFactory;
|
||||||
|
import gr.cite.tools.data.query.QueryFactory;
|
||||||
|
import gr.cite.tools.exception.MyApplicationException;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.exception.MyNotFoundException;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
|
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "api/notification/notification-template")
|
||||||
|
public class NotificationTemplateController {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(NotificationTemplateController.class));
|
||||||
|
|
||||||
|
private final BuilderFactory builderFactory;
|
||||||
|
private final AuditService auditService;
|
||||||
|
private final NotificationTemplateService notificationTemplateService;
|
||||||
|
private final CensorFactory censorFactory;
|
||||||
|
private final QueryFactory queryFactory;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public NotificationTemplateController(BuilderFactory builderFactory,
|
||||||
|
AuditService auditService,
|
||||||
|
NotificationTemplateService notificationTemplateService,
|
||||||
|
CensorFactory censorFactory,
|
||||||
|
QueryFactory queryFactory,
|
||||||
|
MessageSource messageSource) {
|
||||||
|
this.builderFactory = builderFactory;
|
||||||
|
this.auditService = auditService;
|
||||||
|
this.notificationTemplateService = notificationTemplateService;
|
||||||
|
this.censorFactory = censorFactory;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("query")
|
||||||
|
public QueryResult<NotificationTemplate> query(@RequestBody NotificationTemplateLookup lookup) throws MyApplicationException, MyForbiddenException {
|
||||||
|
logger.debug("querying {}", NotificationTemplate.class.getSimpleName());
|
||||||
|
|
||||||
|
this.censorFactory.censor(NotificationTemplateCensor.class).censor(lookup.getProject());
|
||||||
|
|
||||||
|
NotificationTemplateQuery query = lookup.enrich(this.queryFactory).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission);
|
||||||
|
List<NotificationTemplateEntity> data = query.collectAs(lookup.getProject());
|
||||||
|
List<NotificationTemplate> models = this.builderFactory.builder(NotificationTemplateBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(lookup.getProject(), data);
|
||||||
|
long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size();
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Template_Query, "lookup", lookup);
|
||||||
|
|
||||||
|
return new QueryResult<>(models, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
public NotificationTemplate get(@PathVariable UUID id, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + NotificationTemplate.class.getSimpleName()).And("id", id).And("fields", fieldSet));
|
||||||
|
|
||||||
|
this.censorFactory.censor(NotificationTemplateCensor.class).censor(fieldSet);
|
||||||
|
|
||||||
|
NotificationTemplateQuery query = this.queryFactory.query(NotificationTemplateQuery.class).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission).ids(id);
|
||||||
|
NotificationTemplate model = this.builderFactory.builder(NotificationTemplateBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.first());
|
||||||
|
if (model == null)
|
||||||
|
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{id, NotificationTemplate.class.getSimpleName()}, LocaleContextHolder.getLocale()));
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Template_Lookup, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("id", id),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("persist")
|
||||||
|
@Transactional
|
||||||
|
@ValidationFilterAnnotation(validator = NotificationTemplatePersist.NotificationTemplatePersistValidator.ValidatorName, argumentName = "model")
|
||||||
|
public NotificationTemplate persist(@RequestBody NotificationTemplatePersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("persisting" + NotificationTemplate.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
|
||||||
|
|
||||||
|
NotificationTemplate persisted = this.notificationTemplateService.persist(model, fieldSet);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Template_Persist, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("model", model),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("deleting" + NotificationTemplate.class.getSimpleName()).And("id", id));
|
||||||
|
|
||||||
|
this.notificationTemplateService.deleteAndSave(id);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Notification_Template_Delete, "id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package gr.cite.notification.web.controllers;
|
||||||
|
|
||||||
|
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
|
||||||
|
import gr.cite.commons.web.oidc.principal.MyPrincipal;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
|
||||||
|
import gr.cite.notification.audit.AuditableAction;
|
||||||
|
import gr.cite.notification.common.scope.tenant.TenantScope;
|
||||||
|
import gr.cite.notification.web.model.Account;
|
||||||
|
import gr.cite.notification.web.model.AccountBuilder;
|
||||||
|
import gr.cite.tools.auditing.AuditService;
|
||||||
|
import gr.cite.tools.fieldset.BaseFieldSet;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "api/notification/principal", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public class PrincipalController {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(PrincipalController.class));
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
private final AccountBuilder accountBuilder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public PrincipalController(
|
||||||
|
CurrentPrincipalResolver currentPrincipalResolver,
|
||||||
|
AccountBuilder accountBuilder,
|
||||||
|
AuditService auditService) {
|
||||||
|
this.currentPrincipalResolver = currentPrincipalResolver;
|
||||||
|
this.accountBuilder = accountBuilder;
|
||||||
|
this.auditService = auditService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("me")
|
||||||
|
public Account me(FieldSet fieldSet) {
|
||||||
|
logger.debug("me");
|
||||||
|
|
||||||
|
|
||||||
|
if (fieldSet == null || fieldSet.isEmpty()) {
|
||||||
|
fieldSet = new BaseFieldSet(
|
||||||
|
Account._isAuthenticated,
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._subject),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._userId),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._name),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._scope),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._client),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._issuedAt),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._notBefore),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._authenticatedAt),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._expiresAt),
|
||||||
|
BaseFieldSet.asIndexer(Account._principal, Account.PrincipalInfo._more),
|
||||||
|
Account._permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
MyPrincipal principal = this.currentPrincipalResolver.currentPrincipal();
|
||||||
|
|
||||||
|
Account me = this.accountBuilder.build(fieldSet, principal);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Principal_Lookup);
|
||||||
|
//auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return me;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
package gr.cite.notification.web.controllers;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import gr.cite.notification.audit.AuditableAction;
|
||||||
|
import gr.cite.notification.authorization.AuthorizationFlags;
|
||||||
|
import gr.cite.notification.common.enums.IsActive;
|
||||||
|
import gr.cite.notification.common.enums.TenantConfigurationType;
|
||||||
|
import gr.cite.notification.common.scope.tenant.TenantScope;
|
||||||
|
import gr.cite.notification.common.types.tenantconfiguration.NotifierListTenantConfigurationEntity;
|
||||||
|
import gr.cite.notification.data.TenantConfigurationEntity;
|
||||||
|
import gr.cite.notification.model.builder.tenantconfiguration.TenantConfigurationBuilder;
|
||||||
|
import gr.cite.notification.model.censorship.tenantconfiguration.TenantConfigurationCensor;
|
||||||
|
import gr.cite.notification.model.persist.tenantconfiguration.TenantConfigurationPersist;
|
||||||
|
import gr.cite.notification.model.tenantconfiguration.TenantConfiguration;
|
||||||
|
import gr.cite.notification.query.TenantConfigurationQuery;
|
||||||
|
import gr.cite.notification.query.lookup.NotifierListLookup;
|
||||||
|
import gr.cite.notification.query.lookup.TenantConfigurationLookup;
|
||||||
|
import gr.cite.notification.service.tenantconfiguration.TenantConfigurationService;
|
||||||
|
import gr.cite.notification.web.model.QueryResult;
|
||||||
|
import gr.cite.tools.auditing.AuditService;
|
||||||
|
import gr.cite.tools.data.builder.BuilderFactory;
|
||||||
|
import gr.cite.tools.data.censor.CensorFactory;
|
||||||
|
import gr.cite.tools.data.query.QueryFactory;
|
||||||
|
import gr.cite.tools.exception.MyApplicationException;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.exception.MyNotFoundException;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
|
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "api/notification/tenant-configuration")
|
||||||
|
public class TenantConfigurationController {
|
||||||
|
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantConfigurationController.class));
|
||||||
|
|
||||||
|
private final BuilderFactory builderFactory;
|
||||||
|
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
|
private final TenantConfigurationService tenantConfigurationService;
|
||||||
|
|
||||||
|
private final CensorFactory censorFactory;
|
||||||
|
|
||||||
|
private final QueryFactory queryFactory;
|
||||||
|
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final TenantScope tenantScope;
|
||||||
|
|
||||||
|
public TenantConfigurationController(
|
||||||
|
BuilderFactory builderFactory,
|
||||||
|
AuditService auditService,
|
||||||
|
TenantConfigurationService tenantConfigurationService, CensorFactory censorFactory,
|
||||||
|
QueryFactory queryFactory,
|
||||||
|
MessageSource messageSource, TenantScope tenantScope) {
|
||||||
|
this.builderFactory = builderFactory;
|
||||||
|
this.auditService = auditService;
|
||||||
|
this.tenantConfigurationService = tenantConfigurationService;
|
||||||
|
this.censorFactory = censorFactory;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.tenantScope = tenantScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("query")
|
||||||
|
public QueryResult<TenantConfiguration> query(@RequestBody TenantConfigurationLookup lookup) throws MyApplicationException, MyForbiddenException {
|
||||||
|
logger.debug("querying {}", TenantConfiguration.class.getSimpleName());
|
||||||
|
|
||||||
|
this.censorFactory.censor(TenantConfigurationCensor.class).censor(lookup.getProject(), null);
|
||||||
|
|
||||||
|
TenantConfigurationQuery query = lookup.enrich(this.queryFactory).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission);
|
||||||
|
|
||||||
|
List<TenantConfigurationEntity> data = query.collectAs(lookup.getProject());
|
||||||
|
List<TenantConfiguration> models = this.builderFactory.builder(TenantConfigurationBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(lookup.getProject(), data);
|
||||||
|
long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size();
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Tenant_Configuration_Query, "lookup", lookup);
|
||||||
|
|
||||||
|
return new QueryResult<>(models, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
public TenantConfiguration get(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + TenantConfiguration.class.getSimpleName()).And("id", id).And("fields", fieldSet));
|
||||||
|
|
||||||
|
this.censorFactory.censor(TenantConfigurationCensor.class).censor(fieldSet, null);
|
||||||
|
|
||||||
|
TenantConfigurationQuery query = this.queryFactory.query(TenantConfigurationQuery.class).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission).ids(id);
|
||||||
|
TenantConfiguration model = this.builderFactory.builder(TenantConfigurationBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.firstAs(fieldSet));
|
||||||
|
if (model == null)
|
||||||
|
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{id, TenantConfiguration.class.getSimpleName()}, LocaleContextHolder.getLocale()));
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Tenant_Configuration_Lookup, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("id", id),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("current-tenant/{type}")
|
||||||
|
public TenantConfiguration getCurrentTenantType(@PathVariable("type") Short type, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + TenantConfiguration.class.getSimpleName()).And("type", type).And("fields", fieldSet));
|
||||||
|
|
||||||
|
this.censorFactory.censor(TenantConfigurationCensor.class).censor(fieldSet, null);
|
||||||
|
|
||||||
|
TenantConfigurationQuery query = this.queryFactory.query(TenantConfigurationQuery.class).disableTracking().authorize(AuthorizationFlags.OwnerOrPermission).isActive(IsActive.Active).types(TenantConfigurationType.of(type));
|
||||||
|
if (this.tenantScope.isDefaultTenant()) query.tenantIsSet(false);
|
||||||
|
else query.tenantIsSet(true).tenantIds(this.tenantScope.getTenant());
|
||||||
|
|
||||||
|
TenantConfiguration model = this.builderFactory.builder(TenantConfigurationBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.firstAs(fieldSet));
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.TenantConfiguration_LookupByType, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("type", type),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("persist")
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@ValidationFilterAnnotation(validator = TenantConfigurationPersist.TenantConfigurationPersistValidator.ValidatorName, argumentName = "model")
|
||||||
|
public TenantConfiguration persist(@RequestBody TenantConfigurationPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, JsonProcessingException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
|
||||||
|
logger.debug(new MapLogEntry("persisting" + TenantConfiguration.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
|
||||||
|
TenantConfiguration persisted = this.tenantConfigurationService.persist(model, fieldSet);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Tenant_Configuration_Persist, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("model", model),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + TenantConfiguration.class.getSimpleName()).And("id", id));
|
||||||
|
|
||||||
|
this.tenantConfigurationService.deleteAndSave(id);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Tenant_Configuration_Delete, "id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("notifier-list/available")
|
||||||
|
public NotifierListTenantConfigurationEntity getAvailableNotifiers(@RequestBody NotifierListLookup tenantNotifierListLookup)
|
||||||
|
{
|
||||||
|
logger.debug("querying available notifiers");
|
||||||
|
|
||||||
|
NotifierListTenantConfigurationEntity notifierListData = this.tenantConfigurationService.collectTenantAvailableNotifierList(tenantNotifierListLookup.getNotificationTypes());
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Tenant_Configuration_Notifiers_Query, Map.of(
|
||||||
|
"lookup", tenantNotifierListLookup
|
||||||
|
));
|
||||||
|
//this._auditService.TrackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return notifierListData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
package gr.cite.notification.web.controllers;
|
||||||
|
|
||||||
|
import gr.cite.notification.audit.AuditableAction;
|
||||||
|
import gr.cite.notification.authorization.AuthorizationFlags;
|
||||||
|
import gr.cite.notification.common.enums.IsActive;
|
||||||
|
import gr.cite.notification.common.enums.TenantConfigurationType;
|
||||||
|
import gr.cite.notification.common.scope.tenant.TenantScope;
|
||||||
|
import gr.cite.notification.common.types.tenantconfiguration.NotifierListTenantConfigurationEntity;
|
||||||
|
import gr.cite.notification.data.TenantEntity;
|
||||||
|
import gr.cite.notification.data.UserNotificationPreferenceEntity;
|
||||||
|
import gr.cite.notification.event.TenantConfigurationTouchedEvent;
|
||||||
|
import gr.cite.notification.model.UserNotificationPreference;
|
||||||
|
import gr.cite.notification.model.builder.UserNotificationPreferenceBuilder;
|
||||||
|
import gr.cite.notification.model.censorship.UserNotificationPreferenceCensor;
|
||||||
|
import gr.cite.notification.model.persist.UserNotificationPreferencePersist;
|
||||||
|
import gr.cite.notification.query.UserNotificationPreferenceQuery;
|
||||||
|
import gr.cite.notification.query.lookup.NotifierListLookup;
|
||||||
|
import gr.cite.notification.query.lookup.UserNotificationPreferenceLookup;
|
||||||
|
import gr.cite.notification.service.userNotificationPreference.UserNotificationPreferenceService;
|
||||||
|
import gr.cite.notification.web.model.QueryResult;
|
||||||
|
import gr.cite.tools.auditing.AuditService;
|
||||||
|
import gr.cite.tools.data.builder.BuilderFactory;
|
||||||
|
import gr.cite.tools.data.censor.CensorFactory;
|
||||||
|
import gr.cite.tools.data.query.Ordering;
|
||||||
|
import gr.cite.tools.data.query.QueryFactory;
|
||||||
|
import gr.cite.tools.exception.MyApplicationException;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.exception.MyNotFoundException;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import gr.cite.tools.logging.MapLogEntry;
|
||||||
|
import gr.cite.tools.validation.ValidationFilterAnnotation;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "api/notification/notification-preference")
|
||||||
|
public class UserNotificationPreferenceController {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UserNotificationPreferenceController.class));
|
||||||
|
|
||||||
|
private final BuilderFactory builderFactory;
|
||||||
|
private final AuditService auditService;
|
||||||
|
private final UserNotificationPreferenceService userNotificationPreferenceService;
|
||||||
|
private final CensorFactory censorFactory;
|
||||||
|
private final QueryFactory queryFactory;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final TenantScope tenantScope;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserNotificationPreferenceController(BuilderFactory builderFactory,
|
||||||
|
AuditService auditService,
|
||||||
|
UserNotificationPreferenceService userNotificationPreferenceService,
|
||||||
|
CensorFactory censorFactory,
|
||||||
|
QueryFactory queryFactory,
|
||||||
|
MessageSource messageSource, TenantScope tenantScope) {
|
||||||
|
this.builderFactory = builderFactory;
|
||||||
|
this.auditService = auditService;
|
||||||
|
this.userNotificationPreferenceService = userNotificationPreferenceService;
|
||||||
|
this.censorFactory = censorFactory;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.tenantScope = tenantScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("query")
|
||||||
|
public QueryResult<UserNotificationPreference> query(@RequestBody UserNotificationPreferenceLookup lookup) throws MyApplicationException, MyForbiddenException {
|
||||||
|
logger.debug("querying {}", UserNotificationPreference.class.getSimpleName());
|
||||||
|
|
||||||
|
this.censorFactory.censor(UserNotificationPreferenceCensor.class).censor(lookup.getProject(), null);
|
||||||
|
|
||||||
|
UserNotificationPreferenceQuery query = lookup.enrich(this.queryFactory).disableTracking();
|
||||||
|
List<UserNotificationPreferenceEntity> data = query.collectAs(lookup.getProject());
|
||||||
|
List<UserNotificationPreference> models = this.builderFactory.builder(UserNotificationPreferenceBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(lookup.getProject(), data);
|
||||||
|
long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size();
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.User_Notification_Preference_Query, "lookup", lookup);
|
||||||
|
|
||||||
|
return new QueryResult<>(models, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("user/{userId}/current")
|
||||||
|
@Transactional
|
||||||
|
public List<UserNotificationPreference> current(@PathVariable UUID userId, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("retrieving" + UserNotificationPreference.class.getSimpleName()).And("userId", userId).And("fields", fieldSet));
|
||||||
|
|
||||||
|
this.censorFactory.censor(UserNotificationPreferenceCensor.class).censor(fieldSet, userId);
|
||||||
|
|
||||||
|
Ordering ordering = new Ordering();
|
||||||
|
ordering.addAscending(UserNotificationPreference._ordinal);
|
||||||
|
UserNotificationPreferenceQuery query = this.queryFactory.query(UserNotificationPreferenceQuery.class).disableTracking().userId(userId).isActives(IsActive.Active);
|
||||||
|
query.setOrder(ordering);
|
||||||
|
if (this.tenantScope.isMultitenant() && this.tenantScope.isSet()) {
|
||||||
|
if (!this.tenantScope.isDefaultTenant()) {
|
||||||
|
query.tenantIsSet(true).tenantIds(this.tenantScope.getTenant());
|
||||||
|
} else {
|
||||||
|
query.tenantIsSet(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<UserNotificationPreference> model = this.builderFactory.builder(UserNotificationPreferenceBuilder.class).authorize(AuthorizationFlags.OwnerOrPermission).build(fieldSet, query.collectAs(fieldSet));
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.User_Notification_Preference_Lookup, Map.ofEntries(
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("userId", userId),
|
||||||
|
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
|
||||||
|
));
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("notifier-list/available")
|
||||||
|
public NotifierListTenantConfigurationEntity getAvailableNotifiers(@RequestBody NotifierListLookup tenantNotifierListLookup)
|
||||||
|
{
|
||||||
|
logger.debug("querying available notifiers");
|
||||||
|
|
||||||
|
NotifierListTenantConfigurationEntity notifierListData = this.userNotificationPreferenceService.collectUserAvailableNotifierList(tenantNotifierListLookup.getNotificationTypes());
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.User_Available_Notifiers_Query, Map.of(
|
||||||
|
"lookup", tenantNotifierListLookup
|
||||||
|
));
|
||||||
|
//this._auditService.TrackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return notifierListData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("persist")
|
||||||
|
@Transactional
|
||||||
|
@ValidationFilterAnnotation(validator = UserNotificationPreferencePersist.UserNotificationPreferencePersistValidator.ValidatorName, argumentName = "model")
|
||||||
|
public List<UserNotificationPreference> persist(@RequestBody UserNotificationPreferencePersist model, FieldSet fieldSet)
|
||||||
|
{
|
||||||
|
logger.debug(new MapLogEntry("persisting").And("type", TenantConfigurationType.NotifierList).And("model", model).And("fields", fieldSet));
|
||||||
|
|
||||||
|
List<UserNotificationPreference> persisted = this.userNotificationPreferenceService.persist(model, fieldSet);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.User_Notification_Preference_Persist, Map.of(
|
||||||
|
"type", TenantConfigurationType.NotifierList,
|
||||||
|
"model", model,
|
||||||
|
"fields", fieldSet
|
||||||
|
));
|
||||||
|
//this._auditService.TrackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @DeleteMapping("{id}")
|
||||||
|
@Transactional
|
||||||
|
public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
|
||||||
|
logger.debug(new MapLogEntry("deleting" + TenantConfiguration.class.getSimpleName()).And("id", id));
|
||||||
|
|
||||||
|
this.tenantConfigurationService.deleteAndSave(id);
|
||||||
|
|
||||||
|
this.auditService.track(AuditableAction.Tenant_Configuration_Delete, "id", id);
|
||||||
|
|
||||||
|
//this.auditService.trackIdentity(AuditableAction.IdentityTracking_Action);
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
//package gr.cite.intelcomp.stiviewer.web.controllers.error;
|
||||||
|
//
|
||||||
|
//import gr.cite.tools.exception.MyValidationException;
|
||||||
|
//import org.springframework.http.HttpStatus;
|
||||||
|
//import org.springframework.http.ResponseEntity;
|
||||||
|
//import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
//import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
//import org.springframework.web.context.request.WebRequest;
|
||||||
|
//
|
||||||
|
//@ControllerAdvice
|
||||||
|
//public class GenericErrorHandler {
|
||||||
|
//
|
||||||
|
// @ExceptionHandler(MyValidationException.class)
|
||||||
|
// public ResponseEntity<MyValidationException> handleValidationException(MyValidationException e, WebRequest webRequest) {
|
||||||
|
// return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(e);
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -0,0 +1,160 @@
|
||||||
|
package gr.cite.notification.web.model;
|
||||||
|
|
||||||
|
import gr.cite.tools.logging.annotation.LogSensitive;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Account {
|
||||||
|
|
||||||
|
public static class PrincipalInfo {
|
||||||
|
|
||||||
|
public static final String _userId = "userId";
|
||||||
|
public UUID userId;
|
||||||
|
|
||||||
|
public UUID getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(UUID userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _subject = "subject";
|
||||||
|
public UUID subject;
|
||||||
|
|
||||||
|
public UUID getSubject() {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubject(UUID subject) {
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _name = "name";
|
||||||
|
@LogSensitive
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _scope = "scope";
|
||||||
|
public List<String> scope;
|
||||||
|
|
||||||
|
public List<String> getScope() {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScope(List<String> scope) {
|
||||||
|
this.scope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _client = "client";
|
||||||
|
public String client;
|
||||||
|
|
||||||
|
public String getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClient(String client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _notBefore = "notBefore";
|
||||||
|
public Instant notBefore;
|
||||||
|
|
||||||
|
public Instant getNotBefore() {
|
||||||
|
return notBefore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotBefore(Instant notBefore) {
|
||||||
|
this.notBefore = notBefore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _issuedAt = "issuedAt";
|
||||||
|
public Instant issuedAt;
|
||||||
|
|
||||||
|
public Instant getIssuedAt() {
|
||||||
|
return issuedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIssuedAt(Instant issuedAt) {
|
||||||
|
this.issuedAt = issuedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _authenticatedAt = "authenticatedAt";
|
||||||
|
public Instant authenticatedAt;
|
||||||
|
|
||||||
|
public Instant getAuthenticatedAt() {
|
||||||
|
return authenticatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticatedAt(Instant authenticatedAt) {
|
||||||
|
this.authenticatedAt = authenticatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _expiresAt = "expiresAt";
|
||||||
|
public Instant expiresAt;
|
||||||
|
|
||||||
|
public Instant getExpiresAt() {
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpiresAt(Instant expiresAt) {
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _more = "more";
|
||||||
|
@LogSensitive
|
||||||
|
public Map<String, List<String>> more;
|
||||||
|
|
||||||
|
public Map<String, List<String>> getMore() {
|
||||||
|
return more;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMore(Map<String, List<String>> more) {
|
||||||
|
this.more = more;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static final String _isAuthenticated = "isAuthenticated";
|
||||||
|
private Boolean isAuthenticated;
|
||||||
|
|
||||||
|
public Boolean getIsAuthenticated() {
|
||||||
|
return isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsAuthenticated(Boolean authenticated) {
|
||||||
|
isAuthenticated = authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _principal = "principal";
|
||||||
|
private PrincipalInfo principal;
|
||||||
|
|
||||||
|
public PrincipalInfo getPrincipal() {
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrincipal(PrincipalInfo principal) {
|
||||||
|
this.principal = principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String _permissions = "permissions";
|
||||||
|
private List<String> permissions;
|
||||||
|
|
||||||
|
public List<String> getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermissions(List<String> permissions) {
|
||||||
|
this.permissions = permissions;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package gr.cite.notification.web.model;
|
||||||
|
|
||||||
|
import gr.cite.commons.web.authz.configuration.AuthorizationConfiguration;
|
||||||
|
import gr.cite.commons.web.authz.configuration.Permission;
|
||||||
|
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
|
||||||
|
import gr.cite.commons.web.oidc.principal.MyPrincipal;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractorKeys;
|
||||||
|
import gr.cite.notification.common.scope.user.UserScope;
|
||||||
|
import gr.cite.tools.fieldset.BaseFieldSet;
|
||||||
|
import gr.cite.tools.fieldset.FieldSet;
|
||||||
|
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||||
|
public class AccountBuilder {
|
||||||
|
|
||||||
|
private final ClaimExtractor claimExtractor;
|
||||||
|
private final Set<String> excludeMoreClaim;
|
||||||
|
private final AuthorizationConfiguration authorizationConfiguration;
|
||||||
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
private final UserScope userScope;
|
||||||
|
|
||||||
|
public AccountBuilder(ClaimExtractor claimExtractor, AuthorizationConfiguration authorizationConfiguration, CurrentPrincipalResolver currentPrincipalResolver, UserScope userScope) {
|
||||||
|
this.claimExtractor = claimExtractor;
|
||||||
|
this.authorizationConfiguration = authorizationConfiguration;
|
||||||
|
this.currentPrincipalResolver = currentPrincipalResolver;
|
||||||
|
this.userScope = userScope;
|
||||||
|
this.excludeMoreClaim = Set.of(
|
||||||
|
ClaimExtractorKeys.Subject,
|
||||||
|
ClaimExtractorKeys.Name,
|
||||||
|
ClaimExtractorKeys.Scope,
|
||||||
|
ClaimExtractorKeys.Client,
|
||||||
|
ClaimExtractorKeys.IssuedAt,
|
||||||
|
ClaimExtractorKeys.NotBefore,
|
||||||
|
ClaimExtractorKeys.AuthenticatedAt,
|
||||||
|
ClaimExtractorKeys.ExpiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account build(FieldSet fields, MyPrincipal principal) {
|
||||||
|
Account model = new Account();
|
||||||
|
if (principal == null || !principal.isAuthenticated()) {
|
||||||
|
model.setIsAuthenticated(false);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
model.setIsAuthenticated(true);
|
||||||
|
|
||||||
|
FieldSet principalFields = fields.extractPrefixed(BaseFieldSet.asIndexerPrefix(Account._principal));
|
||||||
|
if (!principalFields.isEmpty()) model.setPrincipal(new Account.PrincipalInfo());
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._subject)) model.getPrincipal().setSubject(this.claimExtractor.subjectUUID(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._userId)) model.getPrincipal().setUserId(this.userScope.getUserIdSafe());
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._name)) model.getPrincipal().setName(this.claimExtractor.name(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._scope)) model.getPrincipal().setScope(this.claimExtractor.scope(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._client)) model.getPrincipal().setClient(this.claimExtractor.client(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._issuedAt)) model.getPrincipal().setIssuedAt(this.claimExtractor.issuedAt(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._notBefore)) model.getPrincipal().setNotBefore(this.claimExtractor.notBefore(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._authenticatedAt)) model.getPrincipal().setAuthenticatedAt(this.claimExtractor.authenticatedAt(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._expiresAt)) model.getPrincipal().setExpiresAt(this.claimExtractor.expiresAt(principal));
|
||||||
|
if (principalFields.hasField(Account.PrincipalInfo._more)) {
|
||||||
|
model.getPrincipal().setMore(new HashMap<>());
|
||||||
|
for (String key : this.claimExtractor.knownPublicKeys()) {
|
||||||
|
if (this.excludeMoreClaim.contains(key)) continue;
|
||||||
|
List<String> values = this.claimExtractor.asStrings(principal, key);
|
||||||
|
if (values == null || values.size() == 0) continue;
|
||||||
|
if (!model.getPrincipal().getMore().containsKey(key)) model.getPrincipal().getMore().put(key, new ArrayList<>());
|
||||||
|
model.getPrincipal().getMore().get(key).addAll(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.hasField(Account._permissions)) {
|
||||||
|
List<String> roles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal());
|
||||||
|
Set<String> permissions = authorizationConfiguration.permissionsOfRoles(roles);
|
||||||
|
for (Map.Entry<String, Permission> permissionEntry : authorizationConfiguration.getRawPolicies().entrySet()){
|
||||||
|
if (permissionEntry.getValue().getAllowAuthenticated()){
|
||||||
|
permissions.add(permissionEntry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.setPermissions(new ArrayList<>(permissions));
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package gr.cite.notification.web.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class QueryResult<M> {
|
||||||
|
public QueryResult() { }
|
||||||
|
public QueryResult(List<M> items, long count)
|
||||||
|
{
|
||||||
|
this.items = items;
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<M> items;
|
||||||
|
public long count;
|
||||||
|
|
||||||
|
public List<M> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<M> items) {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCount(long count) {
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static QueryResult Empty()
|
||||||
|
{
|
||||||
|
return new QueryResult(new ArrayList<>(), 0L);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import gr.cite.tools.cache.CacheOptions;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "cache.tenant-by-code")
|
||||||
|
public class TenantByCodeCacheOptions extends CacheOptions {
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import gr.cite.notification.convention.ConventionService;
|
||||||
|
import gr.cite.notification.event.TenantTouchedEvent;
|
||||||
|
import gr.cite.tools.cache.CacheService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TenantByCodeCacheService extends CacheService<TenantByCodeCacheService.TenantByCodeCacheValue> {
|
||||||
|
|
||||||
|
public static class TenantByCodeCacheValue {
|
||||||
|
|
||||||
|
public TenantByCodeCacheValue() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TenantByCodeCacheValue(String tenantCode, UUID tenantId) {
|
||||||
|
this.tenantCode = tenantCode;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
|
||||||
|
public String getTenantCode() {
|
||||||
|
return tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantCode(String tenantCode) {
|
||||||
|
this.tenantCode = tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID tenantId;
|
||||||
|
|
||||||
|
public UUID getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(UUID tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConventionService conventionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TenantByCodeCacheService(TenantByCodeCacheOptions options, ConventionService conventionService) {
|
||||||
|
super(options);
|
||||||
|
this.conventionService = conventionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void handleTenantTouchedEvent(TenantTouchedEvent event) {
|
||||||
|
if (!this.conventionService.isNullOrEmpty(event.getTenantCode()))
|
||||||
|
this.evict(this.buildKey(event.getTenantCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<TenantByCodeCacheValue> valueClass() {
|
||||||
|
return TenantByCodeCacheValue.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String keyOf(TenantByCodeCacheValue value) {
|
||||||
|
return this.buildKey(value.getTenantCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildKey(String code) {
|
||||||
|
HashMap<String, String> keyParts = new HashMap<>();
|
||||||
|
keyParts.put("$code$", code);
|
||||||
|
return this.generateKey(keyParts);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import gr.cite.tools.cache.CacheOptions;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "cache.tenant-by-id")
|
||||||
|
public class TenantByIdCacheOptions extends CacheOptions {
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import gr.cite.notification.convention.ConventionService;
|
||||||
|
import gr.cite.notification.event.TenantTouchedEvent;
|
||||||
|
import gr.cite.tools.cache.CacheService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TenantByIdCacheService extends CacheService<TenantByIdCacheService.TenantByIdCacheValue> {
|
||||||
|
|
||||||
|
public static class TenantByIdCacheValue {
|
||||||
|
|
||||||
|
public TenantByIdCacheValue() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TenantByIdCacheValue(String tenantCode, UUID tenantId) {
|
||||||
|
this.tenantCode = tenantCode;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tenantCode;
|
||||||
|
|
||||||
|
public String getTenantCode() {
|
||||||
|
return tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantCode(String tenantCode) {
|
||||||
|
this.tenantCode = tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID tenantId;
|
||||||
|
|
||||||
|
public UUID getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(UUID tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConventionService conventionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TenantByIdCacheService(TenantByIdCacheOptions options, ConventionService conventionService) {
|
||||||
|
super(options);
|
||||||
|
this.conventionService = conventionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void handleTenantTouchedEvent(TenantTouchedEvent event) {
|
||||||
|
if (event.getTenantId() != null)
|
||||||
|
this.evict(this.buildKey(event.getTenantId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<TenantByIdCacheValue> valueClass() {
|
||||||
|
return TenantByIdCacheValue.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String keyOf(TenantByIdCacheValue value) {
|
||||||
|
return this.buildKey(value.getTenantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildKey(UUID id) {
|
||||||
|
HashMap<String, String> keyParts = new HashMap<>();
|
||||||
|
keyParts.put("$tenantId$", id.toString().toLowerCase(Locale.ROOT));
|
||||||
|
return this.generateKey(keyParts);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
|
||||||
|
import gr.cite.commons.web.authz.service.AuthorizationService;
|
||||||
|
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
|
||||||
|
import gr.cite.notification.authorization.ClaimNames;
|
||||||
|
import gr.cite.notification.authorization.Permission;
|
||||||
|
import gr.cite.notification.common.enums.IsActive;
|
||||||
|
import gr.cite.notification.common.scope.tenant.TenantScope;
|
||||||
|
import gr.cite.notification.common.scope.user.UserScope;
|
||||||
|
import gr.cite.notification.data.TenantEntityManager;
|
||||||
|
import gr.cite.notification.data.TenantUserEntity;
|
||||||
|
import gr.cite.notification.data.UserEntity;
|
||||||
|
import gr.cite.notification.data.tenant.TenantScopedBaseEntity;
|
||||||
|
import gr.cite.notification.errorcode.ErrorThesaurusProperties;
|
||||||
|
import gr.cite.notification.query.utils.BuildSubQueryInput;
|
||||||
|
import gr.cite.notification.query.utils.QueryUtilsService;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import org.hibernate.Session;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.context.request.WebRequestInterceptor;
|
||||||
|
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TenantInterceptor implements WebRequestInterceptor {
|
||||||
|
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantInterceptor.class));
|
||||||
|
private final TenantScope tenantScope;
|
||||||
|
private final UserScope userScope;
|
||||||
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
private final ClaimExtractor claimExtractor;
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
private final TenantScopeProperties tenantScopeProperties;
|
||||||
|
private final UserAllowedTenantCacheService userAllowedTenantCacheService;
|
||||||
|
private final ErrorThesaurusProperties errors;
|
||||||
|
private final QueryUtilsService queryUtilsService;
|
||||||
|
public final TenantEntityManager tenantEntityManager;
|
||||||
|
@PersistenceContext
|
||||||
|
public EntityManager entityManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TenantInterceptor(
|
||||||
|
TenantScope tenantScope,
|
||||||
|
UserScope userScope,
|
||||||
|
CurrentPrincipalResolver currentPrincipalResolver,
|
||||||
|
ClaimExtractor claimExtractor,
|
||||||
|
ApplicationContext applicationContext,
|
||||||
|
TenantScopeProperties tenantScopeProperties,
|
||||||
|
UserAllowedTenantCacheService userAllowedTenantCacheService,
|
||||||
|
ErrorThesaurusProperties errors, QueryUtilsService queryUtilsService, TenantEntityManager tenantEntityManager) {
|
||||||
|
this.tenantScope = tenantScope;
|
||||||
|
this.userScope = userScope;
|
||||||
|
this.currentPrincipalResolver = currentPrincipalResolver;
|
||||||
|
this.claimExtractor = claimExtractor;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
this.tenantScopeProperties = tenantScopeProperties;
|
||||||
|
this.userAllowedTenantCacheService = userAllowedTenantCacheService;
|
||||||
|
this.errors = errors;
|
||||||
|
this.queryUtilsService = queryUtilsService;
|
||||||
|
this.tenantEntityManager = tenantEntityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preHandle(@NotNull WebRequest request) throws InvalidApplicationException, InterruptedException {
|
||||||
|
if (!this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) return;
|
||||||
|
if (!this.tenantScope.isMultitenant()) return;
|
||||||
|
|
||||||
|
boolean isAllowedNoTenant = this.applicationContext.getBean(AuthorizationService.class).authorize(Permission.AllowNoTenant);
|
||||||
|
if (tenantScope.isSet() && this.entityManager != null) {
|
||||||
|
List<String> currentPrincipalTenantCodes = this.claimExtractor.asStrings(this.currentPrincipalResolver.currentPrincipal(), ClaimNames.TenantCodesClaimName);
|
||||||
|
if ((currentPrincipalTenantCodes == null || !currentPrincipalTenantCodes.contains(tenantScope.getTenantCode())) && !isAllowedNoTenant) {
|
||||||
|
logger.warn("tenant not allowed {}", this.tenantScope.getTenant());
|
||||||
|
throw new MyForbiddenException(this.errors.getTenantNotAllowed().getCode(), this.errors.getTenantNotAllowed().getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isUserAllowedTenant = false;
|
||||||
|
if (this.tenantScope.isDefaultTenant()){
|
||||||
|
isUserAllowedTenant = true;
|
||||||
|
} else {
|
||||||
|
UserAllowedTenantCacheService.UserAllowedTenantCacheValue cacheValue = this.userAllowedTenantCacheService.lookup(this.userAllowedTenantCacheService.buildKey(this.userScope.getUserId(), this.tenantScope.getTenant()));
|
||||||
|
if (cacheValue != null) {
|
||||||
|
isUserAllowedTenant = cacheValue.isAllowed();
|
||||||
|
} else {
|
||||||
|
isUserAllowedTenant = this.isUserAllowedTenant();
|
||||||
|
this.userAllowedTenantCacheService.put(new UserAllowedTenantCacheService.UserAllowedTenantCacheValue(this.userScope.getUserId(), this.tenantScope.getTenant(), isUserAllowedTenant));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUserAllowedTenant) {
|
||||||
|
this.tenantEntityManager.reloadTenantFilters();
|
||||||
|
} else {
|
||||||
|
if (isAllowedNoTenant || this.isWhiteListedEndpoint(request)) {
|
||||||
|
tenantScope.setTenant(null, null);
|
||||||
|
} else {
|
||||||
|
logger.warn("tenant not allowed {}", this.tenantScope.getTenant());
|
||||||
|
throw new MyForbiddenException(this.errors.getTenantNotAllowed().getCode(), this.errors.getTenantNotAllowed().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isAllowedNoTenant) {
|
||||||
|
if (!this.isWhiteListedEndpoint(request)) {
|
||||||
|
logger.warn("tenant scope not provided");
|
||||||
|
throw new MyForbiddenException(this.errors.getMissingTenant().getCode(), this.errors.getMissingTenant().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWhiteListedEndpoint(WebRequest request) {
|
||||||
|
String servletPath = ((ServletWebRequest) request).getRequest().getServletPath();
|
||||||
|
if (this.tenantScopeProperties.getWhiteListedEndpoints() != null) {
|
||||||
|
for (String whiteListedEndpoint : this.tenantScopeProperties.getWhiteListedEndpoints()) {
|
||||||
|
if (servletPath.toLowerCase(Locale.ROOT).startsWith(whiteListedEndpoint.toLowerCase(Locale.ROOT))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUserAllowedTenant() throws InvalidApplicationException, InterruptedException {
|
||||||
|
if (userScope.isSet()) {
|
||||||
|
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<UserEntity> query = criteriaBuilder.createQuery(UserEntity.class);
|
||||||
|
Root<UserEntity> root = query.from(UserEntity.class);
|
||||||
|
query.where(criteriaBuilder.and(
|
||||||
|
criteriaBuilder.equal(root.get(UserEntity._isActive), IsActive.Active),
|
||||||
|
criteriaBuilder.in(root.get(UserEntity._id)).value(queryUtilsService.buildSubQuery(new BuildSubQueryInput<>(new BuildSubQueryInput.Builder<>(TenantUserEntity.class, UUID.class)
|
||||||
|
.query(query)
|
||||||
|
.criteriaBuilder(criteriaBuilder)
|
||||||
|
.keyPathFunc((subQueryRoot) -> subQueryRoot.get(TenantUserEntity._userId))
|
||||||
|
.filterFunc((subQueryRoot, cb) ->
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return cb.and(
|
||||||
|
criteriaBuilder.equal(subQueryRoot.get(TenantUserEntity._tenantId), this.tenantScope.getTenant()),
|
||||||
|
criteriaBuilder.equal(subQueryRoot.get(TenantUserEntity._userId), this.userScope.getUserId()),
|
||||||
|
criteriaBuilder.equal(subQueryRoot.get(TenantUserEntity._isActive), IsActive.Active)
|
||||||
|
);
|
||||||
|
} catch (InvalidApplicationException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
));
|
||||||
|
query.multiselect(root.get(UserEntity._id).alias(UserEntity._id));
|
||||||
|
List<UserEntity> results = this.entityManager.createQuery(query).getResultList();
|
||||||
|
return !results.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postHandle(@NonNull WebRequest request, ModelMap model) {
|
||||||
|
this.tenantScope.setTenant(null, null);
|
||||||
|
this.tenantEntityManager.disableTenantFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(@NonNull WebRequest request, Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
|
||||||
|
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
|
||||||
|
import gr.cite.commons.web.oidc.principal.MyPrincipal;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractorContext;
|
||||||
|
import gr.cite.notification.authorization.ClaimNames;
|
||||||
|
import gr.cite.notification.common.enums.IsActive;
|
||||||
|
import gr.cite.notification.common.scope.tenant.TenantScope;
|
||||||
|
import gr.cite.notification.convention.ConventionService;
|
||||||
|
import gr.cite.notification.data.TenantEntity;
|
||||||
|
import gr.cite.notification.errorcode.ErrorThesaurusProperties;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.context.request.WebRequestInterceptor;
|
||||||
|
|
||||||
|
import javax.management.InvalidApplicationException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TenantScopeClaimInterceptor implements WebRequestInterceptor {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantScopeClaimInterceptor.class));
|
||||||
|
private final TenantScope tenantScope;
|
||||||
|
private final ConventionService conventionService;
|
||||||
|
private final TenantScopeProperties tenantScopeProperties;
|
||||||
|
private final ErrorThesaurusProperties errorThesaurusProperties;
|
||||||
|
private final ClaimExtractor claimExtractor;
|
||||||
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
private final String clientTenantClaimName;
|
||||||
|
private final ClaimExtractorContext claimExtractorContext;
|
||||||
|
private final TenantByCodeCacheService tenantByCodeCacheService;
|
||||||
|
private final TenantByIdCacheService tenantByIdCacheService;
|
||||||
|
@PersistenceContext
|
||||||
|
public EntityManager entityManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TenantScopeClaimInterceptor(
|
||||||
|
TenantScope tenantScope,
|
||||||
|
ConventionService conventionService,
|
||||||
|
ClaimExtractor claimExtractor,
|
||||||
|
CurrentPrincipalResolver currentPrincipalResolver,
|
||||||
|
ErrorThesaurusProperties errorThesaurusProperties,
|
||||||
|
TenantScopeProperties tenantScopeProperties,
|
||||||
|
ClaimExtractorContext claimExtractorContext,
|
||||||
|
TenantByCodeCacheService tenantByCodeCacheService,
|
||||||
|
TenantByIdCacheService tenantByIdCacheService
|
||||||
|
) {
|
||||||
|
this.tenantScope = tenantScope;
|
||||||
|
this.conventionService = conventionService;
|
||||||
|
this.currentPrincipalResolver = currentPrincipalResolver;
|
||||||
|
this.claimExtractor = claimExtractor;
|
||||||
|
this.errorThesaurusProperties = errorThesaurusProperties;
|
||||||
|
this.tenantScopeProperties = tenantScopeProperties;
|
||||||
|
this.claimExtractorContext = claimExtractorContext;
|
||||||
|
this.tenantByCodeCacheService = tenantByCodeCacheService;
|
||||||
|
this.tenantByIdCacheService = tenantByIdCacheService;
|
||||||
|
this.clientTenantClaimName = this.tenantScopeProperties.getClientClaimsPrefix() + ClaimNames.TenantClaimName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preHandle(@NotNull WebRequest request) throws InvalidApplicationException {
|
||||||
|
if (!this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) return;
|
||||||
|
if (!this.tenantScope.isMultitenant()) return;
|
||||||
|
|
||||||
|
MyPrincipal principal = this.currentPrincipalResolver.currentPrincipal();
|
||||||
|
if (principal != null && principal.isAuthenticated() /* principal.Claims.Any() */) {
|
||||||
|
boolean scoped = this.scopeByPrincipal(principal);
|
||||||
|
if (!scoped) scoped = this.scopeByClient(principal);
|
||||||
|
if (!scoped && this.tenantScope.isSet() && this.tenantScopeProperties.getEnforceTrustedTenant())
|
||||||
|
throw new MyForbiddenException(this.errorThesaurusProperties.getMissingTenant().getCode(), this.errorThesaurusProperties.getMissingTenant().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean scopeByPrincipal(MyPrincipal principal) {
|
||||||
|
String tenantCode = this.claimExtractor.tenantString(principal);
|
||||||
|
if (this.conventionService.isNullOrEmpty(tenantCode)) tenantCode = this.claimExtractor.asString(principal, this.clientTenantClaimName);
|
||||||
|
if (tenantCode == null || this.conventionService.isNullOrEmpty(tenantCode)) return false;
|
||||||
|
|
||||||
|
if (tenantCode.equalsIgnoreCase(this.tenantScope.getDefaultTenantCode())){
|
||||||
|
logger.debug("parsed tenant header and set tenant to default tenant");
|
||||||
|
this.tenantScope.setTenant(null, tenantCode);
|
||||||
|
this.claimExtractorContext.putReplaceParameter(TenantScope.TenantReplaceParameter, tenantCode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID tenantId = this.conventionService.parseUUIDSafe(tenantCode);
|
||||||
|
if (tenantId == null) {
|
||||||
|
TenantByCodeCacheService.TenantByCodeCacheValue cacheValue = this.tenantByCodeCacheService.lookup(this.tenantByCodeCacheService.buildKey(tenantCode));
|
||||||
|
if (cacheValue != null) {
|
||||||
|
tenantId = cacheValue.getTenantId();
|
||||||
|
} else {
|
||||||
|
tenantId = this.getTenantIdFromDatabase(tenantCode);
|
||||||
|
this.tenantByCodeCacheService.put(new TenantByCodeCacheService.TenantByCodeCacheValue(tenantCode, tenantId));
|
||||||
|
this.tenantByIdCacheService.put(new TenantByIdCacheService.TenantByIdCacheValue(tenantCode, tenantId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug("tenant claim was set to {}", tenantId);
|
||||||
|
TenantByIdCacheService.TenantByIdCacheValue cacheValue = this.tenantByIdCacheService.lookup(this.tenantByIdCacheService.buildKey(tenantId));
|
||||||
|
|
||||||
|
if (cacheValue != null) {
|
||||||
|
tenantCode = cacheValue.getTenantCode();
|
||||||
|
} else {
|
||||||
|
tenantCode = this.getTenantCodeFromDatabase(tenantId);
|
||||||
|
this.tenantByCodeCacheService.put(new TenantByCodeCacheService.TenantByCodeCacheValue(tenantCode, tenantId));
|
||||||
|
this.tenantByIdCacheService.put(new TenantByIdCacheService.TenantByIdCacheValue(tenantCode, tenantId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantId != null) {
|
||||||
|
logger.debug("parsed tenant header and set tenant id to {}", tenantId);
|
||||||
|
this.tenantScope.setTenant(tenantId, tenantCode);
|
||||||
|
this.claimExtractorContext.putReplaceParameter(TenantScope.TenantReplaceParameter, tenantCode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean scopeByClient(MyPrincipal principal) throws InvalidApplicationException {
|
||||||
|
String client = this.claimExtractor.client(principal);
|
||||||
|
|
||||||
|
Boolean isWhiteListed = this.tenantScopeProperties.getWhiteListedClients() != null && !this.conventionService.isNullOrEmpty(client) && this.tenantScopeProperties.getWhiteListedClients().contains(client);
|
||||||
|
logger.debug("client is whitelisted : {}, scope is set: {}, with value {}", isWhiteListed, this.tenantScope.isSet(), (this.tenantScope.isSet() ? this.tenantScope.getTenant() : null));
|
||||||
|
|
||||||
|
return isWhiteListed && this.tenantScope.isSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID getTenantIdFromDatabase(String tenantCode) {
|
||||||
|
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<TenantEntity> query = criteriaBuilder.createQuery(TenantEntity.class);
|
||||||
|
Root<TenantEntity> root = query.from(TenantEntity.class);
|
||||||
|
query = query.where(
|
||||||
|
criteriaBuilder.and(
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._code), tenantCode),
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._isActive), IsActive.Active)
|
||||||
|
)
|
||||||
|
).multiselect(root.get(TenantEntity._id).alias(TenantEntity._id));
|
||||||
|
List<TenantEntity> results = this.entityManager.createQuery(query).getResultList();
|
||||||
|
if (results.size() == 1) {
|
||||||
|
return results.getFirst().getId();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTenantCodeFromDatabase(UUID tenantId) {
|
||||||
|
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<TenantEntity> query = criteriaBuilder.createQuery(TenantEntity.class);
|
||||||
|
Root<TenantEntity> root = query.from(TenantEntity.class);
|
||||||
|
query = query.where(
|
||||||
|
criteriaBuilder.and(
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._id), tenantId),
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._isActive), IsActive.Active)
|
||||||
|
)
|
||||||
|
).multiselect(root.get(TenantEntity._code).alias(TenantEntity._code));
|
||||||
|
List<TenantEntity> results = this.entityManager.createQuery(query).getResultList();
|
||||||
|
if (results.size() == 1) {
|
||||||
|
return results.getFirst().getCode();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postHandle(@NonNull WebRequest request, ModelMap model) {
|
||||||
|
this.tenantScope.setTenant(null, null);
|
||||||
|
this.claimExtractorContext.removeReplaceParameter(TenantScope.TenantReplaceParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(@NonNull WebRequest request, Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(TenantScopeProperties.class)
|
||||||
|
public class TenantScopeConfiguration {
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
|
||||||
|
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractorContext;
|
||||||
|
import gr.cite.notification.authorization.ClaimNames;
|
||||||
|
import gr.cite.notification.common.enums.IsActive;
|
||||||
|
import gr.cite.notification.common.scope.tenant.TenantScope;
|
||||||
|
import gr.cite.notification.convention.ConventionService;
|
||||||
|
import gr.cite.notification.data.TenantEntity;
|
||||||
|
import gr.cite.tools.logging.LoggerService;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.context.request.WebRequestInterceptor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TenantScopeHeaderInterceptor implements WebRequestInterceptor {
|
||||||
|
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantScopeHeaderInterceptor.class));
|
||||||
|
private final TenantScope tenantScope;
|
||||||
|
private final ConventionService conventionService;
|
||||||
|
private final TenantByCodeCacheService tenantByCodeCacheService;
|
||||||
|
private final TenantByIdCacheService tenantByIdCacheService;
|
||||||
|
private final ClaimExtractorContext claimExtractorContext;
|
||||||
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
@PersistenceContext
|
||||||
|
public EntityManager entityManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TenantScopeHeaderInterceptor(
|
||||||
|
TenantScope tenantScope,
|
||||||
|
ConventionService conventionService,
|
||||||
|
TenantByCodeCacheService tenantByCodeCacheService,
|
||||||
|
TenantByIdCacheService tenantByIdCacheService,
|
||||||
|
ClaimExtractorContext claimExtractorContext,
|
||||||
|
CurrentPrincipalResolver currentPrincipalResolver
|
||||||
|
) {
|
||||||
|
this.tenantScope = tenantScope;
|
||||||
|
this.conventionService = conventionService;
|
||||||
|
this.tenantByCodeCacheService = tenantByCodeCacheService;
|
||||||
|
this.tenantByIdCacheService = tenantByIdCacheService;
|
||||||
|
this.claimExtractorContext = claimExtractorContext;
|
||||||
|
this.currentPrincipalResolver = currentPrincipalResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preHandle(@NotNull WebRequest request) {
|
||||||
|
if (!this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) return;
|
||||||
|
if (!this.tenantScope.isMultitenant()) return;
|
||||||
|
|
||||||
|
String tenantCode = request.getHeader(ClaimNames.TenantClaimName);
|
||||||
|
logger.debug("retrieved request tenant header is: {}", tenantCode);
|
||||||
|
if (tenantCode == null || this.conventionService.isNullOrEmpty(tenantCode)) return;
|
||||||
|
|
||||||
|
if (tenantCode.equalsIgnoreCase(this.tenantScope.getDefaultTenantCode())){
|
||||||
|
logger.debug("parsed tenant header and set tenant to default tenant");
|
||||||
|
this.tenantScope.setTenant(null, tenantCode);
|
||||||
|
this.claimExtractorContext.putReplaceParameter(TenantScope.TenantReplaceParameter, tenantCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID tenantId = this.conventionService.parseUUIDSafe(tenantCode);
|
||||||
|
if (tenantId == null) {
|
||||||
|
TenantByCodeCacheService.TenantByCodeCacheValue cacheValue = this.tenantByCodeCacheService.lookup(this.tenantByCodeCacheService.buildKey(tenantCode));
|
||||||
|
if (cacheValue != null) {
|
||||||
|
tenantId = cacheValue.getTenantId();
|
||||||
|
} else {
|
||||||
|
tenantId = this.getTenantIdFromDatabase(tenantCode);
|
||||||
|
this.tenantByCodeCacheService.put(new TenantByCodeCacheService.TenantByCodeCacheValue(tenantCode, tenantId));
|
||||||
|
this.tenantByIdCacheService.put(new TenantByIdCacheService.TenantByIdCacheValue(tenantCode, tenantId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TenantByIdCacheService.TenantByIdCacheValue cacheValue = this.tenantByIdCacheService.lookup(this.tenantByIdCacheService.buildKey(tenantId));
|
||||||
|
if (cacheValue != null) {
|
||||||
|
tenantCode = cacheValue.getTenantCode();
|
||||||
|
} else {
|
||||||
|
tenantCode = this.getTenantCodeFromDatabase(tenantId);
|
||||||
|
this.tenantByCodeCacheService.put(new TenantByCodeCacheService.TenantByCodeCacheValue(tenantCode, tenantId));
|
||||||
|
this.tenantByIdCacheService.put(new TenantByIdCacheService.TenantByIdCacheValue(tenantCode, tenantId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantId != null) {
|
||||||
|
logger.debug("parsed tenant header and set tenant id to {}", tenantId);
|
||||||
|
this.tenantScope.setTenant(tenantId, tenantCode);
|
||||||
|
this.claimExtractorContext.putReplaceParameter(TenantScope.TenantReplaceParameter, tenantCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID getTenantIdFromDatabase(String tenantCode) {
|
||||||
|
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<TenantEntity> query = criteriaBuilder.createQuery(TenantEntity.class);
|
||||||
|
Root<TenantEntity> root = query.from(TenantEntity.class);
|
||||||
|
query = query.where(
|
||||||
|
criteriaBuilder.and(
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._code), tenantCode),
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._isActive), IsActive.Active)
|
||||||
|
)
|
||||||
|
).multiselect(root.get(TenantEntity._id).alias(TenantEntity._id));
|
||||||
|
List<TenantEntity> results = this.entityManager.createQuery(query).getResultList();
|
||||||
|
if (results.size() == 1) {
|
||||||
|
return results.getFirst().getId();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTenantCodeFromDatabase(UUID tenantId) {
|
||||||
|
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<TenantEntity> query = criteriaBuilder.createQuery(TenantEntity.class);
|
||||||
|
Root<TenantEntity> root = query.from(TenantEntity.class);
|
||||||
|
query = query.where(
|
||||||
|
criteriaBuilder.and(
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._id), tenantId),
|
||||||
|
criteriaBuilder.equal(root.get(TenantEntity._isActive), IsActive.Active)
|
||||||
|
)
|
||||||
|
).multiselect(root.get(TenantEntity._code).alias(TenantEntity._code));
|
||||||
|
List<TenantEntity> results = this.entityManager.createQuery(query).getResultList();
|
||||||
|
if (results.size() == 1) {
|
||||||
|
return results.getFirst().getCode();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postHandle(@NonNull WebRequest request, ModelMap model) {
|
||||||
|
|
||||||
|
this.tenantScope.setTenant(null, null);
|
||||||
|
this.claimExtractorContext.removeReplaceParameter(TenantScope.TenantReplaceParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(@NonNull WebRequest request, Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "tenant.interceptor")
|
||||||
|
public class TenantScopeProperties {
|
||||||
|
|
||||||
|
private String clientClaimsPrefix;
|
||||||
|
public String getClientClaimsPrefix() {
|
||||||
|
return clientClaimsPrefix;
|
||||||
|
}
|
||||||
|
public void setClientClaimsPrefix(String clientClaimsPrefix) {
|
||||||
|
this.clientClaimsPrefix = clientClaimsPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<String> whiteListedClients;
|
||||||
|
public HashSet<String> getWhiteListedClients() {
|
||||||
|
return whiteListedClients;
|
||||||
|
}
|
||||||
|
public void setWhiteListedClients(HashSet<String> whiteListedClients) {
|
||||||
|
this.whiteListedClients = whiteListedClients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> whiteListedEndpoints;
|
||||||
|
public List<String> getWhiteListedEndpoints() {
|
||||||
|
return whiteListedEndpoints;
|
||||||
|
}
|
||||||
|
public void setWhiteListedEndpoints(List<String> whiteListedEndpoints) {
|
||||||
|
this.whiteListedEndpoints = whiteListedEndpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean enforceTrustedTenant;
|
||||||
|
public Boolean getEnforceTrustedTenant() {
|
||||||
|
return enforceTrustedTenant;
|
||||||
|
}
|
||||||
|
public void setEnforceTrustedTenant(Boolean enforceTrustedTenant) {
|
||||||
|
this.enforceTrustedTenant = enforceTrustedTenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import gr.cite.tools.cache.CacheOptions;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "cache.user-allowed-tenant")
|
||||||
|
public class UserAllowedTenantCacheOptions extends CacheOptions {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
package gr.cite.notification.web.scope.tenant;
|
||||||
|
|
||||||
|
import gr.cite.notification.event.UserAddedToTenantEvent;
|
||||||
|
import gr.cite.notification.event.UserRemovedFromTenantEvent;
|
||||||
|
import gr.cite.tools.cache.CacheService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserAllowedTenantCacheService extends CacheService<UserAllowedTenantCacheService.UserAllowedTenantCacheValue> {
|
||||||
|
|
||||||
|
public static class UserAllowedTenantCacheValue {
|
||||||
|
|
||||||
|
public UserAllowedTenantCacheValue() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserAllowedTenantCacheValue(UUID userId, UUID tenantId, boolean isAllowed) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
this.isAllowed = isAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID userId;
|
||||||
|
|
||||||
|
public UUID getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(UUID userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID tenantId;
|
||||||
|
|
||||||
|
public UUID getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(UUID tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllowed;
|
||||||
|
|
||||||
|
public boolean isAllowed() {
|
||||||
|
return isAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowed(boolean allowed) {
|
||||||
|
isAllowed = allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserAllowedTenantCacheService(UserAllowedTenantCacheOptions options) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void handleUserRemovedFromTenantEvent(UserRemovedFromTenantEvent event) {
|
||||||
|
this.evict(this.buildKey(event.getUserId(), event.getTenantId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void handleUserAddedToTenantEvent(UserAddedToTenantEvent event) {
|
||||||
|
this.evict(this.buildKey(event.getUserId(), event.getTenantId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<UserAllowedTenantCacheValue> valueClass() {
|
||||||
|
return UserAllowedTenantCacheValue.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String keyOf(UserAllowedTenantCacheValue value) {
|
||||||
|
return this.buildKey(value.getUserId(), value.getTenantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildKey(UUID userId, UUID tenantId) {
|
||||||
|
HashMap<String, String> keyParts = new HashMap<>();
|
||||||
|
keyParts.put("$user_id$", userId.toString().toLowerCase(Locale.ROOT));
|
||||||
|
keyParts.put("$tenant_id$", tenantId.toString().toLowerCase(Locale.ROOT));
|
||||||
|
return this.generateKey(keyParts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package gr.cite.notification.web.scope.user;
|
||||||
|
|
||||||
|
|
||||||
|
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
|
||||||
|
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
|
||||||
|
import gr.cite.notification.common.scope.user.UserScope;
|
||||||
|
import gr.cite.notification.data.UserCredentialEntity;
|
||||||
|
import gr.cite.notification.model.UserCredential;
|
||||||
|
import gr.cite.notification.query.UserCredentialQuery;
|
||||||
|
import gr.cite.tools.data.query.QueryFactory;
|
||||||
|
import gr.cite.tools.exception.MyForbiddenException;
|
||||||
|
import gr.cite.tools.fieldset.BaseFieldSet;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.context.request.WebRequestInterceptor;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserInterceptor implements WebRequestInterceptor {
|
||||||
|
private final UserScope userScope;
|
||||||
|
private final ClaimExtractor claimExtractor;
|
||||||
|
private final CurrentPrincipalResolver currentPrincipalResolver;
|
||||||
|
private final UserInterceptorCacheService userInterceptorCacheService;
|
||||||
|
private final QueryFactory queryFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserInterceptor(
|
||||||
|
UserScope userScope,
|
||||||
|
ClaimExtractor claimExtractor,
|
||||||
|
CurrentPrincipalResolver currentPrincipalResolver,
|
||||||
|
UserInterceptorCacheService userInterceptorCacheService,
|
||||||
|
QueryFactory queryFactory) {
|
||||||
|
this.userScope = userScope;
|
||||||
|
this.currentPrincipalResolver = currentPrincipalResolver;
|
||||||
|
this.claimExtractor = claimExtractor;
|
||||||
|
this.userInterceptorCacheService = userInterceptorCacheService;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preHandle(@NotNull WebRequest request) {
|
||||||
|
UUID userId = null;
|
||||||
|
if (this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) {
|
||||||
|
String subjectId = this.claimExtractor.subjectString(this.currentPrincipalResolver.currentPrincipal());
|
||||||
|
if (subjectId == null || subjectId.isBlank()) throw new MyForbiddenException("Empty subjects not allowed");
|
||||||
|
|
||||||
|
UserInterceptorCacheService.UserInterceptorCacheValue cacheValue = this.userInterceptorCacheService.lookup(this.userInterceptorCacheService.buildKey(subjectId));
|
||||||
|
if (cacheValue != null) {
|
||||||
|
userId = cacheValue.getUserId();
|
||||||
|
} else {
|
||||||
|
userId = this.findExistingUserFromDb(subjectId);
|
||||||
|
if (userId != null) {
|
||||||
|
cacheValue = new UserInterceptorCacheService.UserInterceptorCacheValue(subjectId, userId);
|
||||||
|
this.userInterceptorCacheService.put(cacheValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.userScope.setUserId(userId);
|
||||||
|
}
|
||||||
|
private UUID findExistingUserFromDb(String subjectId) {
|
||||||
|
UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).disableTracking().externalIds(subjectId).firstAs(new BaseFieldSet().ensure(UserCredential._user));
|
||||||
|
if (userCredential != null) {
|
||||||
|
return userCredential.getUserId();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postHandle(@NonNull WebRequest request, ModelMap model) {
|
||||||
|
this.userScope.setUserId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(@NonNull WebRequest request, Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package gr.cite.notification.web.scope.user;
|
||||||
|
|
||||||
|
import gr.cite.tools.cache.CacheOptions;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "cache.user-by-subject-id")
|
||||||
|
public class UserInterceptorCacheOptions extends CacheOptions {
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package gr.cite.notification.web.scope.user;
|
||||||
|
|
||||||
|
import gr.cite.notification.convention.ConventionService;
|
||||||
|
import gr.cite.notification.event.UserCredentialTouchedEvent;
|
||||||
|
import gr.cite.tools.cache.CacheService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserInterceptorCacheService extends CacheService<UserInterceptorCacheService.UserInterceptorCacheValue> {
|
||||||
|
private final ConventionService conventionService;
|
||||||
|
|
||||||
|
public static class UserInterceptorCacheValue {
|
||||||
|
|
||||||
|
public UserInterceptorCacheValue() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserInterceptorCacheValue(String subjectId, UUID userId) {
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getSubjectId() {
|
||||||
|
return subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubjectId(String subjectId) {
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String subjectId;
|
||||||
|
private UUID userId;
|
||||||
|
|
||||||
|
public UUID getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(UUID userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void handleTenantTouchedEvent(UserCredentialTouchedEvent event) {
|
||||||
|
if (!this.conventionService.isNullOrEmpty(event.getSubjectId()))
|
||||||
|
this.evict(this.buildKey(event.getSubjectId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserInterceptorCacheService(UserInterceptorCacheOptions options, ConventionService conventionService1) {
|
||||||
|
super(options);
|
||||||
|
this.conventionService = conventionService1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<UserInterceptorCacheValue> valueClass() {
|
||||||
|
return UserInterceptorCacheValue.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String keyOf(UserInterceptorCacheValue value) {
|
||||||
|
return this.buildKey(value.getSubjectId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String buildKey(String subject) {
|
||||||
|
HashMap<String, String> keyParts = new HashMap<>();
|
||||||
|
keyParts.put("$subject$", subject);
|
||||||
|
return this.generateKey(keyParts);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
spring:
|
||||||
|
jackson:
|
||||||
|
default-property-inclusion: non_null
|
||||||
|
config:
|
||||||
|
import: optional:classpath:config/app.env[.properties], optional:file:../config/app.env[.properties],
|
||||||
|
optional:classpath:config/db.yml[.yml], optional:classpath:config/db-${spring.profiles.active}.yml[.yml], optional:file:../config/db-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/permissions.yml[.yml], optional:classpath:config/permissions-${spring.profiles.active}.yml[.yml], optional:file:../config/permissions-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/errors.yml[.yml], optional:classpath:config/errors-${spring.profiles.active}.yml[.yml], optional:file:../config/errors-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/security.yml[.yml], optional:classpath:config/security-${spring.profiles.active}.yml[.yml], optional:file:../config/security-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/server.yml[.yml], optional:classpath:config/server-${spring.profiles.active}.yml[.yml], optional:file:../config/server-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/logging.yml[.yml], optional:classpath:config/logging-${spring.profiles.active}.yml[.yml], optional:file:../config/logging-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/idpclaims.yml[.yml], optional:classpath:config/idpclaims-${spring.profiles.active}.yml[.yml], optional:file:../config/idpclaims-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/cache.yml[.yml], optional:classpath:config/cache-${spring.profiles.active}.yml[.yml], optional:file:../config/cache-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/tenant.yml[.yml], optional:classpath:config/tenant-${spring.profiles.active}.yml[.yml], optional:file:../config/tenant-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/locale.yml[.yml], optional:classpath:config/locale-${spring.profiles.active}.yml[.yml], optional:file:../config/locale-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/cors.yml[.yml], optional:classpath:config/cors-${spring.profiles.active}.yml[.yml], optional:file:../config/cors-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/notification.yml[.yml], optional:classpath:config/notification-${spring.profiles.active}.yml[.yml], optional:file:../config/notification-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/email.yml[.yml], optional:classpath:config/email-${spring.profiles.active}.yml[.yml], optional:file:../config/email-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/queue.yml[.yml], optional:classpath:config/queue-${spring.profiles.active}.yml[.yml], optional:file:../config/queue-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/cipher.yml[.yml], optional:classpath:config/cipher-${spring.profiles.active}.yml[.yml], optional:file:../config/cipher-${spring.profiles.active}.yml[.yml],
|
||||||
|
optional:classpath:config/formatting.yml[.yml], optional:classpath:config/formatting-${spring.profiles.active}.yml[.yml], optional:file:../config/formatting-${spring.profiles.active}.yml[.yml]
|
|
@ -0,0 +1,62 @@
|
||||||
|
cache:
|
||||||
|
manager:
|
||||||
|
fallbackToNoOpCache: true
|
||||||
|
caffeineCaches:
|
||||||
|
- names: [ apikey ]
|
||||||
|
allowNullValues: true
|
||||||
|
initialCapacity: 100
|
||||||
|
maximumSize: 500
|
||||||
|
enableRecordStats: false
|
||||||
|
expireAfterWriteSeconds: 600
|
||||||
|
- names: [ tenantByCode ]
|
||||||
|
allowNullValues: true
|
||||||
|
initialCapacity: 100
|
||||||
|
maximumSize: 500
|
||||||
|
enableRecordStats: false
|
||||||
|
expireAfterWriteSeconds: 20
|
||||||
|
- names: [ tenantById ]
|
||||||
|
allowNullValues: true
|
||||||
|
initialCapacity: 100
|
||||||
|
maximumSize: 500
|
||||||
|
enableRecordStats: false
|
||||||
|
expireAfterWriteSeconds: 20
|
||||||
|
- names: [ userBySubjectId ]
|
||||||
|
allowNullValues: true
|
||||||
|
initialCapacity: 100
|
||||||
|
maximumSize: 5000
|
||||||
|
enableRecordStats: false
|
||||||
|
expireAfterWriteSeconds: 20
|
||||||
|
- names: [ userAccessTenant ]
|
||||||
|
allowNullValues: true
|
||||||
|
initialCapacity: 100
|
||||||
|
maximumSize: 500
|
||||||
|
enableRecordStats: false
|
||||||
|
expireAfterWriteSeconds: 20
|
||||||
|
- names: [ formattingUserProfile ]
|
||||||
|
allowNullValues: true
|
||||||
|
initialCapacity: 100
|
||||||
|
maximumSize: 500
|
||||||
|
enableRecordStats: false
|
||||||
|
expireAfterWriteSeconds: 20
|
||||||
|
mapCaches:
|
||||||
|
apiKey:
|
||||||
|
name: apikey
|
||||||
|
keyPattern: ntf_resolve_$keyhash$:v0
|
||||||
|
userBySubjectId:
|
||||||
|
name: userBySubjectId
|
||||||
|
keyPattern: ntf_user_by_subject_$subject$:v0
|
||||||
|
tenantByCode:
|
||||||
|
name: tenantByCode
|
||||||
|
keyPattern: ntf_tenant_by_code_$code$:v0
|
||||||
|
tenantById:
|
||||||
|
name: tenantById
|
||||||
|
keyPattern: ntf_tenant_by_id_$tenantId$:v0
|
||||||
|
userAllowedTenant:
|
||||||
|
name: userAccessTenant
|
||||||
|
keyPattern: ntf_user_access_tenant_$user_id$_$tenant_id$:v0
|
||||||
|
formattingUserProfileCache:
|
||||||
|
name: formattingUserProfile
|
||||||
|
keyPattern: ntf_formatting_user_profile$user_id$:v0
|
||||||
|
template:
|
||||||
|
name: template
|
||||||
|
key-pattern: ------
|
|
@ -0,0 +1,35 @@
|
||||||
|
cipher-profiles:
|
||||||
|
profile-map:
|
||||||
|
configuration-profile-name: "configuration"
|
||||||
|
queue-profile-name: "queue"
|
||||||
|
notification-profile-name: "queue"
|
||||||
|
|
||||||
|
cipher:
|
||||||
|
# salted-hash:
|
||||||
|
# default-o: null
|
||||||
|
# options: null
|
||||||
|
symetric-encryption:
|
||||||
|
default-o: null
|
||||||
|
options:
|
||||||
|
configuration:
|
||||||
|
aes:
|
||||||
|
key: ${CIPHER_SYMETRIC_ENCRYPTION_CONFIGURATION_AES_KEY:}
|
||||||
|
iv: ${CIPHER_SYMETRIC_ENCRYPTION_CONFIGURATION_AES_IV:}
|
||||||
|
queue:
|
||||||
|
aes:
|
||||||
|
key: ${CIPHER_SYMETRIC_ENCRYPTION_QUEUE_AES_KEY:}
|
||||||
|
iv: ${CIPHER_SYMETRIC_ENCRYPTION_QUEUE_AES_IV:}
|
||||||
|
masking:
|
||||||
|
default: null
|
||||||
|
options:
|
||||||
|
configuration:
|
||||||
|
character: "*"
|
||||||
|
clear-begining: 2
|
||||||
|
clear-ending: 4
|
||||||
|
at-least-percentage: 70
|
||||||
|
digital-signature:
|
||||||
|
default: null
|
||||||
|
options:
|
||||||
|
configuration:
|
||||||
|
certificate-path: null
|
||||||
|
certificate-password: null
|
|
@ -0,0 +1,3 @@
|
||||||
|
web:
|
||||||
|
cors:
|
||||||
|
allowed-origins: [ http://localhost, http://localhost:4200 ]
|
|
@ -0,0 +1,7 @@
|
||||||
|
web:
|
||||||
|
cors:
|
||||||
|
enabled: true
|
||||||
|
allowed-methods: [ HEAD, GET, POST, PUT, DELETE, PATCH ]
|
||||||
|
allowed-headers: [ Authorization, Cache-Control, Content-Type, Content-Disposition, x-tenant ]
|
||||||
|
exposed-headers: [ Authorization, Cache-Control, Content-Type, Content-Disposition ]
|
||||||
|
allow-credentials: false
|
|
@ -0,0 +1,7 @@
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
show_sql: true
|
||||||
|
format_sql: false
|
|
@ -0,0 +1,29 @@
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
properties:
|
||||||
|
org:
|
||||||
|
hibernate:
|
||||||
|
flushMode: MANUAL
|
||||||
|
hibernate:
|
||||||
|
globally_quoted_identifiers: true
|
||||||
|
ddl-auto: validate
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
hibernate:
|
||||||
|
naming:
|
||||||
|
physical-strategy: gr.cite.notification.data.namingstrategy.PrefixPhysicalNamingStrategy
|
||||||
|
implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
|
||||||
|
datasource:
|
||||||
|
url: ${DB_CONNECTION_STRING}
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
hikari:
|
||||||
|
connection-timeout: 30000
|
||||||
|
minimum-idle: 3
|
||||||
|
maximum-pool-size: 10
|
||||||
|
idle-timeout: 600000
|
||||||
|
max-lifetime: 1800000
|
||||||
|
|
||||||
|
|
||||||
|
naming-strategy:
|
||||||
|
prefix: ntf_
|
|
@ -0,0 +1,14 @@
|
||||||
|
spring:
|
||||||
|
mail:
|
||||||
|
host: ${MAIL_HOST}
|
||||||
|
port: ${MAIL_PORT}
|
||||||
|
username: ${MAIL_USERNAME}
|
||||||
|
password: ${MAIL_PASSWORD}
|
||||||
|
properties:
|
||||||
|
mail:
|
||||||
|
smtp:
|
||||||
|
auth: ${MAIL_AUTH}
|
||||||
|
starttls:
|
||||||
|
enable: ${MAIL_TLS}
|
||||||
|
email:
|
||||||
|
address: ${MAIL_ADDRESS}
|
|
@ -0,0 +1,59 @@
|
||||||
|
error-thesaurus:
|
||||||
|
# common errors start with 1..
|
||||||
|
hash-conflict:
|
||||||
|
code: 100
|
||||||
|
message: there is a hash conflict for the item modified. please reload to get the latest changes
|
||||||
|
forbidden:
|
||||||
|
code: 101
|
||||||
|
message: insufficient rights
|
||||||
|
system-error:
|
||||||
|
code: 102
|
||||||
|
message: an unexpected system error occurred
|
||||||
|
missing-tenant:
|
||||||
|
code: 103
|
||||||
|
message: tenant scope not provided
|
||||||
|
model-validation:
|
||||||
|
code: 106
|
||||||
|
message: validation error
|
||||||
|
tenant-not-allowed:
|
||||||
|
code: 112
|
||||||
|
message: tenant not allowed
|
||||||
|
tenant-tampering:
|
||||||
|
code: 127
|
||||||
|
message: Tenant tampering
|
||||||
|
tenant-configuration-type-can-not-change:
|
||||||
|
code: 128
|
||||||
|
message: Tenant configuration type can not change
|
||||||
|
multiple-tenant-configuration-type-not-allowed:
|
||||||
|
code: 129
|
||||||
|
message: Multiple Tenant Configuration Type Not Allowed
|
||||||
|
|
||||||
|
# annotations¬ification errors start with 2..
|
||||||
|
invalid-api-key:
|
||||||
|
code: 200
|
||||||
|
message: provided APIKey not valid
|
||||||
|
stale-api-key:
|
||||||
|
code: 201
|
||||||
|
message: there was a problem authorizing you with your API key. Please try again. Contact the system administrator if the problem persists
|
||||||
|
sensitive-info:
|
||||||
|
code: 202
|
||||||
|
message: you are attempting to access sensitive information. please don't do that
|
||||||
|
non-person-principal:
|
||||||
|
code: 203
|
||||||
|
message: the operation is available only to person users
|
||||||
|
blocking-consent:
|
||||||
|
code: 204
|
||||||
|
message: user consents are not sufficient to complete the operation
|
||||||
|
single-tenant-configuration-per-type-supported:
|
||||||
|
code: 205
|
||||||
|
message: a single tenant configuration entry per config type is supported
|
||||||
|
incompatible-tenant-configuration-types:
|
||||||
|
code: 206
|
||||||
|
message: the provided tenant configuration type is incompatible
|
||||||
|
missing-totp-token:
|
||||||
|
code: 207
|
||||||
|
message: totp token not provided
|
||||||
|
overlapping-tenant-configuration-notifier-list:
|
||||||
|
code: 208
|
||||||
|
message: Overlapping Tenant Configuration Notifier List
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
formatting:
|
||||||
|
options:
|
||||||
|
integer-format: "%,d"
|
||||||
|
decimal-digits-round: 2
|
||||||
|
decimal-format: "#0.00"
|
||||||
|
date-time-format: "YYYY-MM-dd'T'HH:mm:ss"
|
|
@ -0,0 +1,58 @@
|
||||||
|
idpclient:
|
||||||
|
claims:
|
||||||
|
mapping:
|
||||||
|
Subject:
|
||||||
|
- type: sub
|
||||||
|
Name:
|
||||||
|
- type: name
|
||||||
|
Client:
|
||||||
|
- type: client_id
|
||||||
|
AuthenticationMethod:
|
||||||
|
- type: amr
|
||||||
|
NotBefore:
|
||||||
|
- type: nbf
|
||||||
|
AuthenticatedAt:
|
||||||
|
- type: auth_time
|
||||||
|
ExpiresAt:
|
||||||
|
- type: exp
|
||||||
|
Email:
|
||||||
|
- type: email
|
||||||
|
Roles:
|
||||||
|
- type: resource_access
|
||||||
|
path: dmp_web.roles
|
||||||
|
- type: tenant_roles
|
||||||
|
filterBy: "(.*):::TenantCode::"
|
||||||
|
extractByExpression: "(.*):(.*)"
|
||||||
|
extractExpressionValue: "[[g1]]"
|
||||||
|
GlobalRoles:
|
||||||
|
- type: resource_access
|
||||||
|
path: dmp_web.roles
|
||||||
|
TenantRoles:
|
||||||
|
- type: tenant_roles
|
||||||
|
filterBy: "(.*):::TenantCode::"
|
||||||
|
extractByExpression: "(.*):(.*)"
|
||||||
|
extractExpressionValue: "[[g1]]"
|
||||||
|
Scope:
|
||||||
|
- type: scope
|
||||||
|
AccessToken:
|
||||||
|
- type: x-access-token
|
||||||
|
visibility: SENSITIVE
|
||||||
|
Tenant:
|
||||||
|
- type: x-tenant
|
||||||
|
IssuedAt:
|
||||||
|
- type: iat
|
||||||
|
Issuer:
|
||||||
|
- type: iss
|
||||||
|
Audience:
|
||||||
|
- type: aud
|
||||||
|
TokenType:
|
||||||
|
- type: typ
|
||||||
|
AuthorizedParty:
|
||||||
|
- type: azp
|
||||||
|
Authorities:
|
||||||
|
- type: authorities
|
||||||
|
TenantCodes:
|
||||||
|
- type: tenant_roles
|
||||||
|
filterBy: "(.*):(.*)"
|
||||||
|
extractByExpression: "(.*):(.*)"
|
||||||
|
extractExpressionValue: "[[g2]]"
|
|
@ -0,0 +1,4 @@
|
||||||
|
locale:
|
||||||
|
timezone: UTC
|
||||||
|
language: en
|
||||||
|
culture: en-US
|
|
@ -0,0 +1,2 @@
|
||||||
|
logging:
|
||||||
|
config: classpath:logging/logback-dev.xml
|
|
@ -0,0 +1,35 @@
|
||||||
|
logging:
|
||||||
|
context:
|
||||||
|
request:
|
||||||
|
requestIdKey: req.id
|
||||||
|
requestRemoteHostKey: req.remoteHost
|
||||||
|
requestUriKey: req.requestURI
|
||||||
|
requestQueryStringKey: req.queryString
|
||||||
|
requestUrlKey : req.requestURL
|
||||||
|
requestMethodKey: req.method
|
||||||
|
requestUserAgentKey: req.userAgent
|
||||||
|
requestForwardedForKey: req.xForwardedFor
|
||||||
|
requestSchemeKey: req.scheme
|
||||||
|
requestRemoteAddressKey: req.remoteAddr
|
||||||
|
requestRemotePortKey: req.remotePort
|
||||||
|
requestRemoteUserKey: req.remoteUser
|
||||||
|
principal:
|
||||||
|
subjectKey: usr.subject
|
||||||
|
nameKey: usr.name
|
||||||
|
clientKey: usr.client
|
||||||
|
audit:
|
||||||
|
enable: true
|
||||||
|
requestRemoteHostKey: req.remoteHost
|
||||||
|
requestUriKey: req.requestURI
|
||||||
|
requestQueryStringKey: req.queryString
|
||||||
|
requestUrlKey : req.requestURL
|
||||||
|
requestMethodKey: req.method
|
||||||
|
requestUserAgentKey: req.userAgent
|
||||||
|
requestForwardedForKey: req.xForwardedFor
|
||||||
|
requestSchemeKey: req.scheme
|
||||||
|
requestRemoteAddressKey: req.remoteAddr
|
||||||
|
requestRemotePortKey: req.remotePort
|
||||||
|
requestRemoteUserKey: req.remoteUser
|
||||||
|
principalSubjectKey: usr.subject
|
||||||
|
principalNameKey: usr.name
|
||||||
|
principalClientKey: usr.client
|
|
@ -0,0 +1,728 @@
|
||||||
|
notification:
|
||||||
|
staticFields:
|
||||||
|
fields:
|
||||||
|
- key: "{installation-url}"
|
||||||
|
type: "String"
|
||||||
|
value: "http://localhost:4200"
|
||||||
|
resolver:
|
||||||
|
global-policies:
|
||||||
|
- #planInvitationExternalUser
|
||||||
|
type: 065DEECD-21BB-44AF-9983-E660FDF24BC4
|
||||||
|
contacts: [ email ]
|
||||||
|
- #planInvitationExistingUser
|
||||||
|
type: 4904dea2-5079-46d3-83be-3a19c9ab45dc
|
||||||
|
contacts: [ email, inapp ]
|
||||||
|
- #dpmModified
|
||||||
|
type: 4542262A-22F8-4BAA-9DB6-1C8E70AC1DBB
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #planFinalised
|
||||||
|
type: 90DB0B46-42DE-BD89-AEBF-6F27EFEB256E
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #descriptionCreated
|
||||||
|
type: 8965b1d5-99a6-4acf-9016-c0d0ce341364
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #descriptionModified
|
||||||
|
type: 4FDBFA80-7A71-4A69-B854-67CBB70648F1
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #descriptionFinalised
|
||||||
|
type: 33790bad-94d4-488a-8ee2-7f6295ca18ea
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #descriptionAnnotationCreated
|
||||||
|
type: db1e99d2-a240-4e75-9bb2-ef25b234c1f0
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #mergeAcountConfirmation
|
||||||
|
type: BFE68845-CB05-4C5A-A03D-29161A7C9660
|
||||||
|
contacts: [ email ]
|
||||||
|
- #removeCredentialConfirmation
|
||||||
|
type: C9BC3F16-057E-4BBA-8A5F-36BD835E5604
|
||||||
|
contacts: [ email ]
|
||||||
|
- #planDeposit
|
||||||
|
type: 55736F7A-83AB-4190-AF43-9D031A6F9612
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #descriptionTemplateInvitation
|
||||||
|
type: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
- #contactSupportType
|
||||||
|
type: 5B1D6C52-88F9-418B-9B8A-6F1F963D9EAD
|
||||||
|
contacts: [ email ]
|
||||||
|
- #publicContactSupportType
|
||||||
|
type: B542B606-ACC6-4629-ADEF-4D8EE2F01222
|
||||||
|
contacts: [ email ]
|
||||||
|
- #tenantSpecificInvitationExternalUserType
|
||||||
|
type: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be
|
||||||
|
contacts: [ email ]
|
||||||
|
- #tenantSpecificInvitationExistingUserType
|
||||||
|
type: b3809c17-d1e4-420a-919c-828564114191
|
||||||
|
contacts: [ inapp, email ]
|
||||||
|
message:
|
||||||
|
email:
|
||||||
|
flows:
|
||||||
|
- #planInvitationExternalUser
|
||||||
|
key: 065DEECD-21BB-44AF-9983-E660FDF24BC4
|
||||||
|
subject-path: classpath:notification_templates/planinvitationexternaluser/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planinvitationexternaluser/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{planname}", "{planrole}", "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{planname}]': null
|
||||||
|
'[{planrole}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planInvitationExistingUser
|
||||||
|
key: 4904dea2-5079-46d3-83be-3a19c9ab45dc
|
||||||
|
subject-path: classpath:notification_templates/planinvitationexistinguser/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planinvitationexistinguser/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{planname}", "{planrole}", "{reasonName}", "{installation-url}", "{id}"]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{planname}]': null
|
||||||
|
'[{planrole}]': null
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planModified
|
||||||
|
key: 4542262A-22F8-4BAA-9DB6-1C8E70AC1DBB
|
||||||
|
subject-path: classpath:notification_templates/planmodified/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planmodified/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planFinalised
|
||||||
|
key: 90DB0B46-42DE-BD89-AEBF-6F27EFEB256E
|
||||||
|
subject-path: classpath:notification_templates/planfinalised/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planfinalised/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #desriptionCreated
|
||||||
|
key: 8965b1d5-99a6-4acf-9016-c0d0ce341364
|
||||||
|
subject-path: classpath:notification_templates/descriptioncreated/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptioncreated/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #desriptionModified
|
||||||
|
key: 4FDBFA80-7A71-4A69-B854-67CBB70648F1
|
||||||
|
subject-path: classpath:notification_templates/descriptionmodified/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptionmodified/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #descriptionFinalised
|
||||||
|
key: 33790bad-94d4-488a-8ee2-7f6295ca18ea
|
||||||
|
subject-path: classpath:notification_templates/descriptionfinalised/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptionfinalised/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #descriptionAnnotationCreated
|
||||||
|
key: db1e99d2-a240-4e75-9bb2-ef25b234c1f0
|
||||||
|
subject-path: classpath:notification_templates/descriptionannotationcreated/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptionannotationcreated/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #mergeAccountConfirmation
|
||||||
|
key: BFE68845-CB05-4C5A-A03D-29161A7C9660
|
||||||
|
subject-path: classpath:notification_templates/mergeacountconfirmation/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/mergeacountconfirmation/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{userName}", "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: ---
|
||||||
|
formatting:
|
||||||
|
'[{userName}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{expiration_time}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #removeCredentialConfirmation
|
||||||
|
key: C9BC3F16-057E-4BBA-8A5F-36BD835E5604
|
||||||
|
subject-path: classpath:notification_templates/removecredentialconfirmation/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/removecredentialconfirmation/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional:
|
||||||
|
- key: "{email}"
|
||||||
|
value: email
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: --
|
||||||
|
formatting:
|
||||||
|
'[{email}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planDeposit
|
||||||
|
key: 55736F7A-83AB-4190-AF43-9D031A6F9612
|
||||||
|
subject-path: classpath:notification_templates/plandeposit/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/plandeposit/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #descriptionTemplateInvitation
|
||||||
|
key: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B
|
||||||
|
subject-path: classpath:notification_templates/descriptiontemplateinvitation/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptiontemplateinvitation/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{templateName}", "{installation-url}", "{templateID}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{templateName}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #contactSupportType
|
||||||
|
key: 5B1D6C52-88F9-418B-9B8A-6F1F963D9EAD
|
||||||
|
subject-path: classpath:notification_templates/contactsupport/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ "{subject}" ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/contactsupport/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{description}", "{email}" ]
|
||||||
|
formatting:
|
||||||
|
'[{subject}]': null
|
||||||
|
'[{description}]': null
|
||||||
|
'[{email}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #publicContactSupportType
|
||||||
|
key: B542B606-ACC6-4629-ADEF-4D8EE2F01222
|
||||||
|
subject-path: classpath:notification_templates/publiccontactsupport/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ "{subject}" ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/publiccontactsupport/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{description}", "{email}", "{name}" ]
|
||||||
|
formatting:
|
||||||
|
'[{subject}]': null
|
||||||
|
'[{description}]': null
|
||||||
|
'[{email}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #tenantSpecificInvitationExternalUserType
|
||||||
|
key: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be
|
||||||
|
subject-path: classpath:notification_templates/tenantspecificinvitationexternaluser/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/tenantspecificinvitationexternaluser/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{userName}", "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: ---
|
||||||
|
- key: "{tenantName}"
|
||||||
|
value: OpenCDMP
|
||||||
|
formatting:
|
||||||
|
'[{userName}]': null
|
||||||
|
'[{tenantName}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{expiration_time}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #tenantSpecificInvitationExistingUserType
|
||||||
|
key: b3809c17-d1e4-420a-919c-828564114191
|
||||||
|
subject-path: classpath:notification_templates/tenantspecificinvitationexistinguser/email/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/tenantspecificinvitationexistinguser/email/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{userName}", "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: ---
|
||||||
|
- key: "{tenantName}"
|
||||||
|
value: OpenCDMP
|
||||||
|
formatting:
|
||||||
|
'[{userName}]': null
|
||||||
|
'[{tenantName}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{expiration_time}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
template-cache:
|
||||||
|
prefix: ${CACHE_DISAMBIGUATION:}
|
||||||
|
key-pattern: "{prefix}:Notification_Message_Email_Template:{key}:v0"
|
||||||
|
in-app:
|
||||||
|
flows:
|
||||||
|
- #planInvitationExistingUser
|
||||||
|
key: 4904dea2-5079-46d3-83be-3a19c9ab45dc
|
||||||
|
subject-path: classpath:notification_templates/planinvitationexistinguser/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planinvitationexistinguser/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{planname}", "{planrole}", "{reasonName}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{planname}]': null
|
||||||
|
'[{planrole}]': null
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planModified
|
||||||
|
key: 4542262A-22F8-4BAA-9DB6-1C8E70AC1DBB
|
||||||
|
subject-path: classpath:notification_templates/planmodified/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planmodified/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{id}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planFinalised
|
||||||
|
key: 90DB0B46-42DE-BD89-AEBF-6F27EFEB256E
|
||||||
|
subject-path: classpath:notification_templates/planfinalised/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/planfinalised/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{id}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #desriptionCreated
|
||||||
|
key: 8965b1d5-99a6-4acf-9016-c0d0ce341364
|
||||||
|
subject-path: classpath:notification_templates/descriptioncreated/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptioncreated/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #desriptionModified
|
||||||
|
key: 4FDBFA80-7A71-4A69-B854-67CBB70648F1
|
||||||
|
subject-path: classpath:notification_templates/descriptionmodified/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptionmodified/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{id}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #descriptionFinalised
|
||||||
|
key: 33790bad-94d4-488a-8ee2-7f6295ca18ea
|
||||||
|
subject-path: classpath:notification_templates/descriptionfinalised/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptionfinalised/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{id}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #descriptionAnnotationCreated
|
||||||
|
key: db1e99d2-a240-4e75-9bb2-ef25b234c1f0
|
||||||
|
subject-path: classpath:notification_templates/descriptionannotationcreated/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptionannotationcreated/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{id}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #mergeAccountConfirmation
|
||||||
|
key: BFE68845-CB05-4C5A-A03D-29161A7C9660
|
||||||
|
subject-path: classpath:notification_templates/mergeacountconfirmation/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/mergeacountconfirmation/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{userName}", "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: ---
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{userName}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{confirmationToken}]': null
|
||||||
|
'[{expiration_time}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #removeCredentialConfirmation
|
||||||
|
key: C9BC3F16-057E-4BBA-8A5F-36BD835E5604
|
||||||
|
subject-path: classpath:notification_templates/removecredentialconfirmation/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/removecredentialconfirmation/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional:
|
||||||
|
- key: "{email}"
|
||||||
|
value: email
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: --
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{email}]': null
|
||||||
|
'[{expiration_time}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #planDeposit
|
||||||
|
key: 55736F7A-83AB-4190-AF43-9D031A6F9612
|
||||||
|
subject-path: classpath:notification_templates/plandeposit/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/plandeposit/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{reasonName}", "{name}", "{installation-url}", "{id}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{reasonName}]': null
|
||||||
|
'[{name}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{id}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #descriptionTemplateInvitation
|
||||||
|
key: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B
|
||||||
|
subject-path: classpath:notification_templates/descriptiontemplateinvitation/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/descriptiontemplateinvitation/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{templateName}", "{installation-url}", "{templateID}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{recipient}"
|
||||||
|
value:
|
||||||
|
- key: "{tenant-url-path}"
|
||||||
|
value:
|
||||||
|
formatting:
|
||||||
|
'[{templateName}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{templateID}]': null
|
||||||
|
'[{recipient}]': null
|
||||||
|
'[{tenant-url-path}]': null
|
||||||
|
priority-key: null
|
||||||
|
cipher-fields: [ ]
|
||||||
|
- #tenantSpecificInvitationExistingUserType
|
||||||
|
key: b3809c17-d1e4-420a-919c-828564114191
|
||||||
|
subject-path: classpath:notification_templates/tenantspecificinvitationexistinguser/inapp/subject.{language}.txt
|
||||||
|
subject-field-options:
|
||||||
|
mandatory: [ ]
|
||||||
|
optional: [ ]
|
||||||
|
body-path: classpath:notification_templates/tenantspecificinvitationexistinguser/inapp/body.{language}.html
|
||||||
|
body-field-options:
|
||||||
|
mandatory: [ "{userName}", "{installation-url}", "{confirmationToken}" ]
|
||||||
|
optional:
|
||||||
|
- key: "{expiration_time}"
|
||||||
|
value: ---
|
||||||
|
- key: "{tenantName}"
|
||||||
|
value: OpenCDMP
|
||||||
|
formatting:
|
||||||
|
'[{userName}]': null
|
||||||
|
'[{tenantName}]': null
|
||||||
|
'[{installation-url}]': null
|
||||||
|
'[{expiration_time}]': null
|
||||||
|
cc: [ ]
|
||||||
|
cc-mode: 0
|
||||||
|
bcc: [ ]
|
||||||
|
bcc-mode: 0
|
||||||
|
allow-attachments: false
|
||||||
|
cipher-fields: [ ]
|
||||||
|
template-cache:
|
||||||
|
prefix: ${CACHE_DISAMBIGUATION}
|
||||||
|
key-pattern: "{prefix}:Notification_Message_InApp_Template:{key}:v0"
|
|
@ -0,0 +1,17 @@
|
||||||
|
notification:
|
||||||
|
task:
|
||||||
|
processor:
|
||||||
|
enable: true
|
||||||
|
interval-seconds: 5
|
||||||
|
options:
|
||||||
|
retry-threshold: 300
|
||||||
|
max-retry-delay-seconds: 10800
|
||||||
|
too-old-to-send-seconds: 36000
|
||||||
|
too-old-to-track-seconds: 604800
|
||||||
|
overrides: []
|
||||||
|
ad-hoc-config:
|
||||||
|
ad-hoc-notification-type: null
|
||||||
|
override-cache:
|
||||||
|
template-cache:
|
||||||
|
prefix: ${CACHE_DISAMBIGUATION:}
|
||||||
|
key-pattern: "{prefix}:Notification_Override_Template:{tenant}:{type}:{channel}:{{language}}:v0"
|
|
@ -0,0 +1,193 @@
|
||||||
|
permissions:
|
||||||
|
extendedClaims: [ ]
|
||||||
|
policies:
|
||||||
|
|
||||||
|
# Tenants
|
||||||
|
BrowseTenant:
|
||||||
|
roles: []
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditTenant:
|
||||||
|
roles: []
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteTenant:
|
||||||
|
roles: []
|
||||||
|
claims: [ ]
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
AllowNoTenant:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- InstallationAdmin
|
||||||
|
claims: [ ]
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
# Users
|
||||||
|
BrowseUser:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditUser:
|
||||||
|
roles: []
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteUser:
|
||||||
|
roles: []
|
||||||
|
claims: [ ]
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
# UserContactInfo
|
||||||
|
BrowseUserContactInfo:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- InstallationAdmin
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditUserContactInfo:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- InstallationAdmin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteUserContactInfo:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- InstallationAdmin
|
||||||
|
claims: [ ]
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
#Notification
|
||||||
|
BrowseNotification:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditNotification:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteNotification:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
# TenantConfiguration
|
||||||
|
BrowseTenantConfiguration:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
claims: [ ]
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditTenantConfiguration:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteTenantConfiguration:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
clients: [ "opencdmp-api-dev" ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
#User Notification Preference
|
||||||
|
BrowseUserNotificationPreference:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditUserNotificationPreference:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
|
||||||
|
# ViewPage Permissions
|
||||||
|
ViewNotificationPage:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
ViewNotificationEventRulePage:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
ViewInAppNotificationPage:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
ViewNotificationTemplatePage:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
- TenantConfigManager
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
|
||||||
|
# Notification Template Permissions
|
||||||
|
BrowseNotificationTemplate:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
- TenantConfigManager
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
EditNotificationTemplate:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
- TenantConfigManager
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteNotificationTemplate:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
- TenantAdmin
|
||||||
|
- TenantConfigManager
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
|
||||||
|
# In App Notification Permissions
|
||||||
|
BrowseInAppNotification:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
||||||
|
DeleteInAppNotification:
|
||||||
|
roles:
|
||||||
|
- Admin
|
||||||
|
clients: [ ]
|
||||||
|
allowAnonymous: false
|
||||||
|
allowAuthenticated: false
|
|
@ -0,0 +1,21 @@
|
||||||
|
queue:
|
||||||
|
rabbitmq:
|
||||||
|
enable: true
|
||||||
|
durable: true
|
||||||
|
queue: cite_dmp_devel_notification_inbox_queue
|
||||||
|
exchange: cite_dmp_devel_queue
|
||||||
|
listenerEnabled: true
|
||||||
|
publisherEnabled: true
|
||||||
|
task:
|
||||||
|
publisher:
|
||||||
|
enable: true
|
||||||
|
options:
|
||||||
|
exchange: cite_dmp_devel_queue
|
||||||
|
rabbitmq:
|
||||||
|
enable: true
|
||||||
|
listener:
|
||||||
|
enable: true
|
||||||
|
options:
|
||||||
|
exchange: cite_dmp_devel_queue
|
||||||
|
rabbitmq:
|
||||||
|
enable: true
|
|
@ -0,0 +1,55 @@
|
||||||
|
spring:
|
||||||
|
rabbitmq:
|
||||||
|
host: ${RABBIT_HOST}
|
||||||
|
port: ${RABBIT_PORT}
|
||||||
|
username: ${RABBIT_USER}
|
||||||
|
password: ${RABBIT_PASS}
|
||||||
|
ssl:
|
||||||
|
enabled: false
|
||||||
|
queue:
|
||||||
|
rabbitmq:
|
||||||
|
enable: false
|
||||||
|
appId: ${QUEUE_APP_ID}
|
||||||
|
durable: null
|
||||||
|
queue: null
|
||||||
|
exchange: null
|
||||||
|
listenerEnabled: true
|
||||||
|
publisherEnabled: true
|
||||||
|
#TODO
|
||||||
|
connection-recovery:
|
||||||
|
enable: true
|
||||||
|
network-recovery-interval: 5000
|
||||||
|
unreachable-recovery-interval: 5000
|
||||||
|
task:
|
||||||
|
publisher:
|
||||||
|
enable: false
|
||||||
|
options:
|
||||||
|
exchange: null
|
||||||
|
rabbitmq:
|
||||||
|
enable: false
|
||||||
|
interval-seconds: 5
|
||||||
|
options:
|
||||||
|
retry-threashold: 100
|
||||||
|
retry-delay-step-seconds: 300
|
||||||
|
max-retry-delay-seconds: 10800
|
||||||
|
too-old-to-send-seconds: 604800
|
||||||
|
confirm-timeout-seconds: 30
|
||||||
|
listener:
|
||||||
|
enable: false
|
||||||
|
options:
|
||||||
|
exchange: null
|
||||||
|
tenant-default-locale-removal-topic: tenant_default_locale.remove
|
||||||
|
tenant-default-locale-touched-topic: tenant_default_locale.touch
|
||||||
|
notify-topic: notification.notify
|
||||||
|
tenant-removal-topic: tenant.remove
|
||||||
|
tenant-touched-topic: tenant.touch
|
||||||
|
user-removal-topic: user.remove
|
||||||
|
user-touched-topic: user.touch
|
||||||
|
rabbitmq:
|
||||||
|
enable: false
|
||||||
|
interval-seconds: 5
|
||||||
|
options:
|
||||||
|
retry-threashold: 100
|
||||||
|
retry-delay-step-seconds: 300
|
||||||
|
max-retry-delay-seconds: 10800
|
||||||
|
too-old-to-send-seconds: 604800
|
|
@ -0,0 +1,6 @@
|
||||||
|
web:
|
||||||
|
security:
|
||||||
|
idp:
|
||||||
|
resource:
|
||||||
|
jwt:
|
||||||
|
audiences: [ "dmp_notification" ]
|
|
@ -0,0 +1,14 @@
|
||||||
|
web:
|
||||||
|
security:
|
||||||
|
enabled: true
|
||||||
|
authorized-endpoints: [ api ]
|
||||||
|
allowed-endpoints: [ public ]
|
||||||
|
idp:
|
||||||
|
api-key:
|
||||||
|
enabled: false
|
||||||
|
resource:
|
||||||
|
token-type: JWT #| opaque
|
||||||
|
jwt:
|
||||||
|
claims: [ role, x-role ]
|
||||||
|
issuer-uri: ${IDP_ISSUER_URI}
|
||||||
|
validIssuer: ${IDP_ISSUER_URI}
|
|
@ -0,0 +1,2 @@
|
||||||
|
server:
|
||||||
|
forward-headers-strategy: FRAMEWORK
|
|
@ -0,0 +1,13 @@
|
||||||
|
server:
|
||||||
|
port: ${WEB_PORT}
|
||||||
|
forward-headers-strategy: NONE
|
||||||
|
tomcat:
|
||||||
|
threads:
|
||||||
|
max: 20
|
||||||
|
max-connections: 10000
|
||||||
|
|
||||||
|
spring:
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 10MB
|
||||||
|
max-request-size: 10MB
|
|
@ -0,0 +1,7 @@
|
||||||
|
tenant:
|
||||||
|
multitenancy:
|
||||||
|
is-multitenant: true
|
||||||
|
default-tenant-code: default
|
||||||
|
interceptor:
|
||||||
|
client-claims-prefix: client_
|
||||||
|
enforce-trusted-tenant: false
|
|
@ -0,0 +1,7 @@
|
||||||
|
tenant:
|
||||||
|
multitenancy:
|
||||||
|
is-multitenant: false
|
||||||
|
interceptor:
|
||||||
|
white-listed-clients: [ ]
|
||||||
|
enforce-trusted-tenant: false
|
||||||
|
white-listed-endpoints: [ '/api/notification/principal/me' ]
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration debug="true">
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<Pattern>%date{ISO8601} [%thread] %-5level %logger{36} [%X{req.id}] - %message%n</Pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="TROUBLESHOOTING" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>logs/logging.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/logging.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>100MB</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
<maxHistory>15</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<Pattern>%date{ISO8601} [%thread] %-5level %logger{36} [%X{req.id}] - %message%n</Pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="AUDITING" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>logs/auditing.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/auditing.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>100MB</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
<maxHistory>15</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<Pattern>%date{ISO8601} - %X{req.id} - %message%n</Pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="org.springframework.web" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TROUBLESHOOTING"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</logger>
|
||||||
|
<logger name="org.hibernate" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="TROUBLESHOOTING"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</logger>
|
||||||
|
<logger name="gr.cite" level="DEBUG" additivity="false">
|
||||||
|
<appender-ref ref="TROUBLESHOOTING"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</logger>
|
||||||
|
<logger name="audit" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="AUDITING"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</logger>
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="TROUBLESHOOTING"/>
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,10 @@
|
||||||
|
validation.empty=Value cannot be empty
|
||||||
|
validation.hashempty=Hash must be set
|
||||||
|
validation.lowerthanmin=Value must be larger than {value}
|
||||||
|
validation.largerthanmax=Value must be less than {value}
|
||||||
|
validation.invalidid=Not valid id
|
||||||
|
General_ItemNotFound=Item {0} of type {1} not found
|
||||||
|
Validation_Required={0} is required
|
||||||
|
Validation_OverPosting=Too much info
|
||||||
|
Validation_MaxLength={0} too long
|
||||||
|
Validation_UnexpectedValue=Unexpected value in field {0}
|
|
@ -0,0 +1,6 @@
|
||||||
|
validation.empty=el-Value cannot be empty
|
||||||
|
validation.hashempty=el-Hash must be set
|
||||||
|
validation.lowerthanmin=el-Value must be larger than {value}
|
||||||
|
validation.largerthanmax=el-Value must be less than {value}
|
||||||
|
validation.invalidid=el-Not valid id
|
||||||
|
General_ItemNotFound=el-Item {0} of type {1} not found
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
{description}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
Send by user: {email}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - {subject}
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} made a comment on the Description {name}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/descriptions/edit/{id}" target="_blank">Click here to view it.</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Comment for Description
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} made a comment on the Description {name}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/descriptions/edit/{id}" target="_blank">Click here to view it.</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Comment for Description
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} created new Description with name {name}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/descriptions/overview/{id}" target="_blank">Click here to view it.</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Created
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} created new Description with name {name}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/descriptions/overview/{id}" target="_blank">Click here to view it.</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Created
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just finalised the Description {name}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/descriptions/overview/{id}" target="_blank">Click here to view it.</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Finalised
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just finalised the Description {name}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/descriptions/overview/{id}" target="_blank">Click here to view it.</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Finalised
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just made changes to the Description {name}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/descriptions/overview/{id}" target="_blank">Click here to view it.</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Modified
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just made changes to the Description {name}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/descriptions/overview/{id}" target="_blank">Click here to view it.</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Modified
|
|
@ -0,0 +1,305 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>You have been invited to co-develop the Template {templateName}.</p>
|
||||||
|
<p>Click the button to redirect to {templateName}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/description-templates/{templateID}" target="_blank">{templateName}</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Template Invite
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>You have been invited to co-develop the Template {templateName}.</p>
|
||||||
|
<p>Click the button to redirect to {templateName}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/description-templates/{templateID}" target="_blank">{templateName}</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Description Template Invite
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Merge Account Confirmation
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Merge Account Confirmation
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just publish the {name}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/plans/overview/{id}" target="_blank">Click here to view it.</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Plan Deposit
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just publish the {name}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/plans/overview/{id}" target="_blank">Click here to view it.</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Plan Deposit
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just finalised the Plan {name}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/plans/overview/{id}" target="_blank">Click here to view it.</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Plan Finalised
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just finalised the Plan {name}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/plans/overview/{id}" target="_blank">Click here to view it.</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Plan Finalised
|
|
@ -0,0 +1,304 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just add you to collaborate to the Plan {planname} with role {planrole}.</p>
|
||||||
|
<p>Click the button to redirect to {planname}.</p>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{installation-url}{tenant-url-path}/plans/overview/{id}" target="_blank">Join</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Plan Invite
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>OpenCDMP Notification</title>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<p>Dear {recipient},</p>
|
||||||
|
<p>{reasonName} just added you to collaborate to the Plan {planname} with role {planrole}.</p>
|
||||||
|
<p>Click the button to redirect to {planname}.</p>
|
||||||
|
<a href="{installation-url}{tenant-url-path}/plans/overview/{id}" target="_blank">Join</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
OpenCDMP - Plan Invite
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue