├── .backup └── .gitkeep ├── .gitignore ├── .mailmap ├── .travis.yml ├── CHANGELOG.md ├── INSTALL.md ├── LICENSE ├── README.md ├── Vagrantfile ├── WIKI ├── App │ ├── Ghost.md │ ├── Typecho.md │ └── WordPress.md ├── FAQ │ ├── Billing.md │ ├── Domain.md │ └── Support.md ├── Linux │ ├── Filesystem.md │ └── SSH.md ├── Nginx │ └── JSON-Configure.md └── Runtime │ └── PHP.md ├── app.coffee ├── bin ├── fix-permissions.coffee ├── migrate.coffee └── reconfigure.coffee ├── core ├── billing.coffee ├── cache.coffee ├── db.coffee ├── i18n.coffee ├── locale │ ├── en.json │ └── zh_CN.json ├── middleware.coffee ├── model │ ├── Account.coffee │ ├── CouponCode.coffee │ ├── Financials.coffee │ ├── Notification.coffee │ ├── SecurityLog.coffee │ └── Ticket.coffee ├── notification.coffee ├── pluggable.coffee ├── router │ ├── account.coffee │ ├── admin.coffee │ ├── billing.coffee │ ├── coupon.coffee │ ├── panel.coffee │ └── ticket.coffee ├── static │ ├── script │ │ ├── account │ │ │ ├── login.coffee │ │ │ ├── preferences.coffee │ │ │ └── register.coffee │ │ ├── admin.coffee │ │ ├── layout.coffee │ │ ├── panel.coffee │ │ └── ticket │ │ │ ├── create.coffee │ │ │ └── view.coffee │ └── style │ │ ├── admin.less │ │ ├── layout.less │ │ ├── panel.less │ │ └── ticket.less ├── template │ ├── ticket_create_email.html │ └── ticket_reply_email.html ├── templates.coffee ├── test │ ├── app.test.coffee │ ├── billing.test.coffee │ ├── cache.test.coffee │ ├── i18n.test.coffee │ ├── middleware.test.coffee │ ├── model │ │ ├── Account.test.coffee │ │ ├── CouponCode.test.coffee │ │ ├── Financials.test.coffee │ │ ├── Notification.test.coffee │ │ ├── SecurityLog.test.coffee │ │ └── Ticket.test.coffee │ ├── pluggable.test.coffee │ ├── router │ │ ├── account.test.coffee │ │ ├── admin.test.coffee │ │ ├── billing.test.coffee │ │ ├── coupon.test.coffee │ │ ├── panel.test.coffee │ │ └── ticket.test.coffee │ └── utils.test.coffee ├── utils.coffee └── view │ ├── account │ ├── login.jade │ ├── preferences.jade │ └── register.jade │ ├── admin.jade │ ├── layout.jade │ ├── panel.jade │ ├── panel │ └── financials.jade │ └── ticket │ ├── create.jade │ ├── list.jade │ └── view.jade ├── migration ├── database │ ├── v0.6.0.coffee │ ├── v0.7.1.coffee │ └── v0.8.0.coffee └── system │ ├── v0.7.1.md │ └── v0.8.0.md ├── package.json ├── plugin ├── bitcoin │ ├── bitcoin.coffee │ ├── index.coffee │ ├── locale │ │ ├── en.json │ │ └── zh_CN.json │ ├── reconfigure.coffee │ └── view │ │ └── payment_method.jade ├── linux │ ├── index.coffee │ ├── linux.coffee │ ├── locale │ │ ├── en.json │ │ └── zh_CN.json │ ├── monitor.coffee │ ├── reconfigure.coffee │ ├── static │ │ └── style │ │ │ ├── monitor.less │ │ │ └── panel.less │ ├── test │ │ └── linux.test.coffee │ └── view │ │ ├── monitor.jade │ │ └── widget.jade ├── rpvhost │ ├── index.coffee │ ├── locale │ │ ├── en.json │ │ └── zh_CN.json │ ├── static │ │ └── style │ │ │ ├── green.less │ │ │ └── index.less │ ├── test │ │ └── rpvhost.test.coffee │ ├── view │ │ ├── index.jade │ │ └── payment_method.jade │ └── wiki │ │ └── Terms.md ├── shadowsocks │ ├── index.coffee │ ├── locale │ │ ├── en.json │ │ └── zh_CN.json │ ├── reconfigure.coffee │ ├── router.coffee │ ├── shadowsocks.coffee │ ├── static │ │ ├── script │ │ │ └── panel.coffee │ │ └── style │ │ │ └── panel.less │ ├── test │ │ └── shadowsocks.test.coffee │ ├── view │ │ ├── admin │ │ │ └── sidebar.jade │ │ └── widget.jade │ └── wiki │ │ └── README.md ├── ssh │ ├── index.coffee │ ├── locale │ │ ├── en.json │ │ └── zh_CN.json │ ├── router.coffee │ ├── ssh.coffee │ ├── static │ │ └── script │ │ │ └── panel.coffee │ └── view │ │ └── widget.jade ├── supervisor │ ├── index.coffee │ ├── locale │ │ ├── en.json │ │ └── zh_CN.json │ ├── reconfigure.coffee │ ├── router.coffee │ ├── static │ │ ├── script │ │ │ └── panel.coffee │ │ └── style │ │ │ └── panel.less │ ├── supervisor.coffee │ ├── template │ │ └── program.conf │ ├── test │ │ └── supervisor.test.coffee │ └── view │ │ └── widget.jade └── wiki │ ├── index.coffee │ ├── locale │ ├── en.json │ └── zh_CN.json │ ├── test │ └── wiki.test.coffee │ ├── view │ ├── index.jade │ └── page.jade │ └── wiki.coffee ├── sample ├── core.config.coffee ├── rpvhost.config.coffee └── shadowsocks.config.coffee └── test ├── env.coffee └── full-test.coffee /.backup/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackPlan/RootPanel/9e3b41a87d3a0ac137626a9cb34fac61e3028cdc/.backup/.gitkeep -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage-reporter.html 3 | /npm-debug.log 4 | 5 | /.vagrant 6 | /package.box 7 | 8 | /.idea 9 | *~ 10 | .DS_Store 11 | 12 | /config.coffee 13 | /session.key 14 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | jysperm 2 | jysperm 3 | lyd600lty 4 | kanakin 5 | kanakin 6 | kanakin 7 | kanakin 8 | kinosang 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.11 5 | - 0.10 6 | 7 | before_install: 8 | - sudo apt-get update -qq 9 | - sudo apt-get install -y quota quotatool supervisor 10 | - sudo apt-get install -y python-pip python-m2crypto 11 | - sudo pip install shadowsocks 12 | - sudo mkdir /etc/shadowsocks 13 | 14 | script: npm run test-full 15 | 16 | services: 17 | - mongodb 18 | - redis-server 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.8.0 2 | 对核心代码进行了完整的重构,实现了 supervisor 插件,部分插件等待下个版本进行重构。 3 | 4 | * (新增) 基于 Travis-CI 的自动测试,增加了 Vagrantfile 5 | * (新增) reconfigure 功能,重新应用配置 6 | * (新增) 完成了 Supervisor 插件 7 | * (改进) 重构了 linux, rpvhost, ssh, shadowsocks 插件 8 | * (改进) 重构了数据库升级迁移框架 9 | * (改进) 重构了所有 Model, 改为基于 mongoose 10 | * (改进) 重构了缓存框架,将邮件发送功能抽取为了一个通知框架 11 | * (改进) 重构了插件机制,增强了 hook 的功能,所有插件继承自 Plugin 类 12 | * (改进) 重构了结算机制 13 | * (改进) 重构了国际化组件,支持更好地 fallback, 为前端添加了国际化支持,添加了语言选择功能 14 | * (改进) 将视图文件中全部的字符串提取为了语言资源文件,并翻译了英文版本 15 | * (改进) 从源代码中移除配置文件,在 `sample` 目录提供一组默认配置文件 16 | * (改进) 将 WIKI 抽取为了一个独立的插件,自动生成 WIKI 列表,代替原 WIKI 首页 17 | * (改进) 将比特币支付功能抽取为了一个独立的插件 18 | * (更改) 将文档移动到了 Github WIKI 19 | * (更改) 更换到了 AGPL 授权协议 20 | * (安全) 增加了 CSRF Token 机制 21 | * (安全) 修复了比特币支付部分的一个安全问题 22 | 23 | 252 commits, 242 changed files with 7495 additions and 5327 deletions, by 2 contributors: jysperm, yudong. 24 | 25 | ## v0.7.1(2014.9.2) 26 | 有关 ShadowSocks 的漏洞修复,以及从 v0.6.0 升级的迁移脚本。 27 | 28 | 9 commits, 15 changed files with 183 additions and 42 deletions, by 1 contributors: jysperm. 29 | 30 | ## v0.7.0(2014.8.31) 31 | 用于 2014.8.31,GreenShadow 上线,数据库升级脚本将于 v0.7.1 提供。 32 | 33 | * (新增) 数据库升级迁移脚本,兑换代码生成脚本 34 | * (新增) 工单邮件提醒 35 | * (新增) 完成了 ShadowSocks 插件 36 | * (改进) 将 RP 主机的主页独立为了插件 37 | * (改进) 改进扣费机制,改进插件机制 38 | * (改进) 改用 supervisor 运行,弃用 Makefile, 更新 README, 新增配置文件示例 39 | 40 | 39 commits, 82 changed files with 1110 additions and 440 deletions, by 1 contributors: jysperm. 41 | 42 | ## v0.6.0(2014.8.11) 43 | 用于 2014.8.11, 新版 jp1.rpvhost.net 上线,没有不兼容的数据库更新。 44 | 45 | * (新增) 全局启动脚本,文件权限修复工具 46 | * (新增) 修改帐号 QQ, 密码,邮箱 47 | * (新增) 帐号安全事件记录 48 | * (新增) 兑换代码 49 | * (新增) 管理员面板:删除账户 50 | * (改进) 在 Token 中记录 IP, UA, 和最后使用时间。 51 | 52 | 18 commits, 27 changed files with 366 additions and 29 deletions, by 1 contributors: jysperm. 53 | 54 | ## v0.5.0(2014.8.7) 55 | 用于 2014.8.7, 新版 us1.rpvhost.net 上线。 56 | 57 | * (改进) 在面板磁盘占用中记入数据库体积 58 | * (改进) 细化结算机制 59 | 60 | 11 commits, 13 changed files with 202 additions and 114 deletions, by 1 contributors: jysperm. 61 | 62 | ## v0.4.0 (2014.7.31) 63 | 用于 2014.7.31 的第四次测试,不提供从 v0.3.0 的迁移脚本。 64 | 65 | * (新增) 管理员面板新增:站点列表,禁用站点,工单列表 66 | * (新增) 服务器状态监视器 67 | * (新增) 支持了磁盘空间限制和监控 68 | * (改进) 新增了有关部署 Ghost, Typecho 的文档 69 | * (改进) 将插件的前端文件移动至插件目录 70 | * (废弃) 删除了工单类型的设计 71 | 72 | 24 commits, 74 changed files with 1,274 additions and 594 deletions, by 1 contributors: jysperm. 73 | 74 | ## v0.3.0 (2014.7.26) 75 | 用于 2014.7.26 的第三次测试,不提供从 v0.2.0 的迁移脚本。 76 | 77 | * (新增) 支持了 uwsgi, proxy 等 Nginx 指令 78 | * (新增) 实现了 MongoDB 插件 79 | * (新增) 实现了 Nginx 向导模式 80 | * (新增) 实现了 Redis 插件 81 | * (改进) 重写了全部前端逻辑 82 | * (改进) 测试了资源限制并在面板上显示 83 | 84 | 25 commits, 54 changed files with 882 additions and 269 deletions, by 1 contributors: jysperm. 85 | 86 | ## v0.2.0 (2014.7.21) 87 | 用于 2014.7.21 的第二次测试,不提供从 v0.1.0 的迁移脚本。 88 | 89 | * (新增) 支持了比特币支付 90 | * (新增) 添加了充值页面、付款日志和扣费日志 91 | * (新增) 添加了用户手册页面、服务支持页面和首页 92 | * (新增) 管理员页面和功能 93 | * (新增) Linux 插件和资源监控 94 | * (新增) Memcached 插件 95 | * (新增) MySQL 插件 96 | * (新增) PHP-FPM 插件 97 | * (新增) Nginx 插件,尚只支持 PHP-FPM 站点 98 | * (改进) 细化了安装教程,补充了用户手册 99 | * (改进) 优化了路由绑定,重构了 Model 模型 100 | * (废弃) 删除了原 API 测试 101 | 102 | 168 commits, 146 changed files with 2,761 additions and 1,459 deletions, by 2 contributors: jysperm, yudong. 103 | 104 | ## v0.1.0 (2014.5.18) 105 | 106 | 第一个版本,用于 2014.5.18 的第一次公开测试。 107 | 108 | * 帐号注册,登录 109 | * 订阅和退订套餐 110 | * SSH 插件 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RootPanel 2 | RootPanel 是一个 PaaS 开发框架,提供了用户系统、计费和订单系统、工单系统,允许通过开发插件的方式来支持各种网络服务的管理和销售,默认实现了一些插件来支持例如虚拟主机,ShadowSocks 等常见服务,用户也可以简单地自行编写插件来拓展 RootPanel 的功能。 3 | 4 | RootPanel 具有良好的设计,高度的可定制性,支持多语言和多时区,以及非常高的单元测试覆盖率。 5 | 6 | RootPanel 的文档位于 [Github Wiki](https://github.com/jysperm/RootPanel/wiki). 7 | 8 | ## 安装 9 | 10 | 稳定版本 11 | [![Build Status](https://travis-ci.org/jysperm/RootPanel.svg?branch=stable)](https://travis-ci.org/jysperm/RootPanel) 12 | 13 | git clone -b stable https://github.com/jysperm/RootPanel.git 14 | 15 | 开发版本 16 | [![Build Status](https://travis-ci.org/jysperm/RootPanel.svg?branch=master)](https://travis-ci.org/jysperm/RootPanel) 17 | 18 | git clone https://github.com/jysperm/RootPanel.git 19 | 20 | 试运行和开发推荐使用 [Vagrant box](https://vagrantcloud.com/jysperm/boxes/rootpanel) 21 | 22 | 详细安装步骤:[INSTALL.md](https://github.com/jysperm/RootPanel/blob/master/INSTALL.md) 23 | 24 | ## 配置文件示例 25 | 26 | 请从 `sample` 中选择一个配置文件复制到根目录,重命名为 `config.coffee`: 27 | 28 | core.config.coffee # 仅核心模块 29 | rpvhost.config.coffee # 虚拟主机 (正在重构,目前支持 SSH 和 Supervisor) 30 | shadowsocks.config.coffee # ShadowSocks 代理服务 31 | 32 | ## 从旧版本升级 33 | 34 | # 停止 RootPanel 35 | supervisorctl stop RootPanel 36 | 37 | # 备份数据库 38 | mongodump --authenticationDatabase admin --db RootPanel --out .backup/db -u rpadmin -p 39 | 40 | # 更新源代码 41 | git pull 42 | 43 | 根据 `/migration/system` 中新增的说明文件,执行相应命令来修改系统设置,如果跨越多个版本需要依次执行。 44 | 检查更新日志和 `/sample` 中的默认配置文件,视情况修改配置文件(`config.coffee`). 45 | 46 | # 升级数据库 47 | npm run migrate 48 | 49 | # 应用新的设置 50 | npm run reconfigure 51 | 52 | # 启动 RootPanel 53 | supervisorctl start RootPanel 54 | 55 | ## 技术构成 56 | 57 | * 前端:Bootstrap(3), jQuery, Jade, Less 58 | * 后端:Express, Coffee 59 | * 数据库:MongoDB(2.4), Redis 60 | * 操作系统支持:Ubuntu 14.04 amd64 61 | 62 | ## 开发情况: 63 | 64 | * [ChangeLog](https://github.com/jysperm/RootPanel/blob/master/CHANGELOG.md) 65 | * [Releases](https://github.com/jysperm/RootPanel/releases) 66 | * [TODO List](https://github.com/jysperm/RootPanel/labels/TODO) 67 | 68 | 贡献列表(v0.8.0): 69 | 70 | * jysperm 10149 lines 98% 71 | * yudong 48 lines 1.6% 72 | * kanakin 38 lines 0.4% 73 | 74 | 贡献须知:当你向 RootPanel 贡献代码时,即代表你同意授予 RootPanel 维护团队永久的,不可撤回的代码使用权,包括但不限于以闭源的形式出售商业授权。 75 | 在你首次向 RootPanel 贡献代码时,我们还会人工向你确认一次上述协议。 76 | 77 | ## 许可协议 78 | 79 | * 开源授权:[AGPLv3](https://github.com/jysperm/RootPanel/blob/master/LICENSE) | [CC-SA](http://creativecommons.org/licenses/sa/1.0/) (文档) | Public Domain (配置文件和示例) 80 | * 商业授权(计划中) 81 | * 有关授权的 [FAQ](https://github.com/jysperm/RootPanel/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98#%E6%8E%88%E6%9D%83) 82 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | # based on ubuntu/trusty64 6 | config.vm.box = "jysperm/rootpanel" 7 | config.vm.hostname = "rp.rpvhost.net" 8 | 9 | config.vm.network "private_network", ip: "192.168.33.10" 10 | 11 | config.vm.synced_folder ".", "/vagrant", 12 | owner: "rpadmin", group: "rpadmin" 13 | end 14 | -------------------------------------------------------------------------------- /WIKI/App/Ghost.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 首先你需要成功开通了套餐,修改 SSH 密码。 3 | 4 | 登录 SSH, 执行以下命令下载 Ghost, 请自行到官网查看最新版本的下载地址: 5 | 6 | wget https://ghost.org/zip/ghost-0.4.2.zip 7 | 8 | 解压文件: 9 | 10 | unzip ghost-*.zip -d ghost 11 | 12 | 设置文件权限: 13 | 14 | chmod -R 750 ghost 15 | 16 | 删除安装包: 17 | 18 | rm ghost-*.zip 19 | 20 | 安装依赖: 21 | 22 | npm install --production 23 | 24 | 修改配置文件: 25 | 26 | vi ghost/config.js 27 | 28 | 修改 `production.server` 段下: 29 | 30 | 注释掉: 31 | 32 | // host: '127.0.0.1', 33 | // port: '2368' 34 | 35 | 添加(补全你的用户名): 36 | 37 | socket: '/home/<用户名>/ghost.sock' 38 | 39 | 启动 Ghost: 40 | 41 | NODE_ENV=production forever start ghost/index.js 42 | 43 | 回到面板添加 Nginx 站点(补全你的用户名): 44 | 45 | * 域名:`<用户名>.rp3.rpvhost.net` 46 | * 类型:proxy (反向代理) 47 | * 源地址:`http://unix:/home/<用户名>/ghost.sock:/` 48 | -------------------------------------------------------------------------------- /WIKI/App/Typecho.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 首先你需要成功开通了套餐,然后开启 PHP-FPM, 修改 SSH 密码,MySQL 密码。 3 | 4 | 登录 SSH, 执行以下命令下载 Typecho, 请自行到官网查看最新版本的下载地址: 5 | 6 | wget https://github.com/typecho/typecho/releases/download/v0.9-13.12.12-release/0.9.13.12.12.-release.tar.gz 7 | 8 | 解压文件: 9 | 10 | tar zxvf *-release.tar.gz 11 | mv build typecho 12 | 13 | 设置文件权限: 14 | 15 | chmod -R 750 typecho 16 | 17 | 删除安装包: 18 | 19 | rm *-release.tar.gz 20 | 21 | 进入 MySQL 控制台(需要输入你的 MySQL 密码): 22 | 23 | mysql -p 24 | 25 | (在 MySQL 中) 创建数据库(补全你的用户名): 26 | 27 | CREATE DATABASE `<用户名>_typecho`; 28 | 29 | 回到面板添加 Nginx 站点(补全你的用户名): 30 | 31 | * 域名:`<用户名>.rp3.rpvhost.net` 32 | * 类型:fastcgi (PHP) 33 | * 根目录:`/home/<用户名>/typecho` 34 | 35 | 访问 `<用户名>.rp3.rpvhost.net`, 点击 `下一步`, 正确填写数据库名、数据库用户名(你的用户名)、密码,然后下一个页面中填写你的博客的基本信息,即可完成安装。 36 | 37 | ## 永久链接 38 | 在启用「永久链接」功能时,Typecho 会提示「重写功能检测失败, 请检查你的服务器设置」,请无视该提示,直接「如果你仍然想启用此功能, 请点击这里」即可。 39 | -------------------------------------------------------------------------------- /WIKI/App/WordPress.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 首先你需要成功开通了套餐,然后开启 PHP-FPM, 修改 SSH 密码,修改 MySQL 密码。 3 | 4 | 登录 SSH, 执行以下命令下载 WordPress, 请自行到官网查看最新版本的下载地址: 5 | 6 | wget http://cn.wordpress.org/wordpress-3.9-zh_CN.zip 7 | 8 | 解压文件: 9 | 10 | unzip wordpress-*.zip 11 | 12 | 设置文件权限: 13 | 14 | chmod -R 750 wordpress 15 | 16 | 删除安装包: 17 | 18 | rm wordpress-*.zip 19 | 20 | 进入 MySQL 控制台(需要输入你的 MySQL 密码): 21 | 22 | mysql -p 23 | 24 | (在 MySQL 中) 创建数据库(补全你的用户名): 25 | 26 | CREATE DATABASE `<用户名>_wordpress`; 27 | 28 | 回到面板添加 Nginx 站点(补全你的用户名): 29 | 30 | * 域名:`<用户名>.rp3.rpvhost.net` 31 | * 类型:fastcgi (PHP) 32 | * 根目录:`/home/<用户名>/wordpress` 33 | 34 | 访问 `<用户名>.rp3.rpvhost.net`, 点击 `创建配置文件`, 正确填写数据库名、数据库用户名(你的用户名)、密码,然后下一个页面中填写你的博客的基本信息,即可完成安装。 35 | -------------------------------------------------------------------------------- /WIKI/FAQ/Billing.md: -------------------------------------------------------------------------------- 1 | ## 财务问题 FAQ 2 | 3 | ### 目录 4 | 5 | * RP 主机实行怎样的付费模式? 6 | * RP 主机的不同节点有什么区别? 7 | * 我应该如何充值? 8 | * RP 主机支持随时退款么? 9 | * 如何申请免费试用? 10 | * 什么情况下账户会被强制删除? 11 | 12 | ### RP 主机实行怎样的付费模式? 13 | RP 主机采取预充值模式,你需要先充值到 RP 主机,再开通服务,系统会实时扣费,你也可以随时终止服务(会丢失数据)。 14 | RP 主机价格固定 10 元每月,24 元每季度,一个月即 30 天,一季度即 90 天。 15 | 16 | ### RP 主机的不同节点有什么区别? 17 | RP 主机的不同节点之间是完全独立的,包括域名和帐号等等,在不同的节点需要单独注册帐号。 18 | 不同节点的网络情况不尽相同,可以自行进行感受,因为中国范围很大,网络情况复杂,我们也不能给出哪个节点速度更快的答案。 19 | 20 | ### 我应该如何充值? 21 | 目前支持淘宝和比特币两种充值方式: 22 | 23 | * 淘宝 24 | 25 | 通过面板上的淘宝链接访问 RP 主机的淘宝店,拍下对应宝贝后付款即可。 26 | 购买时注意选择服务器节点选项,备注填写你在 RP 主机的用户名。 27 | 28 | 付款后无需以任何方式催促客服,一般淘宝支付距离充值成功会有 24 小时左右的延时。 29 | 淘宝显示发货后,即为充值成功,你可以在任意时间确认收货。 30 | 31 | * 比特币(推荐) 32 | 33 | 在面板上可以看到你专属的比特币付款地址,你可以直接向该地址发送比特币。 34 | 在经过一次确认后,系统会实时地,自动为你折算使用时间。 35 | 36 | 折算规则:按照实时人民币汇率进行折算,10 元每月。 37 | 若单次付款超过 25 元,即按照 25 元每季度的价格来折算。 38 | 39 | ### RP 主机支持随时退款么? 40 | 你可以随时通过创建工单的方式向客服申请退款,我们会收取 10% 的手续费,同时一切通过活动获得的余额均不参与退款。 41 | 42 | 我们可以退款到支付宝账户,或按照实时汇率退款到比特币账户。 43 | 44 | ### 如何申请免费试用? 45 | 你可以向客服创建一个工单来申请免费试用,你需要用 100 字来介绍一下你的个人信息,然后客服会酌情为你赠送一些余额。 46 | 47 | ### 什么情况下账户会被强制删除? 48 | 欠费超过 5 元人民币,或者欠费超过 15 天,服务将被强行停止,相关数据会被删除。 49 | -------------------------------------------------------------------------------- /WIKI/FAQ/Domain.md: -------------------------------------------------------------------------------- 1 | ## 域名问题 FAQ 2 | 3 | ### 目录 4 | 5 | * 如何绑定我的域名? 6 | * RP 主机需要备案么? 7 | * 我没有域名怎么办? 8 | * 一个域名可以被多个站点绑定么? 9 | 10 | ### 如何绑定我的域名? 11 | 你需要先在你的域名 DNS 服务提供商处,将域名以 CNAME 解析至 RP 主机的域名,然后即可在 RP 主机以该域名创建网站。 12 | 13 | ### RP 主机需要备案么? 14 | RP 主机的服务器均位于中国大陆之外,因此不需要备案。RP 主机也没有工信部的 IDC 经营许可证,因此不能帮助你进行备案。 15 | 16 | ### 我没有域名怎么办? 17 | RP 主机有无限的三级域名可供使用,即 `xxoo.rp3.rpvhost.net`, 其中 `rp3` 这个字段取决于具体节点名。 18 | 19 | ### 一个域名可以被多个站点绑定么? 20 | 不可以,每个域名只能被一个网站绑定,如果其他人绑定了属于你的域名,请联系客服,客服会帮助你解决纠纷。 21 | -------------------------------------------------------------------------------- /WIKI/FAQ/Support.md: -------------------------------------------------------------------------------- 1 | ## 用户支持 FAQ 2 | 3 | ### 目录 4 | 5 | * RP 主机都提供哪些用户支持渠道? 6 | 7 | ### RP 主机都提供哪些用户支持渠道? 8 | 9 | * 工单系统 10 | 11 | [提问的智慧(外链)](http://wiki.woodpecker.org.cn/moin/AskForHelp) 12 | 13 | * 用户 QQ 群 14 | 15 | 群号:12959991 16 | 加入之前请在你的面板个人资料中填写 QQ 号,加群时备注节点和用户名。 17 | 18 | * 电子邮件 19 | 20 | 地址:admins@rpvhost.net 21 | 建议仅在工单系统无法使用的情况下使用邮件。 22 | -------------------------------------------------------------------------------- /WIKI/Linux/Filesystem.md: -------------------------------------------------------------------------------- 1 | ## 文件系统 2 | 3 | ### 用户目录 4 | 在 RP 主机上,你能够修改的文件仅限于你的 home 目录,即 `/home/user`. 5 | 6 | 为了保护你的文件不被其他人访问,请将文件权限设置为 750 或更低的权限。 7 | 8 | ### Unix Socket 9 | 在 RP 主机上,基于 TCP 端口的网络是不安全的,意味着其他用户也可以访问你建立的服务(如 Memcached, MongoDB). 10 | 推荐使用 Unix Socket 来创建服务,因为 Unix Socket 基于文件系统的权限,你可以灵活地设置它的权限,阻止其他用户访问。 11 | 12 | RP 主机自带的 PHP-FPM, Memcached, Redis 均通过 Unix Socket 提供服务;如果你也想使用 Unix Socket 的话,注意要将文件权限设置为 770. 13 | -------------------------------------------------------------------------------- /WIKI/Linux/SSH.md: -------------------------------------------------------------------------------- 1 | ## SSH 2 | 在 RP 主机上,每个用户都表现为一个标准的 Linux 帐号,SSH 将是你管理 RP 主机的主要方式,通过 SSH 你可以在 RP 主机上执行命令,运行程序,管理文件。 3 | 4 | ### SSH 客户端 5 | 6 | * Linux 和 OS X 均内置了 ssh 客户端,直接在终端运行 `ssh` 命令即可。 7 | * Windows 推荐下面两款客户端 8 | 9 | * [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) 10 | 11 | 开源,默认无中文支持。 12 | 13 | * [Xshell](http://www.netsarang.com/download/down_xsh.html) 14 | 15 | 对个人用户免费,有中文 UI. 16 | 17 | ### 登录到服务器 18 | 19 | 服务器即你所注册的节点的域名,如 `jp1.rpvhost.net`. 20 | 21 | 用户名即你在 RP 主机的用户名,SSH 密码需要在 RP 主机的 Web 管理面板上单独设置。 22 | 23 | 端口除了标准的 22 端口,还有 822, 722 两个备用端口可用(用于某些极端网络情况). 24 | 25 | ### 设置公钥登录 26 | 27 | 首先在本地通过 `ssh-keygen -t rsa` 生成密钥对,然后将你的公钥到 `~/.ssh/authorized_keys`, 以实现通过公钥验证来登录到服务器。 28 | 29 | 如果上传公钥后仍出现需要密码的情况,请确认相关文件的权限设置无误: 30 | 31 | * ~: 755 或更低 32 | * ~/.ssh: 755 或更低 33 | * ~/.ssh/authorized_keys: 644 或更低 34 | 35 | 通过在登录时,向 ssh 传递 `-vvT` 参数,可以获得一些进一步的帮助信息。 36 | -------------------------------------------------------------------------------- /WIKI/Nginx/JSON-Configure.md: -------------------------------------------------------------------------------- 1 | ## JSON 配置文件 2 | 这是一种 Nginx 配置文件的替代语法,每个片段对应一个站点,理论上支持 Nginx 的所有指令。 3 | 4 | ### 示例 5 | 6 | { 7 | "listen": 80, 8 | "is_enable": true, 9 | "server_name": [ 10 | "domain1.com", 11 | "domain2.net" 12 | ], 13 | "auto_index": false, 14 | "index": [ 15 | "index.php", 16 | "index.html" 17 | ], 18 | "root": "/home/user/web", 19 | "location": { 20 | "/": { 21 | "try_files": ["$uri", "$uri/", "/index.php?$args"] 22 | }, 23 | "~ \\.php$": { 24 | "fastcgi_pass": "unix:///home/user/phpfpm.sock", 25 | "fastcgi_index": ["index.php"], 26 | "include": "fastcgi_params" 27 | } 28 | } 29 | } 30 | 31 | ### 元素释义 32 | 33 | * Home 下的路径 34 | 35 | 即如果你的用户名为 user, 那么必须是以 `/home/user` 开头的路径,且其中不能含有相对路径(如 `..`). 36 | 37 | * 路径 38 | 39 | 必须为绝对路径。 40 | 41 | * 域名 42 | 43 | 类似于 `xxoo.net`, `sub-domain.xxoo.com`, `localhost` 等;不能有连续的符号(如 `sub..domain.net`), 不能有中文等特殊符号。 44 | 45 | * 文件名 46 | 47 | 类似于 `file`, `index.html` 等;不能有斜杠,不能有除了点、连字符和下划线之外的特殊字符。 48 | 49 | * 布尔值 50 | 51 | true 和 false, 而不是字符串形式的 `"true"` 和 `"false"`. 52 | 53 | * Unix Socket 54 | 55 | 类似于 `unix:///home/user/phpfpm.sock`, 必须以 `unix://` 开头,后面是一个路径。 56 | 57 | ### 指令 58 | 59 | * listen 60 | 61 | * 该站点监听的端口号 62 | * 数字,必须指令 63 | * 只能为 80 64 | 65 | * is_enable 66 | 67 | * 是否启用该站点,RP 主机特有功能 68 | * 布尔值,默认 false 69 | 70 | * server_name 71 | 72 | * 该站点的域名 73 | * 字符串数组,必须指令 74 | * 每一项需为合法的域名,且未被其他站点使用 75 | 76 | * auto_index 77 | 78 | * 在没有首页文件时,是否显示文件列表 79 | * 布尔值,默认 false 80 | 81 | * index 82 | 83 | * 首页文件名 84 | * 字符串数组,默认 `["index.html"]` 85 | * 每一项必须为文件名 86 | 87 | * root 88 | 89 | * 站点根目录 90 | * 字符串,可选参数 91 | * 必须为 Home 下的路径 92 | 93 | * location 94 | 95 | * 站点路径匹配规则 96 | * 对象,默认 `{}` 97 | * 键名目前支持 `/`, `~ \.php$` 98 | 99 | * location - try_files 100 | 101 | * 尝试文件列表 102 | * 字符串数组,可选指令 103 | * 值目前支持 `$uri`, `$uri/`, `/index.php?$args` 104 | 105 | * location - include 106 | 107 | * 包含 Nginx 的默认配置文件 108 | * 字符串,可选指令 109 | * 值只能为 `fastcgi_params`, `uwsgi_params` 110 | 111 | * location - fastcgi_pass 112 | 113 | * 转发请求至 factcgi 服务器 114 | * 字符串,可选指令 115 | * 值必须是一个 Home 下的 Unix Socket 116 | 117 | * location - fastcgi_index 118 | 119 | * 设置 factcgi 的首页文件 120 | * 字符串数组,当出现 fastcgi_pass 时,默认为 `["index.php"]` 121 | * 每一项必须为文件名 122 | 123 | * location - uwsgi_pass 124 | 125 | * 转发请求至 uwsgi 服务器 126 | * 字符串,可选指令 127 | * 值必须是一个 Home 下的 Unix Socket 128 | 129 | * location - proxy_pass 130 | 131 | * 转发请求至 http 服务器 132 | * 字符串,可选指令 133 | * 值为一个 Home 下的 Unix Socket 或一个 URL 134 | 135 | * location - `proxy_set_header` 136 | 137 | * 在转发至 http 服务器时设置 HTTP 头 138 | * 对象,可选指令 139 | * 键名为 `Host` 时,值为一个域名或 `$host` 140 | 141 | * location - proxy_redirect 142 | 143 | * 在转发至 http 服务器时是否跟随 HTTP 重定向 144 | * 布尔值,当出现 fastcgi_pass 时默认 false 145 | -------------------------------------------------------------------------------- /WIKI/Runtime/PHP.md: -------------------------------------------------------------------------------- 1 | ## PHP-FPM 2 | 使用 PHP 环境时,请注意要在面板上开启 PHP-FPM. 3 | -------------------------------------------------------------------------------- /app.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | global.app = exports 4 | 5 | app.libs = 6 | _: require 'underscore' 7 | async: require 'async' 8 | bodyParser: require 'body-parser' 9 | child_process: require 'child_process' 10 | cookieParser: require 'cookie-parser' 11 | copy: require 'copy-to' 12 | csrf: require 'csrf' 13 | crypto: require 'crypto' 14 | depd: require 'depd' 15 | express: require 'express' 16 | fs: require 'fs' 17 | tmp: require 'tmp' 18 | harp: require 'harp' 19 | jade: require 'jade' 20 | markdown: require('markdown').markdown 21 | middlewareInjector: require 'middleware-injector' 22 | moment: require 'moment-timezone' 23 | mongoose: require 'mongoose' 24 | morgan: require 'morgan' 25 | nodemailer: require 'nodemailer' 26 | os: require 'os' 27 | path: require 'path' 28 | redis: require 'redis' 29 | redisStore: require 'connect-redis' 30 | request: require 'request' 31 | expressSession: require 'express-session' 32 | mongooseUniqueValidator: require 'mongoose-unique-validator' 33 | 34 | ObjectID: (require 'mongoose').Types.ObjectId 35 | 36 | ObjectId: (require 'mongoose').Schema.Types.ObjectId 37 | Mixed: (require 'mongoose').Schema.Types.Mixed 38 | 39 | {cookieParser, copy, crypto, bodyParser, depd, express, fs, harp, middlewareInjector, mongoose} = exports.libs 40 | {morgan, nodemailer, path, redis, _} = exports.libs 41 | 42 | app.logger = do -> 43 | unless process.env.NODE_ENV == 'test' 44 | return console 45 | 46 | return { 47 | log: -> 48 | error: console.error 49 | } 50 | 51 | app.package = require './package' 52 | app.deprecate = depd 'rootpanel' 53 | 54 | do -> 55 | config_path = path.join __dirname, 'config.coffee' 56 | 57 | unless fs.existsSync config_path 58 | app.deprecate 'config.coffee not found, copy sample config to ./config.coffee' 59 | fs.writeFileSync config_path, fs.readFileSync path.join __dirname, "./sample/core.config.coffee" 60 | 61 | fs.chmodSync config_path, 0o750 62 | 63 | config = require './config' 64 | 65 | do -> 66 | if fs.existsSync config.web.listen 67 | fs.unlinkSync config.web.listen 68 | 69 | session_key_path = path.join __dirname, 'session.key' 70 | 71 | unless fs.existsSync session_key_path 72 | fs.writeFileSync session_key_path, crypto.randomBytes(48).toString('hex') 73 | fs.chmodSync session_key_path, 0o750 74 | 75 | app.redis = redis.createClient 6379, '127.0.0.1', 76 | auth_pass: config.redis.password 77 | 78 | app.mailer = nodemailer.createTransport config.email.account 79 | app.express = express() 80 | 81 | app.config = config 82 | app.db = require './core/db' 83 | app.utils = require './core/utils' 84 | app.cache = require './core/cache' 85 | app.i18n = require './core/i18n' 86 | app.pluggable = require './core/pluggable' 87 | 88 | app.models = {} 89 | 90 | require './core/model/Account' 91 | require './core/model/Financials' 92 | require './core/model/CouponCode' 93 | require './core/model/Notification' 94 | require './core/model/SecurityLog' 95 | require './core/model/Ticket' 96 | 97 | app.templates = require './core/templates' 98 | app.billing = require './core/billing' 99 | app.middleware = require './core/middleware' 100 | app.notification = require './core/notification' 101 | 102 | unless process.env.NODE_ENV == 'test' 103 | app.express.use morgan 'dev' 104 | 105 | app.express.use bodyParser.json() 106 | app.express.use cookieParser() 107 | app.express.use middlewareInjector 108 | 109 | app.express.use app.middleware.errorHandling 110 | app.express.use app.middleware.session() 111 | app.express.use app.middleware.csrf() 112 | app.express.use app.middleware.authenticate 113 | app.express.use app.middleware.accountHelpers 114 | 115 | app.express.set 'views', path.join(__dirname, 'core/view') 116 | app.express.set 'view engine', 'jade' 117 | 118 | app.express.get '/locale/:language?', app.i18n.downloadLocales 119 | 120 | app.express.use '/account', require './core/router/account' 121 | app.express.use '/billing', require './core/router/billing' 122 | app.express.use '/ticket', require './core/router/ticket' 123 | app.express.use '/coupon', require './core/router/coupon' 124 | app.express.use '/admin', require './core/router/admin' 125 | app.express.use '/panel', require './core/router/panel' 126 | 127 | app.pluggable.initializePlugins() 128 | 129 | app.express.get '/', (req, res) -> 130 | unless res.headerSent 131 | res.redirect '/panel/' 132 | 133 | app.express.use harp.mount './core/static' 134 | 135 | exports.start = _.once -> 136 | app.express.listen config.web.listen, -> 137 | app.started = true 138 | 139 | if fs.existsSync config.web.listen 140 | fs.chmodSync config.web.listen, 0o770 141 | 142 | app.pluggable.selectHook(null, 'app.started').forEach (hook) -> 143 | hook.action() 144 | 145 | app.logger.log "RootPanel start at #{config.web.listen}" 146 | 147 | unless module.parent 148 | exports.start() 149 | -------------------------------------------------------------------------------- /bin/fix-permissions.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | child_process = require 'child_process' 4 | async = require 'async' 5 | fs = require 'fs' 6 | 7 | fs.readdir '/home', (err, files) -> 8 | throw err if err 9 | 10 | async.eachSeries files, (file, callback) -> 11 | async.parallel [ 12 | (callback) -> 13 | child_process.exec "sudo chown -R #{file}:#{file} /home/#{file}", (err) -> 14 | callback err 15 | 16 | (callback) -> 17 | child_process.exec "sudo chmod -R o-rwx /home/#{file}", (err) -> 18 | callback err 19 | 20 | ], (err) -> 21 | throw err if err 22 | console.log "finish chown/chmod for #{file}" 23 | callback() 24 | , -> 25 | process.exit 0 26 | -------------------------------------------------------------------------------- /bin/migrate.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | {MongoClient} = require 'mongodb' 4 | semver = require 'semver' 5 | async = require 'async' 6 | path = require 'path' 7 | fs = require 'fs' 8 | _ = require 'underscore' 9 | 10 | config = require '../config' 11 | 12 | {user, password, host, name} = config.mongodb 13 | mongodb_uri = "mongodb://#{user}:#{password}@#{host}/#{name}" 14 | 15 | latest_version = require('../package.json').version 16 | 17 | migrations = _.map fs.readdirSync("#{__dirname}/../migration/database"), (filename) -> 18 | return filename.match(/v(\d+\.\d+\.\d+)\.coffee/)[1] 19 | 20 | migrations.sort (a, b) -> 21 | if semver.gt a, b 22 | return 1 23 | 24 | if semver.lt a, b 25 | return -1 26 | 27 | return 0 28 | 29 | MongoClient.connect mongodb_uri, (err, db) -> 30 | throw err if err 31 | 32 | cOption = db.collection 'options' 33 | 34 | cOption.findOne 35 | key: 'db_version' 36 | , (err, db_version) -> 37 | unless db_version 38 | console.log 'Migration data not found' 39 | process.exit 0 40 | 41 | current_version = db_version.version 42 | 43 | async.eachSeries migrations, (migration, callback) -> 44 | if semver.gt(migration, current_version) and semver.lte(migration, latest_version) 45 | console.log "Running migration #{migration}..." 46 | 47 | require("#{__dirname}/../migration/database/v#{migration}.coffee") db, (err) -> 48 | return callback err if err 49 | 50 | db.collection('options').update 51 | key: 'db_version' 52 | , 53 | $set: 54 | version: migration 55 | , callback 56 | 57 | else 58 | callback() 59 | 60 | , (err) -> 61 | if err 62 | throw err 63 | else 64 | console.log 'Migration Finish' 65 | process.exit 0 66 | -------------------------------------------------------------------------------- /bin/reconfigure.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | require '../app' 4 | 5 | {async, fs, _} = app.libs 6 | {config, pluggable} = app 7 | {Account} = app.models 8 | 9 | for plugin in fs.readdirSync "#{__dirname}/../plugin" 10 | unless pluggable.plugins[plugin] 11 | pluggable.initializePlugin plugin 12 | 13 | Account.find 14 | 'billing.plans.0': 15 | $exists: true 16 | , (err, accounts) -> 17 | async.eachSeries accounts, (account, callback) -> 18 | original_account = account 19 | 20 | plans = _.filter account.billing.plans, (plan) -> 21 | return config.plans[plan] 22 | 23 | services = _.uniq _.flatten _.compact _.map plans, (plan) -> 24 | return config.plans[plan]?.services 25 | 26 | if _.isEqual(account.billing.services, services) and _.isEqual(account.billing.plans, plans) 27 | return callback() 28 | 29 | Account.findByIdAndUpdate account._id, 30 | $set: 31 | 'billing.plans': plans 32 | 'billing.services': services 33 | , (err, account) -> 34 | services = account.billing.services 35 | original_services = original_account.billing.services 36 | 37 | async.series [ 38 | (callback) -> 39 | async.each _.difference(services, original_services), (service_name, callback) -> 40 | console.log "#{account.username} enabled #{service_name}" 41 | 42 | async.each pluggable.selectHook(account, "service.#{service_name}.enable"), (hook, callback) -> 43 | hook.filter account, callback 44 | , callback 45 | , callback 46 | 47 | (callback) -> 48 | async.each _.difference(original_services, services), (service_name, callback) -> 49 | console.log "#{account.username} disabled #{service_name}" 50 | 51 | async.each pluggable.selectHook(account, "service.#{service_name}.disable"), (hook, callback) -> 52 | hook.filter account, callback 53 | , callback 54 | , callback 55 | ], callback 56 | 57 | , -> 58 | available_plugins = _.union config.plugin.available_extensions, config.plugin.available_services 59 | 60 | async.eachSeries available_plugins, (plugin_name, callback) -> 61 | console.log "Running reconfigure for #{plugin_name}..." 62 | filename = "#{__dirname}/../plugin/#{plugin_name}/reconfigure.coffee" 63 | 64 | unless fs.existsSync filename 65 | return callback() 66 | 67 | require(filename) -> 68 | callback() 69 | 70 | , -> 71 | console.log 'Reconfigure Finish' 72 | process.exit 0 73 | -------------------------------------------------------------------------------- /core/cache.coffee: -------------------------------------------------------------------------------- 1 | stringify = require 'json-stable-stringify' 2 | getParameterNames = require 'get-parameter-names' 3 | CounterCache = require 'counter-cache' 4 | _ = require 'underscore' 5 | 6 | config = require '../config' 7 | 8 | {redis} = app 9 | 10 | exports.counter = new CounterCache() 11 | 12 | exports.hashKey = (key) -> 13 | if _.isString key 14 | return "#{config.redis.prefix}:" + key 15 | else 16 | return "#{config.redis.prefix}:" + stringify key 17 | 18 | # @param key: string|object 19 | # @param setter(COMMAND(value, command_params...), key) 20 | # @param callback(value) 21 | exports.try = (key, setter, callback) -> 22 | original_key = key 23 | key = exports.hashKey key 24 | 25 | redis.get key, (err, value) -> 26 | if value != undefined and value != null 27 | try 28 | callback JSON.parse value 29 | catch e 30 | callback value 31 | 32 | else 33 | setter (value, command_params...) -> 34 | original_value = value 35 | 36 | if _.isObject value 37 | value = JSON.stringify value 38 | 39 | command = _.first getParameterNames setter 40 | command = exports[command.toUpperCase()] 41 | 42 | params = [key, value].concat command_params 43 | params.push -> 44 | callback original_value 45 | 46 | command.apply @, params 47 | 48 | , original_key 49 | 50 | exports.delete = (key, callback) -> 51 | redis.del exports.hashKey(key), -> 52 | callback() 53 | 54 | exports.SET = (key, value, callback) -> 55 | redis.set key, value, callback 56 | 57 | exports.SETEX = (key, value, seconds, callback) -> 58 | redis.setex key, seconds, value, callback 59 | -------------------------------------------------------------------------------- /core/db.coffee: -------------------------------------------------------------------------------- 1 | {config} = app 2 | {mongoose} = app.libs 3 | 4 | {user, password, host, name} = config.mongodb 5 | 6 | if user and password 7 | mongodb_uri = "mongodb://#{user}:#{password}@#{host}/#{name}" 8 | else 9 | mongodb_uri = "mongodb://#{host}/#{name}" 10 | 11 | mongoose.connect mongodb_uri 12 | 13 | mongoose.connection.on 'error', (err) -> 14 | console.error err if err 15 | 16 | mongoose.connection.on 'connected', -> 17 | cOption = mongoose.connection.db.collection 'options' 18 | 19 | cOption.findOne 20 | key: 'db_version' 21 | , (err, db_version) -> 22 | unless db_version 23 | cOption.insert 24 | key: 'db_version' 25 | version: app.package.version 26 | , -> 27 | 28 | module.exports = mongoose.connection 29 | -------------------------------------------------------------------------------- /core/i18n.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs' 3 | _ = require 'underscore' 4 | 5 | stringify = require 'json-stable-stringify' 6 | Negotiator = require 'negotiator' 7 | 8 | utils = require './utils' 9 | cache = require './cache' 10 | config = require '../config' 11 | 12 | i18n_data = {} 13 | 14 | for filename in fs.readdirSync "#{__dirname}/locale" 15 | language = path.basename filename, '.json' 16 | i18n_data[language] = require "#{__dirname}/locale/#{filename}" 17 | config.i18n.available_language = _.union config.i18n.available_language, [language] 18 | 19 | exports.loadForPlugin = (plugin) -> 20 | for filename in fs.readdirSync "#{__dirname}/../plugin/#{plugin.NAME}/locale" 21 | language = path.basename filename, '.json' 22 | i18n_data[language]['plugins'][plugin.NAME] = require "#{__dirname}/../plugin/#{plugin.NAME}/locale/#{filename}" 23 | config.i18n.available_language = _.union config.i18n.available_language, [language] 24 | 25 | exports.parseLanguageCode = parseLanguageCode = (language) -> 26 | [lang, country] = language.replace('-', '_').split '_' 27 | 28 | return { 29 | language: language 30 | lang: lang?.toLowerCase() 31 | country: country?.toUpperCase() 32 | } 33 | 34 | exports.calcLanguagePriority = (req) -> 35 | negotiator = new Negotiator req 36 | 37 | result = [] 38 | 39 | if req.cookies.language 40 | language_info = parseLanguageCode req.cookies.language 41 | 42 | result = _.union result, _.filter config.i18n.available_language, (i) -> 43 | return i.language == language_info.language 44 | 45 | result = _.union result, _.filter config.i18n.available_language, (i) -> 46 | return parseLanguageCode(i).lang == language_info.lang 47 | 48 | result = _.union result, _.filter config.i18n.available_language, (i) -> 49 | return parseLanguageCode(i).lang in negotiator.languages() 50 | 51 | result.push config.i18n.default_language 52 | 53 | result = _.union result, config.i18n.available_language 54 | 55 | return result 56 | 57 | exports.translateByLanguage = (name, language) -> 58 | return '' unless name 59 | 60 | keys = name.split '.' 61 | keys.unshift language 62 | 63 | result = i18n_data 64 | 65 | for item in keys 66 | if result[item] == undefined 67 | return undefined 68 | else 69 | result = result[item] 70 | 71 | return result 72 | 73 | exports.translate = (name, req) -> 74 | priority_order = exports.calcLanguagePriority req 75 | 76 | for language in priority_order 77 | result = exports.translateByLanguage name, language 78 | 79 | if result != undefined 80 | return result 81 | 82 | return name 83 | 84 | exports.getTranslator = (req) -> 85 | return (name, payload) -> 86 | result = exports.translate name, req 87 | 88 | if _.isObject payload 89 | for k, v of payload 90 | result = result.replace new RegExp("__#{k}__", 'g'), v 91 | 92 | return result 93 | 94 | exports.pickClientLocale = (req) -> 95 | cache_key = "client.locale:#{req.cookies['language']}/#{req.headers['accept-language']}" 96 | cached_result = cache.counter.get cache_key 97 | 98 | if cached_result 99 | return cached_result 100 | 101 | priority_order = exports.calcLanguagePriority req 102 | 103 | result = {} 104 | 105 | for language in priority_order 106 | result = _.extend result, i18n_data[language] 107 | 108 | cache.counter.set cache_key, result, NaN 109 | 110 | return result 111 | 112 | exports.clientLocaleHash = (req) -> 113 | return utils.sha256 stringify exports.pickClientLocale req 114 | 115 | exports.downloadLocales = (req, res) -> 116 | if req.params['language'] 117 | req.cookies['language'] = req.params['language'] 118 | 119 | res.json exports.pickClientLocale req 120 | -------------------------------------------------------------------------------- /core/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "RootPanel", 4 | "description": "Pluggable PaaS service development framework" 5 | }, 6 | "plans": { 7 | "sample": { 8 | "name": "Sample Plan", 9 | "description": "10 CNY per month, calculated by hour" 10 | }, 11 | "test": { 12 | "name": "Test Plan", 13 | "description": "Calculated by usages" 14 | } 15 | }, 16 | "common": { 17 | "error": "Error", 18 | "success": "Success", 19 | "save": "Save", 20 | "apply": "Apply", 21 | "change": "Modify", 22 | "charge": "Recharge", 23 | "enable": "Enable", 24 | "disable": "Disable", 25 | "time": "Time", 26 | "amount": "Amount", 27 | "actions": "Action", 28 | "details": "Detail", 29 | "type": "Type", 30 | "generate": "Generate", 31 | "id": "ID", 32 | "close": "Close", 33 | "create": "Create", 34 | "submit": "Submit" 35 | }, 36 | "languages": { 37 | "zh_CN": "Chinese", 38 | "en": "English", 39 | "auto": "Auto" 40 | }, 41 | "error_code": { 42 | "username_exist": "Username already exists", 43 | "email_exist": "Email already exists", 44 | "invalid_username": "Invalid username", 45 | "invalid_password": "Invalid password", 46 | "invalid_email": "Invalid email", 47 | "insufficient_balance": "Insufficient Balance" 48 | }, 49 | "account": { 50 | "": "Account", 51 | "register": "Register", 52 | "login": "Login", 53 | "username": "Username", 54 | "email": "Email", 55 | "password": "Password", 56 | "logout": "Logout", 57 | "preferences": "Preferences", 58 | "financials": "Financials" 59 | }, 60 | "panel": { 61 | "": "Panel", 62 | "overview": "Overview" 63 | }, 64 | "ticket": { 65 | "": "Tickets", 66 | "title": "Title", 67 | "status": "Status", 68 | "create": "Create", 69 | "reply": "Reply", 70 | "replies": "All Replies", 71 | "create_ticket": "Create Ticket", 72 | "create_reply": "Create Reply", 73 | "ticket_list": "Tickets List", 74 | "close_ticket": "Close", 75 | "finish_ticket": "Finish", 76 | "reopen_ticket": "Reopen", 77 | "creator": "Creator", 78 | "members": "Members" 79 | }, 80 | "ticket_status": { 81 | "closed": "Closed", 82 | "open": "Open", 83 | "pending": "Pending", 84 | "finish": "Finish", 85 | "related": "Related" 86 | }, 87 | "plan": { 88 | "": "Plan", 89 | "join": "Join", 90 | "leave": "Leave", 91 | "balance": "Balance", 92 | "currency": { 93 | "CNY": "CNY" 94 | } 95 | }, 96 | "admin": { 97 | "admin_panel": "Admin Area" 98 | }, 99 | "time": { 100 | "day": "Day" 101 | }, 102 | "notification_title": { 103 | "ticket": "__title__ | Ticket" 104 | }, 105 | "view": { 106 | "layout": { 107 | "navigation": "Extend" 108 | }, 109 | "account": { 110 | "password2": "Repeat", 111 | "already_register": "Already has account?", 112 | "no_account": "Has no account?", 113 | "password_inconsistent": "Passwords not matched" 114 | }, 115 | "preferences": { 116 | "options": "Options", 117 | "qq": "QQ", 118 | "coupon_code": "Coupon Code", 119 | "code": "Code", 120 | "update_password": "Update password", 121 | "original_password": "Original", 122 | "new_password": "New", 123 | "repeat_password": "Repeat", 124 | "update_email": "Update Email", 125 | "current_email": "Current Email", 126 | "new_email": "New Email" 127 | }, 128 | "financials": { 129 | "payment_log": "Payment History", 130 | "pay_method": "Pay Method", 131 | "billing_log": "Billing History" 132 | }, 133 | "admin": { 134 | "account_list": "Account lists", 135 | "coupon_code": "Coupon Code", 136 | "confirm_payment": "Confirm", 137 | "delete_account": "Delete Account", 138 | "expired": "Expired", 139 | "empty_expired_tips": "Never expired if empty", 140 | "available_times": "Available times", 141 | "count": "Amount", 142 | "meta": "Meta", 143 | "order_id": "Order Id" 144 | } 145 | }, 146 | "coupons": { 147 | "amount": { 148 | "message": "Cash: __amount__ __currency__" 149 | } 150 | }, 151 | "plugins": {} 152 | } 153 | -------------------------------------------------------------------------------- /core/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "RootPanel", 4 | "description": "插件化 PaaS 服务开发框架" 5 | }, 6 | "plans": { 7 | "sample": { 8 | "name": "示例", 9 | "description": "每月 10 元,按小时计费" 10 | }, 11 | "test": { 12 | "name": "测试", 13 | "description": "按使用量付费" 14 | } 15 | }, 16 | "common": { 17 | "error": "错误", 18 | "success": "成功", 19 | "save": "保存", 20 | "apply": "使用", 21 | "change": "修改", 22 | "charge": "充值", 23 | "enable": "启用", 24 | "disable": "禁用", 25 | "time": "时间", 26 | "amount": "金额", 27 | "actions": "操作", 28 | "details": "详情", 29 | "type": "类型", 30 | "generate": "生成", 31 | "id": "ID", 32 | "close": "关闭", 33 | "create": "创建", 34 | "submit": "提交" 35 | }, 36 | "languages": { 37 | "zh_CN": "简体中文", 38 | "en": "English", 39 | "auto": "自动" 40 | }, 41 | "error_code": { 42 | "username_exist": "用户名已存在", 43 | "email_exist": "邮箱已存在", 44 | "invalid_username": "用户名不符合要求", 45 | "invalid_password": "密码不符合要求", 46 | "invalid_email": "邮箱不符合要求", 47 | "insufficient_balance": "余额不足" 48 | }, 49 | "account": { 50 | "": "帐号", 51 | "register": "注册", 52 | "login": "登录", 53 | "username": "用户名", 54 | "email": "邮箱", 55 | "password": "密码", 56 | "logout": "注销", 57 | "preferences": "首选项", 58 | "financials": "财务" 59 | }, 60 | "panel": { 61 | "": "管理面板", 62 | "overview": "概况" 63 | }, 64 | "ticket": { 65 | "": "工单", 66 | "title": "标题", 67 | "status": "状态", 68 | "create": "创建", 69 | "reply": "回复", 70 | "replies": "全部回复", 71 | "create_ticket": "创建工单", 72 | "create_reply": "创建回复", 73 | "ticket_list": "工单列表", 74 | "close_ticket": "关闭", 75 | "finish_ticket": "完成", 76 | "reopen_ticket": "重新打开", 77 | "creator": "创建者", 78 | "members": "成员" 79 | }, 80 | "ticket_status": { 81 | "closed": "已关闭", 82 | "open": "开放的", 83 | "pending": "等待处理", 84 | "finish": "已完成", 85 | "related": "与我相关" 86 | }, 87 | "plan": { 88 | "": "付费方案", 89 | "join": "订购方案", 90 | "leave": "退订方案", 91 | "balance": "余额", 92 | "currency": { 93 | "CNY": "CNY" 94 | } 95 | }, 96 | "admin": { 97 | "admin_panel": "管理员面板" 98 | }, 99 | "time": { 100 | "day": "天" 101 | }, 102 | "notification_title": { 103 | "ticket": "__title__ | 工单" 104 | }, 105 | "view": { 106 | "layout": { 107 | "navigation": "展开导航" 108 | }, 109 | "account": { 110 | "password2": "重复", 111 | "already_register": "已有帐号?", 112 | "no_account": "还没有账户?", 113 | "password_inconsistent": "两次输入的密码不一致" 114 | }, 115 | "preferences": { 116 | "options": "信息和偏好", 117 | "qq": "QQ", 118 | "coupon_code": "兑换代码", 119 | "code": "代码", 120 | "update_password": "修改密码", 121 | "original_password": "原密码", 122 | "new_password": "新密码", 123 | "repeat_password": "重复密码", 124 | "update_email": "修改邮箱", 125 | "current_email": "当前邮箱", 126 | "new_email": "新邮箱" 127 | }, 128 | "financials": { 129 | "payment_log": "充值记录", 130 | "pay_method": "方式", 131 | "billing_log": "扣费记录" 132 | }, 133 | "admin": { 134 | "account_list": "用户列表", 135 | "coupon_code": "兑换代码", 136 | "confirm_payment": "确认充值", 137 | "delete_account": "删除账户", 138 | "expired": "过期时间", 139 | "empty_expired_tips": "留空表示无限制", 140 | "available_times": "可用次数", 141 | "count": "数量", 142 | "meta": "参数", 143 | "order_id": "订单号" 144 | } 145 | }, 146 | "coupons": { 147 | "amount": { 148 | "message": "代金券:__amount__ __currency__" 149 | } 150 | }, 151 | "plugins": {} 152 | } 153 | -------------------------------------------------------------------------------- /core/middleware.coffee: -------------------------------------------------------------------------------- 1 | {config} = app 2 | {_, expressSession, redisStore, path, fs, moment} = app.libs 3 | {Account} = app.models 4 | 5 | exports.errorHandling = (req, res, next) -> 6 | res.error = (name, param = {}, status = 400) -> 7 | res.status(status).json _.extend param, 8 | error: name 9 | 10 | next() 11 | 12 | exports.session = -> 13 | RedisStore = redisStore expressSession 14 | secret = fs.readFileSync(path.join __dirname, '../session.key').toString() 15 | 16 | return expressSession 17 | store: new RedisStore 18 | client: app.redis 19 | 20 | resave: false 21 | saveUninitialized: false 22 | secret: secret 23 | 24 | exports.csrf = -> 25 | csrf = app.libs.csrf() 26 | 27 | return (req, res, next) -> 28 | if req.path in _.pluck app.pluggable.selectHook(null, 'app.ignore_csrf'), 'path' 29 | return next() 30 | 31 | validator = -> 32 | unless req.method == 'GET' 33 | unless csrf.verify req.session.csrf_secret, req.body.csrf_token 34 | return res.error 'invalid_csrf_token', null, 403 35 | 36 | next() 37 | 38 | if req.session.csrf_secret 39 | return validator() 40 | else 41 | csrf.secret (err, secret) -> 42 | req.session.csrf_secret = secret 43 | req.session.csrf_token = csrf.create secret 44 | 45 | validator() 46 | 47 | exports.authenticate = (req, res, next) -> 48 | token_field = do -> 49 | if req.headers['x-token'] 50 | return req.headers['x-token'] 51 | else 52 | return req.cookies.token 53 | 54 | unless token_field 55 | return next() 56 | 57 | Account.authenticate token_field, (token, account) -> 58 | if token and token.type == 'full_access' 59 | req.token = token 60 | req.account = account 61 | 62 | next() 63 | 64 | exports.accountHelpers = (req, res, next) -> 65 | _.extend res, 66 | language: req.cookies.language ? config.i18n.default_language 67 | timezone: req.cookies.timezone ? config.i18n.default_timezone 68 | 69 | t: app.i18n.getTranslator req 70 | 71 | moment: -> 72 | if res.language and res.language != 'auto' 73 | return moment.apply(@, arguments).locale(res.language).tz(res.timezone) 74 | else if res.timezone 75 | return moment.apply(@, arguments).tz(res.timezone) 76 | else 77 | return moment.apply(@, arguments) 78 | 79 | _.extend req, 80 | res: res 81 | t: res.t 82 | 83 | _.extend res.locals, 84 | _: _ 85 | 86 | app: app 87 | req: req 88 | res: res 89 | config: config 90 | 91 | account: req.account 92 | 93 | t: res.t 94 | moment: res.moment 95 | 96 | selectHook: (name) -> 97 | return app.pluggable.selectHook req.account, name 98 | 99 | next() 100 | 101 | exports.requireAuthenticate = (req, res, next) -> 102 | if req.account 103 | next() 104 | else 105 | if req.method == 'GET' 106 | res.redirect '/account/login/' 107 | else 108 | res.error 'auth_failed', null, 403 109 | 110 | exports.requireAdminAuthenticate = (req, res, next) -> 111 | req.inject [exports.requireAuthenticate], -> 112 | unless 'root' in req.account.groups 113 | if req.method == 'GET' 114 | return res.status(403).end() 115 | else 116 | return res.error 'forbidden' 117 | 118 | next() 119 | 120 | exports.requireInService = (service_name) -> 121 | return (req, res, next) -> 122 | req.inject [exports.requireAuthenticate], -> 123 | unless service_name in req.account.billing.services 124 | return res.error 'not_in_service' 125 | 126 | next() 127 | -------------------------------------------------------------------------------- /core/model/CouponCode.coffee: -------------------------------------------------------------------------------- 1 | {utils, config} = app 2 | {_, ObjectId, mongoose, mongooseUniqueValidator} = app.libs 3 | 4 | CouponCode = mongoose.Schema 5 | code: 6 | required: true 7 | unique: true 8 | type: String 9 | 10 | expired: 11 | type: Date 12 | 13 | available_times: 14 | type: Number 15 | 16 | type: 17 | required: true 18 | type: String 19 | enum: ['amount'] 20 | 21 | meta: 22 | type: Object 23 | 24 | apply_log: [ 25 | account_id: 26 | required: true 27 | type: ObjectId 28 | ref: 'Account' 29 | 30 | created_at: 31 | type: Date 32 | default: Date.now 33 | ] 34 | 35 | CouponCode.plugin mongooseUniqueValidator, 36 | message: 'unique_validation_error' 37 | 38 | config.coupons_meta = coupons_meta = 39 | amount: 40 | validate: (account, coupon, callback) -> 41 | apply_log = _.find coupon.apply_log, (item) -> 42 | return item.account_id.toString() == account._id.toString() 43 | 44 | if apply_log 45 | return callback() 46 | 47 | coupon.constructor.findOne 48 | type: 'amount' 49 | 'meta.category': coupon.meta.category 50 | 'apply_log.account_id': account._id 51 | , (err, result) -> 52 | callback not result 53 | 54 | message: (req, coupon, callback) -> 55 | callback req.t 'coupons.amount.message', 56 | amount: coupon.meta.amount 57 | currency: req.t "plan.currency.#{config.billing.currency}" 58 | 59 | apply: (account, coupon, callback) -> 60 | account.incBalance coupon.meta.amount, 'deposit', 61 | type: 'coupon' 62 | order_id: coupon.code 63 | , callback 64 | 65 | # @param template: [expired], available_times, type, meta 66 | # @param callback(err, coupons) 67 | CouponCode.statics.createCodes = (template, count, callback) -> 68 | coupons = _.map _.range(0, count), -> 69 | return { 70 | code: utils.randomString 16 71 | expired: template.expired or null 72 | available_times: template.available_times 73 | type: template.type 74 | meta: template.meta 75 | apply_log: [] 76 | } 77 | 78 | @create coupons, callback 79 | 80 | CouponCode.methods.getMessage = (req, callback) -> 81 | coupons_meta[@type].message req, @, callback 82 | 83 | # @param callback(is_available) 84 | CouponCode.methods.validateCode = (account, callback) -> 85 | if @available_times <= 0 86 | return callback() 87 | 88 | coupons_meta[@type].validate account, @, callback 89 | 90 | CouponCode.methods.applyCode = (account, callback) -> 91 | if @available_times <= 0 92 | return callback true 93 | 94 | @update 95 | $inc: 96 | available_times: -1 97 | $push: 98 | apply_log: 99 | account_id: account._id 100 | created_at: new Date() 101 | , (err) => 102 | return callback err if err 103 | coupons_meta[@type].apply account, @, callback 104 | 105 | _.extend app.models, 106 | CouponCode: mongoose.model 'CouponCode', CouponCode 107 | -------------------------------------------------------------------------------- /core/model/Financials.coffee: -------------------------------------------------------------------------------- 1 | {pluggable} = app 2 | {_, ObjectId, mongoose} = app.libs 3 | 4 | Financials = mongoose.Schema 5 | account_id: 6 | required: true 7 | type: ObjectId 8 | ref: 'Account' 9 | 10 | type: 11 | required: true 12 | type: String 13 | enum: ['deposit', 'billing', 'usage_billing'] 14 | 15 | amount: 16 | required: true 17 | type: Number 18 | 19 | created_at: 20 | type: Date 21 | default: Date.now 22 | 23 | payload: 24 | type: Object 25 | 26 | _.extend app.models, 27 | Financials: mongoose.model 'Financials', Financials 28 | -------------------------------------------------------------------------------- /core/model/Notification.coffee: -------------------------------------------------------------------------------- 1 | {_, ObjectId, mongoose} = app.libs 2 | 3 | Notification = mongoose.Schema 4 | account_id: 5 | type: ObjectId 6 | ref: 'Account' 7 | 8 | group_name: 9 | type: String 10 | 11 | type: 12 | required: true 13 | type: String 14 | enum: ['payment_success', 'ticket_create', 'ticket_reply'] 15 | 16 | level: 17 | required: true 18 | type: String 19 | enum: ['notice', 'event', 'log'] 20 | 21 | created_at: 22 | type: Date 23 | default: Date.now 24 | 25 | payload: 26 | type: Object 27 | 28 | _.extend app.models, 29 | Notification: mongoose.model 'Notification', Notification 30 | -------------------------------------------------------------------------------- /core/model/SecurityLog.coffee: -------------------------------------------------------------------------------- 1 | {_, ObjectId, mongoose} = app.libs 2 | 3 | SecurityLog = mongoose.Schema 4 | account_id: 5 | required: true 6 | type: ObjectId 7 | ref: 'Account' 8 | 9 | type: 10 | required: true 11 | type: String 12 | enum: ['revoke_token', 'update_password', 'update_email', 'update_preferences'] 13 | 14 | created_at: 15 | type: Date 16 | default: Date.now 17 | 18 | payload: 19 | type: Object 20 | 21 | token: 22 | type: Object 23 | 24 | _.extend app.models, 25 | SecurityLog: mongoose.model 'SecurityLog', SecurityLog 26 | -------------------------------------------------------------------------------- /core/model/Ticket.coffee: -------------------------------------------------------------------------------- 1 | {models, logger} = app 2 | {Account} = app.models 3 | {_, ObjectId, mongoose, markdown, async} = app.libs 4 | 5 | process.nextTick -> 6 | {Account} = app.models 7 | 8 | Reply = mongoose.Schema 9 | account_id: 10 | required: true 11 | type: ObjectId 12 | ref: 'Account' 13 | 14 | created_at: 15 | type: Date 16 | default: Date.now 17 | 18 | content: 19 | required: true 20 | type: String 21 | 22 | content_html: 23 | type: String 24 | 25 | flags: 26 | type: Object 27 | 28 | _.extend app.models, 29 | Reply: mongoose.model 'Reply', Reply 30 | 31 | Ticket = mongoose.Schema 32 | account_id: 33 | required: true 34 | type: ObjectId 35 | ref: 'Account' 36 | 37 | created_at: 38 | type: Date 39 | default: Date.now 40 | 41 | updated_at: 42 | type: Date 43 | default: Date.now 44 | 45 | title: 46 | required: true 47 | type: String 48 | 49 | content: 50 | required: true 51 | type: String 52 | 53 | content_html: 54 | type: String 55 | 56 | status: 57 | required: true 58 | type: String 59 | enum: ['open', 'pending', 'finish', 'closed'] 60 | 61 | flags: 62 | type: Object 63 | 64 | members: [ 65 | ObjectId 66 | ] 67 | 68 | replies: [ 69 | mongoose.modelSchemas.Reply 70 | ] 71 | 72 | Ticket.pre 'save', (next) -> 73 | @content_html = markdown.toHTML @content 74 | next() 75 | 76 | Ticket.methods.createReply = (account, content, status, flags, callback) -> 77 | reply = new models.Reply 78 | account_id: account._id 79 | content: content 80 | content_html: markdown.toHTML content 81 | flags: flags 82 | 83 | reply.validate (err) => 84 | return callback err if err 85 | 86 | @replies.push reply 87 | @members.addToSet account._id 88 | @status = status 89 | @update_at = new Date() 90 | 91 | @save (err) -> 92 | callback err, reply 93 | 94 | Ticket.methods.hasMember = (account) -> 95 | for member in @members 96 | if member.equals account._id 97 | return true 98 | 99 | return false 100 | 101 | Ticket.methods.populateAccounts = (callback) -> 102 | accounts_id = _.uniq [@account_id].concat @members.concat _.pluck(@replies, 'account_id') 103 | 104 | async.map accounts_id, (account_id, callback) -> 105 | Account.findById account_id, callback 106 | 107 | , (err, accounts) => 108 | logger.error err if err 109 | 110 | accounts = _.indexBy _.compact(accounts), '_id' 111 | 112 | result = @toObject() 113 | 114 | result.account = accounts[result.account_id] 115 | 116 | result.members = _.map result.members, (member_id) -> 117 | return accounts[member_id] 118 | 119 | for reply in result.replies 120 | reply.account = accounts[reply.account_id] 121 | 122 | callback result 123 | 124 | _.extend app.models, 125 | Ticket: mongoose.model 'Ticket', Ticket 126 | -------------------------------------------------------------------------------- /core/notification.coffee: -------------------------------------------------------------------------------- 1 | {async, _} = app.libs 2 | {i18n, config, logger, mailer} = app 3 | {Account, Notification} = app.models 4 | 5 | {NOTICE, EVENT, LOG} = _.extend exports, 6 | NOTICE: 'notice' 7 | EVENT: 'event' 8 | LOG: 'log' 9 | 10 | exports.notices_level = notices_level = 11 | ticket_create: NOTICE 12 | ticket_reply: NOTICE 13 | ticket_update: EVENT 14 | 15 | exports.createNotice = (account, type, notice, callback) -> 16 | level = exports.notices_level[type] 17 | 18 | notification = new Notification 19 | account_id: account._id 20 | type: type 21 | level: level 22 | payload: notice 23 | 24 | notification.save -> 25 | app.mailer.sendMail 26 | from: config.email.send_from 27 | to: account.email 28 | subject: notice.title 29 | html: notice.body 30 | , -> 31 | callback notification 32 | 33 | exports.createGroupNotice = (group, type, notice, callback) -> 34 | level = exports.notices_level[type] 35 | 36 | notification = new Notification 37 | group_name: group 38 | type: type 39 | level: level 40 | payload: notice 41 | 42 | notification.save -> 43 | unless level == NOTICE 44 | callback notification 45 | 46 | Account.find 47 | groups: 'root' 48 | , (err, accounts) -> 49 | async.each accounts, (account, callback) -> 50 | app.mailer.sendMail 51 | from: config.email.send_from 52 | to: account.email 53 | subject: notice.title 54 | html: notice.body 55 | , callback 56 | , (err) -> 57 | logger.error err if err 58 | callback notification 59 | -------------------------------------------------------------------------------- /core/router/account.coffee: -------------------------------------------------------------------------------- 1 | {_, async, express} = app.libs 2 | {requireAuthenticate} = app.middleware 3 | {Account, SecurityLog} = app.models 4 | {config, utils, logger} = app 5 | 6 | module.exports = exports = express.Router() 7 | 8 | exports.get '/register', (req, res) -> 9 | res.render 'account/register' 10 | 11 | exports.get '/login', (req, res) -> 12 | res.render 'account/login' 13 | 14 | exports.get '/preferences', requireAuthenticate, (req, res) -> 15 | res.render 'account/preferences' 16 | 17 | exports.get '/session_info/', (req, res) -> 18 | response = 19 | csrf_token: req.session.csrf_token 20 | 21 | if req.account 22 | _.extend response, 23 | id: req.account.id 24 | username: req.account.username 25 | preferences: req.account.preferences 26 | 27 | res.json response 28 | 29 | exports.post '/register', (req, res) -> 30 | Account.register req.body, (err, account) -> 31 | return res.error utils.pickErrorName err if err 32 | 33 | account.createToken 'full_access', 34 | ip: req.headers['x-real-ip'] 35 | ua: req.headers['user-agent'] 36 | , (err, token) -> 37 | logger.error err if err 38 | 39 | res.cookie 'token', token.token, 40 | expires: new Date(Date.now() + config.account.cookie_time) 41 | 42 | res.json 43 | id: account._id 44 | 45 | exports.post '/login', (req, res) -> 46 | Account.search req.body.username, (account) -> 47 | unless account 48 | return res.error 'wrong_password' 49 | 50 | unless account.matchPassword req.body.password 51 | return res.error 'wrong_password' 52 | 53 | account.createToken 'full_access', 54 | ip: req.headers['x-real-ip'] 55 | ua: req.headers['user-agent'] 56 | , (err, token) -> 57 | logger.error err if err 58 | 59 | res.cookie 'token', token.token, 60 | expires: new Date Date.now() + config.account.cookie_time 61 | 62 | res.cookie 'language', account.preferences.language, 63 | expires: new Date Date.now() + config.account.cookie_time 64 | 65 | res.json 66 | id: account._id 67 | token: token 68 | 69 | exports.post '/logout', requireAuthenticate, (req, res) -> 70 | req.token.revoke -> 71 | req.account.createSecurityLog 'revoke_token', req.token, 72 | revoke_ip: req.headers['x-real-ip'] 73 | revoke_ua: req.headers['user-agent'] 74 | , (err) -> 75 | logger.error err if err 76 | 77 | res.clearCookie 'token' 78 | res.json {} 79 | 80 | exports.post '/update_password', requireAuthenticate, (req, res) -> 81 | unless req.account.matchPassword req.body.original_password 82 | return res.error 'wrong_password' 83 | 84 | unless utils.rx.password.test req.body.password 85 | return res.error 'invalid_password' 86 | 87 | req.account.updatePassword req.body.password, -> 88 | req.account.createSecurityLog 'update_password', req.token, {}, (err) -> 89 | logger.error err if err 90 | 91 | res.json {} 92 | 93 | exports.post '/update_email', requireAuthenticate, (req, res) -> 94 | unless req.account.matchPassword req.body.password 95 | return res.error 'wrong_password' 96 | 97 | unless utils.rx.email.test req.body.email 98 | return res.error 'invalid_email' 99 | 100 | req.account.email = req.body.email 101 | 102 | req.account.save (err) -> 103 | logger.error err if err 104 | 105 | req.account.createSecurityLog 'update_email', req.token, 106 | original_email: req.account.email 107 | email: req.body.email 108 | , (err) -> 109 | logger.error err if err 110 | 111 | res.json {} 112 | 113 | exports.post '/update_preferences', requireAuthenticate, (req, res) -> 114 | req.body = _.omit req.body, 'csrf_token' 115 | 116 | for k, v of req.body 117 | if k in ['qq', 'language', 'timezone'] 118 | req.account.preferences[k] = v 119 | req.account.markModified "preferences.#{k}" 120 | else 121 | return res.error 'invalid_field' 122 | 123 | req.account.save (err) -> 124 | logger.error err if err 125 | 126 | req.account.createSecurityLog 'update_preferences', req.token, 127 | original_preferences: _.pick.apply @, [req.account.preferences].concat _.keys(req.body) 128 | preferences: req.body 129 | , (err) -> 130 | logger.error err if err 131 | 132 | res.json {} 133 | -------------------------------------------------------------------------------- /core/router/admin.coffee: -------------------------------------------------------------------------------- 1 | {express, async, _} = app.libs 2 | {requireAdminAuthenticate} = app.middleware 3 | {Account, Ticket, Financials, CouponCode} = app.models 4 | {config, pluggable} = app 5 | 6 | module.exports = exports = express.Router() 7 | 8 | exports.use requireAdminAuthenticate 9 | 10 | exports.get '/', (req, res) -> 11 | Account.find {}, (err, accounts) -> 12 | async.map pluggable.selectHook(null, 'view.admin.sidebars'), (hook, callback) -> 13 | hook.generator req, (html) -> 14 | callback null, html 15 | 16 | , (err, sidebars_html) -> 17 | res.render 'admin', 18 | accounts: accounts 19 | sidebars_html: sidebars_html 20 | coupon_code_types: _.keys config.coupons_meta 21 | 22 | exports.get '/account_details', (req, res) -> 23 | Account.findById req.query.account_id, (err, account) -> 24 | res.json _.omit account.toObject(), 'password', 'password_salt', 'tokens', '__v' 25 | 26 | exports.get '/ticket', (req, res) -> 27 | LIMIT = 10 28 | 29 | async.parallel 30 | pending: (callback) -> 31 | Ticket.find 32 | status: 'pending' 33 | , null, 34 | sort: 35 | updated_at: -1 36 | , callback 37 | 38 | open: (callback) -> 39 | Ticket.find 40 | status: 'open' 41 | , null, 42 | sort: 43 | updated_at: -1 44 | limit: LIMIT 45 | , callback 46 | 47 | finish: (callback) -> 48 | Ticket.find 49 | status: 'finish' 50 | , null, 51 | sort: 52 | updated_at: -1 53 | limit: LIMIT 54 | , callback 55 | 56 | closed: (callback) -> 57 | Ticket.find 58 | status: 'closed' 59 | , null, 60 | sort: 61 | updated_at: -1 62 | limit: LIMIT 63 | , callback 64 | 65 | , (err, result) -> 66 | res.render 'ticket/list', result 67 | 68 | exports.post '/confirm_payment', (req, res) -> 69 | Account.findById req.body.account_id, (err, account) -> 70 | unless account 71 | return res.error 'account_not_exist' 72 | 73 | unless _.isFinite req.body.amount 74 | return res.error 'invalid_amount' 75 | 76 | account.incBalance req.body.amount, 'deposit', 77 | type: req.body.type 78 | order_id: req.body.order_id 79 | , (err) -> 80 | return res.error err if err 81 | res.json {} 82 | 83 | exports.post '/delete_account', (req, res) -> 84 | Account.findById req.body.account_id, (err, account) -> 85 | unless account 86 | return res.error 'account_not_exist' 87 | 88 | unless _.isEmpty account.billing.plans 89 | return res.error 'already_in_plan' 90 | 91 | unless account.billing.balance <= 0 92 | return res.error 'balance_not_empty' 93 | 94 | Account.findByIdAndRemove account._id, -> 95 | res.json {} 96 | 97 | exports.post '/generate_coupon_code', (req, res) -> 98 | coupon_code = _.pick req.body, 'expired', 'available_times', 'type', 'meta' 99 | 100 | CouponCode.createCodes coupon_code, req.body.count, (err, coupon_codes...) -> 101 | res.json coupon_codes 102 | -------------------------------------------------------------------------------- /core/router/billing.coffee: -------------------------------------------------------------------------------- 1 | {express, _} = app.libs 2 | {config, billing} = app 3 | {requireAuthenticate} = app.middleware 4 | {Account} = app.models 5 | 6 | module.exports = exports = express.Router() 7 | 8 | exports.use requireAuthenticate 9 | 10 | exports.post '/join_plan', (req, res) -> 11 | unless req.body.plan in _.keys(config.plans) 12 | return res.error 'invalid_plan' 13 | 14 | if req.body.plan in req.account.billing.plans 15 | return res.error 'already_in_plan' 16 | 17 | billing.triggerBilling req.account, (account) -> 18 | if account.billing.balance <= config.billing.force_freeze.when_balance_below 19 | return res.error 'insufficient_balance' 20 | 21 | billing.joinPlan req, account, req.body.plan, -> 22 | res.json {} 23 | 24 | exports.post '/leave_plan', (req, res) -> 25 | unless req.body.plan in req.account.billing.plans 26 | return res.error 'not_in_plan' 27 | 28 | billing.triggerBilling req.account, (account) -> 29 | billing.leavePlan req, account, req.body.plan, -> 30 | res.json {} 31 | -------------------------------------------------------------------------------- /core/router/coupon.coffee: -------------------------------------------------------------------------------- 1 | {_, express} = app.libs 2 | {requireAuthenticate} = app.middleware 3 | {CouponCode} = app.models 4 | {config, utils, logger} = app 5 | 6 | module.exports = exports = express.Router() 7 | 8 | exports.use requireAuthenticate 9 | 10 | exports.get '/info', (req, res) -> 11 | CouponCode.findOne 12 | code: req.query.code 13 | , (err, coupon) -> 14 | unless coupon 15 | return res.error 'code_not_exist' 16 | 17 | coupon.validateCode req.account, (is_available) -> 18 | unless is_available 19 | return res.error 'code_not_available' 20 | 21 | coupon.getMessage req, (message) -> 22 | res.json 23 | message: message 24 | 25 | exports.post '/apply', (req, res) -> 26 | CouponCode.findOne 27 | code: req.body.code 28 | , (err, coupon) -> 29 | unless coupon 30 | return res.error 'code_not_exist' 31 | 32 | if coupon.expired and Date.now() > coupon.expired.getTime() 33 | return res.error 'code_expired' 34 | 35 | if coupon.available_times and coupon.available_times < 0 36 | return res.error 'code_not_available' 37 | 38 | coupon.validateCode req.account, (is_available) -> 39 | unless is_available 40 | return res.error 'code_not_available' 41 | 42 | coupon.applyCode req.account, -> 43 | res.json {} 44 | -------------------------------------------------------------------------------- /core/router/panel.coffee: -------------------------------------------------------------------------------- 1 | {express, async, _} = app.libs 2 | {requireAuthenticate} = app.middleware 3 | {Account, Financials} = app.models 4 | {pluggable, billing, config} = app 5 | 6 | module.exports = exports = express.Router() 7 | 8 | exports.use requireAuthenticate 9 | 10 | exports.get '/financials', (req, res) -> 11 | LIMIT = 10 12 | 13 | async.parallel 14 | payment_methods: (callback) -> 15 | async.map pluggable.selectHook(req.account, 'billing.payment_methods'), (hook, callback) -> 16 | hook.widget_generator req, (html) -> 17 | callback null, html 18 | , callback 19 | 20 | deposit_log: (callback) -> 21 | Financials.find 22 | account_id: req.account._id 23 | type: 'deposit' 24 | , null, 25 | sort: 26 | created_at: -1 27 | limit: LIMIT 28 | , (err, deposit_logs) -> 29 | async.map deposit_logs, (deposit_log, callback) -> 30 | deposit_log = deposit_log.toObject() 31 | 32 | matched_hook = _.find pluggable.selectHook(req.account, 'view.pay.display_payment_details'), (hook) -> 33 | return hook?.type == deposit_log.payload.type 34 | 35 | unless matched_hook 36 | return callback null, deposit_log 37 | 38 | matched_hook.filter req, deposit_log, (payment_details) -> 39 | deposit_log.payment_details = payment_details 40 | callback null, deposit_log 41 | 42 | , callback 43 | 44 | billing_log: (callback) -> 45 | Financials.find 46 | account_id: req.account._id 47 | type: 48 | $in: ['billing', 'usage_billing'] 49 | , null, 50 | sort: 51 | created_at: -1 52 | limit: LIMIT 53 | , callback 54 | 55 | , (err, result) -> 56 | res.render 'panel/financials', result 57 | 58 | exports.get '/', (req, res) -> 59 | billing.triggerBilling req.account, (account) -> 60 | view_data = 61 | account: account 62 | plans: [] 63 | widgets_html: [] 64 | 65 | for name, info of config.plans 66 | view_data.plans.push _.extend _.clone(info), 67 | name: name 68 | is_enable: name in req.account.billing.plans 69 | 70 | async.map pluggable.selectHook(account, 'view.panel.widgets'), (hook, callback) -> 71 | hook.generator req, (html) -> 72 | callback null, html 73 | 74 | , (err, widgets_html) -> 75 | view_data.widgets_html = widgets_html 76 | 77 | res.render 'panel', view_data 78 | -------------------------------------------------------------------------------- /core/router/ticket.coffee: -------------------------------------------------------------------------------- 1 | {_, async, express} = app.libs 2 | 3 | {requireAuthenticate} = app.middleware 4 | {Account, Ticket} = app.models 5 | {config, notification, logger} = app 6 | 7 | module.exports = exports = express.Router() 8 | 9 | exports.use requireAuthenticate 10 | 11 | exports.param 'id', (req, res, next, id) -> 12 | Ticket.findById id, (err, ticket) -> 13 | logger.error err if err 14 | 15 | unless ticket 16 | return res.error 'ticket_not_exist', null, 404 17 | 18 | unless ticket.hasMember req.account 19 | unless req.account.inGroup 'root' 20 | return res.error 'forbidden', null, 403 21 | 22 | req.ticket = ticket 23 | 24 | next() 25 | 26 | exports.get '/list', (req, res) -> 27 | Ticket.find 28 | $or: [ 29 | account_id: req.account._id 30 | , 31 | members: req.account._id 32 | ] 33 | , null, 34 | sort: 35 | updated_at: -1 36 | , (err, tickets) -> 37 | logger.error err if err 38 | res.render 'ticket/list', 39 | tickets: tickets 40 | 41 | exports.get '/create', (req, res) -> 42 | res.render 'ticket/create' 43 | 44 | exports.get '/view/:id', (req, res) -> 45 | req.ticket.populateAccounts (ticket) -> 46 | res.render 'ticket/view', 47 | ticket: ticket 48 | 49 | exports.post '/create', (req, res) -> 50 | unless /^.+$/.test req.body.title 51 | return res.error 'invalid_title' 52 | 53 | Ticket.create 54 | account_id: req.account._id 55 | title: req.body.title 56 | content: req.body.content 57 | status: if req.account.inGroup 'root' then 'open' else 'pending' 58 | members: [req.account._id] 59 | , (err, ticket) -> 60 | logger.error err if err 61 | 62 | res.json 63 | id: ticket._id 64 | 65 | notification.createGroupNotice 'root', 'ticket_create', 66 | title: res.t 'notification_title.ticket', ticket 67 | body: _.template(app.templates['ticket_create_email']) 68 | t: res.t 69 | ticket: ticket 70 | account: req.account 71 | config: config 72 | , -> 73 | 74 | exports.post '/reply/:id', (req, res) -> 75 | {ticket} = req 76 | 77 | unless req.body.content 78 | return res.error 'invalid_content' 79 | 80 | status = if 'root' in req.account.groups then 'open' else 'pending' 81 | 82 | ticket.createReply req.account, req.body.content, status, {}, (err, reply) -> 83 | logger.error err if err 84 | 85 | res.json 86 | id: reply._id 87 | 88 | async.each ticket.members, (member_id, callback) -> 89 | if member_id.toString() == req.account._id.toString() 90 | return callback() 91 | 92 | Account.findOne 93 | _id: member_id 94 | , (err, account) -> 95 | notification.createNotice account, 'ticket_reply', 96 | title: res.t 'notification_title.ticket', ticket 97 | body: _.template(app.templates['ticket_reply_email']) 98 | t: res.t 99 | ticket: ticket 100 | reply: reply 101 | account: req.account 102 | config: config 103 | , -> 104 | callback() 105 | , -> 106 | 107 | exports.post '/update_status/:id', (req, res) -> 108 | {ticket} = req 109 | 110 | if req.account.inGroup 'root' 111 | allow_status = ['open', 'pending', 'finish', 'closed'] 112 | else 113 | allow_status = ['closed'] 114 | 115 | if req.body.status in allow_status 116 | if ticket.status == req.body.status 117 | return res.error 'already_in_status' 118 | else 119 | return res.error 'invalid_status' 120 | 121 | ticket.update 122 | $set: 123 | status: req.body.status 124 | , -> 125 | res.json {} 126 | -------------------------------------------------------------------------------- /core/static/script/account/login.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.action-login').click -> 3 | request '/account/login', 4 | username: $('.input-username').val() 5 | password: $('.input-password').val() 6 | , -> 7 | location.href = '/panel/' 8 | 9 | $('#password').keypress (e) -> 10 | if e.keyCode == 13 11 | $('.action-login').click() 12 | -------------------------------------------------------------------------------- /core/static/script/account/preferences.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.action-save').click -> 3 | request '/account/update_preferences', 4 | qq: $('.form-setting .input-qq').val() 5 | , -> 6 | alert t 'common.success' 7 | 8 | $('.action-use').click -> 9 | code = $('.form-coupon .input-coupon_code').val() 10 | 11 | request "/coupon/info?code=#{code}", {}, {method: 'get'}, (result) -> 12 | if window.confirm result.message 13 | request '/coupon/apply', 14 | code: code 15 | , -> 16 | alert t 'common.success' 17 | 18 | $('.action-update-password').click -> 19 | password = $('.form-password .input-password').val() 20 | password2 = $('.form-password .input-password2').val() 21 | 22 | if password != password2 23 | return alert t 'view.account.password_inconsistent' 24 | 25 | request '/account/update_password/', 26 | original_password: $('.form-password .input-original_password').val() 27 | password: password 28 | , -> 29 | alert t 'common.success' 30 | 31 | $('.action-update-email').click -> 32 | request '/account/update_email/', 33 | password: $('.form-email .input-password').val() 34 | email: $('.form-email .input-email').val() 35 | , -> 36 | alert t 'common.success' 37 | -------------------------------------------------------------------------------- /core/static/script/account/register.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.action-register').click -> 3 | username = $('.input-username').val() 4 | password = $('.input-password').val() 5 | password2 = $('.input-password2').val() 6 | email = $('.input-email').val() 7 | 8 | unless password == password2 9 | return alert t 'view.account.password_inconsistent' 10 | 11 | request '/account/register', 12 | username: username 13 | password: password 14 | email: email 15 | , -> 16 | location.href = '/panel/' 17 | -------------------------------------------------------------------------------- /core/static/script/admin.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('#tab-account-list .action-confirm-payment').click -> 3 | $('.confirm-payment-modal .input-account-id').html $(@).parents('tr').data 'id' 4 | $('.confirm-payment-modal').modal 'show' 5 | 6 | $('.action-delete-account').click (e) -> 7 | e.preventDefault() 8 | if window.confirm 'Are you sure?' 9 | request '/admin/delete_account', 10 | account_id: $(@).parents('tr').data 'id' 11 | , -> 12 | location.reload() 13 | 14 | $('.action-details').click -> 15 | request "/admin/account_details?account_id=#{$(@).parents('tr').data 'id'}", {}, {method: 'get'}, (account) -> 16 | $('.account-details-modal .label-account-id').text account._id 17 | $('.account-details-modal .label-details').html JSON.stringify account, null, ' ' 18 | $('.account-details-modal').modal 'show' 19 | 20 | $('.confirm-payment-modal .action-confirm-payment').click -> 21 | request '/admin/confirm_payment', 22 | account_id: $('.input-account-id').text() 23 | type: 'taobao' 24 | amount: parseFloat $('.input-amount').val() 25 | order_id: $('.input-order-id').val() 26 | , -> 27 | location.reload() 28 | 29 | $('.action-generate-code').click -> 30 | request '/admin/generate_coupon_code', 31 | expired: $('.input-expired').val() 32 | available_times: parseInt $('.input-available_times').val() 33 | type: $('.input-type').val() 34 | meta: JSON.parse $('.input-meta').val() 35 | count: parseInt $('.input-count').val() 36 | , (coupon_codes) -> 37 | for coupon_code in coupon_codes 38 | $('.output-coupon-code').append "#{coupon_code.code}
" 39 | -------------------------------------------------------------------------------- /core/static/script/layout.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $.ajaxSetup 3 | contentType: 'application/json; charset=UTF-8' 4 | 5 | window.i18n_data = {} 6 | 7 | window.t = (name) -> 8 | keys = name.split '.' 9 | 10 | result = window.i18n_data 11 | 12 | for item in keys 13 | if result[item] == undefined 14 | return name 15 | else 16 | result = result[item] 17 | 18 | if result == undefined 19 | return name 20 | else 21 | return result 22 | 23 | window.tErr = (name) -> 24 | return "error_code.#{name}" 25 | 26 | window.request = (url, param, options, callback) -> 27 | unless callback 28 | callback = options 29 | 30 | jQueryMethod = $[options.method ? 'post'] 31 | 32 | unless options.method == 'get' 33 | param.csrf_token = $('body').data 'csrf-token' 34 | param = JSON.stringify param 35 | else 36 | param = null 37 | 38 | jQueryMethod url, param 39 | .fail (jqXHR) -> 40 | if jqXHR.responseJSON?.error 41 | alert window.t "error_code.#{jqXHR.responseJSON.error}" 42 | else 43 | alert jqXHR.statusText 44 | .success callback 45 | 46 | client_version = localStorage.getItem 'locale_version' 47 | latest_version = $('body').data 'locale-version' 48 | 49 | if client_version == latest_version 50 | window.i18n_data = JSON.parse localStorage.getItem 'locale_cache' 51 | else 52 | $.getJSON "/locale/", (result) -> 53 | window.i18n_data = result 54 | 55 | localStorage.setItem 'locale_version', latest_version 56 | localStorage.setItem 'locale_cache', JSON.stringify result 57 | 58 | $('nav a').each -> 59 | if $(@).attr('href') == location.pathname 60 | $(@).parent().addClass('active') 61 | 62 | $('.label-language').text $.cookie('language') 63 | 64 | if window.location.hash == '#redirect' 65 | $('#site-not-exist').modal 'show' 66 | 67 | $('.action-logout').click (e) -> 68 | e.preventDefault() 69 | request '/account/logout', {}, -> 70 | location.href = '/' 71 | 72 | $('.action-switch-language').click (e) -> 73 | e.preventDefault() 74 | 75 | language = $(@).data 'language' 76 | 77 | $.cookie 'language', language, 78 | expires: 365 79 | path: '/' 80 | 81 | $('.label-language').text language 82 | 83 | if $('body').data 'username' 84 | request '/account/update_preferences/', 85 | language: language 86 | , -> 87 | location.reload() 88 | else 89 | location.reload() 90 | -------------------------------------------------------------------------------- /core/static/script/panel.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.action-leave-plan').click -> 3 | if window.confirm 'Are you sure?' 4 | request '/billing/leave_plan', 5 | plan: $(@).parents('tr').data 'name' 6 | , -> 7 | location.reload() 8 | 9 | $('.action-join-plan').click -> 10 | request '/billing/join_plan', 11 | plan: $(@).parents('tr').data 'name' 12 | , -> 13 | location.reload() 14 | -------------------------------------------------------------------------------- /core/static/script/ticket/create.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.action-create').click -> 3 | request '/ticket/create', 4 | title: $('.input-title').val() 5 | content: $('.input-content').val() 6 | , (result) -> 7 | location.href = "/ticket/view/#{result.id}" 8 | -------------------------------------------------------------------------------- /core/static/script/ticket/view.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | id = $('.row.content').data 'id' 3 | 4 | $('.action-reply').click -> 5 | request "/ticket/reply/#{id}", 6 | content: $('.input-content').val() 7 | , -> 8 | location.reload() 9 | 10 | $('.action-update-status').click -> 11 | request "/ticket/update_status/#{id}", 12 | status: $(@).data 'status' 13 | , -> 14 | location.reload() 15 | -------------------------------------------------------------------------------- /core/static/style/admin.less: -------------------------------------------------------------------------------- 1 | .tab-pane { 2 | form { 3 | margin-top: 50px; 4 | } 5 | 6 | .output-coupon-code { 7 | margin-top: 50px; 8 | } 9 | } 10 | 11 | pre { 12 | font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; 13 | } 14 | -------------------------------------------------------------------------------- /core/static/style/layout.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Ubuntu, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 4 | font-size: 16px; 5 | 6 | > header { 7 | background: #563d7c; 8 | 9 | nav.navbar { 10 | margin-bottom: 0; 11 | } 12 | 13 | nav.navbar-inverse { 14 | background: #563d7c; 15 | border: none; 16 | } 17 | 18 | .navbar-inverse { 19 | .navbar-brand, .navbar-nav > li > a { 20 | color: #fff; 21 | } 22 | 23 | .navbar-nav > .active > a, .navbar-nav > .active > a:hover, .navbar-nav > .active > a:focus { 24 | background-color: #463265; 25 | } 26 | } 27 | } 28 | 29 | #content { 30 | margin: 20px auto; 31 | 32 | header { 33 | margin-bottom: 10px; 34 | padding-bottom: 10px; 35 | border-bottom: 1px solid #ddd; 36 | font-size: 30px; 37 | } 38 | 39 | .row { 40 | margin-bottom: 35px; 41 | 42 | .form-group { 43 | .col-sm-offset-3 button { 44 | margin: 6px 12px; 45 | } 46 | 47 | .col-sm-9 { 48 | padding-left: 5px; 49 | } 50 | } 51 | } 52 | 53 | #sidebar { 54 | padding-left: 15px; 55 | 56 | .row { 57 | margin: 0 0 20px; 58 | 59 | header { 60 | font-size: 22px; 61 | } 62 | 63 | > a { 64 | margin: 0 10px; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | .progress { 72 | position: relative; 73 | } 74 | 75 | .progress span { 76 | position: absolute; 77 | display: block; 78 | width: 100%; 79 | color: black; 80 | } 81 | 82 | .btn { 83 | margin-right: 5px; 84 | } 85 | 86 | .navbar-inverse .navbar-nav > .open > a { 87 | background-color: #463265 !important; 88 | } 89 | -------------------------------------------------------------------------------- /core/static/style/panel.less: -------------------------------------------------------------------------------- 1 | .input-group { 2 | .btn { 3 | width: 100%; 4 | } 5 | } 6 | 7 | .table.plan-list tr :last-child { 8 | text-align: right; 9 | margin-right: 50px; 10 | } 11 | -------------------------------------------------------------------------------- /core/static/style/ticket.less: -------------------------------------------------------------------------------- 1 | .form-group.padding { 2 | padding: 6px 12px; 3 | } 4 | 5 | .img-avatar { 6 | width: 58px; 7 | height: 58px; 8 | } 9 | 10 | .list-group-item { 11 | a { 12 | margin-right: 10px; 13 | } 14 | 15 | .list-content { 16 | margin-left: 68px; 17 | 18 | .label { 19 | margin-right: 5px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/template/ticket_create_email.html: -------------------------------------------------------------------------------- 1 | <%= ticket.content_html %> 2 | 3 |
4 | 5 | Created by <%= account.username %> at <%= ticket.created_at %>, View ticket: 6 | <%= ticket._id %> 7 |
8 | Notice from <%= t(config.web.t_name) %>. 9 | -------------------------------------------------------------------------------- /core/template/ticket_reply_email.html: -------------------------------------------------------------------------------- 1 | <%= reply.content_html %> 2 | 3 |
4 | 5 | Replied by <%= account.username %> at <%= reply.created_at %>, View ticket: 6 | <%= ticket._id %> 7 |
8 | Notice from <%= t(config.web.t_name) %>. 9 | -------------------------------------------------------------------------------- /core/templates.coffee: -------------------------------------------------------------------------------- 1 | {fs, path} = app.libs 2 | 3 | template_data = {} 4 | 5 | for filename in fs.readdirSync "#{__dirname}/template" 6 | template_name = path.basename filename, path.extname(filename) 7 | template_data[template_name] = fs.readFileSync("#{__dirname}/template/#{filename}").toString() 8 | 9 | module.exports = template_data 10 | -------------------------------------------------------------------------------- /core/test/app.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'app', -> 2 | it 'should can startup', -> 3 | @timeout 20000 4 | require('../../app').start() 5 | 6 | it 'should connected to mongodb', (done) -> 7 | async.forever (callback) -> 8 | if app.db.readyState == 1 9 | callback true 10 | else 11 | setImmediate callback 12 | , -> 13 | done() 14 | 15 | it 'app.libs should be loaded', -> 16 | {_, express, fs, mongoose} = app.libs 17 | _.should.be.ok 18 | express.should.be.ok 19 | fs.should.be.ok 20 | mongoose.should.be.ok 21 | 22 | it 'app.logger should be available', -> 23 | app.logger.log.should.be.a 'function' 24 | app.logger.error.should.be.a 'function' 25 | 26 | it 'config.coffee should exists', -> 27 | fs.existsSync("#{__dirname}/../../config.coffee").should.be.ok 28 | 29 | it 'session.key should exists', -> 30 | fs.existsSync("#{__dirname}/../../session.key").should.be.ok 31 | 32 | it 'models should be available', -> 33 | {Account, Ticket} = app.models 34 | Account.find.should.be.a 'function' 35 | Ticket.find.should.be.a 'function' 36 | -------------------------------------------------------------------------------- /core/test/cache.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'cache', -> 2 | cache = null 3 | redis = null 4 | 5 | before -> 6 | {cache, redis} = app 7 | 8 | describe 'hashKey', -> 9 | it 'should success', -> 10 | cache.hashKey('cache_key').should.be.equal 'RP:cache_key' 11 | cache.hashKey({param: 'value'}).should.equal 'RP:{"param":"value"}' 12 | cache.hashKey({a: 'b', c: 'd', e: 2}).should.equal 'RP:{"a":"b","c":"d","e":2}' 13 | cache.hashKey({e: 2, a: 'b', c: 'd'}).should.equal 'RP:{"a":"b","c":"d","e":2}' 14 | 15 | describe 'try', -> 16 | it 'should success when cache not exist', (done) -> 17 | cache.try 'test_key', (SET, key) -> 18 | key.should.be.equal 'test_key' 19 | SET 'test_key_value' 20 | , (value) -> 21 | value.should.be.equal 'test_key_value' 22 | 23 | redis.get 'RP:test_key', (err, value) -> 24 | value.should.be.equal 'test_key_value' 25 | done err 26 | 27 | it 'should success when cache exist', (done) -> 28 | cache.try 'test_key', -> 29 | throw new Error 'should not be called' 30 | , (value) -> 31 | value.should.be.equal 'test_key_value' 32 | done() 33 | 34 | it 'should success with param', (done) -> 35 | cache.try 36 | key: 'test2' 37 | object_id: 10 38 | , (SET, key) -> 39 | key.object_id.should.be.equal 10 40 | SET 100 41 | , (value) -> 42 | value.should.be.equal 100 43 | done() 44 | 45 | it 'should success with JSON and SETEX', (done) -> 46 | cache.try 'test_key3', (SETEX) -> 47 | SETEX 48 | value_of: 'test_key3' 49 | , 60 50 | 51 | , (value) -> 52 | value.value_of.should.be.equal 'test_key3' 53 | 54 | redis.ttl 'RP:test_key3', (err, seconds) -> 55 | seconds.should.above 0 56 | cache.delete 'test_key3', done 57 | 58 | describe 'delete', -> 59 | it 'should success', (done) -> 60 | cache.delete 'test_key', -> 61 | redis.get 'RP:test_key', (err, value) -> 62 | expect(value).to.not.exist 63 | done() 64 | 65 | it 'should success with param', (done) -> 66 | cache.delete 67 | key: 'test2' 68 | object_id: 10 69 | , -> 70 | redis.get 71 | key: 'test2' 72 | object_id: 10 73 | , (err, value) -> 74 | expect(value).to.not.exist 75 | done() 76 | -------------------------------------------------------------------------------- /core/test/i18n.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'i18n', -> 2 | it 'pending' 3 | -------------------------------------------------------------------------------- /core/test/middleware.test.coffee: -------------------------------------------------------------------------------- 1 | express = require 'express' 2 | bodyParser = require 'body-parser' 3 | cookieParser = require 'cookie-parser' 4 | 5 | Account = null 6 | middleware = null 7 | 8 | describe 'middleware', -> 9 | before -> 10 | require '../../app' 11 | {middleware} = app 12 | {Account} = app.models 13 | 14 | describe 'errorHandling', -> 15 | it 'should work with param', (done) -> 16 | server = express() 17 | server.use middleware.errorHandling 18 | 19 | server.use (req, res) -> 20 | res.error 'error_name', 21 | message: 'error_message' 22 | 23 | supertest server 24 | .get '/' 25 | .expect 400 26 | .end (err, res) -> 27 | res.body.error.should.be.equal 'error_name' 28 | res.body.message.should.be.equal 'error_message' 29 | done err 30 | 31 | it 'should work with status code', (done) -> 32 | server = express() 33 | server.use middleware.errorHandling 34 | 35 | server.use (req, res) -> 36 | res.error 'error_name', null, 403 37 | 38 | supertest server 39 | .get '/' 40 | .expect 403 41 | .end (err, res) -> 42 | res.body.error.should.be.equal 'error_name' 43 | done err 44 | 45 | describe 'session', -> 46 | it 'session should be available', (done) -> 47 | server = express() 48 | server.use middleware.session() 49 | 50 | server.use (req, res, next) -> 51 | req.session.should.be.exist 52 | req.session.test_field = 'test_value' 53 | next() 54 | 55 | server.use (req, res) -> 56 | req.session.test_field.should.be.equal 'test_value' 57 | res.send() 58 | 59 | supertest server 60 | .get '/' 61 | .expect 200 62 | .end done 63 | 64 | describe 'csrf', -> 65 | server = express() 66 | agent = supertest.agent server 67 | token = null 68 | 69 | before -> 70 | server.use bodyParser.json() 71 | server.use cookieParser() 72 | server.use middleware.session() 73 | server.use middleware.errorHandling 74 | server.use middleware.csrf() 75 | 76 | server.use (req, res) -> 77 | req.session.csrf_token.should.be.exist 78 | res.json 79 | csrf_token: req.session.csrf_token 80 | 81 | it 'should ignore GET request', (done) -> 82 | agent.get '/' 83 | .expect 200 84 | .end (err, res) -> 85 | token = res.body.csrf_token 86 | done err 87 | 88 | it 'should reject with no token', (done) -> 89 | agent.post '/' 90 | .expect 403 91 | .end done 92 | 93 | it 'should success with token', (done) -> 94 | agent.post '/' 95 | .send 96 | csrf_token: token 97 | .expect 200 98 | .end done 99 | 100 | describe 'authenticate', -> 101 | it 'pending' 102 | 103 | describe 'accountHelpers', -> 104 | it 'pending' 105 | 106 | describe 'requireAuthenticate', -> 107 | it 'pending' 108 | 109 | describe 'requireAdminAuthenticate', -> 110 | it 'pending' 111 | 112 | describe 'requireInService', -> 113 | it 'pending' 114 | -------------------------------------------------------------------------------- /core/test/model/CouponCode.test.coffee: -------------------------------------------------------------------------------- 1 | after (done) -> 2 | app.models.CouponCode.remove 3 | _id: 4 | $in: created_objects.couponcodes 5 | , done 6 | 7 | describe 'model/CouponCode', -> 8 | Account = null 9 | CouponCode = null 10 | 11 | account = null 12 | coupon1 = null 13 | coupon2 = null 14 | coupon3 = null 15 | 16 | before -> 17 | {Account, CouponCode} = app.models 18 | {account} = namespace.accountModel 19 | 20 | after -> 21 | namespace.couponCodeModel = 22 | coupon3: coupon3 23 | 24 | describe 'createCodes', -> 25 | it 'should success', (done) -> 26 | CouponCode.createCodes 27 | available_times: 3 28 | type: 'amount' 29 | meta: 30 | category: 'test' 31 | amount: 4 32 | , 5, (err, coupons...) -> 33 | expect(err).to.not.exist 34 | coupons.should.have.length 5 35 | 36 | [coupon1, coupon2, coupon3] = coupons 37 | 38 | coupon1.available_times.should.be.equal 3 39 | coupon1.type.should.be.equal 'amount' 40 | coupon1.meta.amount.should.be.equal 4 41 | 42 | coupon1.code.should.not.equal coupon2.code 43 | 44 | for coupon in coupons 45 | created_objects.couponcodes.push coupon._id 46 | 47 | done() 48 | 49 | describe 'getMessage', -> 50 | it 'should success', (done) -> 51 | req = 52 | t: app.i18n.getTranslator 53 | headers: {} 54 | cookies: {} 55 | 56 | coupon1.getMessage req, (message) -> 57 | message.should.be.equal '代金券:4 CNY' 58 | done() 59 | 60 | describe 'applyCode', -> 61 | it 'should success', (done) -> 62 | coupon1.applyCode account, (err) -> 63 | expect(err).to.not.exist 64 | 65 | CouponCode.findById coupon1._id, (err, coupon1) -> 66 | coupon1.available_times.should.be.equal 2 67 | 68 | matched_account_id = _.find coupon1.apply_log, (item) -> 69 | return item.account_id.toString() == account.id 70 | 71 | matched_account_id.should.be.exist 72 | 73 | original_account = account 74 | Account.findById account._id, (err, account) -> 75 | (account.billing.balance - original_account.billing.balance).should.be.equal 4 76 | 77 | done() 78 | 79 | describe 'validateCode', -> 80 | it 'should success', (done) -> 81 | coupon2.validateCode {_id: ObjectId()}, (is_available) -> 82 | is_available.should.be.ok 83 | done() 84 | 85 | it 'should fail when used coupon', (done) -> 86 | coupon1.validateCode account, (is_available) -> 87 | expect(is_available).to.not.ok 88 | done() 89 | 90 | it 'should fail when available_times <= 0', (done) -> 91 | coupon2.available_times = 0 92 | coupon2.validateCode account, (is_available) -> 93 | expect(is_available).to.not.ok 94 | done() 95 | -------------------------------------------------------------------------------- /core/test/model/Financials.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'model/Financials', -> 2 | it 'pending' 3 | -------------------------------------------------------------------------------- /core/test/model/Notification.test.coffee: -------------------------------------------------------------------------------- 1 | after (done) -> 2 | app.models.Notification.remove 3 | account_id: 4 | $in: created_objects.accounts 5 | , done 6 | 7 | describe 'model/Notification', -> 8 | it 'pending' 9 | -------------------------------------------------------------------------------- /core/test/model/SecurityLog.test.coffee: -------------------------------------------------------------------------------- 1 | after (done) -> 2 | app.models.SecurityLog.remove 3 | account_id: 4 | $in: created_objects.accounts 5 | , done 6 | 7 | describe 'model/SecurityLog', -> 8 | it 'pending' 9 | -------------------------------------------------------------------------------- /core/test/model/Ticket.test.coffee: -------------------------------------------------------------------------------- 1 | after (done) -> 2 | app.models.Ticket.remove 3 | account_id: 4 | $in: created_objects.accounts 5 | , done 6 | 7 | describe 'model/Ticket', -> 8 | Ticket = null 9 | 10 | account = null 11 | ticket = null 12 | 13 | before -> 14 | {Ticket} = app.models 15 | {account} = namespace.accountModel 16 | 17 | it 'should render markdown before save', (done) -> 18 | Ticket.create 19 | account_id: account._id 20 | title: 'Title' 21 | content: '**CONTENT**' 22 | status: 'open' 23 | , (err, created_ticket) -> 24 | expect(err).to.not.exist 25 | ticket = created_ticket 26 | 27 | created_objects.tickets.push ticket._id 28 | 29 | ticket.title.should.be.equal 'Title' 30 | ticket.content_html.should.be.equal '

