diff --git a/export-schema.sh b/export-schema.sh new file mode 100755 index 000000000..95cc7655d --- /dev/null +++ b/export-schema.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +sqlite3 ~/trilium-data/document.db .schema > schema.sql \ No newline at end of file diff --git a/public/javascripts/setup.js b/public/javascripts/setup.js new file mode 100644 index 000000000..ddc9fa9c4 --- /dev/null +++ b/public/javascripts/setup.js @@ -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(); +} \ No newline at end of file diff --git a/routes/api/login.js b/routes/api/login.js index ea729254c..e09063e57 100644 --- a/routes/api/login.js +++ b/routes/api/login.js @@ -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'); diff --git a/routes/api/migration.js b/routes/api/migration.js index cae0c1c4d..9d75ceab2 100644 --- a/routes/api/migration.js +++ b/routes/api/migration.js @@ -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 }); }); diff --git a/routes/api/setup.js b/routes/api/setup.js new file mode 100644 index 000000000..ea31c84a7 --- /dev/null +++ b/routes/api/setup.js @@ -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; \ No newline at end of file diff --git a/routes/routes.js b/routes/routes.js index 37aa816b4..888dbfc0a 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -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 = { diff --git a/routes/setup.js b/routes/setup.js new file mode 100644 index 000000000..083c90611 --- /dev/null +++ b/routes/setup.js @@ -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; diff --git a/schema.sql b/schema.sql new file mode 100644 index 000000000..3dbe8b647 --- /dev/null +++ b/schema.sql @@ -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 +); diff --git a/services/app_info.js b/services/app_info.js index 74bab1ec0..4dba8a793 100644 --- a/services/app_info.js +++ b/services/app_info.js @@ -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 }; \ No newline at end of file diff --git a/services/auth.js b/services/auth.js index 9ccb5da35..ca5d0a9c7 100644 --- a/services/auth.js +++ b/services/auth.js @@ -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 }; \ No newline at end of file diff --git a/services/migration.js b/services/migration.js index 39771eac4..ce6e88e56 100644 --- a/services/migration.js +++ b/services/migration.js @@ -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 }; \ No newline at end of file diff --git a/services/options.js b/services/options.js index c6e236e21..839838409 100644 --- a/services/options.js +++ b/services/options.js @@ -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 }; \ No newline at end of file diff --git a/services/sql.js b/services/sql.js index d98261282..611760ffe 100644 --- a/services/sql.js +++ b/services/sql.js @@ -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 }; \ No newline at end of file diff --git a/services/sync.js b/services/sync.js index 631c9a235..91d350eb3 100644 --- a/services/sync.js +++ b/services/sync.js @@ -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 }); diff --git a/views/setup.ejs b/views/setup.ejs new file mode 100644 index 000000000..8974ef94e --- /dev/null +++ b/views/setup.ejs @@ -0,0 +1,48 @@ + + +
+ +