diff options
author | Chocobozzz <me@florianbigard.com> | 2018-01-30 13:27:07 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-01-30 13:27:07 +0100 |
commit | ecb4e35f4e6c7304cb274593c13cb47fd5078b75 (patch) | |
tree | 1e238002340bc521afde59d52f406e41298a7aac /server/lib | |
parent | 80d1057bfcd3582af0dacf5ccd5a7a93ef95410b (diff) | |
download | PeerTube-ecb4e35f4e6c7304cb274593c13cb47fd5078b75.tar.gz PeerTube-ecb4e35f4e6c7304cb274593c13cb47fd5078b75.tar.zst PeerTube-ecb4e35f4e6c7304cb274593c13cb47fd5078b75.zip |
Add ability to reset our password
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/emailer.ts | 106 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/email.ts | 22 | ||||
-rw-r--r-- | server/lib/job-queue/job-queue.ts | 9 | ||||
-rw-r--r-- | server/lib/redis.ts | 84 |
4 files changed, 219 insertions, 2 deletions
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts new file mode 100644 index 000000000..f5b68640e --- /dev/null +++ b/server/lib/emailer.ts | |||
@@ -0,0 +1,106 @@ | |||
1 | import { createTransport, Transporter } from 'nodemailer' | ||
2 | import { isTestInstance } from '../helpers/core-utils' | ||
3 | import { logger } from '../helpers/logger' | ||
4 | import { CONFIG } from '../initializers' | ||
5 | import { JobQueue } from './job-queue' | ||
6 | import { EmailPayload } from './job-queue/handlers/email' | ||
7 | import { readFileSync } from 'fs' | ||
8 | |||
9 | class Emailer { | ||
10 | |||
11 | private static instance: Emailer | ||
12 | private initialized = false | ||
13 | private transporter: Transporter | ||
14 | |||
15 | private constructor () {} | ||
16 | |||
17 | init () { | ||
18 | // Already initialized | ||
19 | if (this.initialized === true) return | ||
20 | this.initialized = true | ||
21 | |||
22 | if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) { | ||
23 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) | ||
24 | |||
25 | let tls | ||
26 | if (CONFIG.SMTP.CA_FILE) { | ||
27 | tls = { | ||
28 | ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ] | ||
29 | } | ||
30 | } | ||
31 | |||
32 | this.transporter = createTransport({ | ||
33 | host: CONFIG.SMTP.HOSTNAME, | ||
34 | port: CONFIG.SMTP.PORT, | ||
35 | secure: CONFIG.SMTP.TLS, | ||
36 | tls, | ||
37 | auth: { | ||
38 | user: CONFIG.SMTP.USERNAME, | ||
39 | pass: CONFIG.SMTP.PASSWORD | ||
40 | } | ||
41 | }) | ||
42 | } else { | ||
43 | if (!isTestInstance()) { | ||
44 | logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | async checkConnectionOrDie () { | ||
50 | if (!this.transporter) return | ||
51 | |||
52 | try { | ||
53 | const success = await this.transporter.verify() | ||
54 | if (success !== true) this.dieOnConnectionFailure() | ||
55 | |||
56 | logger.info('Successfully connected to SMTP server.') | ||
57 | } catch (err) { | ||
58 | this.dieOnConnectionFailure(err) | ||
59 | } | ||
60 | } | ||
61 | |||
62 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | ||
63 | const text = `Hi dear user,\n\n` + | ||
64 | `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + | ||
65 | `Please follow this link to reset it: ${resetPasswordUrl}.\n\n` + | ||
66 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
67 | `Cheers,\n` + | ||
68 | `PeerTube.` | ||
69 | |||
70 | const emailPayload: EmailPayload = { | ||
71 | to: [ to ], | ||
72 | subject: 'Reset your PeerTube password', | ||
73 | text | ||
74 | } | ||
75 | |||
76 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
77 | } | ||
78 | |||
79 | sendMail (to: string[], subject: string, text: string) { | ||
80 | if (!this.transporter) { | ||
81 | throw new Error('Cannot send mail because SMTP is not configured.') | ||
82 | } | ||
83 | |||
84 | return this.transporter.sendMail({ | ||
85 | from: CONFIG.SMTP.FROM_ADDRESS, | ||
86 | to: to.join(','), | ||
87 | subject, | ||
88 | text | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | private dieOnConnectionFailure (err?: Error) { | ||
93 | logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, err) | ||
94 | process.exit(-1) | ||
95 | } | ||
96 | |||
97 | static get Instance () { | ||
98 | return this.instance || (this.instance = new this()) | ||
99 | } | ||
100 | } | ||
101 | |||
102 | // --------------------------------------------------------------------------- | ||
103 | |||
104 | export { | ||
105 | Emailer | ||
106 | } | ||
diff --git a/server/lib/job-queue/handlers/email.ts b/server/lib/job-queue/handlers/email.ts new file mode 100644 index 000000000..9d7686116 --- /dev/null +++ b/server/lib/job-queue/handlers/email.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import * as kue from 'kue' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { Emailer } from '../../emailer' | ||
4 | |||
5 | export type EmailPayload = { | ||
6 | to: string[] | ||
7 | subject: string | ||
8 | text: string | ||
9 | } | ||
10 | |||
11 | async function processEmail (job: kue.Job) { | ||
12 | const payload = job.data as EmailPayload | ||
13 | logger.info('Processing email in job %d.', job.id) | ||
14 | |||
15 | return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text) | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | processEmail | ||
22 | } | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 7a2b6c78d..3f176f896 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -5,19 +5,22 @@ import { CONFIG, JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY } from '. | |||
5 | import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' | 5 | import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' |
6 | import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' | 6 | import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' |
7 | import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' | 7 | import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' |
8 | import { EmailPayload, processEmail } from './handlers/email' | ||
8 | import { processVideoFile, VideoFilePayload } from './handlers/video-file' | 9 | import { processVideoFile, VideoFilePayload } from './handlers/video-file' |
9 | 10 | ||
10 | type CreateJobArgument = | 11 | type CreateJobArgument = |
11 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 12 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
12 | { type: 'activitypub-http-unicast', payload: ActivitypubHttpUnicastPayload } | | 13 | { type: 'activitypub-http-unicast', payload: ActivitypubHttpUnicastPayload } | |
13 | { type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } | | 14 | { type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } | |
14 | { type: 'video-file', payload: VideoFilePayload } | 15 | { type: 'video-file', payload: VideoFilePayload } | |
16 | { type: 'email', payload: EmailPayload } | ||
15 | 17 | ||
16 | const handlers: { [ id in JobType ]: (job: kue.Job) => Promise<any>} = { | 18 | const handlers: { [ id in JobType ]: (job: kue.Job) => Promise<any>} = { |
17 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, | 19 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, |
18 | 'activitypub-http-unicast': processActivityPubHttpUnicast, | 20 | 'activitypub-http-unicast': processActivityPubHttpUnicast, |
19 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, | 21 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, |
20 | 'video-file': processVideoFile | 22 | 'video-file': processVideoFile, |
23 | 'email': processEmail | ||
21 | } | 24 | } |
22 | 25 | ||
23 | class JobQueue { | 26 | class JobQueue { |
@@ -43,6 +46,8 @@ class JobQueue { | |||
43 | } | 46 | } |
44 | }) | 47 | }) |
45 | 48 | ||
49 | this.jobQueue.setMaxListeners(15) | ||
50 | |||
46 | this.jobQueue.on('error', err => { | 51 | this.jobQueue.on('error', err => { |
47 | logger.error('Error in job queue.', err) | 52 | logger.error('Error in job queue.', err) |
48 | process.exit(-1) | 53 | process.exit(-1) |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts new file mode 100644 index 000000000..4240cc162 --- /dev/null +++ b/server/lib/redis.ts | |||
@@ -0,0 +1,84 @@ | |||
1 | import { createClient, RedisClient } from 'redis' | ||
2 | import { logger } from '../helpers/logger' | ||
3 | import { generateRandomString } from '../helpers/utils' | ||
4 | import { CONFIG, USER_PASSWORD_RESET_LIFETIME } from '../initializers' | ||
5 | |||
6 | class Redis { | ||
7 | |||
8 | private static instance: Redis | ||
9 | private initialized = false | ||
10 | private client: RedisClient | ||
11 | private prefix: string | ||
12 | |||
13 | private constructor () {} | ||
14 | |||
15 | init () { | ||
16 | // Already initialized | ||
17 | if (this.initialized === true) return | ||
18 | this.initialized = true | ||
19 | |||
20 | this.client = createClient({ | ||
21 | host: CONFIG.REDIS.HOSTNAME, | ||
22 | port: CONFIG.REDIS.PORT | ||
23 | }) | ||
24 | |||
25 | this.client.on('error', err => { | ||
26 | logger.error('Error in Redis client.', err) | ||
27 | process.exit(-1) | ||
28 | }) | ||
29 | |||
30 | if (CONFIG.REDIS.AUTH) { | ||
31 | this.client.auth(CONFIG.REDIS.AUTH) | ||
32 | } | ||
33 | |||
34 | this.prefix = 'redis-' + CONFIG.WEBSERVER.HOST + '-' | ||
35 | } | ||
36 | |||
37 | async setResetPasswordVerificationString (userId: number) { | ||
38 | const generatedString = await generateRandomString(32) | ||
39 | |||
40 | await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME) | ||
41 | |||
42 | return generatedString | ||
43 | } | ||
44 | |||
45 | async getResetPasswordLink (userId: number) { | ||
46 | return this.getValue(this.generateResetPasswordKey(userId)) | ||
47 | } | ||
48 | |||
49 | private getValue (key: string) { | ||
50 | return new Promise<string>((res, rej) => { | ||
51 | this.client.get(this.prefix + key, (err, value) => { | ||
52 | if (err) return rej(err) | ||
53 | |||
54 | return res(value) | ||
55 | }) | ||
56 | }) | ||
57 | } | ||
58 | |||
59 | private setValue (key: string, value: string, expirationMilliseconds: number) { | ||
60 | return new Promise<void>((res, rej) => { | ||
61 | this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { | ||
62 | if (err) return rej(err) | ||
63 | |||
64 | if (ok !== 'OK') return rej(new Error('Redis result is not OK.')) | ||
65 | |||
66 | return res() | ||
67 | }) | ||
68 | }) | ||
69 | } | ||
70 | |||
71 | private generateResetPasswordKey (userId: number) { | ||
72 | return 'reset-password-' + userId | ||
73 | } | ||
74 | |||
75 | static get Instance () { | ||
76 | return this.instance || (this.instance = new this()) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | // --------------------------------------------------------------------------- | ||
81 | |||
82 | export { | ||
83 | Redis | ||
84 | } | ||