Application Web sur Kotlin + Spring Boot + Vue.js (module complémentaire)

Bonjour, chers habitants de Habr!

Comme son nom l'indique, cet article est un ajout à l' application Web précédemment écrite sur Kotlin + Spring Boot + Vue.js , qui nous permet d'améliorer le squelette d'une future application et de la rendre plus facile à travailler avec.

Avant de commencer l'histoire, permettez-moi de remercier tous ceux qui ont commenté dans l'article précédent.

Table des matières




Configuration CI / CD (Heroku)


Prenons l'exemple de la mise en œuvre d'une intégration et d'une livraison continues en utilisant la plate-forme PaaS cloud Heroku .

La première chose que nous devons faire est de mettre le code d'application dans le référentiel sur GitHub . Pour qu'il n'y ait rien de superflu dans le référentiel, je recommande le contenu suivant du fichier .gitignore :

.gitignore
*.class # Help # backend/*.md # Package Files # *.jar *.war *.ear # Eclipse # .settings .project .classpath .studio target # NetBeans # backend/nbproject/private/ backend/nbbuild/ backend/dist/ backend/nbdist/ backend/.nb-gradle/ backend/build/ # Apple # .DS_Store # Intellij # .idea *.iml *.log # logback logback.out.xml backend/src/main/resources/public/ backend/target backend/.mvn backend/mvnw frontend/dist/ frontend/node/ frontend/node_modules/ frontend/npm-debug.log frontend/target !.mvn/wrapper/maven-wrapper.jar 


Important: avant de commencer à travailler avec Heroku, ajoutez un fichier appelé Procfile (sans aucune extension) au répertoire racine avec la ligne:
web: java -Dserver.port=$PORT -jar backend/target/backend-0.0.1-SNAPSHOT.jar , où backend-0.0.1-SNAPSHOT.jar est le nom du fichier JAR d' assembly . Et assurez-vous de vous engager et de pousser .

Remarque: vous pouvez également ajouter le fichier travis.yaml au répertoire racine pour réduire le temps de génération et de déploiement de l'application sur Heroku:

travis.yaml
 language: java jdk: - oraclejdk8 script: mvn clean install jacoco:report coveralls:report cache: directories: - node_modules 


Ensuite:

# 1 Inscrivez-vous sur Heroku .

# 2 Créez une nouvelle application:

Créer une nouvelle application


# 3 Heroku vous permet de connecter des ressources supplémentaires à l'application, par exemple, la base de données PostreSQL. Pour ce faire, faites: Application -> Ressources -> Modules complémentaires -> Heroku Postgres :

Postgres Heroku


# 4 Choisissez un plan:

Sélection du plan


# 5 Vous pouvez maintenant voir la ressource connectée:

Ressource connectée


# 6 Regardez les informations d'identification, elles seront nécessaires pour configurer les variables d'environnement: Paramètres -> Afficher les informations d'identification :

Afficher les informations d'identification


# 7 Définissez les variables d'environnement: Application -> Paramètres -> Reveal Config Vars :

Variables d'environnement


# 8 Définissez les variables d'environnement pour la connexion au format suivant:

 SPRING_DATASOURCE_URL = jdbc:postgresql://<i>hostname:port</i>/<i>db_name</i> SPRING_DATASOURCE_USERNAME = <i>username</i> SPRING_DATASOURCE_PASSWORD = <i>password</i> 

À quoi ça ressemble


# 9 Créez toutes les tables nécessaires dans la nouvelle base de données.

# 10 Le fichier application.properties, respectivement, devrait ressembler à ceci:

application.properties
 spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} spring.jpa.generate-ddl=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 


# 11 Créer un nouveau pipeline - Créer un nouveau pipeline :

Créer un nouveau pipeline


# 12 Méthode de déploiement - GitHub (cliquez sur Se connecter à GitHub et suivez les instructions dans une nouvelle fenêtre).

# 13 Activer les déploiements automatiques :

Activer les déploiements automatiques


# 14 Déploiement manuel - Cliquez sur Déployer la branche pour le premier déploiement. Dans le navigateur, vous verrez la sortie de la ligne de commande.

Déploiement manuel


# 15 Cliquez sur Afficher après la génération réussie pour ouvrir l'application déployée:

Afficher



Protection contre les bots (reCAPTCHA)


La première étape pour activer la vérification reCAPTCHA dans notre application consiste à créer un nouveau reCAPTCH dans le panneau d'administration de Google . Là, nous créons un nouveau site (Ajouter un nouveau site / Créer) et définissons les paramètres suivants:

Paramètres ReCAPTCHA


Dans la section Domaines , vous devez spécifier en plus de l'adresse où l'application va vivre, vous devez spécifier localhost , de sorte que lors du débogage, vous évitiez les problèmes sous la forme de l'impossibilité de se connecter à votre application.

Backend

Enregistrez la clé du site et la clé secrète ...

clé de site / clé secrète


... puis de les affecter à des variables d'environnement, et les noms de variables, à leur tour, à affecter à de nouvelles propriétés application.properties :

 google.recaptcha.key.site=${GOOGLE_RECAPTCHA_KEY_SITE} google.recaptcha.key.secret=${GOOGLE_RECAPTCHA_KEY_SECRET} 

Ajoutez une nouvelle dépendance dans pom.xml pour vérification du côté Google des jetons reCAPTCHA que le client nous enverra:

 <dependency> <groupId>com.mashape.unirest</groupId> <artifactId>unirest-java</artifactId> <version>1.4.9</version> </dependency> 

