2 * web/account.js - Webserver details for account management
4 * @author Calvin Montgomery <cyzon@cyzon.us>
7 var webserver
= require("./webserver");
8 var sendPug
= require("./pug").sendPug
;
9 var Logger
= require("../logger");
10 var db
= require("../database");
11 var $util
= require("../utilities");
12 var Config
= require("../config");
13 var session
= require("../session");
14 var csrf
= require("./csrf");
15 const url
= require("url");
16 import crypto
from 'crypto';
18 const LOGGER
= require('@calzoneman/jsli')('web/accounts');
25 * Handles a GET request for /account/edit
27 function handleAccountEditPage(req
, res
) {
28 sendPug(res
, "account-edit", {});
31 function verifyReferrer(req
, expected
) {
32 const referrer
= req
.header('referer');
39 const parsed
= url
.parse(referrer
);
41 if (parsed
.pathname
!== expected
) {
43 'Possible attempted forgery: %s POSTed to %s',
57 * Handles a POST request to edit a user"s account
59 function handleAccountEdit(req
, res
) {
62 if (!verifyReferrer(req
, '/account/edit')) {
63 res
.status(403).send('Mismatched referrer');
67 var action
= req
.body
.action
;
69 case "change_password":
70 handleChangePassword(req
, res
);
73 handleChangeEmail(req
, res
);
82 * Handles a request to change the user"s password
84 async
function handleChangePassword(req
, res
) {
85 var name
= req
.body
.name
;
86 var oldpassword
= req
.body
.oldpassword
;
87 var newpassword
= req
.body
.newpassword
;
89 if (typeof name
!== "string" ||
90 typeof oldpassword
!== "string" ||
91 typeof newpassword
!== "string") {
96 if (newpassword
.length
=== 0) {
97 sendPug(res
, "account-edit", {
98 errorMessage
: "New password must not be empty"
103 const reqUser
= await webserver
.authorize(req
);
105 sendPug(res
, "account-edit", {
106 errorMessage
: "You must be logged in to change your password"
111 newpassword
= newpassword
.substring(0, 100);
113 db
.users
.verifyLogin(name
, oldpassword
, function (err
, _user
) {
115 sendPug(res
, "account-edit", {
121 db
.users
.setPassword(name
, newpassword
, function (err
, _dbres
) {
123 sendPug(res
, "account-edit", {
129 Logger
.eventlog
.log("[account] " + req
.realIP
+
130 " changed password for " + name
);
132 db
.users
.getUser(name
, function (err
, user
) {
134 return sendPug(res
, "account-edit", {
139 var expiration
= new Date(parseInt(req
.signedCookies
.auth
.split(":")[1]));
140 session
.genSession(user
, expiration
, function (err
, auth
) {
142 return sendPug(res
, "account-edit", {
147 webserver
.setAuthCookie(req
, res
, expiration
, auth
);
149 sendPug(res
, "account-edit", {
150 successMessage
: "Password changed."
159 * Handles a request to change the user"s email
161 function handleChangeEmail(req
, res
) {
162 var name
= req
.body
.name
;
163 var password
= req
.body
.password
;
164 var email
= req
.body
.email
;
166 if (typeof name
!== "string" ||
167 typeof password
!== "string" ||
168 typeof email
!== "string") {
173 if (!$util
.isValidEmail(email
) && email
!== "") {
174 sendPug(res
, "account-edit", {
175 errorMessage
: "Invalid email address"
180 db
.users
.verifyLogin(name
, password
, function (err
, _user
) {
182 sendPug(res
, "account-edit", {
188 db
.users
.setEmail(name
, email
, function (err
, _dbres
) {
190 sendPug(res
, "account-edit", {
195 Logger
.eventlog
.log("[account] " + req
.realIP
+
196 " changed email for " + name
+
198 sendPug(res
, "account-edit", {
199 successMessage
: "Email address changed."
206 * Handles a GET request for /account/channels
208 async
function handleAccountChannelPage(req
, res
) {
209 const user
= await webserver
.authorize(req
);
210 // TODO: error message
212 return sendPug(res
, "account-channels", {
217 db
.channels
.listUserChannels(user
.name
, function (err
, channels
) {
218 sendPug(res
, "account-channels", {
225 * Handles a POST request to modify a user"s channels
227 function handleAccountChannel(req
, res
) {
230 if (!verifyReferrer(req
, '/account/channels')) {
231 res
.status(403).send('Mismatched referrer');
235 var action
= req
.body
.action
;
238 handleNewChannel(req
, res
);
240 case "delete_channel":
241 handleDeleteChannel(req
, res
);
250 * Handles a request to register a new channel
252 async
function handleNewChannel(req
, res
) {
254 var name
= req
.body
.name
;
255 if (typeof name
!== "string") {
260 const user
= await webserver
.authorize(req
);
261 // TODO: error message
263 return sendPug(res
, "account-channels", {
268 db
.channels
.listUserChannels(user
.name
, function (err
, channels
) {
270 sendPug(res
, "account-channels", {
277 if (name
.match(Config
.get("reserved-names.channels"))) {
278 sendPug(res
, "account-channels", {
280 newChannelError
: "That channel name is reserved"
285 if (channels
.length
>= Config
.get("max-channels-per-user")
286 && user
.global_rank
< 255) {
287 sendPug(res
, "account-channels", {
289 newChannelError
: "You are not allowed to register more than " +
290 Config
.get("max-channels-per-user") + " channels."
295 db
.channels
.register(name
, user
.name
, function (err
, _channel
) {
297 Logger
.eventlog
.log("[channel] " + user
.name
+ "@" +
299 " registered channel " + name
);
300 globalMessageBus
.emit('ChannelRegistered', {
310 sendPug(res
, "account-channels", {
312 newChannelError
: err
? err
: undefined
319 * Handles a request to delete a new channel
321 async
function handleDeleteChannel(req
, res
) {
322 var name
= req
.body
.name
;
323 if (typeof name
!== "string") {
328 const user
= await webserver
.authorize(req
);
331 return sendPug(res
, "account-channels", {
337 db
.channels
.lookup(name
, function (err
, channel
) {
339 sendPug(res
, "account-channels", {
341 deleteChannelError
: err
346 if ((!channel
.owner
|| channel
.owner
.toLowerCase() !== user
.name
.toLowerCase()) && user
.global_rank
< 255) {
347 db
.channels
.listUserChannels(user
.name
, function (err2
, channels
) {
348 sendPug(res
, "account-channels", {
349 channels
: err2
? [] : channels
,
350 deleteChannelError
: "You do not have permission to delete this channel"
356 db
.channels
.drop(name
, function (err
) {
358 Logger
.eventlog
.log("[channel] " + user
.name
+ "@" +
359 req
.realIP
+ " deleted channel " +
363 globalMessageBus
.emit('ChannelDeleted', {
367 db
.channels
.listUserChannels(user
.name
, function (err2
, channels
) {
368 sendPug(res
, "account-channels", {
369 channels
: err2
? [] : channels
,
370 deleteChannelError
: err
? err
: undefined
378 * Handles a GET request for /account/profile
380 async
function handleAccountProfilePage(req
, res
) {
381 const user
= await webserver
.authorize(req
);
382 // TODO: error message
384 return sendPug(res
, "account-profile", {
390 db
.users
.getProfile(user
.name
, function (err
, profile
) {
392 sendPug(res
, "account-profile", {
400 sendPug(res
, "account-profile", {
401 profileImage
: profile
.image
,
402 profileText
: profile
.text
,
408 function validateProfileImage(image
, callback
) {
409 var prefix
= "Invalid URL for profile image: ";
410 var link
= image
.trim();
412 process
.nextTick(callback
, null, link
);
414 var data
= url
.parse(link
);
415 if (!data
.protocol
|| data
.protocol
!== 'https:') {
416 process
.nextTick(callback
,
417 new Error(prefix
+ " URL must begin with 'https://'"));
418 } else if (!data
.host
) {
419 process
.nextTick(callback
,
420 new Error(prefix
+ "missing hostname"));
422 process
.nextTick(callback
, null, link
);
428 * Handles a POST request to edit a profile
430 async
function handleAccountProfile(req
, res
) {
433 if (!verifyReferrer(req
, '/account/profile')) {
434 res
.status(403).send('Mismatched referrer');
438 const user
= await webserver
.authorize(req
);
439 // TODO: error message
441 return sendPug(res
, "account-profile", {
444 profileError
: "You must be logged in to edit your profile",
448 var rawImage
= String(req
.body
.image
).substring(0, 255);
449 var text
= String(req
.body
.text
).substring(0, 255);
451 validateProfileImage(rawImage
, (error
, image
) => {
453 db
.users
.getProfile(user
.name
, function (err
, profile
) {
454 var errorMessage
= err
|| error
.message
;
455 sendPug(res
, "account-profile", {
456 profileImage
: profile
? profile
.image
: "",
457 profileText
: profile
? profile
.text
: "",
458 profileError
: errorMessage
464 db
.users
.setProfile(user
.name
, { image
: image
, text
: text
}, function (err
) {
466 sendPug(res
, "account-profile", {
474 globalMessageBus
.emit('UserProfileChanged', {
482 sendPug(res
, "account-profile", {
492 * Handles a GET request for /account/passwordreset
494 function handlePasswordResetPage(req
, res
) {
495 sendPug(res
, "account-passwordreset", {
503 * Handles a POST request to reset a user's password
505 function handlePasswordReset(req
, res
) {
508 if (!verifyReferrer(req
, '/account/passwordreset')) {
509 res
.status(403).send('Mismatched referrer');
513 var name
= req
.body
.name
,
514 email
= req
.body
.email
;
516 if (typeof name
!== "string" || typeof email
!== "string") {
521 if (!$util
.isValidUserName(name
)) {
522 sendPug(res
, "account-passwordreset", {
525 resetErr
: "Invalid username '" + name
+ "'"
530 db
.users
.getEmail(name
, function (err
, actualEmail
) {
532 sendPug(res
, "account-passwordreset", {
540 if (actualEmail
=== '') {
541 sendPug(res
, "account-passwordreset", {
544 resetErr
: `Username ${name} cannot be recovered because it ` +
545 "doesn't have an email address associated with it."
548 } else if (actualEmail
.toLowerCase() !== email
.trim().toLowerCase()) {
549 sendPug(res
, "account-passwordreset", {
552 resetErr
: "Provided email does not match the email address on record for " + name
557 crypto
.randomBytes(20, (err
, bytes
) => {
560 'Could not generate random bytes for password reset: %s',
563 sendPug(res
, "account-passwordreset", {
566 resetErr
: "Internal error when generating password reset"
571 var hash
= bytes
.toString('hex');
572 // 24-hour expiration
573 var expire
= Date
.now() + 86400000;
576 db
.addPasswordReset({
582 }, function (err
, _dbres
) {
584 sendPug(res
, "account-passwordreset", {
592 Logger
.eventlog
.log("[account] " + ip
+ " requested password recovery for " +
593 name
+ " <" + email
+ ">");
595 if (!emailConfig
.getPasswordReset().isEnabled()) {
596 sendPug(res
, "account-passwordreset", {
599 resetErr
: "This server does not have mail support enabled. Please " +
600 "contact an administrator for assistance."
605 const baseUrl
= `${req.realProtocol}://${req.header("host")}`;
607 emailController
.sendPasswordReset({
610 url
: `${baseUrl}/account/passwordrecover/${hash}`
612 sendPug(res
, "account-passwordreset", {
618 LOGGER
.error("Sending password reset email failed: %s", error
);
619 sendPug(res
, "account-passwordreset", {
622 resetErr
: "Sending reset email failed. Please contact an " +
623 "administrator for assistance."
632 * Handles a request for /account/passwordrecover/<hash>
634 function handleGetPasswordRecover(req
, res
) {
635 var hash
= req
.params
.hash
;
636 if (typeof hash
!== "string") {
641 db
.lookupPasswordReset(hash
, function (err
, row
) {
643 sendPug(res
, "account-passwordrecover", {
650 if (Date
.now() >= row
.expire
) {
651 sendPug(res
, "account-passwordrecover", {
653 recoverErr
: "This password recovery link has expired. Password " +
654 "recovery links are valid only for 24 hours after " +
660 sendPug(res
, "account-passwordrecover", {
668 * Handles a POST request for /account/passwordrecover/<hash>
670 function handlePostPasswordRecover(req
, res
) {
671 var hash
= req
.params
.hash
;
672 if (typeof hash
!== "string") {
679 db
.lookupPasswordReset(hash
, function (err
, row
) {
681 sendPug(res
, "account-passwordrecover", {
688 if (Date
.now() >= row
.expire
) {
689 sendPug(res
, "account-passwordrecover", {
691 recoverErr
: "This password recovery link has expired. Password " +
692 "recovery links are valid only for 24 hours after " +
699 const avail
= "abcdefgihkmnpqrstuvwxyz0123456789";
700 for (var i
= 0; i
< 10; i
++) {
701 newpw
+= avail
[Math
.floor(Math
.random() * avail
.length
)];
703 db
.users
.setPassword(row
.name
, newpw
, function (err
) {
705 sendPug(res
, "account-passwordrecover", {
707 recoverErr
: "Database error. Please contact an administrator if " +
714 db
.deletePasswordReset(hash
);
715 Logger
.eventlog
.log("[account] " + ip
+ " recovered password for " + row
.name
);
717 sendPug(res
, "account-passwordrecover", {
727 * Initialize the module
729 init: function (app
, _globalMessageBus
, _emailConfig
, _emailController
) {
730 globalMessageBus
= _globalMessageBus
;
731 emailConfig
= _emailConfig
;
732 emailController
= _emailController
;
734 app
.get("/account/edit", handleAccountEditPage
);
735 app
.post("/account/edit", handleAccountEdit
);
736 app
.get("/account/channels", handleAccountChannelPage
);
737 app
.post("/account/channels", handleAccountChannel
);
738 app
.get("/account/profile", handleAccountProfilePage
);
739 app
.post("/account/profile", handleAccountProfile
);
740 app
.get("/account/passwordreset", handlePasswordResetPage
);
741 app
.post("/account/passwordreset", handlePasswordReset
);
742 app
.get("/account/passwordrecover/:hash", handleGetPasswordRecover
);
743 app
.post("/account/passwordrecover/:hash", handlePostPasswordRecover
);
744 app
.get("/account", function (req
, res
) {
745 res
.redirect("/login");