refactor with promise

This commit is contained in:
jysperm
2015-03-30 00:13:46 +08:00
parent 2f9386f033
commit 2d303e1e3f
38 changed files with 606 additions and 574 deletions

View File

@@ -6,6 +6,7 @@ global.app = module.exports = new EventEmitter()
app.libs =
_: require 'underscore'
Q: require 'q'
fs: require 'fs'
path: require 'path'
jade: require 'jade'
@@ -29,10 +30,11 @@ harp = require 'harp'
{_, fs, path, express} = app.libs
if fs.existsSync "#{__dirname}/config.coffee"
config = require './config'
else
config = require './sample/core.config.coffee'
unless global.config
if fs.existsSync "#{__dirname}/config.coffee"
config = require './config'
else
config = require './sample/core.config.coffee'
app.package = require './package'
utils = require './core/utils'
@@ -60,7 +62,9 @@ mabolo = new Mabolo utils.mongodbUri _.extend config.mongodb,
bunyanMongo = new BunyanMongo()
mabolo.on 'connected', bunyanMongo.setDB.bind bunyanMongo
mabolo.connect().then (db) ->
bunyanMongo.setDB db
.catch console.error
logger = bunyan.createLogger
name: app.package.name
@@ -102,21 +106,19 @@ require './core/model/Ticket'
require './core/model/Component'
app.extends = require './core/extends'
app.templates = require './core/templates'
app.clusters = require './core/clusters'
app.billing = require './core/billing'
app.middleware = require './core/middleware'
app.notification = require './core/notification'
app.applyHooks = ->
app.extends.hook.applyHooks.apply null, arguments
app.getHooks = app.extends.hook.getHooks
app.applyHooks = app.extends.hook.applyHooks
app.express.use bodyParser.json()
app.express.use cookieParser()
app.express.use app.middleware.reqHelpers
app.express.use app.middleware.session()
app.express.use app.middleware.logger()
app.express.use app.middleware.errorHandling
app.express.use app.middleware.csrf()
app.express.use app.middleware.authenticate
app.express.use app.middleware.accountHelpers
@@ -126,9 +128,7 @@ app.express.set 'view engine', 'jade'
app.express.use '/component', require './core/router/component'
app.express.use '/account', require './core/router/account'
app.express.use '/billing', require './core/router/billing'
app.express.use '/ticket', require './core/router/ticket'
app.express.use '/coupon', require './core/router/coupon'
app.express.use '/admin', require './core/router/admin'
app.express.use '/panel', require './core/router/panel'
@@ -149,9 +149,6 @@ app.express.get '/', (req, res) ->
exports.start = _.once ->
app.express.listen config.web.listen, ->
if fs.existsSync config.web.listen
fs.chmodSync config.web.listen, 0o770
app.started = true
app.logger.info "RootPanel start at #{config.web.listen}"
app.emit 'app.started'

View File

@@ -5,8 +5,7 @@
process.nextTick ->
{Account, Financials, Component} = app.models
billing = _.extend exports,
plans: {}
billing = exports
{available_plugins} = config.extends
@@ -132,7 +131,7 @@ billing.createPlan = (name, options) ->
billing.triggerBilling = (account, callback) ->
async.each account.plans, (plan, callback) ->
billing.plans[plan].triggerBilling account, callback
app.plans[plan].triggerBilling account, callback
, (err) ->
return callback err if err
@@ -142,15 +141,15 @@ billing.triggerBilling = (account, callback) ->
callback err, account
billing.acceptUsagesBilling = (account, trigger_name, volume, callback) ->
plan_names = _.filter billing.plans, (plan) ->
plan_names = _.filter app.plans, (plan) ->
return plan.billing_trigger[trigger_name] and account.inPlan plan
async.each plan_names, (plan_name, callback) ->
billing.plans[plan_name].acceptUsagesBilling account, trigger_name, volume, callback
app.plans[plan_name].acceptUsagesBilling account, trigger_name, volume, callback
, callback
billing.runTimeBilling = (callback = -> ) ->
plan_names = _.filter billing.plans, (plan) ->
plan_names = _.filter app.plans, (plan) ->
return plan.billing_trigger.time
Account.find
@@ -192,7 +191,7 @@ billing.isForceFreeze = (account) ->
return false
billing.joinPlan = (account, plan_name, callback) ->
plan = billing.plans[plan_name]
plan = app.plans[plan_name]
modifier =
$set: {}
@@ -222,12 +221,10 @@ billing.joinPlan = (account, plan_name, callback) ->
, callback
, (err) ->
console.log 'async.each', arguments
callback(err)
, callback
billing.leavePlan = (account, plan_name, callback) ->
plan = billing.plans[plan_name]
plan = app.plans[plan_name]
modifier =
$unset: {}

View File

@@ -1,9 +1,9 @@
stringify = require 'json-stable-stringify'
getParameterNames = require 'get-parameter-names'
CounterCache = require 'counter-cache'
_ = require 'underscore'
{redis, config} = app
{_} = app.libs
exports.counter = new CounterCache()

View File

