مساء الخير أيها السكان الأعزاء في هبر!
كما يوحي الاسم ، تعد هذه المقالة إضافة إلى
تطبيق الويب المكتوب مسبقًا
على Kotlin + Spring Boot + Vue.js ، والذي يسمح لنا بتحسين الهيكل العظمي للتطبيق في المستقبل وجعل العمل معه أسهل.
قبل بدء القصة ، اسمحوا لي أن أشكر كل من علق في المقال السابق.
محتوى
إعداد CI / CD (Heroku)
النظر في تنفيذ التكامل المستمر والتسليم باستخدام منصة
Heroku cloud
PaaS كمثال.
أول شيء نحتاج إلى القيام به هو وضع رمز التطبيق في المستودع على
جيثب . بحيث لا يوجد شيء غير ضروري في المستودع ، أوصي بالمحتويات التالية لملف
.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
هام: قبل أن تبدأ العمل مع Heroku ، أضف ملفًا يسمى
Procfile (بدون أي امتداد) إلى الدليل الجذر مع السطر:
web: java -Dserver.port=$PORT -jar backend/target/backend-0.0.1-SNAPSHOT.jar
، حيث
backend-0.0.1-SNAPSHOT.jar هو اسم ملف JAR
التجميع . وتأكد من
القيام الالتزام ودفع .
ملاحظة: يمكنك أيضًا إضافة ملف travis.yaml إلى الدليل الجذر لتقليل وقت إنشاء التطبيق ونشره على Heroku:
travis.yaml language: java jdk: - oraclejdk8 script: mvn clean install jacoco:report coveralls:report cache: directories: - node_modules
ثم:
# 1 سجل في
Heroku .
# 2 إنشاء تطبيق جديد:
# 3 يسمح لك Heroku بتوصيل موارد إضافية بالتطبيق ، على سبيل المثال ، قاعدة بيانات PostreSQL. للقيام بذلك ، قم بما يلي:
التطبيق -> الموارد -> الوظائف الإضافية -> Heroku Postgres :
# 4 اختيار خطة:
# 5 الآن يمكنك رؤية المورد المتصل:
# 6 انظر إلى بيانات الاعتماد ، وستكون هناك حاجة لإعداد متغيرات البيئة:
الإعدادات -> عرض بيانات الاعتماد :
# 7 قم بتعيين متغيرات البيئة:
التطبيق -> الإعدادات -> الكشف عن تباينات التكوين :
# 8 اضبط متغيرات البيئة للاتصال بالتنسيق التالي:
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>
# 9 إنشاء جميع الجداول اللازمة في قاعدة البيانات الجديدة.
# 10: يجب أن يبدو ملف application.properties ، على التوالي ، مثل هذا:
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 إنشاء
خط أنابيب جديد -
إنشاء خط أنابيب جديد :
# 12 طريقة النشر -
GitHub (انقر فوق
الاتصال بـ GitHub واتبع الإرشادات في نافذة جديدة).
# 13 تمكين النشر التلقائي :
# 14 النشر اليدوي - انقر فوق
نشر الفرع للنشر الأول. في المستعرض مباشرةً ، سترى إخراج سطر الأوامر.
# 15 انقر فوق
عرض بعد الإنشاء الناجح لفتح التطبيق المنشور:
حماية بوت (reCAPTCHA)
الخطوة الأولى لتمكين التحقق من reCAPTCHA في تطبيقنا هي إنشاء reCAPTCH جديد في
لوحة إدارة Google . هناك نقوم بإنشاء موقع جديد (إضافة موقع جديد / إنشاء) وتعيين الإعدادات التالية:
في قسم
المجالات ، يجب عليك تحديد بالإضافة إلى العنوان الذي سيعيش فيه التطبيق ، وعليك تحديد
localhost
، بحيث تتجنب المتاعب أثناء تصحيح الأخطاء في شكل عدم القدرة على تسجيل الدخول إلى التطبيق الخاص بك.
الخلفيةحفظ
مفتاح الموقع والمفتاح السري ...
مفتاح الموقع / المفتاح السري ... ثم لتعيينها لمتغيرات البيئة ، والأسماء المتغيرة ، بدورها ، لتعيين خصائص
application.properties جديدة:
google.recaptcha.key.site=${GOOGLE_RECAPTCHA_KEY_SITE} google.recaptcha.key.secret=${GOOGLE_RECAPTCHA_KEY_SECRET}
أضف تبعية جديدة في
pom.xml للتحقق على جانب Google من الرموز المميزة reCAPTCHA التي سيرسلها العميل إلينا:
<dependency> <groupId>com.mashape.unirest</groupId> <artifactId>unirest-java</artifactId> <version>1.4.9</version> </dependency>
لقد حان الوقت لتحديث الكيانات التي نستخدمها لتخويل المستخدمين وتسجيلهم عن طريق إضافة حقل سلسلة لهم لنفس رمز 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 } }
أضف خدمة صغيرة ستقوم ببث الرمز المميز reCAPTCHA في خدمة خاصة من Google والإبلاغ ردا على ما إذا كان الرمز المميز قد تحقق:
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) } }
يجب استخدام هذه الخدمة في وحدة تحكم تسجيل المستخدم والترخيص:
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...
الواجهةالخطوة الأولى هي تثبيت وحفظ حزمة
reCAPTHA
:
$ npm install --save vue-recaptcha
ثم اتصل بالنص في
index.html :
<script src="https://www.google.com/recaptcha/api.js onload=vueRecaptchaApiLoaded&render=explicit" async defer></script>
أضف أي كلمة التحقق إلى أي مساحة خالية على الصفحة:
<vue-recaptcha ref="recaptcha" size="invisible" :sitekey="sitekey" @verify="onCapthcaVerified" @expired="onCaptchaExpired" />
وسوف يقوم زر الإجراء المستهدف (الترخيص أو التسجيل) الآن أولاً باستدعاء طريقة التحقق من الصحة:
<b-button v-on:click="validateCaptcha" variant="primary">Login</b-button>
أضف التبعية للمكونات:
import VueRecaptcha from 'vue-recaptcha'
تحرير
التصدير الافتراضي :
components: { VueRecaptcha }, … data() { … siteKey: <i> </i> … }
وإضافة طرق جديدة:
validateCaptcha()
- والذي يسمى بالنقر فوق الزرonCapthcaVerified(recaptchaToken) onCaptchaExpired()
- الذي يستدعي captcha نفسه
طرق جديدة 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() }
إرسال البريد الإلكتروني
النظر في إمكانية إرسال رسائل إلى التطبيق لدينا من خلال خادم البريد العام ، مثل جوجل أو Mail.ru.
الخطوة الأولى ، على التوالي ، هي إنشاء حساب على خادم البريد المحدد ، إذا لم يكن بالفعل.
الخطوة الثانية التي نحتاجها لإضافة التبعيات التالية في
pom.xml :
اعتمادا على <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>
تحتاج أيضًا إلى إضافة خصائص جديدة إلى
application.properties :
خصائص 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
يمكنك تحديد إعدادات SMTP هنا:
Google و
Mail.ruإنشاء واجهة حيث نعلن عدة طرق:
- لإرسال رسالة نصية عادية
- لإرسال بريد HTML الإلكتروني
- لإرسال بريد إلكتروني باستخدام قالب
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) }
لنقم الآن بإنشاء تطبيق لهذه الواجهة - خدمة إرسال رسائل البريد الإلكتروني:
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() } } }
- نستخدم JavaMailSender الذي تم تكوينه تلقائيًا في Spring لإرسال رسائل البريد الإلكتروني
- إرسال رسائل عادية أمر بسيط للغاية - تحتاج فقط إلى إضافة نص إلى نص الرسالة وإرساله
- يتم تعريف رسائل البريد الإلكتروني
text/html
HTML على أنها رسائل نوع Mime ومحتواها text/html
- لمعالجة قالب رسالة HTML ، نستخدم Spring Template Engine
لنقم بإنشاء قالب بسيط للكتابة باستخدام إطار عمل
Thymeleaf من خلال وضعه في
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>
يتم الإعلان عن العناصر المتغيرة للقالب (في حالتنا ، اسم المستلم ومسار الصورة للتوقيع) باستخدام العناصر النائبة.
الآن قم بإنشاء أو تحديث وحدة تحكم من شأنها إرسال رسائل:
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) }
ملاحظة: للتأكد من أن كل شيء يعمل ، سنرسل أولاً رسائل إلى أنفسنا.
أيضًا ، للتأكد من أن كل شيء يعمل ، يمكننا إنشاء واجهة ويب متواضعة تسحب ببساطة أساليب خدمة الويب:
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>
ملاحظة: لا تنسَ تحديث
router.js
وإضافة الرابط إلى
App.vue
التنقل
App.vue
إذا كنت تقوم بإنشاء مكون جديد.
الهجرة المهد
سأوضح على الفور: في حالة اعتبار هذا العنصر تحسينًا ، دع الجميع يقررون مشروعه. نحن فقط ننظر في كيفية القيام بذلك.
بشكل عام ، يمكنك استخدام
الانتقال من Maven إلى Gradle في تعليم أقل من 5 دقائق ، ولكن من غير المرجح أن ترقى النتيجة إلى مستوى التوقعات. ما زلت أوصي بالقيام بالترحيل يدويًا ، ولن يستغرق الأمر وقتًا طويلاً.
أول شيء يتعين علينا القيام به هو
تثبيت Gradle .
ثم نحتاج إلى تنفيذ الإجراء التالي لكل من المشاريع الفرعية -
backend
fronted
:
# 1 حذف ملفات Maven -
pom.xml ،
.mvn .
# 2 في دليل المشروع الفرعي ، قم بتشغيل gradle init والإجابة على الأسئلة:
- حدد نوع المشروع المراد إنشاؤه: أساسي
- اختر لغة التنفيذ: Kotlin
- حدد build script DSL: Kotlin (بما أننا نكتب مشروعًا في Kotlin)
# 3 حذف
settings.gradle.kts - هذا الملف مطلوب فقط للمشروع الجذر.
# 4 تشغيل
gradle wrapper
.
الآن دعنا ننتقل إلى مشروعنا الجذر. لذلك ، تحتاج إلى اتباع الخطوات 1 و 2 و 4 الموضحة أعلاه للمشاريع الفرعية - كل شيء هو نفسه
باستثناء حذف settings.gradle.kts .
سيبدو تكوين
البنية لمشروع
الواجهة الخلفية كما يلي:
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" } }
- يجب تحديد جميع الإضافات المطلوبة من Kotlin و Spring.
- لا تنسى عن البرنامج المساعد org.jetbrains.kotlin.plugin.jpa - من الضروري الاتصال بقاعدة البيانات
- في التبعيات ، يجب عليك تحديد
runtimeOnly(project(":frontend"))
- نحتاج إلى إنشاء مشروع الواجهة الأمامية أولاً
بناء التكوين لمشروع
الواجهة الأمامية :
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") }
- في المثال الخاص بي ، يتم استخدام المكون الإضافي
org.siouan.frontend
لإنشاء المشروع - في قسم
frontend {...}
يجب أن تشير إلى إصدار Node.js ، وكذلك الأوامر التي تستدعي البرامج النصية للتنظيف والتركيب والتجميع المحددة في package.json
- الآن نقوم
runtimeOnly(project(":frontend"))
الأمامية في ملف JAR واستخدامه كتبعية ( runtimeOnly(project(":frontend"))
في الواجهة الخلفية ) ، لذلك نحن بحاجة إلى وصف مهمة تقوم بنسخ الملفات من دليل التجميع إلى / العامة ويخلق ملف جرة
ملاحظة:- قم بتحرير
vue.config.js
، وتغيير دليل vue.config.js
لإنشاء / dist . - في ملف البرنامج النصي
vue-cli-service build
، حدد بناء vue-cli-service build
أو تأكد من تحديده
يجب أن يحتوي ملف settings.gradle.kts في مشروع الجذر على الكود التالي: ...
rootProject.name = "demo" include(":frontend", ":backend")
... هو اسم المشروع والمشاريع الفرعية.
والآن يمكننا بناء المشروع عن طريق تشغيل الأمر:
./gradlew build
ملاحظة: إذا كانت العناصر النائبة المحددة في
application.properties (على سبيل المثال ،
${SPRING_DATASOURCE_URL}
) لا توجد متغيرات بيئة مقابلة ،
${SPRING_DATASOURCE_URL}
التجميع. لتجنب ذلك ، استخدم
/gradlew build -x
يمكنك التحقق من بنية المشروعات باستخدام أمر
gradle -q projects
، يجب أن تبدو النتيجة مشابهة لهذا:
Root project 'demo' +--- Project ':backend' \--- Project ':frontend'
وأخيرًا ، لتشغيل التطبيق ، يجب تشغيل
./gradlew bootRun
.
.gitignore
يجب إضافة الملفات والمجلدات التالية إلى ملف
.gitignore :
- الخلفية / بناء /
- الواجهة الأمامية / بناء /
- بناء
- .gradle
هام: يجب ألا تضيف ملفات
gradlew
إلى
gradlew
- لا يوجد شيء خطير فيها ، ولكنها ضرورية للتجميع الناجح على خادم بعيد.
نشر على Heroku
دعونا نلقي نظرة على التغييرات التي نحتاج إليها من أجل نشر التطبيق بأمان في Heroku.
# 1 Procfileنحن بحاجة إلى أن نسأل تعليمات Heroku جديدة لإطلاق التطبيق:
web: java -Dserver.port=$PORT -jar backend/build/libs/backend-0.0.1-SNAPSHOT.jar
# 2 متغيرات البيئةHeroku قادر على إعادة تشكيل نوع التطبيق (على سبيل المثال ، تطبيق Spring Boot) واتباع إرشادات التجميع المناسبة. لكن تطبيقنا (مشروع الجذر) لا يبدو وكأنه تطبيق Spring Boot على Heroku. إذا تركنا كل شيء كما هو ، فسيطلب منا Heroku تحديد
stage
. بصراحة ، لا أعرف أين ينتهي هذا المسار ، لأنني لم أتبعه. من الأسهل تحديد متغير
GRADLE_TASK
بقيمة
build
:
# 3 reCAPTCHAعند وضع التطبيق في مجال جديد ، لا تنسَ تحديث كلمة التحقق ، حيث تعمل متغيرات البيئة على
GOOGLE_RECAPTCHA_KEY_SITE
و
GOOGLE_RECAPTCHA_KEY_SECRET
، وكذلك تحديث مفتاح الموقع في الواجهة الفرعية للمشروع الأمامي.
تخزين JWT رمز في ملفات تعريف الارتباط
بادئ ذي بدء ، أوصي بشدة بقراءة مقال "
الرجاء التوقف عن استخدام التخزين المحلي" ، وخاصةً
لماذا يكون التخزين المحلي غير آمن ويجب عدم استخدامه لتخزين البيانات الحساسة .
دعونا نلقي نظرة على كيفية تخزين الرمز المميز JWT في مكان أكثر أمانًا - ملفات تعريف الارتباط في علامة
httpOnly
، حيث لن يكون متاحًا للقراءة / التغيير باستخدام JavaScript.
# 1 إزالة كل المنطق المتعلق بـ JWT من الواجهة الأمامية:
نظرًا لأن الرمز المميز لا يزال غير قابل للوصول بواسطة JavaScript باستخدام طريقة التخزين الجديدة ، يمكنك إزالة جميع الإشارات إليه بأمان من مشروعنا الفرعي.
لكن دور المستخدم دون الرجوع إلى أي بيانات أخرى ليس معلومات مهمة ، فلا يزال من الممكن تخزينه في التخزين المحلي وتحديد ما إذا كان المستخدم مصرحًا به أم لا ، وفقًا لما إذا كان هذا الدور محددًا أم لا.
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 });
كن حذرًا عند إعادة التسكين store/index.js
: إذا لم يعمل التفويض وإلغاء التصريح بشكل صحيح ، فستستمر رسائل الخطأ في وحدة التحكم.# 2 إرجاع JWT كملف تعريف ارتباط في وحدة تحكم التخويل ( وليس في نص الاستجابة ):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) } }
هام : يرجى ملاحظة أن أضع خيارات authCookieName
و isCookieSecure
في application.properties - إرسال ملفات تعريف الارتباط إلى العلم secure
لا يمكن إلا الشبكي، الأمر الذي يجعل من الصعب التصحيح مع المضيف المحلي للغاية. ولكن في الإنتاج ، بالطبع ، من الأفضل استخدام ملفات تعريف الارتباط مع هذه العلامة.أيضًا ، يُنصح الآن باستجابات وحدة التحكم لاستخدام كيان بدون حقل خاص لـ JWT. التحديثرقم 3JwtAuthTokenFilter
:اعتدنا أن نأخذ رمزًا مميزًا من رأس الطلب ، والآن نأخذه من ملفات تعريف الارتباط: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 تمكين CORSإذا كان في مقالتي السابقة لا يزال بإمكانك تخطي هذا السؤال بهدوء ، الآن سيكون من الغريب حماية الرمز المميز JWT دون الحاجة إلى تمكين CORS على الجانب الخلفي.يمكنك إصلاح هذا عن طريق التعديل 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() }
والآن يمكنك إزالة جميع التعليقات التوضيحية @CrossOrigin
من وحدات التحكم.هام: معلمة AllowCredentials مطلوبة لإرسال الطلبات من الواجهة الأمامية. اقرأ المزيد عن هذا هنا .# 5 تحديث الرؤوس على الجانب الأمامي: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 } })
تفتيش
دعونا نحاول تسجيل الدخول إلى التطبيق من خلال تسجيل الدخول من مضيف غير موجود في القائمة المسموح بها WebSecurityConfig.kt
. للقيام بذلك ، قم بتشغيل الواجهة الخلفية على المنفذ 8080
، والواجهة الأمامية ، على سبيل المثال ، 8082
وحاول تسجيل الدخول:طلب تفويض تم رفضه بواسطة سياسة CORS.الآن دعنا نرى كيف تعمل ملفات تعريف ارتباط العلم بشكل عام httpOnly
. للقيام بذلك ، دعنا نذهب ، على سبيل المثال ، إلى الموقع https://kotlinlang.org وننفذه في وحدة تحكم المستعرض: document.cookie
سوف تظهر httpOnly
ملفات تعريف الارتباط غير المرتبطة بهذا الموقع في وحدة التحكم ، والتي ، كما نرى ، يمكن الوصول إليها عبر JavaScript.الآن دعنا نذهب إلى تطبيقنا ، قم بتسجيل الدخول (بحيث يحفظ المتصفح ملف تعريف الارتباط مع JWT) وكرر نفس الشيء:ملاحظة: طريقة تخزين الرمز المميز JWT هذه أكثر موثوقية من استخدام Local Storage ، لكن يجب أن تفهم أنها ليست حلاً سحريًا.تأكيد تسجيل البريد الإلكتروني
فيما يلي خوارزمية مختصرة لتنفيذ هذه المهمة:- بالنسبة لجميع المستخدمين الجدد ،
isEnabled
يتم تعيين السمة في قاعدة البيانات علىfalse
- يتم إنشاء رمز سلسلة من الأحرف التعسفية ، والتي ستكون بمثابة مفتاح لتأكيد التسجيل
- يتم إرسال الرمز المميز إلى المستخدم في البريد كجزء من الارتباط
isEnabled
يتم ضبط السمة على " صحيح" إذا اتبع المستخدم الرابط لفترة زمنية محددة.
الآن النظر في هذه العملية بمزيد من التفصيل.نحتاج إلى جدول لتخزين الرمز المميز لتأكيد التسجيل: 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;
ووفقًا لذلك ، كيان جديد لرسم الخرائط ذات العلاقة بالكائن ...: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) }
... والمستودع: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> }
نحن الآن بحاجة إلى تنفيذ أدوات لإدارة الرموز - إنشاء والتحقق والإرسال عبر البريد الإلكتروني. للقيام بذلك ، نقوم بتعديل UserDetailsServiceImpl
عن طريق إضافة طرق لإنشاء والتحقق من الرمز المميز: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 } }
أضف الآن طريقة لإرسال بريد إلكتروني مع رابط تأكيد إلى 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)} }
ملاحظة:- أوصي بتخزين عنوان URL الخاص بالمضيف في application.properties
- في الرابط الخاص بنا ، نقوم بتمرير معلمتين GET (
token
و confirmRegistration
) إلى العنوان الذي يتم فيه نشر التطبيق. في وقت لاحق سوف اشرح لماذا.
نقوم بتعديل وحدة التحكم في التسجيل كما يلي:- سيقوم جميع المستخدمين الجدد بتعيين القيمة
false
للحقلisEnabled
- بعد إنشاء حساب جديد ، سوف نرسل رسالة بريد إلكتروني لتأكيد التسجيل
- إنشاء وحدة تحكم التحقق من صحة الرمز منفصلة
- مهم: أثناء التفويض ، سنتحقق من التحقق من الحساب:
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 {
الآن ، دعونا نعمل على الواجهة الأمامية:# 1 إنشاء مكون RegistrationConfirmPage.vue
# 2 أضف مسارًا جديدًا router.js
باستخدام المعلمة :token
: { path: '/registration-confirm/:token', name: 'RegistrationConfirmPage', component: RegistrationConfirmPage }
# 3 تحديث SignUp.vue
- بعد إرسال البيانات بنجاح من النماذج ، سنبلغهم أنه لإكمال التسجيل ، يجب عليك اتباع الرابط في الرسالة.# 4 مهم: للأسف ، لا يمكننا إعطاء رابط ثابت لمكون منفصل من شأنه التحقق من صحة الرمز المميز والإبلاغ عن النجاح أو الفشل. ستؤدي بنا الروابط مع مسارات القطع المائلة إلى صفحة التطبيق الأصلية. ولكن يمكننا إخبار طلبنا عن الحاجة إلى تأكيد التسجيل باستخدام معلمة GET المنقولة 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 لنقم بإنشاء مكون يقوم بالتحقق من الرمز المميز والتقارير حول نتيجة التحقق من الصحة: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>
بدلا من الاستنتاج
في الختام من هذه المادة ، أود أن أستنبط وأقول إن مفهوم التطبيق الذي تمت مناقشته في هذا المقال والمقال السابق لم يكن جديدًا في وقت كتابة هذا التقرير. العمل بسرعة إنشاء كومة كاملة من التطبيقات على التمهيد الربيع باستخدام جافا سكريبت الأطر الحديثة الزاوي / تتفاعل / Vue.js يحل بأناقة محب .ومع ذلك ، قد يتم تطبيق الأفكار الموضحة في هذه المقالة حتى باستخدام JHipster ، لذلك آمل أن يجد القراء الذين يأتون إلى هذا المكان هذه المادة مفيدة حتى كغذاء للتفكير.روابط مفيدة