CONTENT

' 31 | 32 | done() 33 | 34 | describe 'createReply', -> 35 | it 'should success', (done) -> 36 | ticket.createReply account, '**REPLY**', 'pending', {}, (err, reply) -> 37 | expect(err).to.not.exist 38 | 39 | reply.content_html.should.be.equal '

REPLY

' 40 | reply.account_id.toString().should.be.equal account.id 41 | 42 | Ticket.findById ticket._id, (err, ticket) -> 43 | ticket.status.should.be.equal 'pending' 44 | ticket.replies.should.have.length 1 45 | done() 46 | 47 | describe 'hasMember', -> 48 | it 'should success', -> 49 | ticket.hasMember(account).should.be.ok 50 | 51 | describe 'populateAccounts', -> 52 | it 'should success', (done) -> 53 | ticket.populateAccounts (result) -> 54 | result.account.username.should.equal account.username 55 | result.members[0].username.should.equal account.username 56 | result.replies[0].account.username.should.equal account.username 57 | done() 58 | -------------------------------------------------------------------------------- /core/test/pluggable.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'pluggable', -> 2 | it 'pending' 3 | -------------------------------------------------------------------------------- /core/test/router/admin.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'router/admin', -> 2 | utils = null 3 | Account = null 4 | 5 | agent = null 6 | csrf_token = null 7 | account_id = null 8 | 9 | before -> 10 | {utils} = app 11 | {Account} = app.models 12 | agent = supertest.agent app.express 13 | 14 | it 'should create a admin account first', (done) -> 15 | username = "admin#{utils.randomString(10).toLowerCase()}" 16 | password = utils.randomString 20 17 | 18 | Account.register 19 | username: username 20 | email: "#{utils.randomString 20}@gmail.com" 21 | password: password 22 | , (err, admin) -> 23 | created_objects.accounts.push admin._id 24 | 25 | admin.groups.push 'root' 26 | admin.save -> 27 | agent.get '/account/session_info' 28 | .expect 200 29 | .end (err, res) -> 30 | csrf_token = res.body.csrf_token 31 | 32 | agent.post '/account/login' 33 | .send 34 | csrf_token: csrf_token 35 | username: username 36 | password: password 37 | .end (err, res) -> 38 | res.body.token.should.be.exist 39 | done err 40 | 41 | it 'should create a account for test', (done) -> 42 | Account.register 43 | username: "account#{utils.randomString(10).toLowerCase()}" 44 | email: "#{utils.randomString 20}@gmail.com" 45 | password: utils.randomString 20 46 | , (err, account) -> 47 | created_objects.accounts.push account._id 48 | account_id = account._id 49 | done err 50 | 51 | it 'GET / when no permission', (done) -> 52 | namespace.accountRouter.agent 53 | .get '/admin' 54 | .expect 403 55 | .end done 56 | 57 | it 'GET /', (done) -> 58 | agent.get '/admin' 59 | .expect 200 60 | .end done 61 | 62 | it 'GET ticket', (done) -> 63 | agent.get '/admin/ticket' 64 | .expect 200 65 | .end done 66 | 67 | it 'POST confirm_payment', (done) -> 68 | agent.post '/admin/confirm_payment' 69 | .send 70 | csrf_token: csrf_token 71 | account_id: account_id 72 | amount: 10 73 | order_id: 'ID' 74 | .expect 200 75 | .end done 76 | 77 | it 'POST confirm_payment with account_id not exist', (done) -> 78 | agent.post '/admin/confirm_payment' 79 | .send 80 | csrf_token: csrf_token 81 | account_id: '14534f8a3d9064cb116c315d' 82 | amount: 10 83 | order_id: 'ID' 84 | .expect 400 85 | .end (err, res) -> 86 | res.body.error.should.be.equal 'account_not_exist' 87 | done err 88 | 89 | it 'POST confirm_payment with invalid amount', (done) -> 90 | agent.post '/admin/confirm_payment' 91 | .send 92 | csrf_token: csrf_token 93 | account_id: account_id 94 | amount: '1x' 95 | .expect 400 96 | .end (err, res) -> 97 | res.body.error.should.be.equal 'invalid_amount' 98 | done err 99 | 100 | it 'POST delete_account', (done) -> 101 | Account.findByIdAndUpdate account_id, 102 | $set: 103 | 'billing.balance': 0 104 | , -> 105 | agent.post '/admin/delete_account' 106 | .send 107 | csrf_token: csrf_token 108 | account_id: account_id 109 | .expect 200 110 | .end (err) -> 111 | Account.findById account_id, (mongo_err, account) -> 112 | expect(mongo_err).to.not.exist 113 | expect(account).to.not.exist 114 | done err 115 | 116 | it 'POST generate_coupon_code', (done) -> 117 | agent.post '/admin/generate_coupon_code' 118 | .send 119 | csrf_token: csrf_token 120 | count: 2 121 | available_times: 3 122 | type: 'amount' 123 | meta: 124 | category: 'test' 125 | amount: 4 126 | .expect 200 127 | .end (err, res) -> 128 | res.body.should.have.length 2 129 | [coupon1, coupon2] = res.body 130 | 131 | coupon1.available_times.should.be.equal 3 132 | coupon1.type.should.be.equal 'amount' 133 | coupon1.meta.amount.should.be.equal 4 134 | 135 | coupon1.code.should.not.equal coupon2.code 136 | 137 | created_objects.couponcodes.push ObjectId coupon1._id 138 | created_objects.couponcodes.push ObjectId coupon2._id 139 | 140 | done err 141 | -------------------------------------------------------------------------------- /core/test/router/billing.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'router/billing', -> 2 | Account = null 3 | 4 | agent = null 5 | account_id = null 6 | csrf_token = null 7 | 8 | before -> 9 | {Account} = app.models 10 | {agent, csrf_token, account_id} = namespace.accountRouter 11 | 12 | it 'POST join_plan when balance = 0', (done) -> 13 | agent.post '/billing/join_plan' 14 | .send 15 | csrf_token: csrf_token 16 | plan: 'billing_test' 17 | .expect 400 18 | .end (err, res) -> 19 | res.body.error.should.be.equal 'insufficient_balance' 20 | done err 21 | 22 | it 'POST join_plan', (done) -> 23 | Account.findByIdAndUpdate account_id, 24 | $set: 25 | 'billing.balance': 10 26 | , -> 27 | agent.post '/billing/join_plan' 28 | .send 29 | csrf_token: csrf_token 30 | plan: 'billing_test' 31 | .expect 200 32 | .end done 33 | 34 | it 'POST join_plan when already joined', (done) -> 35 | agent.post '/billing/join_plan' 36 | .send 37 | csrf_token: csrf_token 38 | plan: 'billing_test' 39 | .expect 400 40 | .end (err, res) -> 41 | res.body.error.should.be.equal 'already_in_plan' 42 | done err 43 | 44 | it 'POST leave_plan', (done) -> 45 | agent.post '/billing/leave_plan' 46 | .send 47 | csrf_token: csrf_token 48 | plan: 'billing_test' 49 | .expect 200 50 | .end done 51 | -------------------------------------------------------------------------------- /core/test/router/coupon.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'router/coupon', -> 2 | agent = null 3 | coupon3 = null 4 | csrf_token = null 5 | 6 | before -> 7 | {agent, csrf_token} = namespace.accountRouter 8 | {coupon3} = namespace.couponCodeModel 9 | 10 | it 'GET info', (done) -> 11 | agent.get '/coupon/info' 12 | .query 13 | code: coupon3.code 14 | .expect 200 15 | .end (err, res) -> 16 | res.body.message.should.be.equal '代金券:4 CNY' 17 | done err 18 | 19 | it 'POST apply', (done) -> 20 | agent.post '/coupon/apply' 21 | .send 22 | csrf_token: csrf_token 23 | code: coupon3.code 24 | .expect 200 25 | .end done 26 | -------------------------------------------------------------------------------- /core/test/router/panel.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'router/panel', -> 2 | agent = null 3 | 4 | before -> 5 | {agent} = namespace.accountRouter 6 | 7 | it 'GET /', (done) -> 8 | agent.get '/panel' 9 | .expect 200 10 | .end done 11 | 12 | it 'GET pay', (done) -> 13 | agent.get '/panel/financials' 14 | .expect 200 15 | .end done 16 | -------------------------------------------------------------------------------- /core/test/router/ticket.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'router/ticket', -> 2 | agent = null 3 | csrf_token = null 4 | 5 | ticket_id = null 6 | 7 | before -> 8 | {agent, csrf_token} = namespace.accountRouter 9 | 10 | it 'GET create with not logged', (done) -> 11 | supertest app.express 12 | .get '/ticket/create' 13 | .redirects 0 14 | .expect 302 15 | .expect 'location', '/account/login/' 16 | .end done 17 | 18 | it 'GET create', (done) -> 19 | agent.get '/ticket/create' 20 | .expect 200 21 | .end done 22 | 23 | it 'POST create', (done) -> 24 | agent.post '/ticket/create' 25 | .send 26 | csrf_token: csrf_token 27 | title: 'Title' 28 | content: '**CONTENT**' 29 | .expect 200 30 | .end (err, res) -> 31 | ticket_id = res.body.id 32 | created_objects.tickets.push ObjectId ticket_id 33 | done err 34 | 35 | it 'GET list', (done) -> 36 | agent.get '/ticket/list' 37 | .expect 200 38 | .expect /Title/ 39 | .end done 40 | 41 | it 'GET view/:id', (done) -> 42 | agent.get "/ticket/view/#{ticket_id}" 43 | .expect 200 44 | .expect /