@@ -1,4 +1,4 @@
{_, async} = app.libs
{_, async, Q} = app.libs
app.hooks =
app:
@@ -77,90 +77,51 @@ exports.selectHookPath = (name, options) ->
return ref[last]
exports.applyHooks = (name, account, options = {}) ->
{execute, pluck, req} = options
exports.getHooks = (name, account, {execute, pluck, req} = {}) ->
return _.compact _.flatten exports.selectHookPath(name).map (hook) ->
{component, timing} = hook
result = []
for hook in exports.selectHookPath(name)
template = hook.component
timing = hook.timing
pushResult = (hook, payload = {}) ->
result = (params) ->
if execute
result.push (callback) ->
params = []
{account, node, components, component} = payload
params.push account if account
params.push node if node
params.push components if components
params.push component if component
params.push callback
hook[execute].apply
req: req
template: template
plugin: hook.plugin
, params
return hook[execute].apply
req: req
component: component
plugin: hook.plugin
, params...
else if pluck
result.push _.extend({}, hook, payload)[pluck]
return _.extend({}, hook, params)[pluck]
else
result.push _.extend {}, hook, payload
return _.extend {}, hook, params
if !template or timing == 'always'
pushResult hook
continue
if !component or timing == 'always'
return result()
unless account
continue
return
if timing == 'available'
if template in account.getAvailableComponentsTemplates()
pushResult hook,
account: account
continue
if component in account.getAvailableComponentsTemplates()
return result account
components = _.filter account.components, (component) ->
return component.template == template.name
if timing == 'once'
unless _.isEmpty components
pushResult hook,
account: account
components: components
continue
return result account, components
if timing == 'every'
for component in components
pushResult hook,
account: account
component: component
continue
return components.map (component) ->
return result account, component
if timing == 'every_node'
components_by_node = _.groupBy components, (component) ->
return component.node_name
return _.each _.groupBy(components, 'node_name'), (node_name, components) ->
return result account, app.nodes[node_name], components
for node_name, components of components_by_node
pushResult hook,
account: account
node: app.nodes[node_name]
components: components
continue
if execute
return (callback) ->
async.series result, callback
else
return result
exports.applyHooks = ->
return Q.all exports.getHooks arguments...
error = (message) ->
err = new Error 'core.extends.hook: ' + message

View File

@@ -1,20 +1,64 @@
expressBunyanLogger = require 'express-bunyan-logger'
expressSession = require 'express-session'
redisStore = require 'connect-redis'
csrf = require 'csrf'
{config} = app
{_, path, fs, moment, crypto} = app.libs
{Account} = app.models
{Account, SecurityLog} = app.models
exports.reqHelpers = (req, res, next) ->
req.getCsrfToken = ->
if req.headers['x-csrf-token']
return req.headers['x-csrf-token']
else
return req.body.csrf_token
req.getTokenCode = ->
if req.headers['x-token']
return req.headers['x-token']
else
return req.cookies.token
req.getClientInfo = ->
return {
ip: req.headers['x-real-ip'] ? req.ip
ua: req.headers['user-agent']
}
req.createSecurityLog = (type, payload, options) ->
SecurityLog.createLog
account: options?.account ? req.account
token: token
type: type
, payload
exports.errorHandling = (req, res, next) ->
res.error = (status, name, param) ->
unless _.isNumber status
[status, name, param] = [400, status, name]
if name?.message
name = name.message
param ?= {}
res.status(status).json _.extend param,
error: name.toString()
if req.method in ['GET', 'HEAD', 'OPTIONS']
res.status(status).send name.toString()
else
res.status(status).json _.extend param,
error: name.toString()
res.createCookie = (name, value) ->
res.cookie name, value,
expires: new Date(Date.now() + config.account.cookie_time)
res.createToken = (account = req.account) ->
account.createToken('full_access', req.getClientInfo()).then (token) ->
res.createCookie('token', token.code).json
account_id: account._id
token: token.code
return token
next()
@@ -47,23 +91,17 @@ exports.session = ->
secret: secret
exports.csrf = ->
csrf = (require 'csrf')()
provider = csrf()
return (req, res, next) ->
csrf_token = do ->
if req.headers['x-csrf-token']
return req.headers['x-csrf-token']
else
return req.body.csrf_token
validator = ->
if req.path in _.pluck app.applyHooks('app.ignore_csrf'), 'path'
if req.path in app.getHooks('app.ignore_csrf', null, pluck: 'path')
return next()
if req.method in ['GET', 'HEAD', 'OPTIONS']
return next()
unless csrf.verify req.session.csrf_secret, csrf_token
unless provider.verify req.session.csrf_secret, req.getCsrfToken()
return res.error 403, 'invalid_csrf_token'
next()
@@ -71,31 +109,23 @@ exports.csrf = ->
if req.session.csrf_secret
return validator()
else
csrf.secret (err, secret) ->
provider.secret (err, secret) ->
req.session.csrf_secret = secret
req.session.csrf_token = csrf.create secret
req.session.csrf_token = provider.create secret
validator()
exports.authenticate = (req, res, next) ->
token_code = do ->
if req.headers['x-token']
return req.headers['x-token']
else
return req.cookies.token
code = req.getTokenCode()
unless token_code
unless code
return next()
Account.authenticate token_code, (token, account) ->
Account.authenticate(code).then ({token, account}) ->
if token and token.type == 'full_access'
_.extend req,
token: token
account: account
account.populate ->
next()
else
next()
@@ -133,10 +163,8 @@ exports.accountHelpers = (req, res, next) ->
site_name: req.t(config.web.t_name)
applyHooks: (name, options) ->
app.applyHooks name, req.account, _.extend {
execute: false
}, options
getHooks: (name, options) ->
app.getHooks name, req.account, options
next()

View File

