diff options
Diffstat (limited to 'trunk/etherpad/src/etherpad/pro/pro_accounts.js')
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_accounts.js | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/trunk/etherpad/src/etherpad/pro/pro_accounts.js b/trunk/etherpad/src/etherpad/pro/pro_accounts.js new file mode 100644 index 0000000..2024970 --- /dev/null +++ b/trunk/etherpad/src/etherpad/pro/pro_accounts.js @@ -0,0 +1,496 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// library for pro accounts + +import("funhtml.*"); +import("sqlbase.sqlobj"); +import("sqlbase.sqlcommon.inTransaction"); +import("email.sendEmail"); +import("cache_utils.syncedWithCache"); +import("stringutils.*"); + +import("etherpad.globals.*"); +import("etherpad.sessions"); +import("etherpad.sessions.getSession"); +import("etherpad.utils.*"); +import("etherpad.pro.domains"); +import("etherpad.control.pro.account_control"); +import("etherpad.pro.pro_utils"); +import("etherpad.pro.pro_quotas"); +import("etherpad.pad.padusers"); +import("etherpad.log"); +import("etherpad.billing.team_billing"); + +jimport("org.mindrot.BCrypt"); +jimport("java.lang.System.out.println"); + +function _dmesg(m) { + if (!isProduction()) { + println(m); + } +} + +function _computePasswordHash(p) { + var pwh; + pwh = BCrypt.hashpw(p, BCrypt.gensalt(10)); + return pwh; +} + +function _withCache(name, fn) { + return syncedWithCache('pro_accounts.'+name, fn); +} + +//---------------------------------------------------------------- +// validation +//---------------------------------------------------------------- + +function validateEmail(email) { + if (!email) { return "Email is required."; } + if (!isValidEmail(email)) { return "\""+email+"\" does not look like a valid email address."; } + return null; +} + +function validateFullName(name) { + if (!name) { return "Full name is required."; } + if (name.length < 2) { return "Full name must be at least 2 characters."; } + return null; +} + +function validatePassword(p) { + if (!p) { return "Password is required."; } + if (p.length < 6) { return "Passwords must be at least 6 characters."; } + return null; +} + +function validateEmailDomainPair(email, domainId) { + // TODO: make sure the same email address cannot exist more than once within + // the same domainid. +} + +/* if domainId is null, then use domainId of current request. */ +function createNewAccount(domainId, fullName, email, password, isAdmin) { + if (!domainId) { + domainId = domains.getRequestDomainId(); + } + email = trim(email); + isAdmin = !!isAdmin; // convert to bool + + // validation + var e; + e = validateEmail(email); if (e) { throw Error(e); } + e = validateFullName(fullName); if (e) { throw Error(e); } + e = validatePassword(password); if (e) { throw Error(e); } + + // xss normalization + fullName = toHTML(fullName); + + // make sure account does not already exist on this domain. + var ret = inTransaction(function() { + var existingAccount = getAccountByEmail(email, domainId); + if (existingAccount) { + throw Error("There is already an account with that email address."); + } + // No existing account. Proceed. + var now = new Date(); + var account = { + domainId: domainId, + fullName: fullName, + email: email, + passwordHash: _computePasswordHash(password), + createdDate: now, + isAdmin: isAdmin + }; + return sqlobj.insert('pro_accounts', account); + }); + + _withCache('does-domain-admin-exist', function(cache) { + delete cache[domainId]; + }); + + pro_quotas.updateAccountUsageCount(domainId); + updateCachedActiveCount(domainId); + + if (ret) { + log.custom('pro-accounts', + {type: "account-created", + accountId: ret, + domainId: domainId, + name: fullName, + email: email, + admin: isAdmin}); + } + + return ret; +} + +function _checkAccess(account) { + if (sessions.isAnEtherpadAdmin()) { + return; + } + if (account.domainId != domains.getRequestDomainId()) { + throw Error("access denied"); + } +} + +function setPassword(account, newPass) { + _checkAccess(account); + var passHash = _computePasswordHash(newPass); + sqlobj.update('pro_accounts', {id: account.id}, {passwordHash: passHash}); + markDirtySessionAccount(account.id); +} + +function setTempPassword(account, tempPass) { + _checkAccess(account); + var tempPassHash = _computePasswordHash(tempPass); + sqlobj.update('pro_accounts', {id: account.id}, {tempPassHash: tempPassHash}); + markDirtySessionAccount(account.id); +} + +function setEmail(account, newEmail) { + _checkAccess(account); + sqlobj.update('pro_accounts', {id: account.id}, {email: newEmail}); + markDirtySessionAccount(account.id); +} + +function setFullName(account, newName) { + _checkAccess(account); + sqlobj.update('pro_accounts', {id: account.id}, {fullName: newName}); + markDirtySessionAccount(account.id); +} + +function setIsAdmin(account, newVal) { + _checkAccess(account); + sqlobj.update('pro_accounts', {id: account.id}, {isAdmin: newVal}); + markDirtySessionAccount(account.id); +} + +function setDeleted(account) { + _checkAccess(account); + if (!isNumeric(account.id)) { + throw new Error("Invalid account id: "+account.id); + } + sqlobj.update('pro_accounts', {id: account.id}, {isDeleted: true}); + markDirtySessionAccount(account.id); + pro_quotas.updateAccountUsageCount(account.domainId); + updateCachedActiveCount(account.domainId); + + log.custom('pro-accounts', + {type: "account-deleted", + accountId: account.id, + domainId: account.domainId, + name: account.fullName, + email: account.email, + admin: account.isAdmin, + createdDate: account.createdDate.getTime()}); +} + +//---------------------------------------------------------------- + +function doesAdminExist() { + var domainId = domains.getRequestDomainId(); + return _withCache('does-domain-admin-exist', function(cache) { + if (cache[domainId] === undefined) { + _dmesg("cache miss for doesAdminExist (domainId="+domainId+")"); + var admins = sqlobj.selectMulti('pro_accounts', {domainId: domainId, isAdmin: true}, {}); + cache[domainId] = (admins.length > 0); + } + return cache[domainId] + }); +} + +function getSessionProAccount() { + if (sessions.isAnEtherpadAdmin()) { + return getEtherpadAdminAccount(); + } + var account = getSession().proAccount; + if (!account) { + return null; + } + if (account.isDeleted) { + delete getSession().proAccount; + return null; + } + return account; +} + +function isAccountSignedIn() { + if (getSessionProAccount()) { + return true; + } else { + return false; + } +} + +function isAdminSignedIn() { + return isAccountSignedIn() && getSessionProAccount().isAdmin; +} + +function requireAccount(message) { + if ((request.path == "/ep/account/sign-in") || + (request.path == "/ep/account/sign-out") || + (request.path == "/ep/account/guest-sign-in") || + (request.path == "/ep/account/guest-knock") || + (request.path == "/ep/account/forgot-password")) { + return; + } + + function checkSessionAccount() { + if (!getSessionProAccount()) { + if (message) { + account_control.setSigninNotice(message); + } + response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url)); + } + } + + checkSessionAccount(); + + if (getSessionProAccount().domainId != domains.getRequestDomainId()) { + // This should theoretically never happen unless the account is spoofing cookies / trying to + // hack the site. + pro_utils.renderFramedMessage("Permission denied."); + response.stop(); + } + // update dirty session account if necessary + _withCache('dirty-session-accounts', function(cache) { + var uid = getSessionProAccount().id; + if (cache[uid]) { + reloadSessionAccountData(uid); + cache[uid] = false; + } + }); + + // need to check again in case dirty update caused account to be marked + // deleted. + checkSessionAccount(); +} + +function requireAdminAccount() { + requireAccount(); + if (!getSessionProAccount().isAdmin) { + pro_utils.renderFramedMessage("Permission denied."); + response.stop(); + } +} + +/* returns undefined on success, error string otherise. */ +function authenticateSignIn(email, password) { + var accountRecord = getAccountByEmail(email, null); + if (!accountRecord) { + return "Account not found: "+email; + } + + if (BCrypt.checkpw(password, accountRecord.passwordHash) != true) { + return "Incorrect password. Please try again."; + } + + signInSession(accountRecord); + + return undefined; // success +} + +function signOut() { + delete getSession().proAccount; +} + +function authenticateTempSignIn(uid, tempPass) { + var emsg = "That password reset link that is no longer valid."; + + var account = getAccountById(uid); + if (!account) { + return emsg+" (Account not found.)"; + } + if (account.domainId != domains.getRequestDomainId()) { + return emsg+" (Wrong domain.)"; + } + if (!account.tempPassHash) { + return emsg+" (Expired.)"; + } + if (BCrypt.checkpw(tempPass, account.tempPassHash) != true) { + return emsg+" (Bad temp pass.)"; + } + + signInSession(account); + + getSession().accountMessage = "Please choose a new password"; + getSession().changePass = true; + + response.redirect("/ep/account/"); +} + +function signInSession(account) { + account.lastLoginDate = new Date(); + account.tempPassHash = null; + sqlobj.updateSingle('pro_accounts', {id: account.id}, account); + reloadSessionAccountData(account.id); + padusers.notifySignIn(); +} + +function listAllDomainAccounts(domainId) { + if (domainId === undefined) { + domainId = domains.getRequestDomainId(); + } + var records = sqlobj.selectMulti('pro_accounts', + {domainId: domainId, isDeleted: false}, {}); + return records; +} + +function listAllDomainAdmins(domainId) { + if (domainId === undefined) { + domainId = domains.getRequestDomainId(); + } + var records = sqlobj.selectMulti('pro_accounts', + {domainId: domainId, isDeleted: false, isAdmin: true}, + {}); + return records; +} + +function getActiveCount(domainId) { + var records = sqlobj.selectMulti('pro_accounts', + {domainId: domainId, isDeleted: false}, {}); + return records.length; +} + +/* getAccountById works for deleted and non-deleted accounts. + * The assumption is that cases whewre you look up an account by ID, you + * want the account info even if the account has been deleted. For + * example, when asking who created a pad. + */ +function getAccountById(accountId) { + var r = sqlobj.selectSingle('pro_accounts', {id: accountId}); + if (r) { + return r; + } else { + return undefined; + } +} + +/* getting an account by email only returns the account if it is + * not deleted. The assumption is that when you look up an account by + * email address, you only want active accounts. Furthermore, some + * deleted accounts may match a given email, but only one non-deleted + * account should ever match a single (email,domainId) pair. + */ +function getAccountByEmail(email, domainId) { + if (!domainId) { + domainId = domains.getRequestDomainId(); + } + var r = sqlobj.selectSingle('pro_accounts', {domainId: domainId, email: email, isDeleted: false}); + if (r) { + return r; + } else { + return undefined; + } +} + +function getFullNameById(id) { + if (!id) { + return null; + } + + return _withCache('names-by-id', function(cache) { + if (cache[id] === undefined) { + _dmesg("cache miss for getFullNameById (accountId="+id+")"); + var r = getAccountById(id); + if (r) { + cache[id] = r.fullName; + } else { + cache[id] = false; + } + } + if (cache[id]) { + return cache[id]; + } else { + return null; + } + }); +} + +function getTempSigninUrl(account, tempPass) { + return [ + 'https://', httpsHost(pro_utils.getFullProHost()), '/ep/account/sign-in?', + 'uid=', account.id, '&tp=', tempPass + ].join(''); +} + + +// TODO: this session account object storage / dirty cache is a +// ridiculous hack. What we should really do is have a caching/access +// layer for accounts similar to accessPad() and accessProPadMeta(), and +// have that abstraction take care of caching and marking accounts as +// dirty. This can be incorporated into getSessionProAccount(), and we +// should actually refactor that into accessSessionProAccount(). + +/* will force session data for this account to be updated next time that + * account requests a page. */ +function markDirtySessionAccount(uid) { + var domainId = domains.getRequestDomainId(); + + _withCache('dirty-session-accounts', function(cache) { + cache[uid] = true; + }); + _withCache('names-by-id', function(cache) { + delete cache[uid]; + }); + _withCache('does-domain-admin-exist', function(cache) { + delete cache[domainId]; + }); +} + +function reloadSessionAccountData(uid) { + if (!uid) { + uid = getSessionProAccount().id; + } + getSession().proAccount = getAccountById(uid); +} + +function getAllAccountsWithEmail(email) { + var accountRecords = sqlobj.selectMulti('pro_accounts', {email: email, isDeleted: false}, {}); + return accountRecords; +} + +function getEtherpadAdminAccount() { + return { + id: 0, + isAdmin: true, + fullName: "ETHERPAD ADMIN", + email: "support@pad.spline.inf.fu-berlin.de", + domainId: domains.getRequestDomainId(), + isDeleted: false + }; +} + +function getCachedActiveCount(domainId) { + return _withCache('user-counts.'+domainId, function(c) { + if (!c.count) { + c.count = getActiveCount(domainId); + } + return c.count; + }); +} + +function updateCachedActiveCount(domainId) { + _withCache('user-counts.'+domainId, function(c) { + c.count = getActiveCount(domainId); + }); +} + + + + + + |