mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 09:58:32 +02:00
remove old python backend files
This commit is contained in:
parent
06328929ec
commit
f49963dc67
54
src/app.py
54
src/app.py
@ -1,54 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
from flask_cors import CORS
|
|
||||||
|
|
||||||
import config_provider
|
|
||||||
import routes
|
|
||||||
from audit_api import audit_api
|
|
||||||
from migration_api import migration_api
|
|
||||||
from notes_api import notes_api
|
|
||||||
from notes_history_api import notes_history_api
|
|
||||||
from notes_move_api import notes_move_api
|
|
||||||
from password_api import password_api
|
|
||||||
from settings_api import settings_api
|
|
||||||
from sql import connect, get_option
|
|
||||||
from tree_api import tree_api
|
|
||||||
|
|
||||||
config = config_provider.get_config()
|
|
||||||
|
|
||||||
documentPath = config['Document']['documentPath']
|
|
||||||
connect(documentPath)
|
|
||||||
|
|
||||||
flask_secret_key = get_option("flask_secret_key")
|
|
||||||
|
|
||||||
if not flask_secret_key:
|
|
||||||
print("Application has not been setup yet. Run 'python setup.py' to finish setup.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = flask_secret_key
|
|
||||||
app.register_blueprint(routes.routes)
|
|
||||||
app.register_blueprint(tree_api)
|
|
||||||
app.register_blueprint(notes_api)
|
|
||||||
app.register_blueprint(notes_move_api)
|
|
||||||
app.register_blueprint(password_api)
|
|
||||||
app.register_blueprint(settings_api)
|
|
||||||
app.register_blueprint(notes_history_api)
|
|
||||||
app.register_blueprint(audit_api)
|
|
||||||
app.register_blueprint(migration_api)
|
|
||||||
|
|
||||||
CORS(app)
|
|
||||||
|
|
||||||
routes.init(app)
|
|
||||||
|
|
||||||
port = config['Network']['port']
|
|
||||||
https = config['Network']['https']
|
|
||||||
certPath = config['Network']['certPath']
|
|
||||||
certKeyPath = config['Network']['certKeyPath']
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
ssl_context = None
|
|
||||||
|
|
||||||
if https == "true":
|
|
||||||
ssl_context = (certPath, certKeyPath)
|
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=port, ssl_context = ssl_context)
|
|
@ -1,19 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify
|
|
||||||
from flask import request
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
from sql import getSingleResult
|
|
||||||
|
|
||||||
audit_api = Blueprint('audit_api', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@audit_api.route('/api/audit/<int:full_load_time>', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_note(full_load_time):
|
|
||||||
browser_id = request.headers['x-browser-id']
|
|
||||||
|
|
||||||
count = getSingleResult("SELECT COUNT(*) AS 'count' FROM audit_log WHERE browser_id != ? AND date_modified >= ?", [browser_id, full_load_time])['count']
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'changed': count > 0
|
|
||||||
})
|
|
@ -1,9 +0,0 @@
|
|||||||
UPDATE_CONTENT = 'CONTENT'
|
|
||||||
UPDATE_TITLE = 'TITLE'
|
|
||||||
CHANGE_POSITION = 'POSITION'
|
|
||||||
CREATE_NOTE = 'CREATE'
|
|
||||||
DELETE_NOTE = 'DELETE'
|
|
||||||
CHANGE_PARENT = 'PARENT'
|
|
||||||
ENCRYPTION = 'ENCRYPTION'
|
|
||||||
CHANGE_PASSWORD = 'PASSWORD'
|
|
||||||
SETTINGS = 'SETTINGS'
|
|
@ -1,53 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
from shutil import copyfile
|
|
||||||
|
|
||||||
import config_provider
|
|
||||||
import utils
|
|
||||||
from sql import get_option, set_option, commit
|
|
||||||
|
|
||||||
|
|
||||||
def regular_backup():
|
|
||||||
now = utils.now_timestamp()
|
|
||||||
last_backup_date = int(get_option('last_backup_date'))
|
|
||||||
|
|
||||||
if now - last_backup_date > 43200:
|
|
||||||
backup_now()
|
|
||||||
|
|
||||||
cleanup_old_backups()
|
|
||||||
|
|
||||||
|
|
||||||
def backup_now():
|
|
||||||
now = utils.now_timestamp()
|
|
||||||
|
|
||||||
config = config_provider.get_config()
|
|
||||||
|
|
||||||
document_path = config['Document']['documentPath']
|
|
||||||
backup_directory = config['Backup']['backupDirectory']
|
|
||||||
|
|
||||||
date_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
|
||||||
|
|
||||||
copyfile(document_path, backup_directory + "/" + "backup-" + date_str + ".db")
|
|
||||||
|
|
||||||
set_option('last_backup_date', now)
|
|
||||||
commit()
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_old_backups():
|
|
||||||
now = datetime.utcnow()
|
|
||||||
config = config_provider.get_config()
|
|
||||||
backup_directory = config['Backup']['backupDirectory']
|
|
||||||
|
|
||||||
for file in os.listdir(backup_directory):
|
|
||||||
match = re.search(r"backup-([0-9 -:]+)\.db", file)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
date_str = match.group(1)
|
|
||||||
|
|
||||||
date = datetime.strptime(date_str, "%Y-%m-%d %H:%M")
|
|
||||||
|
|
||||||
if (now - date).days > 30:
|
|
||||||
print("Removing old backup - " + file)
|
|
||||||
|
|
||||||
os.remove(backup_directory + "/" + file)
|
|
@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
|
|
||||||
import getpass
|
|
||||||
|
|
||||||
import src.my_scrypt
|
|
||||||
import src.sql
|
|
||||||
import src.change_password
|
|
||||||
|
|
||||||
config = src.config_provider.getConfig()
|
|
||||||
src.sql.connect(config['Document']['documentPath'])
|
|
||||||
|
|
||||||
current_password = getpass.getpass(prompt="Enter current password: ")
|
|
||||||
|
|
||||||
new_password1 = getpass.getpass(prompt="Enter new password: ")
|
|
||||||
new_password2 = getpass.getpass(prompt="Repeat the same password: ")
|
|
||||||
|
|
||||||
if new_password1 != new_password2:
|
|
||||||
print('Entered passwords are not identical!')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
ret = src.change_password.change_password(current_password, new_password1)
|
|
||||||
|
|
||||||
if (ret['success']):
|
|
||||||
print("Changes committed. All encrypted notes were re-encrypted successfully with new password key.")
|
|
||||||
print("You can now start application and login with new password.")
|
|
||||||
else:
|
|
||||||
print(ret['message'])
|
|
@ -1,60 +0,0 @@
|
|||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from Crypto.Util import Counter
|
|
||||||
|
|
||||||
import audit_category
|
|
||||||
import my_scrypt
|
|
||||||
import sql
|
|
||||||
|
|
||||||
|
|
||||||
def change_password(current_password, new_password, request = None):
|
|
||||||
current_password_hash = base64.b64encode(my_scrypt.get_verification_hash(current_password))
|
|
||||||
|
|
||||||
if current_password_hash != sql.get_option('password_verification_hash'):
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': "Given current password doesn't match hash"
|
|
||||||
}
|
|
||||||
|
|
||||||
current_password_derived_key = my_scrypt.get_password_derived_key(current_password)
|
|
||||||
|
|
||||||
new_password_verification_key = base64.b64encode(my_scrypt.get_verification_hash(new_password))
|
|
||||||
new_password_encryption_key = my_scrypt.get_password_derived_key(new_password)
|
|
||||||
|
|
||||||
def decrypt(encrypted_base64):
|
|
||||||
encrypted_bytes = base64.b64decode(encrypted_base64)
|
|
||||||
|
|
||||||
aes = get_aes(current_password_derived_key)
|
|
||||||
return aes.decrypt(encrypted_bytes)[4:]
|
|
||||||
|
|
||||||
def encrypt(plain_text):
|
|
||||||
aes = get_aes(new_password_encryption_key)
|
|
||||||
|
|
||||||
digest = hashlib.sha256(plain_text).digest()[:4]
|
|
||||||
|
|
||||||
encrypted_bytes = aes.encrypt(digest + plain_text)
|
|
||||||
|
|
||||||
return base64.b64encode(encrypted_bytes)
|
|
||||||
|
|
||||||
def get_aes(key):
|
|
||||||
return AES.new(key, AES.MODE_CTR, counter=Counter.new(128, initial_value=5))
|
|
||||||
|
|
||||||
encrypted_data_key = sql.get_option('encrypted_data_key')
|
|
||||||
|
|
||||||
decrypted_data_key = decrypt(encrypted_data_key)
|
|
||||||
|
|
||||||
new_encrypted_data_key = encrypt(decrypted_data_key)
|
|
||||||
|
|
||||||
sql.set_option('encrypted_data_key', new_encrypted_data_key)
|
|
||||||
|
|
||||||
sql.set_option('password_verification_hash', new_password_verification_key)
|
|
||||||
|
|
||||||
sql.add_audit(audit_category.CHANGE_PASSWORD, request)
|
|
||||||
|
|
||||||
sql.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'new_encrypted_data_key': new_encrypted_data_key
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import configparser
|
|
||||||
|
|
||||||
def get_config():
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read('config.ini')
|
|
||||||
|
|
||||||
return config
|
|
@ -1,74 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
from sql import get_option, set_option, commit, execute_script
|
|
||||||
|
|
||||||
import backup
|
|
||||||
|
|
||||||
APP_DB_VERSION = 0
|
|
||||||
|
|
||||||
MIGRATIONS_DIR = "src/migrations"
|
|
||||||
|
|
||||||
migration_api = Blueprint('migration_api', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@migration_api.route('/api/migration', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_migration_info():
|
|
||||||
return jsonify({
|
|
||||||
'db_version': int(get_option('db_version')),
|
|
||||||
'app_db_version': APP_DB_VERSION
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@migration_api.route('/api/migration', methods = ['POST'])
|
|
||||||
@login_required
|
|
||||||
def run_migration():
|
|
||||||
migrations = []
|
|
||||||
|
|
||||||
backup.backup_now()
|
|
||||||
|
|
||||||
current_db_version = int(get_option('db_version'))
|
|
||||||
|
|
||||||
for file in os.listdir(MIGRATIONS_DIR):
|
|
||||||
match = re.search(r"([0-9]{4})__([a-zA-Z0-9_ ]+)\.sql", file)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
db_version = int(match.group(1))
|
|
||||||
|
|
||||||
if db_version > current_db_version:
|
|
||||||
name = match.group(2)
|
|
||||||
|
|
||||||
migration_record = {
|
|
||||||
'db_version': db_version,
|
|
||||||
'name': name
|
|
||||||
}
|
|
||||||
|
|
||||||
migrations.append(migration_record)
|
|
||||||
|
|
||||||
with open(MIGRATIONS_DIR + "/" + file, 'r') as sql_file:
|
|
||||||
sql = sql_file.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
execute_script(sql)
|
|
||||||
|
|
||||||
set_option('db_version', db_version)
|
|
||||||
commit()
|
|
||||||
|
|
||||||
migration_record['success'] = True
|
|
||||||
except:
|
|
||||||
migration_record['success'] = False
|
|
||||||
migration_record['error'] = traceback.format_exc()
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
migrations.sort(key=lambda x: x['db_version'])
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'migrations': migrations
|
|
||||||
})
|
|
@ -1,30 +0,0 @@
|
|||||||
import scrypt # pip install scrypt
|
|
||||||
|
|
||||||
import sql
|
|
||||||
|
|
||||||
|
|
||||||
def get_verification_hash(password):
|
|
||||||
salt = sql.get_option('password_verification_salt')
|
|
||||||
|
|
||||||
return get_scrypt_hash(password, salt)
|
|
||||||
|
|
||||||
|
|
||||||
def get_password_derived_key(password):
|
|
||||||
salt = sql.get_option('password_derived_key_salt')
|
|
||||||
|
|
||||||
return get_scrypt_hash(password, salt)
|
|
||||||
|
|
||||||
|
|
||||||
def get_scrypt_hash(password, salt):
|
|
||||||
# scrypt doesn't like unicode strings
|
|
||||||
password = password.encode('ascii', 'ignore')
|
|
||||||
salt = salt.encode('ascii', 'ignore')
|
|
||||||
|
|
||||||
hashed = scrypt.hash(password=password,
|
|
||||||
salt=salt,
|
|
||||||
N=16384,
|
|
||||||
r=8,
|
|
||||||
p=1,
|
|
||||||
buflen=32)
|
|
||||||
|
|
||||||
return hashed
|
|
199
src/notes_api.py
199
src/notes_api.py
@ -1,199 +0,0 @@
|
|||||||
import base64
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
|
||||||
from flask import request
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
import audit_category
|
|
||||||
import utils
|
|
||||||
from sql import delete
|
|
||||||
from sql import execute, insert, commit
|
|
||||||
from sql import getResults, getSingleResult, get_option, add_audit, deleteRecentAudits
|
|
||||||
|
|
||||||
notes_api = Blueprint('notes_api', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@notes_api.route('/api/notes/<string:note_id>', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_note(note_id):
|
|
||||||
execute("update options set opt_value = ? where opt_name = 'start_node'", [note_id])
|
|
||||||
|
|
||||||
detail = getSingleResult("select * from notes where note_id = ?", [note_id])
|
|
||||||
|
|
||||||
if detail['note_clone_id']:
|
|
||||||
note_id = detail['note_clone_id']
|
|
||||||
detail = getSingleResult("select * from notes where note_id = ?", [note_id])
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'detail': detail,
|
|
||||||
'formatting': getResults("select * from formatting where note_id = ? order by note_offset", [note_id]),
|
|
||||||
'links': getResults("select * from links where note_id = ? order by note_offset", [note_id]),
|
|
||||||
'images': getResults("select * from images where note_id = ? order by note_offset", [note_id])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_api.route('/api/notes/<string:note_id>', methods = ['PUT'])
|
|
||||||
@login_required
|
|
||||||
def update_note(note_id):
|
|
||||||
detail = getSingleResult("select * from notes where note_id = ?", [note_id])
|
|
||||||
|
|
||||||
if detail['note_clone_id']:
|
|
||||||
note_id = detail['note_clone_id']
|
|
||||||
|
|
||||||
note = request.get_json(force=True)
|
|
||||||
|
|
||||||
now = utils.now_timestamp()
|
|
||||||
|
|
||||||
history_snapshot_time_interval = float(get_option('history_snapshot_time_interval'))
|
|
||||||
|
|
||||||
history_cutoff = now - history_snapshot_time_interval
|
|
||||||
|
|
||||||
history = getSingleResult("select id from notes_history where note_id = ? and date_modified >= ?", [note_id, history_cutoff])
|
|
||||||
|
|
||||||
if history:
|
|
||||||
execute("update notes_history set note_title = ?, note_text = ?, encryption = ? where id = ?", [
|
|
||||||
note['detail']['note_title'],
|
|
||||||
note['detail']['note_text'],
|
|
||||||
note['detail']['encryption'],
|
|
||||||
history['id']
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
execute("insert into notes_history (note_id, note_title, note_text, encryption, date_modified) values (?, ?, ?, ?, ?)", [
|
|
||||||
note_id,
|
|
||||||
note['detail']['note_title'],
|
|
||||||
note['detail']['note_text'],
|
|
||||||
note['detail']['encryption'],
|
|
||||||
now
|
|
||||||
])
|
|
||||||
|
|
||||||
if note['detail']['note_title'] != detail['note_title']:
|
|
||||||
deleteRecentAudits(audit_category.UPDATE_TITLE, request, note_id)
|
|
||||||
add_audit(audit_category.UPDATE_TITLE, request, note_id)
|
|
||||||
|
|
||||||
if note['detail']['note_text'] != detail['note_text']:
|
|
||||||
deleteRecentAudits(audit_category.UPDATE_CONTENT, request, note_id)
|
|
||||||
add_audit(audit_category.UPDATE_CONTENT, request, note_id)
|
|
||||||
|
|
||||||
if note['detail']['encryption'] != detail['encryption']:
|
|
||||||
add_audit(audit_category.ENCRYPTION, request, note_id, detail['encryption'], note['detail']['encryption'])
|
|
||||||
|
|
||||||
execute("update notes set note_title = ?, note_text = ?, encryption = ?, date_modified = ? where note_id = ?", [
|
|
||||||
note['detail']['note_title'],
|
|
||||||
note['detail']['note_text'],
|
|
||||||
note['detail']['encryption'],
|
|
||||||
now,
|
|
||||||
note_id])
|
|
||||||
|
|
||||||
delete("formatting", note_id)
|
|
||||||
|
|
||||||
for fmt in note['formatting']:
|
|
||||||
insert("formatting", fmt)
|
|
||||||
|
|
||||||
delete("images", note_id)
|
|
||||||
|
|
||||||
for img in note['images']:
|
|
||||||
img['image_data'] = buffer(base64.b64decode(img['image_data']))
|
|
||||||
|
|
||||||
insert("images", img)
|
|
||||||
|
|
||||||
delete("links", note_id)
|
|
||||||
|
|
||||||
for link in note['links']:
|
|
||||||
insert("links", link)
|
|
||||||
|
|
||||||
commit()
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_api.route('/api/notes/<string:note_id>', methods = ['DELETE'])
|
|
||||||
@login_required
|
|
||||||
def delete_note(note_id):
|
|
||||||
children = getResults("select note_id from notes_tree where note_pid = ?", [note_id])
|
|
||||||
|
|
||||||
for child in children:
|
|
||||||
delete_note(child['note_id'])
|
|
||||||
|
|
||||||
delete("notes_tree", note_id)
|
|
||||||
delete("notes", note_id)
|
|
||||||
|
|
||||||
add_audit(audit_category.DELETE_NOTE, request, note_id)
|
|
||||||
|
|
||||||
commit()
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_api.route('/api/notes/<string:parent_note_id>/children', methods = ['POST'])
|
|
||||||
@login_required
|
|
||||||
def create_child(parent_note_id):
|
|
||||||
note = request.get_json(force=True)
|
|
||||||
|
|
||||||
noteId = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(22))
|
|
||||||
|
|
||||||
if parent_note_id == "root":
|
|
||||||
parent_note_id = ""
|
|
||||||
|
|
||||||
new_note_pos = 0
|
|
||||||
|
|
||||||
if note['target'] == 'into':
|
|
||||||
res = getSingleResult('select max(note_pos) as max_note_pos from notes_tree where note_pid = ?', [parent_note_id])
|
|
||||||
max_note_pos = res['max_note_pos']
|
|
||||||
|
|
||||||
if max_note_pos is None: # no children yet
|
|
||||||
new_note_pos = 0
|
|
||||||
else:
|
|
||||||
new_note_pos = max_note_pos + 1
|
|
||||||
elif note['target'] == 'after':
|
|
||||||
after_note = getSingleResult('select note_pos from notes_tree where note_id = ?', [note['target_note_id']])
|
|
||||||
|
|
||||||
new_note_pos = after_note['note_pos'] + 1
|
|
||||||
|
|
||||||
execute('update notes_tree set note_pos = note_pos + 1 where note_pid = ? and note_pos > ?', [parent_note_id, after_note['note_pos']])
|
|
||||||
else:
|
|
||||||
raise Exception('Unknown target: ' + note['target'])
|
|
||||||
|
|
||||||
add_audit(audit_category.CREATE_NOTE, request, noteId)
|
|
||||||
|
|
||||||
now = utils.now_timestamp()
|
|
||||||
|
|
||||||
insert("notes", {
|
|
||||||
'note_id': noteId,
|
|
||||||
'note_title': note['note_title'],
|
|
||||||
'note_text': '',
|
|
||||||
'note_clone_id': '',
|
|
||||||
'date_created': now,
|
|
||||||
'date_modified': now,
|
|
||||||
'icon_info': 'pencil',
|
|
||||||
'is_finished': 0,
|
|
||||||
'encryption': note['encryption']
|
|
||||||
})
|
|
||||||
|
|
||||||
insert("notes_tree", {
|
|
||||||
'note_id': noteId,
|
|
||||||
'note_pid': parent_note_id,
|
|
||||||
'note_pos': new_note_pos,
|
|
||||||
'is_expanded': 0
|
|
||||||
})
|
|
||||||
|
|
||||||
commit()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'note_id': noteId
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_api.route('/api/notes', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def search_notes():
|
|
||||||
search = '%' + request.args['search'] + '%'
|
|
||||||
|
|
||||||
result = getResults("select note_id from notes where note_title like ? or note_text like ?", [search, search])
|
|
||||||
|
|
||||||
noteIdList = []
|
|
||||||
|
|
||||||
for res in result:
|
|
||||||
noteIdList.append(res['note_id'])
|
|
||||||
|
|
||||||
return jsonify(noteIdList)
|
|
@ -1,22 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
from sql import getResults
|
|
||||||
|
|
||||||
notes_history_api = Blueprint('notes_history_api', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@notes_history_api.route('/api/notes-history/<string:note_id>', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_note_history(note_id):
|
|
||||||
history = getResults("select * from notes_history where note_id = ? order by date_modified desc", [note_id])
|
|
||||||
|
|
||||||
return jsonify(history)
|
|
||||||
|
|
||||||
|
|
||||||
@notes_history_api.route('/api/recent-changes/', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_recent_changes():
|
|
||||||
recent_changes = getResults("select * from notes_history order by date_modified desc limit 1000")
|
|
||||||
|
|
||||||
return jsonify(recent_changes)
|
|
@ -1,70 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify
|
|
||||||
from flask import request
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
import audit_category
|
|
||||||
from sql import execute, commit, add_audit
|
|
||||||
from sql import getSingleResult
|
|
||||||
|
|
||||||
notes_move_api = Blueprint('notes_move_api', __name__)
|
|
||||||
|
|
||||||
@notes_move_api.route('/api/notes/<string:note_id>/moveTo/<string:parent_id>', methods = ['PUT'])
|
|
||||||
@login_required
|
|
||||||
def move_to_note(note_id, parent_id):
|
|
||||||
res = getSingleResult('select max(note_pos) as max_note_pos from notes_tree where note_pid = ?', [parent_id])
|
|
||||||
max_note_pos = res['max_note_pos']
|
|
||||||
new_note_pos = 0
|
|
||||||
|
|
||||||
if max_note_pos is None: # no children yet
|
|
||||||
new_note_pos = 0
|
|
||||||
else:
|
|
||||||
new_note_pos = max_note_pos + 1
|
|
||||||
|
|
||||||
execute("update notes_tree set note_pid = ?, note_pos = ? where note_id = ?", [parent_id, new_note_pos, note_id])
|
|
||||||
|
|
||||||
add_audit(audit_category.CHANGE_PARENT, request, note_id)
|
|
||||||
|
|
||||||
commit()
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_move_api.route('/api/notes/<string:note_id>/moveBefore/<string:before_note_id>', methods = ['PUT'])
|
|
||||||
def move_before_note(note_id, before_note_id):
|
|
||||||
before_note = getSingleResult("select * from notes_tree where note_id = ?", [before_note_id])
|
|
||||||
|
|
||||||
if before_note <> None:
|
|
||||||
execute("update notes_tree set note_pos = note_pos + 1 where note_id = ?", [before_note_id])
|
|
||||||
|
|
||||||
execute("update notes_tree set note_pid = ?, note_pos = ? where note_id = ?", [before_note['note_pid'], before_note['note_pos'], note_id])
|
|
||||||
|
|
||||||
add_audit(audit_category.CHANGE_POSITION, request, note_id)
|
|
||||||
|
|
||||||
commit()
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_move_api.route('/api/notes/<string:note_id>/moveAfter/<string:after_note_id>', methods = ['PUT'])
|
|
||||||
def move_after_note(note_id, after_note_id):
|
|
||||||
after_note = getSingleResult("select * from notes_tree where note_id = ?", [after_note_id])
|
|
||||||
|
|
||||||
if after_note <> None:
|
|
||||||
execute("update notes_tree set note_pos = note_pos + 1 where note_pid = ? and note_pos > ?", [after_note['note_pid'], after_note['note_pos']])
|
|
||||||
|
|
||||||
execute("update notes_tree set note_pid = ?, note_pos = ? where note_id = ?", [after_note['note_pid'], after_note['note_pos'] + 1, note_id])
|
|
||||||
|
|
||||||
add_audit(audit_category.CHANGE_POSITION, request, note_id)
|
|
||||||
|
|
||||||
commit()
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
|
|
||||||
@notes_move_api.route('/api/notes/<string:note_id>/expanded/<int:expanded>', methods = ['PUT'])
|
|
||||||
def set_expanded_note(note_id, expanded):
|
|
||||||
execute("update notes_tree set is_expanded = ? where note_id = ?", [expanded, note_id])
|
|
||||||
|
|
||||||
# no audit here, not really important
|
|
||||||
|
|
||||||
commit()
|
|
||||||
return jsonify({})
|
|
@ -1,16 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify, request
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
import change_password
|
|
||||||
|
|
||||||
password_api = Blueprint('password_api', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@password_api.route('/api/password/change', methods = ['POST'])
|
|
||||||
@login_required
|
|
||||||
def change_password():
|
|
||||||
req = request.get_json(force=True)
|
|
||||||
|
|
||||||
result = change_password.change_password(req['current_password'], req['new_password'])
|
|
||||||
|
|
||||||
return jsonify(result)
|
|
101
src/routes.py
101
src/routes.py
@ -1,101 +0,0 @@
|
|||||||
import base64
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import render_template, redirect
|
|
||||||
from flask import request, send_from_directory, Blueprint
|
|
||||||
from flask_login import UserMixin, login_user, logout_user, LoginManager
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
import my_scrypt
|
|
||||||
from migration_api import APP_DB_VERSION
|
|
||||||
from sql import get_option
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
login_manager = LoginManager()
|
|
||||||
user = User()
|
|
||||||
|
|
||||||
|
|
||||||
def init(app):
|
|
||||||
login_manager.init_app(app)
|
|
||||||
login_manager.login_view = 'login_form'
|
|
||||||
|
|
||||||
user.id = get_option('username')
|
|
||||||
|
|
||||||
|
|
||||||
routes = Blueprint('routes', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/login', methods=['GET'])
|
|
||||||
def login_form():
|
|
||||||
return render_template('login.html')
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/app', methods=['GET'])
|
|
||||||
@login_required
|
|
||||||
def show_app():
|
|
||||||
db_version = int(get_option('db_version'))
|
|
||||||
|
|
||||||
if db_version < APP_DB_VERSION:
|
|
||||||
return redirect('migration')
|
|
||||||
|
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/migration', methods=['GET'])
|
|
||||||
@login_required
|
|
||||||
def show_migration():
|
|
||||||
return render_template('migration.html')
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/logout', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def logout():
|
|
||||||
logout_user()
|
|
||||||
return redirect('login')
|
|
||||||
|
|
||||||
|
|
||||||
def verify_password(guessed_password):
|
|
||||||
hashed_password = base64.b64decode(get_option('password_verification_hash'))
|
|
||||||
|
|
||||||
guess_hashed = my_scrypt.get_verification_hash(guessed_password)
|
|
||||||
|
|
||||||
return guess_hashed == hashed_password
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/login', methods=['POST'])
|
|
||||||
def login_post():
|
|
||||||
guessedPassword = request.form['password'].encode('utf-8')
|
|
||||||
|
|
||||||
if request.form['username'] == user.id and verify_password(guessedPassword):
|
|
||||||
rememberMe = True if 'remember-me' in request.form else False
|
|
||||||
|
|
||||||
login_user(user, remember=rememberMe)
|
|
||||||
|
|
||||||
return redirect('app')
|
|
||||||
else:
|
|
||||||
return render_template('login.html', failedAuth=True)
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/stat/<path:path>')
|
|
||||||
def send_stc(path):
|
|
||||||
return send_from_directory(os.path.join(os.getcwd(), 'static'), path)
|
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
if user_id == user.id:
|
|
||||||
return user
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@login_manager.unauthorized_handler
|
|
||||||
def unauthorized_handler():
|
|
||||||
if request.path.startswith('/api'):
|
|
||||||
return 'Unauthorized', 401
|
|
||||||
else:
|
|
||||||
return redirect('/login')
|
|
@ -1,39 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify, request
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
import sql
|
|
||||||
import audit_category
|
|
||||||
|
|
||||||
settings_api = Blueprint('settings_api', __name__)
|
|
||||||
|
|
||||||
ALLOWED_OPTIONS = ['encryption_session_timeout', 'history_snapshot_time_interval']
|
|
||||||
|
|
||||||
|
|
||||||
@settings_api.route('/api/settings', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_settings():
|
|
||||||
dict = {}
|
|
||||||
|
|
||||||
settings = sql.getResults("SELECT opt_name, opt_value FROM options WHERE opt_name IN (%s)" % ',' . join('?' * len(ALLOWED_OPTIONS)), ALLOWED_OPTIONS)
|
|
||||||
|
|
||||||
for set in settings:
|
|
||||||
dict[set['opt_name']] = set['opt_value']
|
|
||||||
|
|
||||||
return jsonify(dict)
|
|
||||||
|
|
||||||
|
|
||||||
@settings_api.route('/api/settings', methods = ['POST'])
|
|
||||||
@login_required
|
|
||||||
def set_settings():
|
|
||||||
req = request.get_json(force=True)
|
|
||||||
|
|
||||||
if req['name'] in ALLOWED_OPTIONS:
|
|
||||||
sql.add_audit(audit_category.SETTINGS, request, None, sql.get_option(req['name']), req['value'], req['name'])
|
|
||||||
|
|
||||||
sql.set_option(req['name'], req['value'])
|
|
||||||
|
|
||||||
sql.commit()
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
else:
|
|
||||||
return jsonify("not allowed option to set")
|
|
56
src/setup.py
56
src/setup.py
@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import getpass
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from Crypto.Util import Counter
|
|
||||||
|
|
||||||
from builtins import input
|
|
||||||
|
|
||||||
import src.my_scrypt
|
|
||||||
|
|
||||||
config = src.config_provider.getConfig()
|
|
||||||
src.sql.connect(config['Document']['documentPath'])
|
|
||||||
|
|
||||||
username = src.sql.getOption("username")
|
|
||||||
|
|
||||||
if username:
|
|
||||||
print("Application has been already set up.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print("Please provide your desired login credentials")
|
|
||||||
|
|
||||||
username = input("Username: ")
|
|
||||||
|
|
||||||
password1 = getpass.getpass()
|
|
||||||
password2 = getpass.getpass(prompt='Repeat the same password: ')
|
|
||||||
|
|
||||||
if password1 == password2:
|
|
||||||
# urandom is secure enough, see https://docs.python.org/2/library/os.html
|
|
||||||
src.sql.setOption('flask_secret_key', base64.b64encode(os.urandom(32)))
|
|
||||||
src.sql.setOption('password_verification_salt', base64.b64encode(os.urandom(32)))
|
|
||||||
src.sql.setOption('password_derived_key_salt', base64.b64encode(os.urandom(32)))
|
|
||||||
|
|
||||||
password_derived_key = src.my_scrypt.get_password_derived_key(password1)
|
|
||||||
|
|
||||||
aes = AES.new(password_derived_key, AES.MODE_CTR, counter=Counter.new(128, initial_value=5))
|
|
||||||
|
|
||||||
data_key = os.urandom(32)
|
|
||||||
data_key_digest = hashlib.sha256(data_key).digest()[:4]
|
|
||||||
|
|
||||||
encrypted_data_key = aes.encrypt(data_key_digest + data_key)
|
|
||||||
|
|
||||||
src.sql.setOption('encrypted_data_key', base64.b64encode(encrypted_data_key))
|
|
||||||
|
|
||||||
verification_hash = src.my_scrypt.get_verification_hash(password1)
|
|
||||||
|
|
||||||
src.sql.setOption('username', username)
|
|
||||||
src.sql.setOption('password_verification_hash', base64.b64encode(verification_hash))
|
|
||||||
|
|
||||||
src.sql.commit()
|
|
||||||
|
|
||||||
print('Application has been set up. You can now login.')
|
|
||||||
else:
|
|
||||||
print('Entered passwords are not identical!')
|
|
93
src/sql.py
93
src/sql.py
@ -1,93 +0,0 @@
|
|||||||
import base64
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
import utils
|
|
||||||
|
|
||||||
conn = None
|
|
||||||
|
|
||||||
|
|
||||||
def dict_factory(cursor, row):
|
|
||||||
d = {}
|
|
||||||
for idx, col in enumerate(cursor.description):
|
|
||||||
if isinstance(row[idx], buffer):
|
|
||||||
d[col[0]] = base64.b64encode(row[idx])
|
|
||||||
else:
|
|
||||||
d[col[0]] = row[idx]
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def connect(document_path):
|
|
||||||
global conn
|
|
||||||
conn = sqlite3.connect(document_path)
|
|
||||||
conn.row_factory = dict_factory
|
|
||||||
|
|
||||||
|
|
||||||
def insert(table_name, rec):
|
|
||||||
# FIXME: SQL injection!
|
|
||||||
keys = ','.join(rec.keys())
|
|
||||||
question_marks = ','.join(list('?' * len(rec)))
|
|
||||||
values = tuple(rec.values())
|
|
||||||
cursor = execute('INSERT INTO ' + table_name + ' (' + keys + ') VALUES (' + question_marks + ')', values)
|
|
||||||
return cursor.lastrowid
|
|
||||||
|
|
||||||
|
|
||||||
def set_option(name, value):
|
|
||||||
execute("UPDATE options SET opt_value = ? WHERE opt_name = ?", [value, name])
|
|
||||||
|
|
||||||
|
|
||||||
def get_option(name):
|
|
||||||
return getSingleResult("SELECT opt_value FROM options WHERE opt_name = ?", [name])['opt_value']
|
|
||||||
|
|
||||||
|
|
||||||
def add_audit(category, request=None, note_id=None, change_from=None, change_to=None, comment=None):
|
|
||||||
now = utils.now_timestamp()
|
|
||||||
|
|
||||||
browser_id = None
|
|
||||||
|
|
||||||
if request:
|
|
||||||
browser_id = request.headers['x-browser-id']
|
|
||||||
|
|
||||||
execute("INSERT INTO audit_log (date_modified, category, browser_id, note_id, change_from, change_to, comment)"
|
|
||||||
" VALUES (?, ?, ?, ?, ?, ?, ?)", [now, category, browser_id, note_id, change_from, change_to, comment])
|
|
||||||
|
|
||||||
|
|
||||||
def deleteRecentAudits(category, request, note_id):
|
|
||||||
browser_id = request.headers['x-browser-id']
|
|
||||||
|
|
||||||
delete_cutoff = utils.now_timestamp() - 10 * 60;
|
|
||||||
|
|
||||||
execute("DELETE FROM audit_log WHERE category = ? AND browser_id = ? AND note_id = ? AND date_modified > ?",
|
|
||||||
[category, browser_id, note_id, delete_cutoff])
|
|
||||||
|
|
||||||
|
|
||||||
def delete(tablename, note_id):
|
|
||||||
execute("DELETE FROM " + tablename + " WHERE note_id = ?", [note_id])
|
|
||||||
|
|
||||||
|
|
||||||
def execute(sql, params=[]):
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(sql, params)
|
|
||||||
return cursor
|
|
||||||
|
|
||||||
|
|
||||||
def execute_script(sql):
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.executescript(sql)
|
|
||||||
return cursor
|
|
||||||
|
|
||||||
|
|
||||||
def getResults(sql, params=[]):
|
|
||||||
cursor = conn.cursor()
|
|
||||||
query = cursor.execute(sql, params)
|
|
||||||
return query.fetchall()
|
|
||||||
|
|
||||||
|
|
||||||
def getSingleResult(sql, params=()):
|
|
||||||
cursor = conn.cursor()
|
|
||||||
query = cursor.execute(sql, params)
|
|
||||||
return query.fetchone()
|
|
||||||
|
|
||||||
|
|
||||||
def commit():
|
|
||||||
conn.commit()
|
|
@ -1,276 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Trilium</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="container">
|
|
||||||
<div id="header" class="hide-toggle">
|
|
||||||
<div id="header-title">
|
|
||||||
Trilium
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="flex-grow: 100;">
|
|
||||||
<button class="btn btn-xs" onclick="showRecentChanges();">Recent changes</button>
|
|
||||||
<button class="btn btn-xs" onclick="showRecentNotes();">Recent notes</button>
|
|
||||||
<button class="btn btn-xs" onclick="showJumpToNote();">Jump to note</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span id="top-message"></span>
|
|
||||||
<span id="error-message"></span>
|
|
||||||
|
|
||||||
<button class="btn btn-xs" onclick="displaySettings();">Settings</button>
|
|
||||||
|
|
||||||
<form action="logout" method="POST" style="display: inline;">
|
|
||||||
<input type="submit" class="btn btn-xs" value="Logout">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hide-toggle" style="grid-area: tree-actions">
|
|
||||||
<a onclick="createNewTopLevelNote()" title="Create new top level note" class="icon-action">
|
|
||||||
<img src="images/icons/file-plus.png" alt="Create new top level note"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="collapseTree()" title="Collapse tree" class="icon-action">
|
|
||||||
<img src="images/icons/list.png" alt="Collapse tree"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="scrollToCurrentNote()" title="Scroll to current note" class="icon-action">
|
|
||||||
<img src="images/icons/crosshair.png" alt="Collapse tree"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="toggleSearch()" title="Search in notes" class="icon-action">
|
|
||||||
<img src="images/icons/search.png" alt="Search in notes"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
|
|
||||||
<p>
|
|
||||||
<label>Search:</label>
|
|
||||||
<input name="search" autocomplete="off">
|
|
||||||
<button id="reset-search-button">×</button>
|
|
||||||
<span id="matches"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hide-toggle" style="grid-area: title;">
|
|
||||||
<div style="display: flex; align-items: center;">
|
|
||||||
<a onclick="encryptNoteAndSendToServer()"
|
|
||||||
title="Encrypt the note so that password will be required to view the note"
|
|
||||||
class="icon-action"
|
|
||||||
id="encrypt-button"
|
|
||||||
style="display: none;">
|
|
||||||
<img src="images/icons/lock.png" alt="Encrypt note"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a onclick="decryptNoteAndSendToServer()"
|
|
||||||
title="Decrypt note permamently so that password will not be required to access this note in the future"
|
|
||||||
class="icon-action"
|
|
||||||
id="decrypt-button"
|
|
||||||
style="display: none;">
|
|
||||||
<img src="images/icons/unlock.png" alt="Decrypt note"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<input autocomplete="off" value="" id="note-title" style="font-size: x-large; border: 0; flex-grow: 100;" tabindex="1">
|
|
||||||
|
|
||||||
<button class="btn btn-xs" onclick="showCurrentNoteHistory();">History</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="overflow: auto; grid-area: note-content; padding-left: 10px;">
|
|
||||||
<div id="note-detail"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="recent-notes-dialog" title="Recent notes" style="display: none;">
|
|
||||||
<select id="recent-notes-select-box" size="20" style="width: 100%">
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<button class="btn btn-sm" id="recent-notes-jump-to">Jump to (enter)</button>
|
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-link">Add link (l)</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="insert-link-dialog" title="Insert link" style="display: none;">
|
|
||||||
<form id="insert-link-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="note-autocomplete">Link to note</label>
|
|
||||||
<input id="note-autocomplete" style="width: 100%;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="link-title">Link title</label>
|
|
||||||
<input id="link-title" style="width: 100%;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-sm">Add link</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="jump-to-note-dialog" title="Jump to note" style="display: none;">
|
|
||||||
<form id="jump-to-note-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="jump-to-note-autocomplete">Jump to note</label>
|
|
||||||
<input id="jump-to-note-autocomplete" style="width: 100%;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-sm">Jump</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="encryption-password-dialog" title="Encrypted note" style="display: none;">
|
|
||||||
<form id="encryption-password-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="encryption-password">This note is encrypted. Enter password to show it:</label>
|
|
||||||
<input id="encryption-password" style="width: 250px;" type="password">
|
|
||||||
<button class="btn btn-sm">Show</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="settings-dialog" title="Settings" style="display: none;">
|
|
||||||
<div id="settings-tabs">
|
|
||||||
<ul>
|
|
||||||
<li><a href="#change-password">Change password</a></li>
|
|
||||||
<li><a href="#encryption-timeout">Encryption timeout</a></li>
|
|
||||||
<li><a href="#history-snapshot-time-interval">History snapshots</a></li>
|
|
||||||
</ul>
|
|
||||||
<div id="change-password">
|
|
||||||
<form id="change-password-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="old-password">Old password</label>
|
|
||||||
<input class="form-control" id="old-password" type="password">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-password1">New password</label>
|
|
||||||
<input class="form-control" id="new-password1" type="password">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-password2">New password once more</label>
|
|
||||||
<input class="form-control" id="new-password2" type="password">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-sm">Change password</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="encryption-timeout">
|
|
||||||
<p>Encryption timeout is a time period after which the encryption key and encrypted data is wiped out from
|
|
||||||
browser's memory. This is measured from the last encryption / decryption activity.</p>
|
|
||||||
|
|
||||||
<form id="encryption-timeout-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="encryption-timeout-in-seconds">Encryption timeout (in seconds)</label>
|
|
||||||
<input class="form-control" id="encryption-timeout-in-seconds" type="number">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-sm">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="history-snapshot-time-interval">
|
|
||||||
<p>History snapshot time interval is time in seconds after which new history record will be created for the note.</p>
|
|
||||||
|
|
||||||
<form id="history-snapshot-time-interval-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="history-snapshot-time-interval-in-seconds">History snapshot time interval (in seconds)</label>
|
|
||||||
<input class="form-control" id="history-snapshot-time-interval-in-seconds" type="number">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-sm">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="note-history-dialog" title="Note history" style="display: none;">
|
|
||||||
<div style="display: flex;">
|
|
||||||
<select id="note-history-list" size="25" style="width: 150px; height: 630px;">
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div id="note-history-content-wrapper" style="flex-grow: 1; margin-left: 20px;">
|
|
||||||
<h3 id="note-history-title" style="margin: 3px;"></h3>
|
|
||||||
|
|
||||||
<div id="note-history-content" style="height: 600px; width: 600px; overflow: auto;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="recent-changes-dialog" title="Recent changes" style="display: none; padding: 20px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tooltip" style="display: none;"></div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
const baseApiUrl = 'api/';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="javascripts/lib/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<!-- bootstrap needs to be included before jQuery UI, otherwise close icon in the dialog will be missing -->
|
|
||||||
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
|
||||||
<script src="public/javascripts/lib/bootstrap/js/bootstrap.js"></script>
|
|
||||||
|
|
||||||
<link href="stat/lib/jqueryui/jquery-ui.min.css" rel="stylesheet">
|
|
||||||
<script src="stat/lib/jqueryui/jquery-ui.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Include Fancytree skin and library -->
|
|
||||||
<link href="stat/lib/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
|
|
||||||
<script src="stat/lib/fancytree/jquery.fancytree-all.js"></script>
|
|
||||||
|
|
||||||
<link href="stat/lib/summernote/summernote.css" rel="stylesheet">
|
|
||||||
<script src="stat/lib/summernote/summernote.js"></script>
|
|
||||||
|
|
||||||
<script src="stat/lib/jquery.hotkeys.js"></script>
|
|
||||||
<script src="stat/lib/jquery.fancytree.hotkeys.js"></script>
|
|
||||||
|
|
||||||
<script src="stat/lib/jquery.ui-contextmenu.min.js"></script>
|
|
||||||
|
|
||||||
<!-- https://github.com/ricmoo/aes-js -->
|
|
||||||
<script src="stat/lib/aes.js"></script>
|
|
||||||
<!-- https://github.com/emn178/js-sha256 -->
|
|
||||||
<script src="stat/lib/sha256.min.js"></script>
|
|
||||||
<!-- https://github.com/ricmoo/scrypt-js -->
|
|
||||||
<script src="stat/lib/scrypt/scrypt.js"></script>
|
|
||||||
<script src="stat/lib/scrypt/buffer.js"></script>
|
|
||||||
<script src="stat/lib/scrypt/setImmediate.js"></script>
|
|
||||||
<script src="stat/lib/scrypt/unorm.js"></script>
|
|
||||||
|
|
||||||
<link href="stat/style.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<script src="stat/js/init.js"></script>
|
|
||||||
|
|
||||||
<!-- Tree scripts -->
|
|
||||||
<script src="stat/js/tree.js"></script>
|
|
||||||
<script src="stat/js/tree_mutations.js"></script>
|
|
||||||
<script src="stat/js/tree_utils.js"></script>
|
|
||||||
<script src="stat/js/drag_and_drop.js"></script>
|
|
||||||
<script src="stat/js/context_menu.js"></script>
|
|
||||||
|
|
||||||
<!-- Note detail -->
|
|
||||||
<script src="stat/js/note.js"></script>
|
|
||||||
<script src="stat/js/encryption.js"></script>
|
|
||||||
|
|
||||||
<!-- dialogs -->
|
|
||||||
<script src="stat/js/recent_notes.js"></script>
|
|
||||||
<script src="stat/js/add_link.js"></script>
|
|
||||||
<script src="stat/js/jump_to_note.js"></script>
|
|
||||||
<script src="stat/js/settings.js"></script>
|
|
||||||
<script src="stat/js/note_history.js"></script>
|
|
||||||
<script src="stat/js/recent_changes.js"></script>
|
|
||||||
|
|
||||||
<script src="stat/js/utils.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,50 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Login</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4 col-md-offset-4">
|
|
||||||
<h1>Trilium login</h1>
|
|
||||||
|
|
||||||
{% if failedAuth %}
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
Username and / or password are incorrect. Please try again.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form action="login" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Username</label>
|
|
||||||
<div class="controls">
|
|
||||||
<input id="username" name="username" placeholder="" class="form-control" type="text">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<div class="controls">
|
|
||||||
<input id="password" name="password" placeholder="" class="form-control" type="password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input id="remember-me" name="rememberme" value="1" type="checkbox"> Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<button class="btn btn-success">Login</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<link href="public/libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
|
||||||
<script src="public/libraries/bootstrap/js/bootstrap.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,57 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Migration</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="width: 800px; margin: auto;">
|
|
||||||
<h1>Migration</h1>
|
|
||||||
|
|
||||||
<div id="up-to-date" style="display:none;">
|
|
||||||
<p>Your database is up-to-date with the application.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="need-to-migrate" style="display:none;">
|
|
||||||
<p>Your database needs to be migrated to new version before you can use the application again.
|
|
||||||
Database will be backed up before migration in case of something going wrong.</p>
|
|
||||||
|
|
||||||
<table class="table table-bordered" style="width: 200px;">
|
|
||||||
<tr>
|
|
||||||
<th>Application version:</th>
|
|
||||||
<td id="app-db-version" style="text-align: right;"></td>
|
|
||||||
<tr>
|
|
||||||
<th>Database version:</th>
|
|
||||||
<td id="db-version" style="text-align: right;"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<button class="btn btn-warning" id="run-migration">Run migration</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="migration-result" style="display:none;">
|
|
||||||
<h2>Migration result</h2>
|
|
||||||
|
|
||||||
<table id="migration-table" class="table">
|
|
||||||
<tr>
|
|
||||||
<th>Database version</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Success</th>
|
|
||||||
<th>Error</th>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
const baseApiUrl = 'api/';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="stat/lib/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<link href="stat/lib/bootstrap/css/bootstrap.css" rel="stylesheet">
|
|
||||||
<script src="stat/lib/bootstrap/js/bootstrap.js"></script>
|
|
||||||
|
|
||||||
<script src="stat/js/migration.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,58 +0,0 @@
|
|||||||
import base64
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
from sql import getResults, getSingleResult, get_option
|
|
||||||
import utils
|
|
||||||
import backup
|
|
||||||
|
|
||||||
tree_api = Blueprint('tree_api', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@tree_api.route('/api/tree', methods = ['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_tree():
|
|
||||||
backup.regular_backup()
|
|
||||||
|
|
||||||
notes = getResults("select "
|
|
||||||
"notes_tree.*, "
|
|
||||||
"COALESCE(clone.note_title, notes.note_title) as note_title, "
|
|
||||||
"notes.note_clone_id, "
|
|
||||||
"notes.encryption, "
|
|
||||||
"case when notes.note_clone_id is null or notes.note_clone_id = '' then 0 else 1 end as is_clone "
|
|
||||||
"from notes_tree "
|
|
||||||
"join notes on notes.note_id = notes_tree.note_id "
|
|
||||||
"left join notes as clone on notes.note_clone_id = clone.note_id "
|
|
||||||
"order by note_pid, note_pos")
|
|
||||||
|
|
||||||
root_notes = []
|
|
||||||
notes_map = {}
|
|
||||||
|
|
||||||
for note in notes:
|
|
||||||
note['children'] = []
|
|
||||||
|
|
||||||
if not note['note_pid']:
|
|
||||||
root_notes.append(note)
|
|
||||||
|
|
||||||
notes_map[note['note_id']] = note
|
|
||||||
|
|
||||||
for note in notes:
|
|
||||||
if note['note_pid'] != "":
|
|
||||||
parent = notes_map[note['note_pid']]
|
|
||||||
|
|
||||||
parent['children'].append(note)
|
|
||||||
parent['folder'] = True
|
|
||||||
|
|
||||||
ret_object = {
|
|
||||||
'notes': root_notes,
|
|
||||||
'start_note_id': getSingleResult('select * from options where opt_name = "start_node"')['opt_value'],
|
|
||||||
'password_verification_salt': get_option('password_verification_salt'),
|
|
||||||
'password_derived_key_salt': get_option('password_derived_key_salt'),
|
|
||||||
'encrypted_data_key': get_option('encrypted_data_key'),
|
|
||||||
'encryption_session_timeout': get_option('encryption_session_timeout'),
|
|
||||||
'browser_id': base64.b64encode(os.urandom(8)), 'full_load_time': utils.now_timestamp()
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify(ret_object)
|
|
@ -1,6 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def now_timestamp():
|
|
||||||
return int(time.mktime(datetime.utcnow().timetuple()))
|
|
Loading…
x
Reference in New Issue
Block a user