@@ -1,4 +1,4 @@
{pluggable, utils, config, models, mabolo} = app
{utils, config, models, mabolo} = app
{_, async} = app.libs
{Financial, SecurityLog, Component} = app.models
{ObjectID} = mabolo
@@ -13,7 +13,7 @@ Token = mabolo.model 'Token',
required: true
type: String
token:
code:
required: true
type: String
@@ -30,13 +30,6 @@ Token = mabolo.model 'Token',
type: Date
default: -> new Date()
Token::revoke = (callback) ->
@parent().update
$pull:
tokens:
token: @token
, callback
Account = mabolo.model 'Account',
username:
required: true
@@ -81,120 +74,99 @@ Account = mabolo.model 'Account',
type: Date
default: -> new Date()
# @param account: username, email, password
# @param callback(err, account)
Account.register = (account, callback) ->
Token::revoke = ->
@parent().update
$pull:
tokens:
code: @code
Account.ensureIndex
Account.register = ({username, email, password}) ->
password_salt = utils.randomSalt()
password = utils.hashPassword password, password_salt
{username, email, password} = account
avatar_url = '//cdn.v2ex.com/gravatar/' + utils.md5(email)
account = new @
username: username
account = new Account
email: email
password: utils.hashPassword(password, password_salt)
username: username
password: password
password_salt: password_salt
preferences:
avatar_url: "//cdn.v2ex.com/gravatar/#{utils.md5(email)}"
avatar_url: avatar_url
language: 'auto'
timezone: config.i18n.default_timezone
plans: {}
pluggable: {}
async.each app.applyHooks('account.before_register'), (hook, callback) ->
hook.filter account, callback
, ->
account.save (err) ->
callback err, account
app.applyHooks('account.before_register',
execute: 'filter'
).then ->
account.save()
# @param callback(account)
Account.search = (stuff, callback) ->
@findOne {username: stuff}, (err, account) =>
Account.search = (identification) ->
@findOne(username: identification).then (account) =>
if account
return callback account
@findOne {email: stuff}, (err, account) =>
if account
return callback account
@findById stuff, (err, account) ->
callback account
# @param callback(token)
Account.generateToken = (callback) ->
token = utils.randomSalt()
@findOne
'tokens.token': token
, (err, result) ->
if result
@generateToken callback
return account
else
callback token
return @findOne(email: identification).then (account) =>
if account
return account
else
return @findById identification
# @param callback(Token, Account)
Account.authenticate = (token, callback) ->
unless token
return callback()
@findOneAndUpdate
'tokens.token': token
Account.authenticate = (token_code) ->
@findOneAndUpdate(
'tokens.token': token_code
,
$set:
'tokens.$.updated_at': new Date()
, (err, account) ->
matched_token = _.findWhere account?.tokens,
token: token
callback matched_token, account
).then (account) ->
return {
account: account
# @param callback(err, token)
Account::createToken = (type, payload, callback) ->
models.Account.generateToken (code) =>
token = new models.Token
type: type
token: code
payload: payload
created_at: new Date()
updated_at: new Date()
token: _.findWhere account?.tokens,
code: token_code
}
@update
$push:
tokens: token
, (err) ->
callback err, token
Account::createToken = (type, payload) ->
token = new Token
type: type
code: utils.randomSalt()
payload: payload
created_at: new Date()
updated_at: new Date()
@update(
$push:
tokens: token
).thenResolve token
Account::matchPassword = (password) ->
return @password == utils.hashPassword(password, @password_salt)
Account::updatePassword = (password, callback) ->
@password_salt = utils.randomSalt()
@password = utils.hashPassword password, @password_salt
@save callback
Account::setPassword = (password) ->
password_salt = utils.randomSalt()
password = utils.hashPassword password, password_salt
# @param callback(err)
Account::incBalance = (amount, type, payload, callback) ->
unless _.isNumber amount
return callback 'invalid_amount'
@update
$set:
password: password
password_salt: password_salt
financials = new models.Financials
account_id: @_id
type: type
amount: amount
payload: payload
Account::setEmail = (email) ->
financials.validate (err) =>
return callback err if err
Account::updatePreferences = (preferences) ->
Account::increaseBalance = (amount, type, payload) ->
Financials.createLog(@, type, amount, payload).then =>
@update
$inc:
balance: amount
, (err) ->
return callback err if err
financials.save (err) ->
callback err
Account::inGroup = (group) ->
return group in @groups
@@ -205,22 +177,11 @@ Account::isAdmin = ->
Account::inPlan = (plan_name) ->
return plan_name in _.keys @plans
Account::createSecurityLog = (type, token, payload, callback) ->
SecurityLog.create
account_id: @_id
type: type
token: _.pick token, 'type', 'token', 'created_at', 'payload'
payload: payload
, callback
Account::availableComponentsTemplates = ->
return _.uniq _.flatten _.compact _.map _.keys(@plans), (plan_name) ->
return _.keys app.plans[plan_name].available_components
Account::populate = (callback) ->
async.parallel
components: (callback) =>
Component.getComponents @, callback
, (err, result) =>
callback _.extend @, result
Account::populate = ->
Component.getComponents(@).then (components) =>
return _.extend @,
components: components

View File

@@ -44,15 +44,13 @@ Component = mabolo.model 'Component',
coworkers: [Coworker]
Component.getComponents = (account, callback) ->
Component.getComponents = (account) ->
@find
$or: [
account_id: account._id
,
'coworkers.account_id': account._id
]
, (err, components) ->
callback err, components
Component::hasMember = (account) ->
if @account_id.equals account._id
@@ -61,11 +59,10 @@ Component::hasMember = (account) ->
return _.some @coworkers, (coworker) ->
return coworker.account_id.equals account._id
Component::markAsStatus = (status, callback) ->
Component::markAsStatus = (status) ->
@update
$set:
status: status
, callback
Component::populate = (callback) ->
{Account} = app.models

View File

@@ -1,6 +1,6 @@
{utils, config, mabolo} = app
{_, ObjectID
, mongoose} = app.libs
{_} = app.libs
{ObjectID} = mabolo
ApplyLog = mabolo.model 'ApplyLog',
account_id:

View File