Il est maintenant temps de mettre à jour les entités que nous utilisons pour autoriser et enregistrer les utilisateurs en leur ajoutant un champ de chaîne pour le même jeton reCAPTCHA:

LoginUser.kt
 import com.fasterxml.jackson.annotation.JsonProperty import java.io.Serializable class LoginUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("password") var password: String? = null @JsonProperty("recapctha_token") var recaptchaToken: String? = null constructor() {} constructor(username: String, password: String, recaptchaToken: String) { this.username = username this.password = password this.recaptchaToken = recaptchaToken } companion object { private const val serialVersionUID = -1764970284520387975L } } 


NewUser.kt
 import com.fasterxml.jackson.annotation.JsonProperty import java.io.Serializable class NewUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("firstName") var firstName: String? = null @JsonProperty("lastName") var lastName: String? = null @JsonProperty("email") var email: String? = null @JsonProperty("password") var password: String? = null @JsonProperty("recapctha_token") var recaptchaToken: String? = null constructor() {} constructor(username: String, firstName: String, lastName: String, email: String, password: String, recaptchaToken: String) { this.username = username this.firstName = firstName this.lastName = lastName this.email = email this.password = password this.recaptchaToken = recaptchaToken } companion object { private const val serialVersionUID = -1764970284520387975L } } 


Ajoutez un petit service qui diffusera le jeton reCAPTCHA à un service Google spécial et indiquera en réponse si le jeton a réussi la vérification:

ReCaptchaService.kt
 import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.web.client.RestOperations import org.springframework.beans.factory.annotation.Autowired import com.mashape.unirest.http.HttpResponse import com.mashape.unirest.http.JsonNode import com.mashape.unirest.http.Unirest @Service("captchaService") class ReCaptchaService { val BASE_VERIFY_URL: String = "https://www.google.com/recaptcha/api/siteverify" @Autowired private val restTemplate: RestOperations? = null @Value("\${google.recaptcha.key.site}") lateinit var keySite: String @Value("\${google.recaptcha.key.secret}") lateinit var keySecret: String fun validateCaptcha(token: String): Boolean { val url: String = String.format(BASE_VERIFY_URL + "?secret=%s&response=%s", keySecret, token) val jsonResponse: HttpResponse<JsonNode> = Unirest.get(url) .header("accept", "application/json").queryString("apiKey", "123") .asJson() return (jsonResponse.getStatus() == 200) } } 


Ce service doit être utilisé dans le contrôleur d'enregistrement et d'autorisation des utilisateurs:

