diff --git a/customize.dist/login.js b/customize.dist/login.js
index 8a27ee4ff..efce968ca 100644
--- a/customize.dist/login.js
+++ b/customize.dist/login.js
@@ -221,21 +221,21 @@ define([
// If they are trying to register,
// and the proxy is empty, then there is no 'legacy user' either
// so we should just shut down this session and disconnect.
- rt.network.disconnect();
+ //rt.network.disconnect();
return; // proceed to the next async block
}
// they tried to just log in but there's no such user
// and since we're here at all there is no modern-block
if (!isRegister && isProxyEmpty(rt.proxy)) {
- rt.network.disconnect(); // clean up after yourself
+ //rt.network.disconnect(); // clean up after yourself
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
- rt.network.disconnect();
+ //rt.network.disconnect();
waitFor.abort();
Feedback.send('LOGIN', true);
return void cb('ALREADY_REGISTERED', res);
@@ -247,6 +247,7 @@ define([
// so setting them is just a precaution to keep things in good shape
res.proxy = rt.proxy;
res.realtime = rt.realtime;
+ res.network = rt.network;
// they're registering...
res.userHash = opt.userHash;
@@ -317,6 +318,7 @@ define([
res.proxy = rt.proxy;
res.realtime = rt.realtime;
+ res.network = rt.network;
// they're registering...
res.userHash = userHash;
@@ -328,14 +330,14 @@ define([
// this really shouldn't happen, but let's handle it anyway
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
- rt.network.disconnect(); // clean up after yourself
+ //rt.network.disconnect(); // clean up after yourself
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
- rt.network.disconnect();
+ //rt.network.disconnect();
waitFor.abort();
res.blockHash = blockHash;
if (shouldImport) {
@@ -462,7 +464,8 @@ define([
var proceed = function (result) {
hashing = false;
- if (test && typeof test === "function" && test()) { return; }
+ // NOTE: test is also use as a cb for the install page
+ if (test && typeof test === "function" && test(result)) { return; }
LocalStore.clearLoginToken();
Realtime.whenRealtimeSyncs(result.realtime, function () {
Exports.redirect();
diff --git a/customize.dist/pages/install.js b/customize.dist/pages/install.js
new file mode 100644
index 000000000..7bbf9e4cd
--- /dev/null
+++ b/customize.dist/pages/install.js
@@ -0,0 +1,87 @@
+define([
+ '/api/config',
+ 'jquery',
+ '/common/hyperscript.js',
+ '/common/common-interface.js',
+ '/customize/messages.js',
+ '/customize/pages.js'
+], function (Config, $, h, UI, Msg, Pages) {
+ Config.adminKeys = [];
+ return function () {
+ // Redirect to drive if this instance already has admins
+ if (Array.isArray(Config.adminKeys) && Config.adminKeys.length) {
+ document.location.href = '/drive/';
+ return;
+ }
+
+/*
+Msg.install_header = "CryptPad Install"; // XXX
+Msg.install_notes = "
Create your first admin account using this form. " +
+ "Please note your password carefully. If you lose it there is no way we can recover your data. "; // XXX
+*/
+Msg.install_token = "Install token";
+
+ document.title = Msg.install_header;
+
+ var frame = function (content) {
+ return [
+ h('div#cp-main', [
+ //Pages.infopageTopbar(),
+ h('div.container.cp-container', [
+ //h('div.row.cp-page-title', h('h1', Msg.install_header)),
+ h('div.row.cp-page-title', h('h1', Msg.register_header)),
+ ].concat(content)),
+ Pages.infopageFooter(),
+ ]),
+ ];
+ };
+
+ return frame([
+ h('div.row.cp-register-det', [
+ h('div#data.hidden.col-md-6', [
+ h('h2', Msg.register_notes_title),
+ //Pages.setHTML(h('div.cp-register-notes'), Msg.install_notes)
+ Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
+ ]),
+ h('div.cp-reg-form.col-md-6', [
+ h('div#userForm.form-group.hidden', [
+ h('div.cp-register-instance', [
+ Msg._getKey('register_instance', [ Pages.Instance.name ]),
+ /*h('br'),
+ h('a', {
+ href: '/features.html'
+ }, Msg.register_whyRegister)*/
+ ]),
+ h('input.form-control#installtoken', {
+ type: 'text',
+ placeholder: Msg.install_token
+ }),
+ h('input.form-control#username', {
+ type: 'text',
+ autocomplete: 'off',
+ autocorrect: 'off',
+ autocapitalize: 'off',
+ spellcheck: false,
+ placeholder: Msg.login_username,
+ autofocus: true,
+ }),
+ h('input.form-control#password', {
+ type: 'password',
+ placeholder: Msg.login_password,
+ }),
+ h('input.form-control#password-confirm', {
+ type: 'password',
+ placeholder: Msg.login_confirm,
+ }),
+ /*h('div.checkbox-container', [
+ UI.createCheckbox('import-recent', Msg.register_importRecent, true)
+ ]),*/
+ h('button#register', Msg.login_register)
+ ])
+ ]),
+ ])
+ ]);
+ };
+
+});
+
diff --git a/customize.dist/src/less2/pages/page-install.less b/customize.dist/src/less2/pages/page-install.less
new file mode 100644
index 000000000..696c3e4c1
--- /dev/null
+++ b/customize.dist/src/less2/pages/page-install.less
@@ -0,0 +1,126 @@
+@import (reference) "../include/infopages.less";
+@import (reference) "../include/colortheme-all.less";
+@import (reference) "../include/alertify.less";
+@import (reference) "../include/checkmark.less";
+@import (reference) "../include/forms.less";
+
+&.cp-page-install {
+ .infopages_main();
+ .forms_main();
+
+ .alertify_main();
+ .checkmark_main(20px);
+
+ .cp-container {
+ .form-group {
+ .cp-register-instance {
+ text-align: center;
+ margin-bottom: 10px;
+ }
+ #register {
+ &.btn {
+ padding: .5rem .5rem;
+ }
+ margin-top: 16px;
+ font-size: 1.25em;
+ min-width: 30%;
+ }
+ }
+ padding-bottom: 3em;
+ min-height: 5vh;
+ }
+ .alertify {
+ // workaround for alertify making empty p
+ p:empty {
+ display: none;
+ }
+
+ nav {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ }
+
+ @media screen and (max-width: 600px) {
+ nav .btn-danger {
+ line-height: inherit;
+ }
+ }
+
+ }
+ .cp-restricted-registration {
+ text-align: center !important;
+ }
+
+ .cp-register-det {
+ #data {
+
+ p {
+ li {
+ margin-bottom: 1em;
+ }
+ .fa {
+ padding-right: 10px;
+ }
+ }
+ h3 {
+ font-weight: 700;
+ margin-bottom: 1em;
+ }
+ }
+ .cp-reg-form {
+ img {
+ margin-top: 0px;
+ position: relative;
+ z-index: 0;
+ }
+ }
+ #userForm {
+ padding: 15px;
+ background-color: @cp_static-card-bg;
+ position: relative;
+ z-index: 2;
+ margin-bottom: 100px;
+ border-radius: @infopages-radius-L;
+ .cp-shadow();
+ .form-control {
+ border-radius: @infopages-radius;
+ color: @cryptpad_text_col;
+ background-color: @cp_forms-bg;
+ margin-bottom: 10px;
+ &:focus {
+ border-color: @cryptpad_color_brand;
+ }
+ .tools_placeholder-color();
+ }
+ .checkbox-container {
+ color: @cryptpad_text_col;
+ }
+ button#register {
+ margin-top: 10px;
+ }
+ }
+
+ .cp-register-notes {
+ ul.cp-notes-list {
+ list-style: none;
+ margin-left: 0;
+ padding-left: 30px;
+ position: relative;
+ li {
+ margin-bottom: 10px;
+ &::before {
+ position: absolute;
+ left: 0;
+ font-family: "FontAwesome";
+ content: "\f071";
+ }
+ .red {
+ background-color: @cp_static-danger;
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/customize.dist/template.js b/customize.dist/template.js
index fae7ca2bd..332471630 100644
--- a/customize.dist/template.js
+++ b/customize.dist/template.js
@@ -53,6 +53,8 @@ $(function () {
if (/^\/register\//.test(pathname)) {
require([ '/register/main.js' ], function () {});
+ } else if (/^\/install\//.test(pathname)) {
+ require([ '/install/main.js' ], function () {});
} else if (/^\/login\//.test(pathname)) {
require([ '/login/main.js' ], function () {});
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js
index 365987229..a05b177a2 100644
--- a/lib/commands/admin-rpc.js
+++ b/lib/commands/admin-rpc.js
@@ -777,6 +777,25 @@ var commands = {
REMOVE_DOCUMENT: removeDocument,
};
+// addFirstAdmin is an anon_rpc command
+Admin.addFirstAdmin = function (Env, data, cb) {
+ var token = data.token;
+ if (token !== Env.installToken) { return void cb('FORBIDDEN'); }
+ if (Array.isArray(Env.admins) && Env.admins.length) { return void cb('EEXISTS'); }
+
+ var key = data.edPublic;
+ if (token.length !== 64 || data.edPublic.length !== 44) { return void cb('INVALID_ARGS'); }
+
+ adminDecree(Env, null, function (err) {
+ if (err) { return void cb(err); }
+ Env.flushCache();
+ cb();
+ }, ['ADD_FIRST_ADMIN', [
+ 'ADD_ADMIN_KEY',
+ [key]
+ ]], "");
+};
+
Admin.command = function (Env, safeKey, data, _cb, Server) {
var cb = Util.once(Util.mkAsync(_cb));
diff --git a/lib/decrees.js b/lib/decrees.js
index 1d48f0164..7afac7578 100644
--- a/lib/decrees.js
+++ b/lib/decrees.js
@@ -322,6 +322,34 @@ commands.RM_QUOTA = function (Env, args) {
return true;
};
+commands.ADD_INSTALL_TOKEN = function (Env, args) {
+ if (!Array.isArray(args) || args.length !== 1 || !args[0]) {
+ throw new Error("INVALID_ARGS");
+ }
+
+ var token = args[0];
+
+ // XXX check length, etc. ?
+
+ Env.installToken = token;
+
+ return true;
+};
+commands.ADD_ADMIN_KEY = function (Env, args) {
+ if (!Array.isArray(args) || args.length !== 1 || !args[0]) {
+ throw new Error("INVALID_ARGS");
+ }
+
+ Env.admins = Env.admins || [];
+
+
+ var key = Keys.canonicalize(args[0]);
+ if (!key) { throw new Error("INVALID_KEY"); }
+ Env.admins.push(key);
+
+ return true;
+};
+
// [, , , ]
var handleCommand = Decrees.handleCommand = function (Env, line) {
var command = line[0];
diff --git a/lib/rpc.js b/lib/rpc.js
index 1f84a1582..811d7f6e0 100644
--- a/lib/rpc.js
+++ b/lib/rpc.js
@@ -22,6 +22,7 @@ const UNAUTHENTICATED_CALLS = {
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
GET_METADATA: Metadata.getMetadata,
+ ADD_FIRST_ADMIN: Admin.addFirstAdmin
};
var isUnauthenticateMessage = function (msg) {
diff --git a/lib/stats.js b/lib/stats.js
index 16c0058e8..2a8e32ca2 100644
--- a/lib/stats.js
+++ b/lib/stats.js
@@ -74,7 +74,7 @@ Stats.instanceData = function (Env) {
}
// Admins can opt-in to providing more detailed information about the extent of the instance's usage
- if (!Env.provideAggregateStatistics) {
+ if (Env.provideAggregateStatistics) {
// check how many instances provide stats before we put more work into it
data.providesAggregateStatistics = true;
}
diff --git a/package.json b/package.json
index 11d0d337b..e4b78eb94 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"get-folder-size": "^2.0.1",
"netflux-websocket": "^0.1.20",
"nthen": "0.1.8",
+ "prompt-confirm": "^2.0.4",
"pull-stream": "^3.6.1",
"saferphore": "0.0.1",
"sortify": "^1.0.4",
@@ -50,6 +51,8 @@
"test": "node scripts/TestSelenium.js",
"test-rpc": "cd scripts/tests && node test-rpc",
"evict-inactive": "node scripts/evict-inactive.js",
- "build": "node scripts/build.js"
+ "build": "node scripts/build.js",
+ "clear": "node scripts/clear.js",
+ "installtoken": "node scripts/install.js"
}
}
diff --git a/scripts/clear.js b/scripts/clear.js
new file mode 100644
index 000000000..f9ca46533
--- /dev/null
+++ b/scripts/clear.js
@@ -0,0 +1,30 @@
+var prompt = require('prompt-confirm');
+const p = new prompt('Are you sure? This will permanently delete all existing data on your instance.');
+
+const nThen = require("nthen");
+const Fs = require("fs");
+const Path = require("path");
+
+var config = require("../lib/load-config");
+var Hash = require('../www/common/common-hash');
+var Env = require("../lib/env").create(config);
+Env.Log = { error: console.log };
+
+var keyOrDefaultString = function (key, def) {
+ return Path.resolve(typeof(config[key]) === 'string'? config[key]: def);
+};
+var paths = Env.paths;
+p.ask(function (answer) {
+ if (!answer) {
+ console.log('Abort');
+ return;
+ }
+ console.log('Deleting all data...');
+ var n = nThen;
+ Object.values(paths).forEach(function (path) {
+ console.log(`Deleting ${path}`);
+ Fs.rmSync(path, { recursive: true, force: true });
+ console.log('Deleted');
+ });
+ console.log('Success');
+});
diff --git a/scripts/install.js b/scripts/install.js
new file mode 100644
index 000000000..0587ce789
--- /dev/null
+++ b/scripts/install.js
@@ -0,0 +1,41 @@
+const nThen = require("nthen");
+const Fs = require("fs");
+const Path = require("path");
+const Decrees = require("../lib/decrees");
+
+var config = require("../lib/load-config");
+var Hash = require('../www/common/common-hash');
+var Env = require("../lib/env").create(config);
+
+Env.Log = { error: console.log };
+
+var path = Path.join(Env.paths.decree, 'decree.ndjson');
+var token;
+nThen(function (w) {
+ Decrees.load(Env, w(function (err) {
+ if (err) {
+ console.error(err);
+ w.abort();
+ return;
+ }
+ if (Env.installToken) {
+ console.log('Existing token');
+ token = Env.installToken;
+ }
+ // XXX IF ADMINS ABORT?
+ }));
+}).nThen(function (w) {
+ if (Env.installToken) { return; }
+ console.log(Env.paths.decree);
+ token = Hash.createChannelId() + Hash.createChannelId();
+ var decree = ["ADD_INSTALL_TOKEN",[token],"",+new Date()];
+ Fs.appendFile(path, JSON.stringify(decree) + '\n', w(function (err) {
+ if (err) { console.log(err); return; }
+ }));
+}).nThen(function () {
+ console.log('Install token:');
+ console.log(token);
+ var url = config.httpUnsafeOrigin + '/install/';
+ console.log(`Please visit ${url} to create your first admin user`);
+
+});
diff --git a/www/checkup/main.js b/www/checkup/main.js
index 1b6e69687..47cd8807c 100644
--- a/www/checkup/main.js
+++ b/www/checkup/main.js
@@ -1530,6 +1530,7 @@ define([
[
'application_config.js',
'pages.js',
+ 'pages/index.js',
].forEach(resource => {
// sort this above errors and warnings and style in a neutral color.
var A = `/customize.dist/${resource}`;
@@ -1712,7 +1713,7 @@ define([
var href = `/customize/${asset}`;
return h('li', [
h('a', {
- href: `href?${+new Date()}`,
+ href: `${href}?${+new Date()}`,
target: '_blank',
}, href),
]);
diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js
index 6a0c08816..e5f1c12b2 100644
--- a/www/file/file-crypto.js
+++ b/www/file/file-crypto.js
@@ -148,7 +148,7 @@ define([
// if metadata is too large, drop the thumbnail.
if (plaintext.length > 65535) {
var temp = JSON.parse(JSON.stringify(metadata));
- delete metadata.thumbnail;
+ delete temp.thumbnail;
plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp));
}
diff --git a/www/install/index.html b/www/install/index.html
new file mode 100644
index 000000000..0a4a2242f
--- /dev/null
+++ b/www/install/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ CryptPad: Collaboration suite, encrypted and open-source
+
+
+
+
+
+
+
+
+
+
diff --git a/www/install/main.js b/www/install/main.js
new file mode 100644
index 000000000..e7c03e3cd
--- /dev/null
+++ b/www/install/main.js
@@ -0,0 +1,179 @@
+define([
+ 'jquery',
+ '/customize/login.js',
+ '/common/cryptpad-common.js',
+ '/common/common-credential.js',
+ '/common/common-interface.js',
+ '/common/common-util.js',
+ '/common/common-realtime.js',
+ '/common/common-constants.js',
+ '/common/common-feedback.js',
+ '/common/outer/local-store.js',
+ '/common/hyperscript.js',
+ '/customize/pages.js',
+ '/common/rpc.js',
+
+ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
+], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages, Rpc) {
+ if (window.top !== window) { return; }
+ var Messages = Cryptpad.Messages;
+ $(function () {
+ if (LocalStore.isLoggedIn()) {
+ // already logged in, redirect to drive
+ document.location.href = '/drive/';
+ return;
+ }
+
+
+ // text and password input fields
+ var $token = $('#installtoken');
+ var $uname = $('#username');
+ var $passwd = $('#password');
+ var $confirm = $('#password-confirm');
+
+ [ $token, $uname, $passwd, $confirm]
+ .some(function ($el) { if (!$el.val()) { $el.focus(); return true; } });
+
+ // checkboxes
+ var $register = $('button#register');
+
+ var registering = false;
+
+ var I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME = false;
+ var br = function () { return h('br'); };
+
+ // If the token is provided in the URL, hide the field
+ var token;
+ if (window.location.hash) {
+ var hash = window.location.hash.slice(1);
+ if (hash.length === 64) {
+ token = hash;
+ $token.hide();
+ console.log(`Install token: ${token}`);
+ }
+ }
+
+ var registerClick = function () {
+ var uname = $uname.val().trim();
+ // trim whitespace surrounding the username since it is otherwise included in key derivation
+ // most people won't realize that its presence is significant
+ $uname.val(uname);
+
+ var passwd = $passwd.val();
+ var confirmPassword = $confirm.val();
+
+ if (!token) { token = $token.val().trim(); }
+
+ var shouldImport = false;
+ var doesAccept;
+ try {
+ // if this throws there's either a horrible bug (which someone will report)
+ // or the instance admins did not configure a terms page.
+ doesAccept = true;
+ } catch (err) {
+ console.error(err);
+ }
+
+ if (Cred.isEmail(uname) && !I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME) {
+ var emailWarning = [
+ Messages.register_emailWarning0,
+ br(), br(),
+ Messages.register_emailWarning1,
+ br(), br(),
+ Messages.register_emailWarning2,
+ br(), br(),
+ Messages.register_emailWarning3,
+ ];
+
+ Feedback.send("EMAIL_USERNAME_WARNING", true);
+
+ return void UI.confirm(emailWarning, function (yes) {
+ if (!yes) { return; }
+ I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME = true;
+ registerClick();
+ });
+ }
+
+ /* basic validation */
+ if (!Cred.isLongEnoughPassword(passwd)) {
+ var warning = Messages._getKey('register_passwordTooShort', [
+ Cred.MINIMUM_PASSWORD_LENGTH
+ ]);
+ return void UI.alert(warning, function () {
+ registering = false;
+ });
+ }
+
+ if (passwd !== confirmPassword) { // do their passwords match?
+ return void UI.alert(Messages.register_passwordsDontMatch);
+ }
+
+ if (Pages.customURLs.terms && !doesAccept) { // do they accept the terms of service? (if they exist)
+ return void UI.alert(Messages.register_mustAcceptTerms);
+ }
+
+ setTimeout(function () {
+ var span = h('span', [
+ h('h2', [
+ h('i.fa.fa-warning'),
+ ' ',
+ Messages.register_warning,
+ ]),
+ Messages.register_warning_note
+ ]);
+
+ UI.confirm(span,
+ function (yes) {
+ if (!yes) { return; }
+
+ Login.loginOrRegisterUI(uname, passwd, true, shouldImport, false, function (data) {
+ var proxy = data.proxy;
+ if (!proxy || !proxy.edPublic) { UI.alert(Messages.error); return true; }
+
+ Rpc.createAnonymous(data.network, function (e, call) {
+ if (e) { UI.alert(Messages.error); return console.error(e); }
+ var anon_rpc = call;
+
+ anon_rpc.send('ADD_FIRST_ADMIN', {
+ token: token,
+ edPublic: proxy.edPublic
+ }, function (e) {
+ if (e) { UI.alert(Messages.error); return console.error(e); }
+ window.location.href = '/drive/';
+ });
+ });
+
+ return true;
+ });
+ registering = true;
+ }, {
+ ok: Messages.register_writtenPassword,
+ cancel: Messages.register_cancel,
+/* If we're certain that we aren't using these "*Class" APIs
+ anywhere else then we can deprecate them and make this a
+ custom modal in common-interface (or here). */
+ cancelClass: 'btn.btn-cancel.btn-register',
+ okClass: 'btn.btn-danger.btn-register',
+ reverseOrder: true,
+ done: function ($dialog) {
+ $dialog.find('> div').addClass('half');
+ },
+ });
+ }, 150);
+ };
+
+ $register.click(registerClick);
+
+ var clickRegister = Util.notAgainForAnother(function () {
+ $register.click();
+ }, 500);
+
+ $register.on('keypress', function (e) {
+ if (e.which === 13) {
+ e.preventDefault();
+ e.stopPropagation();
+ return clickRegister();
+ }
+ });
+ });
+});