@@ -1,13 +1,10 @@
{mabolo} = app
{_, ObjectID
, mongoose} = app.libs
{ObjectID} = mabolo
Financials = mabolo.model 'Financials',
account_id:
required: true
type: ObjectID
ref: 'Account'
type:
@@ -25,3 +22,14 @@ Financials = mabolo.model 'Financials',
payload:
type: Object
Financials.createLog = (account, type, amount, payload) ->
Q().then ->
unless isFinite amount
throw new Error 'invalid_amount'
@create
account_id: account._id
type: type
amount: amount
payload: payload

View File

@@ -5,7 +5,6 @@
Notification = mabolo.model 'Notification',
account_id:
type: ObjectID
ref: 'Account'
group_name:
@@ -27,3 +26,52 @@ Notification = mabolo.model 'Notification',
payload:
type: Object
notices_level =
ticket_create: 'notice'
ticket_reply: 'notice'
ticket_update: 'event'
Notification.createNotice = (account, type, notice) ->
level = notices_level[type]
Notification.create
account_id: account._id
type: type
level: level
payload: notice
.then ->
app.mailer.sendMail
from: config.email.send_from
to: account.email
subject: notice.title
html: notice.body
, ->
callback notification
Notification.createGroupNotice = (group, type, notice) ->
level = exports.notices_level[type]
notification = new Notification
group_name: group
type: type
level: level
payload: notice
notification.save ->
unless level == NOTICE
callback notification
Account.find
groups: 'root'
, (err, accounts) ->
async.each accounts, (account, callback) ->
app.mailer.sendMail
from: config.email.send_from
to: account.email
subject: notice.title
html: notice.body
, callback
, (err) ->
logger.error err if err
callback notification

View File

@@ -11,7 +11,10 @@ SecurityLog = mabolo.model 'SecurityLog',
type:
required: true
type: String
enum: ['revoke_token', 'update_password', 'update_email', 'update_preferences']
enum: [
'login', 'revoke_token'
'update_password', 'update_email', 'update_preferences'
]
created_at:
type: Date
@@ -22,3 +25,10 @@ SecurityLog = mabolo.model 'SecurityLog',
token:
type: Object
SecurityLog.createLog = ({account, token, type}, payload) ->
@create
account_id: account._id
payload: payload
token: _.pick token, 'type', 'token', 'created_at', 'payload'
type: type

View File

@@ -1,4 +1,4 @@
markdown = require('markdown').markdown
{markdown} = require 'markdown'
{models, logger, mabolo} = app
{_, async} = app.libs
@@ -59,59 +59,55 @@ Ticket = mabolo.model 'Ticket',
type: Date
default: -> new Date()
Ticket.create = (account, ticket, callback) ->
{title, content, status} = ticket
Ticket.create = (account, {title, content, status}) ->
unless title?.trim()
return callback 'empty_title'
throw new Error 'empty_title'
if account.isAdmin()
status ?= 'open'
else
status = 'pending'
# TODO: replace `ObjectID account._id.toString()` to `account._id`
@__super__.constructor.create.call @,
account_id: ObjectID account._id.toString()
account_id: account._id
title: title
status: status
content: content
content_html: markdown.toHTML content
members: [ObjectID account._id.toString()]
members: [account._id]
replies: []
, callback
Ticket::hasMember = (account) ->
return _.some @members, (member_id) ->
return member_id.equals account._id
Ticket::setStatusByAccount = (account, status, callback) ->
Ticket::setStatusByAccount = (account, status) ->
if account.isAdmin()
unless status in ['open', 'pending', 'finish', 'closed']
return callback 'invalid_status'
throw new Error 'invalid_status'
else
unless status in ['closed']
return callback 'invalid_status'
throw new Error 'invalid_status'
@setStatus status, callback
@setStatus status
Ticket::setStatus = (status, callback) ->
# TODO: validate status
Ticket::setStatus = (status) ->
unless status in ['open', 'pending', 'finish', 'closed']
throw new Error 'invalid_status'
@update
$set:
status: status
updated_at: new Date()
, callback
Ticket::createReply = (account, reply, callback) ->
Ticket::createReply = (account, {content, status}, callback) ->
{content, status} = reply
# TODO: cant reply after closed
if @status == 'closed'
throw new Error 'already_closed'
unless content?.trim()
return callback 'empty_content'
throw new Error 'empty_content'
if account.isAdmin()
status ?= 'open'
@@ -119,47 +115,33 @@ Ticket::createReply = (account, reply, callback) ->
status = 'pending'
reply = new Reply
_parent: @
account_id: account._id
content: content
content_html: markdown.toHTML content
created_at: new Date()
@update
@update(
$push:
replies: reply
$set:
status: status
updated_at: new Date()
, (err) ->
callback err, reply
).thenResolve reply
Ticket::populateAccounts = (callback) ->
{Account} = app.models
Ticket::populateAccounts = ->
app.models.Account.find
_id:
$in: [
@account_id, @members..., _.pluck(@replies, 'account_id')...
]
async.parallel [
(callback) =>
Account.findById @account_id, (err, account) =>
@account = account
callback err
.then (accounts) =>
@account = _.find accounts, (i) =>
return @account_id.equals i
(callback) =>
Account.find
_id:
$in: @members
, (err, accounts) =>
@members = accounts
callback err
@members = _.filter accounts, (i) =>
return @hasMember i
(callback) =>
Account.find
_id:
$in: _.pluck @replies, 'account_id'
, (err, accounts) =>
for reply in @replies
reply.account = _.find accounts, (account) ->
return reply.account_id.equals account._id
callback err
], callback
for reply in @replies
reply.account = _.find accounts, (i) ->
return reply.account_id.equals account._id

View File