AuthController.kt
 import com.kotlinspringvue.backend.service.ReCaptchaService … @Autowired lateinit var captchaService: ReCaptchaService … if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else [if]... … if (!captchaService.validateCaptcha(newUser.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else... 


Frontend

La première étape consiste à installer et enregistrer le package reCAPTHA :

 $ npm install --save vue-recaptcha 

Connectez-vous ensuite au script dans index.html :

 <script src="https://www.google.com/recaptcha/api.js onload=vueRecaptchaApiLoaded&render=explicit" async defer></script> 

Ajoutez n'importe quel captcha à n'importe quel espace libre sur la page:

 <vue-recaptcha ref="recaptcha" size="invisible" :sitekey="sitekey" @verify="onCapthcaVerified" @expired="onCaptchaExpired" /> 

Et le bouton de l'action cible (autorisation ou enregistrement) appellera maintenant d'abord la méthode de validation:

 <b-button v-on:click="validateCaptcha" variant="primary">Login</b-button> 

Ajoutez la dépendance aux composants:

 import VueRecaptcha from 'vue-recaptcha' 

Modifier l' exportation par défaut :

 components: { VueRecaptcha }, … data() { … siteKey: <i>  </i> … } 

Et ajoutez de nouvelles méthodes:

  • validateCaptcha() - qui est appelé en cliquant sur le bouton
  • onCapthcaVerified(recaptchaToken) onCaptchaExpired() - que le captcha appelle lui-même

De nouvelles méthodes
 validateCaptcha() { this.$refs.recaptcha.execute() }, onCapthcaVerified(recaptchaToken) { AXIOS.post(`/auth/signin`, {'username': this.$data.username, 'password': this.$data.password, 'recapctha_token': recaptchaToken}) .then(response => { this.$store.dispatch('login', {'token': response.data.accessToken, 'roles': response.data.authorities, 'username': response.data.username}); this.$router.push('/home') }, error => { this.showAlert(error.response.data.message); }) .catch(e => { console.log(e); this.showAlert('Server error. Please, report this error website owners'); }) }, onCaptchaExpired() { this.$refs.recaptcha.reset() } 


Résultat



Envoi d'email


Envisagez la possibilité d'envoyer des lettres à notre application via un serveur de messagerie public, tel que Google ou Mail.ru.

La première étape, respectivement, sera de créer un compte sur le serveur de messagerie sélectionné, s'il ne l'est pas déjà.

La deuxième étape, nous devons ajouter les dépendances suivantes dans pom.xml :

Dépendances
 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> 


Vous devez également ajouter de nouvelles propriétés à application.properties :

Propriétés SMTP
 spring.mail.host=${SMTP_MAIL_HOST} spring.mail.port=${SMTP_MAIL_PORT} spring.mail.username=${SMTP_MAIL_USERNAME} spring.mail.password=${SMTP_MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.properties.mail.smtp.connectiontimeout=5000 spring.mail.properties.mail.smtp.timeout=5000 spring.mail.properties.mail.smtp.writetimeout=5000 


Vous pouvez spécifier les paramètres SMTP ici: Google et Mail.ru

Créez une interface où nous déclarons plusieurs méthodes:

  • Pour envoyer un SMS normal
  • Pour envoyer un e-mail HTML
  • Pour envoyer un e-mail à l'aide d'un modèle

EmailService.kt
 package com.kotlinspringvue.backend.email import org.springframework.mail.SimpleMailMessage internal interface EmailService { fun sendSimpleMessage(to: String, subject: String, text: String) fun sendSimpleMessageUsingTemplate(to: String, subject: String, template: String, params:MutableMap<String, Any>) fun sendHtmlMessage(to: String, subject: String, htmlMsg: String) } 


Créons maintenant une implémentation de cette interface - le service d'envoi d'emails:

EmailServiceImpl.kt
 package com.kotlinspringvue.backend.email import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.core.io.FileSystemResource import org.springframework.mail.MailException import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.MimeMessageHelper import org.springframework.stereotype.Component import org.thymeleaf.spring5.SpringTemplateEngine import org.thymeleaf.context.Context import java.io.File import javax.mail.MessagingException import javax.mail.internet.MimeMessage import org.apache.commons.io.IOUtils import org.springframework.core.env.Environment @Component class EmailServiceImpl : EmailService { @Value("\${spring.mail.username}") lateinit var sender: String @Autowired lateinit var environment: Environment @Autowired var emailSender: JavaMailSender? = null @Autowired lateinit var templateEngine: SpringTemplateEngine override fun sendSimpleMessage(to: String, subject: String, text: String) { try { val message = SimpleMailMessage() message.setTo(to) message.setFrom(sender) message.setSubject(subject) message.setText(text) emailSender!!.send(message) } catch (exception: MailException) { exception.printStackTrace() } } override fun sendSimpleMessageUsingTemplate(to: String, subject: String, template: String, params:MutableMap<String, Any>) { val message = emailSender!!.createMimeMessage() val helper = MimeMessageHelper(message, true, "utf-8") var context: Context = Context() context.setVariables(params) val html: String = templateEngine.process(template, context) helper.setTo(to) helper.setFrom(sender) helper.setText(html, true) helper.setSubject(subject) emailSender!!.send(message) } override fun sendHtmlMessage(to: String, subject: String, htmlMsg: String) { try { val message = emailSender!!.createMimeMessage() message.setContent(htmlMsg, "text/html") val helper = MimeMessageHelper(message, false, "utf-8") helper.setTo(to) helper.setFrom(sender) helper.setSubject(subject) emailSender!!.send(message) } catch (exception: MailException) { exception.printStackTrace() } } } 


  • Nous utilisons JavaMailSender configuré automatiquement par Spring pour envoyer des e-mails
  • L'envoi de lettres régulières est extrêmement simple - il vous suffit d'ajouter du texte au corps de la lettre et de l'envoyer
  • Les e-mails HTML sont définis comme des messages de type MIME et leur contenu sous forme de text/html
  • Pour traiter le modèle de message HTML, nous utilisons le moteur de modèle Spring

Créons un modèle simple pour écrire en utilisant le framework Thymeleaf en le plaçant dans src / main / resources / templates / :

emailTemplate.html
 <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Hello</title> </head> <body style="font-family: Arial, Helvetica, sans-serif;"> <h3>Hello!</h3> <div style="margin-top: 20px; margin-bottom: 30px; margin-left: 20px;"> <p>Hello, dear: <b><span th:text="${addresseeName}"></span></b></p> </div> <div> <img th:src="${signatureImage}" width="200px;"/> </div> </body> </html> 


Les éléments changeants du modèle (dans notre cas, le nom du destinataire et le chemin de l'image pour signature) sont déclarés à l'aide d'espaces réservés.

Maintenant, créez ou mettez à jour un contrôleur qui enverra des lettres:

BackendController.kt
 import com.kotlinspringvue.backend.email.EmailServiceImpl import com.kotlinspringvue.backend.web.response.ResponseMessage import org.springframework.beans.factory.annotation.Value import org.springframework.http.ResponseEntity import org.springframework.http.HttpStatus … @Autowired lateinit var emailService: EmailService @Value("\${spring.mail.username}") lateinit var addressee: String … @GetMapping("/sendSimpleEmail") @PreAuthorize("hasRole('USER')") fun sendSimpleEmail(): ResponseEntity<*> { try { emailService.sendSimpleMessage(addressee, "Simple Email", "Hello! This is simple email") } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } @GetMapping("/sendTemplateEmail") @PreAuthorize("hasRole('USER')") fun sendTemplateEmail(): ResponseEntity<*> { try { var params:MutableMap<String, Any> = mutableMapOf() params["addresseeName"] = addressee params["signatureImage"] = "https://coderlook.com/wp-content/uploads/2019/07/spring-by-pivotal.png" emailService.sendSimpleMessageUsingTemplate(addressee, "Template Email", "emailTemplate", params) } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } @GetMapping("/sendHtmlEmail") @PreAuthorize("hasRole('USER')") fun sendHtmlEmail(): ResponseEntity<*> { try { emailService.sendHtmlMessage(addressee, "HTML Email", "<h1>Hello!</h1><p>This is HTML email</p>") } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } 


Remarque: Pour nous assurer que tout fonctionne, nous nous enverrons d'abord des lettres.

De plus, pour nous assurer que tout fonctionne, nous pouvons créer une interface Web modeste qui tirera simplement les méthodes du service Web:

Email.vue
 <template> <div id="email"> <b-button v-on:click="sendSimpleMessage" variant="primary">Simple Email</b-button><br/> <b-button v-on:click="sendEmailUsingTemplate" variant="primary">Template Email</b-button><br/> <b-button v-on:click="sendHTMLEmail" variant="primary">HTML Email</b-button><br/> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'EmailPage', data() { return { counter: 0, username: '', header: {'Authorization': 'Bearer ' + this.$store.getters.getToken} } }, methods: { sendSimpleMessage() { AXIOS.get('/sendSimpleEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK"); }) .catch(error => { console.log('ERROR: ' + error.response.data); }) }, sendEmailUsingTemplate() { AXIOS.get('/sendTemplateEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK") }) .catch(error => { console.log('ERROR: ' + error.response.data); }) }, sendHTMLEmail() { AXIOS.get('/sendHtmlEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK") }) .catch(error => { console.log('ERROR: ' + error.response.data); }) } } } </script> <style> #email { margin-left: 38%; margin-top: 50px; } button { width: 150px; } </style> 


Remarque: n'oubliez pas de mettre à jour router.js et d'ajouter le lien vers la App.vue navigation App.vue si vous créez un nouveau composant.


Migration Gradle


Je vais clarifier tout de suite: si cet élément est considéré comme une amélioration, laissez chacun décider de son propre projet. Nous regardons simplement comment procéder.

En général, vous pouvez utiliser l'instruction Moving from Maven to Gradle en moins de 5 minutes , mais il est peu probable que le résultat soit à la hauteur des attentes. Je recommanderais toujours de faire la migration manuellement, cela ne prendra pas beaucoup de temps.

La première chose que nous devons faire est d' installer Gradle .

Ensuite, nous devons effectuer la procédure suivante pour les deux sous-projets - backend et fronted :

# 1 Supprimer les fichiers Maven - pom.xml , .mvn .

# 2 Dans le répertoire du sous-projet, exécutez gradle init et répondez aux questions:

  • Sélectionnez le type de projet à générer: basique
  • Sélectionnez le langage d'implémentation: Kotlin
  • Sélectionnez le script de construction DSL: Kotlin (puisque nous écrivons un projet dans Kotlin)

# 3 Supprimez settings.gradle.kts - ce fichier n'est nécessaire que pour le projet racine.

# 4 Exécutez gradle wrapper .

Passons maintenant à notre projet racine. Pour cela, vous devez suivre les étapes 1, 2 et 4 décrites ci-dessus pour les sous-projets - tout est le même sauf la suppression de settings.gradle.kts .

La configuration de construction du projet backend ressemblera à ceci:

build.gradle.kts
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.1.3.RELEASE" id("io.spring.dependency-management") version "1.0.8.RELEASE" kotlin("jvm") version "1.3.50" kotlin("plugin.spring") version "1.3.50" id("org.jetbrains.kotlin.plugin.jpa") version "1.3.50" } group = "com.kotlin-spring-vue" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } } dependencies { runtimeOnly(project(":frontend")) implementation("org.springframework.boot:spring-boot-starter-actuator:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-web:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-mail:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-security:2.1.3.RELEASE") implementation("org.postgresql:postgresql:42.2.5") implementation("org.springframework.boot:spring-boot-starter-thymeleaf:2.1.3.RELEASE") implementation("commons-io:commons-io:2.4") implementation("io.jsonwebtoken:jjwt:0.9.0") implementation("io.jsonwebtoken:jjwt-api:0.10.6") implementation("com.mashape.unirest:unirest-java:1.4.9") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") runtimeOnly("org.springframework.boot:spring-boot-devtools:2.1.3.RELEASE") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-noarg:1.3.50") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "1.8" } } 


  • Tous les plugins Kotlin et Spring requis doivent être spécifiés.
  • N'oubliez pas le plugin org.jetbrains.kotlin.plugin.jpa - il est nécessaire de se connecter à la base de données
  • Dans les dépendances, vous devez spécifier runtimeOnly(project(":frontend")) - nous devons d'abord construire le projet frontend

