Merge branch 'soon' into staging

This commit is contained in:
ansuz 2023-01-11 13:26:36 +05:30
commit 9effb40f9f
15 changed files with 545 additions and 10 deletions

View File

@ -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();

View File

@ -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 = "<ul class=\"cp-notes-list\"><li>Create your first admin account using this form.</li>" +
"<li>Please note your password carefully. <span class=\"red\">If you lose it there is no way we can recover your data.</span></li></ul>"; // 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)
])
]),
])
]);
};
});

View File

@ -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;
}
}
}
}
}
}

View File

@ -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)) {

View File

@ -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));

View File

@ -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;
};
// [<command>, <args>, <author>, <time>]
var handleCommand = Decrees.handleCommand = function (Env, line) {
var command = line[0];

View File

@ -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) {

View File

@ -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;
}

View File

@ -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"
}
}

30
scripts/clear.js Normal file
View File

@ -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');
});

41
scripts/install.js Normal file
View File

@ -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`);
});

View File

@ -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),
]);

View File

@ -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));
}

15
www/install/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
</head>
<body class="html">
<noscript></noscript>

179
www/install/main.js Normal file
View File

@ -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();
}
});
});
});