@@ -1,58 +0,0 @@
{async, _} = app.libs
{i18n, config, logger, mailer} = app
{Account, Notification} = app.models
{NOTICE, EVENT, LOG} = _.extend exports,
NOTICE: 'notice'
EVENT: 'event'
LOG: 'log'
exports.notices_level = notices_level =
ticket_create: NOTICE
ticket_reply: NOTICE
ticket_update: EVENT
exports.createNotice = (account, type, notice, callback) ->
level = exports.notices_level[type]
notification = new Notification
account_id: account._id
type: type
level: level
payload: notice
notification.save ->
app.mailer.sendMail
from: config.email.send_from
to: account.email
subject: notice.title
html: notice.body
, ->
callback notification
exports.createGroupNotice = (group, type, notice, callback) ->
level = exports.notices_level[type]
notification = new Notification
group_name: group
type: type
level: level
payload: notice
notification.save ->
unless level == NOTICE
callback notification
Account.find
groups: 'root'
, (err, accounts) ->
async.each accounts, (account, callback) ->
app.mailer.sendMail
from: config.email.send_from
to: account.email
subject: notice.title
html: notice.body
, callback
, (err) ->
logger.error err if err
callback notification

View File

@@ -1,6 +1,6 @@
{_, async, express} = app.libs
{_, express} = app.libs
{requireAuthenticate} = app.middleware
{Account, SecurityLog} = app.models
{Account, SecurityLog, CouponCode} = app.models
{config, utils, logger, i18n} = app
module.exports = exports = express.Router()
@@ -33,68 +33,42 @@ exports.get '/session_info/', (req, res) ->
res.json response
exports.post '/register', (req, res) ->
Account.register req.body, (err, account) ->
return res.error utils.pickErrorName err if err
account.createToken 'full_access',
ip: req.headers['x-real-ip']
ua: req.headers['user-agent']
, (err, token) ->
logger.error err if err
res.cookie 'token', token.token,
expires: new Date(Date.now() + config.account.cookie_time)
res.json
id: account._id
Account.register(req.body).then (account) ->
res.createToken account
.catch res.error
exports.post '/login', (req, res) ->
Account.search req.body.username, (account) ->
unless account
return res.error 'wrong_password'
Account.search(req.body.username).then (account) ->
if account?.matchPassword req.body.password
throw new Error 'wrong_password'
unless account.matchPassword req.body.password
return res.error 'wrong_password'
res.createCookie 'language', account.preferences.language
account.createToken 'full_access',
ip: req.headers['x-real-ip']
ua: req.headers['user-agent']
, (err, token) ->
logger.error err if err
res.cookie 'token', token.token,
expires: new Date Date.now() + config.account.cookie_time
res.cookie 'language', account.preferences.language,
expires: new Date Date.now() + config.account.cookie_time
res.json
id: account._id
res.createToken(account).then (token) ->
req.createSecurityLog 'login', {},
account: account
token: token
exports.post '/logout', requireAuthenticate, (req, res) ->
req.token.revoke ->
req.account.createSecurityLog 'revoke_token', req.token,
revoke_ip: req.headers['x-real-ip']
revoke_ua: req.headers['user-agent']
, (err) ->
logger.error err if err
.catch res.error
res.clearCookie 'token'
res.json {}
exports.post '/logout', requireAuthenticate, (req, res) ->
req.token.revoke().then ->
req.createSecurityLog('revoke_token').then ->
res.clearCookie('token').sendStatus 204
.catch res.error
exports.post '/update_password', requireAuthenticate, (req, res) ->
unless req.account.matchPassword req.body.original_password
return res.error 'wrong_password'
Q().then ->
unless req.account.matchPassword req.body.original_password
throw new Error 'wrong_password'
unless utils.rx.password.test req.body.password
return res.error 'invalid_password'
unless utils.rx.password.test req.body.password
throw new Error 'invalid_password'
req.account.updatePassword req.body.password, ->
req.account.createSecurityLog 'update_password', req.token, {}, (err) ->
logger.error err if err
req.account.setPassword(req.body.password).then ->
req.createSecurityLog 'update_password'
res.json {}
.catch res.error
exports.post '/update_email', requireAuthenticate, (req, res) ->
unless req.account.matchPassword req.body.password
@@ -136,3 +110,43 @@ exports.post '/update_preferences', requireAuthenticate, (req, res) ->
logger.error err if err
res.json {}
exports.use do ->
router = new express.Router()
router.use requireAuthenticate
router.get '/info', (req, res) ->
CouponCode.findOne
code: req.query.code
, (err, coupon) ->
unless coupon
return res.error 'code_not_exist'
coupon.validateCode req.account, (is_available) ->
unless is_available
return res.error 'code_not_available'
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 {}

View File

@@ -1,7 +1,7 @@
{express, async, _} = app.libs
{requireAdminAuthenticate} = app.middleware
{Account, Ticket, Financials, CouponCode} = app.models
{config, pluggable} = app
{config} = app
module.exports = exports = express.Router()

View File

@@ -1,41 +0,0 @@
{express, _} = app.libs
{config, billing} = app
{requireAuthenticate} = app.middleware
{Account} = app.models
module.exports = exports = express.Router()
exports.use requireAuthenticate
{when_balance_below} = config.billing.force_freeze
exports.post '/join_plan', (req, res) ->
{plan} = req.body
unless billing.plans[plan]
return res.error 'invalid_plan'
if req.account.inPlan plan
return res.error 'already_in_plan'
if req.account.balance <= when_balance_below
return res.error 'insufficient_balance'
billing.joinPlan req.account, plan, (err) ->
console.log err
if err
res.error err
else
res.status(204).json {}
exports.post '/leave_plan', (req, res) ->
{plan} = req.body
unless req.account.inPlan plan
return res.error 'not_in_plan'
billing.leavePlan req.account, plan, (err) ->
if err
res.error err
else
res.status(204).json {}

View File