Construire la configuration pour le projet frontal :

build.gradle.kts
 plugins { id("org.siouan.frontend") version "1.2.1" id("java") } group = "com.kotlin-spring-vue" version = "0.0.1-SNAPSHOT" java { targetCompatibility = JavaVersion.VERSION_1_8 } buildscript { repositories { mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } } } frontend { nodeVersion.set("10.16.0") cleanScript.set("run clean") installScript.set("install") assembleScript.set("run build") } tasks.named("jar", Jar::class) { dependsOn("assembleFrontend") from("$buildDir/dist") into("static") } 


  • Dans mon exemple, le plugin org.siouan.frontend est utilisé pour construire le projet
  • Dans la section frontend {...} indiquer la version de Node.js, ainsi que les commandes qui appellent les scripts de nettoyage, d'installation et d'assemblage spécifiés dans package.json
  • Maintenant, nous emballons notre sous-projet frontal dans un fichier JAR et l'utilisons comme dépendance ( runtimeOnly(project(":frontend")) dans le backend ), nous devons donc décrire une tâche qui copie les fichiers du répertoire d'assembly vers / public et crée un fichier jar

Remarque:

  • Modifiez vue.config.js , en changeant le répertoire de construction en build / dist .
  • Dans le fichier de script de construction package.json , spécifiez la construction vue-cli-service build ou assurez-vous qu'il est spécifié