CONTENT<\/strong><\/p>/ 45 | .end done 46 | 47 | it 'GET view/:id when not exist', (done) -> 48 | agent.get '/ticket/view/14534f8a3d9064cb116c315d' 49 | .expect 404 50 | .end done 51 | 52 | it 'POST reply', (done) -> 53 | agent.post "/ticket/reply/#{ticket_id}" 54 | .send 55 | csrf_token: csrf_token 56 | content: '**REPLY**' 57 | .expect 200 58 | .end done 59 | 60 | it 'GET list when replied', (done) -> 61 | agent.get "/ticket/view/#{ticket_id}" 62 | .expect 200 63 | .expect /

CONTENT<\/strong><\/p>/ 64 | .expect /

REPLY<\/strong><\/p>/ 65 | .end done 66 | 67 | it 'POST update_status', (done) -> 68 | agent.post "/ticket/update_status/#{ticket_id}" 69 | .send 70 | csrf_token: csrf_token 71 | status: 'closed' 72 | .expect 200 73 | .end done 74 | 75 | it 'POST update_status with already closed', (done) -> 76 | agent.post "/ticket/update_status/#{ticket_id}" 77 | .send 78 | csrf_token: csrf_token 79 | status: 'closed' 80 | .expect 400 81 | .end done 82 | 83 | it 'POST update_status with no permission', (done) -> 84 | agent.post "/ticket/update_status/#{ticket_id}" 85 | .send 86 | csrf_token: csrf_token 87 | status: 'open' 88 | .expect 400 89 | .end done 90 | -------------------------------------------------------------------------------- /core/test/utils.test.coffee: -------------------------------------------------------------------------------- 1 | utils = require '../utils' 2 | 3 | describe 'utils', -> 4 | describe 'rx', -> 5 | it 'username', -> 6 | utils.rx.username.test('jysperm').should.be.ok 7 | utils.rx.username.test('jy_sperm').should.be.ok 8 | utils.rx.username.test('JYSPERM').should.not.be.ok 9 | utils.rx.username.test('s').should.not.be.ok 10 | utils.rx.username.test('root-panel').should.not.be.ok 11 | utils.rx.username.test('184300584').should.not.be.ok 12 | utils.rx.username.test('jysperm@gmail.com').should.not.be.ok 13 | 14 | it 'email', -> 15 | utils.rx.email.test('jysperm@gmail.com').should.be.ok 16 | utils.rx.email.test('').should.not.be.ok 17 | utils.rx.email.test('jysperm').should.not.be.ok 18 | utils.rx.email.test('jysperm@').should.not.be.ok 19 | utils.rx.email.test('@gmail.com').should.not.be.ok 20 | 21 | it 'password', -> 22 | utils.rx.password.test('passwd').should.be.ok 23 | utils.rx.password.test('').should.not.be.ok 24 | 25 | it 'domain', -> 26 | utils.rx.domain.test('jysperm.me').should.be.ok 27 | utils.rx.domain.test('*.jysperm.me').should.be.ok 28 | utils.rx.domain.test('www.jysperm.me').should.be.ok 29 | utils.rx.domain.test('0-ms.org').should.be.ok 30 | utils.rx.domain.test('localhost').should.be.ok 31 | utils.rx.domain.test('.jysperm.me').should.not.be.ok 32 | utils.rx.domain.test('-jysperm').should.not.be.ok 33 | utils.rx.domain.test('jy sperm').should.not.be.ok 34 | utils.rx.domain.test('jysperm.').should.not.be.ok 35 | 36 | it 'filename', -> 37 | utils.rx.filename.test('filename').should.be.ok 38 | utils.rx.filename.test('').should.not.be.ok 39 | utils.rx.filename.test('"filename').should.not.be.ok 40 | utils.rx.filename.test('file\name').should.not.be.ok 41 | 42 | it 'url', -> 43 | utils.rx.url.test('http://jysperm.me').should.be.ok 44 | utils.rx.url.test('https://jysperm.me/about').should.be.ok 45 | utils.rx.url.test('ssh://jysperm.me').should.not.be.ok 46 | 47 | it 'sha256', -> 48 | expect(utils.sha256()).to.be.not.exist 49 | sha256 = '0554af0347e02ce032a1c6a292ed7e704c734ce338e71b39e21a73fa9b4d8fea' 50 | utils.sha256('jysperm').should.be.equal sha256 51 | 52 | it 'md5', -> 53 | expect(utils.md5()).to.be.not.exist 54 | utils.md5('jysperm').should.be.equal 'ff42fce67bcd7dd060293d3cb42638ba' 55 | 56 | it 'randomSalt', -> 57 | random1 = utils.randomSalt() 58 | random2 = utils.randomSalt() 59 | 60 | random1.should.have.length 64 61 | random1.should.be.not.equal random2 62 | 63 | it 'randomString', -> 64 | random1 = utils.randomString 10 65 | random2 = utils.randomString 10 66 | random3 = utils.randomString 20 67 | 68 | random1.should.have.length 10 69 | random1.should.be.not.equal random2 70 | random3.should.have.length 20 71 | 72 | it 'hashPassword', -> 73 | sha256 = '016899230b83a136fea361680e3a0c687440cd866ae67448fa72b007b04269dc' 74 | utils.hashPassword('passwd', 'salt').should.be.equal sha256 75 | 76 | describe 'wrapAsync', -> 77 | it 'should work with basic usage', (done) -> 78 | func = (callback) -> 79 | callback 'result' 80 | 81 | utils.wrapAsync(func) (err, result) -> 82 | expect(err).to.be.not.exist 83 | result.should.be.equal 'result' 84 | 85 | done() 86 | 87 | it 'should work with async', (done) -> 88 | func = (callback) -> 89 | callback 'result' 90 | 91 | async.parallel [ 92 | utils.wrapAsync func 93 | utils.wrapAsync func 94 | ], (err, result) -> 95 | expect(err).to.be.not.exist 96 | result[0].should.be.equal 'result' 97 | result[1].should.be.equal 'result' 98 | 99 | done() 100 | 101 | describe 'pickErrorName', -> 102 | it 'should work with two errors', -> 103 | error = 104 | errors: 105 | email: 106 | message: 'invalid_email' 107 | 108 | username: 109 | message: 'invalid_username' 110 | 111 | expect(utils.pickErrorName(error) in [ 112 | 'invalid_email', 'invalid_username' 113 | ]).to.be.ok 114 | 115 | it 'should work with no error', -> 116 | expect(utils.pickErrorName({})).to.be.null 117 | expect(utils.pickErrorName({errors: {}})).to.be.null 118 | -------------------------------------------------------------------------------- /core/utils.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | _ = require 'underscore' 3 | 4 | exports.rx = 5 | username: /^[a-z][0-9a-z_]{2,23}$/ 6 | email: /^\w+([-+.]\w+)*@\w+([-+.]\w+)*$/ 7 | password: /^.+$/ 8 | domain: /^(\*\.)?[A-Za-z0-9]+(\-[A-Za-z0-9]+)*(\.[A-Za-z0-9]+(\-[A-Za-z0-9]+)*)*$/ 9 | filename: /^[A-Za-z0-9_\-\.]+$/ 10 | url: /^https?:\/\/[^\s;]*$/ 11 | 12 | exports.sha256 = (data) -> 13 | if data 14 | return crypto.createHash('sha256').update(data).digest('hex') 15 | else 16 | return null 17 | 18 | exports.md5 = (data) -> 19 | if data 20 | return crypto.createHash('md5').update(data).digest('hex') 21 | else 22 | return null 23 | 24 | exports.randomSalt = -> 25 | return exports.sha256 crypto.randomBytes 256 26 | 27 | exports.randomString = (length) -> 28 | char_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 29 | 30 | result = _.map _.range(0, length), -> 31 | return char_map.charAt Math.floor(Math.random() * char_map.length) 32 | 33 | return result.join '' 34 | 35 | exports.hashPassword = (password, password_salt) -> 36 | return exports.sha256 password_salt + exports.sha256(password) 37 | 38 | exports.wrapAsync = (func) -> 39 | return (callback) -> 40 | func (result) -> 41 | callback null, result 42 | 43 | exports.pickErrorName = (error) -> 44 | unless error and error.errors 45 | return null 46 | 47 | if _.isEmpty error.errors 48 | return null 49 | 50 | err = error.errors[_.first(_.keys(error.errors))] 51 | 52 | if err.message == 'unique_validation_error' 53 | return "#{err.path}_exist" 54 | 55 | return err.message 56 | -------------------------------------------------------------------------------- /core/view/account/login.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | prepend header 4 | title #{t('account.login')} | #{t(config.web.t_name)} 5 | 6 | block main 7 | header= t('account.login') 8 | form.form-horizontal 9 | .form-group 10 | label.col-sm-2.col-md-offset-1.control-label= t('account.username') 11 | .col-sm-5 12 | input.input-username.form-control(type='text', required) 13 | .form-group 14 | label.col-sm-2.col-md-offset-1.control-label= t('account.password') 15 | .col-sm-5 16 | input.input-password.form-control(type='password') 17 | .form-group 18 | .col-sm-offset-3 19 | button.action-login.btn.btn-lg.btn-primary(type='button')= t('account.login') 20 | 21 | prepend sidebar 22 | .row 23 | header= t('view.account.no_account') 24 | a.btn.btn-lg.btn-success(href='/account/register/')= t('account.register') 25 | 26 | append footer 27 | script(src='/script/account/login.js') 28 | -------------------------------------------------------------------------------- /core/view/account/preferences.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | prepend header 4 | title #{t('account.preferences')} | #{t(config.web.t_name)} 5 | 6 | block main 7 | .row 8 | header= t('view.preferences.options') 9 | form.form-setting.form-horizontal 10 | .form-group 11 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.qq') 12 | .col-sm-5 13 | input.input-qq.form-control(type='text', value=account.preferences.qq) 14 | .form-group 15 | .col-sm-offset-3 16 | button.action-save.btn.btn-lg.btn-success(type='button')= t('common.save') 17 | 18 | .row 19 | header= t('view.preferences.coupon_code') 20 | form.form-coupon.form-horizontal 21 | .form-group 22 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.code') 23 | .col-sm-5 24 | input.input-coupon_code.form-control(type='text') 25 | .form-group 26 | .col-sm-offset-3 27 | button.action-use.btn.btn-lg.btn-success(type='button')= t('common.apply') 28 | 29 | .row 30 | header= t('view.preferences.update_password') 31 | form.form-password.form-horizontal 32 | .form-group 33 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.original_password') 34 | .col-sm-5 35 | input.input-original_password.form-control(type='password') 36 | .form-group 37 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.new_password') 38 | .col-sm-5 39 | input.input-password.form-control(type='password') 40 | .form-group 41 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.repeat_password') 42 | .col-sm-5 43 | input.input-password2.form-control(type='password') 44 | .form-group 45 | .col-sm-offset-3 46 | button.action-update-password.btn.btn-lg.btn-info(type='button')= t('common.change') 47 | 48 | .row 49 | header= t('view.preferences.update_email') 50 | form.form-email.form-horizontal 51 | .form-group 52 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.current_email') 53 | .col-sm-5 54 | input.form-control(value=account.email, disabled) 55 | .form-group 56 | label.col-sm-2.col-md-offset-1.control-label= t('account.password') 57 | .col-sm-5 58 | input.input-password.form-control(type='password') 59 | .form-group 60 | label.col-sm-2.col-md-offset-1.control-label= t('view.preferences.new_email') 61 | .col-sm-5 62 | input.input-email.form-control(type='text') 63 | .form-group 64 | .col-sm-offset-3 65 | button.action-update-email.btn.btn-lg.btn-info(type='button')= t('common.change') 66 | 67 | 68 | append footer 69 | script(src='/script/account/preferences.js') 70 | -------------------------------------------------------------------------------- /core/view/account/register.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | prepend header 4 | title #{t('account.register')} | #{t(config.web.t_name)} 5 | 6 | block main 7 | header= t('account.register') 8 | form.form-horizontal.signup-form 9 | .form-group 10 | label.col-sm-2.col-md-offset-1.control-label= t('account.username') 11 | .col-sm-5 12 | input.input-username.form-control(type='text') 13 | .form-group 14 | label.col-sm-2.col-md-offset-1.control-label= t('account.email') 15 | .col-sm-5 16 | input.input-email.form-control(type='email') 17 | .form-group 18 | label.col-sm-2.col-md-offset-1.control-label= t('account.password') 19 | .col-sm-5 20 | input.input-password.form-control(type='password') 21 | .form-group 22 | label.col-sm-2.col-md-offset-1.control-label= t('view.account.password2') 23 | .col-sm-5 24 | input.input-password2.form-control(type='password') 25 | .form-group 26 | .col-sm-offset-3 27 | button.action-register.btn.btn-lg.btn-primary(type='button')= t('account.register') 28 | 29 | prepend sidebar 30 | .row 31 | header= t('view.account.already_register') 32 | a.btn.btn-lg.btn-success(href='/account/login/')= t('account.login') 33 | 34 | append footer 35 | script(src='/script/account/register.js') 36 | -------------------------------------------------------------------------------- /core/view/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | block header 6 | link(rel='stylesheet', href='http://cdn.staticfile.org/twitter-bootstrap/3.2.0/css/bootstrap.min.css') 7 | link(rel='stylesheet', href='/style/layout.css') 8 | for hook in selectHook('view.layout.styles') 9 | link(rel='stylesheet', href=hook.path) 10 | 11 | body(data-username="#{account ? account.username : ''}", data-locale-version=app.i18n.clientLocaleHash(req), data-csrf-token=req.session.csrf_token) 12 | header.navbar-fixed-top 13 | .container 14 | nav.navbar.navbar-default.navbar-inverse(role='navigation') 15 | .navbar-header 16 | button.navbar-toggle(type='button', data-toggle='collapse', data-target='#navbar-collapse') 17 | span.sr-only= t('view.layout.navigation') 18 | span.icon-bar 19 | span.icon-bar 20 | span.icon-bar 21 | a.navbar-brand(href='/')= t(config.web.t_name) 22 | #navbar-collapse.collapse.navbar-collapse 23 | ul.nav.navbar-nav 24 | for hook in selectHook('view.layout.menu_bar') 25 | if hook.target 26 | li 27 | a(href=hook.href, target=hook.target)= t(hook.t_body) 28 | else 29 | li 30 | a(href=hook.href)= t(hook.t_body) 31 | ul.nav.navbar-nav.navbar-right 32 | if account 33 | li 34 | a(href='/account/preferences/')= account.username 35 | li 36 | a(href='/panel/')= t('panel.') 37 | if account.inGroup('root') 38 | li 39 | a(href='/admin/')= t('admin.admin_panel') 40 | li 41 | a.action-logout(href='/account/logout/')= t('account.logout') 42 | else 43 | li 44 | a(href='/account/register/')= t('account.register') 45 | li 46 | a(href='/account/login/')= t('account.login') 47 | li.dropdown 48 | a(href='#', data-toggle='dropdown').dropdown-toggle 49 | span.glyphicon.glyphicon-globe 50 | |   51 | span.label-language 52 | ul.dropdown-menu 53 | li 54 | a.action-switch-language(href='#', data-language='auto') #{t('languages.auto')} (auto) 55 | for language in config.i18n.available_language 56 | a.action-switch-language(href='#', data-language=language) #{t('languages.' + language)} (#{language}) 57 | 58 | block content 59 | #content.container 60 | .row 61 | .col-md-9 62 | block main 63 | 64 | #sidebar.col-md-3 65 | block sidebar 66 | .row 67 | header= t(config.web.t_name) 68 | ul 69 | li v#{app.package.version} 70 | li 71 | if config.web.repo.match(/^http/) 72 | a(href=config.web.repo, target='_blank') Source Code 73 | else 74 | a(href='https://github.com/#{config.web.repo}', target='_blank') Source on Github 75 | 76 | #footer 77 | if config.web.google_analytics_id 78 | script. 79 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 80 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 81 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 82 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 83 | ga('create', '#{config.web.google_analytics_id}', 'auto'); 84 | ga('send', 'pageview'); 85 | script(src='http://cdn.staticfile.org/jquery/2.0.3/jquery.min.js') 86 | script(src='http://cdn.staticfile.org/jquery-cookie/1.4.1/jquery.cookie.min.js') 87 | script(src='http://cdn.staticfile.org/underscore.js/1.7.0/underscore-min.js') 88 | script(src='http://cdn.staticfile.org/twitter-bootstrap/3.2.0/js/bootstrap.min.js') 89 | script(src='/script/layout.js') 90 | for hook in selectHook('view.layout.scripts') 91 | script(src=hook.path) 92 | block footer 93 | -------------------------------------------------------------------------------- /core/view/panel.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | prepend header 4 | title #{t('panel.')} | #{t(config.web.t_name)} 5 | 6 | append header 7 | link(rel='stylesheet', href='/style/panel.css') 8 | for hook in selectHook('view.panel.styles') 9 | link(rel='stylesheet', href=hook.path) 10 | 11 | block main 12 | .row 13 | header= t('account.financials') 14 | p #{t('plan.balance')}: #{account.billing.balance.toFixed(2)} #{t('plan.currency.' + config.billing.currency)} 15 | p 16 | a(href= '/panel/financials/').btn.btn-success= t('common.charge') 17 | 18 | .row 19 | header= t('plan.') 20 | table.table.table-hover.plan-list 21 | tbody 22 | for plan in plans 23 | tr(data-name='#{plan.name}') 24 | td 25 | strong= t(plan.t_name) 26 | td= t(plan.t_description) 27 | td 28 | if plan.is_enable 29 | button.action-leave-plan.btn.btn-danger.btn-sm= t('plan.leave') 30 | else 31 | button.action-join-plan.btn.btn-success.btn-sm= t('plan.join') 32 | 33 | for widget_html in widgets_html 34 | != widget_html 35 | 36 | prepend sidebar 37 | .row 38 | p 39 | a.btn.btn-lg.btn-success(href='/ticket/list/')= t('ticket.') 40 | p 41 | a.btn.btn-lg.btn-success(href='/account/preferences/')= t('account.preferences') 42 | 43 | append footer 44 | script(src='/script/panel.js') 45 | for hook in selectHook('view.panel.scripts') 46 | script(src=hook.path) 47 | -------------------------------------------------------------------------------- /core/view/panel/financials.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | prepend header 4 | title #{t('account.financials')} | #{t(config.web.t_name)} 5 | 6 | block main 7 | .row 8 | header= t('account.financials') 9 | 10 | for payment_method in payment_methods 11 | != payment_method 12 | 13 | .row 14 | header= t('view.financials.payment_log') 15 | table.table.table-hover 16 | thead 17 | tr 18 | th= t('common.time') 19 | th= t('common.amount') 20 | th= t('view.financials.pay_method') 21 | tbody 22 | for item in deposit_log 23 | tr 24 | td= moment(item.created_at).format('YYYY-MM-DD HH:mm:ss') 25 | td #{item.amount.toFixed(2)} #{t('plan.currency.' + config.billing.currency)} 26 | if item.payment_details 27 | td!= item.payment_details 28 | else 29 | td #{item.payload.type} #{item.payload.order_id} 30 | 31 | .row 32 | header= t('view.financials.billing_log') 33 | table.table.table-hover 34 | thead 35 | tr 36 | th= t('common.time') 37 | th= t('common.details') 38 | th= t('common.amount') 39 | tbody 40 | for item in billing_log 41 | tr 42 | td= moment(item.created_at).format('YYYY-MM-DD HH:mm:ss') 43 | td 44 | if item.type == 'billing' 45 | each info, plan_name in item.payload 46 | p #{plan_name}: #{info.billing_unit_count} unit before #{info.last_billing_at} 47 | else if item.type == 'usage_billing' && item.payload.service == 'shadowsocks' 48 | p #{item.payload.service}: #{item.payload.traffic_mb.toFixed(1)} MB 49 | td #{item.amount.toFixed(2)} CNY 50 | -------------------------------------------------------------------------------- /core/view/ticket/create.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | prepend header 4 | title #{t('ticket.create_ticket')} | #{t(config.web.t_name)} 5 | 6 | append header 7 | link(rel='stylesheet', href='/style/ticket.css') 8 | 9 | block main 10 | header= t('ticket.create_ticket') 11 | form.form-horizontal 12 | .form-group.padding 13 | input.input-title.form-control(type='text', placeholder= t('ticket.title')) 14 | .form-group.padding 15 | textarea.input-content.form-control(rows='15') 16 | .form-group.padding 17 | button.btn.btn-lg.btn-primary.action-create(type='button')= t('ticket.create') 18 | 19 | append footer 20 | script(src='/script/ticket/create.js') 21 | -------------------------------------------------------------------------------- /core/view/ticket/list.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | mixin displayTicketStatus(status) 4 | - l_status = t('ticket_status.' + status) 5 | 6 | if status == 'closed' 7 | span.text-muted= l_status 8 | else if status == 'open' 9 | span.text-primary= l_status 10 | else if status == 'pending' 11 | span.text-warning= l_status 12 | else if status == 'finish' 13 | span.text-success= l_status 14 | else 15 | | #{l_status} 16 | 17 | mixin displayTicketsTable(status, tickets) 18 | h4= t('ticket_status.' + status) 19 | 20 | table.table.table-hover 21 | thead 22 | tr 23 | th= t('ticket.title') 24 | th= t('ticket.status') 25 | tbody 26 | for ticket in tickets 27 | tr(data-id='#{ticket._id}') 28 | td 29 | a(href='/ticket/view/#{ticket._id}')= ticket.title 30 | td 31 | mixin displayTicketStatus(ticket.status) 32 | 33 | prepend header 34 | title #{t('ticket.ticket_list')} | #{t(config.web.t_name)} 35 | 36 | block main 37 | header= t('ticket.ticket_list') 38 | 39 | if tickets 40 | mixin displayTicketsTable('related', tickets) 41 | 42 | if pending && pending.length 43 | mixin displayTicketsTable('pending', pending) 44 | 45 | if open && open.length 46 | mixin displayTicketsTable('open', open) 47 | 48 | if finish && finish.length 49 | mixin displayTicketsTable('finish', finish) 50 | 51 | if closed && closed.length 52 | mixin displayTicketsTable('closed', closed) 53 | 54 | prepend sidebar 55 | .row 56 | if tickets 57 | a.btn.btn-lg.btn-success(href='/ticket/create/')= t('ticket.create_ticket') 58 | else 59 | a.btn.btn-lg.btn-success(href='/admin/ticket/')= t('ticket.ticket_list') 60 | -------------------------------------------------------------------------------- /core/view/ticket/view.jade: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | prepend header 4 | title #{ticket.title} | #{t(config.web.t_name)} 5 | 6 | append header 7 | link(rel='stylesheet', href='/style/ticket.css') 8 | 9 | block main 10 | .row.content(data-id='#{ticket._id}') 11 | header 12 | | #{ticket.title}   13 | 14 | - l_status = t('ticket_status.' + ticket.status) 15 | 16 | if ticket.status == 'closed' 17 | span.small.text-muted= l_status 18 | else if ticket.status == 'open' 19 | span.small.text-primary= l_status 20 | else if ticket.status == 'pending' 21 | span.small.text-warning= l_status 22 | else if ticket.status == 'finish' 23 | span.small.text-success= l_status 24 | 25 | p!= ticket.content_html 26 | 27 | .row 28 | header= t('ticket.replies') 29 | 30 | ul.list-group 31 | for reply in ticket.replies 32 | li.list-group-item.clearfix 33 | a.pull-left 34 | img.img-avatar(src= reply.account.preferences.avatar_url) 35 | .list-content 36 | p!= reply.content_html 37 | p 38 | span.label.label-info= reply.account.username 39 | span.label.label-default(title=reply.created_at)= moment(reply.created_at).fromNow() 40 | 41 | .row 42 | if ticket.status != 'closed' 43 | header= t('ticket.create_reply') 44 | 45 | form.form-horizontal 46 | if ticket.status != 'closed' 47 | .form-group.padding 48 | textarea.form-control.input-content(rows='5') 49 | .form-group.padding 50 | if ticket.status == 'closed' 51 | button(disabled).btn.btn-lg.btn-primary= t('ticket_status.closed') 52 | else 53 | button.btn.btn-lg.btn-primary.action-reply(type='button')= t('ticket.create_reply') 54 | button(type='button', data-status='closed').btn.btn-lg.btn-danger.action-update-status= t('ticket.close_ticket') 55 | 56 | if req.account.inGroup('root') && (ticket.status == 'open' || ticket.status == 'pending') 57 | button(type='button', data-status='finish').btn.btn-lg.btn-success.action-update-status= t('ticket.finish_ticket') 58 | if req.account.inGroup('root') && ticket.status == 'closed' 59 | button(type='button', data-status='open').btn.btn-lg.btn-success.action-update-status= t('ticket.reopen_ticket') 60 | 61 | prepend sidebar 62 | .row 63 | a.btn.btn-lg.btn-success(href='/ticket/list/')= t('ticket.ticket_list') 64 | 65 | .row 66 | if ticket.account 67 | header= t('ticket.creator') 68 | li.list-group-item.clearfix 69 | a.pull-left 70 | img.img-avatar(src=ticket.account.preferences.avatar_url) 71 | p 72 | span.label.label-info= ticket.account.username 73 | br 74 | span.label.label-default(title=ticket.created_at)= moment(ticket.created_at).fromNow() 75 | 76 | .row 77 | header= t('ticket.members') 78 | for member in ticket.members 79 | if member 80 | a.pull-left 81 | img.img-avatar(src=member.preferences.avatar_url, alt=member.username) 82 | 83 | append footer 84 | script(src='/script/ticket/view.js') 85 | -------------------------------------------------------------------------------- /migration/database/v0.6.0.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (db, callback) -> 2 | cAccount = db.collection 'accounts' 3 | 4 | cAccount.update 5 | 'tokens.available': 6 | $exists: true 7 | , 8 | $unset: 9 | 'tokens.$.available': true 10 | , 11 | multi: true 12 | , (err, rows) -> 13 | console.log "[accounts.tokens.available] update #{rows} rows" 14 | callback err 15 | -------------------------------------------------------------------------------- /migration/database/v0.7.1.coffee: -------------------------------------------------------------------------------- 1 | async = require 'async' 2 | crypto = require 'crypto' 3 | request = require 'request' 4 | 5 | config = require '../../config' 6 | 7 | genAddress = (bitcoin_secret, callback) -> 8 | request 'https://coinbase.com/api/v1/account/generate_receive_address', 9 | method: 'POST' 10 | json: 11 | api_key: config.plugins.bitcoin.coinbase_api_key 12 | address: 13 | callback_url: "#{config.web.url}/bitcoin/coinbase_callback?secret=#{bitcoin_secret}" 14 | , (err, res, body) -> 15 | throw err if err 16 | callback body.address 17 | 18 | module.exports = (db, callback) -> 19 | cAccount = db.collection 'accounts' 20 | 21 | cAccount.find().toArray (err, accounts) -> 22 | async.each accounts, (account, callback) -> 23 | bitcoin_secret = crypto.createHash('sha256').update(crypto.randomBytes(256)).digest('hex') 24 | 25 | genAddress bitcoin_secret, (address) -> 26 | cAccount.update {_id: account._id}, 27 | $set: 28 | 'attribute.bitcoin_deposit_address': address 29 | 'attribute.bitcoin_secret': bitcoin_secret 30 | , callback 31 | 32 | , (err) -> 33 | console.log "[account.attribute.bitcoin_secret] update #{accounts.length} rows" 34 | callback err 35 | -------------------------------------------------------------------------------- /migration/system/v0.7.1.md: -------------------------------------------------------------------------------- 1 | ## Core 2 | 3 | apt-get install supervisor 4 | npm install coffee-script -g 5 | 6 | vi /etc/rc.local 7 | 8 | iptables-restore < /etc/iptables.rules 9 | 10 | vi /etc/supervisor/conf.d/rpadmin.conf 11 | 12 | [program:RootPanel] 13 | command=node /home/rpadmin/RootPanel/start.js 14 | autorestart=true 15 | user=rpadmin 16 | 17 | service supervisor restart 18 | -------------------------------------------------------------------------------- /migration/system/v0.8.0.md: -------------------------------------------------------------------------------- 1 | ## Core 2 | 3 | vi /etc/rc.local 4 | 5 | ln -s /dev/xvda /dev/root 6 | 7 | vi /etc/supervisor/conf.d/rpadmin.conf 8 | 9 | [program:RootPanel] 10 | command = coffee /home/rpadmin/RootPanel/app.coffee 11 | directory = /home/rpadmin/RootPanel 12 | autorestart = true 13 | redirect_stderr = true 14 | user = rpadmin 15 | 16 | service supervisor restart 17 | 18 | ## shadowsocks 19 | 20 | rm /etc/shadowsocks/*.json 21 | mv /etc/supervisor/conf.d/rpadmin.conf rpadmin.conf 22 | rm /etc/supervisor/conf.d/*.conf 23 | mv rpadmin.conf /etc/supervisor/conf.d/rpadmin.conf 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rootpanel", 3 | "version": "0.8.0", 4 | "description": "A pluggable PaaS service development framework", 5 | "homepage": "https://rootpanel.io", 6 | "license": "AGPL-3.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/jysperm/RootPanel.git" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "jysperm", 14 | "email": "jysperm@gmail.com", 15 | "url": "https://jysperm.me" 16 | } 17 | ], 18 | "scripts": { 19 | "start": "./node_modules/.bin/coffee app.coffee", 20 | "migrate": "./node_modules/.bin/coffee bin/migrate.coffee", 21 | "reconfigure": "./node_modules/.bin/coffee bin/reconfigure.coffee", 22 | "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 core/test/*/*.test.coffee plugin/*/test", 23 | "test-only": "./node_modules/.bin/mocha --compilers coffee:coffee-script/register --require test/env -- core/test/*.test.coffee core/test/*/*.test.coffee plugin/*/test", 24 | "test-full": "./node_modules/.bin/coffee test/full-test.coffee", 25 | "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 core/test/*/*.test.coffee plugin/*/test > coverage-reporter.html" 26 | }, 27 | "dependencies": { 28 | "async": "^0.9.0", 29 | "body-parser": "^1.9.0", 30 | "coffee-script": "^1.7.1", 31 | "connect-redis": "^2.0.1", 32 | "cookie-parser": "^1.3.3", 33 | "copy-to": "^1.0.1", 34 | "counter-cache": "^0.1.0", 35 | "csrf": "^2.0.1", 36 | "deepmerge": "^0.2.7", 37 | "depd": "^1.0.0", 38 | "express": "^4.8.4", 39 | "express-session": "^1.8.2", 40 | "harp": "^0.13.0", 41 | "jade": "^1.3.1", 42 | "json-stable-stringify": "^1.0.0", 43 | "markdown": "^0.5.0", 44 | "middleware-injector": "^0.1.1", 45 | "moment-timezone": "^0.2.2", 46 | "mongodb": "^1.4.8", 47 | "mongoose": "^3.8.17", 48 | "mongoose-unique-validator": "^0.4.1", 49 | "morgan": "^1.3.2", 50 | "mysql": "^2.4.2", 51 | "negotiator": "^0.4.8", 52 | "nodemailer": "^1.2.1", 53 | "redis": "^0.12.1", 54 | "request": "^2.45.0", 55 | "tmp": "^0.0.24", 56 | "underscore": "^1.6.0", 57 | "semver": "^4.1.0", 58 | "get-parameter-names": "^0.2.0" 59 | }, 60 | "devDependencies": { 61 | "mocha": "^2.0.0", 62 | "chai": "^1.9.2", 63 | "coffee-coverage": "^0.4.2", 64 | "supertest": "^0.14.0", 65 | "mocha-reporter-cov-summary": "^0.1.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /plugin/bitcoin/bitcoin.coffee: -------------------------------------------------------------------------------- 1 | {request} = app.libs 2 | {config, cache, logger} = app 3 | 4 | # @param callback(address) 5 | exports.genAddress = (bitcoin_secret, callback) -> 6 | if config.plugins.bitcoin.coinbase_api_key == 'coinbase-simple-api-key' 7 | app.deprecate 'Invalid coinbase-simple-api-key' 8 | return callback() 9 | 10 | request 'https://coinbase.com/api/v1/account/generate_receive_address', 11 | method: 'POST' 12 | json: 13 | api_key: config.plugins.bitcoin.coinbase_api_key 14 | address: 15 | callback_url: "#{config.web.url}/bitcoin/coinbase_callback?secret=#{bitcoin_secret}" 16 | , (err, res, body) -> 17 | logger.error if err 18 | callback body.address 19 | 20 | # @param currency: CNY, USD, JPY etc. 21 | # @param callback(rate) 22 | exports.getExchangeRate = (currency, callback) -> 23 | cache.try 24 | key: 'bitcoin.getExchangeRate' 25 | currency: currency 26 | , (SETEX) -> 27 | request 'https://api.coinbase.com/v1/currencies/exchange_rates', (err, res, body) -> 28 | logger.error if err 29 | 30 | body = JSON.parse body 31 | rate = parseFloat body["#{currency.toLowerCase()}_to_btc"] 32 | 33 | SETEX rate, 600 34 | 35 | , callback 36 | -------------------------------------------------------------------------------- /plugin/bitcoin/index.coffee: -------------------------------------------------------------------------------- 1 | {jade, path} = app.libs 2 | {Account} = app.models 3 | {pluggable, config, utils} = app 4 | 5 | exports = module.exports = class BitcoinPlugin extends pluggable.Plugin 6 | @NAME: 'bitcoin' 7 | @type: 'extension' 8 | 9 | bitcoin = require './bitcoin' 10 | 11 | exports.registerHook 'account.before_register', 12 | filter: (account, callback) -> 13 | bitcoin_secret = utils.randomSalt() 14 | 15 | bitcoin.genAddress bitcoin_secret, (address) -> 16 | account.pluggable.bitcoin = 17 | bitcoin_deposit_address: address 18 | bitcoin_secret: bitcoin_secret 19 | 20 | callback() 21 | 22 | exports.registerHook 'billing.payment_methods', 23 | widget_generator: (req, callback) -> 24 | bitcoin.getExchangeRate config.billing.currency, (rate) -> 25 | exports.render 'payment_method', req, 26 | exchange_rate: rate 27 | , callback 28 | 29 | exports.registerHook 'view.pay.display_payment_details', 30 | type: 'bitcoin' 31 | filter: (req, deposit_log, callback) -> 32 | callback exports.t(req) 'view.payment_details', 33 | order_id: deposit_log.payload.order_id 34 | short_order_id: deposit_log.payload.order_id[0 .. 40] 35 | 36 | exports.registerHook 'app.ignore_csrf', 37 | path: '/bitcoin/coinbase_callback' 38 | 39 | app.express.post '/bitcoin/coinbase_callback', (req, res) -> 40 | Account.findOne 41 | 'pluggable.bitcoin.bitcoin_deposit_address': req.body.address 42 | , (err, account) -> 43 | unless account 44 | return res.send 400, 'Invalid Address' 45 | 46 | unless req.query.secret == account.pluggable.bitcoin.bitcoin_secret 47 | return res.send 400, 'Invalid Secret' 48 | 49 | bitcoin.getExchangeRate config.billing.currency, (rate) -> 50 | amount = req.body.amount / rate 51 | 52 | account.incBalance amount, 'deposit', 53 | type: 'bitcoin' 54 | order_id: req.body.transaction.hash 55 | , -> 56 | res.send 'Success' 57 | -------------------------------------------------------------------------------- /plugin/bitcoin/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Bitcoin", 3 | "view": { 4 | "payment_tips": "You can transfer directly to this address. system will automatically recharge for you after first confirmed. The minimum payment amount: 0.0005 BTC.", 5 | "exchange_rate_tips": "Real-time Exchange Rates: 10 CNY = __rate10__ BTC", 6 | "payment_details": "Bitcoin Payment __short_order_id__" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugin/bitcoin/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "比特币", 3 | "view": { 4 | "payment_tips": "你可以直接向该地址发送比特币。在经过一次确认后,系统会实时地,自动为你折算充值金额,最小支付金额为 0.0005 BTC.", 5 | "exchange_rate_tips": "实时汇率:10 CNY = __rate10__ BTC", 6 | "payment_details": "比特币支付 __short_order_id__" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugin/bitcoin/reconfigure.coffee: -------------------------------------------------------------------------------- 1 | {async} = app.libs 2 | {utils} = app 3 | {Account} = app.models 4 | 5 | bitcoin = require './bitcoin' 6 | 7 | module.exports = (callback) -> 8 | Account.find 9 | 'pluggable.bitcoin.bitcoin_deposit_address': 10 | $exists: false 11 | , (err, accounts) -> 12 | async.eachSeries accounts, (account, callback) -> 13 | bitcoin_secret = utils.randomSalt() 14 | 15 | console.log "create bitcoin_address for #{account.username}" 16 | 17 | bitcoin.genAddress bitcoin_secret, (address) -> 18 | account.update 19 | $set: 20 | 'pluggable.bitcoin.bitcoin_deposit_address': address 21 | 'pluggable.bitcoin.bitcoin_secret': bitcoin_secret 22 | , callback 23 | 24 | , (err) -> 25 | throw err if err 26 | callback() 27 | -------------------------------------------------------------------------------- /plugin/bitcoin/view/payment_method.jade: -------------------------------------------------------------------------------- 1 | div 2 | h2= t('') 3 | p= t('view.payment_tips') 4 | p= t('view.exchange_rate_tips', {rate10: (exchange_rate * 10).toFixed(4)}) 5 | pre= account.pluggable.bitcoin.bitcoin_deposit_address 6 | -------------------------------------------------------------------------------- /plugin/linux/index.coffee: -------------------------------------------------------------------------------- 1 | {async, _} = app.libs 2 | {pluggable, config} = app 3 | {requireAuthenticate} = app.middleware 4 | {wrapAsync} = app.utils 5 | 6 | exports = module.exports = class LinuxPlugin extends pluggable.Plugin 7 | @NAME: 'linux' 8 | @type: 'service' 9 | 10 | linux = require './linux' 11 | monitor = require './monitor' 12 | 13 | exports.registerHook 'view.layout.menu_bar', 14 | href: '/public/monitor/' 15 | t_body: 'plugins.linux.server_monitor' 16 | 17 | exports.registerHook 'account.username_filter', 18 | filter: (username, callback) -> 19 | linux.getPasswdMap (passwd_map) -> 20 | linux.getGroup (group_map) -> 21 | if username in _.values passwd_map 22 | callback false 23 | else if username in _.values group_map 24 | callback false 25 | else 26 | callback true 27 | 28 | exports.registerHook 'view.panel.styles', 29 | path: '/plugin/linux/style/panel.css' 30 | 31 | exports.registerHook 'view.panel.widgets', 32 | generator: (req, callback) -> 33 | linux.getResourceUsageByAccount req.account, (resources_usage) -> 34 | resources_usage ?= 35 | username: req.account.username 36 | cpu: 0 37 | memory: 0 38 | storage: 0 39 | process: 0 40 | 41 | exports.render 'widget', req, 42 | usage: resources_usage 43 | , callback 44 | 45 | exports.registerHook 'account.resources_limit_changed', 46 | always_notice: true 47 | filter: (account, callback) -> 48 | linux.setResourceLimit account, callback 49 | 50 | exports.registerServiceHook 'enable', 51 | filter: (account, callback) -> 52 | linux.createUser account, callback 53 | 54 | exports.registerServiceHook 'disable', 55 | filter: (account, callback) -> 56 | linux.deleteUser account, callback 57 | 58 | if config.plugins.linux.monitor_cycle 59 | exports.registerHook 'app.started', 60 | action: -> 61 | monitor.run() 62 | 63 | app.express.get '/public/monitor', requireAuthenticate, (req, res) -> 64 | async.parallel 65 | resources_usage: (callback) -> 66 | linux.getResourceUsageByAccounts (result) -> 67 | callback null, result 68 | system: wrapAsync linux.getSystemInfo 69 | storage: wrapAsync linux.getStorageInfo 70 | process_list: wrapAsync linux.getProcessList 71 | memory: wrapAsync linux.getMemoryInfo 72 | 73 | , (err, result) -> 74 | logger.error err if err 75 | exports.render 'monitor', req, result, (html) -> 76 | res.send html 77 | -------------------------------------------------------------------------------- /plugin/linux/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Linux", 3 | "server_monitor": "Server Status", 4 | "widget": { 5 | "hour_cpu": "CPU in last hour", 6 | "hour_memory": "Memory in last hour", 7 | "storage": "Storage used", 8 | "month_transfer": "traffic of this month" 9 | }, 10 | "monitor": { 11 | "system": "Operator System", 12 | "system_info": "System info", 13 | "cpu": "CPU" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugin/linux/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Linux", 3 | "server_monitor": "服务器状态", 4 | "widget": { 5 | "hour_cpu": "一小时 CPU 时间", 6 | "hour_memory": "一小时内存占用", 7 | "storage": "储存空间", 8 | "month_transfer": "月流量" 9 | }, 10 | "monitor": { 11 | "system": "操作系统", 12 | "system_info": "系统信息", 13 | "cpu": "CPU" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugin/linux/monitor.coffee: -------------------------------------------------------------------------------- 1 | {child_process, os, fs, async, _} = app.libs 2 | {config} = app 3 | {Account} = app.models 4 | 5 | linux = require './linux' 6 | 7 | exports.last_plist = [] 8 | exports.resources_usage = {} 9 | 10 | REDIS_LAST_PLIST = "#{config.redis.prefix}:linux.last_plist" 11 | 12 | exports.run = -> 13 | app.redis.get REDIS_LAST_PLIST, (err, last_plist) -> 14 | exports.last_plist = JSON.parse(last_plist) ? [] 15 | 16 | exports.monitoring -> 17 | setInterval -> 18 | exports.monitoring -> 19 | , config.plugins.linux.monitor_cycle 20 | 21 | exports.monitoring = (callback) -> 22 | REDIS_KEY = "#{config.redis.prefix}:linux.recent_resources_usage" 23 | ITEM_IN_RESOURCES_LIST = 3600 * 1000 / config.plugins.linux.monitor_cycle 24 | 25 | linux.getMemoryInfo (memory_info) -> 26 | linux.getProcessList (plist) -> 27 | plist = _.reject plist, (item) -> 28 | return item.rss == 0 29 | 30 | async.parallel 31 | cpu: (callback) -> 32 | exports.monitoringCpu plist, callback 33 | 34 | memory: (callback) -> 35 | exports.monitoringMemory plist, callback 36 | 37 | , (err, result) -> 38 | app.redis.get REDIS_KEY, (err, recent_resources_usage) -> 39 | recent_resources_usage = JSON.parse(recent_resources_usage) ? [] 40 | recent_resources_usage.push result 41 | recent_resources_usage = _.last recent_resources_usage, ITEM_IN_RESOURCES_LIST 42 | 43 | resources_usage = {} 44 | 45 | increaseAccountUsage = (username, type, value) -> 46 | resources_usage[username] ?= {} 47 | 48 | if resources_usage[username][type] 49 | resources_usage[username][type] += value 50 | else 51 | resources_usage[username][type] = value 52 | 53 | for item in recent_resources_usage 54 | for account_name, cpu_usage of item.cpu 55 | increaseAccountUsage account_name, 'cpu', cpu_usage 56 | 57 | for account_name, memory_usage of item.memory 58 | increaseAccountUsage account_name, 'memory', memory_usage 59 | 60 | for username, usage of resources_usage 61 | base = config.plugins.linux.monitor_cycle / 1000 62 | usage.memory = parseFloat (usage.memory / recent_resources_usage.length / base).toFixed(1) 63 | 64 | async.each _.keys(resources_usage), (username, callback) -> 65 | Account.search username, (account) -> 66 | unless account 67 | return callback() 68 | 69 | if os.loadavg()[0] > 1 70 | if resources_usage[username].cpu > account.resources_limit.cpu 71 | child_process.exec "sudo pkill -SIGKILL -u #{username}", -> 72 | 73 | if memory_info.used_per > 75 74 | if resources_usage[username].memory > account.resources_limit.memory 75 | child_process.exec "sudo pkill -SIGKILL -u #{username}", -> 76 | 77 | callback() 78 | , -> 79 | app.redis.set REDIS_KEY, JSON.stringify(recent_resources_usage), -> 80 | exports.resources_usage = resources_usage 81 | 82 | app.redis.setex REDIS_LAST_PLIST, 60, JSON.stringify(plist), -> 83 | exports.last_plist = plist 84 | callback() 85 | 86 | exports.monitoringCpu = (plist, callback) -> 87 | total_time = {} 88 | 89 | findLastProcess = (process) -> 90 | return _.find exports.last_plist, (i) -> 91 | return i.pid == process.pid and i.user == process.user and i.command == process.command 92 | 93 | addTime = (account_name, time) -> 94 | if total_time[account_name] 95 | total_time[account_name] += time 96 | else 97 | total_time[account_name] = time 98 | 99 | exist_process = _.filter plist, (item) -> 100 | return findLastProcess item 101 | 102 | new_process = _.filter plist, (item) -> 103 | return not findLastProcess item 104 | 105 | for item in exist_process 106 | last_process = findLastProcess item 107 | addTime item.user, item.time - last_process.time 108 | 109 | for item in new_process 110 | addTime item.user, item.time 111 | 112 | callback null, total_time 113 | 114 | exports.monitoringMemory = (plist, callback) -> 115 | total_memory = {} 116 | 117 | addMemory = (account_name, menory) -> 118 | if total_memory[account_name] 119 | total_memory[account_name] += menory 120 | else 121 | total_memory[account_name] = menory 122 | 123 | for item in plist 124 | addMemory item.user, parseInt ((item.rss / 1024) * config.plugins.linux.monitor_cycle / 1000).toFixed() 125 | 126 | callback null, total_memory 127 | -------------------------------------------------------------------------------- /plugin/linux/reconfigure.coffee: -------------------------------------------------------------------------------- 1 | {async, fs, child_process, _} = app.libs 2 | {Account} = app.models 3 | {utils} = app 4 | 5 | linux = require './linux' 6 | 7 | unless fs.existsSync "#{__dirname}/../../.backup/linux" 8 | fs.mkdirSync "#{__dirname}/../../.backup/linux", 0o750 9 | 10 | module.exports = (callback) -> 11 | exists_users = _.filter fs.readdirSync('/home'), (file) -> 12 | return fs.statSync("/home/#{file}").isDirectory() 13 | 14 | async.series [ 15 | (callback) -> 16 | Account.find 17 | 'billing.services': 'linux' 18 | , (err, accounts) -> 19 | async.eachSeries accounts, (account, callback) -> 20 | if account.username in exists_users 21 | linux.setResourceLimit account, callback 22 | else 23 | console.log "created linux user for #{account.username}" 24 | 25 | linux.createUser account, -> 26 | linux.setResourceLimit account, callback 27 | 28 | , callback 29 | 30 | (callback) -> 31 | linux.getPasswdMap (passwd_map) -> 32 | async.eachSeries exists_users, (user, callback) -> 33 | if user in _.values passwd_map 34 | return callback() 35 | 36 | console.log "removed /home/#{user}" 37 | backup_filename = "#{__dirname}/../../.backup/linux/#{user}-#{utils.randomString(5)}" 38 | child_process.exec "sudo mv /home/#{user} #{backup_filename}", callback 39 | 40 | , callback 41 | 42 | ], (err) -> 43 | throw err if err 44 | callback() 45 | -------------------------------------------------------------------------------- /plugin/linux/static/style/monitor.less: -------------------------------------------------------------------------------- 1 | .progress-bar { 2 | text-align: center; 3 | } 4 | 5 | .multi-progress span { 6 | position: inherit; 7 | display: inherit; 8 | width: 100%; 9 | color: inherit; 10 | } 11 | 12 | body #content .row { 13 | margin-bottom: 0px; 14 | } 15 | 16 | .process-list { 17 | overflow-x: scroll; 18 | } 19 | 20 | .col-md-4 { 21 | padding-left: 0; 22 | } 23 | 24 | .col-md-8 { 25 | padding-right: 0; 26 | } 27 | 28 | .process-list td { 29 | font-size: 14px; 30 | font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; 31 | white-space: nowrap; 32 | } 33 | 34 | .progress-bar.blank { 35 | background-color: #f5f5f5; 36 | color: black; 37 | -webkit-box-shadow: none; 38 | box-shadow: none; 39 | } 40 | -------------------------------------------------------------------------------- /plugin/linux/static/style/panel.less: -------------------------------------------------------------------------------- 1 | .linux-widget-table { 2 | tr td:first-child { 3 | width: 200px; 4 | } 5 | 6 | td { 7 | .progress { 8 | margin-bottom: 0; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /plugin/linux/view/monitor.jade: -------------------------------------------------------------------------------- 1 | extends ../../../core/view/layout 2 | 3 | prepend header 4 | title #{t('server_monitor')} | #{t(config.web.t_name)} 5 | 6 | append header 7 | link(rel='stylesheet', href='/plugin/linux/style/monitor.css') 8 | 9 | block content 10 | #content.container 11 | .row 12 | .col-md-4 13 | .panel.panel-default 14 | .panel-heading 15 | h3.panel-title System 16 | table.table 17 | tbody 18 | tr 19 | td Hostname 20 | td= system.hostname 21 | tr 22 | td System 23 | td= system.system 24 | tr 25 | td CPU 26 | td= system.cpu 27 | tr 28 | td Time 29 | td= system.time 30 | tr 31 | td Uptime 32 | td #{(system.uptime / 3600).toFixed(1)} hours 33 | tr 34 | td Loadavg 35 | td= system.loadavg.join(', ') 36 | tr 37 | td Address 38 | td= system.address.join(', ') 39 | 40 | .col-md-8 41 | .panel.panel-default 42 | .panel-heading 43 | h3.panel-title Memory & Storage 44 | .panel-body 45 | p Memory 46 | .progress.multi-progress 47 | .progress-bar.progress-bar-danger(role='progressbar', style='width: #{memory.used_per}%;') 48 | span(title= '#{memory.used}M') #{memory.used_per}% 49 | .progress-bar.progress-bar-info(role='progressbar', style='width: #{memory.cached_per}%;') 50 | span(title= '#{memory.cached}M') #{memory.cached_per}% 51 | .progress-bar.progress-bar-success(role='progressbar', style='width: #{memory.buffers_per}%;') 52 | span(title= '#{memory.buffers}M') #{memory.buffers_per}% 53 | .progress-bar.blank(role='progressbar', style='width: #{memory.free_per}%;') 54 | span(title= '#{memory.free}M') #{memory.free_per}% 55 | p SWAP 56 | .progress.multi-progress 57 | .progress-bar.progress-bar-warning(role='progressbar', style='width: #{memory.swap_used_per}%;') 58 | span(title= '#{memory.swap_used_per}M') #{memory.swap_used_per}% 59 | .progress-bar.blank(role='progressbar', style='width: #{memory.swap_free_per}%;') 60 | span(title= '#{memory.swap_free}M') #{memory.swap_free_per}% 61 | p Storage 62 | .progress.multi-progress 63 | .progress-bar.progress-bar-warning(role='progressbar', style='width: #{storage.used_per}%;') 64 | span(title= '#{storage.used}G') #{storage.used_per}% 65 | .progress-bar.blank(role='progressbar', style='width: #{storage.free_per}%;') 66 | span(title= '#{storage.free}G') #{storage.free_per}% 67 | 68 | .row 69 | .panel.panel-default 70 | .panel-heading 71 | h3.panel-title Users 72 | table.table.table-hover 73 | thead 74 | tr 75 | th User 76 | th Process 77 | th CPU 1 Hour 78 | th Memory 1 Hour 79 | th Storage 80 | tbody 81 | for item in resources_usage 82 | tr 83 | td= item.username 84 | td= item.process 85 | td #{item.cpu}s 86 | td #{item.memory.toFixed(1)}M 87 | td #{(item.storage).toFixed(1)}M 88 | 89 | .row 90 | .panel.panel-default.process-list 91 | .panel-heading 92 | h3.panel-title Process 93 | table.table.table-hover 94 | thead 95 | tr 96 | th USER 97 | th PID 98 | th %CPU 99 | th %MEM 100 | th VSZ 101 | th RSS 102 | th TTY 103 | th STAT 104 | th START 105 | th TIME 106 | th COMMAND 107 | tbody 108 | for process in process_list 109 | tr 110 | td= process.user 111 | td= process.pid 112 | td= process.cpu_per 113 | td= process.mem_per 114 | td= process.vsz 115 | td= process.rss 116 | td= process.tty 117 | td= process.stat 118 | td= process.start 119 | td= process.time 120 | td!= _.escape(process.command).replace(/ /g, ' ') 121 | -------------------------------------------------------------------------------- /plugin/linux/view/widget.jade: -------------------------------------------------------------------------------- 1 | - limit = account.resources_limit 2 | 3 | mixin displayProgressBar(now, limit, unit) 4 | - per = parseInt((now / limit * 100).toFixed()) 5 | - color = per < 85 ? 'success' : 'danger' 6 | 7 | .progress 8 | .progress-bar(class="progress-bar-#{color}", role='progressbar', aria-valuenow='#{per}', 9 | aria-valuemin='0', aria-valuemax='100', style='width: #{per}%;') 10 | span #{now} / #{limit} #{unit} 11 | 12 | .row 13 | header= t('') 14 | table.table.table-hover.linux-widget-table 15 | tbody 16 | tr 17 | td= t('widget.hour_cpu') 18 | td 19 | mixin displayProgressBar(usage.cpu, limit.cpu, 's') 20 | tr 21 | td= t('widget.hour_memory') 22 | td 23 | mixin displayProgressBar(usage.memory, limit.memory, 'M') 24 | tr 25 | td= t('widget.storage') 26 | td 27 | mixin displayProgressBar(usage.storage, limit.storage, 'M') 28 | -------------------------------------------------------------------------------- /plugin/rpvhost/index.coffee: -------------------------------------------------------------------------------- 1 | {jade, path, fs} = app.libs 2 | {pluggable, config} = app 3 | 4 | exports = module.exports = class RPVhostPlugin extends pluggable.Plugin 5 | @NAME: 'rpvhost' 6 | @type: 'extension' 7 | 8 | exports.registerHook 'plugin.wiki.pages', 9 | t_category: 'plugins.rpvhost.' 10 | t_title: 'Terms.md' 11 | language: 'zh_CN' 12 | content_markdown: fs.readFileSync("#{__dirname}/wiki/Terms.md").toString() 13 | 14 | exports.registerHook 'view.layout.menu_bar', 15 | href: '//blog.rpvhost.net' 16 | target: '_blank' 17 | t_body: 'plugins.rpvhost.official_blog' 18 | 19 | exports.registerHook 'billing.payment_methods', 20 | widget_generator: (req, callback) -> 21 | exports.render 'payment_method', req, {}, callback 22 | 23 | exports.registerHook 'view.pay.display_payment_details', 24 | type: 'taobao' 25 | filter: (req, deposit_log, callback) -> 26 | callback exports.t(req) 'view.payment_details', 27 | order_id: deposit_log.payload.order_id 28 | 29 | if config.plugins.rpvhost.green_style 30 | exports.registerHook 'view.layout.styles', 31 | path: '/plugin/rpvhost/style/green.css' 32 | 33 | unless config.plugins.rpvhost.index_page == false 34 | app.express.get '/', (req, res) -> 35 | exports.render 'index', req, {}, (html) -> 36 | res.send html 37 | -------------------------------------------------------------------------------- /plugin/rpvhost/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "RP Virtual Hosting", 3 | "greenshadow": "GreenShadow", 4 | "taobao": "Taobao", 5 | "official_blog": "Official Blog", 6 | "view": { 7 | "payment_tips": "Purchase the following product, and tell us your username and which server.", 8 | "go_pay": "Pay on Taobao", 9 | "payment_details": "Taobao Payment, Order ID: __order_id__" 10 | }, 11 | "plans": { 12 | "all": { 13 | "name": "All Service (Default)", 14 | "description": "Storage 520MB, Memory: 27MB, Traffic of month: 37GB" 15 | }, 16 | "shadowsocks": { 17 | "name": "ShadowSocks", 18 | "description": "Billing by usage, 0.6 CNY / G" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/rpvhost/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "RP 主机", 3 | "greenshadow": "GreenShadow", 4 | "taobao": "淘宝", 5 | "official_blog": "官方博客", 6 | "view": { 7 | "payment_tips": "拍下对应宝贝后付款即可,购买时注意选择服务器节点选项,备注填写你的用户名。", 8 | "go_pay": "淘宝购买", 9 | "payment_details": "淘宝支付,订单号 __order_id__" 10 | }, 11 | "plans": { 12 | "all": { 13 | "name": "所有服务(默认)", 14 | "description": "磁盘: 520MB, 内存: 27MB, 流量: 37GB" 15 | }, 16 | "shadowsocks": { 17 | "name": "ShadowSocks", 18 | "description": "按量付费,0.6 CNY / G" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/rpvhost/static/style/green.less: -------------------------------------------------------------------------------- 1 | body { 2 | > header { 3 | background: #1c9b47; 4 | 5 | nav.navbar-inverse { 6 | background: #1c9b47; 7 | } 8 | 9 | .navbar-inverse { 10 | .navbar-nav > .active > a, .navbar-nav > .open > a, .navbar-nav > .active > a:hover, .navbar-nav > .active > a:focus { 11 | background: #18813f !important; 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugin/rpvhost/static/style/index.less: -------------------------------------------------------------------------------- 1 | #page-title { 2 | padding: 60px 0; 3 | font-size: 22px; 4 | color: #e1d3f7; 5 | text-shadow: 0 1px 0 rgba(0,0,0,.1); 6 | background: #563d7c linear-gradient(to bottom, #563d7c 0, #a17ac3 100%) repeat-x; 7 | 8 | h1 { 9 | color: #fff; 10 | font-size: 60px; 11 | line-height: 60px; 12 | } 13 | } 14 | 15 | #content { 16 | .row { 17 | margin: 45px 0 65px; 18 | } 19 | 20 | .col-md-6 { 21 | padding-right: 80px; 22 | font-size: 18px; 23 | 24 | header { 25 | padding-left: 15px; 26 | } 27 | 28 | li { 29 | line-height: 2em; 30 | } 31 | } 32 | 33 | .col-md-offset-6 { 34 | padding-left: 120px; 35 | 36 | header { 37 | text-align: right; 38 | padding-right: 15px; 39 | } 40 | } 41 | } 42 | 43 | #page-footer { 44 | padding: 60px 0; 45 | font-size: 18px; 46 | color: #ececf7; 47 | background: #a17ac3 linear-gradient(to bottom, #a17ac3 0, #563d7c 100%) repeat-x; 48 | 49 | p { 50 | text-align: right; 51 | margin-bottom: 2px; 52 | color: #fff; 53 | 54 | a { 55 | color: #e1d3f7; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugin/rpvhost/test/rpvhost.test.coffee: -------------------------------------------------------------------------------- 1 | (if isPluginEnable('rpvhost') then describe else describe.skip) 'plugin/rpvhost', -> 2 | agent = null 3 | 4 | before -> 5 | agent = supertest.agent app.express 6 | 7 | describe 'router', -> 8 | it 'GET /', (done) -> 9 | if config.plugins.rpvhost and config.plugins.rpvhost.index_page != false 10 | expect_code = 200 11 | else 12 | expect_code = 302 13 | 14 | agent.get '/' 15 | .redirects 0 16 | .expect expect_code 17 | .end done 18 | -------------------------------------------------------------------------------- /plugin/rpvhost/view/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../core/view/layout 2 | 3 | prepend header 4 | title RP 主机:Linux 虚拟主机 | #{t(config.web.t_name)} 5 | 6 | append header 7 | link(rel='stylesheet', href='/plugin/rpvhost/style/index.css') 8 | 9 | block content 10 | #page-title 11 | .container 12 | h1 RP 主机 13 | p Linux 虚拟主机 14 | 15 | #content.container 16 | .row 17 | .col-md-6.col-md-offset-6 18 | header 支持多种运行环境 19 | ul 20 | li 21 | code console.log('Node.js'); 22 | li 23 | code print('Python') 24 | li 25 | code fmt.Println('Golang') 26 | li 27 | code echo 'PHP'; 28 | 29 | .row 30 | .col-md-6 31 | header 灵活地编写 Nginx 配置 32 | pre. 33 | { 34 | "listen": 80, 35 | "server_name": ["myapp.net"], 36 | "auto_index": false, 37 | "index": ["index.html"], 38 | "root": "/home/user/web", 39 | "location": { 40 | "/": { 41 | "fastcgi_pass": "unix:///home/user/phpfpm.sock", 42 | "fastcgi_index": ["index.php"] 43 | } 44 | } 45 | } 46 | 47 | .row 48 | .col-md-6.col-md-offset-6 49 | header 各种类型的数据库 50 | ul 51 | li 52 | code db.update {name: 'MongoDB'} 53 | li 54 | code SELECT FROM `MySQL` 55 | li 56 | code FLUSH Redis 57 | li 58 | code GET Memcache 59 | 60 | #page-footer 61 | .container 62 | p 63 | a(href='https://github.com/jysperm/RootPanel') RootPanel 64 | |   v#{app.package.version} 65 | p 66 | for author in app.package.contributors 67 | | by   68 | a(href=author.url)= author.name 69 | 70 | #site-not-exist.modal 71 | .modal-dialog 72 | .modal-content 73 | .modal-header 74 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 75 | h4.modal-title 您访问的站点不存在 76 | .modal-body 77 | p 因为您要访问的站点不存在,所以我们将页面重定向到了 RP 主机首页。 78 | p 很可能是由于这个站点的所有者删除或修改了这个站点。 79 | .modal-footer 80 | button.btn.btn-danger(type='button', data-dismiss='modal') 关闭 81 | -------------------------------------------------------------------------------- /plugin/rpvhost/view/payment_method.jade: -------------------------------------------------------------------------------- 1 | div 2 | h2= t('taobao') 3 | p= t('view.payment_tips') 4 | a.btn.btn-success(href='http://item.taobao.com/item.htm?id=#{app.config.plugins.rpvhost.taobao_item_id}')= t('view.go_pay') 5 | -------------------------------------------------------------------------------- /plugin/rpvhost/wiki/Terms.md: -------------------------------------------------------------------------------- 1 | ## 服务条款 2 | 我们将依据此条款为用户提供服务。我们已尽力将此条款写得通俗易懂,请您确保在购买 RP 主机之前,认同以下的全部条款。 3 | 4 | ### 禁止条款 5 | 若用户违反以下禁止条款,我们将首先暂停您的服务,警告您改正相应行为;若您反复违反条款,或者我们认为您在故意违反条款,我们将停止您的账户,并且不会退款,若数据合法可退还数据。 6 | 7 | * 滥用系统资源。过度使用网络,CPU, 内存和磁盘 IO 用于无意义的计算,例如进行「比特币挖矿」;但磁盘空间不存在滥用,您可以随意支配您的磁盘空间。 8 | * 垃圾邮件和信息。只要接收者没有表明他希望接收这些邮件,这些邮件就被定义为垃圾邮件。因为 Linode 对于垃圾邮件态度非常强硬,因此我们也对垃圾邮件实行零容忍。 9 | * 不适宜的内容。包括放置成人色情、诈骗、虚假广告、指导犯罪、私服类型的站点,存在争议的人身攻击、版权侵权的站点,放置容易引起 GFW 或搜索引擎封杀的内容。 10 | * 攻击其他用户和网络。例如尝试进入其他用户的账户,干扰 RP 主机的运行,企图突破 RP 主机的限制;将 RP 主机用作黑客行为之用(如网络代理), 或故意吸引黑客的攻击。 11 | * Linode 所禁止的其他行为(). 12 | 13 | ### 用户义务 14 | 15 | 用户应当认识到: 16 | 17 | * 您对您的程序的行为和产生的数据应为负有完全的责任,用户应当自行保证密码和程序的安全性。在您违反条款时,我们不接受类似于「帐号被盗」「程序出错」「本人不了解情况」这类理由。 18 | * 如果我们认为您的行为可能威胁到 RP 主机的正常运行(如有可能导致被 GFW 封锁, 或者 Linode 方面不允许), 我们可能会要求你移除某些内容。 19 | * 我们只提供我们承诺过的服务,我们并未承诺提供与我们无关的技术问题的客服支持,虽然在大多数情况下,我们确实会为您提供无偿的技术支持。 20 | * 您应当保证与客服沟通渠道(邮件)的通畅,若因为您没有收到来自我们的通知,或者拒绝听从来自客服的建议,后果需要您自行承担。 21 | * 即使我们在做最大的努力,但仍不排除出现数据丢失的可能性,对于这种情况我们只能提供有限的补偿。 22 | * 因为 RP 主机新版中不再存在预付费模式,因此我们可能会随时调节资源参数。 23 | 24 | ### 服务担保 25 | 26 | * 我们可以随时为您的可用余额进行退款,退款产生的手续费(如支付宝收取)由您来承担,通过活动赠送的余额不参与退款。 27 | * 我们保证 99.5% 的可用率,如果在任意连续的 30 天中,故障时间超过 0.5%, 您可以要求我们为过去的一个月退款。 28 | * 我们使用 Linode 的备份服务备份您的数据,会为过去一个月、一周、一天创建备份,但不向用户提供备份查阅服务。 29 | * 我们保证大部分工单会在 8 小时内收到回复,所有工单会在 24 小时内收到回复。 30 | 31 | ### 隐私保护 32 | 33 | * 你的站点绑定的域名会被定期人工检查,以确保未违反以上禁止条款,我们只会通过域名访问或其他非特权渠道(如搜索引擎)的链接访问, 不会直接查看你的文件。 34 | * 根据 Linux 的一些规则, 您的部分信息如用户名, 进程列表, 权限设置有误的文件可能会被其他用户获取。 35 | * 我们不储存您的明文密码,SSH 和 MySQL 等服务的密码会直接交给相应服务来管理。 36 | * 当您求助客服解决问题时, 得到允许后,客服会在必要的限度内查看你的文件。 37 | * 您的邮箱地址会被保密,我们只向你的邮箱中投递工单通知。 38 | -------------------------------------------------------------------------------- /plugin/shadowsocks/index.coffee: -------------------------------------------------------------------------------- 1 | {_, fs} = app.libs 2 | {pluggable, config, utils} = app 3 | {Financials} = app.models 4 | 5 | exports = module.exports = class ShadowSocksPlugin extends pluggable.Plugin 6 | @NAME: 'shadowsocks' 7 | @type: 'service' 8 | @dependencies: ['supervisor', 'linux'] 9 | 10 | shadowsocks = require './shadowsocks' 11 | 12 | exports.registerHook 'plugin.wiki.pages', 13 | always_notice: true 14 | t_category: 'plugins.shadowsocks.' 15 | t_title: 'README.md' 16 | language: 'zh_CN' 17 | content_markdown: fs.readFileSync("#{__dirname}/wiki/README.md").toString() 18 | 19 | exports.registerHook 'view.panel.scripts', 20 | path: '/plugin/shadowsocks/script/panel.js' 21 | 22 | exports.registerHook 'view.panel.styles', 23 | path: '/plugin/shadowsocks/style/panel.css' 24 | 25 | exports.registerHook 'view.panel.widgets', 26 | generator: (req, callback) -> 27 | price_gb = config.plugins.shadowsocks.price_bucket * (1000 * 1000 * 1000 / config.plugins.shadowsocks.billing_bucket) 28 | 29 | shadowsocks.accountUsage req.account, (result) -> 30 | _.extend result, 31 | transfer_remainder: req.account.billing.balance / price_gb 32 | 33 | exports.render 'widget', req, result, callback 34 | 35 | exports.registerHook 'view.admin.sidebars', 36 | generator: (req, callback) -> 37 | Financials.find 38 | type: 'usage_billing' 39 | 'payload.service': 'shadowsocks' 40 | created_at: 41 | $gte: new Date Date.now() - 30 * 24 * 3600 * 1000 42 | , (err, financials) -> 43 | time_range = 44 | traffic_24hours: 24 * 3600 * 1000 45 | traffic_3days: 3 * 24 * 3600 * 1000 46 | traffic_7days: 7 * 24 * 3600 * 1000 47 | traffic_30days: 30 * 24 * 3600 * 1000 48 | 49 | traffic_result = {} 50 | 51 | for name, range of time_range 52 | logs = _.filter financials, (i) -> 53 | return i.created_at.getTime() > Date.now() - range 54 | 55 | traffic_result[name] = _.reduce logs, (memo, i) -> 56 | return memo + i.payload.traffic_mb 57 | , 0 58 | 59 | exports.render 'admin/sidebar', req, traffic_result, callback 60 | 61 | exports.registerHook 'app.started', 62 | action: -> 63 | shadowsocks.initSupervisor -> 64 | 65 | exports.registerServiceHook 'enable', 66 | filter: (account, callback) -> 67 | shadowsocks.initAccount account, callback 68 | 69 | exports.registerServiceHook 'disable', 70 | filter: (account, callback) -> 71 | shadowsocks.deleteAccount account, callback 72 | 73 | app.express.use '/plugin/shadowsocks', require './router' 74 | 75 | if config.plugins.shadowsocks?.monitor_cycle 76 | exports.registerHook 'app.started', 77 | action: -> 78 | setInterval shadowsocks.monitoring, config.plugins.shadowsocks.monitor_cycle 79 | -------------------------------------------------------------------------------- /plugin/shadowsocks/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "ShadowSocks", 3 | "remote_access": "Remote Access", 4 | "remote_port": "Port", 5 | "reset": "Reset", 6 | "transfer": "Traffic", 7 | "method": "Method", 8 | "transfer_remainder": "About __traffic__ G remainder", 9 | "24hours_ago": "24 hours ago", 10 | "3days_ago": "3 days ago", 11 | "7days_ago": "7 days ago", 12 | "30days_ago": "30 days ago" 13 | } 14 | -------------------------------------------------------------------------------- /plugin/shadowsocks/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "ShadowSocks", 3 | "remote_access": "连接", 4 | "remote_port": "端口", 5 | "reset": "重置", 6 | "transfer": "流量", 7 | "method": "加密算法", 8 | "transfer_remainder": "余额折合约 __traffic__ G 流量", 9 | "24hours_ago": "过去 24 小时", 10 | "3days_ago": "过去 3 天", 11 | "7days_ago": "过去 7 天", 12 | "30days_ago": "过去 30 天" 13 | } 14 | -------------------------------------------------------------------------------- /plugin/shadowsocks/reconfigure.coffee: -------------------------------------------------------------------------------- 1 | {async, _, child_process} = app.libs 2 | {Account} = app.models 3 | {config, utils} = app 4 | 5 | shadowsocks = require './shadowsocks' 6 | 7 | module.exports = (callback) -> 8 | async.series [ 9 | (callback) -> 10 | async.each config.plugins.shadowsocks.available_ciphers, (method, callback) -> 11 | shadowsocks.writeSupervisorConfigure method, callback 12 | , callback 13 | 14 | (callback) -> 15 | default_method = _.first config.plugins.shadowsocks.available_ciphers 16 | 17 | Account.find 18 | 'billing.service': 'shadowsocks' 19 | , (err, accounts) -> 20 | async.eachSeries accounts, (account, callback) -> 21 | {port, method, password} = account.pluggable.shadowsocks 22 | 23 | unless method 24 | console.log "created shadowsocks method for #{account.username}}" 25 | account.pluggable.shadowsocks.method = default_method 26 | account.markModified 'pluggable.shadowsocks.method' 27 | 28 | unless password 29 | console.log "created shadowsocks password for #{account.username}}" 30 | account.pluggable.shadowsocks.password = utils.randomString 10 31 | account.markModified 'pluggable.shadowsocks.password' 32 | 33 | if port 34 | account.save callback 35 | else 36 | shadowsocks.generatePort (port) -> 37 | console.log "created shadowsocks port for #{account.username}}" 38 | account.pluggable.shadowsocks.port = port 39 | account.markModified 'pluggable.shadowsocks.port' 40 | account.save callback 41 | 42 | , callback 43 | 44 | (callback) -> 45 | shadowsocks.queryIptablesInfo (iptables_info) -> 46 | Account.find 47 | 'billing.service': 'shadowsocks' 48 | , (err, accounts) -> 49 | async.series [ 50 | (callback) -> 51 | async.eachSeries accounts, (account, callback) -> 52 | port = iptables_info[account.pluggable.shadowsocks.port] 53 | 54 | if port 55 | callback() 56 | else 57 | child_process.exec "sudo iptables -I OUTPUT -p tcp --sport #{port}", callback 58 | , callback 59 | 60 | (callback) -> 61 | async.eachSeries _.keys(iptables_info), (port) -> 62 | matched_account = _.find accounts, (account) -> 63 | return account.pluggable.shadowsocks.port == port 64 | 65 | if matched_account 66 | callback() 67 | else 68 | child_process.exec "sudo iptables -D OUTPUT #{iptables_info[port].num}", callback 69 | , callback 70 | 71 | ], callback 72 | 73 | (callback) -> 74 | child_process.exec 'sudo iptables-save | sudo tee /etc/iptables.rules', callback 75 | 76 | (callback) -> 77 | shadowsocks.updateConfigure callback 78 | 79 | ], (err) -> 80 | throw err if err 81 | callback() 82 | -------------------------------------------------------------------------------- /plugin/shadowsocks/router.coffee: -------------------------------------------------------------------------------- 1 | {utils, config} = app 2 | {markdown, fs, path, express} = app.libs 3 | {requireInService} = app.middleware 4 | 5 | shadowsocks = require './shadowsocks' 6 | 7 | module.exports = exports = express.Router() 8 | 9 | exports.use requireInService 'shadowsocks' 10 | 11 | exports.post '/reset_password', (req, res) -> 12 | password = utils.randomString 10 13 | 14 | req.account.update 15 | $set: 16 | 'pluggable.shadowsocks.password': password 17 | , -> 18 | shadowsocks.updateConfigure -> 19 | res.json {} 20 | 21 | exports.post '/switch_method', (req, res) -> 22 | unless req.body.method in config.plugins.shadowsocks.available_ciphers 23 | return res.error 'invalid_method' 24 | 25 | if req.body.method == req.account.pluggable.shadowsocks.method 26 | return res.error 'already_in_method' 27 | 28 | req.account.update 29 | $set: 30 | 'pluggable.shadowsocks.method': req.body.method 31 | , -> 32 | shadowsocks.updateConfigure -> 33 | res.json {} 34 | -------------------------------------------------------------------------------- /plugin/shadowsocks/static/script/panel.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.widget-shadowsocks .action-reset-password').click -> 3 | if window.confirm 'Are you sure?' 4 | request '/plugin/shadowsocks/reset_password/', {}, -> 5 | location.reload() 6 | 7 | $('.widget-shadowsocks .action-switch-method').click -> 8 | request '/plugin/shadowsocks/switch_method/', 9 | method: $('.widget-shadowsocks .input-method').val() 10 | , -> 11 | alert 'Success' 12 | -------------------------------------------------------------------------------- /plugin/shadowsocks/static/style/panel.less: -------------------------------------------------------------------------------- 1 | .widget-shadowsocks { 2 | .input-group { 3 | margin-bottom: 20px; 4 | } 5 | 6 | table { 7 | tr td { 8 | padding-left: 50px; 9 | } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /plugin/shadowsocks/test/shadowsocks.test.coffee: -------------------------------------------------------------------------------- 1 | (if isPluginEnable('shadowsocks') then describe else describe.skip) 'plugin/shadowsocks', -> 2 | agent = null 3 | utils = null 4 | config = null 5 | Account = null 6 | 7 | csrf_token = null 8 | account_id = null 9 | 10 | before -> 11 | {utils, config} = app 12 | {Account} = app.models 13 | agent = supertest.agent app.express 14 | 15 | config.plans.shadowsocks = 16 | t_name: 'shadowsocks' 17 | t_description: 'shadowsocks' 18 | 19 | services: ['shadowsocks'] 20 | 21 | describe 'router', -> 22 | it 'POST register', (done) -> 23 | agent.get '/account/session_info' 24 | .end (err, res) -> 25 | csrf_token = res.body.csrf_token 26 | 27 | agent.post '/account/register' 28 | .send 29 | csrf_token: csrf_token 30 | username: "test#{utils.randomString(20).toLowerCase()}" 31 | email: "#{utils.randomString 20}@gmail.com" 32 | password: utils.randomString 20 33 | .end (err, res) -> 34 | account_id = res.body.id 35 | created_objects.accounts.push ObjectId account_id 36 | 37 | Account.findByIdAndUpdate account_id, 38 | $set: 39 | 'billing.balance': 10 40 | , -> 41 | done err 42 | 43 | it 'POST join_plan', (done) -> 44 | @timeout 10000 45 | agent.post '/billing/join_plan' 46 | .send 47 | csrf_token: csrf_token 48 | plan: 'shadowsocks' 49 | .expect 200 50 | .end done 51 | 52 | it 'POST reset_password' 53 | 54 | it 'POST switch_method' 55 | 56 | it 'POST leave_plan' 57 | -------------------------------------------------------------------------------- /plugin/shadowsocks/view/admin/sidebar.jade: -------------------------------------------------------------------------------- 1 | .row 2 | header= t('') 3 | table.table.table-hover 4 | tbody 5 | tr 6 | td #{(traffic_24hours / 1000).toFixed(1)}G 7 | td= t('24hours_ago') 8 | tr 9 | td #{(traffic_3days / 1000).toFixed(1)}G 10 | td= t('3days_ago') 11 | tr 12 | td #{(traffic_7days / 1000).toFixed(1)}G 13 | td= t('7days_ago') 14 | tr 15 | td #{(traffic_30days / 1000).toFixed(1)}G 16 | td= t('30days_ago') 17 | -------------------------------------------------------------------------------- /plugin/shadowsocks/view/widget.jade: -------------------------------------------------------------------------------- 1 | .row.widget-shadowsocks 2 | header= t('') 3 | .col-md-6 4 | .panel.panel-default 5 | .panel-heading 6 | h3.panel-title= t('remote_access') 7 | .panel-body 8 | .input-group 9 | span.input-group-addon= t('remote_port') 10 | input.form-control(type='text', value=account.pluggable.shadowsocks.port, disabled) 11 | .input-group 12 | span.input-group-addon= t('account.password') 13 | input.form-control(type='text', value=account.pluggable.shadowsocks.password, disabled) 14 | span.input-group-btn 15 | button.btn.btn-default.action-reset-password(type='button')= t('reset') 16 | .input-group 17 | span.input-group-addon= t('method') 18 | select.input-method.form-control(style='-webkit-appearance: none;') 19 | for method in config.plugins.shadowsocks.available_ciphers 20 | if account.pluggable.shadowsocks.method == method 21 | option(selected='selected')= method 22 | else 23 | option= method 24 | span.input-group-btn 25 | button.btn.btn-default.action-switch-method(type='button')= t('common.change') 26 | 27 | .col-md-6 28 | .panel.panel-default 29 | .panel-heading 30 | h3.panel-title= t('transfer') 31 | .panel-body 32 | p= t('transfer_remainder', {traffic: transfer_remainder.toFixed(1)}) 33 | table.table.table-hover 34 | tbody 35 | tr 36 | td #{(traffic_24hours / 1000).toFixed(1)}G 37 | td= t('24hours_ago') 38 | tr 39 | td #{(traffic_7days / 1000).toFixed(1)}G 40 | td= t('7days_ago') 41 | tr 42 | td #{(traffic_30days / 1000).toFixed(1)}G 43 | td= t('30days_ago') 44 | -------------------------------------------------------------------------------- /plugin/shadowsocks/wiki/README.md: -------------------------------------------------------------------------------- 1 | ## 连接信息 2 | 3 | * 服务器:当前域名(`greenshadow.net`) 4 | * 端口:可在面板上查看 5 | * 密码:可在面板上查看 6 | * 加密方式:aes-256-cfb 7 | 8 | ## 常用客户端 9 | 10 | * [Windows GUI](http://pan.baidu.com/s/1qWry1Co) 11 | * [OS X GoAgentX](http://pan.baidu.com/s/1xWGyE) | [OS X GUI](http://pan.baidu.com/s/1i3va6ZN) 12 | * [Android apk](http://pan.baidu.com/s/1sjjUTgL) 13 | * [More](http://shadowsocks.org/en/download/clients.html) 14 | 15 | ## 付费 16 | 目前支持淘宝和比特币两种充值方式: 17 | 18 | * 淘宝 19 | 20 | 通过面板上的淘宝链接访问 GreenShadow 的淘宝店,拍下对应宝贝后付款即可。购买时请在备注中填写你在 GreenShadow 的用户名。 21 | 22 | 付款后无需以任何方式催促客服,一般淘宝支付距离充值成功会有 24 小时左右的延时。淘宝显示发货后,即为充值成功,你可以在任意时间确认收货。 23 | 24 | * 比特币(推荐) 25 | 26 | 在面板上可以看到你专属的比特币付款地址,你可以直接向该地址发送比特币。在经过一次确认后,系统会实时地,自动按照实时人民币汇率为你折算余额。 27 | 28 | ## 故障排错 29 | 30 | GreenShadow 使用标准的 ShadowSocks 协议,服务器是官方的 Python 版。如果使用过程中遇到障碍请检查这些常见的坑: 31 | 32 | * 将服务器地址换成 IP 试试,某些奇葩客户端可能不支持域名 33 | * 检查端口和密码是否有误,加密算法是否是 aes-256-cfb 34 | * 检查代理是否被开启,是否开启了全局代理或设置了代理规则 35 | * 试试是否可以用其他的 ShadowSocks 代理 36 | * 检查 GreenShadow 官网是否挂掉了,如果是的话请联系我 37 | * 实在折腾不好可以随时找我退钱 38 | 39 | 推荐使用网站上的工单功能,或邮件(网站挂掉的情况下), QQ 不保证一定能答复。 40 | 41 | ## 关于速度 42 | 对此我们实在不能做保证,GreenShadow 使用的是 Linode 的东京机房,本身是不限带宽的,但在高峰时段,可能速度不是非常理想;另外国内各地情况不一,请以实际情况为准;GreenShadow 在不付费的情况下也是可以试用 100M 的,或者您付费后觉得不满意也可以随时要求退款。 43 | 44 | ## 计费细节 45 | 每消耗 100M 流量触发一次扣费(约 0.06 元), 若扣费导致帐号余额低于 0, 则会自动关闭 ShadowSocks 服务。当手动关闭 ShadowSocks 时, 强行触发一次扣费,不足 100M 按 100M 计算。 46 | 47 | ## 硬性限制 48 | 为了防止滥发邮件,暂时封掉了 25 端口。 49 | 50 | ## 国外优秀资源 51 | 52 | * [Google] [Google Plus] [Google Drive] [Google Play] [Chrome Store] 53 | * [Facebook] [Twitter] [Youtube] [Flickr] [Dropbox] [V2EX] [Github] 54 | * [Blogger] [WordPress] [Feedly] [Wikipedia] [XArt] 55 | 56 | ## ShadowSocks 57 | 感谢 [clowwindy](https://github.com/clowwindy) 的 [shadowsocks](https://github.com/clowwindy/shadowsocks). 58 | 59 | ## 禁止条款 60 | 若用户违反以下禁止条款,我们将首先暂停您的服务,警告您改正相应行为;若您反复违反条款,或者我们认为您在故意违反条款,我们将停止您的账户,并且不会退款。 61 | 62 | * 用作发布垃圾邮件和信息;包括电子邮件,博客评论,论坛发言,即时通讯群发等,只要接受者没有表明他希望接收这些信息,就属于垃圾邮件。 63 | * 用作发表不适宜的内容;包括散布成人色情、诈骗、虚假广告、指导犯罪、版权侵权的信息。 64 | * 攻击其他用户和网络;例如尝试进入其他用户的账户,干扰 GreenShadow 主机的运行,企图突破 GreenShadow 的限制;将 GreenShadow 用作黑客行为之用,或故意吸引黑客的攻击。 65 | * Linode 所禁止的其他行为(). 66 | 67 | ## 用户义务 68 | 69 | 用户应当认识到: 70 | 71 | * 您对您的账户的行为应为负有完全的责任,用户应当自行保证密码和程序的安全性。在您违反条款时,我们不接受类似于「帐号被盗」「程序出错」「本人不了解情况」这类理由。 72 | * 我们只提供我们承诺过的服务,我们并未承诺提供与我们无关的技术问题的客服支持,虽然在大多数情况下,我们确实会为您提供无偿的技术支持。 73 | * 您应当保证与客服沟通渠道(邮件)的通畅,若因为您没有收到来自我们的通知,或者拒绝听从来自客服的建议,后果需要您自行承担。 74 | * 因为 GreenShadow 并非预付费,因此我们可能会随时调节资源价格。 75 | 76 | ## 服务担保 77 | 78 | * 我们可以随时为您的可用余额进行退款,退款产生的手续费(如支付宝收取)由您来承担,通过活动赠送的余额不参与退款。 79 | * 我们保证 99.5% 的可用率,如果在任意连续的 30 天中,故障时间超过 0.5%, 您可以要求我们为过去的一个月退款。 80 | * 我们保证大部分工单会在 8 小时内收到回复,所有工单会在 24 小时内收到回复。 81 | 82 | ## 隐私保护 83 | 84 | * 我们不储存您的明文密码,ShadowSocks 的密码是自动生成的。 85 | * 我们会统计您的流量消耗,但不会记录和分析你的流量数据内容。 86 | * 您的邮箱地址会被保密,我们只向你的邮箱中投递工单通知。 87 | -------------------------------------------------------------------------------- /plugin/ssh/index.coffee: -------------------------------------------------------------------------------- 1 | {_} = app.libs 2 | {pluggable} = app 3 | 4 | exports = module.exports = class LinuxPlugin extends pluggable.Plugin 5 | @NAME: 'ssh' 6 | @type: 'service' 7 | @dependencies: ['linux'] 8 | 9 | linux = require '../linux/linux' 10 | 11 | exports.registerHook 'view.panel.scripts', 12 | path: '/plugin/ssh/script/panel.js' 13 | 14 | exports.registerHook 'view.panel.widgets', 15 | generator: (req, callback) -> 16 | linux.getProcessList (process_list) -> 17 | process_list = _.filter process_list, (i) -> 18 | return i.user == req.account.username 19 | 20 | for item in process_list 21 | item.command = (/^[^A-Za-z0.9//]*(.*)/.exec(item.command))[1] 22 | 23 | exports.render 'widget', req, 24 | process_list: process_list 25 | , callback 26 | 27 | app.express.use '/plugin/ssh', require './router' 28 | -------------------------------------------------------------------------------- /plugin/ssh/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "SSH", 3 | "process": "Process", 4 | "memory": "Mem", 5 | "cpu": "CPU", 6 | "kill": "Kill", 7 | "view": { 8 | "update_password": "Update SSH password" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /plugin/ssh/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "SSH", 3 | "process": "进程", 4 | "memory": "内存", 5 | "cpu": "CPU", 6 | "kill": "Kill", 7 | "view": { 8 | "update_password": "设置 SSH 密码" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /plugin/ssh/router.coffee: -------------------------------------------------------------------------------- 1 | {child_process, express} = app.libs 2 | {requireInService} = app.middleware 3 | {cache, logger} = app 4 | 5 | module.exports = exports = express.Router() 6 | 7 | ssh = require './ssh' 8 | 9 | exports.use requireInService 'ssh' 10 | 11 | exports.post '/update_password', (req, res) -> 12 | unless /^.+$/.test req.body.password 13 | return res.error 'invalid_password' 14 | 15 | ssh.updatePassword req.account, req.body.password, -> 16 | res.json {} 17 | 18 | exports.post '/kill', (req, res) -> 19 | pid = parseInt req.body.pid 20 | 21 | ssh.killProcess req.account, pid, -> 22 | res.json {} 23 | -------------------------------------------------------------------------------- /plugin/ssh/ssh.coffee: -------------------------------------------------------------------------------- 1 | {child_process} = app.libs 2 | {cache, logger} = app 3 | 4 | exports.updatePassword = (account, password, callback) -> 5 | chpasswd = child_process.spawn 'sudo', ['chpasswd'] 6 | chpasswd.stdin.end "#{account.username}:#{password}" 7 | 8 | chpasswd.on 'error', logger.error 9 | 10 | chpasswd.on 'exit', -> 11 | callback() 12 | 13 | exports.killProcess = (account, pid, callback) -> 14 | child_process.exec "sudo su #{account.username} -c 'kill #{pid}'", (err) -> 15 | logger.error err if err 16 | 17 | cache.delete 'linux.getProcessList', -> 18 | callback() 19 | -------------------------------------------------------------------------------- /plugin/ssh/static/script/panel.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | $('.widget-ssh .action-kill').click -> 3 | if window.confirm 'Are you sure?' 4 | request '/plugin/ssh/kill', 5 | pid: $(@).parents('tr').data 'id' 6 | , => 7 | $(@).parents('tr').remove() 8 | 9 | $('.widget-ssh .action-update-password').click -> 10 | request '/plugin/ssh/update_password', 11 | password: $('.widget-ssh .input-password').val() 12 | , -> 13 | alert 'Success' 14 | -------------------------------------------------------------------------------- /plugin/ssh/view/widget.jade: -------------------------------------------------------------------------------- 1 | .row.widget-ssh 2 | header= t('') 3 | .col-md-6 4 | .panel.panel-warning 5 | .panel-heading 6 | h3.panel-title= t('view.update_password') 7 | .panel-body 8 | .input-group 9 | input.input-password.form-control(type='password') 10 | span.input-group-btn 11 | button.btn.btn-default.action-update-password(type='button')= t('common.submit') 12 | 13 | .col-md-6 14 | table(style= 'table-layout: fixed;').table.table-hover 15 | thead 16 | tr 17 | th(style= 'width: 240px;')= t('process') 18 | th= t('memory') 19 | th= t('cpu') 20 | th= t('common.actions') 21 | tbody 22 | for process in process_list 23 | tr(data-id= '#{process.pid}') 24 | td(style= 'white-space: nowrap; overflow: hidden;', title= process.command)= process.command 25 | td #{(process.rss / 1024).toFixed(1)}M 26 | td #{process.cpu_per}% 27 | td 28 | button.action-kill.btn.btn-danger.btn-xs= t('kill') 29 | -------------------------------------------------------------------------------- /plugin/supervisor/index.coffee: -------------------------------------------------------------------------------- 1 | {_} = app.libs 2 | {pluggable} = app 3 | 4 | exports = module.exports = class SupervisorPlugin extends pluggable.Plugin 5 | @NAME: 'supervisor' 6 | @type: 'service' 7 | @dependencies: ['linux'] 8 | 9 | supervisor = require './supervisor' 10 | 11 | exports.registerHook 'view.panel.scripts', 12 | path: '/plugin/supervisor/script/panel.js' 13 | 14 | exports.registerHook 'view.panel.styles', 15 | path: '/plugin/supervisor/style/panel.css' 16 | 17 | exports.registerHook 'view.panel.widgets', 18 | generator: (req, callback) -> 19 | supervisor.programsStatus (programs_status) -> 20 | exports.render 'widget', req, 21 | programs_status: _.indexBy programs_status, 'name' 22 | , callback 23 | 24 | exports.registerServiceHook 'enable', 25 | filter: (account, callback) -> 26 | account.update 27 | $set: 28 | 'pluggable.supervisor.programs': [] 29 | , callback 30 | 31 | exports.registerServiceHook 'disable', 32 | filter: (account, callback) -> 33 | supervisor.removePrograms account, -> 34 | supervisor.updateProgram account, null, -> 35 | account.update 36 | $unset: 37 | 'pluggable.supervisor': true 38 | , callback 39 | 40 | app.express.use '/plugin/supervisor', require './router' 41 | -------------------------------------------------------------------------------- /plugin/supervisor/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Supervisor", 3 | "common": { 4 | "name": "Name", 5 | "command": "Start Command", 6 | "options": "Options", 7 | "status": "Status", 8 | "directory": "Start Directory", 9 | "autostart": "Auto start with system", 10 | "autorestart": "Auto restart" 11 | }, 12 | "autorestart": { 13 | "true": "Always restart", 14 | "false": "Don't restart", 15 | "unexpected": "Restart when error" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/supervisor/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Supervisor", 3 | "common": { 4 | "name": "名称", 5 | "command": "启动命令", 6 | "options": "选项", 7 | "status": "状态", 8 | "directory": "启动目录", 9 | "autostart": "随系统启动", 10 | "autorestart": "自动重启" 11 | }, 12 | "autorestart": { 13 | "true": "总是重启", 14 | "false": "不自动重启", 15 | "unexpected": "错误时重启" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/supervisor/reconfigure.coffee: -------------------------------------------------------------------------------- 1 | {async, fs, _, child_process} = app.libs 2 | {Account} = app.models 3 | {utils} = app 4 | 5 | supervisor = require './supervisor' 6 | 7 | unless fs.existsSync "#{__dirname}/../../.backup/supervisor" 8 | fs.mkdirSync "#{__dirname}/../../.backup/supervisor", 0o750 9 | 10 | module.exports = (callback) -> 11 | async.series [ 12 | (callback) -> 13 | Account.find 14 | 'billing.service': 'supervisor' 15 | 'pluggable.supervisor.programs.0': 16 | $exists: true 17 | , (err, accounts) -> 18 | async.eachSeries accounts, (account, callback) -> 19 | async.eachSeries account.pluggable.supervisor.programs, (program, callback) -> 20 | supervisor.writeConfig account, program, callback 21 | , callback 22 | , callback 23 | 24 | (callback) -> 25 | exists_configures = _.filter fs.readdirSync('/etc/supervisor/conf.d'), (file) -> 26 | return file[ ... 1] == '@' 27 | 28 | async.eachSeries exists_configures, (filename, callback) -> 29 | [__, username, name] = filename.match /@([^-]+)-(.*)/ 30 | 31 | Account.findOne 32 | 'username': username 33 | 'billing.service': 'supervisor' 34 | 'pluggable.supervisor.programs.name': name 35 | , (err, account) -> 36 | if account 37 | return callback() 38 | else 39 | console.log "removed /etc/supervisor/conf.d/#{filename}" 40 | backup_filename = "#{__dirname}/../../.backup/supervisor/#{filename}-#{utils.randomString(5)}" 41 | child_process.exec "sudo mv /etc/supervisor/conf.d/#{filename} #{backup_filename}", callback 42 | 43 | , callback 44 | 45 | (callback) -> 46 | child_process.exec 'sudo service supervisor restart', callback 47 | 48 | ], (err) -> 49 | throw err if err 50 | callback() 51 | -------------------------------------------------------------------------------- /plugin/supervisor/router.coffee: -------------------------------------------------------------------------------- 1 | {express, ObjectID, _} = app.libs 2 | {Account} = app.models 3 | {requireInService} = app.middleware 4 | 5 | module.exports = exports = express.Router() 6 | 7 | supervisor = require './supervisor' 8 | 9 | program_sample = 10 | _id: '53c96734c2dad7d6208a0fbe' 11 | name: 'my_app' 12 | command: '/home/jysperm/app' 13 | autostart: true 14 | autorestart: 'true/false/unexpected' 15 | directory: '/home/jysperm' 16 | 17 | require_fields = ['name', 'command', 'autostart', 'autorestart'] 18 | configurable_fields = ['command', 'autostart', 'autorestart', 'directory'] 19 | 20 | restrictProgramFields = (req, res, next) -> 21 | if req.body.name 22 | unless /^[A-Za-z0-9\/\._-]+$/.test req.body.name 23 | return res.error 'invalid_name' 24 | 25 | if req.body.command 26 | unless /^.*$/.test req.body.command 27 | return res.error 'invalid_command' 28 | 29 | if req.body.autostart != undefined 30 | req.body.autostart = if req.body.autostart then true else false 31 | 32 | if req.body.autorestart 33 | unless req.body.autorestart in ['true', 'false', 'unexpected'] 34 | return res.error 'invalid_autorestart' 35 | 36 | if req.body.directory 37 | unless /^.*$/.test req.body.directory 38 | return res.error 'invalid_directory' 39 | 40 | next() 41 | 42 | exports.use requireInService 'supervisor' 43 | 44 | exports.param 'id', (req, res, next, id) -> 45 | Account.findOne 46 | 'pluggable.supervisor.programs._id': ObjectID id 47 | , (err, account) -> 48 | req.program = _.find account?.pluggable.supervisor.programs, (program) -> 49 | return program._id.toString() == id 50 | 51 | unless req.program 52 | return res.error 'program_not_exist' 53 | 54 | unless account.id == req.account.id 55 | return res.error 'program_forbidden' 56 | 57 | next() 58 | 59 | exports.post '/create_program', restrictProgramFields, (req, res) -> 60 | program = _.pick req.body, _.keys(program_sample) 61 | program._id = ObjectID() 62 | program.program_name = "@#{req.account.username}-#{program.name}" 63 | 64 | for field in require_fields 65 | unless field in _.keys req.body 66 | return res.error 'missing_field', 67 | name: field 68 | 69 | if req.body.name in req.account.pluggable.supervisor.programs 70 | return res.error 'name_exist' 71 | 72 | req.account.update 73 | $push: 74 | 'pluggable.supervisor.programs': program 75 | , (err) -> 76 | return res.error err if err 77 | 78 | supervisor.writeConfig req.account, program, -> 79 | supervisor.updateProgram req.account, program, -> 80 | res.json {} 81 | 82 | exports.post '/update_program/:id', restrictProgramFields, (req, res) -> 83 | modifier = 84 | $set: {} 85 | 86 | for k, v of _.pick req.body, configurable_fields 87 | unless v == undefined 88 | modifier.$set["pluggable.supervisor.programs.$.#{k}"] = v 89 | 90 | Account.findOneAndUpdate 91 | 'pluggable.supervisor.programs._id': req.program._id 92 | , modifier, (err, account) -> 93 | return res.error err if err 94 | 95 | program = _.find account.pluggable.supervisor.programs, (program) -> 96 | return program._id.toString() == req.program._id.toString() 97 | 98 | console.log program, account 99 | 100 | supervisor.writeConfig account, program, -> 101 | supervisor.updateProgram account, program, -> 102 | res.json {} 103 | 104 | exports.post '/remove_program/:id', (req, res) -> 105 | req.account.update 106 | $pull: 107 | 'pluggable.supervisor.programs': 108 | _id: req.program._id 109 | , (err) -> 110 | return res.error err if err 111 | 112 | supervisor.removeConfig req.account, req.program, -> 113 | supervisor.updateProgram req.account, null, -> 114 | res.json {} 115 | 116 | exports.get '/program_config/:id', (req, res) -> 117 | req.program.id = req.program._id 118 | delete req.program._id 119 | 120 | res.json req.program 121 | 122 | exports.post '/program_control/:id', (req, res) -> 123 | unless req.body.action in ['start', 'stop', 'restart'] 124 | return res.error 'invalid_action' 125 | 126 | supervisor.programControl req.account, req.program, req.body.action, -> 127 | res.json {} 128 | -------------------------------------------------------------------------------- /plugin/supervisor/static/script/panel.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | diglog = $('#supervisor-dialog') 3 | 4 | $('.widget-supervisor .action-control').click -> 5 | request "/plugin/supervisor/program_control/#{$(@).parents('tr').data('id')}", 6 | action: $(@).data 'action' 7 | , -> 8 | location.reload() 9 | 10 | $('.widget-supervisor .action-create').click -> 11 | diglog.find('.label-program-id').text '' 12 | diglog.find('.input-name').val '' 13 | diglog.find('.input-name').prop 'disabled', false 14 | diglog.find('.input-command').val '' 15 | diglog.find('.input-directory').val "/home/#{$('body').data('username')}" 16 | diglog.find('.input-autostart').prop 'checked', true 17 | diglog.find(".input-autorestart :radio[value=unexpected]").click() 18 | 19 | diglog.find('.action-submit').click -> 20 | if diglog.find('.label-program-id').text() 21 | url = "/plugin/supervisor/update_program/#{diglog.find('.label-program-id').text()}" 22 | else 23 | url= '/plugin/supervisor/create_program' 24 | 25 | request url, 26 | name: diglog.find('.input-name').val() 27 | command: diglog.find('.input-command').val() 28 | directory: diglog.find('.input-directory').val() 29 | autostart: diglog.find('.input-autostart').prop('checked') 30 | autorestart: diglog.find('.input-autorestart :radio:checked').val() 31 | , -> 32 | location.reload() 33 | 34 | $('.widget-supervisor .action-edit').click -> 35 | request "/plugin/supervisor/program_config/#{$(@).parents('tr').data('id')}", {}, 36 | method: 'get' 37 | , (program) -> 38 | diglog.find('.label-program-id').text program.id 39 | diglog.find('.input-name').val program.name 40 | diglog.find('.input-name').prop 'disabled', true 41 | diglog.find('.input-command').val program.command 42 | diglog.find('.input-directory').val program.directory 43 | diglog.find('.input-autostart').prop 'checked', program.autostart 44 | diglog.find(".input-autorestart :radio[value=#{program.autorestart}]").click() 45 | 46 | diglog.modal 'show' 47 | 48 | $('.widget-supervisor .action-remove').click -> 49 | if window.confirm 'Are you sure?' 50 | request "/plugin/supervisor/remove_program/#{$(@).parents('tr').data('id')}", {}, -> 51 | location.reload() 52 | -------------------------------------------------------------------------------- /plugin/supervisor/static/style/panel.less: -------------------------------------------------------------------------------- 1 | @media (min-width: 768px) { 2 | #supervisor-creator .modal-dialog { 3 | margin: 80px auto; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /plugin/supervisor/supervisor.coffee: -------------------------------------------------------------------------------- 1 | {async, child_process, fs, _} = app.libs 2 | {logger} = app 3 | 4 | SupervisorPlugin = require './index' 5 | 6 | exports.removePrograms = (account, callback) -> 7 | async.each account.pluggable.supervisor.programs, (program, callback) -> 8 | exports.removeConfig account, program, -> 9 | callback() 10 | , -> 11 | exports.updateProgram account, null, -> 12 | callback() 13 | 14 | exports.updateProgram = (account, program, callback) -> 15 | child_process.exec 'sudo supervisorctl update', (err) -> 16 | logger.error err if err 17 | 18 | if program and program.autostart 19 | child_process.exec "sudo supervisorctl start #{program.program_name}", (err) -> 20 | logger.error err if err 21 | callback() 22 | else 23 | callback() 24 | 25 | exports.writeConfig = (account, program, callback) -> 26 | SupervisorPlugin.renderTemplate 'program.conf', 27 | account: account 28 | program: program 29 | , (configure) -> 30 | SupervisorPlugin.writeConfigFile "/etc/supervisor/conf.d/#{program.program_name}.conf", configure, -> 31 | callback() 32 | 33 | exports.removeConfig = (account, program, callback) -> 34 | child_process.exec "sudo rm /etc/supervisor/conf.d/#{program.program_name}.conf", (err) -> 35 | logger.error err if err 36 | callback() 37 | 38 | # @param action: start|stop|restart 39 | exports.programControl = (account, program, action, callback) -> 40 | child_process.exec "sudo supervisorctl #{action} #{program.program_name}", (err) -> 41 | logger.error err if err 42 | callback() 43 | 44 | exports.programsStatus = (callback) -> 45 | child_process.exec 'sudo supervisorctl status', (err, stdout) -> 46 | lines = stdout.split '\n' 47 | lines = lines[... lines.length - 1] 48 | 49 | callback _.map lines, (line) -> 50 | [__, name, status, info] = line.match /^(\S+)\s+(\S+)\s+(.*)/ 51 | 52 | if name.match /^([^:]+):/ 53 | [__, name] = name.match /^([^:]+):/ 54 | 55 | status_mapping = 56 | STOPPED: 'stopped' 57 | STARTING: 'running' 58 | RUNNING: 'running' 59 | BACKOFF: 'stopped' 60 | STOPPING: 'running' 61 | EXITED: 'stopped' 62 | FATAL: 'stopped' 63 | UNKNOWN: 'stopped' 64 | 65 | return { 66 | name: name 67 | original_status: status 68 | status: status_mapping[status] 69 | info: info 70 | } 71 | -------------------------------------------------------------------------------- /plugin/supervisor/template/program.conf: -------------------------------------------------------------------------------- 1 | [program:<%= program.program_name %>] 2 | command = <%= program.command %> 3 | autostart = <%= program.autostart.toString() %> 4 | autorestart = <%= program.autorestart %> 5 | user = <%= account.username %> 6 | redirect_stderr = true 7 | <% if (program.stdout_logfile !== false) { %> 8 | stdout_logfile = /home/<%= account.username %>/supervisor-<%= program.name %>.log 9 | <% } %> 10 | <% if(program.directory) { %> 11 | directory = <%= program.directory %> 12 | <% } %> 13 | -------------------------------------------------------------------------------- /plugin/supervisor/test/supervisor.test.coffee: -------------------------------------------------------------------------------- 1 | (if isPluginEnable('supervisor') then describe else describe.skip) 'plugin/supervisor', -> 2 | describe 'router', -> 3 | it 'POST update_program' 4 | 5 | it 'GET program_config' 6 | 7 | it 'POST program_control' 8 | 9 | describe 'programSummary', -> 10 | it 'pending' 11 | 12 | describe 'writeConfig', -> 13 | it 'pending' 14 | 15 | describe 'programStatus', -> 16 | it 'pending' 17 | 18 | describe 'updateProgram', -> 19 | it 'pending' 20 | 21 | describe 'programControl', -> 22 | it 'pending' 23 | 24 | describe 'removeConfig', -> 25 | it 'pending' 26 | 27 | describe 'removePrograms', -> 28 | it 'pending' 29 | -------------------------------------------------------------------------------- /plugin/supervisor/view/widget.jade: -------------------------------------------------------------------------------- 1 | .row.widget-supervisor 2 | header= t('') 3 | table.table.table-hover 4 | thead 5 | tr 6 | th= t('common.name') 7 | th= t('common.command') 8 | th= t('common.options') 9 | th= t('common.status') 10 | th 11 | button.btn.btn-success.btn-xs.action-create(data-toggle='modal', data-target='#supervisor-dialog') 12 | span.glyphicon.glyphicon-plus-sign 13 | 14 | tbody 15 | for program in account.pluggable.supervisor.programs 16 | tr(data-id='#{program._id}') 17 | td= program.name 18 | td= program.command 19 | td 20 | if program.autostart 21 | span(title='autostart=true').glyphicon.glyphicon-share-alt 22 | |   23 | if program.autorestart == 'true' 24 | span(title='autorestart=true').glyphicon.glyphicon-repeat 25 | |   26 | else if program.autorestart == 'unexpected' 27 | span(title='autorestart=unexpected').glyphicon.glyphicon-flash 28 | |   29 | if program.directory 30 | span(title='directory') ~#{program.directory.match(/\/home\/[^\/]+(.*)/)[1]} 31 | td(title= programs_status[program.program_name].info)= programs_status[program.program_name].status 32 | td 33 | if programs_status[program.program_name].status != 'running' 34 | button.btn.btn-success.btn-xs.action-control(type='button', data-action='start') 35 | span.glyphicon.glyphicon-play 36 | if programs_status[program.program_name].status == 'running' 37 | button.btn.btn-warning.btn-xs.action-control(type='button', data-action='restart') 38 | span.glyphicon.glyphicon-repeat 39 | button.btn.btn-danger.btn-xs.action-control(type='button', data-action='stop') 40 | span.glyphicon.glyphicon-stop 41 | button.btn.btn-info.btn-xs.action-edit(type='button') 42 | span.glyphicon.glyphicon-edit 43 | button.btn.btn-danger.btn-xs.action-remove(type='button') 44 | span.glyphicon.glyphicon-trash 45 | 46 | #supervisor-dialog.modal.fade(tabindex='-1', role='dialog', aria-hidden='true') 47 | .modal-dialog 48 | .modal-content 49 | .modal-header 50 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 51 | h3.modal-title 52 | | #{t('')}   53 | span.small.label-program-id 54 | .modal-body 55 | form.form-horizontal(role='form') 56 | .form-group 57 | label.col-sm-2.control-label= t('common.name') 58 | .col-sm-10 59 | input.form-control.input-name(type='text', name='name', placeholder='myapp') 60 | .form-group 61 | label.col-sm-2.control-label= t('common.command') 62 | .col-sm-10 63 | input.form-control.input-command(type='text', name='command', placeholder='node /home/jysperm/app.js') 64 | .form-group 65 | label.col-sm-2.control-label= t('common.directory') 66 | .col-sm-10 67 | input.form-control.input-directory(type='text', name='directory', placeholder='/home/jysperm') 68 | .form-group 69 | label.col-sm-2.control-label » 70 | .checkbox.col-sm-10 71 | label 72 | input.input-autostart(type='checkbox', name='autostart', checked) 73 | | #{t('common.autostart')} 74 | .form-group 75 | label.col-sm-2.control-label » 76 | .controls.col-sm-10.input-autorestart 77 | .radio 78 | label 79 | input(type='radio', name='autorestart', value='unexpected', checked) 80 | | #{t('autorestart.unexpected')} 81 | .radio 82 | label 83 | input(type='radio', name='autorestart', value='true') 84 | | #{t('autorestart.true')} 85 | .radio 86 | label 87 | input(type='radio', name='autorestart', value='false') 88 | | #{t('autorestart.false')} 89 | 90 | .modal-footer 91 | button.btn.btn-success.action-submit(type='button')= t('common.save') 92 | -------------------------------------------------------------------------------- /plugin/wiki/index.coffee: -------------------------------------------------------------------------------- 1 | {fs, path} = app.libs 2 | {pluggable, config} = app 3 | 4 | exports = module.exports = class WikiPlugin extends pluggable.Plugin 5 | @NAME: 'wiki' 6 | @type: 'extension' 7 | 8 | exports.registerHook 'view.layout.menu_bar', 9 | href: '/wiki/' 10 | t_body: 'plugins.wiki.' 11 | 12 | unless config.plugins.wiki?.disable_default_wiki 13 | wiki_path = "#{__dirname}/../../WIKI" 14 | 15 | for category_name in fs.readdirSync(wiki_path) 16 | for file_name in fs.readdirSync("#{wiki_path}/#{category_name}") 17 | exports.registerHook 'plugin.wiki.pages', 18 | t_category: category_name 19 | t_title: file_name 20 | language: 'zh_CN' 21 | content_markdown: fs.readFileSync("#{wiki_path}/#{category_name}/#{file_name}").toString() 22 | 23 | app.express.use '/wiki', require './wiki' 24 | -------------------------------------------------------------------------------- /plugin/wiki/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "User Manual" 3 | } 4 | -------------------------------------------------------------------------------- /plugin/wiki/locale/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "用户手册" 3 | } 4 | -------------------------------------------------------------------------------- /plugin/wiki/test/wiki.test.coffee: -------------------------------------------------------------------------------- 1 | (if isPluginEnable('wiki') then describe else describe.skip) 'plugin/wiki', -> 2 | agent = null 3 | 4 | before -> 5 | agent = supertest.agent app.express 6 | 7 | describe 'router', -> 8 | it 'GET /', (done) -> 9 | agent.get '/wiki' 10 | .expect 200 11 | .end done 12 | 13 | it 'GET /:category/:title', (done) -> 14 | agent.get '/wiki/FAQ/Billing.md' 15 | .expect if config.plugins.wiki?.disable_default_wiki then 404 else 200 16 | .end done 17 | 18 | it 'GET /:category/:title when not exist', (done) -> 19 | agent.get '/wiki/FAQ/not_exist' 20 | .expect 404 21 | .end done 22 | -------------------------------------------------------------------------------- /plugin/wiki/view/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../core/view/layout 2 | 3 | prepend header 4 | title #{t('')} | #{t(config.web.t_name)} 5 | 6 | block main 7 | for category in category_list 8 | h2= category.category 9 | ul 10 | for page in category.pages 11 | li 12 | a(href="/wiki/#{category.t_category}/#{page.t_title}")= page.title 13 | |   (#{page.language}) 14 | -------------------------------------------------------------------------------- /plugin/wiki/view/page.jade: -------------------------------------------------------------------------------- 1 | extends ../../../core/view/layout 2 | 3 | prepend header 4 | title #{title} | #{t(config.web.t_name)} 5 | 6 | block main 7 | header= title 8 | != content 9 | -------------------------------------------------------------------------------- /plugin/wiki/wiki.coffee: -------------------------------------------------------------------------------- 1 | {markdown, path, jade, fs, _, express} = app.libs 2 | {pluggable} = app 3 | 4 | WikiPlugin = require './index' 5 | 6 | module.exports = exports = express.Router() 7 | 8 | exports.get '/', (req, res) -> 9 | pages = pluggable.selectHook req.account, 'plugin.wiki.pages' 10 | 11 | pages_by_category = {} 12 | 13 | for page in pages 14 | page.title = res.t page.t_title 15 | 16 | pages_by_category[page.t_category] ?= [] 17 | pages_by_category[page.t_category].push page 18 | 19 | result = [] 20 | 21 | for category_name, pages of pages_by_category 22 | result.push 23 | t_category: category_name 24 | category: res.t category_name 25 | pages: pages 26 | 27 | view_data = _.extend res.locals, 28 | category_list: result 29 | 30 | WikiPlugin.render 'index', req, view_data, (html) -> 31 | res.send html 32 | 33 | exports.get '/:category/:title', (req, res) -> 34 | matched_page = _.findWhere pluggable.selectHook(req.account, 'plugin.wiki.pages'), 35 | t_category: req.params.category 36 | t_title: req.params.title 37 | 38 | unless matched_page 39 | return res.status(404).end() 40 | 41 | view_data = _.extend res.locals, 42 | title: res.t matched_page.t_title 43 | content: markdown.toHTML matched_page.content_markdown 44 | 45 | WikiPlugin.render 'page', req, view_data, (html) -> 46 | res.send html 47 | -------------------------------------------------------------------------------- /sample/core.config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | web: 3 | t_name: 'plugins.rpvhost.' 4 | url: 'http://rp.rpvhost.net' 5 | listen: '/home/rpadmin/rootpanel.sock' 6 | repo: 'jysperm/RootPanel' 7 | google_analytics_id: '' 8 | 9 | account: 10 | cookie_time: 30 * 24 * 3600 * 1000 11 | 12 | i18n: 13 | available_language: ['zh_CN', 'en'] 14 | default_language: 'zh_CN' 15 | default_timezone: 'Asia/Shanghai' 16 | 17 | plugin: 18 | available_extensions: [] 19 | available_services: [] 20 | 21 | billing: 22 | currency: 'CNY' 23 | 24 | force_freeze: 25 | when_balance_below: 0 26 | when_arrears_above: 0 27 | 28 | billing_cycle: 10 * 60 * 1000 29 | 30 | plans: 31 | sample: 32 | t_name: 'plans.sample.name' 33 | t_description: 'plans.sample.description' 34 | 35 | billing_by_time: 36 | unit: 24 * 3600 * 1000 37 | price: 10 / 30 38 | 39 | services: [] 40 | resources: {} 41 | 42 | test: 43 | t_name: 'plans.test.name' 44 | t_description: 'plans.test.description' 45 | 46 | services: [] 47 | resources: {} 48 | 49 | mongodb: 50 | user: 'rpadmin' 51 | password: 'password' 52 | host: 'localhost' 53 | name: 'RootPanel' 54 | 55 | redis: 56 | host: '127.0.0.1' 57 | port: 6379 58 | password: 'password' 59 | prefix: 'RP' 60 | 61 | email: 62 | send_from: 'robot@rpvhost.net' 63 | 64 | account: 65 | service: 'Postmark' 66 | auth: 67 | user: 'postmark-api-token' 68 | pass: 'postmark-api-token' 69 | 70 | plugins: {} 71 | -------------------------------------------------------------------------------- /sample/rpvhost.config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | web: 3 | t_name: 'plugins.rpvhost.' 4 | url: 'http://rp.rpvhost.net' 5 | listen: '/home/rpadmin/rootpanel.sock' 6 | repo: 'jysperm/RootPanel' 7 | google_analytics_id: '' 8 | 9 | account: 10 | cookie_time: 30 * 24 * 3600 * 1000 11 | 12 | i18n: 13 | available_language: ['zh_CN', 'en'] 14 | default_language: 'zh_CN' 15 | default_timezone: 'Asia/Shanghai' 16 | 17 | plugin: 18 | available_extensions: ['bitcoin', 'wiki', 'rpvhost'] 19 | available_services: ['linux', 'supervisor', 'ssh'] 20 | 21 | billing: 22 | currency: 'CNY' 23 | 24 | force_freeze: 25 | when_balance_below: 0 26 | when_arrears_above: 0 27 | 28 | billing_cycle: 10 * 60 * 1000 29 | 30 | plans: 31 | all: 32 | t_name: 'plugins.rpvhost.plans.all.name' 33 | t_description: 'plugins.rpvhost.plans.all.description' 34 | 35 | billing_by_time: 36 | unit: 24 * 3600 * 1000 37 | price: 10 / 30 38 | 39 | services: ['supervisor', 'linux', 'ssh'] 40 | 41 | resources: 42 | cpu: 144 43 | storage: 520 44 | transfer: 39 45 | memory: 27 46 | 47 | mongodb: 48 | user: 'rpadmin' 49 | password: 'password' 50 | host: 'localhost' 51 | name: 'RootPanel' 52 | 53 | redis: 54 | host: '127.0.0.1' 55 | port: 6379 56 | password: 'password' 57 | prefix: 'RP' 58 | 59 | email: 60 | send_from: 'robot@rpvhost.net' 61 | 62 | account: 63 | service: 'Postmark' 64 | auth: 65 | user: 'postmark-api-token' 66 | pass: 'postmark-api-token' 67 | 68 | plugins: 69 | bitcoin: 70 | coinbase_api_key: 'coinbase-simple-api-key' 71 | 72 | rpvhost: 73 | index_page: true 74 | taobao_item_id: '38370649858' 75 | 76 | linux: 77 | monitor_cycle: 30 * 1000 78 | -------------------------------------------------------------------------------- /sample/shadowsocks.config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | web: 3 | t_name: 'plugins.rpvhost.greenshadow' 4 | url: 'http://greenshadow.net' 5 | listen: '/home/rpadmin/rootpanel.sock' 6 | repo: 'jysperm/RootPanel' 7 | google_analytics_id: '' 8 | 9 | account: 10 | cookie_time: 30 * 24 * 3600 * 1000 11 | 12 | i18n: 13 | available_language: ['zh_CN', 'en'] 14 | default_language: 'zh_CN' 15 | default_timezone: 'Asia/Shanghai' 16 | 17 | plugin: 18 | available_extensions: ['bitcoin', 'wiki', 'rpvhost'] 19 | available_services: ['linux', 'supervisor', 'shadowsocks'] 20 | 21 | billing: 22 | currency: 'CNY' 23 | 24 | force_freeze: 25 | when_balance_below: 0 26 | when_arrears_above: 0 27 | 28 | billing_cycle: 10 * 60 * 1000 29 | 30 | plans: 31 | shadowsocks: 32 | t_name: 'plugins.rpvhost.plans.shadowsocks.name' 33 | t_description: 'plugins.rpvhost.plans.shadowsocks.description' 34 | 35 | services: ['shadowsocks'] 36 | 37 | mongodb: 38 | user: 'rpadmin' 39 | password: 'password' 40 | host: 'localhost' 41 | name: 'RootPanel' 42 | 43 | redis: 44 | host: '127.0.0.1' 45 | port: 6379 46 | password: 'password' 47 | prefix: 'RP' 48 | 49 | email: 50 | send_from: 'robot@rpvhost.net' 51 | 52 | account: 53 | service: 'Postmark' 54 | auth: 55 | user: 'postmark-api-token' 56 | pass: 'postmark-api-token' 57 | 58 | plugins: 59 | linux: 60 | monitor_cycle: null 61 | 62 | bitcoin: 63 | coinbase_api_key: 'coinbase-simple-api-key' 64 | 65 | wiki: 66 | disable_default_wiki: true 67 | 68 | rpvhost: 69 | index_page: false 70 | green_style: true 71 | taobao_item_id: '41040606505' 72 | 73 | shadowsocks: 74 | available_ciphers: ['aes-256-cfb', 'rc4-md5'] 75 | 76 | billing_bucket: 100 * 1000 * 1000 77 | monitor_cycle: 5 * 60 * 1000 78 | price_bucket: 0.06 79 | -------------------------------------------------------------------------------- /test/env.coffee: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test' 2 | 3 | global.config = require '../config' 4 | 5 | global._ = require 'underscore' 6 | global.fs = require 'fs' 7 | global.async = require 'async' 8 | global.deepmerge = require 'deepmerge' 9 | global.chai = require 'chai' 10 | global.supertest = require 'supertest' 11 | global.ObjectId = (require 'mongoose').Types.ObjectId 12 | 13 | if process.env.COV_TEST == 'true' 14 | require('coffee-coverage').register 15 | path: 'relative' 16 | basePath: "#{__dirname}/../.." 17 | exclude: do -> 18 | result = ['test', 'node_modules', '.git', 'sample', 'core/static', 'migration/database'] 19 | 20 | for plugin_name in _.union config.plugin.available_extensions, config.plugin.available_services 21 | result.push "plugin/#{plugin_name}/test" 22 | 23 | return result 24 | 25 | global.expect = chai.expect 26 | 27 | global.created_objects = 28 | accounts: [] 29 | couponcodes: [] 30 | tickets: [] 31 | 32 | global.namespace = {} 33 | 34 | chai.should() 35 | chai.config.includeStack = true 36 | 37 | config.web.listen = 12558 38 | 39 | if process.env.TRAVIS == 'true' 40 | config.mongodb.user = undefined 41 | config.mongodb.password = undefined 42 | 43 | config.redis.password = undefined 44 | 45 | global.isPluginEnable = (name) -> 46 | return name in _.union config.plugin.available_extensions, config.plugin.available_services 47 | -------------------------------------------------------------------------------- /test/full-test.coffee: -------------------------------------------------------------------------------- 1 | child_process = require 'child_process' 2 | async = require 'async' 3 | fs = require 'fs' 4 | _ = require 'underscore' 5 | 6 | async.eachSeries fs.readdirSync("#{__dirname}/../sample"), (filename, callback) -> 7 | fs.writeFileSync "#{__dirname}/../config.coffee", fs.readFileSync "#{__dirname}/../sample/#{filename}" 8 | 9 | console.log "Config: #{filename}" 10 | 11 | params = '--compilers coffee:coffee-script/register --require test/env --reporter node_modules/mocha-reporter-cov-summary -- 12 | core/test/*.test.coffee core/test/*/*.test.coffee plugin/*/test/*.test.coffee'.split(' ') 13 | 14 | proc = child_process.spawn "#{__dirname}/../node_modules/.bin/mocha", params, 15 | env: _.extend process.env, 16 | COV_TEST: 'true' 17 | 18 | proc.stdout.pipe process.stdout 19 | proc.stderr.pipe process.stderr 20 | 21 | proc.on 'close', (code) -> 22 | if code 23 | process.exit code 24 | else 25 | callback() 26 | 27 | , -> 28 | process.exit 0 29 | --------------------------------------------------------------------------------