@@ -1,43 +0,0 @@
{_, express} = app.libs
{requireAuthenticate} = app.middleware
{CouponCode} = app.models
{config, utils, logger} = app
module.exports = exports = express.Router()
exports.use requireAuthenticate
exports.get '/info', (req, res) ->
CouponCode.findOne
code: req.query.code
, (err, coupon) ->
unless coupon
return res.error 'code_not_exist'
coupon.validateCode req.account, (is_available) ->
unless is_available
return res.error 'code_not_available'
coupon.getMessage req, (message) ->
res.json
message: message
exports.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 {}

View File

@@ -1,12 +1,43 @@
{express, async, _} = app.libs
{requireAuthenticate} = app.middleware
{Account, Financials} = app.models
{pluggable, billing, config} = app
{billing, config} = app
module.exports = exports = express.Router()
exports.use requireAuthenticate
exports.post '/join_plan', (req, res) ->
{plan} = req.body
unless billing.plans[plan]
return res.error 'invalid_plan'
if req.account.inPlan plan
return res.error 'already_in_plan'
if req.account.balance <= when_balance_below
return res.error 'insufficient_balance'
billing.joinPlan req.account, plan, (err) ->
console.log err
if err
res.error err
else
res.status(204).json {}
exports.post '/leave_plan', (req, res) ->
{plan} = req.body
unless req.account.inPlan plan
return res.error 'not_in_plan'
billing.leavePlan req.account, plan, (err) ->
if err
res.error err
else
res.status(204).json {}
exports.get '/financials', (req, res) ->
LIMIT = 10

View File

@@ -1,7 +1,7 @@
{_, async, express} = app.libs
{requireAuthenticate, TODO} = app.middleware
{Account, Ticket} = app.models
{config, notification, logger} = app
{config, logger} = app
module.exports = exports = express.Router()
@@ -25,7 +25,7 @@ ticketParam = (req, res, next, id) ->
exports.param 'id', ticketParam
exports.use '/resource', do ->
exports.use '/rest', do ->
rest = new express.Router mergeParams: true
rest.param 'id', ticketParam

View File

@@ -1,14 +1,5 @@
$ ->
$('.action-login').click ->
RP.request '/account/login',
username: $('[name=username]').val()
password: $('[name=password]').val()
, ->
location.href = '/panel/'
$('[name=password]').keypress (e) ->
if e.keyCode == 13
$('.action-login').click()
{request, t} = RP
$('.action-register').click ->
username = $('[name=username]').val()
@@ -19,9 +10,56 @@ $ ->
unless password == password2
return alert t 'view.account.password_inconsistent'
RP.request '/account/register',
request '/account/register',
username: username
password: password
email: email
, ->
location.href = '/panel/'
$('.action-login').click ->
request '/account/login',
username: $('[name=username]').val()
password: $('[name=password]').val()
, ->
location.href = '/panel/'
$('[name=password]').keypress (e) ->
if e.keyCode == 13
$('.action-login').click()
$('.action-save').click ->
request '/account/update_preferences',
qq: $('[name=qq]').val()
, ->
alert t 'common.success'
$('.action-use').click ->
code = $('[name=coupon_code]').val()
request "/coupon/info?code=#{code}", {}, {method: 'get'}, (result) ->
if window.confirm result.message
request '/coupon/apply',
code: code
, ->
alert t 'common.success'
$('.action-update-password').click ->
password = $('.form-password .input-password').val()
password2 = $('.form-password .input-password2').val()
if password != password2
return alert t 'view.account.password_inconsistent'
request '/account/update_password/',
original_password: $('.form-password .input-original_password').val()
password: password
, ->
alert t 'common.success'
$('.action-update-email').click ->
request '/account/update_email/',
password: $('.form-email .input-password').val()
email: $('.form-email .input-email').val()
, ->
alert t 'common.success'

View File

@@ -1,36 +0,0 @@
$ ->
$('.action-save').click ->
request '/account/update_preferences',
qq: $('.form-setting .input-qq').val()
, ->
alert t 'common.success'
$('.action-use').click ->
code = $('.form-coupon .input-coupon_code').val()
request "/coupon/info?code=#{code}", {}, {method: 'get'}, (result) ->
if window.confirm result.message
request '/coupon/apply',
code: code
, ->
alert t 'common.success'
$('.action-update-password').click ->
password = $('.form-password .input-password').val()
password2 = $('.form-password .input-password2').val()
if password != password2
return alert t 'view.account.password_inconsistent'
request '/account/update_password/',
original_password: $('.form-password .input-original_password').val()
password: password
, ->
alert t 'common.success'
$('.action-update-email').click ->
request '/account/update_email/',
password: $('.form-email .input-password').val()
email: $('.form-email .input-email').val()
, ->
alert t 'common.success'

View File

@@ -1,9 +0,0 @@
{fs, path} = app.libs
template_data = {}
for filename in fs.readdirSync "#{__dirname}/template"
template_name = path.basename filename, path.extname(filename)
template_data[template_name] = fs.readFileSync("#{__dirname}/template/#{filename}").toString()
module.exports = template_data

View File

