BlogContacts
BlogGamesContacts

{{item}}

Simple feedback form

30.04.2020

Java, Spring, AngularJS, Material

DemoGitHub
External feedback form for a static site. This application includes a backend and frontend, so that it can be called both from an external site and from the current site. Frontend has a responsive layout for both desktop computers and mobile devices.
The backend uses java mail-sender library for messaging and provides the simple-captcha generation and checking. Three methods are available from frontend: captcha, checkCaptcha and finally sendFeedback, if two previous are passed.
The frontend is expected to use an ajax calls of the backend POST methods. This application uses the AngularJS http service. These calls require configuring of cross origin requests processing on both the backend and frontend.The expected algorithm of user's actions is as follows: first he has to fill the required fields (name, email and message), optionally he may want to add his website address and attach several files. After that, a field with a captcha image becomes available. The browser sends a GET request to the server's captcha endpoint. The server opens a session for this client with a 5 minutes timeout. When user enters the characters from the picture and clicks the send button, the browser sends the first POST request to checkCaptcha and, if it is passed, sends the second POST request to sendFeedback.There are described the main steps to configure this application. The entire code is available on GitHub. You can send feedback to the author using the demo version on Heroku.Frontend of this application (desktop and mobile) looks as follows:
Simple feedback form desktop
Simple feedback form mobile
Example of the incoming message:
Simple feedback form

Configure the application

application.properties - is the main configuration file. It must be present in the resources folder of this application, or in the user's home folder. This is a template:
# Temp directory for attachments
temp.folder=/tmp

# Comma-separated domain names,
# where this application runs
# and where it is called from
allowed.origins=https://drakonoved.herokuapp.com,https://drakonoved.org

# Mail server properties
mail.smtp.host=smtp.mail.ru
mail.smtp.ssl.trust=smtp.mail.ru
mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
mail.smtp.auth=true
mail.smtp.socketFactory.port=465
mail.smtp.port=465

# Letter box name and password
mail.smtp.username=some_letterbox@mail.ru
mail.smtp.password=ReallyToughPassword111!

# User interface properties
form.title=Feedback form
form.homePageName=See GitHub
form.homePageLink=https://github.com/drakonoved
form.enableThemes=true
form.defaultTheme=green
form.atSign=true
form.notice=Concerning any questions, comments and suggestions.

Deploy to heroku

You can download precompiled web application archive and deploy it to some server, such as heroku, with an additional application.properties file. To do this, you can use heroku-cli:
# check heroku-cli is downloaded successfully
heroku --version
# heroku/7.39.5 linux-x64 node-v12.16.2

# install java plugin
heroku plugins:install java
# Installing plugin java... installed v3.1.1

# deploy <war file> to <application name>
heroku war:deploy simple-feedback-form.war --app drakonoved --jdk 14 --includes application.properties
# Uploading simple-feedback-form.war
# -----> Packaging application...
# - app: drakonoved
# - including: application.properties
# - including: webapp-runner.jar
# - including: simple-feedback-form.war
# -----> Creating build...
# - file: slug.tgz
# - size: 30MB
# -----> Uploading build...
# - success
# -----> Deploying...
# remote:
# remote: -----> heroku-deploy app detected
# remote: -----> Installing JDK 14... done
# remote: -----> Discovering process types
# remote:        Procfile declares types -> web
# remote:
# remote: -----> Compressing...
# remote:        Done: 96.9M
# remote: -----> Launching...
# remote:        Released v3
# remote:        https://drakonoved.herokuapp.com/ deployed to Heroku
# remote:
# -----> Done

Customization

Pay attention to these points, before making any changes to the backend or frontend. This application uses custom libraries for sending mail and for the captcha generation and checking. These libraries should be built and installed into local Maven repository (.m2 folder by default) before building this application:
XML
<dependency>
    <groupId>org.drakonoved</groupId>
    <artifactId>mail-sender</artifactId>
    <version>1.0.4</version>
</dependency>

<dependency>
    <groupId>org.drakonoved.cipher</groupId>
    <artifactId>simple-captcha</artifactId>
    <version>1.0.5</version>
</dependency>

Configure cross origin requests processing and POST requests

If you plan to call this application from a server, other than the one on which it is running, then you should configure cross origin requests processing. It should be configured on both the backend and frontend.Configure the list of allowed origins:
Java
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Value("${allowed.origins}")
    private String allowedOrigins;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/*").allowCredentials(true)
                .allowedOrigins(allowedOrigins.split(","));
    }
}
Configure the session cookie to include SameSite=None; Secure:
Java
@Configuration
@EnableSpringHttpSession
public class WebAppConfig implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) {
        servletContext
                .addFilter("sessionRepositoryFilter", DelegatingFilterProxy.class)
                .addMappingForUrlPatterns(null, false, "/*");
    }

    @Bean
    public MapSessionRepository sessionRepository() {
        final Map<String, Session> sessions = new ConcurrentHashMap<>();
        MapSessionRepository sessionRepository =
                new MapSessionRepository(sessions) {
                    @Override
                    public void save(MapSession session) {
                        sessions.entrySet().stream()
                                .filter(entry -> entry.getValue().isExpired())
                                .forEach(entry -> sessions.remove(entry.getKey()));
                        super.save(session);
                    }
                };
        sessionRepository.setDefaultMaxInactiveInterval(60*5);
        return sessionRepository;
    }

    @Bean
    public SessionRepositoryFilter<?> sessionRepositoryFilter(MapSessionRepository sessionRepository) {
        SessionRepositoryFilter<?> sessionRepositoryFilter =
                new SessionRepositoryFilter<>(sessionRepository);

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setSameSite("None");
        cookieSerializer.setUseSecureCookie(true);

        CookieHttpSessionIdResolver cookieHttpSessionIdResolver =
                new CookieHttpSessionIdResolver();
        cookieHttpSessionIdResolver.setCookieSerializer(cookieSerializer);

        sessionRepositoryFilter.setHttpSessionIdResolver(cookieHttpSessionIdResolver);

        return sessionRepositoryFilter;
    }
}
Configure the AngularJS httpProvider to use credentials:
JavaScript
angular.module("application")

.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.defaults.withCredentials = true;
}]);
AngularJS http service POST requests - the first to checkCaptcha and the second to sendFeedback:
JavaScript
scope.formFields = {
    userName: '',
    email: '',
    website: '',
    messageText: '',
    captchaText: '',
};
scope.sendingData = false;
scope.sendFeedback = function() {
    if (!scope.feedbackForm.$valid) return;

    scope.sendingData = true;

    http({
        url: '/checkCaptcha',
        method: "POST",
        params: {captchaText: scope.formFields.captchaText},
        headers: {'Content-Type': undefined },
    }).then(function doSendFeedback(response) {
        http({
            url: '/sendFeedback',
            method: "POST",
            params: scope.formFields,
            headers: {'Content-Type': undefined },
            data: formData,
        }).then((response) => successCallback(response),
            (response) => errorCallback(response));
        }, (response) => errorCallback(response));

    let successCallback = function(response) {
        scope.formFields.captchaText = '';
        scope.feedbackForm.captchaText.$setPristine();
        scope.feedbackForm.captchaText.$setUntouched();
        document.querySelector(".reloadButton").click();
        scope.sendingData = false;
    };
    let errorCallback = function(response) {
        scope.formFields.captchaText = '';
        scope.feedbackForm.captchaText.$setDirty();
        document.querySelector(".reloadButton").click();
        scope.sendingData = false;
    };
};
Privacy policy
Back to Top