mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-04 15:11:31 +08:00 
			
		
		
		
	db upgrades are now handled transparently in the background without bothering the user, closes #119
This commit is contained in:
		
							parent
							
								
									4c8eeb2e6f
								
							
						
					
					
						commit
						14c704d6db
					
				@ -21,11 +21,8 @@ class Entity {
 | 
				
			|||||||
            contentToHash += "|" + this[propertyName];
 | 
					            contentToHash += "|" + this[propertyName];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // this IF is to ease the migration from before hashed options, can be later removed
 | 
					 | 
				
			||||||
        if (this.constructor.tableName !== 'options' || this.isSynced) {
 | 
					 | 
				
			||||||
        this["hash"] = utils.hash(contentToHash).substr(0, 10);
 | 
					        this["hash"] = utils.hash(contentToHash).substr(0, 10);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async save() {
 | 
					    async save() {
 | 
				
			||||||
        await repository.updateEntity(this);
 | 
					        await repository.updateEntity(this);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,46 +0,0 @@
 | 
				
			|||||||
import server from './services/server.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(document).ready(async () => {
 | 
					 | 
				
			||||||
    const {appDbVersion, dbVersion} = await server.get('migration');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log("HI", {appDbVersion, dbVersion});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (appDbVersion === dbVersion) {
 | 
					 | 
				
			||||||
        $("#up-to-date").show();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        $("#need-to-migrate").show();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $("#app-db-version").html(appDbVersion);
 | 
					 | 
				
			||||||
        $("#db-version").html(dbVersion);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$("#run-migration").click(async () => {
 | 
					 | 
				
			||||||
    $("#run-migration").prop("disabled", true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $("#migration-result").show();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const result = await server.post('migration');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const migration of result.migrations) {
 | 
					 | 
				
			||||||
        const row = $('<tr>')
 | 
					 | 
				
			||||||
            .append($('<td>').html(migration.dbVersion))
 | 
					 | 
				
			||||||
            .append($('<td>').html(migration.name))
 | 
					 | 
				
			||||||
            .append($('<td>').html(migration.success ? 'Yes' : 'No'))
 | 
					 | 
				
			||||||
            .append($('<td>').html(migration.success ? 'N/A' : migration.error));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!migration.success) {
 | 
					 | 
				
			||||||
            row.addClass("danger");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $("#migration-table").append(row);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// copy of this shortcut to be able to debug migration problems
 | 
					 | 
				
			||||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
 | 
					 | 
				
			||||||
    require('electron').remote.getCurrentWindow().toggleDevTools();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -5,7 +5,7 @@ import infoService from "./info.js";
 | 
				
			|||||||
function getHeaders() {
 | 
					function getHeaders() {
 | 
				
			||||||
    let protectedSessionId = null;
 | 
					    let protectedSessionId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try { // this is because protected session might not be declared in some cases - like when it's included in migration page
 | 
					    try { // this is because protected session might not be declared in some cases
 | 
				
			||||||
        protectedSessionId = protectedSessionHolder.getProtectedSessionId();
 | 
					        protectedSessionId = protectedSessionHolder.getProtectedSessionId();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch(e) {}
 | 
					    catch(e) {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +0,0 @@
 | 
				
			|||||||
"use strict";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const optionService = require('../../services/options');
 | 
					 | 
				
			||||||
const migrationService = require('../../services/migration');
 | 
					 | 
				
			||||||
const appInfo = require('../../services/app_info');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getMigrationInfo() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        dbVersion: parseInt(await optionService.getOption('dbVersion')),
 | 
					 | 
				
			||||||
        appDbVersion: appInfo.dbVersion
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function executeMigration() {
 | 
					 | 
				
			||||||
    const migrations = await migrationService.migrate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        migrations: migrations
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = {
 | 
					 | 
				
			||||||
    getMigrationInfo,
 | 
					 | 
				
			||||||
    executeMigration
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
"use strict";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function migrationPage(req, res) {
 | 
					 | 
				
			||||||
    res.render('migration', {});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = {
 | 
					 | 
				
			||||||
    migrationPage
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
const indexRoute = require('./index');
 | 
					const indexRoute = require('./index');
 | 
				
			||||||
const loginRoute = require('./login');
 | 
					const loginRoute = require('./login');
 | 
				
			||||||
const migrationRoute = require('./migration');
 | 
					 | 
				
			||||||
const setupRoute = require('./setup');
 | 
					const setupRoute = require('./setup');
 | 
				
			||||||
const multer = require('multer')();
 | 
					const multer = require('multer')();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,7 +13,6 @@ const noteRevisionsApiRoute = require('./api/note_revisions');
 | 
				
			|||||||
const recentChangesApiRoute = require('./api/recent_changes');
 | 
					const recentChangesApiRoute = require('./api/recent_changes');
 | 
				
			||||||
const optionsApiRoute = require('./api/options');
 | 
					const optionsApiRoute = require('./api/options');
 | 
				
			||||||
const passwordApiRoute = require('./api/password');
 | 
					const passwordApiRoute = require('./api/password');
 | 
				
			||||||
const migrationApiRoute = require('./api/migration');
 | 
					 | 
				
			||||||
const syncApiRoute = require('./api/sync');
 | 
					const syncApiRoute = require('./api/sync');
 | 
				
			||||||
const loginApiRoute = require('./api/login');
 | 
					const loginApiRoute = require('./api/login');
 | 
				
			||||||
const eventLogRoute = require('./api/event_log');
 | 
					const eventLogRoute = require('./api/event_log');
 | 
				
			||||||
@ -96,7 +94,6 @@ function register(app) {
 | 
				
			|||||||
    route(GET, '/login', [], loginRoute.loginPage);
 | 
					    route(GET, '/login', [], loginRoute.loginPage);
 | 
				
			||||||
    route(POST, '/login', [], loginRoute.login);
 | 
					    route(POST, '/login', [], loginRoute.login);
 | 
				
			||||||
    route(POST, '/logout', [auth.checkAuth], loginRoute.logout);
 | 
					    route(POST, '/logout', [auth.checkAuth], loginRoute.logout);
 | 
				
			||||||
    route(GET, '/migration', [auth.checkAuthForMigrationPage], migrationRoute.migrationPage);
 | 
					 | 
				
			||||||
    route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
 | 
					    route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apiRoute(GET, '/api/tree', treeApiRoute.getTree);
 | 
					    apiRoute(GET, '/api/tree', treeApiRoute.getTree);
 | 
				
			||||||
@ -180,9 +177,6 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
 | 
					    apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
 | 
				
			||||||
    apiRoute(POST, '/api/search/:searchString', searchRoute.saveSearchToNote);
 | 
					    apiRoute(POST, '/api/search/:searchString', searchRoute.saveSearchToNote);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    route(GET, '/api/migration', [auth.checkApiAuthForMigrationPage], migrationApiRoute.getMigrationInfo, apiResultHandler);
 | 
					 | 
				
			||||||
    route(POST, '/api/migration', [auth.checkApiAuthForMigrationPage], migrationApiRoute.executeMigration, apiResultHandler);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
 | 
					    route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
 | 
				
			||||||
    // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
 | 
					    // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
 | 
				
			||||||
    apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);
 | 
					    apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);
 | 
				
			||||||
 | 
				
			|||||||
@ -12,18 +12,6 @@ async function checkAuth(req, res, next) {
 | 
				
			|||||||
    else if (!req.session.loggedIn && !utils.isElectron()) {
 | 
					    else if (!req.session.loggedIn && !utils.isElectron()) {
 | 
				
			||||||
        res.redirect("login");
 | 
					        res.redirect("login");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (!await sqlInit.isDbUpToDate()) {
 | 
					 | 
				
			||||||
        res.redirect("migration");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        next();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function checkAuthForMigrationPage(req, res, next) {
 | 
					 | 
				
			||||||
    if (!req.session.loggedIn && !utils.isElectron()) {
 | 
					 | 
				
			||||||
        res.redirect("login");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        next();
 | 
					        next();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -35,27 +23,12 @@ async function checkApiAuthOrElectron(req, res, next) {
 | 
				
			|||||||
    if (!req.session.loggedIn && !utils.isElectron()) {
 | 
					    if (!req.session.loggedIn && !utils.isElectron()) {
 | 
				
			||||||
        res.status(401).send("Not authorized");
 | 
					        res.status(401).send("Not authorized");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (await sqlInit.isDbUpToDate()) {
 | 
					 | 
				
			||||||
        next();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        res.status(409).send("Mismatched app versions"); // need better response than that
 | 
					        next();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function checkApiAuth(req, res, next) {
 | 
					async function checkApiAuth(req, res, next) {
 | 
				
			||||||
    if (!req.session.loggedIn) {
 | 
					 | 
				
			||||||
        res.status(401).send("Not authorized");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (await sqlInit.isDbUpToDate()) {
 | 
					 | 
				
			||||||
        next();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        res.status(409).send("Mismatched app versions"); // need better response than that
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function checkApiAuthForMigrationPage(req, res, next) {
 | 
					 | 
				
			||||||
    if (!req.session.loggedIn) {
 | 
					    if (!req.session.loggedIn) {
 | 
				
			||||||
        res.status(401).send("Not authorized");
 | 
					        res.status(401).send("Not authorized");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -79,19 +52,14 @@ async function checkSenderToken(req, res, next) {
 | 
				
			|||||||
    if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
 | 
					    if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
 | 
				
			||||||
        res.status(401).send("Not authorized");
 | 
					        res.status(401).send("Not authorized");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (await sqlInit.isDbUpToDate()) {
 | 
					 | 
				
			||||||
        next();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        res.status(409).send("Mismatched app versions"); // need better response than that
 | 
					        next();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    checkAuth,
 | 
					    checkAuth,
 | 
				
			||||||
    checkAuthForMigrationPage,
 | 
					 | 
				
			||||||
    checkApiAuth,
 | 
					    checkApiAuth,
 | 
				
			||||||
    checkApiAuthForMigrationPage,
 | 
					 | 
				
			||||||
    checkAppNotInitialized,
 | 
					    checkAppNotInitialized,
 | 
				
			||||||
    checkApiAuthOrElectron,
 | 
					    checkApiAuthOrElectron,
 | 
				
			||||||
    checkSenderToken
 | 
					    checkSenderToken
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@ const repository = require('./repository');
 | 
				
			|||||||
const utils = require('./utils');
 | 
					const utils = require('./utils');
 | 
				
			||||||
const dateUtils = require('./date_utils');
 | 
					const dateUtils = require('./date_utils');
 | 
				
			||||||
const appInfo = require('./app_info');
 | 
					const appInfo = require('./app_info');
 | 
				
			||||||
const Option = require('../entities/option');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getOption(name) {
 | 
					async function getOption(name) {
 | 
				
			||||||
    const option = await repository.getOption(name);
 | 
					    const option = await repository.getOption(name);
 | 
				
			||||||
@ -27,6 +26,9 @@ async function setOption(name, value) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function createOption(name, value, isSynced) {
 | 
					async function createOption(name, value, isSynced) {
 | 
				
			||||||
 | 
					    // to avoid circular dependency, need to find better solution
 | 
				
			||||||
 | 
					    const Option = require('../entities/option');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await new Option({
 | 
					    await new Option({
 | 
				
			||||||
        name: name,
 | 
					        name: name,
 | 
				
			||||||
        value: value,
 | 
					        value: value,
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,10 @@ const dbReady = new Promise((resolve, reject) => {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!await isDbUpToDate()) {
 | 
					        if (!await isDbUpToDate()) {
 | 
				
			||||||
            return;
 | 
					            // avoiding circular dependency
 | 
				
			||||||
 | 
					            const migrationService = require('./migration');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await migrationService.migrate();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        resolve(db);
 | 
					        resolve(db);
 | 
				
			||||||
 | 
				
			|||||||
@ -22,13 +22,6 @@ let syncServerCertificate = null;
 | 
				
			|||||||
async function sync() {
 | 
					async function sync() {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        await syncMutexService.doExclusively(async () => {
 | 
					        await syncMutexService.doExclusively(async () => {
 | 
				
			||||||
            if (!await sqlInit.isDbUpToDate()) {
 | 
					 | 
				
			||||||
                return {
 | 
					 | 
				
			||||||
                    success: false,
 | 
					 | 
				
			||||||
                    message: "DB not up to date"
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const syncContext = await login();
 | 
					            const syncContext = await login();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await pushSync(syncContext);
 | 
					            await pushSync(syncContext);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,72 +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>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <a href="/" class="btn btn-success">Continue to app</a>
 | 
					 | 
				
			||||||
      </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>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <a href="/" class="btn btn-success">Continue to app</a>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </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;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const glob = {
 | 
					 | 
				
			||||||
            sourceId: ''
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    </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/migration.js" type="module"></script>
 | 
					 | 
				
			||||||
  </body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user