@@ -0,0 +1,141 @@
describe 'account.register.test', ->
agent = null
csrf_token = null
account_id = null
username = null
password = null
email = null
before ->
agent = supertest.agent app.express
after (done) ->
cleanUpByAccount account_id, done
it 'GET login', (done) ->
agent.get '/account/login'
.expect 200
.end done
it 'GET register', (done) ->
agent.get '/account/register'
.expect 200
.end done
it 'GET session_info', (done) ->
agent.get '/account/session_info'
.expect 200
.end (err, res) ->
res.body.csrf_token.should.be.exist
csrf_token = res.body.csrf_token
done err
it 'POST register', (done) ->
username = 'test' + utils.randomString(8).toLowerCase()
password = utils.randomString 8
email = utils.randomString(8) + '@gmail.com'
agent.post '/account/register'
.send
csrf_token: csrf_token
username: username
email: email
password: password
.expect 200
.expect 'set-cookie', /token=/
.end (err, res) ->
res.body.id.should.have.length 24
account_id = res.body.id
done err
it 'POST register with existed username', (done) ->
agent.post '/account/register'
.send
csrf_token: csrf_token
username: username
email: "#{utils.randomString 8}@gmail.com"
password: password
.expect 400
.end (err, res) ->
res.body.error.should.be.equal 'username_exist'
done err
it 'POST register with invalid email', (done) ->
agent.post '/account/register'
.send
csrf_token: csrf_token
username: "test#{utils.randomString(8).toLowerCase()}"
email: "@gmail.com"
password: password
.expect 400
.end (err, res) ->
res.body.error.should.be.equal 'invalid_email'
done err
it 'POST login', (done) ->
agent.post '/account/login'
.send
csrf_token: csrf_token
username: username
password: password
.expect 200
.expect 'set-cookie', /token=/
.end (err, res) ->
res.body.id.should.be.equal account_id
res.body.token.should.be.exist
done err
it 'GET session_info when logged', (done) ->
agent.get '/account/session_info'
.expect 200
.end (err, res) ->
res.body.csrf_token.should.be.exist
res.body.username.should.be.equal username
res.body.preferences.should.be.a 'object'
done err
it 'POST logout', (done) ->
agent.post '/account/logout'
.send
csrf_token: csrf_token
.expect 200
.expect 'set-cookie', /token=;/
.end done
it 'POST login with email', (done) ->
agent.post '/account/login'
.send
csrf_token: csrf_token
username: email.toLowerCase()
password: password
.expect 200
.expect 'set-cookie', /token=/
.end (err, res) ->
res.body.id.should.be.equal account_id
res.body.token.should.be.exist
done err
it 'POST login with username does not exist', (done) ->
agent.post '/account/login'
.send
csrf_token: csrf_token
username: 'username_not_exist'
password: password
.expect 400
.end (err, res) ->
res.body.error.should.be.equal 'wrong_password'
expect(res.body.token).to.not.exist
done err
it 'POST login with invalid password', (done) ->
agent.post '/account/login'
.send
csrf_token: csrf_token
username: username
password: 'invalid password'
.expect 400
.end (err, res) ->
res.body.error.should.be.equal 'wrong_password'
expect(res.body.token).to.not.exist
done err

View File

@@ -1,4 +1,4 @@
describe 'ticket.test', ->
describe 'ticket.user.test', ->
agent = null
csrf_token = null
@@ -10,7 +10,7 @@ describe 'ticket.test', ->
done err
it 'POST /', (done) ->
agent.post '/ticket/resource/'
agent.post '/ticket/rest/'
.send
csrf_token: csrf_token
title: 'Title'
@@ -22,14 +22,14 @@ describe 'ticket.test', ->
done err
it 'GET /', (done) ->
agent.get '/ticket/resource/'
agent.get '/ticket/rest/'
.expect 200
.end (err, res) ->
res.body.should.be.a 'array'
done err
it 'POST /:id/replies', (done) ->
agent.post "/ticket/resource/#{ticket_id}/replies"
agent.post "/ticket/rest/#{ticket_id}/replies"
.send
csrf_token: csrf_token
content: 'Reply'
@@ -40,14 +40,14 @@ describe 'ticket.test', ->
done err
it 'GET /:id', (done) ->
agent.get "/ticket/resource/#{ticket_id}"
agent.get "/ticket/rest/#{ticket_id}"
.expect 200
.end (err, res) ->
res.body.replies.length.should.be.equal 1
done err
it 'PUT /:id/status', (done) ->
agent.put "/ticket/resource/#{ticket_id}/status"
agent.put "/ticket/rest/#{ticket_id}/status"
.send
csrf_token: csrf_token
status: 'closed'

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{t('account.login')} | #{t(config.web.t_name)}
title #{t('account.login')} | #{site_name}
block main
header= t('account.login')

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{t('account.preferences')} | #{t(config.web.t_name)}
title #{t('account.preferences')} | #{site_name}
block main
.row
@@ -10,7 +10,7 @@ block main
.form-group
label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.qq')
.col-sm-5
input.input-qq.form-control(type='text', value=account.preferences.qq)
input.form-control(name='qq', type='text', value=account.preferences.qq)
.form-group
.col-sm-offset-3
button.action-save.btn.btn-lg.btn-success(type='button')= t('common.save')
@@ -21,7 +21,7 @@ block main
.form-group
label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.code')
.col-sm-5
input.input-coupon_code.form-control(type='text')
input.form-control(name='coupon_code', type='text')
.form-group
.col-sm-offset-3
button.action-use.btn.btn-lg.btn-success(type='button')= t('common.apply')
@@ -66,4 +66,4 @@ block main
append footer
script(src='/script/account/preferences.js')
script(src='/script/account.js')

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{t('account.register')} | #{t(config.web.t_name)}
title #{t('account.register')} | #{site_name}
block main
header= t('account.register')

View File

@@ -1,7 +1,7 @@
extends layout
prepend header
title #{t('admin.admin_panel')} | #{t(config.web.t_name)}
title #{t('admin.admin_panel')} | #{site_name}
append header
link(rel='stylesheet', href='/style/admin.css')
@@ -31,7 +31,7 @@ block main
header 付费方案
each plan, name in app.billing.plans
each plan, name in app.plans
.panel.panel-success
.panel-heading
strong= name

View File

