beginning of #98, new multistep wizard, db creation after user enters username and password

This commit is contained in:
azivner 2018-07-21 08:55:24 +02:00
parent 3972c27e7a
commit 6235a3c886
8 changed files with 167 additions and 90 deletions

16
bin/build-pkg.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
mkdir $PKG_DIR
pkg . --targets node8-linux-x64 --output ${PKG_DIR}/trilium
chmod +x ${PKG_DIR}/trilium
cp node_modules/sqlite3/lib/binding/node-v57-linux-x64/node_sqlite3.node ${PKG_DIR}/
cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
cd dist
7z a trilium-linux-x64-server.7z trilium-linux-x64-server

View file

@ -20,8 +20,7 @@
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
"publish-forge": "electron-forge publish",
"build-pkg": "pkg . --targets node8-linux-x64 --output dist/trilium-linux-x64-server.elf"
"publish-forge": "electron-forge publish"
},
"dependencies": {
"async-mutex": "^0.1.3",
@ -119,7 +118,10 @@
"assets": [
"./db/**/*",
"./src/public/**/*",
"./src/views/**/*"
"./src/views/**/*",
"./node_modules/mozjpeg/vendor/*",
"./node_modules/pngquant-bin/vendor/*",
"./node_modules/giflossy/vendor/*"
]
}
}

View file

@ -1,36 +1,63 @@
import server from './services/server.js';
$("#setup-form").submit(() => {
const username = $("#username").val();
const password1 = $("#password1").val();
const password2 = $("#password2").val();
function SetupModel() {
this.step = ko.observable("setup-type");
this.setupType = ko.observable();
if (!username) {
showAlert("Username can't be empty");
return false;
}
this.setupNewDocument = ko.observable(false);
this.setupSyncFromDesktop = ko.observable(false);
this.setupSyncFromServer = ko.observable(false);
if (!password1) {
showAlert("Password can't be empty");
return false;
}
this.username = ko.observable();
this.password1 = ko.observable();
this.password2 = ko.observable();
if (password1 !== password2) {
showAlert("Both password fields need be identical.");
return false;
}
this.setupTypeSelected = this.getSetupType = () =>
this.setupNewDocument()
|| this.setupSyncFromDesktop()
|| this.setupSyncFromServer();
server.post('setup', {
username: username,
password: password1
}).then(() => {
window.location.replace("/");
});
this.selectSetupType = () => {
this.step(this.getSetupType());
this.setupType(this.getSetupType());
};
return false;
});
this.back = () => this.step("setup-type");
this.finish = () => {
if (this.setupNewDocument()) {
const username = this.username();
const password1 = this.password1();
const password2 = this.password2();
if (!username) {
showAlert("Username can't be empty");
return;
}
if (!password1) {
showAlert("Password can't be empty");
return;
}
if (password1 !== password2) {
showAlert("Both password fields need be identical.");
return;
}
server.post('setup', {
username: username,
password: password1
}).then(() => {
window.location.replace("/");
});
}
};
}
function showAlert(message) {
$("#alert").html(message);
$("#alert").show();
}
}
ko.applyBindings(new SetupModel(), document.getElementById('setup-dialog'));

View file

