@@ -13,6 +13,13 @@ import {toID, updateserver, bash, time} from './utils';
13
13
import * as tables from './tables' ;
14
14
import * as pathModule from 'path' ;
15
15
import IPTools from './ip-tools' ;
16
+ import nodemailer from 'nodemailer' ;
17
+
18
+ // eslint-disable-next-line
19
+ const EMAIL_REGEX = / (?: [ a - z 0 - 9 ! # $ % & \' * + \/ = ? ^ _ ` { | } ~ - ] + (?: \. [ a - z 0 - 9 ! # $ % & \' * + \/ = ? ^ _ ` { | } ~ - ] + ) * | " (?: [ \x01 - \x08 \x0b \x0c \x0e - \x1f \x21 \x23 - \x5b \x5d - \x7f ] | \\ [ \x01 - \x09 \x0b \x0c \x0e - \x7f ] ) * " ) @ (?: (?: [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? \. ) + [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? | \[ (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \. ) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? | [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] : (?: [ \x01 - \x08 \x0b \x0c \x0e - \x1f \x21 - \x5a \x53 - \x7f ] | \\ [ \x01 - \x09 \x0b \x0c \x0e - \x7f ] ) + ) \] ) / i;
20
+
21
+ const mailer = nodemailer . createTransport ( Config . passwordemails . transportOpts ) ;
22
+
16
23
17
24
export const actions : { [ k : string ] : QueryHandler } = {
18
25
async register ( params ) {
@@ -467,6 +474,88 @@ export const actions: {[k: string]: QueryHandler} = {
467
474
matches : await tables . users . selectAll ( [ 'userid' , 'banstate' ] ) `ip = ${ res . ip } ` ,
468
475
} ;
469
476
} ,
477
+ async setemail ( params ) {
478
+ if ( ! this . user . loggedIn ) {
479
+ throw new ActionError ( `You must be logged in to set an email.` ) ;
480
+ }
481
+ if ( ! params . email || typeof params . email !== 'string' ) {
482
+ throw new ActionError ( `You must send an email.` ) ;
483
+ }
484
+ const email = EMAIL_REGEX . exec ( params . email ) ?. [ 0 ] ;
485
+ if ( ! email ) throw new ActionError ( `Invalid email sent.` ) ;
486
+ const data = await tables . users . get ( this . user . id ) ;
487
+ if ( ! data ) throw new ActionError ( `You are not registered.` ) ;
488
+ if ( data . email ?. endsWith ( '@' ) ) {
489
+ throw new ActionError ( `You have 2FA, and do not need to set an email for password resets.` ) ;
490
+ }
491
+ const result = await tables . users . update ( this . user . id , { email} ) ;
492
+
493
+ delete ( data as any ) . passwordhash ;
494
+ return {
495
+ success : ! ! result . changedRows ,
496
+ curuser : Object . assign ( data , { email} ) ,
497
+ } ;
498
+ } ,
499
+ async clearemail ( ) {
500
+ if ( ! this . user . loggedIn ) {
501
+ throw new ActionError ( `You must be logged in to edit your email.` ) ;
502
+ }
503
+ const data = await tables . users . get ( this . user . id ) ;
504
+ if ( ! data ) throw new ActionError ( `You are not registered.` ) ;
505
+ if ( data . email ?. endsWith ( '@' ) ) {
506
+ throw new ActionError (
507
+ `You have 2FA, and need an administrator to set/unset your email manually.`
508
+ ) ;
509
+ }
510
+ const result = await tables . users . update ( this . user . id , { email : null } ) ;
511
+
512
+ delete ( data as any ) . passwordhash ;
513
+ return {
514
+ actionsuccess : ! ! result . changedRows ,
515
+ curuser : Object . assign ( data , { email : null } ) ,
516
+ } ;
517
+ } ,
518
+ async resetpassword ( params ) {
519
+ if ( typeof params . email !== 'string' || ! params . email ) {
520
+ throw new ActionError ( `You must provide an email address.` ) ;
521
+ }
522
+ const email = EMAIL_REGEX . exec ( params . email ) ?. [ 0 ] ;
523
+ if ( ! email ) {
524
+ throw new ActionError ( `Invalid email sent.` ) ;
525
+ }
526
+ const data = await tables . users . selectOne ( ) `email = ${ email } ` ;
527
+ if ( ! data ) {
528
+ // no user associated with that email.
529
+ // ...pretend like it succeeded (we don't wanna leak that it's in use, after all)
530
+ return { success : true } ;
531
+ }
532
+ if ( ! data . email ) {
533
+ // should literally never happen
534
+ throw new Error ( `Account data found with no email, but had an email match` ) ;
535
+ }
536
+ if ( data . email . endsWith ( '@' ) ) {
537
+ throw new ActionError ( `You have 2FA, and so do not need a password reset.` ) ;
538
+ }
539
+ const token = await this . session . createPasswordResetToken ( data . username ) ;
540
+
541
+ await mailer . sendMail ( {
542
+ from : Config . passwordemails . from ,
543
+ to : data . email ,
544
+ subject : "Pokemon Showdown account password reset" ,
545
+ text : (
546
+ `You requested a password reset for the Pokemon Showdown account ${ data . userid } . Click this link https://${ Config . routes . root } /resetpassword/${ token } and follow the instructions to change your password.\n` +
547
+ `Not you? Please contact staff by typing /ht in any chatroom on Pokemon Showdown. \n` +
548
+ `If you are unable to do so, visit the Help chatroom.`
549
+ ) ,
550
+ html : (
551
+ `You requested a password reset for the Pokemon Showdown account ${ data . userid } . ` +
552
+ `Click <a href="https://${ Config . routes . root } /resetpassword/${ token } ">this link</a> and follow the instructions to change your password.<br />` +
553
+ `Not you? Please contact staff by typing <code>/ht</code> in any chatroom on Pokemon Showdown. <br />` +
554
+ `If you are unable to do so, visit the <a href="${ Config . routes . client } /help">Help</a> chatroom.`
555
+ ) ,
556
+ } ) ;
557
+ return { success : true } ;
558
+ } ,
470
559
} ;
471
560
472
561
if ( Config . actions ) {
0 commit comments