Le fichier settings.gradle.kts du projet racine doit contenir le code suivant: ...

 rootProject.name = "demo" include(":frontend", ":backend") 

... est le nom du projet et des sous-projets.

Et maintenant, nous pouvons construire le projet en exécutant la commande: ./gradlew build

Remarque: si pour les espaces réservés spécifiés dans application.properties (par exemple, ${SPRING_DATASOURCE_URL} ) il n'y a pas de variables d'environnement correspondantes, l'assembly échouera. Pour éviter cela, utilisez /gradlew build -x

Vous pouvez vérifier la structure des projets en utilisant la commande gradle -q projects , le résultat devrait ressembler à ceci:

 Root project 'demo' +--- Project ':backend' \--- Project ':frontend' 

Et enfin, pour exécuter l'application, vous devez exécuter ./gradlew bootRun .

.gitignore


Les fichiers et dossiers suivants doivent être ajoutés au fichier .gitignore :

  • backend / build /
  • frontend / build /
  • construire
  • .gradle

Important: vous ne devez pas ajouter de fichiers gradlew à .gitignore - ils ne contiennent rien de dangereux, mais ils sont nécessaires pour un assemblage réussi sur un serveur distant.

Déploiement sur Heroku


Examinons les modifications que nous devons apporter pour que l'application se déploie en toute sécurité sur Heroku.

# 1 Procfile

Nous devons demander à Heroku de nouvelles instructions pour lancer l'application:

 web: java -Dserver.port=$PORT -jar backend/build/libs/backend-0.0.1-SNAPSHOT.jar 

# 2 variables d'environnement

Heroku est capable de refaire le type d'application (par exemple, l'application Spring Boot) et de suivre les instructions d'assemblage appropriées. Mais notre application (le projet racine) ne ressemble pas à une application Spring Boot pour Heroku. Si nous laissons tout tel quel, Heroku nous demandera de définir la stage . Honnêtement, je ne sais pas où se termine ce chemin, car je ne l'ai pas suivi. Il est plus facile de définir une variable GRADLE_TASK avec une valeur de build :



# 3 reCAPTCHA

Lorsque vous placez l'application dans un nouveau domaine, n'oubliez pas de mettre à jour le captcha, les variables d'environnement GOOGLE_RECAPTCHA_KEY_SITE et GOOGLE_RECAPTCHA_KEY_SECRET , ainsi que de mettre à jour la clé de site dans le sous-projet frontal.


Stockage du jeton JWT dans les cookies


Tout d'abord, je vous recommande fortement de lire l'article Veuillez cesser d'utiliser le stockage local , en particulier la section Pourquoi le stockage local n'est pas sécurisé et vous ne devriez pas l'utiliser pour stocker des données sensibles .

Voyons comment vous pouvez stocker le jeton JWT dans un endroit plus sécurisé - les cookies dans le drapeau httpOnly , où il ne sera pas disponible pour la lecture / modification à l'aide de JavaScript.

# 1 Suppression de toute la logique liée à JWT du frontend:
Étant donné que le jeton n'est toujours pas accessible avec JavaScript avec la nouvelle méthode de stockage, vous pouvez supprimer en toute sécurité toutes les références à celui-ci de notre sous-projet.

Mais le rôle de l'utilisateur sans référence à d'autres données n'est pas une information si importante, elle peut toujours être stockée dans le stockage local et déterminer si l'utilisateur est autorisé ou non, selon que ce rôle est défini.

store / index.js
 import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const state = { role: localStorage.getItem('user-role') || '', username: localStorage.getItem('user-name') || '', authorities: localStorage.getItem('authorities') || '', }; const getters = { isAuthenticated: state => { if (state.role != null && state.role != '') { return true; } else { return false; } }, isAdmin: state => { if (state.role === 'admin') { return true; } else { return false; } }, getUsername: state => { return state.username; }, getAuthorities: state => { return state.authorities; } }; const mutations = { auth_login: (state, user) => { localStorage.setItem('user-name', user.username); localStorage.setItem('user-authorities', user.roles); state.username = user.username; state.authorities = user.roles; var isUser = false; var isAdmin = false; for (var i = 0; i < user.roles.length; i++) { if (user.roles[i].authority === 'ROLE_USER') { isUser = true; } else if (user.roles[i].authority === 'ROLE_ADMIN') { isAdmin = true; } } if (isUser) { localStorage.setItem('user-role', 'user'); state.role = 'user'; } if (isAdmin) { localStorage.setItem('user-role', 'admin'); state.role = 'admin'; } }, auth_logout: () => { state.token = ''; state.role = ''; state.username = ''; state.authorities = []; localStorage.removeItem('user-role'); localStorage.removeItem('user-name'); localStorage.removeItem('user-authorities'); } }; const actions = { login: (context, user) => { context.commit('auth_login', user) }, logout: (context) => { context.commit('auth_logout'); } }; export const store = new Vuex.Store({ state, getters, mutations, actions }); 


Soyez prudent lors de la refactorisation store/index.js: si l'autorisation et la désautorisation ne fonctionnent pas correctement, les messages d'erreur seront persistants dans la console.

# 2 Renvoyez le JWT sous forme de cookie dans le contrôleur d'autorisation ( pas dans le corps de la réponse ):

