mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into ha-docker
# Conflicts: # app/coffee/Features/Email/EmailBuilder.coffee
This commit is contained in:
commit
9a3c1c7d22
950 changed files with 395957 additions and 41085 deletions
22
services/web/.gitignore
vendored
22
services/web/.gitignore
vendored
|
@ -47,23 +47,21 @@ TpdsWorker.js
|
|||
BackgroundJobsWorker.js
|
||||
UserAndProjectPopulator.coffee
|
||||
|
||||
public/js/history/versiondetail.js
|
||||
!public/js/libs/
|
||||
public/js/*
|
||||
!public/js/ace/*
|
||||
!public/js/libs/
|
||||
public/js/*.js
|
||||
public/js/libs/sharejs.js
|
||||
public/js/editor.js
|
||||
public/js/home.js
|
||||
public/js/forms.js
|
||||
public/js/gui.js
|
||||
public/js/admin.js
|
||||
public/js/history/*
|
||||
public/js/analytics/
|
||||
public/js/directives/
|
||||
public/js/filters/
|
||||
public/js/ide/
|
||||
public/js/main/
|
||||
public/js/modules/
|
||||
public/js/services/
|
||||
public/js/utils/
|
||||
|
||||
public/stylesheets/style.css
|
||||
public/brand/plans.css
|
||||
public/minjs/
|
||||
|
||||
public/js/main.js
|
||||
Gemfile.lock
|
||||
|
||||
*.swp
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
fs = require "fs"
|
||||
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
|
||||
|
||||
module.exports = (grunt) ->
|
||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||
|
@ -157,12 +158,13 @@ module.exports = (grunt) ->
|
|||
inlineText: false
|
||||
preserveLicenseComments: false
|
||||
paths:
|
||||
"moment": "libs/moment-2.9.0"
|
||||
"moment": "libs/#{PackageVersions.lib('moment')}"
|
||||
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
|
||||
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
|
||||
"pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
|
||||
"ace": "#{PackageVersions.lib('ace')}"
|
||||
shim:
|
||||
"libs/pdf":
|
||||
deps: ["libs/pdfjs-1.3.91/compatibility"]
|
||||
"pdfjs-dist/build/pdf":
|
||||
deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"]
|
||||
|
||||
skipDirOptimize: true
|
||||
modules: [
|
||||
|
@ -171,11 +173,13 @@ module.exports = (grunt) ->
|
|||
exclude: ["libs"]
|
||||
}, {
|
||||
name: "ide",
|
||||
exclude: ["libs", "libs/pdf"]
|
||||
exclude: ["libs", "pdfjs-dist/build/pdf"]
|
||||
}, {
|
||||
name: "libs"
|
||||
},{
|
||||
name: "ace/mode-latex"
|
||||
},{
|
||||
name: "ace/worker-latex"
|
||||
}
|
||||
|
||||
]
|
||||
|
@ -380,8 +384,8 @@ module.exports = (grunt) ->
|
|||
|
||||
grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks)
|
||||
|
||||
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
|
||||
grunt.registerTask 'runq', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec']
|
||||
grunt.registerTask 'run:watch', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
|
||||
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec']
|
||||
|
||||
grunt.registerTask 'default', 'run'
|
||||
|
||||
|
|
|
@ -4,4 +4,4 @@ module.exports = AnalyticsController =
|
|||
recordEvent: (req, res, next) ->
|
||||
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204
|
||||
res.send 204
|
||||
|
|
|
@ -1,43 +1,48 @@
|
|||
Settings = require "settings-sharelatex"
|
||||
settings = require "settings-sharelatex"
|
||||
logger = require "logger-sharelatex"
|
||||
_ = require "underscore"
|
||||
request = require "request"
|
||||
|
||||
if !Settings.analytics?.postgres?
|
||||
module.exports =
|
||||
recordEvent: (user_id, event, segmentation, callback = () ->) ->
|
||||
logger.log {user_id, event, segmentation}, "no event tracking configured, logging event"
|
||||
callback()
|
||||
else
|
||||
Sequelize = require "sequelize"
|
||||
options = _.extend {logging:false}, Settings.analytics.postgres
|
||||
|
||||
sequelize = new Sequelize(
|
||||
Settings.analytics.postgres.database,
|
||||
Settings.analytics.postgres.username,
|
||||
Settings.analytics.postgres.password,
|
||||
options
|
||||
)
|
||||
|
||||
Event = sequelize.define("Event", {
|
||||
user_id: Sequelize.STRING,
|
||||
event: Sequelize.STRING,
|
||||
segmentation: Sequelize.JSONB
|
||||
})
|
||||
makeRequest = (opts, callback)->
|
||||
if settings.apis?.analytics?.url?
|
||||
urlPath = opts.url
|
||||
opts.url = "#{settings.apis.analytics.url}#{urlPath}"
|
||||
request opts, callback
|
||||
else
|
||||
callback()
|
||||
|
||||
module.exports =
|
||||
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
|
||||
if user_id? and typeof(user_id) != "string"
|
||||
user_id = user_id.toString()
|
||||
if user_id == Settings.smokeTest?.userId
|
||||
# Don't record smoke tests analytics
|
||||
return callback()
|
||||
Event
|
||||
.create({ user_id, event, segmentation })
|
||||
.then(
|
||||
(result) -> callback(),
|
||||
(error) ->
|
||||
logger.err {err: error, user_id, event, segmentation}, "error recording analytics event"
|
||||
callback(error)
|
||||
)
|
||||
|
||||
sync: () -> sequelize.sync()
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
|
||||
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
|
||||
if user_id+"" == settings.smokeTest?.userId+""
|
||||
return callback()
|
||||
opts =
|
||||
body:
|
||||
event:event
|
||||
segmentation:segmentation
|
||||
json:true
|
||||
method:"POST"
|
||||
timeout:1000
|
||||
url: "/user/#{user_id}/event"
|
||||
makeRequest opts, callback
|
||||
|
||||
|
||||
getLastOccurance: (user_id, event, callback = (error) ->) ->
|
||||
opts =
|
||||
body:
|
||||
event:event
|
||||
json:true
|
||||
method:"POST"
|
||||
timeout:1000
|
||||
url: "/user/#{user_id}/event/last_occurnace"
|
||||
makeRequest opts, (err, response, body)->
|
||||
if err?
|
||||
console.log response, opts
|
||||
logger.err {user_id, err}, "error getting last occurance of event"
|
||||
return callback err
|
||||
else
|
||||
return callback null, body
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
AnnouncementsHandler = require("./AnnouncementsHandler")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
logger = require("logger-sharelatex")
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
module.exports =
|
||||
|
||||
getUndreadAnnouncements: (req, res, next)->
|
||||
if !settings?.apis?.analytics?.url? or !settings.apis.blog.url?
|
||||
return res.json []
|
||||
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "getting unread announcements"
|
||||
AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)->
|
||||
if err?
|
||||
logger.err {err, user_id}, "unable to get unread announcements"
|
||||
next(err)
|
||||
else
|
||||
res.json announcements
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
AnalyticsManager = require("../Analytics/AnalyticsManager")
|
||||
BlogHandler = require("../Blog/BlogHandler")
|
||||
async = require("async")
|
||||
_ = require("lodash")
|
||||
logger = require("logger-sharelatex")
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
module.exports =
|
||||
|
||||
getUnreadAnnouncements : (user_id, callback = (err, announcements)->)->
|
||||
async.parallel {
|
||||
lastEvent: (cb)->
|
||||
AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb
|
||||
announcements: (cb)->
|
||||
BlogHandler.getLatestAnnouncements cb
|
||||
}, (err, results)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting unread announcements"
|
||||
return callback(err)
|
||||
|
||||
announcements = _.sortBy(results.announcements, "date").reverse()
|
||||
|
||||
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
|
||||
|
||||
announcementIndex = _.findIndex announcements, (announcement)->
|
||||
announcement.id == lastSeenBlogId
|
||||
|
||||
announcements = _.map announcements, (announcement, index)->
|
||||
if announcementIndex == -1
|
||||
read = false
|
||||
else if index >= announcementIndex
|
||||
read = true
|
||||
else
|
||||
read = false
|
||||
announcement.read = read
|
||||
return announcement
|
||||
|
||||
logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements"
|
||||
|
||||
callback null, announcements
|
|
@ -11,63 +11,126 @@ basicAuth = require('basic-auth-connect')
|
|||
UserHandler = require("../User/UserHandler")
|
||||
UserSessionsManager = require("../User/UserSessionsManager")
|
||||
Analytics = require "../Analytics/AnalyticsManager"
|
||||
passport = require 'passport'
|
||||
|
||||
module.exports = AuthenticationController =
|
||||
login: (req, res, next = (error) ->) ->
|
||||
AuthenticationController.doLogin req.body, req, res, next
|
||||
|
||||
doLogin: (options, req, res, next) ->
|
||||
email = options.email?.toLowerCase()
|
||||
password = options.password
|
||||
redir = Url.parse(options.redir or "/project").path
|
||||
serializeUser: (user, callback) ->
|
||||
lightUser =
|
||||
_id: user._id
|
||||
first_name: user.first_name
|
||||
last_name: user.last_name
|
||||
isAdmin: user.isAdmin
|
||||
email: user.email
|
||||
referal_id: user.referal_id
|
||||
session_created: (new Date()).toISOString()
|
||||
ip_address: user._login_req_ip
|
||||
callback(null, lightUser)
|
||||
|
||||
deserializeUser: (user, cb) ->
|
||||
cb(null, user)
|
||||
|
||||
afterLoginSessionSetup: (req, user, callback=(err)->) ->
|
||||
req.login user, (err) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id, err}, "error from req.login"
|
||||
return callback(err)
|
||||
# Regenerate the session to get a new sessionID (cookie value) to
|
||||
# protect against session fixation attacks
|
||||
oldSession = req.session
|
||||
req.session.destroy (err) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id, err}, "error when trying to destroy old session"
|
||||
return callback(err)
|
||||
req.sessionStore.generate(req)
|
||||
for key, value of oldSession
|
||||
req.session[key] = value
|
||||
# copy to the old `session.user` location, for backward-comptability
|
||||
req.session.user = req.session.passport.user
|
||||
req.session.save (err) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id}, "error saving regenerated session after login"
|
||||
return callback(err)
|
||||
UserSessionsManager.trackSession(user, req.sessionID, () ->)
|
||||
callback(null)
|
||||
|
||||
passportLogin: (req, res, next) ->
|
||||
# This function is middleware which wraps the passport.authenticate middleware,
|
||||
# so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
|
||||
# and send a `{redir: ""}` response on success
|
||||
passport.authenticate('local', (err, user, info) ->
|
||||
if err?
|
||||
return next(err)
|
||||
if user # `user` is either a user object or false
|
||||
redir = AuthenticationController._getRedirectFromSession(req) || "/project"
|
||||
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
|
||||
if err?
|
||||
return next(err)
|
||||
AuthenticationController._clearRedirectFromSession(req)
|
||||
res.json {redir: redir}
|
||||
else
|
||||
res.json message: info
|
||||
)(req, res, next)
|
||||
|
||||
doPassportLogin: (req, username, password, done) ->
|
||||
email = username.toLowerCase()
|
||||
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
|
||||
return done(err) if err?
|
||||
if !isAllowed
|
||||
logger.log email:email, "too many login requests"
|
||||
res.statusCode = 429
|
||||
return res.send
|
||||
message:
|
||||
text: req.i18n.translate("to_many_login_requests_2_mins"),
|
||||
type: 'error'
|
||||
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
|
||||
AuthenticationManager.authenticate email: email, password, (error, user) ->
|
||||
return next(error) if error?
|
||||
return done(error) if error?
|
||||
if user?
|
||||
UserHandler.setupLoginData user, ->
|
||||
LoginRateLimiter.recordSuccessfulLogin email
|
||||
AuthenticationController._recordSuccessfulLogin user._id
|
||||
AuthenticationController.establishUserSession req, user, (error) ->
|
||||
return next(error) if error?
|
||||
req.session.justLoggedIn = true
|
||||
logger.log email: email, user_id: user._id.toString(), "successful log in"
|
||||
Analytics.recordEvent user._id, "user-logged-in"
|
||||
res.json redir: redir
|
||||
# async actions
|
||||
UserHandler.setupLoginData(user, ()->)
|
||||
LoginRateLimiter.recordSuccessfulLogin(email)
|
||||
AuthenticationController._recordSuccessfulLogin(user._id)
|
||||
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
|
||||
logger.log email: email, user_id: user._id.toString(), "successful log in"
|
||||
req.session.justLoggedIn = true
|
||||
# capture the request ip for use when creating the session
|
||||
user._login_req_ip = req.ip
|
||||
return done(null, user)
|
||||
else
|
||||
AuthenticationController._recordFailedLogin()
|
||||
logger.log email: email, "failed log in"
|
||||
res.json message:
|
||||
text: req.i18n.translate("email_or_password_wrong_try_again"),
|
||||
type: 'error'
|
||||
return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
|
||||
|
||||
getLoggedInUserId: (req, callback = (error, user_id) ->) ->
|
||||
if req?.session?.user?._id?
|
||||
callback null, req.session.user._id.toString()
|
||||
setInSessionUser: (req, props) ->
|
||||
for key, value of props
|
||||
if req?.session?.passport?.user?
|
||||
req.session.passport.user[key] = value
|
||||
if req?.session?.user?
|
||||
req.session.user[key] = value
|
||||
|
||||
isUserLoggedIn: (req) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
return (user_id not in [null, undefined, false])
|
||||
|
||||
# TODO: perhaps should produce an error if the current user is not present
|
||||
getLoggedInUserId: (req) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
if user
|
||||
return user._id
|
||||
else
|
||||
callback null, null
|
||||
return null
|
||||
|
||||
getLoggedInUser: (req, callback = (error, user) ->) ->
|
||||
if req.session?.user?._id?
|
||||
query = req.session.user._id
|
||||
getSessionUser: (req) ->
|
||||
if req?.session?.user?
|
||||
return req.session.user
|
||||
else if req?.session?.passport?.user
|
||||
return req.session.passport.user
|
||||
else
|
||||
return callback null, null
|
||||
|
||||
UserGetter.getUser query, callback
|
||||
return null
|
||||
|
||||
requireLogin: () ->
|
||||
doRequest = (req, res, next = (error) ->) ->
|
||||
if !req.session.user?
|
||||
if !AuthenticationController.isUserLoggedIn(req)
|
||||
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
|
||||
else
|
||||
req.user = req.session.user
|
||||
return next()
|
||||
req.user = AuthenticationController.getSessionUser(req)
|
||||
next()
|
||||
|
||||
return doRequest
|
||||
|
||||
|
@ -81,10 +144,11 @@ module.exports = AuthenticationController =
|
|||
|
||||
if req.headers['authorization']?
|
||||
return AuthenticationController.httpAuth(req, res, next)
|
||||
else if req.session.user?
|
||||
else if AuthenticationController.isUserLoggedIn(req)
|
||||
return next()
|
||||
else
|
||||
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
|
||||
AuthenticationController._setRedirectInSession(req)
|
||||
return res.redirect "/login"
|
||||
|
||||
httpAuth: basicAuth (user, pass)->
|
||||
|
@ -94,22 +158,23 @@ module.exports = AuthenticationController =
|
|||
return isValid
|
||||
|
||||
_redirectToLoginOrRegisterPage: (req, res)->
|
||||
if req.query.zipUrl? or req.query.project_name?
|
||||
if (req.query.zipUrl? or
|
||||
req.query.project_name? or
|
||||
req.path == '/user/subscription/new')
|
||||
return AuthenticationController._redirectToRegisterPage(req, res)
|
||||
else
|
||||
AuthenticationController._redirectToLoginPage(req, res)
|
||||
|
||||
|
||||
_redirectToLoginPage: (req, res) ->
|
||||
logger.log url: req.url, "user not logged in so redirecting to login page"
|
||||
req.query.redir = req.path
|
||||
AuthenticationController._setRedirectInSession(req)
|
||||
url = "/login?#{querystring.stringify(req.query)}"
|
||||
res.redirect url
|
||||
Metrics.inc "security.login-redirect"
|
||||
|
||||
_redirectToRegisterPage: (req, res) ->
|
||||
logger.log url: req.url, "user not logged in so redirecting to register page"
|
||||
req.query.redir = req.path
|
||||
AuthenticationController._setRedirectInSession(req)
|
||||
url = "/register?#{querystring.stringify(req.query)}"
|
||||
res.redirect url
|
||||
Metrics.inc "security.login-redirect"
|
||||
|
@ -127,25 +192,15 @@ module.exports = AuthenticationController =
|
|||
Metrics.inc "user.login.failed"
|
||||
callback()
|
||||
|
||||
establishUserSession: (req, user, callback = (error) ->) ->
|
||||
lightUser =
|
||||
_id: user._id
|
||||
first_name: user.first_name
|
||||
last_name: user.last_name
|
||||
isAdmin: user.isAdmin
|
||||
email: user.email
|
||||
referal_id: user.referal_id
|
||||
session_created: (new Date()).toISOString()
|
||||
ip_address: req.ip
|
||||
# Regenerate the session to get a new sessionID (cookie value) to
|
||||
# protect against session fixation attacks
|
||||
oldSession = req.session
|
||||
req.session.destroy()
|
||||
req.sessionStore.generate(req)
|
||||
for key, value of oldSession
|
||||
req.session[key] = value
|
||||
_setRedirectInSession: (req, value) ->
|
||||
if !value?
|
||||
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
|
||||
if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
|
||||
req.session.postLoginRedirect = value
|
||||
|
||||
req.session.user = lightUser
|
||||
_getRedirectFromSession: (req) ->
|
||||
return req?.session?.postLoginRedirect || null
|
||||
|
||||
UserSessionsManager.trackSession(user, req.sessionID, () ->)
|
||||
callback()
|
||||
_clearRedirectFromSession: (req) ->
|
||||
if req.session?
|
||||
delete req.session.postLoginRedirect
|
||||
|
|
|
@ -29,6 +29,9 @@ module.exports = AuthenticationManager =
|
|||
callback null, null
|
||||
|
||||
setUserPassword: (user_id, password, callback = (error) ->) ->
|
||||
if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length
|
||||
return callback("password is too long")
|
||||
|
||||
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
|
||||
return callback(error) if error?
|
||||
bcrypt.hash password, salt, (error, hash) ->
|
||||
|
|
|
@ -3,6 +3,7 @@ async = require "async"
|
|||
logger = require "logger-sharelatex"
|
||||
ObjectId = require("mongojs").ObjectId
|
||||
Errors = require "../Errors/Errors"
|
||||
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
|
||||
module.exports = AuthorizationMiddlewear =
|
||||
ensureUserCanReadMultipleProjects: (req, res, next) ->
|
||||
|
@ -20,7 +21,7 @@ module.exports = AuthorizationMiddlewear =
|
|||
AuthorizationMiddlewear.redirectToRestricted req, res, next
|
||||
else
|
||||
next()
|
||||
|
||||
|
||||
ensureUserCanReadProject: (req, res, next) ->
|
||||
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
|
||||
return next(error) if error?
|
||||
|
@ -32,7 +33,7 @@ module.exports = AuthorizationMiddlewear =
|
|||
else
|
||||
logger.log {user_id, project_id}, "denying user read access to project"
|
||||
AuthorizationMiddlewear.redirectToRestricted req, res, next
|
||||
|
||||
|
||||
ensureUserCanWriteProjectSettings: (req, res, next) ->
|
||||
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
|
||||
return next(error) if error?
|
||||
|
@ -44,7 +45,7 @@ module.exports = AuthorizationMiddlewear =
|
|||
else
|
||||
logger.log {user_id, project_id}, "denying user write access to project settings"
|
||||
AuthorizationMiddlewear.redirectToRestricted req, res, next
|
||||
|
||||
|
||||
ensureUserCanWriteProjectContent: (req, res, next) ->
|
||||
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
|
||||
return next(error) if error?
|
||||
|
@ -56,7 +57,7 @@ module.exports = AuthorizationMiddlewear =
|
|||
else
|
||||
logger.log {user_id, project_id}, "denying user write access to project settings"
|
||||
AuthorizationMiddlewear.redirectToRestricted req, res, next
|
||||
|
||||
|
||||
ensureUserCanAdminProject: (req, res, next) ->
|
||||
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
|
||||
return next(error) if error?
|
||||
|
@ -68,7 +69,7 @@ module.exports = AuthorizationMiddlewear =
|
|||
else
|
||||
logger.log {user_id, project_id}, "denying user admin access to project"
|
||||
AuthorizationMiddlewear.redirectToRestricted req, res, next
|
||||
|
||||
|
||||
ensureUserIsSiteAdmin: (req, res, next) ->
|
||||
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
|
@ -90,22 +91,22 @@ module.exports = AuthorizationMiddlewear =
|
|||
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
|
||||
return callback(error) if error?
|
||||
callback(null, user_id, project_id)
|
||||
|
||||
|
||||
_getUserId: (req, callback = (error, user_id) ->) ->
|
||||
if req.session?.user?._id?
|
||||
user_id = req.session.user._id
|
||||
else
|
||||
user_id = null
|
||||
callback null, user_id
|
||||
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
return callback(null, user_id)
|
||||
|
||||
redirectToRestricted: (req, res, next) ->
|
||||
res.redirect "/restricted"
|
||||
|
||||
res.redirect "/restricted?from=#{encodeURIComponent(req.url)}"
|
||||
|
||||
restricted : (req, res, next)->
|
||||
if req.session.user?
|
||||
if AuthenticationController.isUserLoggedIn(req)
|
||||
res.render 'user/restricted',
|
||||
title:'restricted'
|
||||
else
|
||||
logger.log "user not logged in and trying to access #{req.url}, being redirected to login"
|
||||
res.redirect '/register'
|
||||
|
||||
from = req.query.from
|
||||
logger.log {from: from}, "redirecting to login"
|
||||
redirect_to = "/login"
|
||||
if from?
|
||||
AuthenticationController._setRedirectInSession(req, from)
|
||||
res.redirect redirect_to
|
||||
|
|
|
@ -2,14 +2,15 @@ BetaProgramHandler = require './BetaProgramHandler'
|
|||
UserLocator = require "../User/UserLocator"
|
||||
Settings = require "settings-sharelatex"
|
||||
logger = require 'logger-sharelatex'
|
||||
AuthenticationController = require '../Authentication/AuthenticationController'
|
||||
|
||||
|
||||
module.exports = BetaProgramController =
|
||||
|
||||
optIn: (req, res, next) ->
|
||||
user_id = req?.session?.user?._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "user opting in to beta program"
|
||||
if !user_id
|
||||
if !user_id?
|
||||
return next(new Error("no user id in session"))
|
||||
BetaProgramHandler.optIn user_id, (err) ->
|
||||
if err
|
||||
|
@ -17,9 +18,9 @@ module.exports = BetaProgramController =
|
|||
return res.redirect "/beta/participate"
|
||||
|
||||
optOut: (req, res, next) ->
|
||||
user_id = req?.session?.user?._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "user opting out of beta program"
|
||||
if !user_id
|
||||
if !user_id?
|
||||
return next(new Error("no user id in session"))
|
||||
BetaProgramHandler.optOut user_id, (err) ->
|
||||
if err
|
||||
|
@ -27,7 +28,7 @@ module.exports = BetaProgramController =
|
|||
return res.redirect "/beta/participate"
|
||||
|
||||
optInPage: (req, res, next)->
|
||||
user_id = req.session?.user?._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "showing beta participation page for user"
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
if err
|
||||
|
|
|
@ -28,7 +28,7 @@ module.exports = BlogController =
|
|||
try
|
||||
data = JSON.parse(data)
|
||||
if settings.cdn?.web?.host?
|
||||
data?.content = data?.content?.replace(/src="([^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
|
||||
data?.content = data?.content?.replace(/src="(\/[^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
|
||||
catch err
|
||||
logger.err err:err, data:data, "error parsing data from data"
|
||||
res.render "blog/blog_holder", data
|
||||
|
|
24
services/web/app/coffee/Features/Blog/BlogHandler.coffee
Normal file
24
services/web/app/coffee/Features/Blog/BlogHandler.coffee
Normal file
|
@ -0,0 +1,24 @@
|
|||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
_ = require("underscore")
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = BlogHandler =
|
||||
|
||||
getLatestAnnouncements: (callback)->
|
||||
blogUrl = "#{settings.apis.blog.url}/blog/latestannouncements.json"
|
||||
opts =
|
||||
url:blogUrl
|
||||
json:true
|
||||
timeout:500
|
||||
request.get opts, (err, res, announcements)->
|
||||
if err?
|
||||
return callback err
|
||||
if res.statusCode != 200
|
||||
return callback("blog announcement returned non 200")
|
||||
logger.log announcementsLength: announcements?.length, "announcements returned"
|
||||
announcements = _.map announcements, (announcement)->
|
||||
announcement.url = "/blog#{announcement.url}"
|
||||
announcement.date = new Date(announcement.date)
|
||||
return announcement
|
||||
callback(err, announcements)
|
61
services/web/app/coffee/Features/Chat/ChatApiHandler.coffee
Normal file
61
services/web/app/coffee/Features/Chat/ChatApiHandler.coffee
Normal file
|
@ -0,0 +1,61 @@
|
|||
request = require("request")
|
||||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports = ChatApiHandler =
|
||||
_apiRequest: (opts, callback = (error, data) ->) ->
|
||||
request opts, (error, response, data) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
return callback null, data
|
||||
else
|
||||
error = new Error("chat api returned non-success code: #{response.statusCode}")
|
||||
error.statusCode = response.statusCode
|
||||
logger.error {err: error, opts}, "error sending request to chat api"
|
||||
return callback error
|
||||
|
||||
sendGlobalMessage: (project_id, user_id, content, callback)->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
|
||||
method: "POST"
|
||||
json: {user_id, content}
|
||||
}, callback
|
||||
|
||||
getGlobalMessages: (project_id, limit, before, callback)->
|
||||
qs = {}
|
||||
qs.limit = limit if limit?
|
||||
qs.before = before if before?
|
||||
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
|
||||
method: "GET"
|
||||
qs: qs
|
||||
json: true
|
||||
}, callback
|
||||
|
||||
sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages"
|
||||
method: "POST"
|
||||
json: {user_id, content}
|
||||
}, callback
|
||||
|
||||
getThreads: (project_id, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads"
|
||||
method: "GET"
|
||||
json: true
|
||||
}, callback
|
||||
|
||||
resolveThread: (project_id, thread_id, user_id, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/resolve"
|
||||
method: "POST"
|
||||
json: {user_id}
|
||||
}, callback
|
||||
|
||||
reopenThread: (project_id, thread_id, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen"
|
||||
method: "POST"
|
||||
}, callback
|
|
@ -1,29 +1,34 @@
|
|||
ChatHandler = require("./ChatHandler")
|
||||
ChatApiHandler = require("./ChatApiHandler")
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
logger = require("logger-sharelatex")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserInfoManager = require('../User/UserInfoManager')
|
||||
UserInfoController = require('../User/UserInfoController')
|
||||
CommentsController = require('../Comments/CommentsController')
|
||||
|
||||
module.exports =
|
||||
sendMessage: (req, res, next)->
|
||||
project_id = req.params.project_id
|
||||
content = req.body.content
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if !user_id?
|
||||
err = new Error('no logged-in user')
|
||||
return next(err)
|
||||
ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
|
||||
return next(err) if err?
|
||||
UserInfoManager.getPersonalInfo message.user_id, (err, user) ->
|
||||
return next(err) if err?
|
||||
message.user = UserInfoController.formatPersonalInfo(user)
|
||||
EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)->
|
||||
res.send(204)
|
||||
|
||||
|
||||
sendMessage: (req, res)->
|
||||
project_id = req.params.Project_id
|
||||
user_id = req.session.user._id
|
||||
messageContent = req.body.content
|
||||
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
|
||||
return res.sendStatus(500)
|
||||
EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)->
|
||||
res.send()
|
||||
|
||||
getMessages: (req, res)->
|
||||
project_id = req.params.Project_id
|
||||
getMessages: (req, res, next)->
|
||||
project_id = req.params.project_id
|
||||
query = req.query
|
||||
logger.log project_id:project_id, query:query, "getting messages"
|
||||
ChatHandler.getMessages project_id, query, (err, messages)->
|
||||
if err?
|
||||
logger.err err:err, query:query, "problem getting messages from chat api"
|
||||
return res.sendStatus 500
|
||||
logger.log length:messages?.length, "sending messages to client"
|
||||
res.set 'Content-Type', 'application/json'
|
||||
res.send messages
|
||||
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
|
||||
return next(err) if err?
|
||||
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
|
||||
return next(err) if err?
|
||||
logger.log length: messages?.length, "sending messages to client"
|
||||
res.json messages
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
request = require("request")
|
||||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
|
||||
sendMessage: (project_id, user_id, messageContent, callback)->
|
||||
opts =
|
||||
method:"post"
|
||||
json:
|
||||
content:messageContent
|
||||
user_id:user_id
|
||||
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
|
||||
request opts, (err, response, body)->
|
||||
if err?
|
||||
logger.err err:err, "problem sending new message to chat"
|
||||
callback(err, body)
|
||||
|
||||
|
||||
|
||||
getMessages: (project_id, query, callback)->
|
||||
qs = {}
|
||||
qs.limit = query.limit if query?.limit?
|
||||
qs.before = query.before if query?.before?
|
||||
|
||||
opts =
|
||||
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
|
||||
method:"get"
|
||||
qs: qs
|
||||
|
||||
request opts, (err, response, body)->
|
||||
callback(err, body)
|
|
@ -11,28 +11,7 @@ module.exports = CollaboratorsEmailHandler =
|
|||
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
|
||||
].join("&")
|
||||
|
||||
notifyUserOfProjectShare: (project_id, email, callback)->
|
||||
Project
|
||||
.findOne(_id: project_id )
|
||||
.select("name owner_ref")
|
||||
.populate('owner_ref')
|
||||
.exec (err, project)->
|
||||
emailOptions =
|
||||
to: email
|
||||
replyTo: project.owner_ref.email
|
||||
project:
|
||||
name: project.name
|
||||
url: "#{Settings.siteUrl}/project/#{project._id}?" + [
|
||||
"project_name=#{encodeURIComponent(project.name)}"
|
||||
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
|
||||
"new_email=#{encodeURIComponent(email)}"
|
||||
"r=#{project.owner_ref.referal_id}" # Referal
|
||||
"rs=ci" # referral source = collaborator invite
|
||||
].join("&")
|
||||
owner: project.owner_ref
|
||||
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
|
||||
|
||||
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
|
||||
notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
|
||||
Project
|
||||
.findOne(_id: project_id )
|
||||
.select("name owner_ref")
|
||||
|
@ -45,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
|
|||
name: project.name
|
||||
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
|
||||
owner: project.owner_ref
|
||||
sendingUser_id: sendingUser._id
|
||||
EmailHandler.sendEmail "projectInvite", emailOptions, callback
|
||||
|
|
|
@ -4,10 +4,13 @@ UserGetter = require "../User/UserGetter"
|
|||
CollaboratorsHandler = require('./CollaboratorsHandler')
|
||||
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
|
||||
logger = require('logger-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
AnalyticsManger = require("../Analytics/AnalyticsManager")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
rateLimiter = require("../../infrastructure/RateLimiter")
|
||||
|
||||
module.exports = CollaboratorsInviteController =
|
||||
|
||||
|
@ -20,11 +23,36 @@ module.exports = CollaboratorsInviteController =
|
|||
return next(err)
|
||||
res.json({invites: invites})
|
||||
|
||||
_checkShouldInviteEmail: (sendingUser, email, callback=(err, shouldAllowInvite)->) ->
|
||||
if Settings.restrictInvitesToExistingAccounts == true
|
||||
logger.log {email}, "checking if user exists with this email"
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
|
||||
return callback(err) if err?
|
||||
userExists = user? and user?._id?
|
||||
callback(null, userExists)
|
||||
else
|
||||
UserGetter.getUser sendingUser._id, {features:1, _id:1}, (err, user)->
|
||||
if err?
|
||||
return callback(err)
|
||||
collabLimit = user?.features?.collaborators || 1
|
||||
if collabLimit == -1
|
||||
collabLimit = 20
|
||||
collabLimit = collabLimit * 10
|
||||
opts =
|
||||
endpointName: "invite_to_project"
|
||||
timeInterval: 60 * 30
|
||||
subjectName: sendingUser._id
|
||||
throttle: collabLimit
|
||||
rateLimiter.addCount opts, callback
|
||||
|
||||
inviteToProject: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
email = req.body.email
|
||||
sendingUser = req.session.user
|
||||
sendingUser = AuthenticationController.getSessionUser(req)
|
||||
sendingUserId = sendingUser._id
|
||||
if email == sendingUser.email
|
||||
logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project"
|
||||
return res.json {invite: null, error: 'cannot_invite_self'}
|
||||
logger.log {projectId, email, sendingUserId}, "inviting to project"
|
||||
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
|
||||
return next(error) if error?
|
||||
|
@ -36,13 +64,20 @@ module.exports = CollaboratorsInviteController =
|
|||
if !email? or email == ""
|
||||
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
||||
return res.sendStatus(400)
|
||||
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
|
||||
CollaboratorsInviteController._checkShouldInviteEmail sendingUser, email, (err, shouldAllowInvite)->
|
||||
if err?
|
||||
logger.err {projectId, email, sendingUserId}, "error creating project invite"
|
||||
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
|
||||
return next(err)
|
||||
logger.log {projectId, email, sendingUserId}, "invite created"
|
||||
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
|
||||
return res.json {invite: invite}
|
||||
if !shouldAllowInvite
|
||||
logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
|
||||
return res.json {invite: null, error: 'cannot_invite_non_user'}
|
||||
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
|
||||
if err?
|
||||
logger.err {projectId, email, sendingUserId}, "error creating project invite"
|
||||
return next(err)
|
||||
logger.log {projectId, email, sendingUserId}, "invite created"
|
||||
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
|
||||
return res.json {invite: invite}
|
||||
|
||||
revokeInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
|
@ -58,8 +93,8 @@ module.exports = CollaboratorsInviteController =
|
|||
resendInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
inviteId = req.params.invite_id
|
||||
sendingUser = req.session.user
|
||||
logger.log {projectId, inviteId}, "resending invite"
|
||||
sendingUser = AuthenticationController.getSessionUser(req)
|
||||
CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
|
||||
if err?
|
||||
logger.err {projectId, inviteId}, "error resending invite"
|
||||
|
@ -69,11 +104,11 @@ module.exports = CollaboratorsInviteController =
|
|||
viewInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
token = req.params.token
|
||||
currentUser = req.session.user
|
||||
_renderInvalidPage = () ->
|
||||
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
|
||||
res.render "project/invite/not-valid", {title: "Invalid Invite"}
|
||||
# check if the user is already a member of the project
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error checking if user is member of project"
|
||||
|
@ -111,14 +146,16 @@ module.exports = CollaboratorsInviteController =
|
|||
|
||||
acceptInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
inviteId = req.params.invite_id
|
||||
{token} = req.body
|
||||
currentUser = req.session.user
|
||||
logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
|
||||
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
|
||||
token = req.params.token
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
logger.log {projectId, userId: currentUser._id, token}, "got request to accept invite"
|
||||
CollaboratorsInviteHandler.acceptInvite projectId, token, currentUser, (err) ->
|
||||
if err?
|
||||
logger.err {projectId, inviteId}, "error accepting invite by token"
|
||||
logger.err {projectId, token}, "error accepting invite by token"
|
||||
return next(err)
|
||||
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
|
||||
AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {inviteId:inviteId, projectId:projectId})
|
||||
res.redirect "/project/#{projectId}"
|
||||
AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id})
|
||||
if req.xhr
|
||||
res.sendStatus 204 # Done async via project page notification
|
||||
else
|
||||
res.redirect "/project/#{projectId}"
|
||||
|
|
|
@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
|
|||
|
||||
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
|
||||
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
|
||||
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)->
|
||||
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)->
|
||||
return callback(err) if err?
|
||||
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
|
||||
return callback(err) if err?
|
||||
|
@ -77,10 +77,12 @@ module.exports = CollaboratorsInviteHandler =
|
|||
if err?
|
||||
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
|
||||
return callback(err)
|
||||
# Send email and notification in background
|
||||
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
|
||||
if err?
|
||||
logger.err {projectId, email}, "error sending messages for invite"
|
||||
callback(err, invite)
|
||||
callback(null, invite)
|
||||
|
||||
|
||||
revokeInvite: (projectId, inviteId, callback=(err)->) ->
|
||||
logger.log {projectId, inviteId}, "removing invite"
|
||||
|
@ -102,7 +104,7 @@ module.exports = CollaboratorsInviteHandler =
|
|||
return callback(null)
|
||||
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
|
||||
if err?
|
||||
logger.err {projectid, inviteId}, "error resending invite messages"
|
||||
logger.err {projectId, inviteId}, "error resending invite messages"
|
||||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
|
@ -117,15 +119,15 @@ module.exports = CollaboratorsInviteHandler =
|
|||
return callback(null, null)
|
||||
callback(null, invite)
|
||||
|
||||
acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
|
||||
logger.log {projectId, inviteId, userId: user._id}, "accepting invite"
|
||||
acceptInvite: (projectId, tokenString, user, callback=(err)->) ->
|
||||
logger.log {projectId, userId: user._id, tokenString}, "accepting invite"
|
||||
CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId}, "error finding invite"
|
||||
logger.err {err, projectId, tokenString}, "error finding invite"
|
||||
return callback(err)
|
||||
if !invite
|
||||
err = new Errors.NotFoundError("no matching invite found")
|
||||
logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
|
||||
logger.log {err, projectId, tokenString}, "no matching invite found"
|
||||
return callback(err)
|
||||
inviteId = invite._id
|
||||
CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
|
||||
|
|
|
@ -24,7 +24,13 @@ module.exports =
|
|||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "invite-to-project"
|
||||
params: ["Project_id"]
|
||||
maxRequests: 200
|
||||
maxRequests: 100
|
||||
timeInterval: 60 * 10
|
||||
}),
|
||||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "invite-to-project-ip"
|
||||
ipOnly:true
|
||||
maxRequests: 100
|
||||
timeInterval: 60 * 10
|
||||
}),
|
||||
AuthenticationController.requireLogin(),
|
||||
|
@ -66,7 +72,7 @@ module.exports =
|
|||
)
|
||||
|
||||
webRouter.post(
|
||||
'/project/:Project_id/invite/:invite_id/accept',
|
||||
'/project/:Project_id/invite/token/:token/accept',
|
||||
AuthenticationController.requireLogin(),
|
||||
CollaboratorsInviteController.acceptInvite
|
||||
)
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
ChatApiHandler = require("../Chat/ChatApiHandler")
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
logger = require("logger-sharelatex")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserInfoManager = require('../User/UserInfoManager')
|
||||
UserInfoController = require('../User/UserInfoController')
|
||||
async = require "async"
|
||||
|
||||
module.exports = CommentsController =
|
||||
sendComment: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
content = req.body.content
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if !user_id?
|
||||
err = new Error('no logged-in user')
|
||||
return next(err)
|
||||
logger.log {project_id, thread_id, user_id, content}, "sending comment"
|
||||
ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) ->
|
||||
return next(err) if err?
|
||||
UserInfoManager.getPersonalInfo comment.user_id, (err, user) ->
|
||||
return next(err) if err?
|
||||
comment.user = UserInfoController.formatPersonalInfo(user)
|
||||
EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) ->
|
||||
res.send 204
|
||||
|
||||
getThreads: (req, res, next) ->
|
||||
{project_id} = req.params
|
||||
logger.log {project_id}, "getting comment threads for project"
|
||||
ChatApiHandler.getThreads project_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
CommentsController._injectUserInfoIntoThreads threads, (error, threads) ->
|
||||
return next(err) if err?
|
||||
res.json threads
|
||||
|
||||
resolveThread: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {project_id, thread_id, user_id}, "resolving comment thread"
|
||||
ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) ->
|
||||
return next(err) if err?
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)->
|
||||
res.send 204
|
||||
|
||||
reopenThread: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
logger.log {project_id, thread_id}, "reopening comment thread"
|
||||
ChatApiHandler.reopenThread project_id, thread_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
|
||||
res.send 204
|
||||
|
||||
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
|
||||
userCache = {}
|
||||
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return callback(error) if error?
|
||||
user = UserInfoController.formatPersonalInfo user
|
||||
userCache[user_id] = user
|
||||
callback null, user
|
||||
|
||||
jobs = []
|
||||
for thread_id, thread of threads
|
||||
do (thread) ->
|
||||
if thread.resolved
|
||||
jobs.push (cb) ->
|
||||
getUserDetails thread.resolved_by_user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
thread.resolved_by_user = user
|
||||
cb()
|
||||
for message in thread.messages
|
||||
do (message) ->
|
||||
jobs.push (cb) ->
|
||||
getUserDetails message.user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
message.user = user
|
||||
cb()
|
||||
|
||||
async.series jobs, (error) ->
|
||||
return callback(error) if error?
|
||||
return callback null, threads
|
|
@ -48,6 +48,10 @@ module.exports = ClsiCookieManager =
|
|||
multi.exec (err)->
|
||||
callback(err, serverId)
|
||||
|
||||
clearServerId: (project_id, callback = (err)->)->
|
||||
if !clsiCookiesEnabled
|
||||
return callback()
|
||||
rclient.del buildKey(project_id), callback
|
||||
|
||||
getCookieJar: (project_id, callback = (err, jar)->)->
|
||||
if !clsiCookiesEnabled
|
||||
|
|
|
@ -16,53 +16,53 @@ module.exports = CompileController =
|
|||
res.setTimeout(5 * 60 * 1000)
|
||||
project_id = req.params.Project_id
|
||||
isAutoCompile = !!req.query?.auto_compile
|
||||
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
options = {
|
||||
isAutoCompile: isAutoCompile
|
||||
}
|
||||
if req.body?.rootDoc_id?
|
||||
options.rootDoc_id = req.body.rootDoc_id
|
||||
else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
|
||||
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
|
||||
if req.body?.compiler
|
||||
options.compiler = req.body.compiler
|
||||
if req.body?.draft
|
||||
options.draft = req.body.draft
|
||||
if req.body?.check in ['validate', 'error', 'silent']
|
||||
options.check = req.body.check
|
||||
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
|
||||
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
|
||||
return next(error) if error?
|
||||
options = {
|
||||
isAutoCompile: isAutoCompile
|
||||
res.contentType("application/json")
|
||||
res.status(200).send JSON.stringify {
|
||||
status: status
|
||||
outputFiles: outputFiles
|
||||
compileGroup: limits?.compileGroup
|
||||
clsiServerId:clsiServerId
|
||||
validationProblems:validationProblems
|
||||
}
|
||||
if req.body?.rootDoc_id?
|
||||
options.rootDoc_id = req.body.rootDoc_id
|
||||
else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
|
||||
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
|
||||
if req.body?.compiler
|
||||
options.compiler = req.body.compiler
|
||||
if req.body?.draft
|
||||
options.draft = req.body.draft
|
||||
if req.body?.check in ['validate', 'error', 'silent']
|
||||
options.check = req.body.check
|
||||
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
|
||||
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
|
||||
return next(error) if error?
|
||||
res.contentType("application/json")
|
||||
res.status(200).send JSON.stringify {
|
||||
status: status
|
||||
outputFiles: outputFiles
|
||||
compileGroup: limits?.compileGroup
|
||||
clsiServerId:clsiServerId
|
||||
validationProblems:validationProblems
|
||||
}
|
||||
|
||||
stopCompile: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
|
||||
CompileManager.stopCompile project_id, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
|
||||
CompileManager.stopCompile project_id, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.status(200).send()
|
||||
res.status(200).send()
|
||||
|
||||
_compileAsUser: (req, callback) ->
|
||||
# callback with user_id if per-user, undefined otherwise
|
||||
if not Settings.disablePerUserCompiles
|
||||
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
return callback(null, user_id)
|
||||
else
|
||||
callback() # do a per-project compile, not per-user
|
||||
|
||||
_downloadAsUser: (req, callback) ->
|
||||
# callback with user_id if per-user, undefined otherwise
|
||||
if not Settings.disablePerUserCompiles
|
||||
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
return callback(null, user_id)
|
||||
else
|
||||
callback() # do a per-project compile, not per-user
|
||||
|
||||
|
@ -151,9 +151,9 @@ module.exports = CompileController =
|
|||
{page, h, v} = req.query
|
||||
if not page?.match(/^\d+$/)
|
||||
return next(new Error("invalid page parameter"))
|
||||
if not h?.match(/^\d+\.\d+$/)
|
||||
if not h?.match(/^-?\d+\.\d+$/)
|
||||
return next(new Error("invalid h parameter"))
|
||||
if not v?.match(/^\d+\.\d+$/)
|
||||
if not v?.match(/^-?\d+\.\d+$/)
|
||||
return next(new Error("invalid v parameter"))
|
||||
# whether this request is going to a per-user container
|
||||
CompileController._compileAsUser req, (error, user_id) ->
|
||||
|
|
|
@ -6,33 +6,32 @@ Modules = require "../../infrastructure/Modules"
|
|||
|
||||
module.exports = ContactsController =
|
||||
getContacts: (req, res, next) ->
|
||||
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
|
||||
return next(error) if error?
|
||||
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
|
||||
UserGetter.getUsers contact_ids, {
|
||||
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
|
||||
}, (error, contacts) ->
|
||||
return next(error) if error?
|
||||
UserGetter.getUsers contact_ids, {
|
||||
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
|
||||
}, (error, contacts) ->
|
||||
return next(error) if error?
|
||||
|
||||
# UserGetter.getUsers may not preserve order so put them back in order
|
||||
positions = {}
|
||||
for contact_id, i in contact_ids
|
||||
positions[contact_id] = i
|
||||
contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
|
||||
|
||||
# Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
|
||||
contacts = contacts.filter (c) -> !c.holdingAccount
|
||||
|
||||
contacts = contacts.map(ContactsController._formatContact)
|
||||
|
||||
Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
|
||||
return next(error) if error?
|
||||
contacts = contacts.concat(additional_contacts...)
|
||||
res.send({
|
||||
contacts: contacts
|
||||
})
|
||||
|
||||
# UserGetter.getUsers may not preserve order so put them back in order
|
||||
positions = {}
|
||||
for contact_id, i in contact_ids
|
||||
positions[contact_id] = i
|
||||
contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
|
||||
|
||||
# Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
|
||||
contacts = contacts.filter (c) -> !c.holdingAccount
|
||||
|
||||
contacts = contacts.map(ContactsController._formatContact)
|
||||
|
||||
Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
|
||||
return next(error) if error?
|
||||
contacts = contacts.concat(additional_contacts...)
|
||||
res.send({
|
||||
contacts: contacts
|
||||
})
|
||||
|
||||
_formatContact: (contact) ->
|
||||
return {
|
||||
id: contact._id?.toString()
|
||||
|
|
|
@ -29,8 +29,23 @@ module.exports = DocstoreManager =
|
|||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
|
||||
callback(error)
|
||||
|
||||
getAllRanges: (project_id, callback = (error) ->) ->
|
||||
logger.log { project_id }, "getting all doc ranges for project in docstore api"
|
||||
url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges"
|
||||
request.get {
|
||||
url: url
|
||||
json: true
|
||||
}, (error, res, docs) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null, docs)
|
||||
else
|
||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||
logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore"
|
||||
callback(error)
|
||||
|
||||
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) ->
|
||||
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
|
||||
if typeof(options) == "function"
|
||||
callback = options
|
||||
options = {}
|
||||
|
@ -45,19 +60,21 @@ module.exports = DocstoreManager =
|
|||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
|
||||
callback(null, doc.lines, doc.rev)
|
||||
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
|
||||
else
|
||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
|
||||
callback(error)
|
||||
|
||||
updateDoc: (project_id, doc_id, lines, callback = (error, modified, rev) ->) ->
|
||||
updateDoc: (project_id, doc_id, lines, version, ranges, callback = (error, modified, rev) ->) ->
|
||||
logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api"
|
||||
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
|
||||
request.post {
|
||||
url: url
|
||||
json:
|
||||
lines: lines
|
||||
version: version
|
||||
ranges: ranges
|
||||
}, (error, res, result) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
|
|
|
@ -95,7 +95,7 @@ module.exports = DocumentUpdaterHandler =
|
|||
logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
|
||||
return callback(error)
|
||||
|
||||
getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
|
||||
getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
|
||||
timer = new metrics.Timer("get-document")
|
||||
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
|
||||
logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
|
||||
|
@ -110,7 +110,7 @@ module.exports = DocumentUpdaterHandler =
|
|||
body = JSON.parse(body)
|
||||
catch error
|
||||
return callback(error)
|
||||
callback null, body.lines, body.version, body.ops
|
||||
callback null, body.lines, body.version, body.ranges, body.ops
|
||||
else
|
||||
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
@ -137,15 +137,21 @@ module.exports = DocumentUpdaterHandler =
|
|||
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
||||
getNumberOfDocsInMemory : (callback)->
|
||||
request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)->
|
||||
try
|
||||
body = JSON.parse body
|
||||
catch err
|
||||
logger.err err:err, "error parsing response from doc updater about the total number of docs"
|
||||
callback(err, body?.total)
|
||||
|
||||
|
||||
acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
|
||||
timer = new metrics.Timer("accept-change")
|
||||
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
|
||||
logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
|
||||
request.post url, (error, res, body)->
|
||||
timer.done()
|
||||
if error?
|
||||
logger.error {err:error, project_id, doc_id, change_id}, "error accepting change in doc updater"
|
||||
return callback(error)
|
||||
if res.statusCode >= 200 and res.statusCode < 300
|
||||
logger.log {project_id, doc_id, change_id}, "accepted change in document updater"
|
||||
return callback(null)
|
||||
else
|
||||
logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
||||
PENDINGUPDATESKEY = "PendingUpdates"
|
||||
DOCLINESKEY = "doclines"
|
||||
|
|
|
@ -7,7 +7,7 @@ module.exports =
|
|||
doc_id = req.params.doc_id
|
||||
plain = req?.query?.plain == 'true'
|
||||
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
|
||||
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev) ->
|
||||
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) ->
|
||||
if error?
|
||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||
return next(error)
|
||||
|
@ -18,14 +18,16 @@ module.exports =
|
|||
res.type "json"
|
||||
res.send JSON.stringify {
|
||||
lines: lines
|
||||
version: version
|
||||
ranges: ranges
|
||||
}
|
||||
|
||||
setDocument: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
doc_id = req.params.doc_id
|
||||
lines = req.body.lines
|
||||
{lines, version, ranges} = req.body
|
||||
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
|
||||
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, (error) ->
|
||||
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) ->
|
||||
if error?
|
||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||
return next(error)
|
||||
|
|
|
@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
|||
ProjectDeleter = require("../Project/ProjectDeleter")
|
||||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
EditorRealTimeController = require("./EditorRealTimeController")
|
||||
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
|
||||
async = require('async')
|
||||
LockManager = require("../../infrastructure/LockManager")
|
||||
_ = require('underscore')
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
_ = require("underscore")
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports = _.template """
|
||||
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||
<h3 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: px; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: px; padding: 0; text-align: left; word-wrap: normal;">
|
||||
<%= title %>
|
||||
</h3>
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= greeting %>
|
||||
</p>
|
||||
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= message %>
|
||||
</p>
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<center data-parsed="" style="min-width: 532px; width: 100%;">
|
||||
<table class="button float-center" style="Margin: 0 0 16px 0; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 0 16px 0; padding: 0; text-align: center; vertical-align: top; width: auto;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #a93529; border: 2px solid #a93529; border-collapse: collapse !important; color: #fefefe; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||
<a href="<%= ctaURL %>" style="Margin: 0; border: 0 solid #a93529; border-radius: 3px; color: #fefefe; display: inline-block; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; line-height: 1.3; margin: 0; padding: 8px 16px 8px 16px; text-align: left; text-decoration: none;">
|
||||
<%= ctaText %>
|
||||
</a>
|
||||
</td></tr></table></td></tr></table>
|
||||
</center>
|
||||
<% if (secondaryMessage) { %>
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<p class="avoid-auto-linking" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||
<%= secondaryMessage %>
|
||||
</p>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
|
||||
</tr></tbody></table>
|
||||
<% if (gmailGoToAction) { %>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "EmailMessage",
|
||||
"potentialAction": {
|
||||
"@type": "ViewAction",
|
||||
"target": "<%= gmailGoToAction.target %>",
|
||||
"url": "<%= gmailGoToAction.target %>",
|
||||
"name": "<%= gmailGoToAction.name %>"
|
||||
},
|
||||
"description": "<%= gmailGoToAction.description %>"
|
||||
}
|
||||
</script>
|
||||
<% } %>
|
||||
"""
|
|
@ -1,16 +1,30 @@
|
|||
_ = require('underscore')
|
||||
|
||||
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
||||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||
BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout")
|
||||
|
||||
SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
|
||||
|
||||
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
|
||||
|
||||
templates = {}
|
||||
|
||||
|
||||
templates.registered =
|
||||
subject: _.template "Activate your #{settings.appName} Account"
|
||||
layout: PersonalEmailLayout
|
||||
type: "notification"
|
||||
plainTextTemplate: _.template """
|
||||
Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
|
||||
|
||||
Click here to set your password and log in: <%= setNewPasswordUrl %>
|
||||
|
||||
If you have any questions or problems, please contact #{settings.adminEmail}
|
||||
"""
|
||||
compiledTemplate: _.template """
|
||||
<p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
|
||||
|
||||
|
@ -19,14 +33,28 @@ templates.registered =
|
|||
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
|
||||
"""
|
||||
|
||||
|
||||
templates.canceledSubscription =
|
||||
subject: _.template "ShareLaTeX thoughts"
|
||||
layout: PersonalEmailLayout
|
||||
type:"lifecycle"
|
||||
plainTextTemplate: _.template """
|
||||
Hi <%= first_name %>,
|
||||
|
||||
I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
|
||||
|
||||
https://sharelatex.typeform.com/to/f5lBiZ
|
||||
|
||||
Thank you in advance.
|
||||
|
||||
Henry
|
||||
|
||||
ShareLaTeX Co-founder
|
||||
"""
|
||||
compiledTemplate: _.template '''
|
||||
<p>Hi <%= first_name %>,</p>
|
||||
|
||||
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/F7OzIY">this survey</a>?</p>
|
||||
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/f5lBiZ">this survey</a>?</p>
|
||||
|
||||
<p>Thank you in advance.</p>
|
||||
|
||||
|
@ -36,94 +64,112 @@ ShareLaTeX Co-founder
|
|||
</p>
|
||||
'''
|
||||
|
||||
|
||||
templates.passwordResetRequested =
|
||||
subject: _.template "Password Reset - #{settings.appName}"
|
||||
layout: NotificationEmailLayout
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
compiledTemplate: _.template """
|
||||
<h2>Password Reset</h2>
|
||||
<p>
|
||||
plainTextTemplate: _.template """
|
||||
Password Reset
|
||||
|
||||
We got a request to reset your #{settings.appName} password.
|
||||
<p>
|
||||
<center>
|
||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||
<div style="padding-right:10px;padding-left:10px">
|
||||
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
|
||||
<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
Reset password
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
Click this link to reset your password: <%= setNewPasswordUrl %>
|
||||
|
||||
If you ignore this message, your password won't be changed.
|
||||
<p>
|
||||
|
||||
If you didn't request a password reset, let us know.
|
||||
|
||||
</p>
|
||||
<p>Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
Thank you
|
||||
|
||||
templates.projectSharedWithYou =
|
||||
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
compiledTemplate: _.template """
|
||||
<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
|
||||
<center>
|
||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||
<div style="padding-right:10px;padding-left:10px">
|
||||
<a href="<%= project.url %>" style="text-decoration:none" target="_blank">
|
||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
View Project
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
<p> Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "Password Reset"
|
||||
greeting: "Hi,"
|
||||
message: "We got a request to reset your #{settings.appName} password."
|
||||
secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
|
||||
ctaText: "Reset password"
|
||||
ctaURL: opts.setNewPasswordUrl
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
templates.projectInvite =
|
||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||
layout: NotificationEmailLayout
|
||||
subject: _.template "<%= project.name.slice(0, 40) %> - shared by <%= owner.email %>"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
compiledTemplate: _.template """
|
||||
<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
|
||||
<center>
|
||||
<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
|
||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
View Project
|
||||
</span>
|
||||
</a>
|
||||
</center>
|
||||
<p> Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
plainTextTemplate: _.template """
|
||||
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
|
||||
|
||||
Follow this link to view the project: <%= inviteUrl %>
|
||||
|
||||
Thank you
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "#{ opts.project.name.slice(0, 40) } – shared by #{ opts.owner.email }"
|
||||
greeting: "Hi,"
|
||||
message: "#{ opts.owner.email } wants to share “#{ opts.project.name.slice(0, 40) }” with you."
|
||||
secondaryMessage: null
|
||||
ctaText: "View project"
|
||||
ctaURL: opts.inviteUrl
|
||||
gmailGoToAction:
|
||||
target: opts.inviteUrl
|
||||
name: "View project"
|
||||
description: "Join #{ opts.project.name.slice(0, 40) } at ShareLaTeX"
|
||||
})
|
||||
|
||||
templates.completeJoinGroupAccount =
|
||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||
layout: NotificationEmailLayout
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
compiledTemplate: _.template """
|
||||
<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
|
||||
<center>
|
||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||
<div style="padding-right:10px;padding-left:10px">
|
||||
<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank">
|
||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
Verify now
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
<p> Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
plainTextTemplate: _.template """
|
||||
Hi, please verify your email to join the <%= group_name %> and get your free premium account
|
||||
|
||||
Click this link to verify now: <%= completeJoinUrl %>
|
||||
|
||||
Thank You
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "Verify Email to join #{ opts.group_name } group"
|
||||
greeting: "Hi,"
|
||||
message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
|
||||
secondaryMessage: null
|
||||
ctaText: "Verify now"
|
||||
ctaURL: opts.completeJoinUrl
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
templates.testEmail =
|
||||
subject: _.template "A Test Email from ShareLaTeX"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Hi,
|
||||
|
||||
This is a test email sent from ShareLaTeX.
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "A Test Email from ShareLaTeX"
|
||||
greeting: "Hi,"
|
||||
message: "This is a test email sent from ShareLaTeX"
|
||||
secondaryMessage: null
|
||||
ctaText: "Open ShareLaTeX"
|
||||
ctaURL: "/"
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
module.exports =
|
||||
templates: templates
|
||||
|
@ -137,5 +183,6 @@ module.exports =
|
|||
return {
|
||||
subject : template.subject(opts)
|
||||
html: template.layout(opts)
|
||||
text: template?.plainTextTemplate?(opts)
|
||||
type:template.type
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ EmailBuilder = require "./EmailBuilder"
|
|||
EmailSender = require "./EmailSender"
|
||||
|
||||
if !settings.email?
|
||||
settings.email =
|
||||
settings.email =
|
||||
lifecycleEnabled:false
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ module.exports =
|
|||
if email.type == "lifecycle" and !settings.email.lifecycle
|
||||
return callback()
|
||||
opts.html = email.html
|
||||
opts.text = email.text
|
||||
opts.subject = email.subject
|
||||
EmailSender.sendEmail opts, (err)->
|
||||
callback(err)
|
||||
callback(err)
|
||||
|
|
|
@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
|
|||
nodemailer = require("nodemailer")
|
||||
sesTransport = require('nodemailer-ses-transport')
|
||||
sgTransport = require('nodemailer-sendgrid-transport')
|
||||
|
||||
rateLimiter = require('../../infrastructure/RateLimiter')
|
||||
_ = require("underscore")
|
||||
|
||||
if Settings.email? and Settings.email.fromAddress?
|
||||
|
@ -25,7 +25,7 @@ else if Settings?.email?.parameters?.sendgridApiKey?
|
|||
logger.log "using sendgrid for email"
|
||||
nm_client = nodemailer.createTransport(sgTransport({auth:{api_key:Settings?.email?.parameters?.sendgridApiKey}}))
|
||||
else if Settings?.email?.parameters?
|
||||
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth")
|
||||
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth", "ignoreTLS")
|
||||
|
||||
|
||||
logger.log "using smtp for email"
|
||||
|
@ -39,21 +39,39 @@ if nm_client?
|
|||
else
|
||||
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
|
||||
|
||||
checkCanSendEmail = (options, callback)->
|
||||
if !options.sendingUser_id? #email not sent from user, not rate limited
|
||||
return callback(null, true)
|
||||
opts =
|
||||
endpointName: "send_email"
|
||||
timeInterval: 60 * 60 * 3
|
||||
subjectName: options.sendingUser_id
|
||||
throttle: 100
|
||||
rateLimiter.addCount opts, callback
|
||||
|
||||
module.exports =
|
||||
sendEmail : (options, callback = (error) ->)->
|
||||
logger.log receiver:options.to, subject:options.subject, "sending email"
|
||||
metrics.inc "email"
|
||||
options =
|
||||
to: options.to
|
||||
from: defaultFromAddress
|
||||
subject: options.subject
|
||||
html: options.html
|
||||
replyTo: options.replyTo || Settings.email.replyToAddress
|
||||
socketTimeout: 30 * 1000
|
||||
client.sendMail options, (err, res)->
|
||||
checkCanSendEmail options, (err, canContinue)->
|
||||
if err?
|
||||
logger.err err:err, "error sending message"
|
||||
else
|
||||
logger.log "Message sent to #{options.to}"
|
||||
callback(err)
|
||||
return callback(err)
|
||||
if !canContinue
|
||||
logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
|
||||
return callback("rate limit hit sending email")
|
||||
metrics.inc "email"
|
||||
options =
|
||||
to: options.to
|
||||
from: defaultFromAddress
|
||||
subject: options.subject
|
||||
html: options.html
|
||||
text: options.text
|
||||
replyTo: options.replyTo || Settings.email.replyToAddress
|
||||
socketTimeout: 30 * 1000
|
||||
if Settings.email.textEncoding?
|
||||
opts.textEncoding = textEncoding
|
||||
client.sendMail options, (err, res)->
|
||||
if err?
|
||||
logger.err err:err, "error sending message"
|
||||
else
|
||||
logger.log "Message sent to #{options.to}"
|
||||
callback(err)
|
||||
|
|
|
@ -0,0 +1,380 @@
|
|||
_ = require("underscore")
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports = _.template """
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="Margin: 0; background: #f6f6f6 !important; margin: 0; min-height: 100%; padding: 0;">
|
||||
<head>
|
||||
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Project invite</title>
|
||||
<style>.avoid-auto-linking a,
|
||||
.avoid-auto-linking a[href] {
|
||||
color: #a93529 !important;
|
||||
text-decoration: none !important;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
-webkit-hyphens: none;
|
||||
hyphens: none; }
|
||||
.avoid-auto-linking a:visited,
|
||||
.avoid-auto-linking a[href]:visited {
|
||||
color: #a93529; }
|
||||
.avoid-auto-linking a:hover,
|
||||
.avoid-auto-linking a[href]:hover {
|
||||
color: #80281f; }
|
||||
.avoid-auto-linking a:active,
|
||||
.avoid-auto-linking a[href]:active {
|
||||
color: #80281f; }
|
||||
@media only screen {
|
||||
html {
|
||||
min-height: 100%;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 596px) {
|
||||
.small-float-center {
|
||||
margin: 0 auto !important;
|
||||
float: none !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.small-text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.small-text-left {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.small-text-right {
|
||||
text-align: right !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 596px) {
|
||||
.hide-for-large {
|
||||
display: block !important;
|
||||
width: auto !important;
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
font-size: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 596px) {
|
||||
table.body table.container .hide-for-large,
|
||||
table.body table.container .row.hide-for-large {
|
||||
display: table !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 596px) {
|
||||
table.body table.container .callout-inner.hide-for-large {
|
||||
display: table-cell !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 596px) {
|
||||
table.body table.container .show-for-large {
|
||||
display: none !important;
|
||||
width: 0;
|
||||
mso-hide: all;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 596px) {
|
||||
table.body img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table.body center {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
table.body .container {
|
||||
width: 95% !important;
|
||||
}
|
||||
|
||||
table.body .columns,
|
||||
table.body .column {
|
||||
height: auto !important;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
|
||||
table.body .columns .column,
|
||||
table.body .columns .columns,
|
||||
table.body .column .column,
|
||||
table.body .column .columns {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
table.body .collapse .columns,
|
||||
table.body .collapse .column {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
td.small-1,
|
||||
th.small-1 {
|
||||
display: inline-block !important;
|
||||
width: 8.33333% !important;
|
||||
}
|
||||
|
||||
td.small-2,
|
||||
th.small-2 {
|
||||
display: inline-block !important;
|
||||
width: 16.66667% !important;
|
||||
}
|
||||
|
||||
td.small-3,
|
||||
th.small-3 {
|
||||
display: inline-block !important;
|
||||
width: 25% !important;
|
||||
}
|
||||
|
||||
td.small-4,
|
||||
th.small-4 {
|
||||
display: inline-block !important;
|
||||
width: 33.33333% !important;
|
||||
}
|
||||
|
||||
td.small-5,
|
||||
th.small-5 {
|
||||
display: inline-block !important;
|
||||
width: 41.66667% !important;
|
||||
}
|
||||
|
||||
td.small-6,
|
||||
th.small-6 {
|
||||
display: inline-block !important;
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
td.small-7,
|
||||
th.small-7 {
|
||||
display: inline-block !important;
|
||||
width: 58.33333% !important;
|
||||
}
|
||||
|
||||
td.small-8,
|
||||
th.small-8 {
|
||||
display: inline-block !important;
|
||||
width: 66.66667% !important;
|
||||
}
|
||||
|
||||
td.small-9,
|
||||
th.small-9 {
|
||||
display: inline-block !important;
|
||||
width: 75% !important;
|
||||
}
|
||||
|
||||
td.small-10,
|
||||
th.small-10 {
|
||||
display: inline-block !important;
|
||||
width: 83.33333% !important;
|
||||
}
|
||||
|
||||
td.small-11,
|
||||
th.small-11 {
|
||||
display: inline-block !important;
|
||||
width: 91.66667% !important;
|
||||
}
|
||||
|
||||
td.small-12,
|
||||
th.small-12 {
|
||||
display: inline-block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.columns td.small-12,
|
||||
.column td.small-12,
|
||||
.columns th.small-12,
|
||||
.column th.small-12 {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-1,
|
||||
table.body th.small-offset-1 {
|
||||
margin-left: 8.33333% !important;
|
||||
Margin-left: 8.33333% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-2,
|
||||
table.body th.small-offset-2 {
|
||||
margin-left: 16.66667% !important;
|
||||
Margin-left: 16.66667% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-3,
|
||||
table.body th.small-offset-3 {
|
||||
margin-left: 25% !important;
|
||||
Margin-left: 25% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-4,
|
||||
table.body th.small-offset-4 {
|
||||
margin-left: 33.33333% !important;
|
||||
Margin-left: 33.33333% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-5,
|
||||
table.body th.small-offset-5 {
|
||||
margin-left: 41.66667% !important;
|
||||
Margin-left: 41.66667% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-6,
|
||||
table.body th.small-offset-6 {
|
||||
margin-left: 50% !important;
|
||||
Margin-left: 50% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-7,
|
||||
table.body th.small-offset-7 {
|
||||
margin-left: 58.33333% !important;
|
||||
Margin-left: 58.33333% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-8,
|
||||
table.body th.small-offset-8 {
|
||||
margin-left: 66.66667% !important;
|
||||
Margin-left: 66.66667% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-9,
|
||||
table.body th.small-offset-9 {
|
||||
margin-left: 75% !important;
|
||||
Margin-left: 75% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-10,
|
||||
table.body th.small-offset-10 {
|
||||
margin-left: 83.33333% !important;
|
||||
Margin-left: 83.33333% !important;
|
||||
}
|
||||
|
||||
table.body td.small-offset-11,
|
||||
table.body th.small-offset-11 {
|
||||
margin-left: 91.66667% !important;
|
||||
Margin-left: 91.66667% !important;
|
||||
}
|
||||
|
||||
table.body table.columns td.expander,
|
||||
table.body table.columns th.expander {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
table.body .right-text-pad,
|
||||
table.body .text-pad-right {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
table.body .left-text-pad,
|
||||
table.body .text-pad-left {
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
table.menu {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.menu td,
|
||||
table.menu th {
|
||||
width: auto !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
table.menu.vertical td,
|
||||
table.menu.vertical th,
|
||||
table.menu.small-vertical td,
|
||||
table.menu.small-vertical th {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
table.menu[align="center"] {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
table.button.small-expand,
|
||||
table.button.small-expanded {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.button.small-expand table,
|
||||
table.button.small-expanded table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.button.small-expand table a,
|
||||
table.button.small-expanded table a {
|
||||
text-align: center !important;
|
||||
width: 100% !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
table.button.small-expand center,
|
||||
table.button.small-expanded center {
|
||||
min-width: 0;
|
||||
}
|
||||
}</style>
|
||||
</head>
|
||||
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#F6F6F6" style="-moz-box-sizing: border-box; -ms-text-size-adjust: 100%; -webkit-box-sizing: border-box; -webkit-text-size-adjust: 100%; Margin: 0; background: #f6f6f6 !important; box-sizing: border-box; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; min-height: 100%; min-width: 100%; padding: 0; text-align: left; width: 100% !important;">
|
||||
<!-- <span class="preheader"></span> -->
|
||||
<table class="body" border="0" cellspacing="0" cellpadding="0" width="100%" height="100%" style="Margin: 0; background: #f6f6f6 !important; border-collapse: collapse; border-spacing: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; height: 100%; line-height: 1.3; margin: 0; min-height: 100%; padding: 0; text-align: left; vertical-align: top; width: 100%;">
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||
<td class="body-cell" align="center" valign="top" bgcolor="#F6F6F6" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #f6f6f6 !important; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; padding-bottom: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||
<center data-parsed="" style="min-width: 580px; width: 100%;">
|
||||
|
||||
<table align="center" class="wrapper header float-center" style="Margin: 0 auto; background: #fefefe; border-bottom: solid 1px #cfcfcf; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||
<table align="center" class="container" style="Margin: 0 auto; background: transparent; border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||
<table class="row collapse" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 0; padding-left: 0; padding-right: 0; text-align: left; width: 588px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||
<h1 class="sl-logotype" style="Margin: 0; Margin-bottom: 0; color: #333333; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 26px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 0; padding: 0; text-align: left; word-wrap: normal;">
|
||||
<span>S</span><span class="sl-logotype-small" style="font-size: 80%;">HARE</span><span>L</span><span class="sl-logotype-small" style="font-size: 80%;">A</span><span>T</span><span class="sl-logotype-small" style="font-size: 80%;">E</span><span>X</span>
|
||||
</h1>
|
||||
</th>
|
||||
<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
|
||||
</tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></table>
|
||||
<table class="spacer float-center" style="Margin: 0 auto; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<table align="center" class="container main float-center" style="Margin: 0 auto; Margin-top: 10px; background: #fefefe; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; margin-top: 10px; padding: 0; text-align: center; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
|
||||
<%= body %>
|
||||
|
||||
<table class="wrapper secondary" align="center" style="background: #f6f6f6; border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="10px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 10px; font-weight: normal; hyphens: auto; line-height: 10px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;"><small style="color: #7a7a7a; font-size: 80%;">
|
||||
#{ settings.appName} • <a href="#{ settings.siteUrl }" style="Margin: 0; color: #a93529; font-family: Helvetica, Arial, sans-serif; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; text-decoration: none;">#{ settings.siteUrl }</a>
|
||||
</small></p>
|
||||
</td></tr></table>
|
||||
</td></tr></tbody></table>
|
||||
|
||||
</center>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- prevent Gmail on iOS font size manipulation -->
|
||||
<div style="display:none; white-space:nowrap; font:15px courier; line-height:0;"> </div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
"""
|
|
@ -1,5 +1,6 @@
|
|||
Errors = require "./Errors"
|
||||
logger = require "logger-sharelatex"
|
||||
AuthenticationController = require '../Authentication/AuthenticationController'
|
||||
|
||||
module.exports = ErrorController =
|
||||
notFound: (req, res)->
|
||||
|
@ -11,15 +12,16 @@ module.exports = ErrorController =
|
|||
res.status(500)
|
||||
res.render 'general/500',
|
||||
title: "Server Error"
|
||||
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
if error?.code is 'EBADCSRFTOKEN'
|
||||
logger.warn err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf"
|
||||
logger.warn err: error,url:req.url, method:req.method, user:user, "invalid csrf"
|
||||
res.sendStatus(403)
|
||||
return
|
||||
if error instanceof Errors.NotFoundError
|
||||
logger.warn {err: error, url: req.url}, "not found error"
|
||||
ErrorController.notFound req, res
|
||||
else
|
||||
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
|
||||
ErrorController.serverError req, res
|
||||
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
|
||||
ErrorController.serverError req, res
|
||||
|
|
|
@ -63,7 +63,8 @@ Reporter = (res) ->
|
|||
|
||||
res.contentType("application/json")
|
||||
if failures.length > 0
|
||||
res.send 500, JSON.stringify(results, null, 2)
|
||||
logger.err failures:failures, "health check failed"
|
||||
res.status(500).send(JSON.stringify(results, null, 2))
|
||||
else
|
||||
res.send 200, JSON.stringify(results, null, 2)
|
||||
res.status(200).send(JSON.stringify(results, null, 2))
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
logger = require "logger-sharelatex"
|
||||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
|
||||
module.exports = HistoryController =
|
||||
proxyToHistoryApi: (req, res, next = (error) ->) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
url = settings.apis.trackchanges.url + req.url
|
||||
logger.log url: url, "proxying to track-changes api"
|
||||
getReq = request(
|
||||
url: url
|
||||
method: req.method
|
||||
headers:
|
||||
"X-User-Id": user_id
|
||||
)
|
||||
getReq.pipe(res)
|
||||
getReq.on "error", (error) ->
|
||||
logger.error err: error, "track-changes API error"
|
||||
next(error)
|
|
@ -0,0 +1,28 @@
|
|||
settings = require "settings-sharelatex"
|
||||
request = require "request"
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = HistoryManager =
|
||||
flushProject: (project_id, callback = (error) ->) ->
|
||||
logger.log project_id: project_id, "flushing project in track-changes api"
|
||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
|
||||
request.post url, (error, res, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null)
|
||||
else
|
||||
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
||||
logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
|
||||
callback(error)
|
||||
|
||||
archiveProject: (project_id, callback = ()->)->
|
||||
logger.log project_id: project_id, "archving project in track-changes api"
|
||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
|
||||
request.post url, (error, res, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null)
|
||||
else
|
||||
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
||||
logger.error err: error, project_id: project_id, "error archving project in track-changes api"
|
||||
callback(error)
|
|
@ -5,8 +5,6 @@ DocstoreManager = require("../Docstore/DocstoreManager")
|
|||
ProjectGetter = require("../Project/ProjectGetter")
|
||||
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
|
||||
Project = require("../../models/Project").Project
|
||||
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
|
||||
|
||||
|
||||
MILISECONDS_IN_DAY = 86400000
|
||||
module.exports = InactiveProjectManager =
|
||||
|
@ -52,7 +50,6 @@ module.exports = InactiveProjectManager =
|
|||
logger.log project_id:project_id, "deactivating inactive project"
|
||||
jobs = [
|
||||
(cb)-> DocstoreManager.archiveProject project_id, cb
|
||||
# (cb)-> TrackChangesManager.archiveProject project_id, cb
|
||||
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
|
||||
]
|
||||
async.series jobs, (err)->
|
||||
|
|
|
@ -11,8 +11,8 @@ module.exports =
|
|||
messageOpts =
|
||||
groupName: licence.name
|
||||
subscription_id: licence.subscription_id
|
||||
logger.log user_id:user._id, key:key, "creating notification key for user"
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback
|
||||
logger.log user_id:user._id, key:@key, "creating notification key for user"
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
|
||||
|
||||
read: (callback = ->)->
|
||||
NotificationsHandler.markAsReadWithKey user._id, @key, callback
|
||||
|
@ -26,6 +26,6 @@ module.exports =
|
|||
projectId: project._id.toString()
|
||||
token: invite.token
|
||||
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, true, callback
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
|
||||
read: (callback=()->) ->
|
||||
NotificationsHandler.markAsReadByKeyOnly @key, callback
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
NotificationsHandler = require("./NotificationsHandler")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
||||
module.exports =
|
||||
|
||||
getAllUnreadNotifications: (req, res)->
|
||||
NotificationsHandler.getUserNotifications req.session.user._id, (err, unreadNotifications)->
|
||||
unreadNotifications = _.map unreadNotifications, (notification)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)->
|
||||
unreadNotifications = _.map unreadNotifications, (notification)->
|
||||
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
|
||||
return notification
|
||||
res.send(unreadNotifications)
|
||||
|
||||
markNotificationAsRead: (req, res)->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
notification_id = req.params.notification_id
|
||||
NotificationsHandler.markAsRead user_id, notification_id, ->
|
||||
res.send()
|
||||
|
|
|
@ -29,16 +29,15 @@ module.exports =
|
|||
unreadNotifications = []
|
||||
callback(null, unreadNotifications)
|
||||
|
||||
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
|
||||
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
|
||||
payload = {
|
||||
key:key
|
||||
messageOpts:messageOpts
|
||||
templateKey:templateKey
|
||||
forceCreate: true
|
||||
}
|
||||
if expiryDateTime?
|
||||
payload.expires = expiryDateTime
|
||||
if forceCreate
|
||||
payload.forceCreate = true
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
timeout: oneSecond
|
||||
|
|
|
@ -18,6 +18,8 @@ InactiveProjectManager = require("../InactiveData/InactiveProjectManager")
|
|||
ProjectUpdateHandler = require("./ProjectUpdateHandler")
|
||||
ProjectGetter = require("./ProjectGetter")
|
||||
PrivilegeLevels = require("../Authorization/PrivilegeLevels")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
PackageVersions = require("../../infrastructure/PackageVersions")
|
||||
|
||||
module.exports = ProjectController =
|
||||
|
||||
|
@ -45,10 +47,10 @@ module.exports = ProjectController =
|
|||
async.series jobs, (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus(204)
|
||||
|
||||
|
||||
updateProjectAdminSettings: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
|
||||
|
||||
jobs = []
|
||||
if req.body.publicAccessLevel?
|
||||
jobs.push (callback) ->
|
||||
|
@ -88,32 +90,33 @@ module.exports = ProjectController =
|
|||
project_id = req.params.Project_id
|
||||
projectName = req.body.projectName
|
||||
logger.log project_id:project_id, projectName:projectName, "cloning project"
|
||||
if !req.session.user?
|
||||
if !AuthenticationController.isUserLoggedIn(req)
|
||||
return res.send redir:"/register"
|
||||
projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)->
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
|
||||
if err?
|
||||
logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project"
|
||||
logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project"
|
||||
return next(err)
|
||||
res.send(project_id:project._id)
|
||||
|
||||
|
||||
newProject: (req, res)->
|
||||
user = req.session.user
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
projectName = req.body.projectName?.trim()
|
||||
template = req.body.template
|
||||
logger.log user: user, projectType: template, name: projectName, "creating project"
|
||||
logger.log user: user_id, projectType: template, name: projectName, "creating project"
|
||||
async.waterfall [
|
||||
(cb)->
|
||||
if template == 'example'
|
||||
projectCreationHandler.createExampleProject user._id, projectName, cb
|
||||
projectCreationHandler.createExampleProject user_id, projectName, cb
|
||||
else
|
||||
projectCreationHandler.createBasicProject user._id, projectName, cb
|
||||
projectCreationHandler.createBasicProject user_id, projectName, cb
|
||||
], (err, project)->
|
||||
if err?
|
||||
logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project"
|
||||
logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project"
|
||||
res.sendStatus 500
|
||||
else
|
||||
logger.log project: project, user: user, name: projectName, templateType: template, "created project"
|
||||
logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
|
||||
res.send {project_id:project._id}
|
||||
|
||||
|
||||
|
@ -131,7 +134,8 @@ module.exports = ProjectController =
|
|||
|
||||
projectListPage: (req, res, next)->
|
||||
timer = new metrics.Timer("project-list")
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
async.parallel {
|
||||
tags: (cb)->
|
||||
TagsHandler.getAllTags user_id, cb
|
||||
|
@ -140,7 +144,7 @@ module.exports = ProjectController =
|
|||
projects: (cb)->
|
||||
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
|
||||
hasSubscription: (cb)->
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
|
||||
user: (cb) ->
|
||||
User.findById user_id, "featureSwitches", cb
|
||||
}, (err, results)->
|
||||
|
@ -149,7 +153,9 @@ module.exports = ProjectController =
|
|||
return next(err)
|
||||
logger.log results:results, user_id:user_id, "rendering project list"
|
||||
tags = results.tags[0]
|
||||
notifications = require("underscore").map results.notifications, (notification)->
|
||||
|
||||
|
||||
notifications = require("underscore").map results.notifications, (notification)->
|
||||
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
|
||||
return notification
|
||||
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
|
||||
|
@ -183,19 +189,19 @@ module.exports = ProjectController =
|
|||
if !Settings.editorIsOpen
|
||||
return res.render("general/closed", {title:"updating_site"})
|
||||
|
||||
if req.session.user?
|
||||
user_id = req.session.user._id
|
||||
if AuthenticationController.isUserLoggedIn(req)
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
anonymous = false
|
||||
else
|
||||
anonymous = true
|
||||
user_id = null
|
||||
|
||||
project_id = req.params.Project_id
|
||||
logger.log project_id:project_id, "loading editor"
|
||||
logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor"
|
||||
|
||||
async.parallel {
|
||||
project: (cb)->
|
||||
ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb
|
||||
ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1, track_changes: 1 }, cb
|
||||
user: (cb)->
|
||||
if !user_id?
|
||||
cb null, defaultSettingsForAnonymousUser(user_id)
|
||||
|
@ -259,7 +265,9 @@ module.exports = ProjectController =
|
|||
fontSize : user.ace.fontSize
|
||||
autoComplete: user.ace.autoComplete
|
||||
pdfViewer : user.ace.pdfViewer
|
||||
syntaxValidation: user.ace.syntaxValidation
|
||||
}
|
||||
trackChangesEnabled: !!project.track_changes
|
||||
privilegeLevel: privilegeLevel
|
||||
chatUrl: Settings.apis.chat.url
|
||||
anonymous: anonymous
|
||||
|
@ -320,6 +328,7 @@ defaultSettingsForAnonymousUser = (user_id)->
|
|||
autoComplete: true
|
||||
spellCheckLanguage: ""
|
||||
pdfViewer: ""
|
||||
syntaxValidation: true
|
||||
subscription:
|
||||
freeTrial:
|
||||
allowed: true
|
||||
|
@ -328,8 +337,8 @@ defaultSettingsForAnonymousUser = (user_id)->
|
|||
|
||||
THEME_LIST = []
|
||||
do generateThemeList = () ->
|
||||
files = fs.readdirSync __dirname + '/../../../../public/js/ace'
|
||||
files = fs.readdirSync __dirname + '/../../../../public/js/' + PackageVersions.lib('ace')
|
||||
for file in files
|
||||
if file.slice(-2) == "js" and file.match(/^theme-/)
|
||||
cleanName = file.slice(0,-3).slice(6)
|
||||
THEME_LIST.push cleanName
|
||||
THEME_LIST.push cleanName
|
||||
|
|
|
@ -19,6 +19,11 @@ module.exports = ProjectEditorHandler =
|
|||
|
||||
if !result.invites?
|
||||
result.invites = []
|
||||
|
||||
hasTrackChanges = false
|
||||
for member in members
|
||||
if member.privilegeLevel == "owner" and member.user?.featureSwitches?.track_changes
|
||||
hasTrackChanges = true
|
||||
|
||||
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
|
||||
result.owner = owner
|
||||
|
@ -32,6 +37,7 @@ module.exports = ProjectEditorHandler =
|
|||
compileGroup:"standard"
|
||||
templates: false
|
||||
references: false
|
||||
trackChanges: hasTrackChanges
|
||||
})
|
||||
|
||||
return result
|
||||
|
|
|
@ -126,7 +126,7 @@ module.exports = ProjectEntityHandler =
|
|||
doc = new Doc name: docName
|
||||
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
|
||||
# which hasn't been created in docstore.
|
||||
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
|
||||
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) ->
|
||||
return callback(err) if err?
|
||||
|
||||
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
|
||||
|
@ -292,7 +292,7 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(err)
|
||||
callback(err, folder, parentFolder_id)
|
||||
|
||||
updateDocLines : (project_id, doc_id, lines, callback = (error) ->)->
|
||||
updateDocLines : (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
||||
return callback(err) if err?
|
||||
return callback(new Errors.NotFoundError("project not found")) if !project?
|
||||
|
@ -307,7 +307,7 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(error)
|
||||
|
||||
logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc"
|
||||
DocstoreManager.updateDoc project_id, doc_id, lines, (err, modified, rev) ->
|
||||
DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) ->
|
||||
if err?
|
||||
logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore"
|
||||
return callback(err)
|
||||
|
|
|
@ -81,7 +81,7 @@ module.exports = ProjectLocator =
|
|||
else
|
||||
getRootDoc project
|
||||
|
||||
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
|
||||
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity, type)->)->
|
||||
|
||||
getParentFolder = (haystackFolder, foldersList, level, cb)->
|
||||
if foldersList.length == 0
|
||||
|
@ -100,12 +100,22 @@ module.exports = ProjectLocator =
|
|||
|
||||
getEntity = (folder, entityName, cb)->
|
||||
if !entityName?
|
||||
return cb null, folder
|
||||
enteties = _.union folder.fileRefs, folder.docs, folder.folders
|
||||
result = _.find enteties, (entity)->
|
||||
entity?.name.toLowerCase() == entityName.toLowerCase()
|
||||
return cb null, folder, "folder"
|
||||
for file in folder.fileRefs or []
|
||||
if file?.name.toLowerCase() == entityName.toLowerCase()
|
||||
result = file
|
||||
type = "file"
|
||||
for doc in folder.docs or []
|
||||
if doc?.name.toLowerCase() == entityName.toLowerCase()
|
||||
result = doc
|
||||
type = "doc"
|
||||
for childFolder in folder.folders or []
|
||||
if childFolder?.name.toLowerCase() == entityName.toLowerCase()
|
||||
result = childFolder
|
||||
type = "folder"
|
||||
|
||||
if result?
|
||||
cb null, result
|
||||
cb null, result, type
|
||||
else
|
||||
cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found")
|
||||
|
||||
|
@ -117,7 +127,7 @@ module.exports = ProjectLocator =
|
|||
if !project?
|
||||
return callback("project could not be found for finding a element #{project_or_id}")
|
||||
if needlePath == '' || needlePath == '/'
|
||||
return callback(null, project.rootFolder[0])
|
||||
return callback(null, project.rootFolder[0], "folder")
|
||||
|
||||
if needlePath.indexOf('/') == 0
|
||||
needlePath = needlePath.substring(1)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
logger = require('logger-sharelatex')
|
||||
ReferalHandler = require('./ReferalHandler')
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports =
|
||||
module.exports =
|
||||
bonus: (req, res)->
|
||||
ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
ReferalHandler.getReferedUserIds user_id, (err, refered_users)->
|
||||
res.render "referal/bonus",
|
||||
title: "bonus_please_recommend_us"
|
||||
refered_users: refered_users
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
User = require("../../models/User").User
|
||||
|
||||
module.exports = RefererMiddleware =
|
||||
getUserReferalId: (req, res, next) ->
|
||||
if req.session? and req.session.user?
|
||||
User.findById req.session.user._id, (error, user) ->
|
||||
return next(error) if error?
|
||||
req.session.user.referal_id = user.referal_id
|
||||
next()
|
||||
else
|
||||
next()
|
|
@ -1,32 +1,33 @@
|
|||
RateLimiter = require "../../infrastructure/RateLimiter"
|
||||
logger = require "logger-sharelatex"
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports = RateLimiterMiddlewear =
|
||||
###
|
||||
Do not allow more than opts.maxRequests from a single client in
|
||||
opts.timeInterval. Pass an array of opts.params to segment this based on
|
||||
parameters in the request URL, e.g.:
|
||||
|
||||
|
||||
app.get "/project/:project_id", RateLimiterMiddlewear.rateLimit(endpointName: "open-editor", params: ["project_id"])
|
||||
|
||||
|
||||
will rate limit each project_id separately.
|
||||
|
||||
|
||||
Unique clients are identified by user_id if logged in, and IP address if not.
|
||||
###
|
||||
rateLimit: (opts) ->
|
||||
return (req, res, next) ->
|
||||
if req.session?.user?
|
||||
user_id = req.session.user._id
|
||||
else
|
||||
user_id = req.ip
|
||||
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
|
||||
params = (opts.params or []).map (p) -> req.params[p]
|
||||
params.push user_id
|
||||
subjectName = params.join(":")
|
||||
if opts.ipOnly
|
||||
subjectName = req.ip
|
||||
if !opts.endpointName?
|
||||
throw new Error("no endpointName provided")
|
||||
options = {
|
||||
endpointName: opts.endpointName
|
||||
timeInterval: opts.timeInterval or 60
|
||||
subjectName: params.join(":")
|
||||
subjectName: subjectName
|
||||
throttle: opts.maxRequests or 6
|
||||
}
|
||||
RateLimiter.addCount options, (error, canContinue)->
|
||||
|
@ -37,4 +38,4 @@ module.exports = RateLimiterMiddlewear =
|
|||
logger.warn options, "rate limit exceeded"
|
||||
res.status(429) # Too many requests
|
||||
res.write("Rate limit reached, please try again later")
|
||||
res.end()
|
||||
res.end()
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
Settings = require("settings-sharelatex")
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
crypto = require("crypto")
|
||||
async = require("async")
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
_getEmailKey : (email)->
|
||||
hash = crypto.createHash("md5").update(email).digest("hex")
|
||||
return "e_sess:#{hash}"
|
||||
|
||||
tracksession:(sessionId, email, callback = ->)->
|
||||
session_lookup_key = @_getEmailKey(email)
|
||||
rclient.set session_lookup_key, sessionId, callback
|
||||
|
||||
invalidateSession:(email, callback = ->)->
|
||||
session_lookup_key = @_getEmailKey(email)
|
||||
rclient.get session_lookup_key, (err, sessionId)->
|
||||
async.series [
|
||||
(cb)-> rclient.del sessionId, cb
|
||||
(cb)-> rclient.del session_lookup_key, cb
|
||||
], callback
|
||||
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
request = require 'request'
|
||||
Settings = require 'settings-sharelatex'
|
||||
logger = require 'logger-sharelatex'
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
TEN_SECONDS = 1000 * 10
|
||||
|
||||
module.exports = SpellingController =
|
||||
proxyRequestToSpellingApi: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
url = req.url.slice("/spelling".length)
|
||||
url = "/user/#{req.session.user._id}#{url}"
|
||||
url = "/user/#{user_id}#{url}"
|
||||
req.headers["Host"] = Settings.apis.spelling.host
|
||||
request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
|
||||
.on "error", (error) ->
|
||||
|
|
|
@ -5,12 +5,13 @@ Path = require "path"
|
|||
fs = require "fs"
|
||||
|
||||
ErrorController = require "../Errors/ErrorController"
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
|
||||
|
||||
module.exports = HomeController =
|
||||
index : (req,res)->
|
||||
if req.session.user
|
||||
if AuthenticationController.isUserLoggedIn(req)
|
||||
if req.query.scribtex_path?
|
||||
res.redirect "/project?scribtex_path=#{req.query.scribtex_path}"
|
||||
else
|
||||
|
@ -33,4 +34,4 @@ module.exports = HomeController =
|
|||
res.render "external/#{page}.jade",
|
||||
title: title
|
||||
else
|
||||
ErrorController.notFound(req, res, next)
|
||||
ErrorController.notFound(req, res, next)
|
||||
|
|
|
@ -1,75 +1,16 @@
|
|||
request = require("request")
|
||||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
ErrorController = require "../Errors/ErrorController"
|
||||
StaticPageHelpers = require("./StaticPageHelpers")
|
||||
sanitize = require('sanitizer')
|
||||
Settings = require("settings-sharelatex")
|
||||
contentful = require('contentful')
|
||||
marked = require("marked")
|
||||
sixpack = require("../../infrastructure/Sixpack")
|
||||
|
||||
|
||||
|
||||
module.exports = UniversityController =
|
||||
module.exports = UniversityController =
|
||||
|
||||
getPage: (req, res, next)->
|
||||
url = req.url?.toLowerCase()
|
||||
universityUrl = "#{settings.apis.university.url}#{url}"
|
||||
if StaticPageHelpers.shouldProxy(url)
|
||||
return UniversityController._directProxy universityUrl, res
|
||||
|
||||
logger.log url:url, "proxying request to university api"
|
||||
request.get universityUrl, (err, r, data)->
|
||||
if r?.statusCode == 404
|
||||
return UniversityController.getContentfulPage(req, res, next)
|
||||
if err?
|
||||
return res.send 500
|
||||
data = data.trim()
|
||||
try
|
||||
data = JSON.parse(data)
|
||||
data.content = data.content.replace(/__ref__/g, sanitize.escape(req.query.ref))
|
||||
catch err
|
||||
logger.err err:err, data:data, "error parsing data from data"
|
||||
res.render "university/university_holder", data
|
||||
|
||||
url = req.url?.toLowerCase().replace(".html","")
|
||||
return res.redirect("/i#{url}")
|
||||
|
||||
getIndexPage: (req, res)->
|
||||
client = sixpack.client(req?.session?.user?._id?.toString() || req.ip)
|
||||
client.participate 'instapage-pages', ['default', 'instapage'], (err, response)->
|
||||
if response?.alternative?.name == "instapage"
|
||||
return res.redirect("/i/university")
|
||||
else
|
||||
req.url = "/university/index.html"
|
||||
UniversityController.getPage req, res
|
||||
|
||||
_directProxy: (originUrl, res)->
|
||||
upstream = request.get(originUrl)
|
||||
upstream.on "error", (error) ->
|
||||
logger.error err: error, "university proxy error"
|
||||
upstream.pipe res
|
||||
|
||||
getContentfulPage: (req, res, next)->
|
||||
console.log Settings.contentful
|
||||
if !Settings.contentful?.uni?.space? and !Settings.contentful?.uni?.accessToken?
|
||||
return ErrorController.notFound(req, res, next)
|
||||
|
||||
client = contentful.createClient({
|
||||
space: Settings.contentful?.uni?.space
|
||||
accessToken: Settings.contentful?.uni?.accessToken
|
||||
})
|
||||
|
||||
url = req.url?.toLowerCase().replace("/university/","")
|
||||
client.getEntries({content_type: 'caseStudy', 'fields.slug':url})
|
||||
.catch (e)->
|
||||
return res.send 500
|
||||
.then (entry)->
|
||||
if !entry? or !entry.items? or entry.items.length == 0
|
||||
return ErrorController.notFound(req, res, next)
|
||||
viewData = entry.items[0].fields
|
||||
viewData.html = marked(viewData.content)
|
||||
res.render "university/case_study", viewData:viewData
|
||||
|
||||
|
||||
return res.redirect("/i/university")
|
||||
|
||||
|
|
|
@ -418,7 +418,15 @@ module.exports = RecurlyWrapper =
|
|||
url: "subscriptions/#{subscriptionId}/cancel",
|
||||
method: "put"
|
||||
}, (error, response, body) ->
|
||||
callback(error)
|
||||
if error?
|
||||
RecurlyWrapper._parseXml body, (_err, parsed) ->
|
||||
if parsed?.error?.description == "A canceled subscription can't transition to canceled"
|
||||
logger.log {subscriptionId, error, body}, "subscription already cancelled, not really an error, proceeding"
|
||||
callback(null)
|
||||
else
|
||||
callback(error)
|
||||
else
|
||||
callback(null)
|
||||
)
|
||||
|
||||
reactivateSubscription: (subscriptionId, callback) ->
|
||||
|
|
|
@ -8,192 +8,190 @@ Settings = require 'settings-sharelatex'
|
|||
logger = require('logger-sharelatex')
|
||||
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
|
||||
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
||||
UserGetter = require "../User/UserGetter"
|
||||
|
||||
module.exports = SubscriptionController =
|
||||
|
||||
plansPage: (req, res, next) ->
|
||||
plans = SubscriptionViewModelBuilder.buildViewModel()
|
||||
if !req.session.user?
|
||||
baseUrl = "/register?redir="
|
||||
else
|
||||
baseUrl = ""
|
||||
viewName = "subscriptions/plans"
|
||||
if req.query.v?
|
||||
viewName = "#{viewName}_#{req.query.v}"
|
||||
logger.log viewName:viewName, "showing plans page"
|
||||
currentUser = null
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
|
||||
return next(err) if err?
|
||||
res.render viewName,
|
||||
title: "plans_and_pricing"
|
||||
plans: plans
|
||||
baseUrl: baseUrl
|
||||
gaExperiments: Settings.gaExperiments.plansPage
|
||||
recomendedCurrency:recomendedCurrency
|
||||
render = () ->
|
||||
res.render viewName,
|
||||
title: "plans_and_pricing"
|
||||
plans: plans
|
||||
gaExperiments: Settings.gaExperiments.plansPage
|
||||
recomendedCurrency:recomendedCurrency
|
||||
shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if user_id?
|
||||
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
|
||||
return next(err) if err?
|
||||
currentUser = user
|
||||
render()
|
||||
else
|
||||
render()
|
||||
|
||||
#get to show the recurly.js page
|
||||
paymentPage: (req, res, next) ->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) =>
|
||||
return next(error) if error?
|
||||
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
|
||||
return next(err) if err?
|
||||
if hasSubscription or !plan?
|
||||
res.redirect "/user/subscription"
|
||||
else
|
||||
currency = req.query.currency?.toUpperCase()
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
|
||||
return next(err) if err?
|
||||
if recomendedCurrency? and !currency?
|
||||
currency = recomendedCurrency
|
||||
RecurlyWrapper.sign {
|
||||
subscription:
|
||||
plan_code : req.query.planCode
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
|
||||
return next(err) if err?
|
||||
if hasSubscription or !plan?
|
||||
res.redirect "/user/subscription"
|
||||
else
|
||||
currency = req.query.currency?.toUpperCase()
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
|
||||
return next(err) if err?
|
||||
if recomendedCurrency? and !currency?
|
||||
currency = recomendedCurrency
|
||||
RecurlyWrapper.sign {
|
||||
subscription:
|
||||
plan_code : req.query.planCode
|
||||
currency: currency
|
||||
account_code: user._id
|
||||
}, (error, signature) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/new",
|
||||
title : "subscribe"
|
||||
plan_code: req.query.planCode
|
||||
currency: currency
|
||||
countryCode:countryCode
|
||||
plan:plan
|
||||
showStudentPlan: req.query.ssp
|
||||
recurlyConfig: JSON.stringify
|
||||
currency: currency
|
||||
account_code: user._id
|
||||
}, (error, signature) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/new",
|
||||
title : "subscribe"
|
||||
plan_code: req.query.planCode
|
||||
currency: currency
|
||||
countryCode:countryCode
|
||||
plan:plan
|
||||
showStudentPlan: req.query.ssp
|
||||
recurlyConfig: JSON.stringify
|
||||
currency: currency
|
||||
subdomain: Settings.apis.recurly.subdomain
|
||||
showCouponField: req.query.scf
|
||||
showVatField: req.query.svf
|
||||
couponCode: req.query.cc or ""
|
||||
subdomain: Settings.apis.recurly.subdomain
|
||||
showCouponField: req.query.scf
|
||||
showVatField: req.query.svf
|
||||
couponCode: req.query.cc or ""
|
||||
|
||||
|
||||
|
||||
userSubscriptionPage: (req, res, next) ->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) =>
|
||||
return next(error) if error?
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
|
||||
return next(err) if err?
|
||||
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
|
||||
if subscription?.customAccount
|
||||
logger.log user: user, "redirecting to custom account page"
|
||||
res.redirect "/user/subscription/custom_account"
|
||||
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
|
||||
logger.log user:user, "redirecting to group subscription invite page"
|
||||
res.redirect groupLicenceInviteUrl
|
||||
else if !hasSubOrIsGroupMember
|
||||
logger.log user: user, "redirecting to plans"
|
||||
res.redirect "/user/subscription/plans"
|
||||
else
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
|
||||
return next(error) if error?
|
||||
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
|
||||
plans = SubscriptionViewModelBuilder.buildViewModel()
|
||||
res.render "subscriptions/dashboard",
|
||||
title: "your_subscription"
|
||||
recomendedCurrency: subscription?.currency
|
||||
taxRate:subscription?.taxRate
|
||||
plans: plans
|
||||
subscription: subscription || {}
|
||||
groupSubscriptions: groupSubscriptions
|
||||
subscriptionTabActive: true
|
||||
user:user
|
||||
saved_billing_details: req.query.saved_billing_details?
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
|
||||
return next(err) if err?
|
||||
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
|
||||
if subscription?.customAccount
|
||||
logger.log user: user, "redirecting to custom account page"
|
||||
res.redirect "/user/subscription/custom_account"
|
||||
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
|
||||
logger.log user:user, "redirecting to group subscription invite page"
|
||||
res.redirect groupLicenceInviteUrl
|
||||
else if !hasSubOrIsGroupMember
|
||||
logger.log user: user, "redirecting to plans"
|
||||
res.redirect "/user/subscription/plans"
|
||||
else
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
|
||||
return next(error) if error?
|
||||
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
|
||||
plans = SubscriptionViewModelBuilder.buildViewModel()
|
||||
res.render "subscriptions/dashboard",
|
||||
title: "your_subscription"
|
||||
recomendedCurrency: subscription?.currency
|
||||
taxRate:subscription?.taxRate
|
||||
plans: plans
|
||||
subscription: subscription || {}
|
||||
groupSubscriptions: groupSubscriptions
|
||||
subscriptionTabActive: true
|
||||
user:user
|
||||
saved_billing_details: req.query.saved_billing_details?
|
||||
|
||||
userCustomSubscriptionPage: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
|
||||
return next(err) if err?
|
||||
if !subscription?
|
||||
err = new Error("subscription null for custom account, user:#{user?._id}")
|
||||
logger.warn err:err, "subscription is null for custom accounts page"
|
||||
return next(err)
|
||||
res.render "subscriptions/custom_account",
|
||||
title: "your_subscription"
|
||||
subscription: subscription
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
|
||||
return next(err) if err?
|
||||
if !subscription?
|
||||
err = new Error("subscription null for custom account, user:#{user?._id}")
|
||||
logger.warn err:err, "subscription is null for custom accounts page"
|
||||
return next(err)
|
||||
res.render "subscriptions/custom_account",
|
||||
title: "your_subscription"
|
||||
subscription: subscription
|
||||
|
||||
|
||||
editBillingDetailsPage: (req, res, next) ->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
|
||||
return next(err) if err?
|
||||
if !hasSubscription
|
||||
res.redirect "/user/subscription"
|
||||
else
|
||||
RecurlyWrapper.sign {
|
||||
account_code: user._id
|
||||
}, (error, signature) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/edit-billing-details",
|
||||
title : "update_billing_details"
|
||||
recurlyConfig: JSON.stringify
|
||||
currency: "USD"
|
||||
subdomain: Settings.apis.recurly.subdomain
|
||||
signature : signature
|
||||
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
|
||||
user :
|
||||
id : user._id
|
||||
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
|
||||
return next(err) if err?
|
||||
if !hasSubscription
|
||||
res.redirect "/user/subscription"
|
||||
else
|
||||
RecurlyWrapper.sign {
|
||||
account_code: user._id
|
||||
}, (error, signature) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/edit-billing-details",
|
||||
title : "update_billing_details"
|
||||
recurlyConfig: JSON.stringify
|
||||
currency: "USD"
|
||||
subdomain: Settings.apis.recurly.subdomain
|
||||
signature : signature
|
||||
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
|
||||
user :
|
||||
id : user._id
|
||||
|
||||
updateBillingDetails: (req, res, next) ->
|
||||
res.redirect "/user/subscription?saved_billing_details=true"
|
||||
|
||||
createSubscription: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return callback(error) if error?
|
||||
recurly_token_id = req.body.recurly_token_id
|
||||
subscriptionDetails = req.body.subscriptionDetails
|
||||
logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
|
||||
SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
|
||||
return res.sendStatus 500
|
||||
res.sendStatus 201
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
recurly_token_id = req.body.recurly_token_id
|
||||
subscriptionDetails = req.body.subscriptionDetails
|
||||
logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
|
||||
SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
|
||||
return res.sendStatus 500
|
||||
res.sendStatus 201
|
||||
|
||||
successful_subscription: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) =>
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
|
||||
return next(error) if error?
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/successful_subscription",
|
||||
title: "thank_you"
|
||||
subscription:subscription
|
||||
res.render "subscriptions/successful_subscription",
|
||||
title: "thank_you"
|
||||
subscription:subscription
|
||||
|
||||
cancelSubscription: (req, res, next) ->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
logger.log user_id:user._id, "canceling subscription"
|
||||
SubscriptionHandler.cancelSubscription user, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
|
||||
return next(err)
|
||||
res.redirect "/user/subscription"
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log user_id:user._id, "canceling subscription"
|
||||
SubscriptionHandler.cancelSubscription user, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
|
||||
return next(err)
|
||||
res.redirect "/user/subscription"
|
||||
|
||||
updateSubscription: (req, res, next)->
|
||||
_origin = req?.query?.origin || null
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
planCode = req.body.plan_code
|
||||
if !planCode?
|
||||
err = new Error('plan_code is not defined')
|
||||
logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
planCode = req.body.plan_code
|
||||
if !planCode?
|
||||
err = new Error('plan_code is not defined')
|
||||
logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
|
||||
return next(err)
|
||||
logger.log planCode: planCode, user_id:user._id, "updating subscription"
|
||||
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
|
||||
return next(err)
|
||||
logger.log planCode: planCode, user_id:user._id, "updating subscription"
|
||||
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
|
||||
return next(err)
|
||||
res.redirect "/user/subscription"
|
||||
res.redirect "/user/subscription"
|
||||
|
||||
reactivateSubscription: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
logger.log user_id:user._id, "reactivating subscription"
|
||||
SubscriptionHandler.reactivateSubscription user, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
|
||||
return next(err)
|
||||
res.redirect "/user/subscription"
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log user_id:user._id, "reactivating subscription"
|
||||
SubscriptionHandler.reactivateSubscription user, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
|
||||
return next(err)
|
||||
res.redirect "/user/subscription"
|
||||
|
||||
recurlyCallback: (req, res, next)->
|
||||
logger.log data: req.body, "received recurly callback"
|
||||
|
@ -207,47 +205,44 @@ module.exports = SubscriptionController =
|
|||
res.sendStatus 200
|
||||
|
||||
renderUpgradeToAnnualPlanPage: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
return next(err) if err?
|
||||
planCode = subscription?.planCode.toLowerCase()
|
||||
if planCode?.indexOf("annual") != -1
|
||||
planName = "annual"
|
||||
else if planCode?.indexOf("student") != -1
|
||||
planName = "student"
|
||||
else if planCode?.indexOf("collaborator") != -1
|
||||
planName = "collaborator"
|
||||
if !hasSubscription
|
||||
return res.redirect("/user/subscription/plans")
|
||||
logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
|
||||
res.render "subscriptions/upgradeToAnnual",
|
||||
title: "Upgrade to annual"
|
||||
planName: planName
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
return next(err) if err?
|
||||
planCode = subscription?.planCode.toLowerCase()
|
||||
if planCode?.indexOf("annual") != -1
|
||||
planName = "annual"
|
||||
else if planCode?.indexOf("student") != -1
|
||||
planName = "student"
|
||||
else if planCode?.indexOf("collaborator") != -1
|
||||
planName = "collaborator"
|
||||
if !hasSubscription
|
||||
return res.redirect("/user/subscription/plans")
|
||||
logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
|
||||
res.render "subscriptions/upgradeToAnnual",
|
||||
title: "Upgrade to annual"
|
||||
planName: planName
|
||||
|
||||
processUpgradeToAnnualPlan: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
{planName} = req.body
|
||||
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
|
||||
annualPlanName = "#{planName}-annual"
|
||||
logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
|
||||
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "error updating subscription"
|
||||
return next(err)
|
||||
res.sendStatus 200
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
{planName} = req.body
|
||||
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
|
||||
annualPlanName = "#{planName}-annual"
|
||||
logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
|
||||
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "error updating subscription"
|
||||
return next(err)
|
||||
res.sendStatus 200
|
||||
|
||||
extendTrial: (req, res, next)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
return next(err) if err?
|
||||
SubscriptionHandler.extendTrial subscription, 14, (err)->
|
||||
if err?
|
||||
res.send 500
|
||||
else
|
||||
res.send 200
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
return next(err) if err?
|
||||
SubscriptionHandler.extendTrial subscription, 14, (err)->
|
||||
if err?
|
||||
res.send 500
|
||||
else
|
||||
res.send 200
|
||||
|
||||
recurlyNotificationParser: (req, res, next) ->
|
||||
xml = ""
|
||||
|
|
|
@ -3,27 +3,28 @@ logger = require("logger-sharelatex")
|
|||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
ErrorsController = require("../Errors/ErrorController")
|
||||
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
_ = require("underscore")
|
||||
async = require("async")
|
||||
|
||||
module.exports =
|
||||
|
||||
addUserToGroup: (req, res)->
|
||||
adminUserId = req.session.user._id
|
||||
adminUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
newEmail = req.body?.email?.toLowerCase()?.trim()
|
||||
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
|
||||
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
|
||||
if err?
|
||||
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
|
||||
return res.sendStatus 500
|
||||
result =
|
||||
result =
|
||||
user:user
|
||||
if err and err.limitReached
|
||||
result.limitReached = true
|
||||
res.json(result)
|
||||
|
||||
removeUserFromGroup: (req, res)->
|
||||
adminUserId = req.session.user._id
|
||||
adminUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
userToRemove_id = req.params.user_id
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
|
||||
|
@ -31,10 +32,10 @@ module.exports =
|
|||
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
|
||||
|
||||
removeSelfFromGroup: (req, res)->
|
||||
adminUserId = req.query.admin_user_id
|
||||
userToRemove_id = req.session.user._id
|
||||
userToRemove_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
|
||||
if err?
|
||||
|
@ -43,7 +44,7 @@ module.exports =
|
|||
res.send()
|
||||
|
||||
renderSubscriptionGroupAdminPage: (req, res)->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
|
||||
if !subscription.groupPlan
|
||||
return res.redirect("/")
|
||||
|
@ -55,11 +56,11 @@ module.exports =
|
|||
|
||||
renderGroupInvitePage: (req, res)->
|
||||
group_subscription_id = req.params.subscription_id
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id)
|
||||
if !licence?
|
||||
return ErrorsController.notFound(req, res)
|
||||
jobs =
|
||||
jobs =
|
||||
partOfGroup: (cb)->
|
||||
SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb
|
||||
subscription: (cb)->
|
||||
|
@ -77,22 +78,26 @@ module.exports =
|
|||
|
||||
beginJoinGroup: (req, res)->
|
||||
subscription_id = req.params.subscription_id
|
||||
user_id = req.session.user._id
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
if !currentUser?
|
||||
logger.err {subscription_id}, "error getting current user"
|
||||
return res.sendStatus 500
|
||||
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
|
||||
if !licence?
|
||||
return ErrorsController.notFound(req, res)
|
||||
SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)->
|
||||
SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)->
|
||||
if err?
|
||||
res.sendStatus 500
|
||||
else
|
||||
res.sendStatus 200
|
||||
|
||||
completeJoin: (req, res)->
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
subscription_id = req.params.subscription_id
|
||||
if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)?
|
||||
return ErrorsController.notFound(req, res)
|
||||
email = req?.session?.user?.email
|
||||
logger.log subscription_id:subscription_id, user_id:req?.session?.user?._id, email:email, "starting the completion of joining group"
|
||||
email = currentUser?.email
|
||||
logger.log subscription_id:subscription_id, user_id:currentUser?._id, email:email, "starting the completion of joining group"
|
||||
SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)->
|
||||
if err? and err == "token_not_found"
|
||||
return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
|
||||
|
@ -109,10 +114,10 @@ module.exports =
|
|||
return ErrorsController.notFound(req, res)
|
||||
res.render "subscriptions/group/successful_join",
|
||||
title: "Sucessfully joined group"
|
||||
licenceName:licence.name
|
||||
licenceName:licence.name
|
||||
|
||||
exportGroupCsv: (req, res)->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user_id: user_id, "exporting group csv"
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
|
||||
if !subscription.groupPlan
|
||||
|
|
|
@ -7,6 +7,8 @@ SubscriptionUpdater = require("./SubscriptionUpdater")
|
|||
LimitationsManager = require('./LimitationsManager')
|
||||
EmailHandler = require("../Email/EmailHandler")
|
||||
Events = require "../../infrastructure/Events"
|
||||
Analytics = require("../Analytics/AnalyticsManager")
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
|
@ -52,6 +54,7 @@ module.exports =
|
|||
setTimeout (-> EmailHandler.sendEmail "canceledSubscription", emailOpts
|
||||
), ONE_HOUR_IN_MS
|
||||
Events.emit "cancelSubscription", user._id
|
||||
Analytics.recordEvent user._id, "subscription-canceled"
|
||||
callback()
|
||||
else
|
||||
callback()
|
||||
|
|
|
@ -14,7 +14,11 @@ module.exports =
|
|||
logger.log user_id:user_id, "got users subscription"
|
||||
callback(err, subscription)
|
||||
|
||||
getMemberSubscriptions: (user_id, callback) ->
|
||||
getMemberSubscriptions: (user_or_id, callback) ->
|
||||
if user_or_id? and user_or_id._id?
|
||||
user_id = user_or_id._id
|
||||
else if user_or_id?
|
||||
user_id = user_or_id
|
||||
logger.log user_id: user_id, "getting users group subscriptions"
|
||||
Subscription.find(member_ids: user_id).populate("admin_id").exec callback
|
||||
|
||||
|
@ -25,4 +29,4 @@ module.exports =
|
|||
Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback
|
||||
|
||||
getGroupSubscriptionMemberOf: (user_id, callback)->
|
||||
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
|
||||
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
|
||||
|
|
|
@ -4,6 +4,7 @@ PlansLocator = require("./PlansLocator")
|
|||
SubscriptionFormatters = require("./SubscriptionFormatters")
|
||||
LimitationsManager = require("./LimitationsManager")
|
||||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
logger = require('logger-sharelatex')
|
||||
_ = require("underscore")
|
||||
|
||||
module.exports =
|
||||
|
@ -16,6 +17,10 @@ module.exports =
|
|||
if subscription?
|
||||
return callback(error) if error?
|
||||
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
|
||||
if !plan?
|
||||
err = new Error("No plan found for planCode '#{subscription.planCode}'")
|
||||
logger.error {user_id: user._id, err}, "error getting subscription plan for user"
|
||||
return callback(err)
|
||||
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
|
||||
tax = recurlySubscription?.tax_in_cents || 0
|
||||
callback null, {
|
||||
|
@ -39,7 +44,7 @@ module.exports =
|
|||
allPlans = {}
|
||||
plans.forEach (plan)->
|
||||
allPlans[plan.planCode] = plan
|
||||
|
||||
|
||||
result =
|
||||
allPlans: allPlans
|
||||
|
||||
|
@ -49,7 +54,7 @@ module.exports =
|
|||
|
||||
result.studentAccounts = _.filter plans, (plan)->
|
||||
plan.planCode.indexOf("student") != -1
|
||||
|
||||
|
||||
result.groupMonthlyPlans = _.filter plans, (plan)->
|
||||
plan.groupPlan and !plan.annual
|
||||
|
||||
|
@ -63,4 +68,3 @@ module.exports =
|
|||
!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
@ -1,48 +1,49 @@
|
|||
TagsHandler = require("./TagsHandler")
|
||||
logger = require("logger-sharelatex")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports =
|
||||
getAllTags: (req, res, next)->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "getting tags"
|
||||
TagsHandler.getAllTags user_id, (error, allTags)->
|
||||
return next(error) if error?
|
||||
res.json(allTags)
|
||||
|
||||
|
||||
createTag: (req, res, next) ->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
name = req.body.name
|
||||
logger.log {user_id, name}, "creating tag"
|
||||
TagsHandler.createTag user_id, name, (error, tag) ->
|
||||
return next(error) if error?
|
||||
res.json(tag)
|
||||
|
||||
|
||||
addProjectToTag: (req, res, next) ->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
{tag_id, project_id} = req.params
|
||||
logger.log {user_id, tag_id, project_id}, "adding tag to project"
|
||||
TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.status(204).end()
|
||||
|
||||
|
||||
removeProjectFromTag: (req, res, next) ->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
{tag_id, project_id} = req.params
|
||||
logger.log {user_id, tag_id, project_id}, "removing tag from project"
|
||||
TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.status(204).end()
|
||||
|
||||
|
||||
deleteTag: (req, res, next) ->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
tag_id = req.params.tag_id
|
||||
logger.log {user_id, tag_id}, "deleting tag"
|
||||
TagsHandler.deleteTag user_id, tag_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.status(204).end()
|
||||
|
||||
|
||||
renameTag: (req, res, next) ->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
tag_id = req.params.tag_id
|
||||
name = req.body?.name
|
||||
if !name?
|
||||
|
|
|
@ -33,16 +33,11 @@ module.exports =
|
|||
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
|
||||
|
||||
deleteUpdate: (project_id, path, source, callback)->
|
||||
projectLocator.findElementByPath project_id, path, (err, element)->
|
||||
type = 'file'
|
||||
projectLocator.findElementByPath project_id, path, (err, element, type)->
|
||||
if err? || !element?
|
||||
logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
|
||||
return callback()
|
||||
if element.lines?
|
||||
type = 'doc'
|
||||
else if element.folders?
|
||||
type = 'folder'
|
||||
logger.log project_id:project_id, updateType:path, updateType:type, element:element, "processing update to delete entity from tpds"
|
||||
logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds"
|
||||
editorController.deleteEntity project_id, element._id, type, source, (err)->
|
||||
logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds"
|
||||
callback()
|
||||
|
@ -56,12 +51,13 @@ module.exports =
|
|||
return callback(err)
|
||||
logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds"
|
||||
if doc_id?
|
||||
editorController.setDoc project_id, doc_id, user_id, docLines, source, (err)->
|
||||
callback()
|
||||
editorController.setDoc project_id, doc_id, user_id, docLines, source, callback
|
||||
else
|
||||
setupNewEntity project_id, path, (err, folder, fileName)->
|
||||
editorController.addDoc project_id, folder._id, fileName, docLines, source, (err)->
|
||||
callback()
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file"
|
||||
return callback(err)
|
||||
editorController.addDoc project_id, folder._id, fileName, docLines, source, callback
|
||||
|
||||
processFile: (project_id, file_id, fsPath, path, source, callback)->
|
||||
finish = (err)->
|
||||
|
@ -69,10 +65,13 @@ module.exports =
|
|||
callback(err)
|
||||
logger.log project_id:project_id, file_id:file_id, path:path, "processing file update from tpds"
|
||||
setupNewEntity project_id, path, (err, folder, fileName) =>
|
||||
if file_id?
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file"
|
||||
return callback(err)
|
||||
else if file_id?
|
||||
editorController.replaceFile project_id, file_id, fsPath, source, finish
|
||||
else
|
||||
editorController.addFile project_id, folder._id, fileName, fsPath, source, finish
|
||||
editorController.addFile project_id, folder?._id, fileName, fsPath, source, finish
|
||||
|
||||
writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
|
||||
if !file_id?
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
DocstoreManager = require "../Docstore/DocstoreManager"
|
||||
UserInfoManager = require "../User/UserInfoManager"
|
||||
async = require "async"
|
||||
|
||||
module.exports = RangesManager =
|
||||
getAllRanges: (project_id, callback = (error, docs) ->) ->
|
||||
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
|
||||
return callback(error) if error?
|
||||
DocstoreManager.getAllRanges project_id, callback
|
||||
|
||||
getAllChangesUsers: (project_id, callback = (error, users) ->) ->
|
||||
user_ids = {}
|
||||
RangesManager.getAllRanges project_id, (error, docs) ->
|
||||
return callback(error) if error?
|
||||
jobs = []
|
||||
for doc in docs
|
||||
for change in doc.ranges?.changes or []
|
||||
user_ids[change.metadata.user_id] = true
|
||||
|
||||
async.mapSeries Object.keys(user_ids), (user_id, cb) ->
|
||||
UserInfoManager.getPersonalInfo user_id, cb
|
||||
, callback
|
|
@ -1,21 +1,42 @@
|
|||
RangesManager = require "./RangesManager"
|
||||
logger = require "logger-sharelatex"
|
||||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
UserInfoController = require "../User/UserInfoController"
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
TrackChangesManager = require "./TrackChangesManager"
|
||||
|
||||
module.exports = TrackChangesController =
|
||||
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
||||
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
|
||||
getAllRanges: (req, res, next) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log {project_id}, "request for project ranges"
|
||||
RangesManager.getAllRanges project_id, (error, docs = []) ->
|
||||
return next(error) if error?
|
||||
url = settings.apis.trackchanges.url + req.url
|
||||
logger.log url: url, "proxying to track-changes api"
|
||||
getReq = request(
|
||||
url: url
|
||||
method: req.method
|
||||
headers:
|
||||
"X-User-Id": user_id
|
||||
)
|
||||
getReq.pipe(res)
|
||||
getReq.on "error", (error) ->
|
||||
logger.error err: error, "track-changes API error"
|
||||
next(error)
|
||||
docs = ({id: d._id, ranges: d.ranges} for d in docs)
|
||||
res.json docs
|
||||
|
||||
getAllChangesUsers: (req, res, next) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log {project_id}, "request for project range users"
|
||||
RangesManager.getAllChangesUsers project_id, (error, users) ->
|
||||
return next(error) if error?
|
||||
users = (UserInfoController.formatPersonalInfo(user) for user in users)
|
||||
# Get rid of any anonymous/deleted user objects
|
||||
users = users.filter (u) -> u?.id?
|
||||
res.json users
|
||||
|
||||
acceptChange: (req, res, next) ->
|
||||
{project_id, doc_id, change_id} = req.params
|
||||
logger.log {project_id, doc_id, change_id}, "request to accept change"
|
||||
DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) ->
|
||||
return next(error) if error?
|
||||
EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)->
|
||||
res.send 204
|
||||
|
||||
toggleTrackChanges: (req, res, next) ->
|
||||
{project_id} = req.params
|
||||
track_changes_on = !!req.body.on
|
||||
logger.log {project_id, track_changes_on}, "request to toggle track changes"
|
||||
TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) ->
|
||||
return next(error) if error?
|
||||
EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)->
|
||||
res.send 204
|
||||
|
|
|
@ -1,28 +1,5 @@
|
|||
settings = require "settings-sharelatex"
|
||||
request = require "request"
|
||||
logger = require "logger-sharelatex"
|
||||
Project = require("../../models/Project").Project
|
||||
|
||||
module.exports = TrackChangesManager =
|
||||
flushProject: (project_id, callback = (error) ->) ->
|
||||
logger.log project_id: project_id, "flushing project in track-changes api"
|
||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
|
||||
request.post url, (error, res, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null)
|
||||
else
|
||||
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
||||
logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
|
||||
callback(error)
|
||||
|
||||
archiveProject: (project_id, callback = ()->)->
|
||||
logger.log project_id: project_id, "archving project in track-changes api"
|
||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
|
||||
request.post url, (error, res, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null)
|
||||
else
|
||||
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
|
||||
logger.error err: error, project_id: project_id, "error archving project in track-changes api"
|
||||
callback(error)
|
||||
toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
|
||||
Project.update {_id: project_id}, {track_changes: track_changes_on}, callback
|
||||
|
|
|
@ -4,11 +4,12 @@ fs = require "fs"
|
|||
Path = require "path"
|
||||
FileSystemImportManager = require "./FileSystemImportManager"
|
||||
ProjectUploadManager = require "./ProjectUploadManager"
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports = ProjectUploadController =
|
||||
uploadProject: (req, res, next) ->
|
||||
timer = new metrics.Timer("project-upload")
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
{originalname, path} = req.files.qqfile
|
||||
name = Path.basename(originalname, ".zip")
|
||||
ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) ->
|
||||
|
@ -24,7 +25,7 @@ module.exports = ProjectUploadController =
|
|||
project: project._id, file_path: path, file_name: name,
|
||||
"uploaded project"
|
||||
res.send success: true, project_id: project._id
|
||||
|
||||
|
||||
uploadFile: (req, res, next) ->
|
||||
timer = new metrics.Timer("file-upload")
|
||||
name = req.files.qqfile?.originalname
|
||||
|
@ -35,7 +36,7 @@ module.exports = ProjectUploadController =
|
|||
logger.err project_id:project_id, name:name, "bad name when trying to upload file"
|
||||
return res.send success: false
|
||||
logger.log folder_id:folder_id, project_id:project_id, "getting upload file request"
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
|
||||
fs.unlink path, ->
|
||||
timer.done()
|
||||
|
@ -50,6 +51,3 @@ module.exports = ProjectUploadController =
|
|||
project_id: project_id, file_path: path, file_name: name, folder_id: folder_id
|
||||
"uploaded file"
|
||||
res.send success: true, entity_id: entity?._id
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -8,27 +8,50 @@ logger = require("logger-sharelatex")
|
|||
metrics = require("../../infrastructure/Metrics")
|
||||
Url = require("url")
|
||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserSessionsManager = require("./UserSessionsManager")
|
||||
UserUpdater = require("./UserUpdater")
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports = UserController =
|
||||
|
||||
deleteUser: (req, res)->
|
||||
user_id = req.session.user._id
|
||||
UserDeleter.deleteUser user_id, (err)->
|
||||
if !err?
|
||||
req.session?.destroy()
|
||||
res.sendStatus(200)
|
||||
tryDeleteUser: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
password = req.body.password
|
||||
logger.log {user_id}, "trying to delete user account"
|
||||
if !password? or password == ''
|
||||
logger.err {user_id}, 'no password supplied for attempt to delete account'
|
||||
return res.sendStatus(403)
|
||||
AuthenticationManager.authenticate {_id: user_id}, password, (err, user) ->
|
||||
if err?
|
||||
logger.err {user_id}, 'error authenticating during attempt to delete account'
|
||||
return next(err)
|
||||
if !user
|
||||
logger.err {user_id}, 'auth failed during attempt to delete account'
|
||||
return res.sendStatus(403)
|
||||
UserDeleter.deleteUser user_id, (err) ->
|
||||
if err?
|
||||
logger.err {user_id}, "error while deleting user account"
|
||||
return next(err)
|
||||
sessionId = req.sessionID
|
||||
req.logout?()
|
||||
req.session.destroy (err) ->
|
||||
if err?
|
||||
logger.err err: err, 'error destorying session'
|
||||
return next(err)
|
||||
UserSessionsManager.untrackSession(user, sessionId)
|
||||
res.sendStatus(200)
|
||||
|
||||
unsubscribe: (req, res)->
|
||||
UserLocator.findById req.session.user._id, (err, user)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
newsLetterManager.unsubscribe user, ->
|
||||
res.send()
|
||||
|
||||
updateUserSettings : (req, res)->
|
||||
logger.log user: req.session.user, "updating account settings"
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
usingExternalAuth = settings.ldap? or settings.saml?
|
||||
logger.log user_id: user_id, "updating account settings"
|
||||
User.findById user_id, (err, user)->
|
||||
if err? or !user?
|
||||
logger.err err:err, user_id:user_id, "problem updaing user settings"
|
||||
|
@ -54,13 +77,19 @@ module.exports = UserController =
|
|||
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
|
||||
if req.body.pdfViewer?
|
||||
user.ace.pdfViewer = req.body.pdfViewer
|
||||
if req.body.syntaxValidation?
|
||||
user.ace.syntaxValidation = req.body.syntaxValidation
|
||||
user.save (err)->
|
||||
newEmail = req.body.email?.trim().toLowerCase()
|
||||
if !newEmail? or newEmail == user.email
|
||||
if !newEmail? or newEmail == user.email or usingExternalAuth
|
||||
# end here, don't update email
|
||||
AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name})
|
||||
return res.sendStatus 200
|
||||
else if newEmail.indexOf("@") == -1
|
||||
# email invalid
|
||||
return res.sendStatus(400)
|
||||
else
|
||||
# update the user email
|
||||
UserUpdater.changeEmailAddress user_id, newEmail, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address"
|
||||
|
@ -73,7 +102,7 @@ module.exports = UserController =
|
|||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting user for email update"
|
||||
return res.send 500
|
||||
req.session.user.email = user.email
|
||||
AuthenticationController.setInSessionUser(req, {email: user.email, first_name: user.first_name, last_name: user.last_name})
|
||||
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
|
||||
if err?
|
||||
logger.err err:err, "error populateGroupLicenceInvite"
|
||||
|
@ -81,9 +110,10 @@ module.exports = UserController =
|
|||
|
||||
logout : (req, res)->
|
||||
metrics.inc "user.logout"
|
||||
logger.log user: req?.session?.user, "logging out"
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log user: user, "logging out"
|
||||
sessionId = req.sessionID
|
||||
user = req?.session?.user
|
||||
req.logout?() # passport logout
|
||||
req.session.destroy (err)->
|
||||
if err
|
||||
logger.err err: err, 'error destorying session'
|
||||
|
@ -102,13 +132,22 @@ module.exports = UserController =
|
|||
setNewPasswordUrl: setNewPasswordUrl
|
||||
}
|
||||
|
||||
clearSessions: (req, res, next = (error) ->) ->
|
||||
metrics.inc "user.clear-sessions"
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log {user_id: user._id}, "clearing sessions for user"
|
||||
UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) ->
|
||||
return next(err) if err?
|
||||
res.sendStatus 201
|
||||
|
||||
changePassword : (req, res, next = (error) ->)->
|
||||
metrics.inc "user.password-change"
|
||||
oldPass = req.body.currentPassword
|
||||
AuthenticationManager.authenticate {_id:req.session.user._id}, oldPass, (err, user)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
AuthenticationManager.authenticate {_id:user_id}, oldPass, (err, user)->
|
||||
return next(err) if err?
|
||||
if(user)
|
||||
logger.log user: req.session.user, "changing password"
|
||||
logger.log user: user._id, "changing password"
|
||||
newPassword1 = req.body.newPassword1
|
||||
newPassword2 = req.body.newPassword2
|
||||
if newPassword1 != newPassword2
|
||||
|
@ -128,7 +167,7 @@ module.exports = UserController =
|
|||
type:'success'
|
||||
text:'Your password has been changed'
|
||||
else
|
||||
logger.log user: user, "current password wrong"
|
||||
logger.log user_id: user_id, "current password wrong"
|
||||
res.send
|
||||
message:
|
||||
type:'error'
|
||||
|
|
|
@ -17,6 +17,7 @@ module.exports =
|
|||
user = new User()
|
||||
user.email = opts.email
|
||||
user.holdingAccount = opts.holdingAccount
|
||||
user.ace.syntaxValidation = true
|
||||
|
||||
username = opts.email.match(/^[^@]*/)
|
||||
if opts.first_name? and opts.first_name.length != 0
|
||||
|
|
|
@ -3,12 +3,14 @@ logger = require("logger-sharelatex")
|
|||
UserDeleter = require("./UserDeleter")
|
||||
UserUpdater = require("./UserUpdater")
|
||||
sanitize = require('sanitizer')
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports = UserController =
|
||||
getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) ->
|
||||
logger.log user: req.user, "reciving request for getting logged in users personal info"
|
||||
return next(new Error("User is not logged in")) if !req.user?
|
||||
UserGetter.getUser req.user._id, {
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user_id: user_id, "reciving request for getting logged in users personal info"
|
||||
return next(new Error("User is not logged in")) if !user_id?
|
||||
UserGetter.getUser user_id, {
|
||||
first_name: true, last_name: true,
|
||||
role:true, institution:true,
|
||||
email: true, signUpDate: true
|
||||
|
@ -24,20 +26,14 @@ module.exports = UserController =
|
|||
UserController.sendFormattedPersonalInfo(user, res, next)
|
||||
|
||||
sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
|
||||
UserController._formatPersonalInfo user, (error, info) ->
|
||||
return next(error) if error?
|
||||
res.send JSON.stringify(info)
|
||||
info = UserController.formatPersonalInfo(user)
|
||||
res.send JSON.stringify(info)
|
||||
|
||||
_formatPersonalInfo: (user, callback = (error, info) ->) ->
|
||||
callback null, {
|
||||
id: user._id.toString()
|
||||
first_name: user.first_name
|
||||
last_name: user.last_name
|
||||
email: user.email
|
||||
signUpDate: user.signUpDate
|
||||
role: user.role
|
||||
institution: user.institution
|
||||
}
|
||||
|
||||
|
||||
|
||||
formatPersonalInfo: (user, callback = (error, info) ->) ->
|
||||
if !user?
|
||||
return {}
|
||||
formatted_user = { id: user._id.toString() }
|
||||
for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"]
|
||||
if user[key]?
|
||||
formatted_user[key] = user[key]
|
||||
return formatted_user
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
UserGetter = require "./UserGetter"
|
||||
|
||||
module.exports = UserInfoManager =
|
||||
getPersonalInfo: (user_id, callback = (error) ->) ->
|
||||
UserGetter.getUser user_id, { _id: true, first_name: true, last_name: true, email: true }, callback
|
|
@ -1,9 +1,11 @@
|
|||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
UserSessionsManager = require("./UserSessionsManager")
|
||||
ErrorController = require("../Errors/ErrorController")
|
||||
logger = require("logger-sharelatex")
|
||||
Settings = require("settings-sharelatex")
|
||||
fs = require('fs')
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports =
|
||||
|
||||
|
@ -18,18 +20,17 @@ module.exports =
|
|||
|
||||
res.render 'user/register',
|
||||
title: 'register'
|
||||
redir: req.query.redir
|
||||
sharedProjectData: sharedProjectData
|
||||
newTemplateData: newTemplateData
|
||||
new_email:req.query.new_email || ""
|
||||
|
||||
|
||||
activateAccountPage: (req, res) ->
|
||||
# An 'activation' is actually just a password reset on an account that
|
||||
# was set with a random password originally.
|
||||
logger.log query:req.query, "activiate account page called"
|
||||
if !req.query?.user_id? or !req.query?.token?
|
||||
return ErrorController.notFound(req, res)
|
||||
|
||||
|
||||
UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) ->
|
||||
return next(error) if error?
|
||||
if !user
|
||||
|
@ -47,17 +48,35 @@ module.exports =
|
|||
token: req.query.token
|
||||
|
||||
loginPage : (req, res)->
|
||||
# if user is being sent to /login with explicit redirect (redir=/foo),
|
||||
# such as being sent from the editor to /login, then set the redirect explicitly
|
||||
if req.query.redir? and !AuthenticationController._getRedirectFromSession(req)?
|
||||
logger.log {redir: req.query.redir}, "setting explicit redirect from login page"
|
||||
AuthenticationController._setRedirectInSession(req, req.query.redir)
|
||||
res.render 'user/login',
|
||||
title: 'login',
|
||||
redir: req.query.redir,
|
||||
email: req.query.email
|
||||
|
||||
settingsPage : (req, res, next)->
|
||||
logger.log user: req.session.user, "loading settings page"
|
||||
UserLocator.findById req.session.user._id, (err, user)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user: user_id, "loading settings page"
|
||||
shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin)
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
return next(err) if err?
|
||||
res.render 'user/settings',
|
||||
title:'account_settings'
|
||||
user: user,
|
||||
shouldAllowEditingDetails: shouldAllowEditingDetails
|
||||
languages: Settings.languages,
|
||||
accountSettingsTabActive: true
|
||||
|
||||
sessionsPage: (req, res, next) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log user_id: user._id, "loading sessions page"
|
||||
UserSessionsManager.getAllUserSessions user, [req.sessionID], (err, sessions) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id}, "error getting all user sessions"
|
||||
return next(err)
|
||||
res.render 'user/sessions',
|
||||
title: "sessions"
|
||||
sessions: sessions
|
||||
|
|
|
@ -3,14 +3,11 @@ redis = require('redis-sharelatex')
|
|||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
_ = require('underscore')
|
||||
UserSessionsRedis = require('./UserSessionsRedis')
|
||||
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
rclient = UserSessionsRedis.client()
|
||||
|
||||
module.exports = UserSessionsManager =
|
||||
|
||||
_sessionSetKey: (user) ->
|
||||
return "UserSessions:#{user._id}"
|
||||
|
||||
# mimic the key used by the express sessions
|
||||
_sessionKey: (sessionId) ->
|
||||
return "sess:#{sessionId}"
|
||||
|
@ -23,7 +20,7 @@ module.exports = UserSessionsManager =
|
|||
logger.log {user_id: user._id}, "no sessionId to track, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id, sessionId}, "onLogin handler"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
|
||||
value = UserSessionsManager._sessionKey sessionId
|
||||
rclient.multi()
|
||||
.sadd(sessionSetKey, value)
|
||||
|
@ -43,7 +40,7 @@ module.exports = UserSessionsManager =
|
|||
logger.log {user_id: user._id}, "no sessionId to untrack, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id, sessionId}, "onLogout handler"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
|
||||
value = UserSessionsManager._sessionKey sessionId
|
||||
rclient.multi()
|
||||
.srem(sessionSetKey, value)
|
||||
|
@ -55,15 +52,45 @@ module.exports = UserSessionsManager =
|
|||
UserSessionsManager._checkSessions(user, () ->)
|
||||
callback()
|
||||
|
||||
getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) ->
|
||||
exclude = _.map(exclude, UserSessionsManager._sessionKey)
|
||||
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
|
||||
rclient.smembers sessionSetKey, (err, sessionKeys) ->
|
||||
if err?
|
||||
logger.err user_id: user._id, "error getting all session keys for user from redis"
|
||||
return callback(err)
|
||||
sessionKeys = _.filter sessionKeys, (k) -> !(_.contains(exclude, k))
|
||||
if sessionKeys.length == 0
|
||||
logger.log {user_id: user._id}, "no other sessions found, returning"
|
||||
return callback(null, [])
|
||||
|
||||
Async.mapSeries sessionKeys, ((k, cb) -> rclient.get(k, cb)), (err, sessions) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id}, "error getting all sessions for user from redis"
|
||||
return callback(err)
|
||||
|
||||
result = []
|
||||
for session in sessions
|
||||
if session is null
|
||||
continue
|
||||
session = JSON.parse(session)
|
||||
session_user = session?.user or session?.passport?.user
|
||||
result.push {
|
||||
ip_address: session_user.ip_address,
|
||||
session_created: session_user.session_created
|
||||
}
|
||||
|
||||
return callback(null, result)
|
||||
|
||||
revokeAllUserSessions: (user, retain, callback=(err)->) ->
|
||||
if !retain
|
||||
if !retain?
|
||||
retain = []
|
||||
retain = retain.map((i) -> UserSessionsManager._sessionKey(i))
|
||||
if !user
|
||||
if !user?
|
||||
logger.log {}, "no user to revoke sessions for, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id}, "revoking all existing sessions for user"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
|
||||
rclient.smembers sessionSetKey, (err, sessionKeys) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
|
||||
|
@ -73,12 +100,18 @@ module.exports = UserSessionsManager =
|
|||
logger.log {user_id: user._id}, "no sessions in UserSessions set to delete, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user"
|
||||
rclient.multi()
|
||||
.del(keysToDelete)
|
||||
.srem(sessionSetKey, keysToDelete)
|
||||
.exec (err, result) ->
|
||||
|
||||
deletions = keysToDelete.map (k) ->
|
||||
(cb) ->
|
||||
rclient.del k, cb
|
||||
|
||||
Async.series deletions, (err, _result) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "errror revoking all sessions for user"
|
||||
return callback(err)
|
||||
rclient.srem sessionSetKey, keysToDelete, (err) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user"
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error removing session set for user"
|
||||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
|
@ -86,7 +119,7 @@ module.exports = UserSessionsManager =
|
|||
if !user
|
||||
logger.log {}, "no user to touch sessions for, returning"
|
||||
return callback(null)
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
|
||||
rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set"
|
||||
|
@ -98,7 +131,7 @@ module.exports = UserSessionsManager =
|
|||
logger.log {}, "no user, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id}, "checking sessions for user"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
|
||||
rclient.smembers sessionSetKey, (err, sessionKeys) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
redis = require 'redis-sharelatex'
|
||||
ioredis = require 'ioredis'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
redisSessionsSettings = Settings.redis.websessions or Settings.redis.web
|
||||
|
||||
module.exports = Redis =
|
||||
client: () ->
|
||||
if redisSessionsSettings?.cluster?
|
||||
logger.log {}, "using redis cluster for web sessions"
|
||||
rclient = new ioredis.Cluster(redisSessionsSettings.cluster)
|
||||
else
|
||||
rclient = redis.createClient(redisSessionsSettings)
|
||||
return rclient
|
||||
|
||||
sessionSetKey: (user) ->
|
||||
if redisSessionsSettings?.cluster?
|
||||
return "UserSessions:{#{user._id}}"
|
||||
else
|
||||
return "UserSessions:#{user._id}"
|
|
@ -5,42 +5,54 @@ Settings = require('settings-sharelatex')
|
|||
SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters')
|
||||
querystring = require('querystring')
|
||||
SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
|
||||
AuthenticationController = require("../Features/Authentication/AuthenticationController")
|
||||
_ = require("underscore")
|
||||
async = require("async")
|
||||
Modules = require "./Modules"
|
||||
Url = require "url"
|
||||
|
||||
PackageVersions = require "./PackageVersions"
|
||||
fingerprints = {}
|
||||
Path = require 'path'
|
||||
|
||||
|
||||
jsPath =
|
||||
if Settings.useMinifiedJs
|
||||
"/minjs/"
|
||||
else
|
||||
"/js/"
|
||||
|
||||
ace = PackageVersions.lib('ace')
|
||||
pdfjs = PackageVersions.lib('pdfjs')
|
||||
|
||||
logger.log "Generating file fingerprints..."
|
||||
for path in [
|
||||
"#{jsPath}libs/require.js",
|
||||
"#{jsPath}ide.js",
|
||||
"#{jsPath}main.js",
|
||||
"#{jsPath}libs.js",
|
||||
"#{jsPath}ace/ace.js",
|
||||
"#{jsPath}libs/pdfjs-1.3.91/pdf.js",
|
||||
"#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js",
|
||||
"#{jsPath}libs/pdfjs-1.3.91/compatibility.js",
|
||||
"/stylesheets/style.css"
|
||||
]
|
||||
filePath = Path.join __dirname, "../../../", "public#{path}"
|
||||
getFileContent = (filePath)->
|
||||
filePath = Path.join __dirname, "../../../", "public#{filePath}"
|
||||
exists = fs.existsSync filePath
|
||||
if exists
|
||||
content = fs.readFileSync filePath
|
||||
hash = crypto.createHash("md5").update(content).digest("hex")
|
||||
logger.log "#{filePath}: #{hash}"
|
||||
fingerprints[path] = hash
|
||||
return content
|
||||
else
|
||||
logger.log filePath:filePath, "file does not exist for fingerprints"
|
||||
return ""
|
||||
|
||||
logger.log "Generating file fingerprints..."
|
||||
pathList = [
|
||||
["#{jsPath}libs/require.js"]
|
||||
["#{jsPath}ide.js"]
|
||||
["#{jsPath}main.js"]
|
||||
["#{jsPath}libs.js"]
|
||||
["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"]
|
||||
["#{jsPath}libs/#{pdfjs}/pdf.js"]
|
||||
["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
|
||||
["#{jsPath}libs/#{pdfjs}/compatibility.js"]
|
||||
["/stylesheets/style.css"]
|
||||
]
|
||||
|
||||
for paths in pathList
|
||||
contentList = _.map(paths, getFileContent)
|
||||
content = contentList.join("")
|
||||
hash = crypto.createHash("md5").update(content).digest("hex")
|
||||
_.each paths, (filePath)->
|
||||
logger.log "#{filePath}: #{hash}"
|
||||
fingerprints[filePath] = hash
|
||||
|
||||
getFingerprint = (path) ->
|
||||
if fingerprints[path]?
|
||||
|
@ -59,12 +71,13 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
res.locals.session = req.session
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next)->
|
||||
|
||||
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
|
||||
if cdnBlocked and !req.session.cdnBlocked?
|
||||
logger.log user_id:req?.session?.user?._id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
|
||||
logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
|
||||
req.session.cdnBlocked = true
|
||||
|
||||
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
|
||||
|
@ -77,16 +90,16 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
staticFilesBase = Settings.cdn?.web?.darkHost
|
||||
else
|
||||
staticFilesBase = ""
|
||||
|
||||
|
||||
res.locals.jsPath = jsPath
|
||||
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
|
||||
|
||||
res.locals.lib = PackageVersions.lib
|
||||
|
||||
res.locals.buildJsPath = (jsFile, opts = {})->
|
||||
path = Path.join(jsPath, jsFile)
|
||||
|
||||
doFingerPrint = opts.fingerprint != false
|
||||
|
||||
|
||||
if !opts.qs?
|
||||
opts.qs = {}
|
||||
|
||||
|
@ -95,14 +108,13 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
|
||||
if opts.cdn != false
|
||||
path = Url.resolve(staticFilesBase, path)
|
||||
|
||||
|
||||
qs = querystring.stringify(opts.qs)
|
||||
|
||||
if qs? and qs.length > 0
|
||||
path = path + "?" + qs
|
||||
return path
|
||||
|
||||
|
||||
res.locals.buildCssPath = (cssFile)->
|
||||
path = Path.join("/stylesheets/", cssFile)
|
||||
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
|
||||
|
@ -115,7 +127,7 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
|
||||
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.settings = Settings
|
||||
next()
|
||||
|
||||
|
@ -123,7 +135,9 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
res.locals.translate = (key, vars = {}) ->
|
||||
vars.appName = Settings.appName
|
||||
req.i18n.translate(key, vars)
|
||||
res.locals.currentUrl = req.originalUrl
|
||||
# Don't include the query string parameters, otherwise Google
|
||||
# treats ?nocdn=true as the canonical version
|
||||
res.locals.currentUrl = Url.parse(req.originalUrl).pathname
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
|
@ -131,9 +145,10 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2)
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next) ->
|
||||
res.locals.getUserEmail = ->
|
||||
email = req?.session?.user?.email or ""
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
email = user?.email or ""
|
||||
return email
|
||||
next()
|
||||
|
||||
|
@ -143,15 +158,17 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
return formatedPrivileges[privilegeLevel] || "Private"
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.buildReferalUrl = (referal_medium) ->
|
||||
url = Settings.siteUrl
|
||||
if req.session? and req.session.user? and req.session.user.referal_id?
|
||||
url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
if currentUser? and currentUser?.referal_id?
|
||||
url+="?r=#{currentUser.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
|
||||
return url
|
||||
res.locals.getReferalId = ->
|
||||
if req.session? and req.session.user? and req.session.user.referal_id
|
||||
return req.session.user.referal_id
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
if currentUser? and currentUser?.referal_id?
|
||||
return currentUser.referal_id
|
||||
res.locals.getReferalTagLine = ->
|
||||
tagLines = [
|
||||
"Roar!"
|
||||
|
@ -167,7 +184,11 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
return ""
|
||||
|
||||
res.locals.getLoggedInUserId = ->
|
||||
return req.session.user?._id
|
||||
return AuthenticationController.getLoggedInUserId(req)
|
||||
res.locals.isUserLoggedIn = ->
|
||||
return AuthenticationController.isUserLoggedIn(req)
|
||||
res.locals.getSessionUser = ->
|
||||
return AuthenticationController.getSessionUser(req)
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
|
@ -179,25 +200,26 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
return req.query?[field]
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.fingerprint = getFingerprint
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.formatPrice = SubscriptionFormatters.formatPrice
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.externalAuthenticationSystemUsed = ->
|
||||
Settings.ldap?
|
||||
Settings.ldap? or Settings.saml?
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
if req.session.user?
|
||||
currentUser = AuthenticationController.getSessionUser(req)
|
||||
if currentUser?
|
||||
res.locals.user =
|
||||
email: req.session.user.email
|
||||
first_name: req.session.user.first_name
|
||||
last_name: req.session.user.last_name
|
||||
email: currentUser.email
|
||||
first_name: currentUser.first_name
|
||||
last_name: currentUser.last_name
|
||||
if req.session.justRegistered
|
||||
res.locals.justRegistered = true
|
||||
delete req.session.justRegistered
|
||||
|
@ -222,8 +244,10 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
for key, value of Settings.nav
|
||||
res.locals.nav[key] = _.clone(Settings.nav[key])
|
||||
res.locals.templates = Settings.templateLinks
|
||||
if res.locals.nav.header
|
||||
console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead"
|
||||
next()
|
||||
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
SystemMessageManager.getMessages (error, messages = []) ->
|
||||
res.locals.systemMessages = messages
|
||||
|
@ -246,5 +270,3 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
res.locals.moduleIncludes = Modules.moduleIncludes
|
||||
res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable
|
||||
next()
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module.exports =
|
||||
user: (user) ->
|
||||
if !user?
|
||||
return null
|
||||
if !user._id?
|
||||
user = {_id : user}
|
||||
return {
|
||||
|
@ -10,6 +12,8 @@ module.exports =
|
|||
}
|
||||
|
||||
project: (project) ->
|
||||
if !project?
|
||||
return null
|
||||
if !project._id?
|
||||
project = {_id: project}
|
||||
return {
|
||||
|
|
|
@ -18,6 +18,10 @@ module.exports = Modules =
|
|||
applyRouter: (webRouter, apiRouter) ->
|
||||
for module in @modules
|
||||
module.router?.apply(webRouter, apiRouter)
|
||||
|
||||
applyNonCsrfRouter: (webRouter, apiRouter) ->
|
||||
for module in @modules
|
||||
module.nonCsrfRouter?.apply(webRouter, apiRouter)
|
||||
|
||||
viewIncludes: {}
|
||||
loadViewIncludes: (app) ->
|
||||
|
@ -25,14 +29,14 @@ module.exports = Modules =
|
|||
for module in @modules
|
||||
for view, partial of module.viewIncludes or {}
|
||||
@viewIncludes[view] ||= []
|
||||
@viewIncludes[view].push fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade"))
|
||||
@viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
|
||||
|
||||
moduleIncludes: (view, locals) ->
|
||||
partials = Modules.viewIncludes[view] or []
|
||||
compiledPartials = Modules.viewIncludes[view] or []
|
||||
html = ""
|
||||
for partial in partials
|
||||
compiler = jade.compile(partial, doctype: "html")
|
||||
html += compiler(locals)
|
||||
for compiledPartial in compiledPartials
|
||||
d = new Date()
|
||||
html += compiledPartial(locals)
|
||||
return html
|
||||
|
||||
moduleIncludesAvailable: (view) ->
|
||||
|
@ -58,4 +62,4 @@ module.exports = Modules =
|
|||
return callback(error) if error?
|
||||
return callback null, results
|
||||
|
||||
Modules.loadModules()
|
||||
Modules.loadModules()
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
version = {
|
||||
"pdfjs": "1.6.210p2"
|
||||
"moment": "2.9.0"
|
||||
"ace": "1.2.5"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
version: version
|
||||
|
||||
lib: (name) ->
|
||||
if version[name]?
|
||||
return "#{name}-#{version[name]}"
|
||||
else
|
||||
return "#{name}"
|
||||
}
|
|
@ -8,7 +8,9 @@ expressLocals = require('./ExpressLocals')
|
|||
Router = require('../router')
|
||||
metrics.inc("startup")
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
|
||||
|
||||
sessionsRedisClient = UserSessionsRedis.client()
|
||||
|
||||
session = require("express-session")
|
||||
RedisStore = require('connect-redis')(session)
|
||||
|
@ -19,7 +21,11 @@ csrf = require('csurf')
|
|||
csrfProtection = csrf()
|
||||
cookieParser = require('cookie-parser')
|
||||
|
||||
sessionStore = new RedisStore(client:rclient)
|
||||
# Init the session store
|
||||
sessionStore = new RedisStore(client:sessionsRedisClient)
|
||||
|
||||
passport = require('passport')
|
||||
LocalStrategy = require('passport-local').Strategy
|
||||
|
||||
Mongoose = require("./Mongoose")
|
||||
|
||||
|
@ -32,6 +38,7 @@ Modules = require "./Modules"
|
|||
|
||||
ErrorController = require "../Features/Errors/ErrorController"
|
||||
UserSessionsManager = require "../Features/User/UserSessionsManager"
|
||||
AuthenticationController = require "../Features/Authentication/AuthenticationController"
|
||||
|
||||
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
|
||||
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
|
||||
|
@ -83,6 +90,29 @@ webRouter.use session
|
|||
secure: Settings.secureCookie
|
||||
store: sessionStore
|
||||
key: Settings.cookieName
|
||||
rolling: true
|
||||
|
||||
# passport
|
||||
webRouter.use passport.initialize()
|
||||
webRouter.use passport.session()
|
||||
|
||||
passport.use(new LocalStrategy(
|
||||
{
|
||||
passReqToCallback: true,
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
},
|
||||
AuthenticationController.doPassportLogin
|
||||
))
|
||||
passport.serializeUser(AuthenticationController.serializeUser)
|
||||
passport.deserializeUser(AuthenticationController.deserializeUser)
|
||||
|
||||
Modules.hooks.fire 'passportSetup', passport, (err) ->
|
||||
if err?
|
||||
logger.err {err}, "error setting up passport in modules"
|
||||
|
||||
Modules.applyNonCsrfRouter(webRouter, apiRouter)
|
||||
|
||||
webRouter.use csrfProtection
|
||||
webRouter.use translations.expressMiddlewear
|
||||
webRouter.use translations.setLangBasedOnDomainMiddlewear
|
||||
|
@ -90,8 +120,8 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear
|
|||
# Measure expiry from last request, not last login
|
||||
webRouter.use (req, res, next) ->
|
||||
req.session.touch()
|
||||
if req?.session?.user?
|
||||
UserSessionsManager.touch(req.session.user, (err)->)
|
||||
if AuthenticationController.isUserLoggedIn(req)
|
||||
UserSessionsManager.touch(AuthenticationController.getSessionUser(req), (err)->)
|
||||
next()
|
||||
|
||||
webRouter.use ReferalConnect.use
|
||||
|
|
|
@ -32,6 +32,7 @@ ProjectSchema = new Schema
|
|||
archived : { type: Boolean }
|
||||
deletedDocs : [DeletedDocSchema]
|
||||
imageName : { type: String }
|
||||
track_changes : { type: Boolean }
|
||||
|
||||
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
|
||||
if project_or_id._id?
|
||||
|
|
|
@ -26,6 +26,7 @@ UserSchema = new Schema
|
|||
autoComplete: {type : Boolean, default: true}
|
||||
spellCheckLanguage : {type : String, default: "en"}
|
||||
pdfViewer : {type : String, default: "pdfjs"}
|
||||
syntaxValidation : {type : Boolean}
|
||||
}
|
||||
features : {
|
||||
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
|
||||
|
@ -38,7 +39,7 @@ UserSchema = new Schema
|
|||
references: { type:Boolean, default: Settings.defaultFeatures.references }
|
||||
}
|
||||
featureSwitches : {
|
||||
pdfng: { type: Boolean }
|
||||
track_changes: { type: Boolean }
|
||||
}
|
||||
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
|
||||
refered_users: [ type:ObjectId, ref:'User' ]
|
||||
|
|
|
@ -11,7 +11,6 @@ SubscriptionRouter = require './Features/Subscription/SubscriptionRouter'
|
|||
UploadsRouter = require './Features/Uploads/UploadsRouter'
|
||||
metrics = require('./infrastructure/Metrics')
|
||||
ReferalController = require('./Features/Referal/ReferalController')
|
||||
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
|
||||
AuthenticationController = require('./Features/Authentication/AuthenticationController')
|
||||
TagsController = require("./Features/Tags/TagsController")
|
||||
NotificationsController = require("./Features/Notifications/NotificationsController")
|
||||
|
@ -22,10 +21,11 @@ UserPagesController = require('./Features/User/UserPagesController')
|
|||
DocumentController = require('./Features/Documents/DocumentController')
|
||||
CompileManager = require("./Features/Compile/CompileManager")
|
||||
CompileController = require("./Features/Compile/CompileController")
|
||||
ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
|
||||
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
||||
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
||||
FileStoreController = require("./Features/FileStore/FileStoreController")
|
||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||
HistoryController = require("./Features/History/HistoryController")
|
||||
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
|
||||
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
|
||||
ChatController = require("./Features/Chat/ChatController")
|
||||
|
@ -39,6 +39,9 @@ ReferencesController = require('./Features/References/ReferencesController')
|
|||
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
|
||||
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
||||
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
||||
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
|
||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||
CommentsController = require "./Features/Comments/CommentsController"
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -52,7 +55,8 @@ module.exports = class Router
|
|||
webRouter.get '/login', UserPagesController.loginPage
|
||||
AuthenticationController.addEndpointToLoginWhitelist '/login'
|
||||
|
||||
webRouter.post '/login', AuthenticationController.login
|
||||
webRouter.post '/login', AuthenticationController.passportLogin
|
||||
|
||||
webRouter.get '/logout', UserController.logout
|
||||
webRouter.get '/restricted', AuthorizationMiddlewear.restricted
|
||||
|
||||
|
@ -70,12 +74,12 @@ module.exports = class Router
|
|||
RealTimeProxyRouter.apply(webRouter, apiRouter)
|
||||
ContactRouter.apply(webRouter, apiRouter)
|
||||
AnalyticsRouter.apply(webRouter, apiRouter)
|
||||
|
||||
|
||||
Modules.applyRouter(webRouter, apiRouter)
|
||||
|
||||
|
||||
if Settings.enableSubscriptions
|
||||
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
|
||||
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
|
||||
|
||||
webRouter.get '/blog', BlogController.getIndexPage
|
||||
webRouter.get '/blog/*', BlogController.getPage
|
||||
|
@ -87,8 +91,11 @@ module.exports = class Router
|
|||
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
|
||||
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
|
||||
|
||||
webRouter.get '/user/sessions', AuthenticationController.requireLogin(), UserPagesController.sessionsPage
|
||||
webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
|
||||
|
||||
webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
|
||||
webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
|
||||
webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser
|
||||
|
||||
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
|
||||
apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
|
||||
|
@ -166,9 +173,14 @@ module.exports = class Router
|
|||
|
||||
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject
|
||||
|
||||
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
||||
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
|
||||
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||
|
||||
webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges
|
||||
webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers
|
||||
webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange
|
||||
webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges
|
||||
|
||||
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
||||
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||
|
@ -181,7 +193,10 @@ module.exports = class Router
|
|||
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
|
||||
|
||||
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
|
||||
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
|
||||
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
|
||||
|
||||
webRouter.get '/announcements', AuthenticationController.requireLogin(), AnnouncementsController.getUndreadAnnouncements
|
||||
|
||||
|
||||
# Deprecated in favour of /internal/project/:project_id but still used by versioning
|
||||
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
|
||||
|
@ -215,8 +230,14 @@ module.exports = class Router
|
|||
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
||||
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
|
||||
|
||||
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||
|
||||
# Note: Read only users can still comment
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment
|
||||
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
|
||||
|
||||
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
||||
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||
|
@ -251,14 +272,27 @@ module.exports = class Router
|
|||
apiRouter.get '/health_check/redis', HealthCheckController.checkRedis
|
||||
|
||||
apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
|
||||
project_id = req.params.Project_id
|
||||
sendRes = _.once (statusCode, message)->
|
||||
res.writeHead statusCode
|
||||
res.end message
|
||||
CompileManager.compile req.params.Project_id, "test-compile", {}, () ->
|
||||
sendRes 200, "Compiler returned in less than 10 seconds"
|
||||
setTimeout (() ->
|
||||
res.status statusCode
|
||||
res.send message
|
||||
ClsiCookieManager.clearServerId project_id # force every compile to a new server
|
||||
# set a timeout
|
||||
handler = setTimeout (() ->
|
||||
sendRes 500, "Compiler timed out"
|
||||
handler = null
|
||||
), 10000
|
||||
# use a valid user id for testing
|
||||
test_user_id = "123456789012345678901234"
|
||||
# run the compile
|
||||
CompileManager.compile project_id, test_user_id, {}, (error, status) ->
|
||||
clearTimeout handler if handler?
|
||||
if error?
|
||||
sendRes 500, "Compiler returned error #{error.message}"
|
||||
else if status is "success"
|
||||
sendRes 200, "Compiler returned in less than 10 seconds"
|
||||
else
|
||||
sendRes 500, "Compiler returned failure #{status}"
|
||||
|
||||
apiRouter.get "/ip", (req, res, next) ->
|
||||
res.send({
|
||||
|
|
|
@ -3,7 +3,8 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
head
|
||||
title Something went wrong
|
||||
link(rel="icon", href="/favicon.ico")
|
||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||
if buildCssPath
|
||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
|
||||
body
|
||||
.content
|
||||
|
@ -12,7 +13,9 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
.col-md-8.col-md-offset-2.text-center
|
||||
.page-header
|
||||
h2 Oh dear, something went wrong.
|
||||
p: img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
|
||||
if buildImgPath
|
||||
p
|
||||
img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
|
||||
p
|
||||
| Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
|
||||
p
|
||||
|
|
|
@ -11,4 +11,4 @@ block content
|
|||
| Sorry, ShareLaTeX is briefly down for maintenance.
|
||||
| We should be back within minutes, but if not, or you have
|
||||
| an urgent request, please contact us at
|
||||
| support@sharelatex.com
|
||||
| #{settings.adminEmail}
|
||||
|
|
|
@ -51,15 +51,17 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
script(type="text/javascript").
|
||||
window.csrfToken = "#{csrfToken}";
|
||||
|
||||
block scripts
|
||||
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
|
||||
script(type="text/javascript").
|
||||
var noCdnKey = "nocdn=true"
|
||||
var cdnBlocked = typeof jQuery === 'undefined'
|
||||
var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
|
||||
if (cdnBlocked && !noCdnAlreadyInUrl) {
|
||||
if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
|
||||
window.location.search += '&'+noCdnKey;
|
||||
}
|
||||
|
||||
block scripts
|
||||
|
||||
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
|
||||
|
||||
script.
|
||||
|
@ -110,23 +112,27 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
include layout/navbar
|
||||
|
||||
block content
|
||||
|
||||
div(ng-controller="AbTestController")
|
||||
- if(typeof(suppressFooter) == "undefined")
|
||||
include layout/footer
|
||||
|
||||
|
||||
|
||||
- if (typeof(lookingForScribtex) != "undefined" && lookingForScribtex)
|
||||
span(ng-controller="ScribtexPopupController")
|
||||
include scribtex-modal
|
||||
|
||||
|
||||
- if(typeof(suppressFooter) == "undefined")
|
||||
block requirejs
|
||||
script(type='text/javascript').
|
||||
// minimal requirejs configuration (can be extended/overridden)
|
||||
window.requirejs = {
|
||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||
"paths" : {
|
||||
"moment": "libs/moment-2.7.0"
|
||||
"moment": "libs/#{lib('moment')}"
|
||||
},
|
||||
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||
"config":{
|
||||
"moment":{
|
||||
"noGlobal": true
|
||||
}
|
||||
}
|
||||
};
|
||||
script(
|
||||
|
@ -135,7 +141,6 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
src=buildJsPath('libs/require.js')
|
||||
)
|
||||
|
||||
|
||||
include contact-us-modal
|
||||
include sentry
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ nav.navbar.navbar-default
|
|||
.navbar-collapse.collapse(collapse="navCollapsed")
|
||||
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
if (session && session.user && session.user.isAdmin)
|
||||
if (getSessionUser() && getSessionUser().isAdmin)
|
||||
li.dropdown(class="subdued", dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| Admin
|
||||
|
@ -24,8 +24,11 @@ nav.navbar.navbar-default
|
|||
li
|
||||
a(href="/admin/user") Manage Users
|
||||
|
||||
each item in nav.header
|
||||
if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in))
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
|
||||
if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
|
||||
if item.dropdown
|
||||
li.dropdown(class=item.class, dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
|
@ -47,7 +50,35 @@ nav.navbar.navbar-default
|
|||
a(href=item.url, class=item.class) !{translate(item.text)}
|
||||
else
|
||||
| !{translate(item.text)}
|
||||
|
||||
|
||||
|
||||
|
||||
// logged out
|
||||
if !getSessionUser()
|
||||
// register link
|
||||
if !externalAuthenticationSystemUsed()
|
||||
li
|
||||
a(href="/register") #{translate('register')}
|
||||
|
||||
// login link
|
||||
li
|
||||
a(href="/login") #{translate('log_in')}
|
||||
|
||||
// projects link and account menu
|
||||
if getSessionUser()
|
||||
li
|
||||
a(href="/project") #{translate('Projects')}
|
||||
li.dropdown(dropdown)
|
||||
a.dropbodw-toggle(href, dropdown-toggle)
|
||||
| #{translate('Account')}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
li
|
||||
div.subdued #{getUserEmail()}
|
||||
li.divider
|
||||
li
|
||||
a(href="/user/settings") #{translate('Account Settings')}
|
||||
if nav.showSubscriptionLink
|
||||
li
|
||||
a(href="/user/subscription") #{translate('subscription')}
|
||||
li.divider
|
||||
li
|
||||
a(href="/logout") #{translate('log_out')}
|
||||
|
|
|
@ -3,7 +3,6 @@ extends ../layout
|
|||
block vars
|
||||
- var suppressNavbar = true
|
||||
- var suppressFooter = true
|
||||
- var suppressDefaultJs = true
|
||||
- var suppressSystemMessages = true
|
||||
|
||||
block content
|
||||
|
@ -16,7 +15,8 @@ block content
|
|||
p.text-center.text-danger(ng-if="state.error").ng-cloak
|
||||
span(ng-bind-html="state.error")
|
||||
|
||||
|
||||
include ./editor/feature-onboarding
|
||||
|
||||
.global-alerts(ng-cloak)
|
||||
.alert.alert-danger.small(ng-if="connection.forced_disconnect")
|
||||
strong #{translate("disconnected")}
|
||||
|
@ -38,7 +38,7 @@ block content
|
|||
|
||||
include ./editor/left-menu
|
||||
|
||||
#chat-wrapper(
|
||||
#chat-wrapper.full-size(
|
||||
layout="chat",
|
||||
spacing-open="12",
|
||||
spacing-closed="0",
|
||||
|
@ -66,7 +66,7 @@ block content
|
|||
.ui-layout-center
|
||||
include ./editor/editor
|
||||
include ./editor/binary-file
|
||||
include ./editor/track-changes
|
||||
include ./editor/history
|
||||
include ./editor/publish-template
|
||||
|
||||
.ui-layout-east
|
||||
|
@ -86,7 +86,17 @@ block content
|
|||
.modal-footer
|
||||
button.btn.btn-info(ng-click="done()") #{translate("ok")}
|
||||
|
||||
script(src='/socket.io/socket.io.js')
|
||||
script(type="text/ng-template", id="lockEditorModalTemplate")
|
||||
.modal-header
|
||||
h3 {{ title }}
|
||||
.modal-body(ng-bind-html="message")
|
||||
|
||||
block requirejs
|
||||
script(type="text/javascript" src='/socket.io/socket.io.js')
|
||||
|
||||
//- don't use cdn for workers
|
||||
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false})
|
||||
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
|
||||
|
||||
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
|
||||
//- and doesn't prematurely end the script tag.
|
||||
|
@ -97,50 +107,46 @@ block content
|
|||
window.csrfToken = "!{csrfToken}";
|
||||
window.anonymous = #{anonymous};
|
||||
window.maxDocLength = #{maxDocLength};
|
||||
window.trackChangesEnabled = #{trackChangesEnabled};
|
||||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||
window.requirejs = {
|
||||
"paths" : {
|
||||
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}",
|
||||
"moment": "libs/moment-2.7.0",
|
||||
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
|
||||
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
|
||||
"moment": "libs/#{lib('moment')}",
|
||||
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
|
||||
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
|
||||
"ace": "#{lib('ace')}"
|
||||
},
|
||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||
"waitSeconds": 0,
|
||||
"shim": {
|
||||
"libs/pdf": {
|
||||
deps: ["libs/pdfjs-1.3.91/compatibility"]
|
||||
"pdfjs-dist/build/pdf": {
|
||||
"deps": ["libs/#{lib('pdfjs')}/compatibility"]
|
||||
},
|
||||
"ace/ext-searchbox": {
|
||||
deps: ["ace/ace"]
|
||||
"deps": ["ace/ace"]
|
||||
},
|
||||
"ace/ext-modelist": {
|
||||
"deps": ["ace/ace"]
|
||||
},
|
||||
"ace/ext-language_tools": {
|
||||
deps: ["ace/ace"]
|
||||
"deps": ["ace/ace"]
|
||||
}
|
||||
},
|
||||
config:{
|
||||
moment:{
|
||||
noGlobal: true
|
||||
"config":{
|
||||
"moment":{
|
||||
"noGlobal": true
|
||||
}
|
||||
}
|
||||
};
|
||||
window.aceFingerprint = "#{fingerprint(jsPath + 'ace/ace.js')}"
|
||||
|
||||
- locals.suppressDefaultJs = true
|
||||
|
||||
- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
|
||||
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
|
||||
- var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn
|
||||
|
||||
|
||||
script(type='text/javascript').
|
||||
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
|
||||
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
|
||||
window.aceWorkerPath = "#{aceWorkerPath}";
|
||||
|
||||
script(
|
||||
data-main=buildJsPath("ide.js", {fingerprint:false}),
|
||||
baseurl=fullJsPath,
|
||||
data-ace-base=buildJsPath('ace', {fingerprint:false}),
|
||||
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
|
||||
src=buildJsPath('libs/require.js')
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -7,8 +7,21 @@ div.full-size(
|
|||
resize-proportionally="true"
|
||||
initial-size-east="'50%'"
|
||||
minimum-restore-size-east="300"
|
||||
allow-overflow-on="'center'"
|
||||
)
|
||||
.ui-layout-center
|
||||
.ui-layout-center(
|
||||
ng-controller="ReviewPanelController",
|
||||
ng-class="{\
|
||||
'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
|
||||
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
|
||||
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
||||
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
|
||||
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
|
||||
'rp-size-expanded': ui.reviewPanelOpen,\
|
||||
'rp-layout-left': reviewPanel.layoutToLeft,\
|
||||
'rp-loading-threads': reviewPanel.loadingThreads\
|
||||
}"
|
||||
)
|
||||
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
|
||||
span(ng-show="editor.open_doc_id")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
|
@ -24,7 +37,7 @@ div.full-size(
|
|||
keybindings="settings.mode",
|
||||
font-size="settings.fontSize",
|
||||
auto-complete="settings.autoComplete",
|
||||
spell-check="true",
|
||||
spell-check="!anonymous",
|
||||
spell-check-language="project.spellCheckLanguage",
|
||||
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
|
||||
show-print-margin="false",
|
||||
|
@ -32,12 +45,31 @@ div.full-size(
|
|||
last-updated="editor.last_updated",
|
||||
cursor-position="editor.cursorPosition",
|
||||
goto-line="editor.gotoLine",
|
||||
resize-on="layout:main:resize,layout:pdf:resize",
|
||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle",
|
||||
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
|
||||
read-only="!permissions.write",
|
||||
on-ctrl-enter="recompileViaKey"
|
||||
file-name="editor.open_doc_name",
|
||||
on-ctrl-enter="recompileViaKey",
|
||||
syntax-validation="settings.syntaxValidation",
|
||||
review-panel="reviewPanel",
|
||||
events-bridge="reviewPanelEventsBridge"
|
||||
track-changes-enabled="project.features.trackChanges",
|
||||
track-changes= "editor.trackChanges",
|
||||
doc-id="editor.open_doc_id"
|
||||
renderer-data="reviewPanel.rendererData"
|
||||
)
|
||||
|
||||
a.rp-track-changes-indicator(
|
||||
href
|
||||
ng-if="editor.wantTrackChanges"
|
||||
ng-click="toggleReviewPanel();"
|
||||
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
|
||||
) Track changes is
|
||||
strong on
|
||||
|
||||
|
||||
include ./review-panel
|
||||
|
||||
.ui-layout-east
|
||||
div(ng-if="ui.pdfLayout == 'sideBySide'")
|
||||
include ./pdf
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
.feat-onboard(
|
||||
ng-controller="FeatureOnboardingController"
|
||||
ng-class="('feat-onboard-step' + innerStep)"
|
||||
ng-if="!state.loading && ui.showCodeCheckerOnboarding"
|
||||
ng-cloak
|
||||
)
|
||||
.feat-onboard-wrapper
|
||||
h1.feat-onboard-title
|
||||
| Introducing
|
||||
span.feat-onboard-title-name Code check
|
||||
div(ng-if="innerStep === 1;")
|
||||
p.feat-onboard-description
|
||||
span.feat-onboard-description-name Code check
|
||||
| will highlight potential problems in your LaTeX code, allowing you to handle errors earlier and become more productive.
|
||||
.row
|
||||
video.feat-onboard-video(autoplay, loop)
|
||||
source(src="/img/teasers/code-checker/code-checker.mp4", type="video/mp4")
|
||||
img(src="/img/teasers/code-checker/code-checker.gif")
|
||||
.row.feat-onboard-adv-wrapper
|
||||
.col-xs-4
|
||||
h2.feat-onboard-adv-title
|
||||
| Missing
|
||||
span.feat-onboard-adv-title-highlight brackets
|
||||
p Forgot to place a closing bracket? We'll warn you.
|
||||
.col-xs-4
|
||||
h2.feat-onboard-adv-title
|
||||
| Unclosed
|
||||
span.feat-onboard-adv-title-highlight environments
|
||||
p
|
||||
| Know when you are missing an
|
||||
code \end{...}
|
||||
| command.
|
||||
.col-xs-4
|
||||
h2.feat-onboard-adv-title
|
||||
| Incorrect
|
||||
span.feat-onboard-adv-title-highlight nesting
|
||||
p
|
||||
| Order matters. Get notified when you use an
|
||||
code \end{...}
|
||||
| too soon.
|
||||
.feat-onboard-btn-wrapper
|
||||
button.btn.btn-primary(ng-click="turnCodeCheckOn();") Yes, turn Code check on
|
||||
.feat-onboard-btn-wrapper
|
||||
button.btn.btn-default(ng-click="turnCodeCheckOff();") No, disable it for now
|
||||
div(ng-if="innerStep === 2;")
|
||||
p.feat-onboard-description
|
||||
| Remember: you can always turn
|
||||
span.feat-onboard-description-name Code check
|
||||
em on
|
||||
| or
|
||||
em off
|
||||
|, in the settings menu.
|
||||
.feat-onboard-btn-wrapper
|
||||
button.btn.btn-primary(ng-click="dismiss();") OK, got it
|
|
@ -70,13 +70,13 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
|||
ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
|
||||
)
|
||||
|
||||
li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'")
|
||||
li(ng-show="deletedDocs.length > 0 && ui.view == 'history'")
|
||||
h3 #{translate("deleted_files")}
|
||||
li(
|
||||
ng-class="{ 'selected': entity.selected }",
|
||||
ng-repeat="entity in deletedDocs | orderBy:'name'",
|
||||
ng-controller="FileTreeEntityController",
|
||||
ng-show="ui.view == 'track-changes'"
|
||||
ng-show="ui.view == 'history'"
|
||||
)
|
||||
.entity
|
||||
.entity-name(
|
||||
|
@ -315,8 +315,12 @@ script(type='text/ng-template', id='newDocModalTemplate')
|
|||
required,
|
||||
ng-model="inputs.name",
|
||||
on-enter="create()",
|
||||
select-name-on="open"
|
||||
select-name-on="open",
|
||||
ng-pattern="validFileRegex",
|
||||
name="name"
|
||||
)
|
||||
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.pattern")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
|
@ -341,8 +345,12 @@ script(type='text/ng-template', id='newFolderModalTemplate')
|
|||
required,
|
||||
ng-model="inputs.name",
|
||||
on-enter="create()",
|
||||
select-name-on="open"
|
||||
select-name-on="open",
|
||||
ng-pattern="validFileRegex",
|
||||
name="name"
|
||||
)
|
||||
.text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.pattern")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
|
@ -414,3 +422,13 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
|
|||
)
|
||||
span(ng-hide="state.inflight") #{translate("delete")}
|
||||
span(ng-show="state.inflight") #{translate("deleting")}...
|
||||
|
||||
script(type='text/ng-template', id='invalidFileNameModalTemplate')
|
||||
.modal-header
|
||||
h3 #{translate('invalid_file_name')}
|
||||
.modal-body
|
||||
p #{translate('files_cannot_include_invalid_characters')}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-click="$close()"
|
||||
) #{translate('ok')}
|
|
@ -1,19 +1,19 @@
|
|||
header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="ui.leftMenuShown = true"
|
||||
tooltip='#{translate("menu")}',
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
i.fa.fa-fw.fa-bars
|
||||
a(
|
||||
href="/project"
|
||||
tooltip="#{translate('back_to_projects')}",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
i.fa.fa-fw.fa-level-up
|
||||
header.toolbar.toolbar-header.toolbar-with-labels(
|
||||
ng-cloak,
|
||||
ng-hide="state.loading"
|
||||
)
|
||||
.toolbar-left
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="ui.leftMenuShown = true;",
|
||||
)
|
||||
i.fa.fa-fw.fa-bars
|
||||
p.toolbar-label #{translate("menu")}
|
||||
a(
|
||||
href="/project"
|
||||
)
|
||||
i.fa.fa-fw.fa-level-up
|
||||
|
||||
span(ng-controller="PdfViewToggleController")
|
||||
a(
|
||||
href,
|
||||
|
@ -85,39 +85,40 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
|
|||
) {{ user.name.slice(0,1) }}
|
||||
| {{ user.name }}
|
||||
|
||||
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="project.features.trackChanges",
|
||||
ng-class="{ active: ui.reviewPanelOpen }"
|
||||
ng-click="toggleReviewPanel()"
|
||||
)
|
||||
i.review-icon
|
||||
p.toolbar-label Review
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="permissions.admin",
|
||||
tooltip="#{translate('share')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-mouseenter="trackHover('share')"
|
||||
ng-click="openShareProjectModal()",
|
||||
ng-controller="ShareController"
|
||||
ng-click="openShareProjectModal();",
|
||||
ng-controller="ShareController",
|
||||
)
|
||||
i.fa.fa-fw.fa-group
|
||||
p.toolbar-label #{translate("share")}
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-mouseenter="trackHover('track-changes')"
|
||||
ng-click="toggleTrackChanges()",
|
||||
ng-class="{ active: (ui.view == 'track-changes') }"
|
||||
tooltip="#{translate('recent_changes')}",
|
||||
tooltip-placement="bottom"
|
||||
ng-click="toggleHistory();",
|
||||
ng-class="{ active: (ui.view == 'history') }",
|
||||
)
|
||||
i.fa.fa-fw.fa-history
|
||||
p.toolbar-label #{translate("history")}
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
tooltip="#{translate('chat')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-class="{ active: ui.chatOpen }",
|
||||
ng-mouseenter="trackHover('chat')"
|
||||
ng-click="toggleChat()",
|
||||
ng-click="toggleChat();",
|
||||
ng-controller="ChatButtonController",
|
||||
ng-show="!anonymous"
|
||||
ng-show="!anonymous",
|
||||
)
|
||||
i.fa.fa-fw.fa-comment(
|
||||
ng-class="{ 'bounce': unreadMessages > 0 }"
|
||||
)
|
||||
span.label.label-info(
|
||||
ng-show="unreadMessages > 0"
|
||||
) {{ unreadMessages }}
|
||||
) {{ unreadMessages }}
|
||||
p.toolbar-label #{translate("chat")}
|
|
@ -1,61 +1,100 @@
|
|||
div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||
span(ng-controller="TrackChangesPremiumPopup")
|
||||
.upgrade-prompt(ng-show="!project.features.versioning")
|
||||
.message(ng-show="project.owner._id == user.id")
|
||||
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
|
||||
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
div#history(ng-show="ui.view == 'history'")
|
||||
span(ng-controller="HistoryPremiumPopup")
|
||||
.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
|
||||
|
||||
div(ng-if="project.owner._id == user.id")
|
||||
div(sixpack-switch="teaser-history")
|
||||
.message(sixpack-default)
|
||||
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
|
||||
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
p.text-center(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('history')"
|
||||
sixpack-convert="teaser-history"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
p.text-center(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('track-changes')"
|
||||
) #{translate("start_free_trial")}
|
||||
.message.message-wider(sixpack-when="focused")
|
||||
header.message-header
|
||||
h3 History
|
||||
|
||||
.message-body
|
||||
h4.teaser-title See who changed what. Go back to previous versions.
|
||||
img.teaser-img(
|
||||
src="/img/teasers/history/teaser-history.png"
|
||||
alt="History"
|
||||
)
|
||||
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| Catch up with your collaborators changes
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| See changes over any time period
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| Revert your documents to previous versions
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| Restore deleted files
|
||||
p.text-center(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('history')"
|
||||
sixpack-convert="teaser-history"
|
||||
) Try it for free
|
||||
|
||||
.message(ng-show="project.owner._id != user.id")
|
||||
p #{translate("ask_proj_owner_to_upgrade_for_history")}
|
||||
p
|
||||
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
|
||||
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
|
||||
|
||||
aside.change-list(
|
||||
ng-controller="TrackChangesListController"
|
||||
ng-controller="HistoryListController"
|
||||
infinite-scroll="loadMore()"
|
||||
infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'track-changes'"
|
||||
infinite-scroll-disabled="history.loading || history.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'history'"
|
||||
)
|
||||
.infinite-scroll-inner
|
||||
ul.list-unstyled(
|
||||
ng-class="{\
|
||||
'hover-state': trackChanges.hoveringOverListSelectors\
|
||||
'hover-state': history.hoveringOverListSelectors\
|
||||
}"
|
||||
)
|
||||
li.change(
|
||||
ng-repeat="update in trackChanges.updates"
|
||||
ng-repeat="update in history.updates"
|
||||
ng-class="{\
|
||||
'first-in-day': update.meta.first_in_day,\
|
||||
'selected': update.inSelection,\
|
||||
|
@ -65,7 +104,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
'hover-selected-to': update.hoverSelectedTo,\
|
||||
'hover-selected-from': update.hoverSelectedFrom,\
|
||||
}"
|
||||
ng-controller="TrackChangesListItemController"
|
||||
ng-controller="HistoryListItemController"
|
||||
)
|
||||
|
||||
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
|
||||
|
@ -108,55 +147,55 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
.color-square(style="background-color: hsl(100, 100%, 50%)")
|
||||
span #{translate("anonymous")}
|
||||
|
||||
.loading(ng-show="trackChanges.loading")
|
||||
.loading(ng-show="history.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
|
||||
.diff-panel.full-size(ng-controller="TrackChangesDiffController")
|
||||
.diff-panel.full-size(ng-controller="HistoryDiffController")
|
||||
.diff(
|
||||
ng-show="!!trackChanges.diff && !trackChanges.diff.loading && !trackChanges.diff.deleted && !trackChanges.diff.error"
|
||||
ng-show="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error"
|
||||
)
|
||||
.toolbar.toolbar-alt
|
||||
span.name
|
||||
| <strong>{{trackChanges.diff.highlights.length}} </strong>
|
||||
| <strong>{{history.diff.highlights.length}} </strong>
|
||||
ng-pluralize(
|
||||
count="trackChanges.diff.highlights.length",
|
||||
count="history.diff.highlights.length",
|
||||
when="{\
|
||||
'one': 'change',\
|
||||
'other': 'changes'\
|
||||
}"
|
||||
)
|
||||
| in <strong>{{trackChanges.diff.doc.name}}</strong>
|
||||
| in <strong>{{history.diff.doc.name}}</strong>
|
||||
.toolbar-right
|
||||
a.btn.btn-danger.btn-sm(
|
||||
href,
|
||||
ng-click="openRestoreDiffModal()"
|
||||
) #{translate("restore_to_before_these_changes")}
|
||||
.diff-editor.hide-ace-cursor(
|
||||
ace-editor="track-changes",
|
||||
ace-editor="history",
|
||||
theme="settings.theme",
|
||||
font-size="settings.fontSize",
|
||||
text="trackChanges.diff.text",
|
||||
highlights="trackChanges.diff.highlights",
|
||||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="trackChanges.diff.deleted && !trackChanges.diff.restoreDeletedSuccess"
|
||||
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p
|
||||
a.btn.btn-primary.btn-lg(
|
||||
href,
|
||||
ng-click="restoreDeletedDoc()",
|
||||
ng-disabled="trackChanges.diff.restoreInProgress"
|
||||
ng-disabled="history.diff.restoreInProgress"
|
||||
) #{translate("restore")}
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess"
|
||||
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_restored", {filename:"{{ trackChanges.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_restored_back_to_editor")}
|
||||
p
|
||||
a.btn.btn-default(
|
||||
|
@ -164,13 +203,13 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
ng-click="backToEditorAfterRestore()",
|
||||
) #{translate("file_restored_back_to_editor_btn")}
|
||||
|
||||
.loading-panel(ng-show="trackChanges.diff.loading")
|
||||
.loading-panel(ng-show="history.diff.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="trackChanges.diff.error")
|
||||
.error-panel(ng-show="history.diff.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
|
||||
script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate")
|
||||
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
|
@ -66,6 +66,31 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
|
|||
.hotkey
|
||||
span.combination {{ctrl}} + I
|
||||
span.description Italic Text
|
||||
|
||||
h3 #{translate("autocomplete")}
|
||||
.row
|
||||
.col-xs-6
|
||||
.hotkey
|
||||
span.combination Ctrl + Space
|
||||
span.description Autocomplete Menu
|
||||
|
||||
.col-xs-6
|
||||
.hotkey
|
||||
span.combination Tab / Up / Down
|
||||
span.description Select Candidate
|
||||
|
||||
.hotkey
|
||||
span.combination Enter
|
||||
span.description Insert Candidate
|
||||
|
||||
h3 !{translate("autocomplete_references")}
|
||||
.row
|
||||
.col-xs-6
|
||||
.hotkey
|
||||
span.combination Ctrl + Space
|
||||
span.description Search References
|
||||
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-click="cancel()"
|
||||
|
|
|
@ -105,6 +105,14 @@ aside#left-menu.full-size(
|
|||
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
|
||||
)
|
||||
|
||||
.form-controls.code-check-setting
|
||||
label(for="syntaxValidation") #{translate("syntax_validation")}
|
||||
select(
|
||||
name="syntaxValidation"
|
||||
ng-model="settings.syntaxValidation"
|
||||
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
|
||||
)
|
||||
|
||||
.form-controls
|
||||
label(for="theme") #{translate("theme")}
|
||||
select(
|
||||
|
@ -197,6 +205,10 @@ script(type='text/ng-template', id='wordCountModalTemplate')
|
|||
)
|
||||
div(ng-if="!status.loading")
|
||||
.container-fluid
|
||||
.row(ng-show='data.messages.length > 0')
|
||||
.col-xs-12
|
||||
.alert.alert-danger
|
||||
p(style="white-space: pre-wrap") {{data.messages}}
|
||||
.row
|
||||
.col-xs-4
|
||||
.pull-right #{translate("total_words")} :
|
||||
|
|
|
@ -229,7 +229,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
|
||||
p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
|
||||
|
||||
p
|
||||
div
|
||||
.files-dropdown-container
|
||||
a.btn.btn-default.btn-sm(
|
||||
href,
|
||||
|
@ -344,7 +344,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")}
|
||||
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
|
||||
p Plus:
|
||||
p
|
||||
div
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
|
|
332
services/web/app/views/project/editor/review-panel.jade
Normal file
332
services/web/app/views/project/editor/review-panel.jade
Normal file
|
@ -0,0 +1,332 @@
|
|||
#review-panel
|
||||
.review-panel-toolbar
|
||||
resolved-comments-dropdown(
|
||||
entries="reviewPanel.resolvedComments"
|
||||
threads="reviewPanel.commentThreads"
|
||||
resolved-ids="reviewPanel.resolvedThreadIds"
|
||||
docs="docs"
|
||||
on-open="refreshResolvedCommentsDropdown();"
|
||||
on-unresolve="unresolveComment(threadId);"
|
||||
on-delete="deleteComment(entryId, threadId);"
|
||||
is-loading="reviewPanel.dropdown.loading"
|
||||
permissions="permissions"
|
||||
)
|
||||
span.review-panel-toolbar-label(ng-if="permissions.write")
|
||||
span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is
|
||||
strong off
|
||||
span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is
|
||||
strong on
|
||||
review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges", on-toggle="toggleTrackChanges")
|
||||
span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write")
|
||||
span(ng-if="editor.wantTrackChanges === false") Track Changes is
|
||||
strong off
|
||||
span(ng-if="editor.wantTrackChanges === true") Track Changes is
|
||||
strong on
|
||||
span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges")
|
||||
i.fa.fa-spin.fa-spinner
|
||||
|
||||
.rp-entry-list(
|
||||
review-panel-sorted
|
||||
ng-if="reviewPanel.subView === SubViews.CUR_FILE"
|
||||
)
|
||||
.rp-entry-list-inner
|
||||
.rp-entry-wrapper(
|
||||
ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
|
||||
ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
|
||||
)
|
||||
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
||||
change-entry(
|
||||
entry="entry"
|
||||
user="users[entry.metadata.user_id]"
|
||||
on-reject="rejectChange(entry_id);"
|
||||
on-accept="acceptChange(entry_id);"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
permissions="permissions"
|
||||
)
|
||||
|
||||
div(ng-if="entry.type === 'comment'")
|
||||
comment-entry(
|
||||
entry="entry"
|
||||
threads="reviewPanel.commentThreads"
|
||||
on-resolve="resolveComment(entry, entry_id)"
|
||||
on-reply="submitReply(entry, entry_id);"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
permissions="permissions"
|
||||
ng-if="!reviewPanel.loadingThreads"
|
||||
)
|
||||
|
||||
div(ng-if="entry.type === 'add-comment' && permissions.comment")
|
||||
add-comment-entry(
|
||||
on-start-new="startNewComment();"
|
||||
on-submit="submitNewComment(content);"
|
||||
on-cancel="cancelNewComment();"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
)
|
||||
|
||||
.rp-entry-list(
|
||||
ng-if="reviewPanel.subView === SubViews.OVERVIEW"
|
||||
)
|
||||
.rp-loading(ng-if="reviewPanel.overview.loading")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
.rp-overview-file(
|
||||
ng-repeat="doc in docs"
|
||||
ng-if="!reviewPanel.overview.loading"
|
||||
)
|
||||
.rp-overview-file-header(
|
||||
ng-if="reviewPanel.entries[doc.doc.id] | notEmpty"
|
||||
)
|
||||
| {{ doc.path }}
|
||||
.rp-entry-wrapper(
|
||||
ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries"
|
||||
ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
|
||||
)
|
||||
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
||||
change-entry(
|
||||
entry="entry"
|
||||
user="users[entry.metadata.user_id]"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
ng-click="gotoEntry(doc.doc.id, entry)"
|
||||
permissions="permissions"
|
||||
)
|
||||
|
||||
div(ng-if="entry.type === 'comment'")
|
||||
comment-entry(
|
||||
entry="entry"
|
||||
threads="reviewPanel.commentThreads"
|
||||
on-reply="submitReply(entry, entry_id);"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
ng-click="gotoEntry(doc.doc.id, entry)"
|
||||
permissions="permissions"
|
||||
)
|
||||
|
||||
.rp-nav
|
||||
a.rp-nav-item(
|
||||
href
|
||||
ng-click="setSubView(SubViews.CUR_FILE);"
|
||||
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }"
|
||||
)
|
||||
i.fa.fa-file-text-o
|
||||
span.rp-nav-label Current file
|
||||
a.rp-nav-item(
|
||||
href
|
||||
ng-click="setSubView(SubViews.OVERVIEW);"
|
||||
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }"
|
||||
)
|
||||
i.fa.fa-list
|
||||
span.rp-nav-label Overview
|
||||
|
||||
|
||||
script(type='text/ng-template', id='changeEntryTemplate')
|
||||
div
|
||||
.rp-entry-callout(
|
||||
ng-class="'rp-entry-callout-' + entry.type"
|
||||
)
|
||||
.rp-entry-indicator(
|
||||
ng-switch="entry.type"
|
||||
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
|
||||
ng-click="onIndicatorClick();"
|
||||
)
|
||||
i.fa.fa-pencil(ng-switch-when="insert")
|
||||
i.rp-icon-delete(ng-switch-when="delete")
|
||||
.rp-entry(
|
||||
ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]"
|
||||
)
|
||||
.rp-entry-body
|
||||
.rp-entry-action-icon(ng-switch="entry.type")
|
||||
i.fa.fa-pencil(ng-switch-when="insert")
|
||||
i.rp-icon-delete(ng-switch-when="delete")
|
||||
.rp-entry-details
|
||||
.rp-entry-description(ng-switch="entry.type")
|
||||
span(ng-switch-when="insert") Added
|
||||
ins.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
|
||||
a.rp-collapse-toggle(
|
||||
href
|
||||
ng-if="needsCollapsing"
|
||||
ng-click="toggleCollapse();"
|
||||
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
|
||||
span(ng-switch-when="delete") Deleted
|
||||
del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
|
||||
a.rp-collapse-toggle(
|
||||
href
|
||||
ng-if="needsCollapsing"
|
||||
ng-click="toggleCollapse();"
|
||||
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
|
||||
.rp-entry-metadata
|
||||
| {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •
|
||||
span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
|
||||
.rp-entry-actions(ng-if="permissions.write")
|
||||
a.rp-entry-button(href, ng-click="onReject();")
|
||||
i.fa.fa-times
|
||||
| Reject
|
||||
a.rp-entry-button(href, ng-click="onAccept();")
|
||||
i.fa.fa-check
|
||||
| Accept
|
||||
|
||||
script(type='text/ng-template', id='commentEntryTemplate')
|
||||
.rp-comment-wrapper(
|
||||
ng-class="{ 'rp-comment-wrapper-resolving': state.animating }"
|
||||
)
|
||||
.rp-entry-callout.rp-entry-callout-comment
|
||||
.rp-entry-indicator(
|
||||
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
|
||||
ng-click="onIndicatorClick();"
|
||||
)
|
||||
i.fa.fa-comment
|
||||
.rp-entry.rp-entry-comment(
|
||||
ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }"
|
||||
)
|
||||
div
|
||||
.rp-comment(
|
||||
ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
|
||||
)
|
||||
p.rp-comment-content
|
||||
span.rp-entry-user(
|
||||
style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
|
||||
) {{ comment.user.name }}:
|
||||
| {{ comment.content }}
|
||||
.rp-entry-metadata
|
||||
| {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
|
||||
.rp-loading(ng-if="!threads[entry.thread_id] || threads[entry.thread_id].submitting")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
.rp-comment-reply(ng-if="permissions.comment")
|
||||
textarea.rp-comment-input(
|
||||
ng-model="entry.replyContent"
|
||||
ng-keypress="handleCommentReplyKeyPress($event);"
|
||||
stop-propagation="click"
|
||||
placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}"
|
||||
)
|
||||
.rp-entry-actions
|
||||
button.rp-entry-button(
|
||||
ng-click="animateAndCallOnResolve();"
|
||||
ng-if="permissions.comment && permissions.write"
|
||||
)
|
||||
i.fa.fa-inbox
|
||||
| Resolve
|
||||
button.rp-entry-button(
|
||||
ng-click="onReply();"
|
||||
ng-if="permissions.comment"
|
||||
ng-disabled="!entry.replyContent.length"
|
||||
)
|
||||
i.fa.fa-reply
|
||||
| Reply
|
||||
|
||||
script(type='text/ng-template', id='resolvedCommentEntryTemplate')
|
||||
.rp-resolved-comment
|
||||
div
|
||||
.rp-resolved-comment-context
|
||||
| Quoted text on
|
||||
span.rp-resolved-comment-context-file {{ thread.docName }}
|
||||
p.rp-resolved-comment-context-quote
|
||||
span {{ thread.content | limitTo:(isCollapsed ? contentLimit : thread.content.length)}}
|
||||
a.rp-collapse-toggle(
|
||||
href
|
||||
ng-if="needsCollapsing"
|
||||
ng-click="toggleCollapse();"
|
||||
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
|
||||
.rp-comment(
|
||||
ng-repeat="comment in thread.messages track by comment.id"
|
||||
)
|
||||
p.rp-comment-content
|
||||
span.rp-entry-user(
|
||||
style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
|
||||
ng-if="$first || comment.user.id !== thread.messages[$index - 1].user.id"
|
||||
) {{ comment.user.name }}:
|
||||
| {{ comment.content }}
|
||||
.rp-entry-metadata
|
||||
| {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
|
||||
.rp-comment.rp-comment-resolver
|
||||
p.rp-comment-resolver-content
|
||||
span.rp-entry-user(
|
||||
style="color: hsl({{ thread.resolved_by_user.hue }}, 70%, 40%);"
|
||||
) {{ thread.resolved_by_user.name }}:
|
||||
| Marked as resolved.
|
||||
.rp-entry-metadata
|
||||
| {{ thread.resolved_at | date : 'MMM d, y h:mm a' }}
|
||||
|
||||
.rp-entry-actions(ng-if="permissions.comment && permissions.write")
|
||||
a.rp-entry-button(
|
||||
href
|
||||
ng-click="onUnresolve({ 'threadId': thread.threadId });"
|
||||
)
|
||||
| Re-open
|
||||
//- a.rp-entry-button(
|
||||
//- href
|
||||
//- ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });"
|
||||
//- )
|
||||
//- | Delete
|
||||
|
||||
|
||||
script(type='text/ng-template', id='addCommentEntryTemplate')
|
||||
div
|
||||
.rp-entry-callout.rp-entry-callout-add-comment
|
||||
.rp-entry-indicator(
|
||||
ng-if="!commentState.adding"
|
||||
ng-click="startNewComment(); onIndicatorClick();"
|
||||
tooltip="Add a comment"
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
i.fa.fa-commenting
|
||||
.rp-entry.rp-entry-add-comment(
|
||||
ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]"
|
||||
)
|
||||
a.rp-add-comment-btn(
|
||||
href
|
||||
ng-if="!state.isAdding"
|
||||
ng-click="startNewComment();"
|
||||
)
|
||||
i.fa.fa-comment
|
||||
| Add comment
|
||||
div(ng-if="state.isAdding")
|
||||
.rp-new-comment
|
||||
textarea.rp-comment-input(
|
||||
ng-model="state.content"
|
||||
ng-keypress="handleCommentKeyPress($event);"
|
||||
placeholder="Add your comment here"
|
||||
focus-on="comment:new:open"
|
||||
)
|
||||
.rp-entry-actions
|
||||
button.rp-entry-button(
|
||||
ng-click="cancelNewComment();"
|
||||
)
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
button.rp-entry-button(
|
||||
ng-click="submitNewComment()"
|
||||
ng-disabled="!state.content.length"
|
||||
)
|
||||
i.fa.fa-comment
|
||||
| Comment
|
||||
|
||||
script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
|
||||
.resolved-comments
|
||||
.resolved-comments-backdrop(
|
||||
ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }"
|
||||
ng-click="state.isOpen = false"
|
||||
)
|
||||
a.resolved-comments-toggle(
|
||||
href
|
||||
ng-click="toggleOpenState();"
|
||||
tooltip="Resolved Comments"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
i.fa.fa-inbox
|
||||
.resolved-comments-dropdown(
|
||||
ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }"
|
||||
)
|
||||
.rp-loading(ng-if="isLoading")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
.resolved-comments-scroller(
|
||||
ng-if="!isLoading"
|
||||
)
|
||||
resolved-comment-entry(
|
||||
ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true"
|
||||
thread="thread"
|
||||
on-unresolve="handleUnresolve(threadId);"
|
||||
on-delete="handleDelete(entryId, threadId);"
|
||||
permissions="permissions"
|
||||
)
|
||||
.rp-loading(ng-if="!resolvedComments.length")
|
||||
| No resolved threads.
|
||||
|
|
@ -137,10 +137,17 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
p.small(ng-show="startedFreeTrial")
|
||||
| #{translate("refresh_page_after_starting_free_trial")}.
|
||||
|
||||
.modal-footer
|
||||
.modal-footer.modal-footer-share
|
||||
.modal-footer-left
|
||||
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
|
||||
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
|
||||
span.text-danger.error(ng-show="state.error")
|
||||
span(ng-switch="state.errorReason")
|
||||
span(ng-switch-when="cannot_invite_non_user")
|
||||
| #{translate("cannot_invite_non_user")}
|
||||
span(ng-switch-when="cannot_invite_self")
|
||||
| #{translate("cannot_invite_self")}
|
||||
span(ng-switch-default)
|
||||
| #{translate("generic_something_went_wrong")}
|
||||
button.btn.btn-default(
|
||||
ng-click="done()"
|
||||
) #{translate("close")}
|
||||
|
|
|
@ -20,7 +20,7 @@ block content
|
|||
form.form(
|
||||
name="acceptForm",
|
||||
method="POST",
|
||||
action="/project/#{invite.projectId}/invite/#{invite._id}/accept"
|
||||
action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
|
||||
)
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
input(name='token', type='hidden', value="#{invite.token}")
|
||||
|
|
|
@ -19,11 +19,49 @@ block content
|
|||
}
|
||||
};
|
||||
|
||||
.content.content-alt(ng-controller="ProjectPageController")
|
||||
.content.content-alt.project-list-page(ng-controller="ProjectPageController")
|
||||
.container
|
||||
|
||||
.announcements(
|
||||
ng-controller="AnnouncementsController"
|
||||
ng-class="{ 'announcements-open': ui.isOpen }"
|
||||
ng-cloak
|
||||
)
|
||||
.announcements-backdrop(
|
||||
ng-if="ui.isOpen"
|
||||
ng-click="toggleAnnouncementsUI();"
|
||||
)
|
||||
a.announcements-btn(
|
||||
href
|
||||
ng-if="announcements.length"
|
||||
ng-click="toggleAnnouncementsUI();"
|
||||
ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.newItems }"
|
||||
)
|
||||
span.announcements-badge(ng-if="ui.newItems") {{ ui.newItems }}
|
||||
.announcements-body(
|
||||
ng-if="ui.isOpen"
|
||||
)
|
||||
.announcements-scroller
|
||||
.announcement(
|
||||
ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id"
|
||||
)
|
||||
h2.announcement-header {{ announcement.title }}
|
||||
p.announcement-description(ng-bind-html="announcement.excerpt")
|
||||
.announcement-meta
|
||||
p.announcement-date {{ announcement.date | date:"longDate" }}
|
||||
a.announcement-link(
|
||||
ng-href="{{ announcement.url }}"
|
||||
target="_blank"
|
||||
) Read more
|
||||
div.text-center(
|
||||
ng-if="ui.newItems > 0 && ui.newItems < announcements.length"
|
||||
)
|
||||
a.btn.btn-default.btn-sm(
|
||||
href
|
||||
ng-click="showAll();"
|
||||
) Show all
|
||||
|
||||
.row(ng-cloak)
|
||||
span(ng-show="first_sign_up == 'default' || projects.length > 0")
|
||||
span(ng-if="projects.length > 0")
|
||||
aside.col-md-2.col-xs-3
|
||||
include ./list/side-bar
|
||||
|
||||
|
@ -31,8 +69,8 @@ block content
|
|||
include ./list/notifications
|
||||
include ./list/project-list
|
||||
|
||||
span(ng-if="first_sign_up == 'minimial' && projects.length == 0")
|
||||
span(ng-if="projects.length === 0")
|
||||
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
|
||||
include ./list/project-list-minimal
|
||||
include ./list/empty-project-list
|
||||
|
||||
include ./list/modals
|
46
services/web/app/views/project/list/empty-project-list.jade
Normal file
46
services/web/app/views/project/list/empty-project-list.jade
Normal file
|
@ -0,0 +1,46 @@
|
|||
.row.row-spaced
|
||||
.col-xs-12
|
||||
.card.card-thin.project-list-card
|
||||
div.welcome.text-centered(ng-cloak)
|
||||
h2 #{translate("welcome_to_sl")}
|
||||
p #{translate("new_to_latex_look_at")}
|
||||
a(href="/templates") #{translate("templates").toLowerCase()}
|
||||
| #{translate("or")}
|
||||
a(href="/learn") #{translate("latex_help_guide")}
|
||||
|
||||
|
||||
.row
|
||||
.col-md-offset-4.col-md-4
|
||||
.dropdown.minimal-create-proj-dropdown(dropdown)
|
||||
a.btn.btn-success.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
| Create First Project
|
||||
|
||||
ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu")
|
||||
li
|
||||
a(
|
||||
href,
|
||||
ng-click="openCreateProjectModal()"
|
||||
) #{translate("blank_project")}
|
||||
li
|
||||
a(
|
||||
href,
|
||||
ng-click="openCreateProjectModal('example')"
|
||||
) #{translate("example_project")}
|
||||
li
|
||||
a(
|
||||
href,
|
||||
ng-click="openUploadProjectModal()"
|
||||
) #{translate("upload_project")}
|
||||
!= moduleIncludes("newProjectMenu", locals)
|
||||
if (templates)
|
||||
li.divider
|
||||
li.dropdown-header #{translate("templates")}
|
||||
each item in templates
|
||||
li
|
||||
a.menu-indent(href=item.url) #{translate(item.name)}
|
||||
|
||||
|
|
@ -4,14 +4,31 @@ span(ng-controller="NotificationsController").userNotifications
|
|||
ng-cloak
|
||||
)
|
||||
li.notification_entry(
|
||||
ng-repeat="unreadNotification in notifications",
|
||||
ng-repeat="notification in notifications",
|
||||
)
|
||||
.row(ng-hide="unreadNotification.hide")
|
||||
.row(ng-hide="notification.hide")
|
||||
.col-xs-12
|
||||
.alert.alert-info
|
||||
.alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController")
|
||||
div.notification_inner
|
||||
span(ng-bind-html="unreadNotification.html").notification_body
|
||||
.notification_body(ng-show="!notification.accepted")
|
||||
| !{translate("notification_project_invite_message")}
|
||||
a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
|
||||
span(ng-show="!notification.inflight") #{translate("join_project")}
|
||||
span(ng-show="notification.inflight")
|
||||
i.fa.fa-fw.fa-spinner.fa-spin
|
||||
|
|
||||
| #{translate("joining")}...
|
||||
.notification_body(ng-show="notification.accepted")
|
||||
| !{translate("notification_project_invite_accepted_message")}
|
||||
a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
|
||||
span().notification_close
|
||||
button(ng-click="dismiss(unreadNotification)").close.pull-right
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-if="notification.templateKey != 'notification_project_invite'")
|
||||
div.notification_inner
|
||||
span(ng-bind-html="notification.html").notification_body
|
||||
span().notification_close
|
||||
button(ng-click="dismiss(notification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
.row
|
||||
.col-xs-12(ng-cloak)
|
||||
|
||||
.project-tools(ng-cloak)
|
||||
.btn-toolbar(ng-show="filter != 'archived'")
|
||||
.btn-group(ng-hide="selectedProjects.length < 1")
|
||||
a.btn.btn-default(
|
||||
href='#',
|
||||
tooltip="#{translate('download')}",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="downloadSelectedProjects()"
|
||||
)
|
||||
i.fa.fa-cloud-download
|
||||
a.btn.btn-default(
|
||||
href='#',
|
||||
tooltip="#{translate('delete')}",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="openArchiveProjectsModal()"
|
||||
)
|
||||
i.fa.fa-trash-o
|
||||
|
||||
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
|
||||
a.btn.btn-default.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('add_to_folders')}",
|
||||
tooltip-append-to-body="true",
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-folder-open-o
|
||||
|
|
||||
span.caret
|
||||
ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu(
|
||||
role="menu"
|
||||
ng-controller="TagListController"
|
||||
)
|
||||
li.dropdown-header #{translate("add_to_folder")}
|
||||
li(
|
||||
ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'",
|
||||
ng-controller="TagDropdownItemController"
|
||||
)
|
||||
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
|
||||
i.fa(
|
||||
ng-class="{\
|
||||
'fa-check-square-o': areSelectedProjectsInTag == true,\
|
||||
'fa-square-o': areSelectedProjectsInTag == false,\
|
||||
'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\
|
||||
}"
|
||||
)
|
||||
| {{tag.name}}
|
||||
li.divider
|
||||
li
|
||||
a(href="#", ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")}
|
||||
|
||||
.btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown
|
||||
a.btn.btn-default.dropdown-toggle(
|
||||
href='#',
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
) #{translate("more")}
|
||||
span.caret
|
||||
ul.dropdown-menu.dropdown-menu-right(role="menu")
|
||||
li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")
|
||||
a(
|
||||
href='#',
|
||||
ng-click="openRenameProjectModal()"
|
||||
) #{translate("rename")}
|
||||
li
|
||||
a(
|
||||
href='#',
|
||||
ng-click="openCloneProjectModal()"
|
||||
) #{translate("make_copy")}
|
||||
|
||||
.btn-toolbar(ng-show="filter == 'archived'")
|
||||
.btn-group(ng-hide="selectedProjects.length < 1")
|
||||
a.btn.btn-default(
|
||||
href='#',
|
||||
data-original-title="Restore",
|
||||
data-toggle="tooltip",
|
||||
data-placement="bottom",
|
||||
ng-click="restoreSelectedProjects()"
|
||||
) #{translate("restore")}
|
||||
|
||||
.btn-group(ng-hide="selectedProjects.length < 1")
|
||||
a.btn.btn-danger(
|
||||
href='#',
|
||||
data-original-title="Delete Forever",
|
||||
data-toggle="tooltip",
|
||||
data-placement="bottom",
|
||||
ng-click="openDeleteProjectsModal()"
|
||||
) #{translate("delete_forever")}
|
||||
|
||||
.row.row-spaced
|
||||
.col-xs-12
|
||||
.card.card-thin.project-list-card
|
||||
ul.list-unstyled.project-list.structured-list(
|
||||
select-all-list,
|
||||
ng-if="projects.length > 0",
|
||||
max-height="projectListHeight - 25",
|
||||
ng-cloak
|
||||
)
|
||||
li.container-fluid
|
||||
.row
|
||||
.col-xs-6
|
||||
input.select-all(
|
||||
select-all,
|
||||
type="checkbox"
|
||||
)
|
||||
span.header.clickable(ng-click="changePredicate('name')") #{translate("title")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('name')")
|
||||
.col-xs-2
|
||||
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
|
||||
.col-xs-4
|
||||
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
|
||||
li.project_entry.container-fluid(
|
||||
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
|
||||
ng-controller="ProjectListItemController"
|
||||
)
|
||||
.row
|
||||
.col-xs-6
|
||||
input.select-item(
|
||||
select-individual,
|
||||
type="checkbox",
|
||||
ng-model="project.selected"
|
||||
)
|
||||
span
|
||||
a.projectName(href="/project/{{project.id}}") {{project.name}}
|
||||
span(
|
||||
ng-controller="TagListController"
|
||||
)
|
||||
a.label.label-default.tag-label(
|
||||
href,
|
||||
ng-repeat='tag in project.tags',
|
||||
ng-click="selectTag(tag)"
|
||||
) {{tag.name}}
|
||||
.col-xs-2
|
||||
span.owner {{ownerName()}}
|
||||
.col-xs-4
|
||||
span.last-modified {{project.lastUpdated | formatDate}}
|
||||
li(
|
||||
ng-if="visibleProjects.length == 0",
|
||||
ng-cloak
|
||||
)
|
||||
.row
|
||||
.col-xs-12.text-centered
|
||||
small #{translate("no_projects")}
|
||||
|
||||
div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
|
||||
h2 #{translate("welcome_to_sl")}
|
||||
p #{translate("new_to_latex_look_at")}
|
||||
a(href="/templates") #{translate("templates").toLowerCase()}
|
||||
| #{translate("or")}
|
||||
a(href="/learn") #{translate("latex_help_guide")}
|
||||
|
||||
|
||||
.row
|
||||
.col-md-offset-4.col-md-4
|
||||
.dropdown.minimal-create-proj-dropdown(dropdown)
|
||||
a.btn.btn-success.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
| Create First Project
|
||||
|
||||
ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu", style="text-align:center;")
|
||||
li
|
||||
a(
|
||||
href,
|
||||
ng-click="openCreateProjectModal()"
|
||||
sixpack-convert="first_sign_up",
|
||||
) #{translate("blank_project")}
|
||||
li
|
||||
a(
|
||||
href,
|
||||
sixpack-convert="first_sign_up",
|
||||
ng-click="openCreateProjectModal('example')"
|
||||
) #{translate("example_project")}
|
||||
li
|
||||
a(
|
||||
href,
|
||||
sixpack-convert="first_sign_up",
|
||||
ng-click="openUploadProjectModal()"
|
||||
) #{translate("upload_project")}
|
||||
!= moduleIncludes("newProjectMenu", locals)
|
||||
if (templates)
|
||||
li.divider
|
||||
li.dropdown-header #{translate("templates")}
|
||||
each item in templates
|
||||
li
|
||||
a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)}
|
||||
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
input.form-control.col-md-7.col-xs-12(
|
||||
placeholder="#{translate('search_projects')}…",
|
||||
autofocus='autofocus',
|
||||
ng-model="searchText",
|
||||
ng-model="searchText.value",
|
||||
focus-on='search:clear',
|
||||
ng-keyup="searchProjects()"
|
||||
)
|
||||
|
@ -16,7 +16,7 @@
|
|||
i.fa.fa-times.form-control-feedback(
|
||||
ng-click="clearSearchText()",
|
||||
style="cursor: pointer;",
|
||||
ng-show="searchText.length > 0"
|
||||
ng-show="searchText.value.length > 0"
|
||||
)
|
||||
//- i.fa.fa-remove
|
||||
|
||||
|
|
|
@ -109,52 +109,17 @@
|
|||
) #{translate("complete")}
|
||||
|
||||
|
||||
.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
|
||||
span(sixpack-default).text-centered
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
span(sixpack-when="random").text-centered
|
||||
span(ng-if="randomView == 'default'")
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
span(ng-if="randomView == 'dropbox'")
|
||||
hr
|
||||
.card.card-thin
|
||||
p
|
||||
span Get Dropbox Sync
|
||||
p
|
||||
img(src=buildImgPath("dropbox/simple_logo.png"))
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
span(ng-if="randomView == 'github'")
|
||||
hr
|
||||
.card.card-thin
|
||||
p
|
||||
span Get Github Sync
|
||||
p
|
||||
img(src=buildImgPath("github/octocat.jpg"))
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
script.
|
||||
window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
|
||||
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
|
||||
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue