-
- About {{ instanceName }} instance
+
+
About {{ instanceName }} instance
+
+
Contact administrator
@@ -46,3 +48,5 @@
+
+
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index b451e85aa..75cf57322 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -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 {
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts
index 36e7a8e5b..d3ee8a1e4 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -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
index 000000000..2b3fb32f3
--- /dev/null
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html
@@ -0,0 +1,50 @@
+
+
+
+
+
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
index 000000000..260d77888
--- /dev/null
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss
@@ -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
index 000000000..2f707bd53
--- /dev/null
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
@@ -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
+ }
+ )
+ }
+}
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts
index ff6e8ef41..9c6b29740 100644
--- a/client/src/app/+about/about.module.ts
+++ b/client/src/app/+about/about.module.ts
@@ -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: [
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 5351f18d5..f33e6f20c 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -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
(ServerService.BASE_CONFIG_URL + '/about')
- }
-
private loadVideoAttributeEnum (
attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
hashToPopulate: VideoConstant[],
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
index 74e385b3d..fdcbedb71 100644
--- a/client/src/app/shared/forms/form-validators/index.ts
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -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
index 000000000..5bb852858
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/instance-validators.service.ts
@@ -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
index 000000000..61321ecce
--- /dev/null
+++ b/client/src/app/shared/instance/instance.service.ts
@@ -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(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)))
+
+ }
+}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index c99c87c00..d1320aeec 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -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,
diff --git a/scripts/clean/server/test.sh b/scripts/clean/server/test.sh
index 75ad491bf..b897c30ba 100755
--- a/scripts/clean/server/test.sh
+++ b/scripts/clean/server/test.sh
@@ -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 () {
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 43b20e078..dd06a0597 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -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
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 72d846957..955d55206 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -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)) {
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index a7bc7eec8..7905d9ffa 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -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',
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 9b1c5122f..f384a254e 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -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.')
}
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
index d82e19230..d85afc2ff 100644
--- a/server/middlewares/validators/server.ts
+++ b/server/middlewares/validators/server.ts
@@ -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.' })