implemented initial setup of the app

This commit is contained in:
azivner 2017-12-03 22:29:23 -05:00
parent a3f57622ff
commit 6546548848
15 changed files with 316 additions and 30 deletions

3
export-schema.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
sqlite3 ~/trilium-data/document.db .schema > schema.sql

View File

@ -0,0 +1,34 @@
$("#setup-form").submit(() => {
const username = $("#username").val();
const password1 = $("#password1").val();
const password2 = $("#password2").val();
if (!username) {
showAlert("Username can't be empty");
return false;
}
if (!password1) {
showAlert("Password can't be empty");
return false;
}
if (password1 !== password2) {
showAlert("Both password fields need be identical.");
return false;
}
server.post('setup', {
username: username,
password: password1
}).then(() => {
window.location.replace("/");
});
return false;
});
function showAlert(message) {
$("#alert").html(message);
$("#alert").show();
}

View File

@ -9,6 +9,7 @@ const source_id = require('../../services/source_id');
const auth = require('../../services/auth');
const password_encryption = require('../../services/password_encryption');
const protected_session = require('../../services/protected_session');
const app_info = require('../../services/app_info');
router.post('/sync', async (req, res, next) => {
const timestamp = req.body.timestamp;
@ -22,9 +23,9 @@ router.post('/sync', async (req, res, next) => {
const dbVersion = req.body.dbVersion;
if (dbVersion !== migration.APP_DB_VERSION) {
if (dbVersion !== app_info.db_version) {
res.status(400);
res.send({ message: 'Non-matching db versions, local is version ' + migration.APP_DB_VERSION });
res.send({ message: 'Non-matching db versions, local is version ' + app_info.db_version });
}
const documentSecret = await options.getOption('document_secret');

View File

@ -5,11 +5,12 @@ const router = express.Router();
const auth = require('../../services/auth');
const options = require('../../services/options');
const migration = require('../../services/migration');
const app_info = require('../../services/app_info');
router.get('', auth.checkApiAuthForMigrationPage, async (req, res, next) => {
res.send({
db_version: parseInt(await options.getOption('db_version')),
app_db_version: migration.APP_DB_VERSION
app_db_version: app_info.db_version
});
});

32
routes/api/setup.js Normal file
View File

@ -0,0 +1,32 @@
"use strict";
const express = require('express');
const router = express.Router();
const auth = require('../../services/auth');
const options = require('../../services/options');
const sql = require('../../services/sql');
const utils = require('../../services/utils');
const my_scrypt = require('../../services/my_scrypt');
const password_encryption = require('../../services/password_encryption');
router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
const { username, password } = req.body;
await sql.doInTransaction(async () => {
await options.setOption('username', username);
await options.setOption('password_verification_salt', utils.randomSecureToken(32));
await options.setOption('password_derived_key_salt', utils.randomSecureToken(32));
const passwordVerificationKey = utils.toBase64(await my_scrypt.getVerificationHash(password));
await options.setOption('password_verification_hash', passwordVerificationKey);
await password_encryption.setDataKey(password, utils.randomSecureToken(16));
});
sql.setDbReadyAsResolved();
res.send({});
});
module.exports = router;

View File

@ -2,6 +2,7 @@ const indexRoute = require('./index');
const loginRoute = require('./login');
const logoutRoute = require('./logout');
const migrationRoute = require('./migration');
const setupRoute = require('./setup');
// API routes
const treeApiRoute = require('./api/tree');
@ -19,12 +20,14 @@ const recentNotesRoute = require('./api/recent_notes');
const appInfoRoute = require('./api/app_info');
const exportRoute = require('./api/export');
const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup');
function register(app) {
app.use('/', indexRoute);
app.use('/login', loginRoute);
app.use('/logout', logoutRoute);
app.use('/migration', migrationRoute);
app.use('/setup', setupRoute);
app.use('/api/tree', treeApiRoute);
app.use('/api/notes', notesApiRoute);
@ -41,6 +44,7 @@ function register(app) {
app.use('/api/app-info', appInfoRoute);
app.use('/api/export', exportRoute);
app.use('/api/import', importRoute);
app.use('/api/setup', setupApiRoute);
}
module.exports = {

11
routes/setup.js Normal file
View File

@ -0,0 +1,11 @@
"use strict";
const express = require('express');
const router = express.Router();
const auth = require('../services/auth');
router.get('', auth.checkAppNotInitialized, (req, res, next) => {
res.render('setup', {});
});
module.exports = router;

92
schema.sql Normal file
View File

@ -0,0 +1,92 @@
CREATE TABLE `migrations` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`version` INTEGER NOT NULL,
`success` INTEGER NOT NULL,
`error` TEXT
);
CREATE TABLE `sync` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entity_name` TEXT NOT NULL,
`entity_id` TEXT NOT NULL,
`sync_date` INTEGER NOT NULL
, source_id TEXT);
CREATE UNIQUE INDEX `IDX_sync_entity_name_id` ON `sync` (
`entity_name`,
`entity_id`
);
CREATE INDEX `IDX_sync_sync_date` ON `sync` (
`sync_date`
);
CREATE TABLE IF NOT EXISTS "options" (
`opt_name` TEXT NOT NULL PRIMARY KEY,
`opt_value` TEXT,
`date_modified` INT
);
CREATE TABLE `event_log` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`note_id` TEXT,
`comment` TEXT,
`date_added` INTEGER NOT NULL
);
CREATE INDEX `IDX_event_log_date_added` ON `event_log` (
`date_added`
);
CREATE TABLE IF NOT EXISTS "notes" (
`note_id` TEXT NOT NULL,
`note_title` TEXT,
`note_text` TEXT,
`date_created` INT,
`date_modified` INT,
`is_protected` INT NOT NULL DEFAULT 0,
`is_deleted` INT NOT NULL DEFAULT 0,
PRIMARY KEY(`note_id`)
);
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
`is_deleted`
);
CREATE TABLE IF NOT EXISTS "notes_history" (
`note_history_id` TEXT NOT NULL PRIMARY KEY,
`note_id` TEXT NOT NULL,
`note_title` TEXT,
`note_text` TEXT,
`is_protected` INT,
`date_modified_from` INT,
`date_modified_to` INT
);
CREATE INDEX `IDX_notes_history_note_id` ON `notes_history` (
`note_id`
);
CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` (
`date_modified_from`
);
CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` (
`date_modified_to`
);
CREATE TABLE `source_ids` (
`source_id` TEXT NOT NULL,
`date_created` INTEGER NOT NULL,
PRIMARY KEY(`source_id`)
);
CREATE TABLE IF NOT EXISTS "notes_tree" (
[note_tree_id] VARCHAR(30) PRIMARY KEY NOT NULL,
[note_id] VARCHAR(30) NOT NULL,
[note_pid] VARCHAR(30) NOT NULL,
[note_pos] INTEGER NOT NULL,
[is_expanded] BOOLEAN NULL ,
date_modified INTEGER NOT NULL DEFAULT 0,
is_deleted INTEGER NOT NULL DEFAULT 0
, `prefix` TEXT);
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
`note_tree_id`
);
CREATE INDEX `IDX_notes_tree_note_id_note_pid` ON `notes_tree` (
`note_id`,
`note_pid`
);
CREATE TABLE `recent_notes` (
'note_tree_id'TEXT NOT NULL PRIMARY KEY,
`note_path` TEXT NOT NULL,
`date_accessed` INTEGER NOT NULL ,
is_deleted INT
);

View File

@ -2,11 +2,12 @@
const build = require('./build');
const packageJson = require('../package');
const migration = require('./migration');
const APP_DB_VERSION = 48;
module.exports = {
app_version: packageJson.version,
db_version: migration.APP_DB_VERSION,
db_version: APP_DB_VERSION,
build_date: build.build_date,
build_revision: build.build_revision
};

View File

@ -2,16 +2,22 @@
const migration = require('./migration');
const utils = require('./utils');
const options = require('./options');
async function checkAuth(req, res, next) {
if (!req.session.loggedIn && !utils.isElectron()) {
const username = await options.getOption('username');
if (!username) {
res.redirect("setup");
}
else if (!req.session.loggedIn && !utils.isElectron()) {
res.redirect("login");
}
else if (await migration.isDbUpToDate()) {
next();
else if (!await migration.isDbUpToDate()) {
res.redirect("migration");
}
else {
res.redirect("migration");
next();
}
}
@ -45,9 +51,21 @@ async function checkApiAuthForMigrationPage(req, res, next) {
}
}
async function checkAppNotInitialized(req, res, next) {
const username = await options.getOption('username');
if (username) {
res.status(400).send("App already initialized.");
}
else {
next();
}
}
module.exports = {
checkAuth,
checkAuthForMigrationPage,
checkApiAuth,
checkApiAuthForMigrationPage
checkApiAuthForMigrationPage,
checkAppNotInitialized
};

View File

@ -3,8 +3,8 @@ const sql = require('./sql');
const options = require('./options');
const fs = require('fs-extra');
const log = require('./log');
const app_info = require('./app_info');
const APP_DB_VERSION = 48;
const MIGRATIONS_DIR = "migrations";
async function migrate() {
@ -84,11 +84,10 @@ async function migrate() {
async function isDbUpToDate() {
const dbVersion = parseInt(await options.getOption('db_version'));
return dbVersion >= APP_DB_VERSION;
return dbVersion >= app_info.db_version;
}
module.exports = {
migrate,
isDbUpToDate,
APP_DB_VERSION
isDbUpToDate
};

View File

@ -1,6 +1,7 @@
const sql = require('./sql');
const utils = require('./utils');
const sync_table = require('./sync_table');
const app_info = require('./app_info');
const SYNCED_OPTIONS = [ 'username', 'password_verification_hash', 'encrypted_data_key', 'protected_session_timeout',
'history_snapshot_time_interval' ];
@ -20,21 +21,38 @@ async function setOption(optName, optValue) {
await sync_table.addOptionsSync(optName);
}
await sql.execute("UPDATE options SET opt_value = ?, date_modified = ? WHERE opt_name = ?",
[optValue, utils.nowTimestamp(), optName]);
await sql.replace("options", {
opt_name: optName,
opt_value: optValue,
date_modified: utils.nowTimestamp()
});
}
sql.dbReady.then(async () => {
if (!await getOption('document_id') || !await getOption('document_secret')) {
await sql.doInTransaction(async () => {
await setOption('document_id', utils.randomSecureToken(16));
await setOption('document_secret', utils.randomSecureToken(16));
});
}
});
async function initOptions() {
await setOption('document_id', utils.randomSecureToken(16));
await setOption('document_secret', utils.randomSecureToken(16));
await setOption('username', '');
await setOption('password_verification_hash', '');
await setOption('password_verification_salt', '');
await setOption('password_derived_key_salt', '');
await setOption('encrypted_data_key', '');
await setOption('encrypted_data_key_iv', '');
await setOption('start_note_tree_id', '');
await setOption('protected_session_timeout', 600);
await setOption('history_snapshot_time_interval', 600);
await setOption('last_backup_date', utils.nowTimestamp());
await setOption('db_version', app_info.db_version);
await setOption('last_synced_pull', app_info.db_version);
await setOption('last_synced_push', 0);
await setOption('last_synced_push', 0);
}
module.exports = {
getOption,
setOption,
initOptions,
SYNCED_OPTIONS
};

View File

@ -11,15 +11,33 @@ async function createConnection() {
const dbConnected = createConnection();
let dbReadyResolve = null;
const dbReady = new Promise((resolve, reject) => {
dbConnected.then(async db => {
dbReadyResolve = () => resolve(db);
const tableResults = await getResults("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'");
if (tableResults.length !== 1) {
console.log("No connection to initialized DB.");
process.exit(1);
}
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
resolve(db);
const schema = fs.readFileSync('schema.sql', 'UTF-8');
await doInTransaction(async () => {
await executeScript(schema);
await require('./options').initOptions();
});
// we don't resolve dbReady promise because user needs to setup the username and password to initialize
// the database
}
else {
const username = await getSingleValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
if (username) {
resolve(db);
}
}
})
.catch(e => {
console.log("Error connecting to DB.", e);
@ -27,6 +45,10 @@ const dbReady = new Promise((resolve, reject) => {
});
});
function setDbReadyAsResolved() {
dbReadyResolve();
}
async function insert(table_name, rec, replace = false) {
const keys = Object.keys(rec);
if (keys.length === 0) {
@ -181,5 +203,6 @@ module.exports = {
getFlattenedResults,
execute,
executeScript,
doInTransaction
doInTransaction,
setDbReadyAsResolved
};

View File

@ -13,6 +13,7 @@ const syncUpdate = require('./sync_update');
const content_hash = require('./content_hash');
const event_log = require('./event_log');
const fs = require('fs');
const app_info = require('./app_info');
const SYNC_SERVER = config['Sync']['syncServerHost'];
const isSyncSetup = !!SYNC_SERVER;
@ -94,7 +95,7 @@ async function login() {
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
dbVersion: migration.APP_DB_VERSION,
dbVersion: app_info.db_version,
hash: hash
});

48
views/setup.ejs Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Setup</title>
</head>
<body>
<div style="width: 500px; margin: auto;">
<h1>Trilium setup</h1>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<form id="setup-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" placeholder="Arbitrary string">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" id="password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" id="password2" placeholder="Password">
</div>
<button type="submit" class="btn btn-default">Save</button>
</form>
</div>
<script type="text/javascript">
const baseApiUrl = 'api/';
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="libraries/jquery.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.js"></script>
<script src="javascripts/setup.js"></script>
<script src="javascripts/utils.js"></script>
<script src="javascripts/server.js"></script>
</body>
</html>