AuthController.kt
 @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String @Value("\${ksvg.app.isCookieSecure}") var isCookieSecure: Boolean = true @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser, response: HttpServletResponse): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (userCandidate.isPresent) { val user: User = userCandidate.get() val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)) SecurityContextHolder.getContext().setAuthentication(authentication) val jwt: String = jwtProvider.generateJwtToken(user.username!!) val cookie: Cookie = Cookie(authCookieName, jwt) cookie.maxAge = jwtProvider.jwtExpiration!! cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(SuccessfulSigninResponse(user.username, authorities)) } else { return ResponseEntity(ResponseMessage("User not found!"), HttpStatus.BAD_REQUEST) } } 


Important : Remarque S'il vous plaît que je mets les options authCookieNameet isCookieSecuredans application.properties - envoyer des cookies au drapeau securene peut https, ce qui le rend extrêmement difficile de débogage avec localhost. MAIS en production, bien sûr, il est préférable d'utiliser des cookies avec ce drapeau.

De plus, il est désormais recommandé que les réponses du contrôleur utilisent une entité sans champ spécial pour le JWT.

# 3 Mise à jour JwtAuthTokenFilter:
Nous avions l'habitude de prendre un jeton de l'en-tête de la demande, maintenant nous le prenons des cookies:

JwtAuthTokenFilter.kt
 @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String ... private fun getJwt(request: HttpServletRequest): String? { for (cookie in request.cookies) { if (cookie.name == authCookieName) { return cookie.value } } return null } 


# 4 Activation de CORS
Si, dans mon article précédent, vous pouviez toujours ignorer cette question, il serait maintenant étrange de protéger le jeton JWT sans avoir à activer CORS côté backend.

Vous pouvez résoudre ce problème en modifiant WebSecurityConfig.kt:

WebSecurityConfig.kt
 @Bean fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOrigins = Arrays.asList("http://localhost:8080", "http://localhost:8081", "https://kotlin-spring-vue-gradle-demo.herokuapp.com") configuration.allowedHeaders = Arrays.asList("*") configuration.allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowCredentials = true configuration.maxAge = 3600 val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", configuration) return source } @Throws(Exception::class) override fun configure(http: HttpSecurity) { http .cors().and() .csrf().disable().authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java) http.headers().cacheControl().disable() } 


Et maintenant, vous pouvez supprimer toutes les annotations @CrossOrigindes contrôleurs.

Important: le paramètre AllowCredentials est requis pour envoyer des demandes depuis le frontend. En savoir plus à ce sujet ici .

# 5 Mise à jour des en-têtes du côté frontal:

http-commons.js
 export const AXIOS = axios.create({ baseURL: `/api`, headers: { 'Access-Control-Allow-Origin': ['http://localhost:8080', 'http://localhost:8081', 'https://kotlin-spring-vue-gradle-demo.herokuapp.com'], 'Access-Control-Allow-Methods': 'GET,POST,DELETE,PUT,OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Credentials': true } }) 


Vérifier


Essayons de nous connecter à l'application en nous connectant à partir d'un hôte qui ne figure pas dans la liste autorisée WebSecurityConfig.kt. Pour ce faire, exécutez le backend sur le port 8080et le frontend, par exemple, sur 8082et essayez de vous connecter:

Résultat


Demande d'autorisation rejetée par la politique CORS.

Voyons maintenant comment les cookies de drapeau fonctionnent en général httpOnly. Pour ce faire, allons par exemple sur le site https://kotlinlang.org et exécutons-le dans la console du navigateur:

 document.cookie 

Résultat


Les non- httpOnlycookies liés à ce site apparaîtront dans la console , qui, comme nous le voyons, sont accessibles via JavaScript.

Maintenant, allons dans notre application, connectez-vous (pour que le navigateur enregistre le cookie avec JWT) et répétez la même chose:

Résultat


Remarque: cette méthode de stockage d'un jeton JWT est plus fiable que l'utilisation du stockage local, mais vous devez comprendre qu'il ne s'agit pas d'une panacée.


Confirmation d'inscription par e-mail


Un bref algorithme pour effectuer cette tâche est le suivant:

  1. Pour tous les nouveaux utilisateurs, l'attribut isEnabledde la base de données est défini surfalse
  2. Un jeton de chaîne est généré à partir de caractères arbitraires, qui servira de clé pour confirmer l'enregistrement
  3. Le token est envoyé à l'utilisateur par mail dans le cadre du lien
  4. L'attribut isEnabledest défini sur true si l'utilisateur suit le lien pendant une période de temps définie.

Examinons maintenant ce processus plus en détail.

Nous avons besoin d'une table pour stocker le jeton pour confirmer l'inscription:

 CREATE TABLE public.verification_token ( id serial NOT NULL, token character varying, expiry_date timestamp without time zone, user_id integer, PRIMARY KEY (id) ); ALTER TABLE public.verification_token ADD CONSTRAINT verification_token_users_fk FOREIGN KEY (user_id) REFERENCES public.users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE; 

Et, en conséquence, une nouvelle entité pour le mappage relationnel-objet ...:

VerificationToken.kt
 package com.kotlinspringvue.backend.jpa import java.sql.* import javax.persistence.* import java.util.Calendar @Entity @Table(name = "verification_token") data class VerificationToken( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = 0, @Column(name = "token") var token: String? = null, @Column(name = "expiry_date") val expiryDate: Date, @OneToOne(targetEntity = User::class, fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST]) @JoinColumn(nullable = false, name = "user_id") val user: User ) { constructor(token: String?, user: User) : this(0, token, calculateExpiryDate(1440), user) } private fun calculateExpiryDate(expiryTimeInMinutes: Int): Date { val cal = Calendar.getInstance() cal.time = Timestamp(cal.time.time) cal.add(Calendar.MINUTE, expiryTimeInMinutes) return Date(cal.time.time) } 


