From 9212a39cff3e88650bf33b1d3a54af3643d95ace Mon Sep 17 00:00:00 2001 From: Robert Helmer Date: Wed, 23 Jan 2013 12:04:06 -0800 Subject: [PATCH] teampad feature --- src/ep.json | 3 + src/node/db/Pad.js | 10 + src/node/db/PadManager.js | 20 ++ src/node/db/SecurityManager.js | 21 ++ src/node/db/SessionManager.js | 60 ++++++ src/node/db/TeamManager.js | 211 ++++++++++++++++++++ src/node/handler/PadMessageHandler.js | 18 +- src/node/handler/SocketIORouter.js | 10 +- src/node/hooks/express/teampad.js | 276 ++++++++++++++++++++++++++ src/node/padaccess.js | 10 +- src/static/css/teampad.css | 86 ++++++++ src/templates/index.html | 9 + src/templates/teampad/index.html | 72 +++++++ src/templates/teampad/team.html | 69 +++++++ 14 files changed, 871 insertions(+), 4 deletions(-) create mode 100644 src/node/db/TeamManager.js create mode 100644 src/node/hooks/express/teampad.js create mode 100644 src/static/css/teampad.css create mode 100644 src/templates/teampad/index.html create mode 100644 src/templates/teampad/team.html diff --git a/src/ep.json b/src/ep.json index 89c8964aa..3a1246232 100644 --- a/src/ep.json +++ b/src/ep.json @@ -23,6 +23,9 @@ { "name": "adminsettings", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer", "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" } + }, + { "name": "teampad", "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/teampad:expressCreateServer" } } ] } diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 4701e82a3..f89efaf75 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -38,6 +38,7 @@ var Pad = function Pad(id) { this.head = -1; this.chatHead = -1; this.publicStatus = false; + this.teamStatus = false; this.passwordHash = null; this.id = id; this.savedRevisions = []; @@ -551,6 +552,15 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() { return this.savedRevisions; }; +Pad.prototype.getTeamStatus = function getTeamStatus() { + return this.teamStatus; +}; + +Pad.prototype.setTeamStatus = function setTeamStatus(teamStatus) { + this.teamStatus = teamStatus; + this.saveToDatabase(); +}; + /* Crypto helper methods */ function hash(password, salt) diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 5e0af4643..2bd3d99ee 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -236,3 +236,23 @@ exports.unloadPad = function(padId) if(globalPads.get(padId)) globalPads.remove(padId); } + +//checks if a pad is a "team pad" +exports.isTeamPad = function(padId) +{ + var isTeamPad = false; + db.get("pad:"+padId, function(err, value) + { + if(ERR(err)) return; + + if(value != null && value.atext && value.teamStatus){ + isTeamPad = true; + } + else + { + isTeamPad = false; + } + }); + + return isTeamPad; +} diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 4289e39ca..c96f9ac30 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -49,6 +49,27 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback) callback(null, {accessStatus: "deny"}); return; } + } + else if(padManager.isTeamPad(padID)) + { + authorManager.getAuthor4Token(token, function(err, author) + { + if(ERR(err, callback)) return; + + // TODO check session + console.log('sessionCookie: ' + sessionCookie); + sessionManager.getSessionInfo(sessionCookie, function(err, result) { + if (err) { + statusObject = {accessStatus: "denyTeamPad", authorID: author}; + callback(null, statusObject); + } else { + // TODO figure out how to force authorID to match account name... + statusObject = {accessStatus: "grant", authorID: author}; + callback(null, statusObject); + } + }); + }); + } // a session is not required, so we'll check if it's a public pad else diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 5ce4f7487..b8eb0de4c 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -360,6 +360,66 @@ function listSessionsWithDBKey (dbkey, callback) }); } +/** + * Creates a new session based on an externally-verified account (e.g. persona) + */ +exports.createVerifiedSession = function(sessionID, account, validUntil, callback) +{ + async.series([ + + //check validUntil and create the session db entry + function(callback) + { + //check if rev is a number + if(typeof validUntil != "number") + { + //try to parse the number + if(!isNaN(parseInt(validUntil))) + { + validUntil = parseInt(validUntil); + } + else + { + callback(new customError("validUntil is not a number","apierror")); + return; + } + } + + //ensure this is not a negativ number + if(validUntil < 0) + { + callback(new customError("validUntil is a negativ number","apierror")); + return; + } + + //ensure this is not a float value + if(!is_int(validUntil)) + { + callback(new customError("validUntil is a float value","apierror")); + return; + } + + //check if validUntil is in the future + if(Math.floor(new Date().getTime()/1000) > validUntil) + { + callback(new customError("validUntil is in the past","apierror")); + return; + } + + //set the session into the database + db.set("session:" + sessionID, {"account": account, "validUntil": validUntil}); + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + + //return error and sessionID + callback(null, {sessionID: sessionID}); + }) +} + //checks if a number is an int function is_int(value) { diff --git a/src/node/db/TeamManager.js b/src/node/db/TeamManager.js new file mode 100644 index 000000000..47798739b --- /dev/null +++ b/src/node/db/TeamManager.js @@ -0,0 +1,211 @@ +/** + * The Team Manager provides functions to manage teams in the database + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var ERR = require("async-stacktrace"); +var customError = require("../utils/customError"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var db = require("./DB").db; +var async = require("async"); +var padManager = require("./PadManager"); +var sessionManager = require("./SessionManager"); + +exports.listAllTeams = function(callback) { + db.get("teams", function (err, teams) { + if(ERR(err, callback)) return; + + // there are no teams + if(teams == null) { + callback(null, {teamIDs: []}); + return; + } + + var teamIDs = []; + for ( var teamID in teams) { + teamIDs.push(teamID); + } + callback(null, {teamIDs: teamIDs}); + }); +} + +exports.doesTeamExist = function(teamID, callback) +{ + //try to get the team entry + db.get("team:" + teamID, function (err, team) + { + if(ERR(err, callback)) return; + callback(null, team != null); + }); +} + +exports.createTeam = function(teamName, pads, accounts, admins, callback) +{ + //search for non existing teamID + var teamID = "t." + randomString(16); + + //create the team + db.set("team:" + teamID, {name: teamName, pads: pads, accounts: accounts, + admins: admins}); + + //list the team + exports.listAllTeams(function(err, teams) { + if(ERR(err, callback)) return; + teams = teams? teams.teamIDs : []; + + teams.push(teamID); + + // regenerate team list + var newTeams = {}; + async.forEach(teams, function(team, cb) { + newTeams[team] = 1; + cb(); + },function() { + db.set("teams", newTeams); + callback(null, {teamID: teamID}); + }); + }); +} + +exports.createTeamPad = function(teamName, teamID, padName, text, callback) +{ + //create the padID + var padID = teamName + "+" + padName; + + async.series([ + //ensure team exists + function (callback) + { + exports.doesTeamExist(teamID, function(err, exists) + { + if(ERR(err, callback)) return; + + //team does not exist + if(exists == false) + { + callback(new customError("teamID does not exist","apierror")); + } + //team exists, everything is fine + else + { + callback(); + } + }); + }, + //ensure pad does not exists + function (callback) + { + padManager.doesPadExists(padID, function(err, exists) + { + if(ERR(err, callback)) return; + + //pad exists already + if(exists == true) + { + callback(new customError("padName does already exist","apierror")); + } + //pad does not exist, everything is fine + else + { + callback(); + } + }); + }, + //create the pad + function (callback) + { + padManager.getPad(padID, text, function(err, pad) + { + if(ERR(err, callback)) return; + + pad.setTeamStatus(true); + + callback(); + }); + }, + //add to DB + function (callback) + { + db.get("team:" + teamID, function(err, result) + { + if(ERR(err, callback)) return; + + result.pads.push(padID); + db.set('team:' + teamID, result); + }); + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, {padID: padID}); + }); +} + +exports.listInfo = function(teamID, callback) +{ + exports.doesTeamExist(teamID, function(err, exists) + { + if(ERR(err, callback)) return; + + //team does not exist + if(exists == false) + { + callback(new customError("teamID does not exist","apierror")); + } + //team exists, let's get the info + else + { + db.get("team:" + teamID, function(err, result) + { + if(ERR(err, callback)) return; + + callback(null, result); + }); + } + }); +} + +exports.addAccountToTeam = function(teamID, account, callback) +{ + exports.doesTeamExist(teamID, function(err, exists) + { + if(ERR(err, callback)) return; + + //team does not exist + if(exists == false) + { + console.log('debug1: ' + teamID); + callback(new customError("teamID does not exist","apierror")); + } + //team exists, let's get the info + else + { + db.get("team:" + teamID, function(err, result) + { + if(ERR(err, callback)) return; + + result.accounts.push(account); + console.log('setting team to: ' + result); + db.set("team:" + teamID, result); + callback(null, result); + }); + } + }); +} diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 6781cd884..25e439585 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -238,7 +238,14 @@ exports.handleMessage = function(client, message) // our "sessions" "connections". // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess(message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(message.padID)) { + sessionID = message.sessionid; + } else { + sessionID = client.handshake.sessionID; + } + securityManager.checkAccess(message.padId, sessionID, message.token, message.password, function(err, statusObject) { if(ERR(err, callback)) return; @@ -874,7 +881,14 @@ function handleClientReady(client, message) // our "sessions" "connections". // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess (padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(message.padID)) { + sessionID = message.sessionid; + } else { + sessionID = client.handshake.sessionID; + } + securityManager.checkAccess (padIds.padId, sessionID, message.token, message.password, function(err, statusObject) { if(ERR(err, callback)) return; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index f3b82b8c7..159e8cee1 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -23,6 +23,7 @@ var ERR = require("async-stacktrace"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var securityManager = require("../db/SecurityManager"); +var padManager = require("../db/PadManager"); /** * Saves all components @@ -108,7 +109,14 @@ exports.setSocketIO = function(_socket) //this message has everything to try an authorization if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { - securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(message.padID)) { + sessionID = message.sessionid; + } else { + sessionID = client.handshake.sessionID; + } + securityManager.checkAccess (message.padId, sessionID, message.token, message.password, function(err, statusObject) { ERR(err); diff --git a/src/node/hooks/express/teampad.js b/src/node/hooks/express/teampad.js new file mode 100644 index 000000000..97b880ced --- /dev/null +++ b/src/node/hooks/express/teampad.js @@ -0,0 +1,276 @@ +var express = require('express'), + async = require('async'), + eejs = require('ep_etherpad-lite/node/eejs'), + teamManager = require('ep_etherpad-lite/node/db/TeamManager'), + sessionManager = require('ep_etherpad-lite/node/db/SessionManager'), + padManager = require('ep_etherpad-lite/node/db/PadManager'), + https = require('https'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.use(express.bodyParser()); + + // TODO use more generic, pluggable auth, hardcoded to persona for now + args.app.post('/teampad/verify', function(req, res) { + console.log('sign in attempt'); + var body = JSON.stringify({ + assertion: req.param('assertion', null), + audience: 'http://' + req.headers.host + }); + + var vreq = https.request({ + host: 'persona.org', + path: '/verify', + method: 'POST', + headers: { + 'Content-Length': body.length, + 'Content-Type': 'application/json' + } + }, function(vres) { + var body = ''; + vres.on('data', function(chunk) { body += chunk; }); + vres.on('end', function() { + try { + account = JSON.parse(body).email; + validUntil = JSON.parse(body).expires; + console.log(body); + var sessionID = req.signedCookies.express_sid; + sessionManager.createVerifiedSession( + sessionID, account, validUntil, function(err, result) { + if (err) { + console.log(err); + return; + } + }); + console.log(account + ' logged in'); + } catch(e) { + console.log(e); + } + }); + res.redirect('/teampad'); + }); + vreq.write(body); + vreq.end(); + }); + + args.app.post('/teampad/createteam', function(req, res) { + var sessionID = req.cookies.express_sid, + currentUser = null, + signedIn = false, + teamName = null, + rawTeamName = req.param('teamname', null); + + async.waterfall([ + function(callback) { + sessionManager.getSessionInfo(sessionID, callback); + }, + function(result, callback) { + currentUser = result.account; + signedIn = true; + callback(); + }, + function(callback) { + console.log('about to sanitize ' + rawTeamName); + padManager.sanitizePadId(rawTeamName, function(teamName) { + callback(null, teamName); + }) + }, + function(result, callback) { + teamName = result; + console.log('sanitized ' + teamName); + teamManager.createTeam(teamName, [], [currentUser], [currentUser], + callback); + }, + function(teamID, callback) { + console.log(teamID + ' created for ' + teamName); + res.redirect('/teampad'); + } + ], function(err) { + console.log('error: ' + err); + res.redirect('/teampad'); + }); + }); + + args.app.post('/teampad/createpad', function(req, res) { + var sessionID = req.cookies.express_sid; + + var teamName = null, + padName = null, + currentUser = null, + signedIn = false, + teamID = req.param('teamID', null), + rawTeamName = req.param('teamname', null), + rawPadName = req.param('padname', null); + + async.waterfall([ + function(callback) { + sessionManager.getSessionInfo(sessionID, callback); + }, + function(result, callback) { + currentUser = result.account; + signedIn = true; + padManager.sanitizePadId(rawTeamName, function(teamName) { + callback(null, teamName); + }); + }, + function(result, callback) { + teamName = result; + padManager.sanitizePadId(rawPadName, function(padName) { + callback(null, padName); + }); + }, + function(result, callback) { + padName = result; + teamManager.createTeamPad(teamName, teamID, padName, 'super sekrit!', + callback); + }, + function(callback) { + console.log(padName + ' created for ' + teamName); + res.redirect('/teampad/' + teamName); + } + ], function(err) { + console.log(err); + res.redirect('/teampad'); + }); + }); + + args.app.post('/teampad/addaccount', function(req, res) { + var sessionID = req.cookies.express_sid, + currentUser = null, + signedIn = false, + teamName = null, + teamID = req.param('teamID', null), + rawTeamName = req.param('teamname', null), + account = req.param('accountname', null); + + async.waterfall([ + function(callback) { + sessionManager.getSessionInfo(sessionID, callback); + }, + function(result, callback) { + currentUser = result.account; + padManager.sanitizePadId(rawTeamName, function(teamName) { + callback(null, teamName); + }); + }, + function(result, callback) { + teamName = result; + console.log('teamID: ' + teamID); + teamManager.addAccountToTeam(teamID, account, callback); + }, + function(result, callback) { + teamID = result; + console.log(account+ ' added to ' + teamID); + res.redirect('/teampad/' + teamName); + }, + ], function(err) { + console.log(err); + }); + }); + + args.app.get('/teampad', function(req, res) { + var sessionID = req.cookies.express_sid; + var currentUser = null; + var signedIn = false; + + sessionManager.getSessionInfo(sessionID, function(err, result) { + if (err) { + console.log(err); + } else { + currentUser = result.account; + signedIn = true; + } + }); + + var teamsInfo = []; + + // TODO an index for finding teams by account would make this + // *way* faster and easier... + teamManager.listAllTeams(function(err, teams) { + for (var team in teams.teamIDs) { + teamID = teams.teamIDs[team]; + teamManager.listInfo(teamID, function(err, info) { + if (info.accounts) { + if (info.accounts.indexOf(currentUser) != -1) { + teamsInfo.push(info); + } + } + }); + } + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); + + res.send(eejs.require('ep_etherpad-lite/templates/teampad/index.html', + { teamsInfo: teamsInfo, + signedIn: signedIn, + currentUser: currentUser})); + }); + }); + + args.app.get('/teampad/:teamName', function(req, res) { + var sessionID = req.cookies.express_sid; + var currentUser = null; + var signedIn = false; + + sessionManager.getSessionInfo(sessionID, function(err, result) { + if (err) { + console.log(err); + res.redirect('/teampad'); + } else { + currentUser = result.account; + signedIn = true; + + var teamName = req.path.split('/')[2]; + var teamInfo = { + pads: [], + accounts: [], + name: [], + teamID: [] + }; + + // TODO an index for finding pads/accounts by team would make this + // *way* faster and easier... + teamManager.listAllTeams(function(err, teams) { + for (var team in teams.teamIDs) { + teamID = teams.teamIDs[team]; + teamManager.listInfo(teamID, function(err, info) { + if (info.name) { + if (teamName === info.name) { + teamInfo = info; + teamInfo.teamID = teamID; + } + } + }); + } + + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); + + res.send(eejs.require('ep_etherpad-lite/templates/teampad/team.html', + {teamInfo: teamInfo, + signedIn: false})); + }); + } + }); + }); + + // TODO implement, for now we are linking to normal pads via templates + args.app.get('/teampad/:teamName/:padName', function(req, res) { + var sessionID = req.cookies.express_sid; + var currentUser = null; + var signedIn = false; + + sessionManager.getSessionInfo(sessionID, function(err, result) { + if (err) { + console.log(err); + res.redirect('/teampad'); + } else { + currentUser = result.account; + signedIn = true; + } + }); + + var padName = req.path.split('/')[3]; + + res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); + + res.send(eejs.require('ep_etherpad-lite/templates/teampad/pad.html')); + }); +} diff --git a/src/node/padaccess.js b/src/node/padaccess.js index d87809149..cc2dc3729 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,5 +1,6 @@ var ERR = require("async-stacktrace"); var securityManager = require('./db/SecurityManager'); +var padManager = require("./db/PadManager"); //checks for padAccess module.exports = function (req, res, callback) { @@ -7,7 +8,14 @@ module.exports = function (req, res, callback) { // FIXME: Why is this ever undefined?? if (req.cookies === undefined) req.cookies = {}; - securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { + // FIXME: always use httponly session cookies + var sessionID = null; + if (padManager.isTeamPad(req.params.pad)) { + sessionID = req.cookies.sessionid; + } else { + sessionID = req.cookies.express_sid; + } + securityManager.checkAccess(req.params.pad, sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) { if(ERR(err, callback)) return; //there is access, continue diff --git a/src/static/css/teampad.css b/src/static/css/teampad.css new file mode 100644 index 000000000..2e8b8e118 --- /dev/null +++ b/src/static/css/teampad.css @@ -0,0 +1,86 @@ +body { + margin: 0; + color: #333; + font: 14px helvetica, sans-serif; + background: #ddd; + background: -webkit-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -moz-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -ms-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -o-radial-gradient(circle,#aaa,#eee 60%) center fixed; + border-top: 8px solid rgba(51,51,51,.8); +} +#wrapper { + margin-top: 160px; + padding: 15px; + background: #fff; + opacity: .9; + box-shadow: 0px 1px 8px rgba(0,0,0,0.3); + max-width: 700px; + margin: auto; + border-radius: 0 0 7px 7px; +} +h1 { + font-size: 29px; +} +h2 { + font-size: 24px; +} +.separator { + margin: 10px 0; + height: 1px; + background: #aaa; + background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); +} +form { + margin-bottom: 0; +} +#inner { + width: 300px; + margin: 0 auto; +} +input { + font-weight: bold; + font-size: 15px; +} +input[type="button"] { + padding: 4px 6px; + margin: 0; +} +input[type="button"].do-install, input[type="button"].do-uninstall { + float: right; + width: 100px; +} +input[type="button"]#do-search { + display: block; +} +input[type="text"] { + border-radius: 3px; + box-sizing: border-box; + -moz-box-sizing: border-box; + padding: 10px; + *padding: 0; /* IE7 hack */ + width: 100%; + outline: none; + border: 1px solid #ddd; + margin: 0 0 5px 0; + max-width: 500px; +} +table { + border: 1px solid #ddd; + border-radius: 3px; + border-spacing: 0; + width: 100%; + margin: 20px 0; +} +table thead tr { + background: #eee; +} +td, th { + padding: 5px; +} +.template { + display: none; +} diff --git a/src/templates/index.html b/src/templates/index.html index c3c13db32..77614dfc7 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -146,6 +146,9 @@ text-align: center; } } + #teampads { + font: 32px verdana,arial,sans-serif; + } @@ -162,6 +165,12 @@ <% e.end_block(); %> +
+ +
+ + + + +
+

Etherpad Lite

+ <% if (signedIn) { %> +
+ +

Teams you are a member of

+ <% if (teamsInfo.length == 0) { %> + None yet! + <% } else { %> + + + + + + + + + <% for (var i=0; i < teamsInfo.length; i++) { %> + + + + + <% } %> + +
Team name
+ <%= teamsInfo[i].name %> + + <% if (teamsInfo[i].admins.indexOf(currentUser) != undefined) { %> + [admin] + <% } %> +
+ <% } %> +
+
+

Create new team:

+ + + + + <% } else { %> + + Sign in to create, manage and see your team pads → + +
+ +
+ + + + + <% } %> +

+ diff --git a/src/templates/teampad/team.html b/src/templates/teampad/team.html new file mode 100644 index 000000000..7881ba52a --- /dev/null +++ b/src/templates/teampad/team.html @@ -0,0 +1,69 @@ + + + + <%= teamInfo.name %> + + +
+

Etherpad Lite

+
+

Pads belonging to this team:

+ <% if (teamInfo.pads.length == 0) { %> + None yet! + <% } else { %> + + + + + + + + + <% for (var i=0; i < teamInfo.pads.length; i++) { %> + + + + + <% } %> + +
Pad name
+ <% var teamName = teamInfo.pads[i].split('+')[1]; %> + <%= teamName %> +
+ <% } %> +
+

Create pad:

+ + + + +
+
+

Authors belonging to this team:

+ + + + + + + + + <% for (var i=0; i < teamInfo.accounts.length; i++) { %> + + + + + <% } %> + +
Account name
+ <%= teamInfo.accounts[i] %> +
+
+

Add account to team:

+ + + + +
+
+