mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-05 07:09:02 +02:00
Merge branch 'master' into node-4.2
This commit is contained in:
+10
-12
@@ -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"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -199,7 +203,7 @@ module.exports = (grunt) ->
|
||||
acceptance:
|
||||
src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"]
|
||||
options:
|
||||
timeout: 10000
|
||||
timeout: 40000
|
||||
reporter: grunt.option('reporter') or 'spec'
|
||||
grep: grunt.option("grep")
|
||||
|
||||
@@ -380,63 +384,10 @@ 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 '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'
|
||||
|
||||
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
|
||||
|
||||
|
||||
grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
|
||||
done = @async()
|
||||
email = grunt.option("email")
|
||||
if !email?
|
||||
console.error "Usage: grunt create-admin-user --email joe@example.com"
|
||||
process.exit(1)
|
||||
|
||||
settings = require "settings-sharelatex"
|
||||
UserRegistrationHandler = require "./app/js/Features/User/UserRegistrationHandler"
|
||||
OneTimeTokenHandler = require "./app/js/Features/Security/OneTimeTokenHandler"
|
||||
UserRegistrationHandler.registerNewUser {
|
||||
email: email
|
||||
password: require("crypto").randomBytes(32).toString("hex")
|
||||
}, (error, user) ->
|
||||
if error? and error?.message != "EmailAlreadyRegistered"
|
||||
throw error
|
||||
user.isAdmin = true
|
||||
user.save (error) ->
|
||||
throw error if error?
|
||||
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
|
||||
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||
return next(err) if err?
|
||||
|
||||
console.log ""
|
||||
console.log """
|
||||
Successfully created #{email} as an admin user.
|
||||
|
||||
Please visit the following URL to set a password for #{email} and log in:
|
||||
|
||||
#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}
|
||||
|
||||
"""
|
||||
done()
|
||||
|
||||
grunt.registerTask 'delete-user', "deletes a user and all their data", () ->
|
||||
done = @async()
|
||||
email = grunt.option("email")
|
||||
if !email?
|
||||
console.error "Usage: grunt delete-user --email joe@example.com"
|
||||
process.exit(1)
|
||||
settings = require "settings-sharelatex"
|
||||
UserGetter = require "./app/js/Features/User/UserGetter"
|
||||
UserDeleter = require "./app/js/Features/User/UserDeleter"
|
||||
UserGetter.getUser email:email, (error, user) ->
|
||||
if error?
|
||||
throw error
|
||||
if !user?
|
||||
console.log("user #{email} not in database, potentially already deleted")
|
||||
return done()
|
||||
UserDeleter.deleteUser user._id, (err)->
|
||||
if err?
|
||||
throw err
|
||||
done()
|
||||
@@ -0,0 +1,7 @@
|
||||
AnalyticsManager = require "./AnalyticsManager"
|
||||
|
||||
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
|
||||
@@ -0,0 +1,43 @@
|
||||
Settings = require "settings-sharelatex"
|
||||
logger = require "logger-sharelatex"
|
||||
_ = require "underscore"
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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()
|
||||
@@ -10,62 +10,115 @@ Settings = require "settings-sharelatex"
|
||||
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)
|
||||
|
||||
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
|
||||
req.login user, (err) ->
|
||||
# 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
|
||||
# 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 next(err)
|
||||
UserSessionsManager.trackSession(user, req.sessionID, () ->)
|
||||
res.json {redir: req._redir}
|
||||
else
|
||||
res.json message: info
|
||||
)(req, res, next)
|
||||
|
||||
doPassportLogin: (req, username, password, done) ->
|
||||
email = username.toLowerCase()
|
||||
redir = Url.parse(req?.body?.redir or "/project").path
|
||||
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"
|
||||
res.json redir: redir
|
||||
# async actions
|
||||
UserHandler.setupLoginData(user, ()->)
|
||||
LoginRateLimiter.recordSuccessfulLogin(email)
|
||||
AuthenticationController._recordSuccessfulLogin(user._id)
|
||||
Analytics.recordEvent(user._id, "user-logged-in")
|
||||
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
|
||||
req._redir = redir
|
||||
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
|
||||
|
||||
@@ -79,7 +132,7 @@ 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"
|
||||
@@ -97,7 +150,6 @@ module.exports = AuthenticationController =
|
||||
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
|
||||
@@ -124,26 +176,3 @@ module.exports = AuthenticationController =
|
||||
_recordFailedLogin: (callback = (error) ->) ->
|
||||
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
|
||||
|
||||
req.session.user = lightUser
|
||||
|
||||
UserSessionsManager.trackSession(user, req.sessionID, () ->)
|
||||
callback()
|
||||
|
||||
@@ -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?
|
||||
redirect_to += "?redir=#{encodeURIComponent(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
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
ChatHandler = require("./ChatHandler")
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
logger = require("logger-sharelatex")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
||||
module.exports =
|
||||
|
||||
|
||||
sendMessage: (req, res)->
|
||||
sendMessage: (req, res, next)->
|
||||
project_id = req.params.Project_id
|
||||
user_id = req.session.user._id
|
||||
messageContent = req.body.content
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if !user_id?
|
||||
err = new Error('no logged-in user')
|
||||
return next(err)
|
||||
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"
|
||||
|
||||
@@ -4,7 +4,9 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler"
|
||||
EditorRealTimeController = require "../Editor/EditorRealTimeController"
|
||||
LimitationsManager = require "../Subscription/LimitationsManager"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
mimelib = require("mimelib")
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
module.exports = CollaboratorsController =
|
||||
addUserToProject: (req, res, next) ->
|
||||
@@ -16,11 +18,11 @@ module.exports = CollaboratorsController =
|
||||
return res.json { user: false }
|
||||
else
|
||||
{email, privileges} = req.body
|
||||
|
||||
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
|
||||
|
||||
email = EmailHelper.parseEmail(email)
|
||||
if !email? or email == ""
|
||||
return res.status(400).send("invalid email address")
|
||||
|
||||
|
||||
adding_user_id = req.session?.user?._id
|
||||
CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) =>
|
||||
return next(error) if error?
|
||||
@@ -35,8 +37,9 @@ module.exports = CollaboratorsController =
|
||||
user_id = req.params.user_id
|
||||
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true}
|
||||
res.sendStatus 204
|
||||
|
||||
|
||||
removeSelfFromProject: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
user_id = req.session?.user?._id
|
||||
@@ -50,3 +53,11 @@ module.exports = CollaboratorsController =
|
||||
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
|
||||
callback()
|
||||
|
||||
getAllMembers: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
logger.log {projectId}, "getting all active members for project"
|
||||
CollaboratorsHandler.getAllMembers projectId, (err, members) ->
|
||||
if err?
|
||||
logger.err {projectId}, "error getting members for project"
|
||||
return next(err)
|
||||
res.json({members: members})
|
||||
|
||||
@@ -2,7 +2,15 @@ Project = require("../../models/Project").Project
|
||||
EmailHandler = require("../Email/EmailHandler")
|
||||
Settings = require "settings-sharelatex"
|
||||
|
||||
module.exports =
|
||||
|
||||
module.exports = CollaboratorsEmailHandler =
|
||||
|
||||
_buildInviteUrl: (project, invite) ->
|
||||
"#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
|
||||
"project_name=#{encodeURIComponent(project.name)}"
|
||||
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
|
||||
].join("&")
|
||||
|
||||
notifyUserOfProjectShare: (project_id, email, callback)->
|
||||
Project
|
||||
.findOne(_id: project_id )
|
||||
@@ -22,4 +30,19 @@ module.exports =
|
||||
"rs=ci" # referral source = collaborator invite
|
||||
].join("&")
|
||||
owner: project.owner_ref
|
||||
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
|
||||
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
|
||||
|
||||
notifyUserOfProjectInvite: (project_id, email, invite, 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
|
||||
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
|
||||
owner: project.owner_ref
|
||||
EmailHandler.sendEmail "projectInvite", emailOptions, callback
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
UserCreator = require('../User/UserCreator')
|
||||
Project = require("../../models/Project").Project
|
||||
mimelib = require("mimelib")
|
||||
logger = require('logger-sharelatex')
|
||||
UserGetter = require "../User/UserGetter"
|
||||
ContactManager = require "../Contacts/ContactManager"
|
||||
@@ -8,8 +7,12 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
|
||||
async = require "async"
|
||||
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
|
||||
Errors = require "../Errors/Errors"
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
|
||||
|
||||
|
||||
module.exports = CollaboratorsHandler =
|
||||
|
||||
getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
|
||||
Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
|
||||
return callback(error) if error?
|
||||
@@ -21,12 +24,12 @@ module.exports = CollaboratorsHandler =
|
||||
for member_id in project.collaberator_refs or []
|
||||
members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
|
||||
return callback null, members
|
||||
|
||||
|
||||
getMemberIds: (project_id, callback = (error, member_ids) ->) ->
|
||||
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
|
||||
return callback(error) if error?
|
||||
return callback null, members.map (m) -> m.id
|
||||
|
||||
|
||||
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
|
||||
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
|
||||
return callback(error) if error?
|
||||
@@ -41,7 +44,7 @@ module.exports = CollaboratorsHandler =
|
||||
(error) ->
|
||||
return callback(error) if error?
|
||||
callback null, result
|
||||
|
||||
|
||||
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
|
||||
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
|
||||
# then optimise this.
|
||||
@@ -51,12 +54,12 @@ module.exports = CollaboratorsHandler =
|
||||
if member.id == user_id?.toString()
|
||||
return callback null, member.privilegeLevel
|
||||
return callback null, PrivilegeLevels.NONE
|
||||
|
||||
|
||||
getMemberCount: (project_id, callback = (error, count) ->) ->
|
||||
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
|
||||
return callback(error) if error?
|
||||
return callback null, (members or []).length
|
||||
|
||||
|
||||
getCollaboratorCount: (project_id, callback = (error, count) ->) ->
|
||||
CollaboratorsHandler.getMemberCount project_id, (error, count) ->
|
||||
return callback(error) if error?
|
||||
@@ -69,14 +72,14 @@ module.exports = CollaboratorsHandler =
|
||||
if member.id.toString() == user_id.toString()
|
||||
return callback null, true, member.privilegeLevel
|
||||
return callback null, false, null
|
||||
|
||||
|
||||
getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) ->
|
||||
Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=>
|
||||
return callback(err) if err?
|
||||
Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=>
|
||||
return callback(err) if err?
|
||||
callback(null, readAndWriteProjects, readOnlyProjects)
|
||||
|
||||
|
||||
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
|
||||
logger.log user_id: user_id, project_id: project_id, "removing user"
|
||||
conditions = _id:project_id
|
||||
@@ -86,7 +89,7 @@ module.exports = CollaboratorsHandler =
|
||||
if err?
|
||||
logger.error err: err, "problem removing user from project collaberators"
|
||||
callback(err)
|
||||
|
||||
|
||||
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
|
||||
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
|
||||
return callback(error) if error?
|
||||
@@ -98,10 +101,9 @@ module.exports = CollaboratorsHandler =
|
||||
return cb() if !project?
|
||||
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
|
||||
async.series jobs, callback
|
||||
|
||||
|
||||
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
|
||||
emails = mimelib.parseAddresses(unparsed_email)
|
||||
email = emails[0]?.address?.toLowerCase()
|
||||
email = EmailHelper.parseEmail(unparsed_email)
|
||||
if !email? or email == ""
|
||||
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
|
||||
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
|
||||
@@ -118,7 +120,7 @@ module.exports = CollaboratorsHandler =
|
||||
existing_users = existing_users.map (u) -> u.toString()
|
||||
if existing_users.indexOf(user_id.toString()) > -1
|
||||
return callback null # User already in Project
|
||||
|
||||
|
||||
if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
|
||||
level = {"collaberator_refs":user_id}
|
||||
logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user"
|
||||
@@ -128,11 +130,6 @@ module.exports = CollaboratorsHandler =
|
||||
else
|
||||
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
|
||||
|
||||
# Do these in the background
|
||||
UserGetter.getUser user_id, {email: 1}, (error, user) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, user_id}, "error getting user while adding to project"
|
||||
CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email
|
||||
ContactManager.addContact adding_user_id, user_id
|
||||
|
||||
Project.update { _id: project_id }, { $addToSet: level }, (error) ->
|
||||
@@ -143,3 +140,12 @@ module.exports = CollaboratorsHandler =
|
||||
if error?
|
||||
logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
|
||||
callback()
|
||||
|
||||
getAllMembers: (projectId, callback=(err, members)->) ->
|
||||
logger.log {projectId}, "fetching all members"
|
||||
CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) ->
|
||||
if error?
|
||||
logger.err {projectId, error}, "error getting members for project"
|
||||
return callback(error)
|
||||
{owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers)
|
||||
callback(null, members)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
ProjectGetter = require "../Project/ProjectGetter"
|
||||
LimitationsManager = require "../Subscription/LimitationsManager"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
CollaboratorsHandler = require('./CollaboratorsHandler')
|
||||
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
|
||||
logger = require('logger-sharelatex')
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
AnalyticsManger = require("../Analytics/AnalyticsManager")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
|
||||
module.exports = CollaboratorsInviteController =
|
||||
|
||||
getAllInvites: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
logger.log {projectId}, "getting all active invites for project"
|
||||
CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) ->
|
||||
if err?
|
||||
logger.err {projectId}, "error getting invites for project"
|
||||
return next(err)
|
||||
res.json({invites: invites})
|
||||
|
||||
inviteToProject: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
email = req.body.email
|
||||
sendingUser = AuthenticationController.getSessionUser(req)
|
||||
sendingUserId = sendingUser._id
|
||||
logger.log {projectId, email, sendingUserId}, "inviting to project"
|
||||
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
|
||||
return next(error) if error?
|
||||
if !allowed
|
||||
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
|
||||
return res.json {invite: null}
|
||||
{email, privileges} = req.body
|
||||
email = EmailHelper.parseEmail(email)
|
||||
if !email? or email == ""
|
||||
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
||||
return res.sendStatus(400)
|
||||
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
|
||||
inviteId = req.params.invite_id
|
||||
logger.log {projectId, inviteId}, "revoking invite"
|
||||
CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) ->
|
||||
if err?
|
||||
logger.err {projectId, inviteId}, "error revoking invite"
|
||||
return next(err)
|
||||
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
|
||||
res.sendStatus(201)
|
||||
|
||||
resendInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
inviteId = req.params.invite_id
|
||||
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"
|
||||
return next(err)
|
||||
res.sendStatus(201)
|
||||
|
||||
viewInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
token = req.params.token
|
||||
_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"
|
||||
return next(err)
|
||||
if isMember
|
||||
logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
|
||||
return res.redirect "/project/#{projectId}"
|
||||
# get the invite
|
||||
CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
|
||||
if err?
|
||||
logger.err {projectId, token}, "error getting invite by token"
|
||||
return next(err)
|
||||
# check if invite is gone, or otherwise non-existent
|
||||
if !invite?
|
||||
logger.log {projectId, token}, "no invite found for this token"
|
||||
return _renderInvalidPage()
|
||||
# check the user who sent the invite exists
|
||||
UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error getting project owner"
|
||||
return next(err)
|
||||
if !owner?
|
||||
logger.log {projectId}, "no project owner found"
|
||||
return _renderInvalidPage()
|
||||
# fetch the project name
|
||||
ProjectGetter.getProject projectId, {}, (err, project) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error getting project"
|
||||
return next(err)
|
||||
if !project?
|
||||
logger.log {projectId}, "no project found"
|
||||
return _renderInvalidPage()
|
||||
# finally render the invite
|
||||
res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
|
||||
|
||||
acceptInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
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, 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", {projectId:projectId, userId:currentUser._id})
|
||||
if req.xhr
|
||||
res.sendStatus 204 # Done async via project page notification
|
||||
else
|
||||
res.redirect "/project/#{projectId}"
|
||||
@@ -0,0 +1,144 @@
|
||||
ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
|
||||
logger = require('logger-sharelatex')
|
||||
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
|
||||
CollaboratorsHandler = require "./CollaboratorsHandler"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
ProjectGetter = require "../Project/ProjectGetter"
|
||||
Async = require "async"
|
||||
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
|
||||
Errors = require "../Errors/Errors"
|
||||
Crypto = require 'crypto'
|
||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
|
||||
|
||||
module.exports = CollaboratorsInviteHandler =
|
||||
|
||||
getAllInvites: (projectId, callback=(err, invites)->) ->
|
||||
logger.log {projectId}, "fetching invites for project"
|
||||
ProjectInvite.find {projectId: projectId}, (err, invites) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error getting invites from mongo"
|
||||
return callback(err)
|
||||
logger.log {projectId, count: invites.length}, "found invites for project"
|
||||
callback(null, invites)
|
||||
|
||||
getInviteCount: (projectId, callback=(err, count)->) ->
|
||||
logger.log {projectId}, "counting invites for project"
|
||||
ProjectInvite.count {projectId: projectId}, (err, count) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error getting invites from mongo"
|
||||
return callback(err)
|
||||
callback(null, count)
|
||||
|
||||
_trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
|
||||
email = invite.email
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
|
||||
if err?
|
||||
logger.err {projectId, email}, "error checking if user exists"
|
||||
return callback(err)
|
||||
if !existingUser?
|
||||
logger.log {projectId, email}, "no existing user found, returning"
|
||||
return callback(null)
|
||||
ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) ->
|
||||
if err?
|
||||
logger.err {projectId, email}, "error getting project"
|
||||
return callback(err)
|
||||
if !project?
|
||||
logger.log {projectId}, "no project found while sending notification, returning"
|
||||
return callback(null)
|
||||
NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
|
||||
|
||||
_tryCancelInviteNotification: (inviteId, callback=()->) ->
|
||||
NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback)
|
||||
|
||||
_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)->
|
||||
return callback(err) if err?
|
||||
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
|
||||
return callback(err) if err?
|
||||
callback()
|
||||
|
||||
inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) ->
|
||||
logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite"
|
||||
Crypto.randomBytes 24, (err, buffer) ->
|
||||
if err?
|
||||
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token"
|
||||
return callback(err)
|
||||
token = buffer.toString('hex')
|
||||
invite = new ProjectInvite {
|
||||
email: email
|
||||
token: token
|
||||
sendingUserId: sendingUser._id
|
||||
projectId: projectId
|
||||
privileges: privileges
|
||||
}
|
||||
invite.save (err, invite) ->
|
||||
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(null, invite)
|
||||
|
||||
|
||||
revokeInvite: (projectId, inviteId, callback=(err)->) ->
|
||||
logger.log {projectId, inviteId}, "removing invite"
|
||||
ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId}, "error removing invite"
|
||||
return callback(err)
|
||||
CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->)
|
||||
callback(null)
|
||||
|
||||
resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) ->
|
||||
logger.log {projectId, inviteId}, "resending invite email"
|
||||
ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId}, "error finding invite"
|
||||
return callback(err)
|
||||
if !invite?
|
||||
logger.err {err, projectId, inviteId}, "no invite found, nothing to resend"
|
||||
return callback(null)
|
||||
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
|
||||
if err?
|
||||
logger.err {projectId, inviteId}, "error resending invite messages"
|
||||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
|
||||
logger.log {projectId, tokenString}, "fetching invite by token"
|
||||
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error fetching invite"
|
||||
return callback(err)
|
||||
if !invite?
|
||||
logger.err {err, projectId, token: tokenString}, "no invite found"
|
||||
return callback(null, null)
|
||||
callback(null, 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, tokenString}, "error finding invite"
|
||||
return callback(err)
|
||||
if !invite
|
||||
err = new Errors.NotFoundError("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) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project"
|
||||
return callback(err)
|
||||
# Remove invite
|
||||
logger.log {projectId, inviteId}, "removing invite"
|
||||
ProjectInvite.remove {_id: inviteId}, (err) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId}, "error removing invite"
|
||||
return callback(err)
|
||||
CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()->
|
||||
callback()
|
||||
@@ -1,6 +1,8 @@
|
||||
CollaboratorsController = require('./CollaboratorsController')
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
|
||||
CollaboratorsInviteController = require('./CollaboratorsInviteController')
|
||||
RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
|
||||
|
||||
module.exports =
|
||||
apply: (webRouter, apiRouter) ->
|
||||
@@ -8,3 +10,63 @@ module.exports =
|
||||
|
||||
webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject
|
||||
webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject
|
||||
|
||||
webRouter.get(
|
||||
'/project/:Project_id/members',
|
||||
AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanAdminProject,
|
||||
CollaboratorsController.getAllMembers
|
||||
)
|
||||
|
||||
# invites
|
||||
webRouter.post(
|
||||
'/project/:Project_id/invite',
|
||||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "invite-to-project"
|
||||
params: ["Project_id"]
|
||||
maxRequests: 200
|
||||
timeInterval: 60 * 10
|
||||
}),
|
||||
AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanAdminProject,
|
||||
CollaboratorsInviteController.inviteToProject
|
||||
)
|
||||
|
||||
webRouter.get(
|
||||
'/project/:Project_id/invites',
|
||||
AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanAdminProject,
|
||||
CollaboratorsInviteController.getAllInvites
|
||||
)
|
||||
|
||||
webRouter.delete(
|
||||
'/project/:Project_id/invite/:invite_id',
|
||||
AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanAdminProject,
|
||||
CollaboratorsInviteController.revokeInvite
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/project/:Project_id/invite/:invite_id/resend',
|
||||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "resend-invite"
|
||||
params: ["Project_id"]
|
||||
maxRequests: 200
|
||||
timeInterval: 60 * 10
|
||||
}),
|
||||
AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanAdminProject,
|
||||
CollaboratorsInviteController.resendInvite
|
||||
)
|
||||
|
||||
webRouter.get(
|
||||
'/project/:Project_id/invite/token/:token',
|
||||
AuthenticationController.requireLogin(),
|
||||
CollaboratorsInviteController.viewInvite
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/project/:Project_id/invite/token/:token/accept',
|
||||
AuthenticationController.requireLogin(),
|
||||
CollaboratorsInviteController.acceptInvite
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -147,6 +147,7 @@ module.exports = ClsiManager =
|
||||
timeout: options.timeout
|
||||
imageName: project.imageName
|
||||
draft: !!options.draft
|
||||
check: options.check
|
||||
rootResourcePath: rootResourcePath
|
||||
resources: resources
|
||||
}
|
||||
|
||||
@@ -16,51 +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
|
||||
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
|
||||
|
||||
@@ -149,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()
|
||||
|
||||
@@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager")
|
||||
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
|
||||
Metrics = require('../../infrastructure/Metrics')
|
||||
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
|
||||
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
|
||||
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
|
||||
|
||||
module.exports = EditorHttpController =
|
||||
@@ -30,6 +31,7 @@ module.exports = EditorHttpController =
|
||||
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
|
||||
|
||||
_buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
|
||||
logger.log {project_id, user_id}, "building the joinProject view"
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
|
||||
return callback(error) if error?
|
||||
return callback(new Error("not found")) if !project?
|
||||
@@ -40,10 +42,13 @@ module.exports = EditorHttpController =
|
||||
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
|
||||
return callback(error) if error?
|
||||
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
|
||||
callback null, null, false
|
||||
else
|
||||
logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
|
||||
return callback null, null, false
|
||||
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
|
||||
return callback(error) if error?
|
||||
logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
|
||||
callback(null,
|
||||
ProjectEditorHandler.buildProjectModelView(project, members),
|
||||
ProjectEditorHandler.buildProjectModelView(project, members, invites),
|
||||
privilegeLevel
|
||||
)
|
||||
|
||||
@@ -135,5 +140,3 @@ module.exports = EditorHttpController =
|
||||
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ settings = require("settings-sharelatex")
|
||||
|
||||
templates = {}
|
||||
|
||||
templates.registered =
|
||||
templates.registered =
|
||||
subject: _.template "Activate your #{settings.appName} Account"
|
||||
layout: PersonalEmailLayout
|
||||
type: "notification"
|
||||
@@ -19,14 +19,14 @@ templates.registered =
|
||||
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
|
||||
"""
|
||||
|
||||
templates.canceledSubscription =
|
||||
templates.canceledSubscription =
|
||||
subject: _.template "ShareLaTeX thoughts"
|
||||
layout: PersonalEmailLayout
|
||||
type:"lifecycle"
|
||||
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,7 +36,7 @@ ShareLaTeX Co-founder
|
||||
</p>
|
||||
'''
|
||||
|
||||
templates.passwordResetRequested =
|
||||
templates.passwordResetRequested =
|
||||
subject: _.template "Password Reset - #{settings.appName}"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
@@ -66,7 +66,7 @@ If you didn't request a password reset, let us know.
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
|
||||
templates.projectSharedWithYou =
|
||||
templates.projectSharedWithYou =
|
||||
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
@@ -87,8 +87,33 @@ templates.projectSharedWithYou =
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
|
||||
templates.projectInvite =
|
||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
|
||||
|
||||
templates.completeJoinGroupAccount =
|
||||
Follow this link to view the project: <%= inviteUrl %>
|
||||
|
||||
Thank you
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
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>
|
||||
"""
|
||||
|
||||
templates.completeJoinGroupAccount =
|
||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
@@ -121,6 +146,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)
|
||||
|
||||
@@ -49,8 +49,11 @@ module.exports =
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
mimelib = require("mimelib")
|
||||
|
||||
|
||||
module.exports = EmailHelper =
|
||||
|
||||
parseEmail: (email) ->
|
||||
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
|
||||
if !email? or email == ""
|
||||
return null
|
||||
else
|
||||
return email
|
||||
@@ -1,16 +1,31 @@
|
||||
logger = require("logger-sharelatex")
|
||||
NotificationsHandler = require("./NotificationsHandler")
|
||||
|
||||
module.exports =
|
||||
module.exports =
|
||||
|
||||
# Note: notification keys should be url-safe
|
||||
|
||||
groupPlan: (user, licence)->
|
||||
key : "join-sub-#{licence.subscription_id}"
|
||||
create: (callback = ->)->
|
||||
messageOpts =
|
||||
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, 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
|
||||
|
||||
projectInvite: (invite, project, sendingUser, user) ->
|
||||
key: "project-invite-#{invite._id}"
|
||||
create: (callback=()->) ->
|
||||
messageOpts =
|
||||
userName: sendingUser.first_name
|
||||
projectName: project.name
|
||||
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, 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()
|
||||
|
||||
@@ -10,10 +10,10 @@ makeRequest = (opts, callback)->
|
||||
else
|
||||
request(opts, callback)
|
||||
|
||||
module.exports =
|
||||
module.exports =
|
||||
|
||||
getUserNotifications: (user_id, callback)->
|
||||
opts =
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
json: true
|
||||
timeout: oneSecond
|
||||
@@ -29,21 +29,25 @@ module.exports =
|
||||
unreadNotifications = []
|
||||
callback(null, unreadNotifications)
|
||||
|
||||
createNotification: (user_id, key, templateKey, messageOpts, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
timeout: oneSecond
|
||||
method:"POST"
|
||||
json: {
|
||||
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
|
||||
payload = {
|
||||
key:key
|
||||
messageOpts:messageOpts
|
||||
templateKey:templateKey
|
||||
}
|
||||
forceCreate: true
|
||||
}
|
||||
if expiryDateTime?
|
||||
payload.expires = expiryDateTime
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
timeout: oneSecond
|
||||
method:"POST"
|
||||
json: payload
|
||||
logger.log opts:opts, "creating notification for user"
|
||||
makeRequest opts, callback
|
||||
|
||||
markAsReadWithKey: (user_id, key, callback)->
|
||||
opts =
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
method: "DELETE"
|
||||
timeout: oneSecond
|
||||
@@ -52,7 +56,7 @@ module.exports =
|
||||
}
|
||||
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
|
||||
makeRequest opts, callback
|
||||
|
||||
|
||||
|
||||
markAsRead: (user_id, notification_id, callback)->
|
||||
opts =
|
||||
@@ -61,3 +65,13 @@ module.exports =
|
||||
timeout:oneSecond
|
||||
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
|
||||
makeRequest opts, callback
|
||||
|
||||
# removes notification by key, without regard for user_id,
|
||||
# should not be exposed to user via ui/router
|
||||
markAsReadByKeyOnly: (key, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/key/#{key}"
|
||||
method: "DELETE"
|
||||
timeout: oneSecond
|
||||
logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
|
||||
makeRequest opts, callback
|
||||
|
||||
@@ -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,7 @@ 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,8 +187,8 @@ 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
|
||||
@@ -259,6 +263,7 @@ module.exports = ProjectController =
|
||||
fontSize : user.ace.fontSize
|
||||
autoComplete: user.ace.autoComplete
|
||||
pdfViewer : user.ace.pdfViewer
|
||||
syntaxValidation: user.ace.syntaxValidation
|
||||
}
|
||||
privilegeLevel: privilegeLevel
|
||||
chatUrl: Settings.apis.chat.url
|
||||
@@ -320,6 +325,7 @@ defaultSettingsForAnonymousUser = (user_id)->
|
||||
autoComplete: true
|
||||
spellCheckLanguage: ""
|
||||
pdfViewer: ""
|
||||
syntaxValidation: true
|
||||
subscription:
|
||||
freeTrial:
|
||||
allowed: true
|
||||
@@ -328,8 +334,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
_ = require("underscore")
|
||||
|
||||
module.exports = ProjectEditorHandler =
|
||||
buildProjectModelView: (project, members) ->
|
||||
buildProjectModelView: (project, members, invites) ->
|
||||
result =
|
||||
_id : project._id
|
||||
name : project.name
|
||||
@@ -15,17 +15,16 @@ module.exports = ProjectEditorHandler =
|
||||
deletedByExternalDataSource : project.deletedByExternalDataSource || false
|
||||
deletedDocs: project.deletedDocs
|
||||
members: []
|
||||
|
||||
owner = null
|
||||
for member in members
|
||||
if member.privilegeLevel == "owner"
|
||||
owner = member.user
|
||||
else
|
||||
result.members.push @buildUserModelView member.user, member.privilegeLevel
|
||||
if owner?
|
||||
result.owner = @buildUserModelView owner, "owner"
|
||||
invites: invites
|
||||
|
||||
result.features = _.defaults(owner?.features or {}, {
|
||||
if !result.invites?
|
||||
result.invites = []
|
||||
|
||||
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
|
||||
result.owner = owner
|
||||
result.members = members
|
||||
|
||||
result.features = _.defaults(ownerFeatures or {}, {
|
||||
collaborators: -1 # Infinite
|
||||
versioning: false
|
||||
dropbox:false
|
||||
@@ -37,6 +36,18 @@ module.exports = ProjectEditorHandler =
|
||||
|
||||
return result
|
||||
|
||||
buildOwnerAndMembersViews: (members) ->
|
||||
owner = null
|
||||
ownerFeatures = null
|
||||
filteredMembers = []
|
||||
for member in members
|
||||
if member.privilegeLevel == "owner"
|
||||
ownerFeatures = member.user.features
|
||||
owner = @buildUserModelView member.user, "owner"
|
||||
else
|
||||
filteredMembers.push @buildUserModelView member.user, member.privilegeLevel
|
||||
{owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers}
|
||||
|
||||
buildUserModelView: (user, privileges) ->
|
||||
_id : user._id
|
||||
first_name : user.first_name
|
||||
|
||||
@@ -258,6 +258,7 @@ module.exports = ProjectEntityHandler =
|
||||
if !foundFolder?
|
||||
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
|
||||
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
|
||||
return callback(err) if err?
|
||||
newFolder.parentFolder_id = parentFolder_id
|
||||
previousFolders.push newFolder
|
||||
callback null, previousFolders
|
||||
@@ -268,6 +269,7 @@ module.exports = ProjectEntityHandler =
|
||||
|
||||
|
||||
async.reduce folders, [], procesFolder, (err, folders)->
|
||||
return callback(err) if err?
|
||||
lastFolder = folders[folders.length-1]
|
||||
folders = _.select folders, (folder)->
|
||||
!folder.filterOut
|
||||
|
||||
@@ -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()
|
||||
@@ -4,7 +4,7 @@ settings = require("settings-sharelatex")
|
||||
ProjectGetter = require "../Project/ProjectGetter"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
U = require('underscore')
|
||||
_ = require('underscore')
|
||||
Async = require('async')
|
||||
|
||||
oneMinInMs = 60 * 1000
|
||||
@@ -22,24 +22,24 @@ module.exports = ReferencesHandler =
|
||||
_findBibFileIds: (project) ->
|
||||
ids = []
|
||||
_process = (folder) ->
|
||||
(folder.fileRefs or []).forEach (file) ->
|
||||
_.each (folder.fileRefs or []), (file) ->
|
||||
if file?.name?.match(/^.*\.bib$/)
|
||||
ids.push(file._id)
|
||||
(folder.folders or []).forEach (folder) ->
|
||||
_.each (folder.folders or []), (folder) ->
|
||||
_process(folder)
|
||||
(project.rootFolder or []).forEach (rootFolder) ->
|
||||
_.each (project.rootFolder or []), (rootFolder) ->
|
||||
_process(rootFolder)
|
||||
return ids
|
||||
|
||||
_findBibDocIds: (project) ->
|
||||
ids = []
|
||||
_process = (folder) ->
|
||||
(folder.docs or []).forEach (doc) ->
|
||||
_.each (folder.docs or []), (doc) ->
|
||||
if doc?.name?.match(/^.*\.bib$/)
|
||||
ids.push(doc._id)
|
||||
(folder.folders or []).forEach (folder) ->
|
||||
_.each (folder.folders or []), (folder) ->
|
||||
_process(folder)
|
||||
(project.rootFolder or []).forEach (rootFolder) ->
|
||||
_.each (project.rootFolder or []), (rootFolder) ->
|
||||
_process(rootFolder)
|
||||
return ids
|
||||
|
||||
|
||||
@@ -30,5 +30,5 @@ module.exports =
|
||||
multi.get buildKey(token)
|
||||
multi.del buildKey(token)
|
||||
multi.exec (err, results)->
|
||||
callback err, results[0]
|
||||
callback err, results?[0]
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
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
|
||||
if !opts.endpointName?
|
||||
@@ -37,4 +35,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,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,70 +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)->
|
||||
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")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ User = require("../../models/User").User
|
||||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
Settings = require("settings-sharelatex")
|
||||
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
|
||||
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
|
||||
|
||||
module.exports =
|
||||
|
||||
@@ -20,10 +21,12 @@ module.exports =
|
||||
return callback(error) if error?
|
||||
CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) =>
|
||||
return callback(error) if error?
|
||||
if current_number + x_collaborators <= allowed_number or allowed_number < 0
|
||||
callback null, true
|
||||
else
|
||||
callback null, false
|
||||
CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) =>
|
||||
return callback(error) if error?
|
||||
if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0
|
||||
callback null, true
|
||||
else
|
||||
callback null, false
|
||||
|
||||
userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) ->
|
||||
@userHasSubscription user, (err, hasSubscription, subscription)=>
|
||||
@@ -41,7 +44,7 @@ module.exports =
|
||||
hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true)
|
||||
logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
|
||||
callback err, hasValidSubscription, subscription
|
||||
|
||||
|
||||
userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) ->
|
||||
logger.log user_id: user._id, "checking is user is member of subscription groups"
|
||||
SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) ->
|
||||
@@ -65,4 +68,3 @@ getOwnerOfProject = (project_id, callback)->
|
||||
return callback(error) if error?
|
||||
User.findById project.owner_ref, (error, owner) ->
|
||||
callback(error, owner)
|
||||
|
||||
|
||||
@@ -29,13 +29,15 @@ module.exports = RecurlyWrapper =
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{user._id}"
|
||||
method: "GET"
|
||||
expect404: true
|
||||
}, (error, response, responseBody) ->
|
||||
if error
|
||||
if response.statusCode == 404 # actually not an error in this case, just no existing account
|
||||
cache.userExists = false
|
||||
return next(null, cache)
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
|
||||
return next(error)
|
||||
if response.statusCode == 404 # actually not an error in this case, just no existing account
|
||||
logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed"
|
||||
cache.userExists = false
|
||||
return next(null, cache)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
|
||||
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
|
||||
if err
|
||||
@@ -236,10 +238,14 @@ module.exports = RecurlyWrapper =
|
||||
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
|
||||
"Accept" : "application/xml"
|
||||
"Content-Type" : "application/xml; charset=utf-8"
|
||||
expect404 = options.expect404
|
||||
delete options.expect404
|
||||
request options, (error, response, body) ->
|
||||
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
|
||||
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404)
|
||||
logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
|
||||
error = "Recurly API returned with status code: #{response.statusCode}"
|
||||
if response.statusCode == 404 and expect404
|
||||
logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response"
|
||||
callback(error, response, body)
|
||||
|
||||
sign : (parameters, callback) ->
|
||||
|
||||
@@ -13,15 +13,16 @@ module.exports = SubscriptionController =
|
||||
|
||||
plansPage: (req, res, next) ->
|
||||
plans = SubscriptionViewModelBuilder.buildViewModel()
|
||||
if !req.session.user?
|
||||
baseUrl = "/register?redir="
|
||||
else
|
||||
if AuthenticationController.isUserLoggedIn(req)
|
||||
baseUrl = ""
|
||||
else
|
||||
baseUrl = "/register?redir="
|
||||
viewName = "subscriptions/plans"
|
||||
if req.query.v?
|
||||
viewName = "#{viewName}_#{req.query.v}"
|
||||
logger.log viewName:viewName, "showing plans page"
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
|
||||
return next(err) if err?
|
||||
res.render viewName,
|
||||
title: "plans_and_pricing"
|
||||
plans: plans
|
||||
@@ -31,200 +32,210 @@ module.exports = SubscriptionController =
|
||||
|
||||
#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)->
|
||||
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
|
||||
|
||||
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) ->
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
|
||||
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)->
|
||||
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/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)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) =>
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
|
||||
res.render "subscriptions/successful_subscription",
|
||||
title: "thank_you"
|
||||
subscription:subscription
|
||||
successful_subscription: (req, res, next)->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/successful_subscription",
|
||||
title: "thank_you"
|
||||
subscription:subscription
|
||||
|
||||
cancelSubscription: (req, res, next) ->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
logger.log user_id:user._id, "canceling subscription"
|
||||
return next(error) if error?
|
||||
SubscriptionHandler.cancelSubscription user, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
|
||||
res.redirect "/user/subscription"
|
||||
|
||||
updateSubscription: (req, res)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
return next(error) if error?
|
||||
planCode = req.body.plan_code
|
||||
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"
|
||||
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"
|
||||
|
||||
reactivateSubscription: (req, res)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
logger.log user_id:user._id, "reactivating subscription"
|
||||
return next(error) if error?
|
||||
SubscriptionHandler.reactivateSubscription user, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
|
||||
res.redirect "/user/subscription"
|
||||
updateSubscription: (req, res, next)->
|
||||
_origin = req?.query?.origin || null
|
||||
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)
|
||||
res.redirect "/user/subscription"
|
||||
|
||||
recurlyCallback: (req, res)->
|
||||
reactivateSubscription: (req, res, next)->
|
||||
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"
|
||||
# we only care if a subscription has exipired
|
||||
if req.body? and req.body["expired_subscription_notification"]?
|
||||
recurlySubscription = req.body["expired_subscription_notification"].subscription
|
||||
SubscriptionHandler.recurlyCallback recurlySubscription, ->
|
||||
SubscriptionHandler.recurlyCallback recurlySubscription, (err)->
|
||||
return next(err) if err?
|
||||
res.sendStatus 200
|
||||
else
|
||||
res.sendStatus 200
|
||||
|
||||
renderUpgradeToAnnualPlanPage: (req, res)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
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
|
||||
renderUpgradeToAnnualPlanPage: (req, res, next)->
|
||||
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)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
{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)->
|
||||
processUpgradeToAnnualPlan: (req, res, next)->
|
||||
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)->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
return next(err) if err?
|
||||
SubscriptionHandler.extendTrial subscription, 14, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user._id, "error updating subscription"
|
||||
res.sendStatus 500
|
||||
res.send 500
|
||||
else
|
||||
res.sendStatus 200
|
||||
|
||||
extendTrial: (req, res)->
|
||||
AuthenticationController.getLoggedInUser req, (error, user) ->
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
SubscriptionHandler.extendTrial subscription, 14, (err)->
|
||||
if err?
|
||||
res.send 500
|
||||
else
|
||||
res.send 200
|
||||
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
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports =
|
||||
|
||||
webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
|
||||
webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
|
||||
webRouter.post '/user/subscription/billing-details/update', AuthenticationController.requireLogin(), SubscriptionController.updateBillingDetails
|
||||
|
||||
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -5,17 +5,16 @@ AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
|
||||
module.exports = TrackChangesController =
|
||||
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
||||
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -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,18 +25,18 @@ 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
|
||||
path = req.files.qqfile.path
|
||||
name = req.files.qqfile?.originalname
|
||||
path = req.files.qqfile?.path
|
||||
project_id = req.params.Project_id
|
||||
folder_id = req.query.folder_id
|
||||
if !name? or name.length == 0 or name.length > 150
|
||||
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,6 +8,7 @@ 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"
|
||||
@@ -15,20 +16,21 @@ settings = require "settings-sharelatex"
|
||||
module.exports = UserController =
|
||||
|
||||
deleteUser: (req, res)->
|
||||
user_id = req.session.user._id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
UserDeleter.deleteUser user_id, (err)->
|
||||
if !err?
|
||||
req.session?.destroy()
|
||||
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)
|
||||
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,9 +56,12 @@ 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
|
||||
AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name})
|
||||
return res.sendStatus 200
|
||||
else if newEmail.indexOf("@") == -1
|
||||
return res.sendStatus(400)
|
||||
@@ -73,7 +78,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 +86,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 +108,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
|
||||
|
||||
@@ -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
|
||||
@@ -38,6 +40,3 @@ module.exports = UserController =
|
||||
role: user.role
|
||||
institution: user.institution
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -22,14 +24,14 @@ module.exports =
|
||||
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
|
||||
@@ -53,11 +55,23 @@ module.exports =
|
||||
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"
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
return next(err) if err?
|
||||
res.render 'user/settings',
|
||||
title:'account_settings'
|
||||
user: user,
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
|
||||
crypto = require("crypto")
|
||||
EmailHandler = require("../Email/EmailHandler")
|
||||
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
|
||||
Analytics = require "../Analytics/AnalyticsManager"
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports = UserRegistrationHandler =
|
||||
@@ -62,6 +63,7 @@ module.exports = UserRegistrationHandler =
|
||||
cb() #this can be slow, just fire it off
|
||||
], (err)->
|
||||
logger.log user: user, "registered"
|
||||
Analytics.recordEvent user._id, "user-registered"
|
||||
callback(err, user)
|
||||
|
||||
registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) ->
|
||||
|
||||
@@ -55,11 +55,40 @@ module.exports = UserSessionsManager =
|
||||
UserSessionsManager._checkSessions(user, () ->)
|
||||
callback()
|
||||
|
||||
getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) ->
|
||||
exclude = _.map(exclude, UserSessionsManager._sessionKey)
|
||||
sessionSetKey = UserSessionsManager._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, [])
|
||||
rclient.mget sessionKeys, (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"
|
||||
|
||||
@@ -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}/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,28 +71,35 @@ 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: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"
|
||||
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
|
||||
isLive = !isDark and !isSmoke
|
||||
|
||||
if cdnAvailable and isLive
|
||||
if cdnAvailable and isLive and !cdnBlocked
|
||||
staticFilesBase = Settings.cdn?.web?.host
|
||||
else if darkCdnAvailable and isDark
|
||||
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 = {}
|
||||
|
||||
@@ -89,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)
|
||||
@@ -109,7 +127,7 @@ module.exports = (app, webRouter, apiRouter)->
|
||||
|
||||
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.settings = Settings
|
||||
next()
|
||||
|
||||
@@ -117,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)->
|
||||
@@ -125,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()
|
||||
|
||||
@@ -137,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!"
|
||||
@@ -161,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) ->
|
||||
@@ -173,11 +200,11 @@ 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()
|
||||
|
||||
@@ -187,11 +214,12 @@ module.exports = (app, webRouter, apiRouter)->
|
||||
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
|
||||
@@ -217,7 +245,7 @@ module.exports = (app, webRouter, apiRouter)->
|
||||
res.locals.nav[key] = _.clone(Settings.nav[key])
|
||||
res.locals.templates = Settings.templateLinks
|
||||
next()
|
||||
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
SystemMessageManager.getMessages (error, messages = []) ->
|
||||
res.locals.systemMessages = messages
|
||||
@@ -240,5 +268,3 @@ module.exports = (app, webRouter, apiRouter)->
|
||||
res.locals.moduleIncludes = Modules.moduleIncludes
|
||||
res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable
|
||||
next()
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ request = require("request")
|
||||
settings = require("settings-sharelatex")
|
||||
_ = require("underscore")
|
||||
logger = require("logger-sharelatex")
|
||||
URL = require("url")
|
||||
|
||||
currencyMappings = {
|
||||
"GB":"GBP"
|
||||
@@ -31,7 +32,7 @@ module.exports = GeoIpLookup =
|
||||
return callback(e)
|
||||
ip = ip.trim().split(" ")[0]
|
||||
opts =
|
||||
url: "#{settings.apis.geoIpLookup.url}/#{ip}"
|
||||
url: URL.resolve(settings.apis.geoIpLookup.url,ip)
|
||||
timeout: 1000
|
||||
json:true
|
||||
logger.log ip:ip, opts:opts, "getting geo ip details"
|
||||
|
||||
@@ -25,14 +25,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) ->
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
version = {
|
||||
"pdfjs": "1.6.210p1"
|
||||
"moment": "2.9.0"
|
||||
"ace": "1.2.5"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
version: version
|
||||
|
||||
lib: (name) ->
|
||||
if version[name]?
|
||||
return "#{name}-#{version[name]}"
|
||||
else
|
||||
return "#{name}"
|
||||
}
|
||||
@@ -21,6 +21,9 @@ cookieParser = require('cookie-parser')
|
||||
|
||||
sessionStore = new RedisStore(client:rclient)
|
||||
|
||||
passport = require('passport')
|
||||
LocalStrategy = require('passport-local').Strategy
|
||||
|
||||
Mongoose = require("./Mongoose")
|
||||
|
||||
oneDayInMilliseconds = 86400000
|
||||
@@ -32,6 +35,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)
|
||||
@@ -87,11 +91,26 @@ webRouter.use csrfProtection
|
||||
webRouter.use translations.expressMiddlewear
|
||||
webRouter.use translations.setLangBasedOnDomainMiddlewear
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
mongoose = require 'mongoose'
|
||||
Settings = require 'settings-sharelatex'
|
||||
|
||||
|
||||
Schema = mongoose.Schema
|
||||
ObjectId = Schema.ObjectId
|
||||
|
||||
ProjectInviteSchema = new Schema
|
||||
project_id: ObjectId
|
||||
from_user_id: ObjectId
|
||||
privilegeLevel: String
|
||||
# For existing users
|
||||
to_user_id: ObjectId
|
||||
# For non-existant users
|
||||
hashed_token: String
|
||||
email: String
|
||||
|
||||
EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30
|
||||
|
||||
ExpiryDate = () ->
|
||||
timestamp = new Date()
|
||||
timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS)
|
||||
return timestamp
|
||||
|
||||
|
||||
|
||||
ProjectInviteSchema = new Schema(
|
||||
{
|
||||
email: String
|
||||
token: String
|
||||
sendingUserId: ObjectId
|
||||
projectId: ObjectId
|
||||
privileges: String
|
||||
createdAt: {type: Date, default: Date.now}
|
||||
expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}}
|
||||
},
|
||||
{
|
||||
collection: 'projectInvites'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
|
||||
|
||||
|
||||
ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
|
||||
|
||||
mongoose.model 'ProjectInvite', ProjectInviteSchema
|
||||
exports.ProjectInvite = ProjectInvite
|
||||
exports.ProjectInviteSchema = ProjectInviteSchema
|
||||
exports.ProjectInviteSchema = ProjectInviteSchema
|
||||
exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS
|
||||
|
||||
@@ -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, default: true}
|
||||
}
|
||||
features : {
|
||||
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
|
||||
|
||||
@@ -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,6 +21,7 @@ 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")
|
||||
@@ -38,6 +38,7 @@ ContactRouter = require("./Features/Contacts/ContactRouter")
|
||||
ReferencesController = require('./Features/References/ReferencesController')
|
||||
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
|
||||
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
||||
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
@@ -51,7 +52,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
|
||||
|
||||
@@ -68,12 +70,13 @@ module.exports = class Router
|
||||
StaticPagesRouter.apply(webRouter, apiRouter)
|
||||
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
|
||||
@@ -85,6 +88,9 @@ 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
|
||||
|
||||
@@ -179,7 +185,7 @@ 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
|
||||
|
||||
# Deprecated in favour of /internal/project/:project_id but still used by versioning
|
||||
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
|
||||
@@ -249,14 +255,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({
|
||||
@@ -280,4 +299,4 @@ module.exports = class Router
|
||||
metrics.inc("client-side-error")
|
||||
res.sendStatus(204)
|
||||
|
||||
webRouter.get '*', ErrorController.notFound
|
||||
webRouter.get '*', ErrorController.notFound
|
||||
|
||||
@@ -22,7 +22,7 @@ block content
|
||||
ul.list-unstyled.text-center
|
||||
li
|
||||
i.fa.fa-fw.fa-book
|
||||
| #{translate("mendeley_integration")}
|
||||
| #{translate("syntax_checking")}
|
||||
.row.text-centered
|
||||
.col-md-12
|
||||
if user.betaProgram
|
||||
|
||||
@@ -8,6 +8,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||
window.similarproducts = true
|
||||
style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; }
|
||||
|
||||
|
||||
-if (typeof(gaExperiments) != "undefined")
|
||||
|!{gaExperiments}
|
||||
|
||||
@@ -46,83 +47,22 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||
- else
|
||||
script(type='text/javascript').
|
||||
window.ga = function() { console.log("would send to GA", arguments) };
|
||||
|
||||
// Countly Analytics
|
||||
if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token)
|
||||
script(type="text/javascript").
|
||||
var Countly = Countly || {};
|
||||
Countly.q = Countly.q || [];
|
||||
Countly.app_key = '#{settings.analytics.countly.token}';
|
||||
Countly.url = '#{settings.analytics.countly.server}';
|
||||
!{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' }
|
||||
|
||||
(function() {
|
||||
var cly = document.createElement('script'); cly.type = 'text/javascript';
|
||||
cly.async = true;
|
||||
//enter url of script here
|
||||
cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js';
|
||||
cly.onload = function(){Countly.init()};
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);
|
||||
})();
|
||||
|
||||
script(type="text/javascript")
|
||||
if (session && session.user)
|
||||
- var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : '');
|
||||
| Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]);
|
||||
|
||||
if (justRegistered)
|
||||
| Countly.q.push(['add_event',{ key: 'user-registered' }]);
|
||||
|
||||
if (justLoggedIn)
|
||||
| Countly.q.push(['add_event',{ key: 'user-logged-in' }]);
|
||||
|
||||
if (user && user.features)
|
||||
- featureFlagSet = false;
|
||||
|
||||
if user.features.hasOwnProperty('collaborators')
|
||||
| Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('compileGroup')
|
||||
| Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('compileTimeout')
|
||||
| Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('dropbox')
|
||||
| Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('github')
|
||||
| Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('references')
|
||||
| Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('templates')
|
||||
| Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('versioning')
|
||||
| Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
|
||||
if featureFlagSet
|
||||
| Countly.q.push(['userData.save'])
|
||||
|
||||
// End countly Analytics
|
||||
|
||||
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 && navigator.userAgent.indexOf("Googlebot") == -1) {
|
||||
window.location.search += '&'+noCdnKey;
|
||||
}
|
||||
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
|
||||
|
||||
script.
|
||||
window.sharelatex = {
|
||||
siteUrl: '#{settings.siteUrl}',
|
||||
@@ -171,23 +111,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(
|
||||
@@ -196,7 +140,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
|
||||
@@ -25,7 +25,7 @@ nav.navbar.navbar-default
|
||||
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))
|
||||
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)
|
||||
|
||||
@@ -3,7 +3,6 @@ extends ../layout
|
||||
block vars
|
||||
- var suppressNavbar = true
|
||||
- var suppressFooter = true
|
||||
- var suppressDefaultJs = true
|
||||
- var suppressSystemMessages = true
|
||||
|
||||
block content
|
||||
@@ -66,7 +65,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 +85,11 @@ block content
|
||||
.modal-footer
|
||||
button.btn.btn-info(ng-click="done()") #{translate("ok")}
|
||||
|
||||
script(src='/socket.io/socket.io.js')
|
||||
block requirejs
|
||||
script(type="text/javascript" src='/socket.io/socket.io.js')
|
||||
|
||||
//- don't use cdn for worker
|
||||
- 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.
|
||||
@@ -100,47 +103,43 @@ block content
|
||||
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-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
|
||||
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
|
||||
|
||||
- var aceWorkerPath = user.betaProgram ? buildJsPath(lib('ace'), {cdn:false,fingerprint:false}) : "" // don't use cdn for worker
|
||||
|
||||
script(type='text/javascript').
|
||||
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
|
||||
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')
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -41,11 +41,12 @@ aside.chat(
|
||||
}"
|
||||
)
|
||||
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}")
|
||||
p(
|
||||
mathjax,
|
||||
ng-repeat="content in message.contents track by $index"
|
||||
)
|
||||
span(ng-bind-html="content | linky:'_blank'")
|
||||
.message-content
|
||||
p(
|
||||
mathjax,
|
||||
ng-repeat="content in message.contents track by $index"
|
||||
)
|
||||
span(ng-bind-html="content | linky:'_blank'")
|
||||
|
||||
.new-message
|
||||
textarea(
|
||||
|
||||
@@ -36,6 +36,7 @@ div.full-size(
|
||||
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
|
||||
read-only="!permissions.write",
|
||||
on-ctrl-enter="recompileViaKey"
|
||||
syntax-validation="settings.syntaxValidation"
|
||||
)
|
||||
|
||||
.ui-layout-east
|
||||
|
||||
@@ -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,120 +1,380 @@
|
||||
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"
|
||||
div(ng-if="!shouldABTestHeaderLabels")
|
||||
header.toolbar.toolbar-header(
|
||||
ng-cloak,
|
||||
ng-hide="state.loading"
|
||||
)
|
||||
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
|
||||
span(ng-controller="PdfViewToggleController")
|
||||
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,
|
||||
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
|
||||
tooltip="PDF",
|
||||
href="/project"
|
||||
tooltip="#{translate('back_to_projects')}",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="togglePdfView()",
|
||||
ng-class="{ 'active': ui.view == 'pdf' }"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
i.fa.fa-file-pdf-o
|
||||
|
||||
.toolbar-center.project-name(ng-controller="ProjectNameController")
|
||||
span.name(
|
||||
ng-dblclick="!permissions.admin || startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
) {{ project.name }}
|
||||
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="inputs.name",
|
||||
ng-show="state.renaming",
|
||||
on-enter="finishRenaming()",
|
||||
ng-blur="finishRenaming()",
|
||||
select-name-when="state.renaming"
|
||||
)
|
||||
|
||||
a.rename(
|
||||
ng-if="permissions.admin",
|
||||
href='#',
|
||||
tooltip-placement="bottom",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
)
|
||||
i.fa.fa-pencil
|
||||
|
||||
.toolbar-right
|
||||
span.online-users(
|
||||
ng-show="onlineUsersArray.length > 0"
|
||||
ng-controller="OnlineUsersController"
|
||||
)
|
||||
span(ng-if="onlineUsersArray.length < 4")
|
||||
span.online-user(
|
||||
ng-repeat="user in onlineUsersArray",
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
|
||||
popover="{{ user.name }}"
|
||||
popover-placement="bottom"
|
||||
popover-append-to-body="true"
|
||||
popover-trigger="mouseenter"
|
||||
ng-click="gotoUser(user)"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
|
||||
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
||||
span.online-user.online-user-multi(
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('connected_users')}",
|
||||
tooltip-placement="left"
|
||||
)
|
||||
strong {{ onlineUsersArray.length }}
|
||||
i.fa.fa-fw.fa-user
|
||||
ul.dropdown-menu.pull-right
|
||||
li.dropdown-header #{translate('connected_users')}
|
||||
li(ng-repeat="user in onlineUsersArray")
|
||||
a(href, ng-click="gotoUser(user)")
|
||||
span.online-user(
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
| {{ user.name }}
|
||||
|
||||
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="permissions.admin",
|
||||
tooltip="#{translate('share')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-click="openShareProjectModal()",
|
||||
ng-controller="ShareController"
|
||||
)
|
||||
i.fa.fa-fw.fa-group
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleTrackChanges()",
|
||||
ng-class="{ active: (ui.view == 'track-changes') }"
|
||||
tooltip="#{translate('recent_changes')}",
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-fw.fa-history
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
tooltip="#{translate('chat')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-class="{ active: ui.chatOpen }",
|
||||
ng-click="toggleChat()",
|
||||
ng-controller="ChatButtonController",
|
||||
ng-show="!anonymous"
|
||||
)
|
||||
i.fa.fa-fw.fa-comment(
|
||||
ng-class="{ 'bounce': unreadMessages > 0 }"
|
||||
i.fa.fa-fw.fa-level-up
|
||||
|
||||
span(ng-controller="PdfViewToggleController")
|
||||
a(
|
||||
href,
|
||||
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
|
||||
tooltip="PDF",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="togglePdfView()",
|
||||
ng-class="{ 'active': ui.view == 'pdf' }"
|
||||
)
|
||||
span.label.label-info(
|
||||
ng-show="unreadMessages > 0"
|
||||
) {{ unreadMessages }}
|
||||
i.fa.fa-file-pdf-o
|
||||
|
||||
.toolbar-center.project-name(ng-controller="ProjectNameController")
|
||||
span.name(
|
||||
ng-dblclick="!permissions.admin || startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
) {{ project.name }}
|
||||
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="inputs.name",
|
||||
ng-show="state.renaming",
|
||||
on-enter="finishRenaming()",
|
||||
ng-blur="finishRenaming()",
|
||||
select-name-when="state.renaming"
|
||||
)
|
||||
|
||||
a.rename(
|
||||
ng-if="permissions.admin",
|
||||
href='#',
|
||||
tooltip-placement="bottom",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
)
|
||||
i.fa.fa-pencil
|
||||
|
||||
.toolbar-right
|
||||
span.online-users(
|
||||
ng-show="onlineUsersArray.length > 0"
|
||||
ng-controller="OnlineUsersController"
|
||||
)
|
||||
span(ng-if="onlineUsersArray.length < 4")
|
||||
span.online-user(
|
||||
ng-repeat="user in onlineUsersArray",
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
|
||||
popover="{{ user.name }}"
|
||||
popover-placement="bottom"
|
||||
popover-append-to-body="true"
|
||||
popover-trigger="mouseenter"
|
||||
ng-click="gotoUser(user)"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
|
||||
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
||||
span.online-user.online-user-multi(
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('connected_users')}",
|
||||
tooltip-placement="left"
|
||||
)
|
||||
strong {{ onlineUsersArray.length }}
|
||||
i.fa.fa-fw.fa-user
|
||||
ul.dropdown-menu.pull-right
|
||||
li.dropdown-header #{translate('connected_users')}
|
||||
li(ng-repeat="user in onlineUsersArray")
|
||||
a(href, ng-click="gotoUser(user)")
|
||||
span.online-user(
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
| {{ user.name }}
|
||||
|
||||
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="permissions.admin",
|
||||
tooltip="#{translate('share')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-click="openShareProjectModal()",
|
||||
ng-controller="ShareController",
|
||||
)
|
||||
i.fa.fa-fw.fa-group
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleHistory()",
|
||||
ng-class="{ active: (ui.view == 'history') }"
|
||||
tooltip="#{translate('recent_changes')}",
|
||||
tooltip-placement="bottom",
|
||||
)
|
||||
i.fa.fa-fw.fa-history
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
tooltip="#{translate('chat')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-class="{ active: ui.chatOpen }",
|
||||
ng-click="toggleChat()",
|
||||
ng-controller="ChatButtonController",
|
||||
ng-show="!anonymous",
|
||||
)
|
||||
i.fa.fa-fw.fa-comment(
|
||||
ng-class="{ 'bounce': unreadMessages > 0 }"
|
||||
)
|
||||
span.label.label-info(
|
||||
ng-show="unreadMessages > 0"
|
||||
) {{ unreadMessages }}
|
||||
|
||||
div(ng-if="shouldABTestHeaderLabels")
|
||||
div(sixpack-switch="editor-header")
|
||||
header.toolbar.toolbar-header(
|
||||
ng-cloak,
|
||||
ng-hide="state.loading"
|
||||
sixpack-default
|
||||
)
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="ui.leftMenuShown = true; trackABTestConversion('menu');"
|
||||
tooltip='#{translate("menu")}',
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
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
|
||||
|
||||
span(ng-controller="PdfViewToggleController")
|
||||
a(
|
||||
href,
|
||||
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
|
||||
tooltip="PDF",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="togglePdfView()",
|
||||
ng-class="{ 'active': ui.view == 'pdf' }"
|
||||
)
|
||||
i.fa.fa-file-pdf-o
|
||||
|
||||
.toolbar-center.project-name(ng-controller="ProjectNameController")
|
||||
span.name(
|
||||
ng-dblclick="!permissions.admin || startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
) {{ project.name }}
|
||||
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="inputs.name",
|
||||
ng-show="state.renaming",
|
||||
on-enter="finishRenaming()",
|
||||
ng-blur="finishRenaming()",
|
||||
select-name-when="state.renaming"
|
||||
)
|
||||
|
||||
a.rename(
|
||||
ng-if="permissions.admin",
|
||||
href='#',
|
||||
tooltip-placement="bottom",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
)
|
||||
i.fa.fa-pencil
|
||||
|
||||
.toolbar-right
|
||||
span.online-users(
|
||||
ng-show="onlineUsersArray.length > 0"
|
||||
ng-controller="OnlineUsersController"
|
||||
)
|
||||
span(ng-if="onlineUsersArray.length < 4")
|
||||
span.online-user(
|
||||
ng-repeat="user in onlineUsersArray",
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
|
||||
popover="{{ user.name }}"
|
||||
popover-placement="bottom"
|
||||
popover-append-to-body="true"
|
||||
popover-trigger="mouseenter"
|
||||
ng-click="gotoUser(user)"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
|
||||
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
||||
span.online-user.online-user-multi(
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('connected_users')}",
|
||||
tooltip-placement="left"
|
||||
)
|
||||
strong {{ onlineUsersArray.length }}
|
||||
i.fa.fa-fw.fa-user
|
||||
ul.dropdown-menu.pull-right
|
||||
li.dropdown-header #{translate('connected_users')}
|
||||
li(ng-repeat="user in onlineUsersArray")
|
||||
a(href, ng-click="gotoUser(user)")
|
||||
span.online-user(
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
| {{ user.name }}
|
||||
|
||||
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="permissions.admin",
|
||||
tooltip="#{translate('share')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-click="openShareProjectModal(); trackABTestConversion('share');",
|
||||
ng-controller="ShareController",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-group
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleHistory(); trackABTestConversion('history');",
|
||||
ng-class="{ active: (ui.view == 'history') }"
|
||||
tooltip="#{translate('recent_changes')}",
|
||||
tooltip-placement="bottom",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-history
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
tooltip="#{translate('chat')}",
|
||||
tooltip-placement="bottom",
|
||||
ng-class="{ active: ui.chatOpen }",
|
||||
ng-click="toggleChat(); trackABTestConversion('chat');",
|
||||
ng-controller="ChatButtonController",
|
||||
ng-show="!anonymous",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-comment(
|
||||
ng-class="{ 'bounce': unreadMessages > 0 }"
|
||||
)
|
||||
span.label.label-info(
|
||||
ng-show="unreadMessages > 0"
|
||||
) {{ unreadMessages }}
|
||||
|
||||
header.toolbar.toolbar-header.toolbar-with-labels(
|
||||
ng-cloak,
|
||||
ng-hide="state.loading"
|
||||
sixpack-when="labels"
|
||||
)
|
||||
.toolbar-left
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="ui.leftMenuShown = true; trackABTestConversion('menu');",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
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,
|
||||
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
|
||||
tooltip="PDF",
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="togglePdfView()",
|
||||
ng-class="{ 'active': ui.view == 'pdf' }"
|
||||
)
|
||||
i.fa.fa-file-pdf-o
|
||||
|
||||
.toolbar-center.project-name(ng-controller="ProjectNameController")
|
||||
span.name(
|
||||
ng-dblclick="!permissions.admin || startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
) {{ project.name }}
|
||||
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="inputs.name",
|
||||
ng-show="state.renaming",
|
||||
on-enter="finishRenaming()",
|
||||
ng-blur="finishRenaming()",
|
||||
select-name-when="state.renaming"
|
||||
)
|
||||
|
||||
a.rename(
|
||||
ng-if="permissions.admin",
|
||||
href='#',
|
||||
tooltip-placement="bottom",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
)
|
||||
i.fa.fa-pencil
|
||||
|
||||
.toolbar-right
|
||||
span.online-users(
|
||||
ng-show="onlineUsersArray.length > 0"
|
||||
ng-controller="OnlineUsersController"
|
||||
)
|
||||
span(ng-if="onlineUsersArray.length < 4")
|
||||
span.online-user(
|
||||
ng-repeat="user in onlineUsersArray",
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
|
||||
popover="{{ user.name }}"
|
||||
popover-placement="bottom"
|
||||
popover-append-to-body="true"
|
||||
popover-trigger="mouseenter"
|
||||
ng-click="gotoUser(user)"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
|
||||
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
|
||||
span.online-user.online-user-multi(
|
||||
dropdown-toggle,
|
||||
tooltip="#{translate('connected_users')}",
|
||||
tooltip-placement="left"
|
||||
)
|
||||
strong {{ onlineUsersArray.length }}
|
||||
i.fa.fa-fw.fa-user
|
||||
ul.dropdown-menu.pull-right
|
||||
li.dropdown-header #{translate('connected_users')}
|
||||
li(ng-repeat="user in onlineUsersArray")
|
||||
a(href, ng-click="gotoUser(user)")
|
||||
span.online-user(
|
||||
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
|
||||
) {{ user.name.slice(0,1) }}
|
||||
| {{ user.name }}
|
||||
|
||||
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="permissions.admin",
|
||||
ng-click="openShareProjectModal(); trackABTestConversion('share');",
|
||||
ng-controller="ShareController",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-group
|
||||
p.toolbar-label #{translate("share")}
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleHistory(); trackABTestConversion('history');",
|
||||
ng-class="{ active: (ui.view == 'history') }",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-history
|
||||
p.toolbar-label #{translate("history")}
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-class="{ active: ui.chatOpen }",
|
||||
ng-click="toggleChat(); trackABTestConversion('chat');",
|
||||
ng-controller="ChatButtonController",
|
||||
ng-show="!anonymous",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-comment(
|
||||
ng-class="{ 'bounce': unreadMessages > 0 }"
|
||||
)
|
||||
span.label.label-info(
|
||||
ng-show="unreadMessages > 0"
|
||||
) {{ unreadMessages }}
|
||||
p.toolbar-label #{translate("chat")}
|
||||
+38
-26
@@ -1,5 +1,5 @@
|
||||
div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||
span(ng-controller="TrackChangesPremiumPopup")
|
||||
div#history(ng-show="ui.view == 'history'")
|
||||
span(ng-controller="HistoryPremiumPopup")
|
||||
.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"})}
|
||||
@@ -33,29 +33,29 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('track-changes')"
|
||||
ng-click="startFreeTrial('history')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
|
||||
.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 +65,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,57 +108,69 @@ 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"
|
||||
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-click="restoreDeletedDoc()",
|
||||
ng-disabled="history.diff.restoreInProgress"
|
||||
) #{translate("restore")}
|
||||
.loading-panel(ng-show="trackChanges.diff.loading")
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
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(
|
||||
href,
|
||||
ng-click="backToEditorAfterRestore()",
|
||||
) #{translate("file_restored_back_to_editor_btn")}
|
||||
|
||||
.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,15 @@ aside#left-menu.full-size(
|
||||
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
|
||||
)
|
||||
|
||||
if (user.betaProgram)
|
||||
.form-controls
|
||||
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 +206,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")} :
|
||||
|
||||
@@ -25,7 +25,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
dropdown-toggle
|
||||
)
|
||||
span.caret
|
||||
ul.dropdown-menu.dropdown-menu-right
|
||||
ul.dropdown-menu.dropdown-menu-left
|
||||
li.dropdown-header #{translate("compile_mode")}
|
||||
li
|
||||
a(href, ng-click="draft = false")
|
||||
@@ -36,6 +36,19 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
i.fa.fa-fw(ng-class="{'fa-check': draft}")
|
||||
| #{translate("fast")}
|
||||
span.subdued [draft]
|
||||
li.dropdown-header #{translate("compile_time_checks")}
|
||||
li
|
||||
a(href, ng-click="stop_on_validation_error = true")
|
||||
i.fa.fa-fw(ng-class="{'fa-check': stop_on_validation_error}")
|
||||
| #{translate("stop_on_validation_error")}
|
||||
li
|
||||
a(href, ng-click="stop_on_validation_error = false")
|
||||
i.fa.fa-fw(ng-class="{'fa-check': !stop_on_validation_error}")
|
||||
| #{translate("ignore_validation_errors")}
|
||||
li
|
||||
a(href, ng-click="recompile({check:true})")
|
||||
i.fa.fa-fw()
|
||||
| #{translate("run_syntax_check_now")}
|
||||
a(
|
||||
href
|
||||
ng-click="stop()"
|
||||
@@ -100,6 +113,23 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
strong #{translate("compile_error")}.
|
||||
span #{translate("generic_failed_compile_message")}.
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.failedCheck")
|
||||
strong #{translate("failed_compile_check")}.
|
||||
p
|
||||
p.text-center(ng-show="!check")
|
||||
a.text-info(
|
||||
href,
|
||||
ng-disabled="pdf.compiling",
|
||||
ng-click="recompile({try:true})"
|
||||
) #{translate("failed_compile_check_try")}
|
||||
|  #{translate("failed_compile_option_or")} 
|
||||
a.text-info(
|
||||
href,
|
||||
ng-disabled="pdf.compiling",
|
||||
ng-click="recompile({force:true})"
|
||||
) #{translate("failed_compile_check_ignore")}
|
||||
| .
|
||||
|
||||
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
|
||||
.alert(
|
||||
ng-class="{\
|
||||
@@ -115,7 +145,8 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
|
|
||||
span(ng-show="entry.file") {{ entry.file }}
|
||||
span(ng-show="entry.line") , line {{ entry.line }}
|
||||
p.entry-message(ng-show="entry.message") {{ entry.message }}
|
||||
p.entry-message(ng-show="entry.message")
|
||||
| {{ entry.type }} {{ entry.message }}
|
||||
.card.card-hint(
|
||||
ng-if="entry.humanReadableHint"
|
||||
stop-propagation="click"
|
||||
@@ -126,7 +157,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||
ng-show="entry.humanReadableHint",
|
||||
ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
|
||||
.card-hint-actions.clearfix
|
||||
.card-hint-ext-link(ng-if="wikiEnabled")
|
||||
.card-hint-ext-link(ng-if="wikiEnabled && entry.extraInfoURL")
|
||||
a(
|
||||
ng-href="{{ entry.extraInfoURL }}",
|
||||
ng-click="trackLogHintsLearnMore()"
|
||||
@@ -198,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,
|
||||
@@ -313,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
|
||||
|
||||
@@ -27,12 +27,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||
) #{translate("make_private")}
|
||||
.row.project-member
|
||||
.col-xs-8 {{ project.owner.email }}
|
||||
.text-right(
|
||||
.text-left(
|
||||
ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
|
||||
) #{translate("owner")}
|
||||
.row.project-member(ng-repeat="member in project.members")
|
||||
.col-xs-8 {{ member.email }}
|
||||
.col-xs-3.text-right
|
||||
.col-xs-3.text-left
|
||||
span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
|
||||
span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
|
||||
.col-xs-1
|
||||
@@ -43,6 +43,23 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||
ng-click="removeMember(member)"
|
||||
)
|
||||
i.fa.fa-times
|
||||
.row.project-invite(ng-repeat="invite in project.invites")
|
||||
.col-xs-8 {{ invite.email }}
|
||||
div.small
|
||||
| #{translate("invite_not_accepted")}.
|
||||
a(href="#", ng-click="resendInvite(invite, $event)") #{translate("resend")}
|
||||
.col-xs-3.text-left
|
||||
// todo: get invite privileges
|
||||
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
|
||||
span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")}
|
||||
.col-xs-1
|
||||
a(
|
||||
href
|
||||
tooltip="#{translate('revoke_invite')}"
|
||||
tooltip-placement="bottom"
|
||||
ng-click="revokeInvite(invite)"
|
||||
)
|
||||
i.fa.fa-times
|
||||
.row.invite-controls
|
||||
form(ng-show="canAddCollaborators")
|
||||
.small #{translate("share_with_your_collabs")}
|
||||
@@ -78,6 +95,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||
button.btn.btn-info(
|
||||
type="submit"
|
||||
ng-mousedown="addMembers()"
|
||||
ng-keyup="$event.keyCode == 13 ? addMembers() : null"
|
||||
) #{translate("share")}
|
||||
div(ng-hide="canAddCollaborators")
|
||||
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
|
||||
@@ -123,7 +141,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||
.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")}
|
||||
button.btn.btn-primary(
|
||||
button.btn.btn-default(
|
||||
ng-click="done()"
|
||||
) #{translate("close")}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
extends ../../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
.card.project-invite-invalid
|
||||
.page-header.text-centered
|
||||
h1 #{translate("invite_not_valid")}
|
||||
.row.text-center
|
||||
.col-md-12
|
||||
p
|
||||
| #{translate("invite_not_valid_description")}.
|
||||
.row.text-center.actions
|
||||
.col-md-12
|
||||
a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
extends ../../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
.card.project-invite-accept
|
||||
.page-header.text-centered
|
||||
h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})}
|
||||
em
|
||||
span.project-name #{project.name}
|
||||
.row.text-center
|
||||
.col-md-12
|
||||
p
|
||||
| #{translate("accepting_invite_as")}
|
||||
em #{user.email}
|
||||
.row
|
||||
.col-md-12
|
||||
form.form(
|
||||
name="acceptForm",
|
||||
method="POST",
|
||||
action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
|
||||
)
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
input(name='token', type='hidden', value="#{invite.token}")
|
||||
.form-group.text-center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
| #{translate("join_project")}
|
||||
.form-group.text-center
|
||||
|
||||
@@ -23,7 +23,7 @@ block content
|
||||
.container
|
||||
|
||||
.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 +31,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
|
||||
@@ -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,12 +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
|
||||
span(ng-bind-html="unreadNotification.html")
|
||||
button(ng-click="dismiss(unreadNotification)").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController")
|
||||
div.notification_inner
|
||||
.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(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,202 +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(dropdown)
|
||||
a.btn.btn-success.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
| Create First Project
|
||||
style.
|
||||
.dropdown{text-align:center;}
|
||||
.button, .dropdown-menu{margin:2px auto}
|
||||
.dropdown-menu{width:200px; left:50%; margin-left:-100px;}
|
||||
|
||||
ul.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
|
||||
|
||||
|
||||
@@ -155,6 +155,6 @@
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
script.
|
||||
window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
|
||||
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ block content
|
||||
.container(ng-controller="UserSubscriptionController")
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
if saved_billing_details
|
||||
.alert.alert-success
|
||||
i.fa.fa-check
|
||||
|
|
||||
| #{translate("your_billing_details_were_saved")}
|
||||
.card(ng-if="view == 'overview'")
|
||||
.page-header
|
||||
h1 #{translate("your_subscription")}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
- locals.supressDefaultJs = true
|
||||
script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath)
|
||||
script(src=buildJsPath('libs/recurly.min.js'))
|
||||
block scripts
|
||||
script(src=buildJsPath('libs/recurly.min.js', {fingerprint:false}))
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
@@ -19,7 +18,7 @@ block content
|
||||
Recurly.config(!{recurlyConfig})
|
||||
Recurly.buildBillingInfoUpdateForm({
|
||||
target : "#billingDetailsForm",
|
||||
successURL : "#{successURL}?_csrf=#{csrfToken}",
|
||||
successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails",
|
||||
signature : "!{signature}",
|
||||
accountCode : "#{user.id}"
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ extends ../../layout
|
||||
block scripts
|
||||
script(type='text/javascript').
|
||||
window.group_subscription_id = '#{group_subscription_id}'
|
||||
window.has_personal_subscription = '#{has_personal_subscription}'
|
||||
window.has_personal_subscription = #{has_personal_subscription}
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extends ../layout
|
||||
block scripts
|
||||
|
||||
block scripts
|
||||
script(src="https://js.recurly.com/v3/recurly.js")
|
||||
|
||||
script(type='text/javascript').
|
||||
@@ -11,10 +11,6 @@ block scripts
|
||||
window.couponCode = "#{couponCode}"
|
||||
|
||||
block content
|
||||
- locals.supressDefaultJs = true
|
||||
script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath)
|
||||
|
||||
|
||||
.content.content-alt
|
||||
.container(ng-controller="NewSubscriptionController" ng-cloak)
|
||||
.row.card-group
|
||||
@@ -51,226 +47,218 @@ block content
|
||||
div(ng-if="normalPrice")
|
||||
span.small Normally {{price.currency.symbol}}{{normalPrice}}
|
||||
.row
|
||||
.col-md-12
|
||||
form(ng-show="planName")
|
||||
|
||||
div()
|
||||
.col-md-12()
|
||||
form(
|
||||
ng-if="planName"
|
||||
name="simpleCCForm"
|
||||
novalidate
|
||||
)
|
||||
div.payment-method-toggle
|
||||
a.payment-method-toggle-switch(
|
||||
href
|
||||
ng-click="setPaymentMethod('credit_card');"
|
||||
ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
|
||||
)
|
||||
i.fa.fa-cc-mastercard.fa-2x
|
||||
span
|
||||
i.fa.fa-cc-visa.fa-2x
|
||||
span
|
||||
i.fa.fa-cc-amex.fa-2x
|
||||
a.payment-method-toggle-switch(
|
||||
href
|
||||
ng-click="setPaymentMethod('paypal');"
|
||||
ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
|
||||
)
|
||||
i.fa.fa-cc-paypal.fa-2x
|
||||
|
||||
.alert.alert-warning.small(ng-show="genericError")
|
||||
strong {{genericError}}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.form-group
|
||||
.row
|
||||
.col-md-6
|
||||
label.radio-inline
|
||||
input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
|
||||
i.fa.fa-cc-mastercard.fa-3x
|
||||
span
|
||||
i.fa.fa-cc-visa.fa-3x
|
||||
.col-md-6
|
||||
label.radio-inline
|
||||
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
|
||||
i.fa.fa-cc-paypal.fa-3x
|
||||
div(ng-if="paymentMethod.value === 'credit_card'")
|
||||
.row
|
||||
.col-xs-6
|
||||
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
|
||||
label(for="first-name") #{translate('first_name')}
|
||||
input#first-name.form-control(
|
||||
type="text"
|
||||
maxlength='255'
|
||||
data-recurly="first_name"
|
||||
name="firstName"
|
||||
ng-model="data.first_name"
|
||||
required
|
||||
)
|
||||
span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }}
|
||||
.col-xs-6
|
||||
.form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
|
||||
label(for="last-name") #{translate('last_name')}
|
||||
input#last-name.form-control(
|
||||
type="text"
|
||||
maxlength='255'
|
||||
data-recurly="last_name"
|
||||
name="lastName"
|
||||
ng-model="data.last_name"
|
||||
required
|
||||
)
|
||||
span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }}
|
||||
|
||||
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''")
|
||||
label(for="card-no") #{translate("credit_card_number")}
|
||||
input#card-no.form-control(
|
||||
type="text"
|
||||
ng-model="data.number"
|
||||
name="ccNumber"
|
||||
ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;"
|
||||
ng-blur="validateCardNumber();"
|
||||
required
|
||||
cc-format-card-number
|
||||
)
|
||||
span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
|
||||
|
||||
.row
|
||||
.col-xs-6
|
||||
.form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''")
|
||||
label #{translate("expiry")}
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="data.mmYY"
|
||||
name="expiry"
|
||||
placeholder="MM / YY"
|
||||
ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;"
|
||||
ng-blur="updateExpiry(); validateExpiry()"
|
||||
required
|
||||
cc-format-expiry
|
||||
)
|
||||
span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
|
||||
|
||||
.alert.alert-warning.small(ng-show="genericError")
|
||||
strong {{genericError}}
|
||||
.col-xs-6
|
||||
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''")
|
||||
label #{translate("security_code")}
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="data.cvv"
|
||||
ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;"
|
||||
ng-blur="validateCvv()"
|
||||
name="cvv"
|
||||
required
|
||||
cc-format-sec-code
|
||||
)
|
||||
.form-control-feedback
|
||||
a.form-helper(
|
||||
href
|
||||
tabindex="-1"
|
||||
tooltip-template="'cvv-tooltip-tpl.html'"
|
||||
tooltip-trigger="mouseenter"
|
||||
tooltip-append-to-body="true"
|
||||
) ?
|
||||
span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
|
||||
|
||||
span(ng-hide="paymentMethod == 'paypal'")
|
||||
.row
|
||||
.col-md-12
|
||||
.form-group
|
||||
div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
|
||||
div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
|
||||
input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
|
||||
.col-md-3
|
||||
.form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
|
||||
input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
|
||||
.row
|
||||
.col-md-12
|
||||
div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
|
||||
.row
|
||||
.col-md-3
|
||||
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
|
||||
select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
|
||||
option(value="", disabled, selected) Month
|
||||
option(value="01") 01
|
||||
option(value="02") 02
|
||||
option(value="03") 03
|
||||
option(value="04") 04
|
||||
option(value="05") 05
|
||||
option(value="06") 06
|
||||
option(value="07") 07
|
||||
option(value="08") 08
|
||||
option(value="09") 09
|
||||
option(value="10") 10
|
||||
option(value="11") 11
|
||||
option(value="12") 12
|
||||
.col-md-4
|
||||
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
|
||||
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
|
||||
option(value="", disabled, selected) Year
|
||||
option(value="2016") 2016
|
||||
option(value="2017") 2017
|
||||
option(value="2018") 2018
|
||||
option(value="2019") 2019
|
||||
option(value="2020") 2020
|
||||
option(value="2021") 2021
|
||||
option(value="2022") 2022
|
||||
option(value="2023") 2023
|
||||
option(value="2024") 2024
|
||||
option(value="2025") 2025
|
||||
option(value="2026") 2026
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
|
||||
input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
|
||||
.col-md-6
|
||||
.form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
|
||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
|
||||
hr
|
||||
.row
|
||||
.col-md-12
|
||||
.form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
|
||||
label #{translate("billing_address")}
|
||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
|
||||
.form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
|
||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
|
||||
.form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
|
||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
|
||||
.row
|
||||
.col-md-7
|
||||
.form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
|
||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
|
||||
.col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
|
||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
|
||||
.row
|
||||
.col-md-7
|
||||
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
|
||||
select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
|
||||
|
||||
div
|
||||
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
|
||||
label(for="country") #{translate('country')}
|
||||
select#country.form-control(
|
||||
data-recurly="country"
|
||||
ng-model="data.country"
|
||||
name="country"
|
||||
ng-change="updateCountry()"
|
||||
required
|
||||
)
|
||||
mixin countries_options()
|
||||
.row
|
||||
.col-md-8
|
||||
if (showCouponField)
|
||||
.form-group
|
||||
input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
|
||||
.row
|
||||
.col-md-8
|
||||
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
|
||||
|
||||
if (showVatField)
|
||||
.form-group
|
||||
input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
|
||||
.row
|
||||
.col-xs-7
|
||||
.form-group
|
||||
button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
|
||||
|
||||
.col-xs-3.pricingBreakdown
|
||||
div(ng-if="price.next.subtotal != price.next.total") Subtotal
|
||||
div(ng-if="price.next.tax!='0.00'") Tax
|
||||
div
|
||||
strong Total
|
||||
.col-xs-2
|
||||
div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
|
||||
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
|
||||
div.pull-right
|
||||
strong {{price.currency.symbol}}{{price.next.total}}
|
||||
label(for="vat-no") #{translate('vat_number')}
|
||||
input#vat-no.form-control(
|
||||
type="text"
|
||||
ng-blur="applyVatNumber()"
|
||||
ng-model="data.vat_number"
|
||||
)
|
||||
if (showCouponField)
|
||||
.form-group
|
||||
label(for="coupon-code") #{translate('coupon_code')}
|
||||
input#coupon-code.form-control(
|
||||
type="text"
|
||||
ng-blur="applyCoupon()"
|
||||
ng-model="data.coupon"
|
||||
)
|
||||
|
||||
p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")}
|
||||
|
||||
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
|
||||
hr.thin
|
||||
span Total:
|
||||
strong {{price.currency.symbol}}{{price.next.total}}
|
||||
span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
|
||||
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
|
||||
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
||||
hr.thin
|
||||
|
||||
span(sixpack-switch="payment-left-menu-bottom")
|
||||
div.payment-submit
|
||||
button.btn.btn-success.btn-block(
|
||||
ng-click="submit()"
|
||||
ng-disabled="processing || !isFormValid(simpleCCForm);"
|
||||
)
|
||||
span(ng-show="processing")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
|
|
||||
| {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
|
||||
|
||||
.col-md-3.col-md-pull-4(sixpack-default)
|
||||
if showStudentPlan == 'true'
|
||||
a.btn-primary.btn.plansPageStudentLink(
|
||||
href,
|
||||
ng-click="switchToStudent()"
|
||||
) #{translate("half_price_student")}
|
||||
|
||||
.card.card-first
|
||||
.paymentPageFeatures
|
||||
h3 #{translate("unlimited_projects")}
|
||||
p #{translate("create_unlimited_projects")}
|
||||
.col-md-3.col-md-pull-4
|
||||
if showStudentPlan == 'true'
|
||||
a.btn-primary.btn.plansPageStudentLink(
|
||||
href,
|
||||
ng-click="switchToStudent()"
|
||||
) #{translate("half_price_student")}
|
||||
|
||||
.card.card-first
|
||||
.paymentPageFeatures
|
||||
h3 #{translate("unlimited_projects")}
|
||||
p #{translate("create_unlimited_projects")}
|
||||
|
||||
h3
|
||||
if plan.features.collaborators == -1
|
||||
- var collaboratorCount = 'Unlimited'
|
||||
else
|
||||
- var collaboratorCount = plan.features.collaborators
|
||||
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
|
||||
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
|
||||
|
||||
h3
|
||||
if plan.features.collaborators == -1
|
||||
- var collaboratorCount = 'Unlimited'
|
||||
else
|
||||
- var collaboratorCount = plan.features.collaborators
|
||||
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
|
||||
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
|
||||
|
||||
h3 #{translate("full_doc_history")}
|
||||
p #{translate("see_what_has_been")}
|
||||
span.added #{translate("added")}
|
||||
| #{translate("and")}
|
||||
span.removed #{translate("removed")}.
|
||||
| #{translate("restore_to_any_older_version")}.
|
||||
|
||||
h3 #{translate("sync_to_dropbox")}
|
||||
p
|
||||
| #{translate("acces_work_from_anywhere")}.
|
||||
| #{translate("work_offline_and_sync_with_dropbox")}.
|
||||
|
||||
hr
|
||||
|
||||
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
|
||||
hr
|
||||
span
|
||||
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
|
||||
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
|
||||
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
|
||||
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
|
||||
.col-md-3.col-md-pull-4(sixpack-when="bolder")
|
||||
if showStudentPlan == 'true'
|
||||
a.btn-primary.btn.plansPageStudentLink(
|
||||
href,
|
||||
ng-click="switchToStudent()"
|
||||
) #{translate("half_price_student")}
|
||||
|
||||
.card.card-first
|
||||
.paymentPageFeatures
|
||||
.page-header
|
||||
h2 #{translate("features")}
|
||||
h3
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
h3 #{translate("full_doc_history")}
|
||||
p #{translate("see_what_has_been")}
|
||||
span.added #{translate("added")}
|
||||
| #{translate("and")}
|
||||
span.removed #{translate("removed")}.
|
||||
| #{translate("restore_to_any_older_version")}.
|
||||
|
||||
h3
|
||||
i.fa.fa-check
|
||||
if plan.features.collaborators == -1
|
||||
- var collaboratorCount = 'Unlimited'
|
||||
else
|
||||
- var collaboratorCount = plan.features.collaborators
|
||||
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
|
||||
h3 #{translate("sync_to_dropbox")}
|
||||
p
|
||||
| #{translate("acces_work_from_anywhere")}.
|
||||
| #{translate("work_offline_and_sync_with_dropbox")}.
|
||||
|
||||
h3
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
hr
|
||||
|
||||
h3
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
h3
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
h3
|
||||
i.fa.fa-check
|
||||
| #{translate("Compile Larger Projects")}
|
||||
hr
|
||||
|
||||
h2.text-center 30 Day Guarantee
|
||||
hr
|
||||
span
|
||||
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
|
||||
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
|
||||
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
|
||||
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
|
||||
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
|
||||
hr
|
||||
span
|
||||
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
|
||||
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
|
||||
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
|
||||
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
|
||||
|
||||
|
||||
script(type="text/javascript").
|
||||
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
|
||||
|
||||
script(
|
||||
type="text/ng-template"
|
||||
id="cvv-tooltip-tpl.html"
|
||||
)
|
||||
p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card.
|
||||
p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card.
|
||||
|
||||
mixin countries_options()
|
||||
option(value='', disabled, selected) #{translate("country")}
|
||||
option(value='-') --------------
|
||||
|
||||
@@ -72,7 +72,10 @@ block content
|
||||
li
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(href="/register") #{translate("sign_up_now")}
|
||||
a.btn.btn-info(
|
||||
href="/register"
|
||||
style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
|
||||
) #{translate("sign_up_now")}
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
.card-header
|
||||
@@ -136,7 +139,10 @@ block content
|
||||
li
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(href="/register") #{translate("sign_up_now")}
|
||||
a.btn.btn-info(
|
||||
href="/register"
|
||||
style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
|
||||
) #{translate("sign_up_now")}
|
||||
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
|
||||
@@ -6,9 +6,13 @@ block content
|
||||
.row
|
||||
.registration_message
|
||||
if sharedProjectData.user_first_name !== undefined
|
||||
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:sharedProjectData.project_name})}
|
||||
div #{translate("join_sl_to_view_project")}.
|
||||
a(href="/login") #{translate("login_here")}
|
||||
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})}
|
||||
em #{sharedProjectData.project_name}
|
||||
div
|
||||
| #{translate("join_sl_to_view_project")}.
|
||||
div
|
||||
| #{translate("if_you_are_registered")},
|
||||
a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")}
|
||||
else if newTemplateData.templateName !== undefined
|
||||
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
extends ../layout
|
||||
|
||||
|
||||
block scripts
|
||||
script(type='text/javascript').
|
||||
window.otherSessions = !{JSON.stringify(sessions)}
|
||||
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
|
||||
.card.clear-user-sessions(ng-controller="ClearSessionsController", ng-cloak)
|
||||
.page-header
|
||||
h1 #{translate("your_sessions")}
|
||||
|
||||
div
|
||||
p.small
|
||||
| #{translate("clear_sessions_description")}
|
||||
|
||||
div
|
||||
div(ng-if="state.otherSessions.length == 0")
|
||||
p.text-center
|
||||
| #{translate("no_other_sessions")}
|
||||
|
||||
div(ng-if="state.success == true")
|
||||
p.text-success.text-center
|
||||
| #{translate('clear_sessions_success')}
|
||||
|
||||
div(ng-if="state.otherSessions.length != 0")
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th #{translate("ip_address")}
|
||||
th #{translate("session_created_at")}
|
||||
tr(ng-repeat="session in state.otherSessions")
|
||||
td {{session.ip_address}}
|
||||
td {{session.session_created | formatDate}}
|
||||
|
||||
p.actions
|
||||
.text-center
|
||||
button.btn.btn-lg.btn-primary(
|
||||
ng-click="clearSessions()"
|
||||
) #{translate('clear_sessions')}
|
||||
|
||||
div(ng-if="state.error == true")
|
||||
p.text-danger.error
|
||||
| #{translate('generic_something_went_wrong')}
|
||||
@@ -79,7 +79,7 @@ block content
|
||||
required,
|
||||
complex-password
|
||||
)
|
||||
span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.currentPassword.$dirty", ng-bind-html="complexPasswordErrorMessage")
|
||||
span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage")
|
||||
.form-group
|
||||
label(for='newPassword2') #{translate("confirm_new_password")}
|
||||
input.form-control(
|
||||
@@ -88,9 +88,11 @@ block content
|
||||
placeholder='*********',
|
||||
ng-model="newPassword2",
|
||||
equals="passwordField"
|
||||
)
|
||||
span.small.text-primary(ng-show="changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
|
||||
| #{translate("doesnt_match")}
|
||||
)
|
||||
span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty")
|
||||
| #{translate("doesnt_match")}
|
||||
span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
|
||||
| #{translate("Invalid Password")}
|
||||
.actions
|
||||
button.btn.btn-primary(
|
||||
type='submit',
|
||||
@@ -99,16 +101,25 @@ block content
|
||||
|
||||
| !{moduleIncludes("userSettings", locals)}
|
||||
|
||||
hr
|
||||
|
||||
h3
|
||||
| #{translate("sharelatex_beta_program")}
|
||||
|
||||
if (user.betaProgram)
|
||||
hr
|
||||
|
||||
h3
|
||||
| #{translate("sharelatex_beta_program")}
|
||||
|
||||
p.small
|
||||
| #{translate("beta_program_already_participating")}
|
||||
div
|
||||
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
|
||||
|
||||
div
|
||||
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
|
||||
|
||||
hr
|
||||
|
||||
h3
|
||||
| #{translate("sessions")}
|
||||
|
||||
div
|
||||
a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
|
||||
|
||||
hr
|
||||
|
||||
@@ -140,7 +151,7 @@ block content
|
||||
.modal-header
|
||||
h3 #{translate("delete_account")}
|
||||
.modal-body
|
||||
p !{translate("delete_account_warning_message")}
|
||||
p !{translate("delete_account_warning_message_2")}
|
||||
form(novalidate, name="deleteAccountForm")
|
||||
input.form-control(
|
||||
type="text",
|
||||
|
||||
@@ -106,8 +106,8 @@ module.exports = settings =
|
||||
url: ""
|
||||
# references:
|
||||
# url: "http://localhost:3040"
|
||||
# notifications:
|
||||
# url: "http://localhost:3042"
|
||||
notifications:
|
||||
url: "http://localhost:3042"
|
||||
|
||||
templates:
|
||||
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
|
||||
@@ -116,7 +116,7 @@ module.exports = settings =
|
||||
|
||||
# cdn:
|
||||
# web:
|
||||
# host:"http://cdn.sharelatex.dev:3000"
|
||||
# host:"http://nowhere.sharelatex.dev"
|
||||
# darkHost:"http://cdn.sharelatex.dev:3000"
|
||||
|
||||
# Where your instance of ShareLaTeX can be found publically. Used in emails
|
||||
@@ -155,7 +155,7 @@ module.exports = settings =
|
||||
collaborators: -1
|
||||
dropbox: true
|
||||
versioning: true
|
||||
compileTimeout: 60
|
||||
compileTimeout: 180
|
||||
compileGroup: "standard"
|
||||
references: true
|
||||
templates: true
|
||||
@@ -166,6 +166,8 @@ module.exports = settings =
|
||||
price: 0
|
||||
features: defaultFeatures
|
||||
}]
|
||||
|
||||
enableSubscriptions:false
|
||||
|
||||
# i18n
|
||||
# ------
|
||||
@@ -193,8 +195,8 @@ module.exports = settings =
|
||||
# passwordStrengthOptions:
|
||||
# pattern: "aA$3"
|
||||
# length:
|
||||
# min: 8
|
||||
# max: 50
|
||||
# min: 1
|
||||
# max: 10
|
||||
|
||||
# Email support
|
||||
# -------------
|
||||
|
||||
Generated
+1111
-833
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,10 @@
|
||||
"nodemailer-sendgrid-transport": "^0.2.0",
|
||||
"nodemailer-ses-transport": "^1.3.0",
|
||||
"optimist": "0.6.1",
|
||||
"passport": "^0.3.2",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^6.0.3",
|
||||
"pg-hstore": "^2.3.2",
|
||||
"redback": "0.4.0",
|
||||
"redis": "0.10.1",
|
||||
"redis-sharelatex": "0.0.9",
|
||||
@@ -53,6 +57,7 @@
|
||||
"requests": "^0.1.7",
|
||||
"rimraf": "2.2.6",
|
||||
"sanitizer": "0.1.1",
|
||||
"sequelize": "^3.2.0",
|
||||
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
|
||||
"sixpack-client": "^1.0.0",
|
||||
"temp": "^0.8.3",
|
||||
|
||||
@@ -112,6 +112,8 @@ define [
|
||||
[asyncFormCtrl, ngModelCtrl] = ctrl
|
||||
|
||||
ngModelCtrl.$parsers.unshift (modelValue) ->
|
||||
|
||||
|
||||
isValid = passField.validatePass()
|
||||
email = asyncFormCtrl.getEmail() || window.usersEmail
|
||||
if !isValid
|
||||
@@ -121,5 +123,8 @@ define [
|
||||
if modelValue.indexOf(email) != -1 or modelValue.indexOf(startOfEmail) != -1
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage = "Password can not contain email address"
|
||||
if opts.length.max? and modelValue.length == opts.length.max
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached"
|
||||
ngModelCtrl.$setValidity('complexPassword', isValid)
|
||||
return modelValue
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory 'ccUtils', () ->
|
||||
defaultFormat = /(\d{1,4})/g;
|
||||
defaultInputFormat = /(?:^|\s)(\d{4})$/
|
||||
|
||||
cards = [
|
||||
# Credit cards
|
||||
{
|
||||
type: 'visa'
|
||||
patterns: [4]
|
||||
format: defaultFormat
|
||||
length: [13, 16]
|
||||
cvcLength: [3]
|
||||
luhn: true
|
||||
}
|
||||
{
|
||||
type: 'mastercard'
|
||||
patterns: [
|
||||
51, 52, 53, 54, 55,
|
||||
22, 23, 24, 25, 26, 27
|
||||
]
|
||||
format: defaultFormat
|
||||
length: [16]
|
||||
cvcLength: [3]
|
||||
luhn: true
|
||||
}
|
||||
{
|
||||
type: 'amex'
|
||||
patterns: [34, 37]
|
||||
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
|
||||
length: [15]
|
||||
cvcLength: [3..4]
|
||||
luhn: true
|
||||
}
|
||||
{
|
||||
type: 'dinersclub'
|
||||
patterns: [30, 36, 38, 39]
|
||||
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
|
||||
length: [14]
|
||||
cvcLength: [3]
|
||||
luhn: true
|
||||
}
|
||||
{
|
||||
type: 'discover'
|
||||
patterns: [60, 64, 65, 622]
|
||||
format: defaultFormat
|
||||
length: [16]
|
||||
cvcLength: [3]
|
||||
luhn: true
|
||||
}
|
||||
{
|
||||
type: 'unionpay'
|
||||
patterns: [62, 88]
|
||||
format: defaultFormat
|
||||
length: [16..19]
|
||||
cvcLength: [3]
|
||||
luhn: false
|
||||
}
|
||||
{
|
||||
type: 'jcb'
|
||||
patterns: [35]
|
||||
format: defaultFormat
|
||||
length: [16]
|
||||
cvcLength: [3]
|
||||
luhn: true
|
||||
}
|
||||
]
|
||||
|
||||
cardFromNumber = (num) ->
|
||||
num = (num + '').replace(/\D/g, "")
|
||||
for card in cards
|
||||
for pattern in card.patterns
|
||||
p = pattern + ""
|
||||
return card if num.substr(0, p.length) == p
|
||||
|
||||
cardFromType = (type) ->
|
||||
return card for card in cards when card.type is type
|
||||
|
||||
cardType = (num) ->
|
||||
return null unless num
|
||||
cardFromNumber(num)?.type or null
|
||||
|
||||
formatCardNumber = (num) ->
|
||||
num = num.replace(/\D/g, '')
|
||||
card = cardFromNumber(num)
|
||||
return num unless card
|
||||
|
||||
upperLength = card.length[card.length.length - 1]
|
||||
num = num[0...upperLength]
|
||||
|
||||
if card.format.global
|
||||
num.match(card.format)?.join(' ')
|
||||
else
|
||||
groups = card.format.exec(num)
|
||||
return unless groups?
|
||||
groups.shift()
|
||||
groups = $.grep(groups, (n) -> n) # Filter empty groups
|
||||
groups.join(' ')
|
||||
|
||||
formatExpiry = (expiry) ->
|
||||
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
|
||||
return '' unless parts
|
||||
|
||||
mon = parts[1] || ''
|
||||
sep = parts[2] || ''
|
||||
year = parts[3] || ''
|
||||
|
||||
if year.length > 0
|
||||
sep = ' / '
|
||||
|
||||
else if sep is ' /'
|
||||
mon = mon.substring(0, 1)
|
||||
sep = ''
|
||||
|
||||
else if mon.length == 2 or sep.length > 0
|
||||
sep = ' / '
|
||||
|
||||
else if mon.length == 1 and mon not in ['0', '1']
|
||||
mon = "0#{mon}"
|
||||
sep = ' / '
|
||||
|
||||
return mon + sep + year
|
||||
|
||||
parseExpiry = (value = "") ->
|
||||
[month, year] = value.split(/[\s\/]+/, 2)
|
||||
|
||||
# Allow for year shortcut
|
||||
if year?.length is 2 and /^\d+$/.test(year)
|
||||
prefix = (new Date).getFullYear()
|
||||
prefix = prefix.toString()[0..1]
|
||||
year = prefix + year
|
||||
|
||||
month = parseInt(month, 10)
|
||||
year = parseInt(year, 10)
|
||||
|
||||
return unless !isNaN(month) and !isNaN(year)
|
||||
|
||||
month: month, year: year
|
||||
|
||||
return {
|
||||
fromNumber: cardFromNumber
|
||||
fromType: cardFromType
|
||||
cardType: cardType
|
||||
formatExpiry: formatExpiry
|
||||
formatCardNumber: formatCardNumber
|
||||
defaultFormat: defaultFormat
|
||||
defaultInputFormat: defaultInputFormat
|
||||
parseExpiry: parseExpiry
|
||||
}
|
||||
|
||||
App.factory 'ccFormat', (ccUtils, $filter) ->
|
||||
hasTextSelected = ($target) ->
|
||||
# If some text is selected
|
||||
return true if $target.prop('selectionStart')? and
|
||||
$target.prop('selectionStart') isnt $target.prop('selectionEnd')
|
||||
|
||||
# If some text is selected in IE
|
||||
if document?.selection?.createRange?
|
||||
return true if document.selection.createRange().text
|
||||
|
||||
false
|
||||
|
||||
safeVal = (value, $target) ->
|
||||
try
|
||||
cursor = $target.prop('selectionStart')
|
||||
catch error
|
||||
cursor = null
|
||||
|
||||
last = $target.val()
|
||||
$target.val(value)
|
||||
|
||||
if cursor != null && $target.is(":focus")
|
||||
cursor = value.length if cursor is last.length
|
||||
|
||||
# This hack looks for scenarios where we are changing an input's value such
|
||||
# that "X| " is replaced with " |X" (where "|" is the cursor). In those
|
||||
# scenarios, we want " X|".
|
||||
#
|
||||
# For example:
|
||||
# 1. Input field has value "4444| "
|
||||
# 2. User types "1"
|
||||
# 3. Input field has value "44441| "
|
||||
# 4. Reformatter changes it to "4444 |1"
|
||||
# 5. By incrementing the cursor, we make it "4444 1|"
|
||||
#
|
||||
# This is awful, and ideally doesn't go here, but given the current design
|
||||
# of the system there does not appear to be a better solution.
|
||||
#
|
||||
# Note that we can't just detect when the cursor-1 is " ", because that
|
||||
# would incorrectly increment the cursor when backspacing, e.g. pressing
|
||||
# backspace in this scenario: "4444 1|234 5".
|
||||
if last != value
|
||||
prevPair = last[cursor-1..cursor]
|
||||
currPair = value[cursor-1..cursor]
|
||||
digit = value[cursor]
|
||||
cursor = cursor + 1 if /\d/.test(digit) and
|
||||
prevPair == "#{digit} " and currPair == " #{digit}"
|
||||
|
||||
$target.prop('selectionStart', cursor)
|
||||
$target.prop('selectionEnd', cursor)
|
||||
|
||||
# Replace Full-Width Chars
|
||||
replaceFullWidthChars = (str = '') ->
|
||||
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
|
||||
halfWidth = '0123456789'
|
||||
|
||||
value = ''
|
||||
chars = str.split('')
|
||||
|
||||
# Avoid using reserved word `char`
|
||||
for chr in chars
|
||||
idx = fullWidth.indexOf(chr)
|
||||
chr = halfWidth[idx] if idx > -1
|
||||
value += chr
|
||||
|
||||
value
|
||||
|
||||
# Format Numeric
|
||||
reFormatNumeric = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
setTimeout ->
|
||||
value = $target.val()
|
||||
value = replaceFullWidthChars(value)
|
||||
value = value.replace(/\D/g, '')
|
||||
safeVal(value, $target)
|
||||
|
||||
# Format Card Number
|
||||
reFormatCardNumber = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
setTimeout ->
|
||||
value = $target.val()
|
||||
value = replaceFullWidthChars(value)
|
||||
value = ccUtils.formatCardNumber(value)
|
||||
safeVal(value, $target)
|
||||
|
||||
formatCardNumber = (e) ->
|
||||
# Only format if input is a number
|
||||
digit = String.fromCharCode(e.which)
|
||||
return unless /^\d+$/.test(digit)
|
||||
|
||||
$target = $(e.currentTarget)
|
||||
value = $target.val()
|
||||
card = ccUtils.fromNumber(value + digit)
|
||||
length = (value.replace(/\D/g, '') + digit).length
|
||||
|
||||
upperLength = 16
|
||||
upperLength = card.length[card.length.length - 1] if card
|
||||
return if length >= upperLength
|
||||
|
||||
# Return if focus isn't at the end of the text
|
||||
return if $target.prop('selectionStart')? and
|
||||
$target.prop('selectionStart') isnt value.length
|
||||
|
||||
if card && card.type is 'amex'
|
||||
# AMEX cards are formatted differently
|
||||
re = /^(\d{4}|\d{4}\s\d{6})$/
|
||||
else
|
||||
re = /(?:^|\s)(\d{4})$/
|
||||
|
||||
# If '4242' + 4
|
||||
if re.test(value)
|
||||
e.preventDefault()
|
||||
setTimeout -> $target.val(value + ' ' + digit)
|
||||
|
||||
# If '424' + 2
|
||||
else if re.test(value + digit)
|
||||
e.preventDefault()
|
||||
setTimeout -> $target.val(value + digit + ' ')
|
||||
|
||||
formatBackCardNumber = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
value = $target.val()
|
||||
|
||||
# Return unless backspacing
|
||||
return unless e.which is 8
|
||||
|
||||
# Return if focus isn't at the end of the text
|
||||
return if $target.prop('selectionStart')? and
|
||||
$target.prop('selectionStart') isnt value.length
|
||||
|
||||
# Remove the digit + trailing space
|
||||
if /\d\s$/.test(value)
|
||||
e.preventDefault()
|
||||
setTimeout -> $target.val(value.replace(/\d\s$/, ''))
|
||||
# Remove digit if ends in space + digit
|
||||
else if /\s\d?$/.test(value)
|
||||
e.preventDefault()
|
||||
setTimeout -> $target.val(value.replace(/\d$/, ''))
|
||||
|
||||
getFormattedCardNumber = (num) ->
|
||||
num = num.replace(/\D/g, '')
|
||||
card = ccUtils.fromNumber(num)
|
||||
return num unless card
|
||||
|
||||
upperLength = card.length[card.length.length - 1]
|
||||
num = num[0...upperLength]
|
||||
|
||||
if card.format.global
|
||||
num.match(card.format)?.join(' ')
|
||||
else
|
||||
groups = card.format.exec(num)
|
||||
return unless groups?
|
||||
groups.shift()
|
||||
groups = $.grep(groups, (n) -> n) # Filter empty groups
|
||||
groups.join(' ')
|
||||
|
||||
parseCardNumber = (value) ->
|
||||
if value? then value.replace(/\s/g, '') else value
|
||||
|
||||
# Format Expiry
|
||||
reFormatExpiry = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
setTimeout ->
|
||||
value = $target.val()
|
||||
value = replaceFullWidthChars(value)
|
||||
value = ccUtils.formatExpiry(value)
|
||||
safeVal(value, $target)
|
||||
|
||||
|
||||
formatExpiry = (e) ->
|
||||
# Only format if input is a number
|
||||
digit = String.fromCharCode(e.which)
|
||||
return unless /^\d+$/.test(digit)
|
||||
|
||||
$target = $(e.currentTarget)
|
||||
val = $target.val() + digit
|
||||
|
||||
if /^\d$/.test(val) and val not in ['0', '1']
|
||||
e.preventDefault()
|
||||
setTimeout -> $target.val("0#{val} / ")
|
||||
|
||||
else if /^\d\d$/.test(val)
|
||||
e.preventDefault()
|
||||
setTimeout ->
|
||||
# Split for months where we have the second digit > 2 (past 12) and turn
|
||||
# that into (m1)(m2) => 0(m1) / (m2)
|
||||
m1 = parseInt(val[0], 10)
|
||||
m2 = parseInt(val[1], 10)
|
||||
if m2 > 2 and m1 != 0
|
||||
$target.val("0#{m1} / #{m2}")
|
||||
else
|
||||
$target.val("#{val} / ")
|
||||
|
||||
formatForwardExpiry = (e) ->
|
||||
digit = String.fromCharCode(e.which)
|
||||
return unless /^\d+$/.test(digit)
|
||||
|
||||
$target = $(e.currentTarget)
|
||||
val = $target.val()
|
||||
|
||||
if /^\d\d$/.test(val)
|
||||
$target.val("#{val} / ")
|
||||
|
||||
formatForwardSlash = (e) ->
|
||||
which = String.fromCharCode(e.which)
|
||||
return unless which is '/' or which is ' '
|
||||
|
||||
$target = $(e.currentTarget)
|
||||
val = $target.val()
|
||||
|
||||
if /^\d$/.test(val) and val isnt '0'
|
||||
$target.val("0#{val} / ")
|
||||
|
||||
formatBackExpiry = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
value = $target.val()
|
||||
|
||||
# Return unless backspacing
|
||||
return unless e.which is 8
|
||||
|
||||
# Return if focus isn't at the end of the text
|
||||
return if $target.prop('selectionStart')? and
|
||||
$target.prop('selectionStart') isnt value.length
|
||||
|
||||
# Remove the trailing space + last digit
|
||||
if /\d\s\/\s$/.test(value)
|
||||
e.preventDefault()
|
||||
setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
|
||||
|
||||
parseExpiry = (value) ->
|
||||
if value?
|
||||
dateAsObj = ccUtils.parseExpiry(value)
|
||||
|
||||
return unless dateAsObj?
|
||||
|
||||
expiry = new Date dateAsObj.year, dateAsObj.month - 1
|
||||
|
||||
return $filter('date')(expiry, 'MM/yyyy')
|
||||
|
||||
# Format CVC
|
||||
reFormatCVC = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
setTimeout ->
|
||||
value = $target.val()
|
||||
value = replaceFullWidthChars(value)
|
||||
value = value.replace(/\D/g, '')[0...4]
|
||||
safeVal(value, $target)
|
||||
|
||||
# Restrictions
|
||||
restrictNumeric = (e) ->
|
||||
# Key event is for a browser shortcut
|
||||
return true if e.metaKey or e.ctrlKey
|
||||
|
||||
# If keycode is a space
|
||||
return false if e.which is 32
|
||||
|
||||
# If keycode is a special char (WebKit)
|
||||
return true if e.which is 0
|
||||
|
||||
# If char is a special char (Firefox)
|
||||
return true if e.which < 33
|
||||
|
||||
input = String.fromCharCode(e.which)
|
||||
|
||||
# Char is a number or a space
|
||||
!!/[\d\s]/.test(input)
|
||||
|
||||
restrictCardNumber = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
digit = String.fromCharCode(e.which)
|
||||
return unless /^\d+$/.test(digit)
|
||||
|
||||
return if hasTextSelected($target)
|
||||
|
||||
# Restrict number of digits
|
||||
value = ($target.val() + digit).replace(/\D/g, '')
|
||||
card = ccUtils.fromNumber(value)
|
||||
|
||||
if card
|
||||
value.length <= card.length[card.length.length - 1]
|
||||
else
|
||||
# All other cards are 16 digits long
|
||||
value.length <= 16
|
||||
|
||||
restrictExpiry = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
digit = String.fromCharCode(e.which)
|
||||
return unless /^\d+$/.test(digit)
|
||||
|
||||
return if hasTextSelected($target)
|
||||
|
||||
value = $target.val() + digit
|
||||
value = value.replace(/\D/g, '')
|
||||
|
||||
return false if value.length > 6
|
||||
|
||||
restrictCVC = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
digit = String.fromCharCode(e.which)
|
||||
return unless /^\d+$/.test(digit)
|
||||
|
||||
return if hasTextSelected($target)
|
||||
|
||||
val = $target.val() + digit
|
||||
val.length <= 4
|
||||
|
||||
setCardType = (e) ->
|
||||
$target = $(e.currentTarget)
|
||||
val = $target.val()
|
||||
cardType = ccUtils.cardType(val) or 'unknown'
|
||||
|
||||
unless $target.hasClass(cardType)
|
||||
allTypes = (card.type for card in cards)
|
||||
|
||||
$target.removeClass('unknown')
|
||||
$target.removeClass(allTypes.join(' '))
|
||||
|
||||
$target.addClass(cardType)
|
||||
$target.toggleClass('identified', cardType isnt 'unknown')
|
||||
$target.trigger('payment.cardType', cardType)
|
||||
|
||||
return {
|
||||
hasTextSelected
|
||||
replaceFullWidthChars
|
||||
reFormatNumeric
|
||||
reFormatCardNumber
|
||||
formatCardNumber
|
||||
formatBackCardNumber
|
||||
getFormattedCardNumber
|
||||
parseCardNumber
|
||||
reFormatExpiry
|
||||
formatExpiry
|
||||
formatForwardExpiry
|
||||
formatForwardSlash
|
||||
formatBackExpiry
|
||||
parseExpiry
|
||||
reFormatCVC
|
||||
restrictNumeric
|
||||
restrictCardNumber
|
||||
restrictExpiry
|
||||
restrictCVC
|
||||
setCardType
|
||||
}
|
||||
|
||||
App.directive "ccFormatExpiry", (ccFormat) ->
|
||||
restrict: "A"
|
||||
require: "ngModel"
|
||||
link: (scope, el, attrs, ngModel) ->
|
||||
el.on "keypress", ccFormat.restrictNumeric
|
||||
el.on "keypress", ccFormat.restrictExpiry
|
||||
el.on "keypress", ccFormat.formatExpiry
|
||||
el.on "keypress", ccFormat.formatForwardSlash
|
||||
el.on "keypress", ccFormat.formatForwardExpiry
|
||||
el.on "keydown", ccFormat.formatBackExpiry
|
||||
el.on "change", ccFormat.reFormatExpiry
|
||||
el.on "input", ccFormat.reFormatExpiry
|
||||
el.on "paste", ccFormat.reFormatExpiry
|
||||
|
||||
ngModel.$parsers.push ccFormat.parseExpiry
|
||||
ngModel.$formatters.push ccFormat.parseExpiry
|
||||
|
||||
App.directive "ccFormatCardNumber", (ccFormat) ->
|
||||
restrict: "A"
|
||||
require: "ngModel"
|
||||
link: (scope, el, attrs, ngModel) ->
|
||||
el.on "keypress", ccFormat.restrictNumeric
|
||||
el.on "keypress", ccFormat.restrictCardNumber
|
||||
el.on "keypress", ccFormat.formatCardNumber
|
||||
el.on "keydown", ccFormat.formatBackCardNumber
|
||||
el.on "paste", ccFormat.reFormatCardNumber
|
||||
|
||||
ngModel.$parsers.push ccFormat.parseCardNumber
|
||||
ngModel.$formatters.push ccFormat.getFormattedCardNumber
|
||||
|
||||
App.directive "ccFormatSecCode", (ccFormat) ->
|
||||
restrict: "A"
|
||||
require: "ngModel"
|
||||
link: (scope, el, attrs, ngModel) ->
|
||||
el.on "keypress", ccFormat.restrictNumeric
|
||||
el.on "keypress", ccFormat.restrictCVC
|
||||
el.on "paste", ccFormat.reFormatCVC
|
||||
el.on "change", ccFormat.reFormatCVC
|
||||
el.on "input", ccFormat.reFormatCVC
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user