... et le référentiel:

VerificationTokenRepository.kt
 package com.kotlinspringvue.backend.repository import com.kotlinspringvue.backend.jpa.VerificationToken import org.springframework.data.jpa.repository.JpaRepository import java.util.* interface VerificationTokenRepository : JpaRepository<VerificationToken, Long> { fun findByToken(token: String): Optional<VerificationToken> } 


Nous devons maintenant implémenter des outils de gestion des jetons - création, vérification et envoi par e-mail. Pour ce faire, nous modifions UserDetailsServiceImplen ajoutant des méthodes pour créer et vérifier le jeton:

UserDetailsServiceImpl.kt
 override fun createVerificationTokenForUser(token: String, user: User) { tokenRepository.save(VerificationToken(token, user)) } override fun validateVerificationToken(token: String): String { val verificationToken: Optional<VerificationToken> = tokenRepository.findByToken(token) if (verificationToken.isPresent) { val user: User = verificationToken.get().user val cal: Calendar = Calendar.getInstance() if ((verificationToken.get().expiryDate.time - cal.time.time) <= 0) { tokenRepository.delete(verificationToken.get()) return TOKEN_EXPIRED } user.enabled = true tokenRepository.delete(verificationToken.get()) userRepository.save(user) return TOKEN_VALID } else { return TOKEN_INVALID } } 


Ajoutez maintenant une méthode pour envoyer un e-mail avec un lien de confirmation à EmailServiceImpl:

EmailServiceImpl.kt
  @Value("\${host.url}") lateinit var hostUrl: String @Autowired lateinit var userDetailsService: UserDetailsServiceImpl ... override fun sendRegistrationConfirmationEmail(user: User) { val token = UUID.randomUUID().toString() userDetailsService.createVerificationTokenForUser(token, user) val link = "$hostUrl/?token=$token&confirmRegistration=true" val msg = "<p>Please, follow the link to complete your registration:</p><p><a href=\"$link\">$link</a></p>" user.email?.let{sendHtmlMessage(user.email!!, "KSVG APP: Registration Confirmation", msg)} } 


Remarque:

  • Je recommanderais de stocker l'URL de l'hôte dans application.properties
  • Dans notre lien, nous transmettons deux paramètres GET ( tokenet confirmRegistration) à l'adresse où l'application est déployée. Plus tard, je vais expliquer pourquoi.

Nous modifions le contrôleur d'inscription comme suit:

  • Tous les nouveaux utilisateurs définiront la valeur falsedu champisEnabled
  • Après avoir créé un nouveau compte, nous vous enverrons un e-mail pour confirmer l'inscription
  • Créer un contrôleur de validation de jeton distinct
  • Important: lors de l'autorisation nous vérifierons si le compte est vérifié:

