diff --git a/coffeelint.json b/coffeelint.json index 23c2950..7309b68 100644 --- a/coffeelint.json +++ b/coffeelint.json @@ -38,10 +38,6 @@ "level": "error", "limitComments": true }, - "missing_fat_arrows": { - "name": "missing_fat_arrows", - "level": "warn" - }, "newlines_after_classes": { "name": "newlines_after_classes", "value": 1, diff --git a/core/billing-manager.coffee b/core/billing-manager.coffee index e223808..7b64f28 100644 --- a/core/billing-manager.coffee +++ b/core/billing-manager.coffee @@ -64,7 +64,7 @@ class BillingPlan setupDefaultComponents: (account) -> Q.all _.values(@components).map ({type, defaults}) -> Q.all defaults.map (defaultOptions) -> - rp.components.byName(type).createComponent account, defaultOptions(account) + root.components.byName(type).createComponent account, defaultOptions(account) triggerTimeBilling: (account) -> unless @billing.time diff --git a/core/i18n-manager.coffee b/core/i18n-manager.coffee index e2556ac..bdf5aa4 100644 --- a/core/i18n-manager.coffee +++ b/core/i18n-manager.coffee @@ -79,8 +79,8 @@ module.exports = class I18nManager Return {Object}. ### - packClientLocale: (language) -> - return @packLocale @alternativeLanguages language + packTranslations: (language) -> + return @packTranslationsByLanguages @alternativeLanguages language ### Public: Get hash of packaged translations by language or request. @@ -89,8 +89,8 @@ module.exports = class I18nManager Return {Object}. ### - localeHash: (language) -> - utils.sha256 jsonStableStringify @pickClientLocale language + translationsHash: (language) -> + utils.sha256 jsonStableStringify @packTranslations language ### Public: Translate name by languages. @@ -135,9 +135,11 @@ module.exports = class I18nManager * `languages` {Array} of {String} + TODO: Cache. + Return {Object}. ### - packLocale: (languages) -> + packTranslationsByLanguages: (languages) -> result = {} for language in languages diff --git a/core/middleware.coffee b/core/middleware.coffee index 0ee35d5..b78fc3c 100644 --- a/core/middleware.coffee +++ b/core/middleware.coffee @@ -1,11 +1,14 @@ expressBunyanLogger = require 'express-bunyan-logger' expressSession = require 'express-session' redisStore = require 'connect-redis' +moment = require 'moment-timezone' +crypto = require 'crypto' csrf = require 'csrf' +path = require 'path' +fs = require 'fs' +_ = require 'lodash' -{config} = app -{_, path, fs, moment, crypto} = app.libs -{Account, SecurityLog} = app.models +{Account, SecurityLog, config} = root exports.reqHelpers = (req, res, next) -> req.getCsrfToken = -> @@ -36,10 +39,10 @@ exports.reqHelpers = (req, res, next) -> return req.cookies?.timezone ? config.i18n.default_timezone req.getTranslator = -> - return rp.translatorByReq req + return root.i18n.translator req req.getMoment = -> - return moment.apply(@, arguments).locale(req.getLanguage()).tz(req.getTimezone()) + return moment.apply(arguments...).locale(req.getLanguage()).tz(req.getTimezone()) req.createSecurityLog = (type, options, {account, token} = {}) -> SecurityLog.createLog (account ? req.account), @@ -87,6 +90,7 @@ exports.renderHelpers = (req, res, next) -> next() exports.logger = -> + # TODO: refactor return expressBunyanLogger genReqId: (req) -> req.sessionID parseUA: false @@ -122,7 +126,7 @@ exports.csrf = -> if req.path in app.getHooks('app.ignore_csrf', null, pluck: 'path') return next() - if req.method in ['GET', 'HEAD', 'OPTIONS'] + if req.method in ['HEAD', 'OPTIONS'] return next() unless provider.verify req.session.csrf_secret, req.getCsrfToken() @@ -145,17 +149,15 @@ exports.authenticate = (req, res, next) -> _.extend req, token: token account: account - .finally next exports.requireAuthenticate = (req, res, next) -> if req.account next() + else if req.method == 'GET' + res.redirect '/account/login/' else - if req.method == 'GET' - res.redirect '/account/login/' - else - res.error 403, 'auth_failed' + res.error 403, 'auth_failed' exports.requireAdminAuthenticate = (req, res, next) -> if req.account?.isAdmin() diff --git a/core/model/coupon-code.coffee b/core/model/coupon-code.coffee index 35d0699..ad158c4 100644 --- a/core/model/coupon-code.coffee +++ b/core/model/coupon-code.coffee @@ -53,6 +53,14 @@ CouponCode = mabolo.model 'CouponCode', # Public: Apply log of coupon apply_log: [ApplyLog] +CouponCode.ensureIndex + code: 1 +, + unique: true + +CouponCode.findByCode = (code, options...) -> + @findOne code: code, options... + ### Public: Create coupons. @@ -76,6 +84,9 @@ CouponCode.createCoupons = ({type, options, expired_at, available_times}, count) expired_at: expired_at available_times: available_times +CouponCode::pick = -> + return _.omit @, 'apply_log' + ### Public: Check availability for specified account. @@ -84,10 +95,13 @@ CouponCode.createCoupons = ({type, options, expired_at, available_times}, count) Return {Promise} resolve with {Boolean}. ### CouponCode::validate = (account) -> - if @available_times <= 0 - return Q false - @populate().then -> + if @available_times != undefined and @available_times <= 0 + return false + + if @expired and new Date() > @expired + return false + @provider.validate account, @ ### @@ -98,18 +112,19 @@ CouponCode::validate = (account) -> Return {Promise}. ### CouponCode::apply = (account) -> - if @available_times <= 0 - throw new Error 'coupon_unavailable' - - @populate().then -> - @provider.apply(account, @).then => - @update - $inc: - available_times: -1 - $push: - apply_log: - account_id: account._id - created_at: new Date() + @validate(account).then (available) -> + unless available + throw new Error 'coupon_unavailable' + .then => + @provider.apply account, @ + .then => + @update + $inc: + available_times: -1 + $push: + apply_log: + account_id: account._id + created_at: new Date() ### Public: Populate. diff --git a/core/registry/view.coffee b/core/registry/view.coffee index d45e5ac..32a2656 100644 --- a/core/registry/view.coffee +++ b/core/registry/view.coffee @@ -58,9 +58,9 @@ module.exports = class ViewRegistry async.detect([ view - rp.resolve view - rp.resolve 'core', view - rp.resolve 'core/view', view + root.resolve view + root.resolve 'core', view + root.resolve 'core/view', view ], fs.exists).then (filename) -> fs.read(filename).then (source) -> return jade.compile extendSource(source), diff --git a/core/router/account.coffee b/core/router/account.coffee index 6cf2673..ba65a0c 100644 --- a/core/router/account.coffee +++ b/core/router/account.coffee @@ -1,38 +1,69 @@ -{_, express} = app.libs -{requireAuthenticate} = app.middleware -{Account, SecurityLog, CouponCode} = app.models -{config, utils, logger, i18n} = app +express = require 'express' +_ = require 'lodash' -module.exports = exports = express.Router() +utils = require '../utils' -exports.get '/register', (req, res) -> +{i18n, Account, CouponCode} = root +{requireAuthenticate} = root.middleware + +module.router = router = express.Router() + +### + Router: GET /account/register + + Response HTML. +### +router.get '/register', (req, res) -> res.render 'account/register' -exports.get '/login', (req, res) -> +### + Router: GET /account/login + + Response HTML. +### +router.get '/login', (req, res) -> res.render 'account/login' -exports.get '/locale/:language?', (req, res) -> - if req.params['language'] - req.cookies['language'] = req.params['language'] +### + Router: GET /account/translations/:language? - res.json i18n.pickClientLocale i18n.getLanguagesByReq req + Response {Object}. +### +router.get '/translations/:language?', (req, res) -> + if req.params.language + res.json i18n.packTranslations req.params.language + else + res.json i18n.packTranslations req -exports.get '/preferences', requireAuthenticate, (req, res) -> +### + Router: GET /account/preferences/edit + + Response HTML. +### +router.get '/preferences/edit', requireAuthenticate, (req, res) -> res.render 'account/preferences' -exports.get '/session_info/', (req, res) -> - response = - csrf_token: req.session.csrf_token +### + Router: GET /account/self - if req.account - _.extend response, - account_id: req.account._id - username: req.account.username - preferences: req.account.preferences + Response {Account} from {Account::pick} +### +router.get '/self', requireAuthenticate, (req, res) -> + res.json req.account.pick 'self' - res.json response +### + Router: POST /account/register -exports.post '/register', (req, res) -> + Request {Object} + + * `username` {String} + * `email` {String} + * `password` {String} + + Response {Token}. + Set-Cookie: token. +### +router.post '/register', (req, res) -> Account.register(req.body).then (account) -> res.createToken account .catch (err) -> @@ -43,7 +74,18 @@ exports.post '/register', (req, res) -> else res.error err -exports.post '/login', (req, res) -> +### + Router: POST /account/login + + Request {Object} + + * `username` {String} Username, email or account_id. + * `password` {String} + + Response {Token}. + Set-Cookie: token, language. +### +router.post '/login', (req, res) -> Account.search(req.body.username).then (account) -> unless account?.matchPassword req.body.password throw new Error 'wrong_password' @@ -61,14 +103,28 @@ exports.post '/login', (req, res) -> else res.error err -exports.post '/logout', requireAuthenticate, (req, res) -> - req.token.revoke().then -> +### + Router: POST /account/logout + + Set-Cookie: token. +### +router.post '/logout', requireAuthenticate, (req, res) -> + req.token.revoke().done -> req.createSecurityLog('revoke_token').then -> res.clearCookie('token').sendStatus 204 - .catch res.error + , res.error -exports.post '/update_password', requireAuthenticate, (req, res) -> - Q().then -> +### + Router: PUT /account/password + + Request {Object} + + * `original_password` {String} + * `password` {String} + +### +router.put '/password', requireAuthenticate, (req, res) -> + Q().done -> unless req.account.matchPassword req.body.original_password throw new Error 'wrong_password' @@ -77,10 +133,20 @@ exports.post '/update_password', requireAuthenticate, (req, res) -> req.account.setPassword(req.body.password).then -> req.createSecurityLog 'update_password' + res.sendStatus 204 - .catch res.error + , res.error -exports.post '/update_email', requireAuthenticate, (req, res) -> +### + Router: PUT /email + + Request {Object} + + * `email` {String} + * `password` {String} + +### +router.post '/update_email', requireAuthenticate, (req, res) -> Q().done -> unless req.account.matchPassword req.body.password throw new Error 'wrong_password' @@ -95,46 +161,47 @@ exports.post '/update_email', requireAuthenticate, (req, res) -> original_email: original_email current_email: req.account.email + res.sendStatus 204 + , req.error -exports.post '/update_preferences', requireAuthenticate, (req, res) -> +### + Router: PATCH /account/preferences -exports.use do -> - router = new express.Router() + Request {Preferences}. - router.use requireAuthenticate + Response {Preferences}. +### +router.patch '/preferences', requireAuthenticate, (req, res) -> + req.account.updatePreferences(req.body).done (preferences) -> + res.json preferences + , res.error - router.get '/info', (req, res) -> - CouponCode.findOne - code: req.query.code - , (err, coupon) -> - unless coupon - return res.error 'code_not_exist' +router.use '/coupons', do (router = express.Router()) -> + ### + Router: GET /account/coupons/:code - coupon.validateCode req.account, (is_available) -> - unless is_available - return res.error 'code_not_available' + Response {CouponCode}. + ### + router.get '/:code', (req, res) -> + CouponCode.findByCode(req.params.code).done (coupon) -> + if coupon + coupon.populate(req: req).then -> + coupon.validate(req.account).then (available) -> + res.json _.extend coupon.pick(), + available: available + else + throw new Error 'coupon_not_found' + , res.error - coupon.getMessage req, (message) -> - res.json - message: message - - router.post '/apply', (req, res) -> - CouponCode.findOne - code: req.body.code - , (err, coupon) -> - unless coupon - return res.error 'code_not_exist' - - if coupon.expired and Date.now() > coupon.expired.getTime() - return res.error 'code_expired' - - if coupon.available_times and coupon.available_times < 0 - return res.error 'code_not_available' - - coupon.validateCode req.account, (is_available) -> - unless is_available - return res.error 'code_not_available' - - coupon.applyCode req.account, -> - res.json {} + ### + Router: POST /account/coupons/:code/apply + ### + router.post '/:code/apply', requireAuthenticate, (req, res) -> + CouponCode.findByCode(req.params.code).done (coupon) -> + if coupon + coupon.apply(account).then -> + res.sendStatus 204 + else + throw new Error 'coupon_not_found' + , res.error