]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Implement contact form in the client
authorChocobozzz <me@florianbigard.com>
Thu, 10 Jan 2019 10:12:41 +0000 (11:12 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 10 Jan 2019 10:32:38 +0000 (11:32 +0100)
18 files changed:
client/src/app/+about/about-instance/about-instance.component.html
client/src/app/+about/about-instance/about-instance.component.scss
client/src/app/+about/about-instance/about-instance.component.ts
client/src/app/+about/about-instance/contact-admin-modal.component.html [new file with mode: 0644]
client/src/app/+about/about-instance/contact-admin-modal.component.scss [new file with mode: 0644]
client/src/app/+about/about-instance/contact-admin-modal.component.ts [new file with mode: 0644]
client/src/app/+about/about.module.ts
client/src/app/core/server/server.service.ts
client/src/app/shared/forms/form-validators/index.ts
client/src/app/shared/forms/form-validators/instance-validators.service.ts [new file with mode: 0644]
client/src/app/shared/instance/instance.service.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts
scripts/clean/server/test.sh
server/controllers/api/config.ts
server/initializers/checker-after-init.ts
server/initializers/checker-before-init.ts
server/lib/emailer.ts
server/middlewares/validators/server.ts

index 37ff795f5a061660e82f601817cda7f4f2e605e7..8c700752e8ca35c4c01be0309d42f10039f955cf 100644 (file)
@@ -1,7 +1,9 @@
 <div class="row">
   <div class="col-md-12 col-xl-6">
-    <div i18n class="about-instance-title">
-      About {{ instanceName }} instance
+    <div class="about-instance-title">
+      <div i18n>About {{ instanceName }} instance</div>
+
+      <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
     </div>
 
     <div class="short-description">
@@ -46,3 +48,5 @@
     <my-instance-features-table></my-instance-features-table>
   </div>
 </div>
+
+<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>
index b451e85aa452ea2c46911bc4c5c8eedc5d645653..75cf573220df28e4fe45792f11010147a7f4c3ea 100644 (file)
@@ -2,9 +2,19 @@
 @import '_mixins';
 
 .about-instance-title {
-  font-size: 20px;
-  font-weight: bold;
-  margin-bottom: 15px;
+  display: flex;
+  justify-content: space-between;
+
+  & > div {
+    font-size: 20px;
+    font-weight: bold;
+    margin-bottom: 15px;
+  }
+
+  & > .contact-admin {
+    @include peertube-button;
+    @include orange-button;
+  }
 }
 
 .section-title {
index 36e7a8e5b00f5cc8a339269554d144217c0c2cd3..d3ee8a1e437f0c98dbccad2f8abe297edf57de68 100644 (file)
@@ -1,7 +1,9 @@
-import { Component, OnInit } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
 import { Notifier, ServerService } from '@app/core'
 import { MarkdownService } from '@app/videos/shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
+import { InstanceService } from '@app/shared/instance/instance.service'
 
 @Component({
   selector: 'my-about-instance',
@@ -9,6 +11,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './about-instance.component.scss' ]
 })
 export class AboutInstanceComponent implements OnInit {
+  @ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent
+
   shortDescription = ''
   descriptionHTML = ''
   termsHTML = ''
@@ -16,6 +20,7 @@ export class AboutInstanceComponent implements OnInit {
   constructor (
     private notifier: Notifier,
     private serverService: ServerService,
+    private instanceService: InstanceService,
     private markdownService: MarkdownService,
     private i18n: I18n
   ) {}
@@ -32,8 +37,12 @@ export class AboutInstanceComponent implements OnInit {
     return this.serverService.getConfig().signup.allowed
   }
 
+  get isContactFormEnabled () {
+    return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled
+  }
+
   ngOnInit () {
-    this.serverService.getAbout()
+    this.instanceService.getAbout()
       .subscribe(
         res => {
           this.shortDescription = res.instance.shortDescription
@@ -45,4 +54,8 @@ export class AboutInstanceComponent implements OnInit {
       )
   }
 
+  openContactModal () {
+    return this.contactAdminModal.show()
+  }
+
 }
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html
new file mode 100644 (file)
index 0000000..2b3fb32
--- /dev/null
@@ -0,0 +1,50 @@
+<ng-template #modal>
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
+    <span class="close" aria-label="Close" role="button" (click)="hide()"></span>
+  </div>
+
+  <div class="modal-body">
+
+    <form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
+      <div class="form-group">
+        <label i18n for="fromName">Your name</label>
+        <input
+          type="text" id="fromName"
+          formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
+        >
+        <div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
+      </div>
+
+      <div class="form-group">
+        <label i18n for="fromEmail">Your email</label>
+        <input
+          type="text" id="fromEmail"
+          formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
+        >
+        <div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
+      </div>
+
+      <div class="form-group">
+        <label i18n for="body">Your message</label>
+        <textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }">
+        </textarea>
+        <div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
+      </div>
+
+      <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+      <div class="form-group inputs">
+        <span i18n class="action-button action-button-cancel" (click)="hide()">
+          Cancel
+        </span>
+
+        <input
+          type="submit" i18n-value value="Submit" class="action-button-submit"
+          [disabled]="!form.valid"
+        >
+      </div>
+    </form>
+
+  </div>
+</ng-template>
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss
new file mode 100644 (file)
index 0000000..260d778
--- /dev/null
@@ -0,0 +1,11 @@
+@import 'variables';
+@import 'mixins';
+
+input[type=text] {
+  @include peertube-input-text(340px);
+  display: block;
+}
+
+textarea {
+  @include peertube-textarea(100%, 200px);
+}
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
new file mode 100644 (file)
index 0000000..2f707bd
--- /dev/null
@@ -0,0 +1,72 @@
+import { Component, OnInit, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { FormReactive, InstanceValidatorsService } from '@app/shared'
+import { InstanceService } from '@app/shared/instance/instance.service'
+
+@Component({
+  selector: 'my-contact-admin-modal',
+  templateUrl: './contact-admin-modal.component.html',
+  styleUrls: [ './contact-admin-modal.component.scss' ]
+})
+export class ContactAdminModalComponent extends FormReactive implements OnInit {
+  @ViewChild('modal') modal: NgbModal
+
+  error: string
+
+  private openedModal: NgbModalRef
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private modalService: NgbModal,
+    private instanceValidatorsService: InstanceValidatorsService,
+    private instanceService: InstanceService,
+    private notifier: Notifier,
+    private i18n: I18n
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.buildForm({
+      fromName: this.instanceValidatorsService.FROM_NAME,
+      fromEmail: this.instanceValidatorsService.FROM_EMAIL,
+      body: this.instanceValidatorsService.BODY
+    })
+  }
+
+  show () {
+    this.openedModal = this.modalService.open(this.modal, { keyboard: false })
+  }
+
+  hide () {
+    this.form.reset()
+    this.error = undefined
+
+    this.openedModal.close()
+    this.openedModal = null
+  }
+
+  sendForm () {
+    const fromName = this.form.value['fromName']
+    const fromEmail = this.form.value[ 'fromEmail' ]
+    const body = this.form.value[ 'body' ]
+
+    this.instanceService.contactAdministrator(fromEmail, fromName, body)
+        .subscribe(
+          () => {
+            this.notifier.success(this.i18n('Your message has been sent.'))
+            this.hide()
+          },
+
+          err => {
+            this.error = err.status === 403
+              ? this.i18n('You already sent this form recently')
+              : err.message
+          }
+        )
+  }
+}
index ff6e8ef414a41848ae64d924b00d348f6393bf5e..9c6b29740dd4f833f044e1c84506709474c7aeff 100644 (file)
@@ -5,6 +5,7 @@ import { AboutComponent } from './about.component'
 import { SharedModule } from '../shared'
 import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
 import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
+import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
 
 @NgModule({
   imports: [
@@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub
   declarations: [
     AboutComponent,
     AboutInstanceComponent,
-    AboutPeertubeComponent
+    AboutPeertubeComponent,
+    ContactAdminModalComponent
   ],
 
   exports: [
index 5351f18d5fd74742fec663f725b12b64b00d4152..f33e6f20ce9648ce0d771ff08fbc5804c9e722cd 100644 (file)
@@ -13,6 +13,7 @@ import { sortBy } from '@app/shared/misc/utils'
 
 @Injectable()
 export class ServerService {
+  private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
   private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
   private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
   private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
@@ -147,10 +148,6 @@ export class ServerService {
     return this.videoPrivacies
   }
 
-  getAbout () {
-    return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about')
-  }
-
   private loadVideoAttributeEnum (
     attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
     hashToPopulate: VideoConstant<string | number>[],
index 74e385b3d769d868ccaa6d7b23915782109422f1..fdcbedb71c026e17992aa35259488efdcf831caf 100644 (file)
@@ -1,6 +1,7 @@
 export * from './custom-config-validators.service'
 export * from './form-validator.service'
 export * from './host'
+export * from './instance-validators.service'
 export * from './login-validators.service'
 export * from './reset-password-validators.service'
 export * from './user-validators.service'
diff --git a/client/src/app/shared/forms/form-validators/instance-validators.service.ts b/client/src/app/shared/forms/form-validators/instance-validators.service.ts
new file mode 100644 (file)
index 0000000..5bb8528
--- /dev/null
@@ -0,0 +1,48 @@
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Validators } from '@angular/forms'
+import { BuildFormValidator } from '@app/shared'
+import { Injectable } from '@angular/core'
+
+@Injectable()
+export class InstanceValidatorsService {
+  readonly FROM_EMAIL: BuildFormValidator
+  readonly FROM_NAME: BuildFormValidator
+  readonly BODY: BuildFormValidator
+
+  constructor (private i18n: I18n) {
+
+    this.FROM_EMAIL = {
+      VALIDATORS: [ Validators.required, Validators.email ],
+      MESSAGES: {
+        'required': this.i18n('Email is required.'),
+        'email': this.i18n('Email must be valid.')
+      }
+    }
+
+    this.FROM_NAME = {
+      VALIDATORS: [
+        Validators.required,
+        Validators.minLength(1),
+        Validators.maxLength(120)
+      ],
+      MESSAGES: {
+        'required': this.i18n('Your name is required.'),
+        'minlength': this.i18n('Your name must be at least 1 character long.'),
+        'maxlength': this.i18n('Your name cannot be more than 120 characters long.')
+      }
+    }
+
+    this.BODY = {
+      VALIDATORS: [
+        Validators.required,
+        Validators.minLength(3),
+        Validators.maxLength(5000)
+      ],
+      MESSAGES: {
+        'required': this.i18n('A message is required.'),
+        'minlength': this.i18n('The message must be at least 3 characters long.'),
+        'maxlength': this.i18n('The message cannot be more than 5000 characters long.')
+      }
+    }
+  }
+}
diff --git a/client/src/app/shared/instance/instance.service.ts b/client/src/app/shared/instance/instance.service.ts
new file mode 100644 (file)
index 0000000..61321ec
--- /dev/null
@@ -0,0 +1,36 @@
+import { catchError } from 'rxjs/operators'
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { environment } from '../../../environments/environment'
+import { RestExtractor, RestService } from '../rest'
+import { About } from '../../../../../shared/models/server'
+
+@Injectable()
+export class InstanceService {
+  private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
+  private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
+
+  constructor (
+    private authHttp: HttpClient,
+    private restService: RestService,
+    private restExtractor: RestExtractor
+  ) {
+  }
+
+  getAbout () {
+    return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
+               .pipe(catchError(res => this.restExtractor.handleError(res)))
+  }
+
+  contactAdministrator (fromEmail: string, fromName: string, message: string) {
+    const body = {
+      fromEmail,
+      fromName,
+      body: message
+    }
+
+    return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body)
+               .pipe(catchError(res => this.restExtractor.handleError(res)))
+
+  }
+}
index c99c87c00cd50a00229d35a99bcb129b1b39088c..d1320aeece0f4226b0391e33a9a8dd7c38620c13 100644 (file)
@@ -37,6 +37,7 @@ import {
   LoginValidatorsService,
   ReactiveFileComponent,
   ResetPasswordValidatorsService,
+  InstanceValidatorsService,
   TextareaAutoResizeDirective,
   UserValidatorsService,
   VideoAbuseValidatorsService,
@@ -65,6 +66,7 @@ import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.com
 import { UserHistoryService } from '@app/shared/users/user-history.service'
 import { UserNotificationService } from '@app/shared/users/user-notification.service'
 import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
+import { InstanceService } from '@app/shared/instance/instance.service'
 
 @NgModule({
   imports: [
@@ -185,8 +187,10 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications
     OverviewService,
     VideoChangeOwnershipValidatorsService,
     VideoAcceptOwnershipValidatorsService,
+    InstanceValidatorsService,
     BlocklistService,
     UserHistoryService,
+    InstanceService,
 
     I18nPrimengCalendarService,
     ScreenService,
index 75ad491bfd945a308f9238f92f8bdcb8ed38e24c..b897c30baf8e34005477bd7fef835e399348dde9 100755 (executable)
@@ -13,7 +13,7 @@ recreateDB () {
 }
 
 removeFiles () {
-  rm -rf "./test$1" "./config/local-test-$1.json"
+  rm -rf "./test$1" "./config/local-test.json" "./config/local-test-$1.json"
 }
 
 dropRedis () {
index 43b20e07879bc981d43da52408688c3c4d6e7c94..dd06a0597da6ae63b2ded487ef3686b91457da89 100644 (file)
@@ -65,7 +65,7 @@ async function getConfig (req: express.Request, res: express.Response) {
       }
     },
     email: {
-      enabled: Emailer.Instance.isEnabled()
+      enabled: Emailer.isEnabled()
     },
     contactForm: {
       enabled: CONFIG.CONTACT_FORM.ENABLED
index 72d84695773eb4f0c01f025c7c9c6ff74a6068c3..955d55206cc40e73dade1619f18fdce2ebbb2e63 100644 (file)
@@ -10,6 +10,7 @@ import { getServerActor } from '../helpers/utils'
 import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
 import { isArray } from '../helpers/custom-validators/misc'
 import { uniq } from 'lodash'
+import { Emailer } from '../lib/emailer'
 
 async function checkActivityPubUrls () {
   const actor = await getServerActor()
@@ -32,9 +33,19 @@ async function checkActivityPubUrls () {
 // Some checks on configuration files
 // Return an error message, or null if everything is okay
 function checkConfig () {
-  const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
+
+  if (!Emailer.isEnabled()) {
+    if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
+      return 'Emailer is disabled but you require signup email verification.'
+    }
+
+    if (CONFIG.CONTACT_FORM.ENABLED) {
+      logger.warn('Emailer is disabled so the contact form will not work.')
+    }
+  }
 
   // NSFW policy
+  const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
   {
     const available = [ 'do_not_list', 'blur', 'display' ]
     if (available.indexOf(defaultNSFWPolicy) === -1) {
@@ -68,6 +79,7 @@ function checkConfig () {
     }
   }
 
+  // Check storage directory locations
   if (isProdInstance()) {
     const configStorage = config.get('storage')
     for (const key of Object.keys(configStorage)) {
index a7bc7eec83b20d4c163fb81a80c19f1f59bbd583..7905d9ffaee5c087fb876077609b7f6d54928b0d 100644 (file)
@@ -15,7 +15,7 @@ function checkMissedConfig () {
     'storage.redundancy', 'storage.tmp',
     'log.level',
     'user.video_quota', 'user.video_quota_daily',
-    'cache.previews.size', 'admin.email',
+    'cache.previews.size', 'admin.email', 'contact_form.enabled',
     'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
     'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
     'redundancy.videos.strategies', 'redundancy.videos.check_interval',
index 9b1c5122ff2575f85494d2e00e26b23862dcad28..f384a254e017d6912a71e8063ce2c25c19aa8512 100644 (file)
@@ -18,7 +18,6 @@ class Emailer {
   private static instance: Emailer
   private initialized = false
   private transporter: Transporter
-  private enabled = false
 
   private constructor () {}
 
@@ -27,7 +26,7 @@ class Emailer {
     if (this.initialized === true) return
     this.initialized = true
 
-    if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) {
+    if (Emailer.isEnabled()) {
       logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
 
       let tls
@@ -55,8 +54,6 @@ class Emailer {
         tls,
         auth
       })
-
-      this.enabled = true
     } else {
       if (!isTestInstance()) {
         logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -64,8 +61,8 @@ class Emailer {
     }
   }
 
-  isEnabled () {
-    return this.enabled
+  static isEnabled () {
+    return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
   }
 
   async checkConnectionOrDie () {
@@ -374,7 +371,7 @@ class Emailer {
   }
 
   sendMail (to: string[], subject: string, text: string, from?: string) {
-    if (!this.enabled) {
+    if (!Emailer.isEnabled()) {
       throw new Error('Cannot send mail because SMTP is not configured.')
     }
 
index d82e19230bd12dd036713810f4cb45723c1b0af7..d85afc2ffee8875356422d7ad5ae5ec8b0469b60 100644 (file)
@@ -50,7 +50,7 @@ const contactAdministratorValidator = [
         .end()
     }
 
-    if (Emailer.Instance.isEnabled() === false) {
+    if (Emailer.isEnabled() === false) {
       return res
         .status(409)
         .send({ error: 'Emailer is not enabled on this instance.' })