AuthController.kt
 package com.kotlinspringvue.backend.controller import com.kotlinspringvue.backend.email.EmailService import javax.validation.Valid import java.util.* import java.util.stream.Collectors import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.ui.Model import com.kotlinspringvue.backend.model.LoginUser import com.kotlinspringvue.backend.model.NewUser import com.kotlinspringvue.backend.web.response.SuccessfulSigninResponse import com.kotlinspringvue.backend.web.response.ResponseMessage import com.kotlinspringvue.backend.jpa.User import com.kotlinspringvue.backend.repository.UserRepository import com.kotlinspringvue.backend.repository.RoleRepository import com.kotlinspringvue.backend.jwt.JwtProvider import com.kotlinspringvue.backend.service.ReCaptchaService import com.kotlinspringvue.backend.service.UserDetailsService import org.springframework.beans.factory.annotation.Value import org.springframework.context.ApplicationEventPublisher import org.springframework.web.bind.annotation.* import org.springframework.web.context.request.WebRequest import java.io.UnsupportedEncodingException import javax.servlet.http.Cookie import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_VALID import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_INVALID import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_EXPIRED @RestController @RequestMapping("/api/auth") class AuthController() { @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String @Value("\${ksvg.app.isCookieSecure}") var isCookieSecure: Boolean = true @Autowired lateinit var authenticationManager: AuthenticationManager @Autowired lateinit var userRepository: UserRepository @Autowired lateinit var roleRepository: RoleRepository @Autowired lateinit var encoder: PasswordEncoder @Autowired lateinit var jwtProvider: JwtProvider @Autowired lateinit var captchaService: ReCaptchaService @Autowired lateinit var userService: UserDetailsService @Autowired lateinit var emailService: EmailService @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser, response: HttpServletResponse): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (userCandidate.isPresent) { val user: User = userCandidate.get() if (!user.enabled) { return ResponseEntity(ResponseMessage("Account is not verified yet! Please, follow the link in the confirmation email."), HttpStatus.UNAUTHORIZED) } val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)) SecurityContextHolder.getContext().setAuthentication(authentication) val jwt: String = jwtProvider.generateJwtToken(user.username!!) val cookie: Cookie = Cookie(authCookieName, jwt) cookie.maxAge = jwtProvider.jwtExpiration!! cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(SuccessfulSigninResponse(user.username, authorities)) } else { return ResponseEntity(ResponseMessage("User not found!"), HttpStatus.BAD_REQUEST) } } @PostMapping("/signup") fun registerUser(@Valid @RequestBody newUser: NewUser): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(newUser.username!!) if (!captchaService.validateCaptcha(newUser.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (!userCandidate.isPresent) { if (usernameExists(newUser.username!!)) { return ResponseEntity(ResponseMessage("Username is already taken!"), HttpStatus.BAD_REQUEST) } else if (emailExists(newUser.email!!)) { return ResponseEntity(ResponseMessage("Email is already in use!"), HttpStatus.BAD_REQUEST) } try { // Creating user's account val user = User( 0, newUser.username!!, newUser.firstName!!, newUser.lastName!!, newUser.email!!, encoder.encode(newUser.password), false ) user.roles = Arrays.asList(roleRepository.findByName("ROLE_USER")) val registeredUser = userRepository.save(user) emailService.sendRegistrationConfirmationEmail(registeredUser) } catch (e: Exception) { return ResponseEntity(ResponseMessage("Server error. Please, contact site owner"), HttpStatus.SERVICE_UNAVAILABLE) } return ResponseEntity(ResponseMessage("Please, follow the link in the confirmation email to complete the registration."), HttpStatus.OK) } else { return ResponseEntity(ResponseMessage("User already exists!"), HttpStatus.BAD_REQUEST) } } @PostMapping("/registrationConfirm") @CrossOrigin(origins = ["*"]) @Throws(UnsupportedEncodingException::class) fun confirmRegistration(request: HttpServletRequest, model: Model, @RequestParam("token") token: String): ResponseEntity<*> { when(userService.validateVerificationToken(token)) { TOKEN_VALID -> return ResponseEntity.ok(ResponseMessage("Registration confirmed")) TOKEN_INVALID -> return ResponseEntity(ResponseMessage("Token is invalid!"), HttpStatus.BAD_REQUEST) TOKEN_EXPIRED -> return ResponseEntity(ResponseMessage("Token is invalid!"), HttpStatus.UNAUTHORIZED) } return ResponseEntity(ResponseMessage("Server error. Please, contact site owner"), HttpStatus.SERVICE_UNAVAILABLE) } @PostMapping("/logout") fun logout(response: HttpServletResponse): ResponseEntity<*> { val cookie: Cookie = Cookie(authCookieName, null) cookie.maxAge = 0 cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) return ResponseEntity.ok(ResponseMessage("Successfully logged")) } private fun emailExists(email: String): Boolean { return userRepository.findByUsername(email).isPresent } private fun usernameExists(username: String): Boolean { return userRepository.findByUsername(username).isPresent } } 


Maintenant, travaillons sur le frontend:

# 1 Créez le composant RegistrationConfirmPage.vue

# 2 Ajoutez un nouveau chemin router.jsavec le paramètre :token:

 { path: '/registration-confirm/:token', name: 'RegistrationConfirmPage', component: RegistrationConfirmPage } 

# 3 Mise à jour SignUp.vue- après avoir envoyé avec succès les données des formulaires, nous les informerons que pour terminer l'inscription, vous devez suivre le lien dans la lettre.

# 4 Important: hélas, nous ne pouvons pas fournir de lien fixe vers un composant distinct qui validerait le jeton et signalerait un succès ou un échec. Des liens avec des barres obliques nous mèneront tout de même à la page d'origine de l'application. Mais nous pouvons indiquer à notre application la nécessité de confirmer l'enregistrement en utilisant le paramètre GET transmis confirmRegistration:

 methods: { confirmRegistration() { if (this.$route.query.confirmRegistration === 'true' && this.$route.query.token != null) { this.$router.push({name: 'RegistrationConfirmPage', params: { token: this.$route.query.token}}); } }, ... mounted() { this.confirmRegistration(); } 

# 5 Créons un composant qui effectue la validation des jetons et rend compte du résultat de la validation:

RegistrationConfirmPage.vue
 <template> <div id="registration-confirm"> <div class="confirm-form"> <b-card title="Confirmation" tag="article" style="max-width: 20rem;" class="mb-2" > <div v-if="isSuccess"> <p class="success">Account is successfully verified!</p> <router-link to="/login"> <b-button variant="primary">Login</b-button> </router-link> </div> <div v-if="isError"> <p class="fail">Verification failed:</p> <p>{{ errorMessage }}</p> </div> </b-card> </div> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'RegistrationConfirmPage', data() { return { isSuccess: false, isError: false, errorMessage: '' } }, methods: { executeVerification() { AXIOS.post(`/auth/registrationConfirm`, null, {params: { 'token': this.$route.params.token}}) .then(response => { this.isSuccess = true; console.log(response); }, error => { this.isError = true; this.errorMessage = error.response.data.message; }) .catch(e => { console.log(e); this.errorMessage = 'Server error. Please, report this error website owners'; }) } }, mounted() { this.executeVerification(); } } </script> <style scoped> .confirm-form { margin-left: 38%; margin-top: 50px; } .success { color: green; } .fail { color: red; } </style> 


Résultat


Au lieu d'une conclusion


En conclusion de ce document, je voudrais faire une digression et dire que le concept même de l'application discuté dans cet article et l'article précédent n'était pas nouveau au moment de la rédaction. Tâche de créer rapidement une pile complète d'applications au démarrage Spring en utilisant JavaScript-cadres modernes angulaire / React / Vue.js résout élégamment Hipster .

Cependant, les idées décrites dans cet article peuvent être mises en œuvre même en utilisant JHipster, donc j'espère que les lecteurs qui viendront à cet endroit trouveront ce matériel utile même comme aliment de réflexion.


Liens utiles


Source: https://habr.com/ru/post/fr482222/


All Articles