mirror of
https://github.com/zadam/trilium.git
synced 2025-06-05 01:18:44 +02:00
implemented initial setup of the app
This commit is contained in:
parent
a3f57622ff
commit
6546548848
3
export-schema.sh
Executable file
3
export-schema.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sqlite3 ~/trilium-data/document.db .schema > schema.sql
|
34
public/javascripts/setup.js
Normal file
34
public/javascripts/setup.js
Normal 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();
|
||||
}
|
@ -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');
|
||||
|
@ -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
32
routes/api/setup.js
Normal 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;
|
@ -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
11
routes/setup.js
Normal 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
92
schema.sql
Normal 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
|
||||
);
|
@ -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
|
||||
};
|
@ -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
|
||||
};
|
@ -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
|
||||
};
|
@ -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
|
||||
};
|
@ -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
|
||||
};
|
@ -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
48
views/setup.ejs
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user