@@ -5,8 +5,8 @@ html
block header
link(rel='stylesheet', href='/bower_components/bootstrap/dist/css/bootstrap.min.css')
link(rel='stylesheet', href='/style/layout.css')
for hook in applyHooks('view.layout.styles')
link(rel='stylesheet', href=hook.path)
for path in getHooks('view.layout.styles', {pluck: 'path'})
link(rel='stylesheet', href=path)
body(data-username="#{account ? account.username : ''}", data-locale-version=app.i18n.localeHash(req), data-csrf-token=req.session.csrf_token)
header.navbar-fixed-top
@@ -21,7 +21,7 @@ html
a.navbar-brand(href='/')= t(config.web.t_name)
#navbar-collapse.collapse.navbar-collapse
ul.nav.navbar-nav
for hook in applyHooks('view.layout.menu_bar')
for hook in getHooks('view.layout.menu_bar')
li
a(href=hook.href, target=hook.target)= hook.plugin.getTranslator(req)(hook.t_body)
ul.nav.navbar-nav.navbar-right
@@ -85,6 +85,6 @@ html
script(src='/bower_components/backbone/backbone.js')
script(src='/bower_components/bootstrap/dist/js/bootstrap.min.js')
script(src='/script/layout.js')
for hook in applyHooks('view.layout.scripts')
script(src=hook.path)
for path in getHooks('view.layout.scripts', {pluck: 'path'})
script(src=path)
block footer

View File

@@ -1,7 +1,7 @@
extends layout
prepend header
title #{t('panel.')} | #{t(config.web.t_name)}
title #{t('panel.')} | #{site_name}
append header
link(rel='stylesheet', href='/style/panel.css')

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{t('account.financials')} | #{t(config.web.t_name)}
title #{t('account.financials')} | #{site_name}
block main
.row

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{t('ticket.create_ticket')} | #{t(config.web.t_name)}
title #{t('ticket.create_ticket')} | #{site_name}
append header
link(rel='stylesheet', href='/style/ticket.css')

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{t('ticket.ticket_list')} | #{t(config.web.t_name)}
title #{t('ticket.ticket_list')} | #{site_name}
block main
#list-view

View File

@@ -1,7 +1,7 @@
extends ../layout
prepend header
title #{req.ticket.title} | #{t(config.web.t_name)}
title #{req.ticket.title} | #{site_name}
append header
link(rel='stylesheet', href='/style/ticket.css')

View File

@@ -18,9 +18,7 @@
"scripts": {
"install": "./node_modules/.bin/bower install",
"start": "./node_modules/.bin/coffee app.coffee | ./node_modules/.bin/bunyan -o short",
"test": "COV_TEST=true ./node_modules/.bin/mocha --compilers coffee:coffee-script/register --require test/env --reporter node_modules/mocha-reporter-cov-summary -- core/test/*.test.coffee plugins/*/test",
"test-only": "./node_modules/.bin/mocha --compilers coffee:coffee-script/register --require test/env -b -- core/test/*.test.coffee plugins/*/test",
"test-cov-html": "COV_TEST=true ./node_modules/.bin/mocha --compilers coffee:coffee-script/register --require test/env --reporter html-cov -- core/test/*.test.coffee plugins/*/test > coverage-reporter.html"
"test": "COV_TEST=true ./node_modules/.bin/mocha --compilers coffee:coffee-script/register --require test/env --reporter node_modules/mocha-reporter-cov-summary -- core/test/*.test.coffee plugins/*/test"
},
"dependencies": {
"async": "^0.9.0",
@@ -42,7 +40,7 @@
"insight": "^0.4.3",
"jade": "^1.7.0",
"json-stable-stringify": "^1.0.0",
"mabolo": "^0.1.3",
"mabolo": "^0.3.0-rc.1",
"markdown": "^0.5.0",
"moment-timezone": "^0.2.5",
"mongodb": "^2.0.7",
@@ -54,7 +52,8 @@
"request": "^2.48.0",
"semver": "^4.1.0",
"ssh2": "^0.3.6",
"underscore": "^1.7.0"
"underscore": "^1.7.0",
"q": "^1.2.0"
},
"devDependencies": {
"chai": "^1.10.0",

View File

@@ -1,7 +1,7 @@
extends ../../../core/view/layout
prepend header
title #{t('server_monitor')} | #{t(config.web.t_name)}
title #{t('server_monitor')} | #{site_name}
append header
link(rel='stylesheet', href='/plugin/linux/style/monitor.css')

View File

@@ -5,13 +5,13 @@ global._ = require 'underscore'
global.fs = require 'fs'
if fs.existsSync "#{__dirname}/../config.coffee"
config = require '../config'
global.config = require '../config'
else
config = require '../sample/core.config.coffee'
global.config = require '../sample/core.config.coffee'
global.Q = require 'q'
global.chai = require 'chai'
global.async = require 'async'
global.config = config
global.supertest = require 'supertest'
if process.env.COV_TEST == 'true'

View File

@@ -1,4 +1,4 @@
utils = require '../core/utils'
global.utils = require '../core/utils'
createAgent = (callback) ->
agent = supertest.agent app.express
@@ -9,6 +9,9 @@ createAgent = (callback) ->
agent: agent
csrf_token: res.body.csrf_token
cleanUpByAccount = ({account_id}, callback) ->
app.models.Account.findByIdAndRemove account_id, callback
createLoggedAgent = (callback) ->
createAgent (err, {agent, csrf_token}) ->
username = 'test' + utils.randomString(8).toLowerCase()
@@ -22,6 +25,9 @@ createLoggedAgent = (callback) ->
email: email
password: password
.end (err, res) ->
after (done) ->
cleanUpByAccount res.body.account_id, done
callback err,
agent: agent
username: username
@@ -32,4 +38,5 @@ createLoggedAgent = (callback) ->
_.extend global,
createAgent: createAgent
cleanUpByAccount: cleanUpByAccount
createLoggedAgent: createLoggedAgent