@ -1,25 +1,11 @@
"use strict";
const optionService = require('../../services/options');
const sqlInit = require('../../services/sql_init');
const utils = require('../../services/utils');
const myScryptService = require('../../services/my_scrypt');
const passwordEncryptionService = require('../../services/password_encryption');
async function setup(req) {
const { username, password } = req.body;
await optionService.setOption('username', username);
await optionService.setOption('passwordVerificationSalt', utils.randomSecureToken(32));
await optionService.setOption('passwordDerivedKeySalt', utils.randomSecureToken(32));
const passwordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(password));
await optionService.setOption('passwordVerificationHash', passwordVerificationKey);
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16));
sqlInit.setDbReadyAsResolved();
await sqlInit.createInitialDatabase(username, password);
}
module.exports = {

View file

@ -1,10 +1,5 @@
const repository = require('./repository');
const utils = require('./utils');
const dateUtils = require('./date_utils');
const appInfo = require('./app_info');
async function getOption(name) {
const option = await repository.getOption(name);
const option = await require('./repository').getOption(name);
if (!option) {
throw new Error("Option " + name + " doesn't exist");
@ -14,7 +9,7 @@ async function getOption(name) {
}
async function setOption(name, value) {
const option = await repository.getOption(name);
const option = await require('./repository').getOption(name);
if (!option) {
throw new Error(`Option ${name} doesn't exist`);
@ -36,32 +31,8 @@ async function createOption(name, value, isSynced) {
}).save();
}
async function initOptions(startNotePath) {
await createOption('documentId', utils.randomSecureToken(16), false);
await createOption('documentSecret', utils.randomSecureToken(16), false);
await createOption('username', '', true);
await createOption('passwordVerificationHash', '', true);
await createOption('passwordVerificationSalt', '', true);
await createOption('passwordDerivedKeySalt', '', true);
await createOption('encryptedDataKey', '', true);
await createOption('encryptedDataKeyIv', '', true);
await createOption('startNotePath', startNotePath, false);
await createOption('protectedSessionTimeout', 600, true);
await createOption('noteRevisionSnapshotTimeInterval', 600, true);
await createOption('lastBackupDate', dateUtils.nowDate(), false);
await createOption('dbVersion', appInfo.dbVersion, false);
await createOption('lastSyncedPull', appInfo.dbVersion, false);
await createOption('lastSyncedPush', 0, false);
await createOption('zoomFactor', 1.0, false);
await createOption('theme', 'white', false);
}
module.exports = {
getOption,
setOption,
initOptions
createOption
};

View file

@ -0,0 +1,41 @@
const optionService = require('./options');
const passwordEncryptionService = require('./password_encryption');
const myScryptService = require('./my_scrypt');
const appInfo = require('./app_info');
const utils = require('./utils');
const dateUtils = require('./date_utils');
async function initOptions(startNotePath, username, password) {
await optionService.createOption('documentId', utils.randomSecureToken(16), false);
await optionService.createOption('documentSecret', utils.randomSecureToken(16), false);
await optionService.createOption('startNotePath', startNotePath, false);
await optionService.createOption('protectedSessionTimeout', 600, true);
await optionService.createOption('noteRevisionSnapshotTimeInterval', 600, true);
await optionService.createOption('lastBackupDate', dateUtils.nowDate(), false);
await optionService.createOption('dbVersion', appInfo.dbVersion, false);
await optionService.createOption('lastSyncedPull', appInfo.dbVersion, false);
await optionService.createOption('lastSyncedPush', 0, false);
await optionService.createOption('zoomFactor', 1.0, false);
await optionService.createOption('theme', 'white', false);
await optionService.createOption('username', username);
await optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32));
await optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32));
const passwordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(password));
await optionService.createOption('passwordVerificationHash', passwordVerificationKey);
// passwordEncryptionService expects these options to already exist
await optionService.createOption('encryptedDataKey', '');
await optionService.createOption('encryptedDataKeyIv', '');
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16));
}
module.exports = {
initOptions
};

View file

@ -5,6 +5,7 @@ const sqlite = require('sqlite');
const resourceDir = require('./resource_dir');
const appInfo = require('./app_info');
const sql = require('./sql');
const options = require('./options');
const cls = require('./cls');
async function createConnection() {
@ -30,7 +31,9 @@ const dbReady = new Promise((resolve, reject) => {
const tableResults = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'");
if (tableResults.length !== 1) {
await createInitialDatabase();
log.info("DB not found, please visit setup page to initialize Trilium.");
return;
}
schemaReadyResolve();
@ -52,7 +55,7 @@ const dbReady = new Promise((resolve, reject) => {
});
});
async function createInitialDatabase() {
async function createInitialDatabase(username, password) {
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
@ -70,14 +73,13 @@ async function createInitialDatabase() {
const startNoteId = await sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
await require('./options').initOptions(startNoteId);
await require('./options_init').initOptions(startNoteId, username, password);
await require('./sync_table').fillAllSyncRows();
});
log.info("Schema and initial content generated. Waiting for user to enter username/password to finish setup.");
// we don't resolve dbReady promise because user needs to setup the username and password to initialize
// the database
setDbReadyAsResolved();
}
function setDbReadyAsResolved() {
@ -113,5 +115,6 @@ module.exports = {
schemaReady,
isUserInitialized,
setDbReadyAsResolved,
isDbUpToDate
isDbUpToDate,
createInitialDatabase
};

View file

@ -5,31 +5,60 @@
<title>Setup</title>
</head>
<body>
<div style="width: 500px; margin: auto;">
<div id="setup-dialog" style="width: 500px; margin: auto;">
<h1>Trilium Notes setup</h1>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<p>You're almost done with the setup. That last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<div id="setup-type" data-bind="visible: step() == 'setup-type'">
<div class="radio">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupNewDocument">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupSyncFromDesktop">
I have server instance up and I want to setup sync with it</label>
</div>
<div class="radio">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupSyncFromServer">
I have desktop instance already and I want to setup sync with it</label>
</div>
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<div data-bind="visible: step() == 'new-document'">
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<form id="setup-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" placeholder="Arbitrary string">
<input type="text" class="form-control" data-bind="value: username" placeholder="Arbitrary string">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" id="password1" placeholder="Password">
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" id="password2" placeholder="Password">
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
</div>
<button type="submit" class="btn btn-default">Save</button>
</form>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
&nbsp;
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div data-bind="visible: step() == 'sync-from-desktop'">
sync from desktop
</div>
<div data-bind="visible: step() == 'sync-from-server'">
sync from server
</div>
</div>
<script type="text/javascript">
@ -47,6 +76,8 @@
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.js"></script>
<script src="/libraries/knockout.min.js"></script>
<script src="javascripts/setup.js" type="module"></script>
</body>
</html>