Browse Source

Externalize craftlab http library, build access menu

isundil 1 year ago
parent
commit
816cc002aa

+ 10 - 5
main.js

@@ -1,21 +1,19 @@
 #!/bin/node
 
+const CONFIG = require('./src/config.js');
 const path = require('path');
 const fs = require('fs');
 const Router = require('node-simple-router');
 const http = require('http');
-const CONFIG = require('./src/config.js');
 const Security = require('./src/security.js');
 const RouterUtils = require('./src/routerUtils.js').RouterUtils;
 
-const CR2Parser = require('./src/filetype/cr2.js').ExifParser;
-
 const UPDATE_INTERVAL = 1800000; // 30 min
 
 function App() {
     this.router = new Router({ static_route: __dirname+"/static/" });
     this.routerUtils = new RouterUtils(this);
-    this.databaseHelper = require('./src/databaseHelper.js').DatabaseHelper;
+    this.databaseHelper = require('craftlabhttpserver/src/databaseHelper').DatabaseHelper;
     this.libraryManager = require('./src/libraryManager.js').LibraryManager;
 }
 
@@ -28,7 +26,14 @@ App.prototype.init = async function() {
 
         "./router/index.js"
     ].forEach(i => require(i).register(this));
-    await this.databaseHelper.init();
+
+    await this.databaseHelper.init([
+        require('./model/session.js').SessionModel,
+        require('./model/mediaItem.js').MediaFileModel,
+        require('./model/mediaItemMeta.js').MediaFileMetaModel,
+        require('./model/mediaItemTag.js').MediaFileTagModel,
+        require('./model/access.js').AccessModel
+    ]);
 }
 
 App.prototype.run = async function() {

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "@mdi/font": "^7.3.67",
     "bootstrap": "^5.3.2",
     "bootstrap-icons": "^1.11.2",
+    "craftlabhttpserver": "git:isundil/craftlabHttpServer",
     "crypto": "^1.0.1",
     "geokit": "^1.1.0",
     "imagemagick": "^0.1.3",

+ 7 - 2
router/api.js

@@ -8,7 +8,7 @@ const MediaService = require('../model/mediaService.js');
 module.exports = { register: app => {
     app.router.get("/api/access/list", (req, res) => {
         app.routerUtils.onApiRequest(req, res);
-        app.routerUtils.jsonResponse(res, req.accessList);
+        app.routerUtils.jsonResponse(res, req.accessList || []);
     });
     app.router.post("/api/access/link", async (req, res) => { // /api/access/link, post: { linkId: string }
         app.routerUtils.onApiRequest(req, res);
@@ -57,7 +57,12 @@ module.exports = { register: app => {
         if (!data)
             return app.routerUtils.onPageNotFound(res);
         try {
-            let thumbnail = await (await app.libraryManager.findMedia(data.path))?.createThumbnail(req.body?.w || 0, req.body?.h || 0, req.body?.q || 6);
+            let thumbnail = null;
+            try {
+                thumbnail = await (await app.libraryManager.findMedia(data.path))?.createThumbnail(req.body?.w || 0, req.body?.h || 0, req.body?.q || 6);
+            } catch (err) {
+                return app.routerUtils.apiError(res);
+            }
             if (!thumbnail)
                 return app.routerUtils.onPageNotFound(res);
             res.setHeader("Content-Type", "image/jpeg");

+ 16 - 41
src/config.js

@@ -1,52 +1,27 @@
 
+const CONFIG = require('craftlabhttpserver/src/config.js');
+
 const FILENAME = process.mainModule.path + "/config.json";
 const fs = require('fs');
 const path = require('path');
 
-function validNumber(input) {
-    return !Number.isNaN(Number.parseInt(input));
-}
-
-function validNotEmptyString(input) {
-    return !!input && (""+input).length;
-}
-
-function pickConfig(defaultConfig, configContent) {
-    let configEntries = {};
-    for (let i in defaultConfig) {
-        if (configContent[i] === undefined)
-            configEntries[i] = defaultConfig[i].value;
-        else
-            configEntries[i] = configContent[i];
-        if (!defaultConfig[i].valid(configEntries[i])) {
-            console.error(`Invalid entry in configuration for ${i}: Found ${configEntries[i]}`);
-            hasErrors = true;
-        }
-    }
-    return configEntries;
-}
-
-let hasErrors = false;
-let configEntries = {};
+let configEntries = CONFIG;
+
 (() => {
-    let configFile = (process?.argv?.[2] || FILENAME);
-    console.log("CONFIG: Loading configuration from "+configFile);
-    let configContent = JSON.parse(fs.readFileSync(configFile));
-    let defaultConfig = {
-        port: { value: 80, valid: validNumber },
-        instanceHostname: { value: require('os').hostname(), valid: validNotEmptyString },
-        ldapUrl: { value: "", valid: validNotEmptyString },
-        ldapBindDN: { value: "", valid: validNotEmptyString },
-        ldapBindPwd: { value: "", valid: validNotEmptyString },
-        ldapBase: { value: "", valid: validNotEmptyString },
-        database: { value: "", valid: validNotEmptyString }
-    };
-
-    configEntries = pickConfig(defaultConfig, configContent);
+    CONFIG.Initialize({
+            port: { value: 80, valid: CONFIG.validNumber },
+            instanceHostname: { value: require('os').hostname(), valid: CONFIG.validNotEmptyString },
+            ldapUrl: { value: "", valid: CONFIG.validNotEmptyString },
+            ldapBindDN: { value: "", valid: CONFIG.validNotEmptyString },
+            ldapBindPwd: { value: "", valid: CONFIG.validNotEmptyString },
+            ldapBase: { value: "", valid: CONFIG.validNotEmptyString },
+            database: { value: "", valid: CONFIG.validNotEmptyString }
+        });
 
     configEntries.photoLibraries = [];
-    for (let i of configContent.photoLibraries)
-        if (validNotEmptyString(i))
+    let hasErrors = false;
+    for (let i of CONFIG.getInitialContent().photoLibraries)
+        if (CONFIG.validNotEmptyString(i))
             configEntries.photoLibraries.push(i);
 
     configEntries.ldapFilters = [];

+ 0 - 209
src/databaseHelper.js

@@ -1,209 +0,0 @@
-
-const sqlite3 = require('sqlite3');
-const SessionModel = require('../model/session.js').SessionModel;
-const MediaFileModel = require('../model/mediaItem.js').MediaFileModel;
-const MediaMetaModel = require('../model/mediaItemMeta.js').MediaFileMetaModel;
-const MediaTagModel = require('../model/mediaItemTag.js').MediaFileTagModel;
-const AccessModel = require('../model/access.js').AccessModel;
-const CONFIG = require('./config.js');
-
-let ALL_MODELS = [ SessionModel, MediaFileModel, MediaMetaModel, MediaTagModel, AccessModel ];
-
-function DatabaseHelper() {
-    this.db = null;
-}
-
-DatabaseHelper.prototype.init = function() {
-    return new Promise((ok, ko) => {
-        this.db = new sqlite3.Database(CONFIG.database, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE | sqlite3.OPEN_FULLMUTEX, async (err) => {
-            if (err) {
-                ko(err);
-                return;
-            }
-            let types = ALL_MODELS;
-            for (let i =0; i < types.length; ++i) {
-                let instance = new types[i]();
-                await instance.createOrUpdateBase(this);
-            }
-            console.log("Database is ready");
-            ok();
-        });
-    });
-}
-
-DatabaseHelper.prototype.runSql = function(sqlStatement, args) {
-    console.info("DatabaseHelper::runSql ", sqlStatement, args);
-    return new Promise((ok, ko) => {
-        if (sqlStatement.indexOf('--') >= 0) {
-            console.error("Rejecting SQL request containing comments");
-            ko("SQL Comment Error");
-            return;
-        }
-        try {
-            this.db.all(sqlStatement, args, (err, data) => {
-                if (err) {
-                    console.error(err);
-                    ko(err);
-                } else {
-                    ok(data);
-                }
-            });
-        } catch (err) {
-            console.error(err);
-            ko(err);
-        }
-    });
-}
-
-DatabaseHelper.prototype.insertOne = async function(object) {
-    let columns = [];
-    let args = [];
-
-    for (let [key, val] of Object.entries(object.describe())) {
-        columns.push("`"+key+"`");
-        args.push(val);
-    }
-    let request = "insert into `" +object.getTableName()
-            +"` (" +columns.join(", ")
-            +") VALUES (" +columns.map(i => "?").join(",")+")";
-    await this.runSql(request, args);
-}
-
-DatabaseHelper.prototype.insertMultipleSameTable = async function(arr) {
-    let columns = [];
-    let args = [];
-
-    if (!arr || !arr.length)
-        return;
-    for (let [key, val] of Object.entries(arr[0].describe()))
-        columns.push("`"+key+"`");
-    let request = "insert into `" +arr[0].getTableName()
-            +"` (" +columns.join(", ")
-            +") VALUES ";
-    let valuesArr = [];
-    request += arr.map(i => ("(" +columns.map(i => "?").join(",")+")")).join(',');
-    for (let i of arr)
-    {
-        for (let [key, val] of Object.entries(i.describe()))
-            args.push(val);
-    }
-    await this.runSql(request, args);
-}
-
-DatabaseHelper.prototype.upsertOne = async function(obj) {
-    let columns = [];
-    let columnsVal = [];
-    let args = [];
-    let i =0;
-
-    for (let [key, val] of Object.entries(obj.describe())) {
-        columns.push("`"+key+"`");
-        columnsVal.push("`"+key+"`=:"+(i++));
-        args.push(val);
-    }
-    let request = "insert into `" +obj.getTableName()
-            +"` (" +columns.join(", ")
-            +") VALUES (" +columns.map((_, i) => `:${i}`).join(",")+")"
-            +" ON CONFLICT DO UPDATE SET " +columnsVal.join(", ");
-    await this.runSql(request, args);
-}
-
-DatabaseHelper.prototype.buildWhere = function(whereObj, columns, args) {
-    columns = columns || [];
-    args = args || [];
-    if (!whereObj) {
-        columns.push("1=1");
-    } else {
-        for (let [key, val] of Object.entries(whereObj)) {
-            if (val === null) {
-                columns.push("`"+key+"` is null");
-            } else if (Array.isArray(val)) {
-                columns.push("`"+key+"` in (" +val.map(i => "?").join(",") + ")");
-                for (let i of val) args.push(i);
-            } else {
-                columns.push("`"+key+"`=?");
-                args.push(val);
-            }
-        }
-    }
-    return {
-        columns: columns,
-        args: args
-    };
-}
-
-DatabaseHelper.prototype.maxVersion = async function(instanceName) {
-    let queryParts = [];
-    for (let i of ALL_MODELS)
-        queryParts.push("select max(" +i.prototype.versionColumn.call(null) +") as v from " +i.prototype.getTableName.call(null) +" where instance=:0");
-    let version = await this.runSql("select max(v) as v from (" +queryParts.join(" union ") +")", [ instanceName ]);
-    return (version?.[0]?.v) || 0;
-}
-
-DatabaseHelper.prototype.versionFetch = async function(objectPrototype, minVersion) {
-    let query = "select * from `" +objectPrototype.prototype.getTableName.call(null) +"` where `" +objectPrototype.prototype.versionColumn.call(null)+"` > ? and instance=?";
-    let result = [];
-    for (let i of await this.runSql(query, [minVersion, CONFIG.instanceHostname]))
-    {
-        let resultObj = new objectPrototype();
-        resultObj.fromDb(i);
-        result.push(resultObj);
-    }
-    return result;
-}
-
-DatabaseHelper.prototype.fetch = async function(objectPrototype, where, orderBy) {
-    let result = await this.fetchRaw("*", objectPrototype.prototype.getTableName.call(null), where, orderBy);
-    let resultArr = [];
-    for (let i of result)
-    {
-        let resultObj = new objectPrototype();
-        resultObj.fromDb(i);
-        resultArr.push(resultObj);
-    }
-    return resultArr;
-}
-
-DatabaseHelper.prototype.fetchRaw = async function(columns, tableName, where, orderBy) {
-    let whereArgs = this.buildWhere(where);
-    let query = "select " + (Array.isArray(columns) ? columns.join(","): columns) +" from `" +tableName +"` where " +whereArgs.columns.join(" and ");
-    if (orderBy)
-        query += " ORDER BY " +Object.keys(orderBy || {}).map(i => "`"+i+"` " +(orderBy[i] === 'DESC' ? "DESC":"ASC")).join(",");
-    return await this.runSql(query, whereArgs.args);
-}
-
-DatabaseHelper.prototype.findOne = async function(objectPrototype, where) {
-    let whereArgs = this.buildWhere(where);
-    let result = await this.runSql("select * from `" +objectPrototype.prototype.getTableName.call(null) +"` where " +whereArgs.columns.join(" and "), whereArgs.args);
-    if (result && result.length)
-    {
-        let resultObj = new objectPrototype();
-        resultObj.fromDb(result[0]);
-        return resultObj;
-    }
-    return null;
-}
-
-DatabaseHelper.prototype.update = async function(where, object) {
-    if (object.lastUpdated)
-        object.lastUpdated = new Date();
-    await this.rawUpdate(object, where, object.describe());
-}
-
-DatabaseHelper.prototype.rawUpdate = async function(model, where, values) {
-    let columns = [];
-    let args = [];
-
-    for (let [key, val] of Object.entries(values)) {
-        columns.push("`"+key+"`=?");
-        args.push(val);
-    }
-    let whereArgs = this.buildWhere(where, [], args);
-    let query = "update `" +(model.prototype?.getTableName ? model.prototype.getTableName() : model.getTableName())
-            +"` set " +columns.join(", ")
-            +" WHERE " +whereArgs.columns.join(" and ");
-    await this.runSql(query, args);
-}
-
-module.exports.DatabaseHelper = new DatabaseHelper();
-

+ 45 - 32
src/filetype/imagemagick.js

@@ -6,26 +6,31 @@ const geokit = require('geokit');
 
 function readMeta(path) {
     return new Promise((ok, ko) => {
-        im.identify(['-format', '%[EXIF:*]Compression=%[compression]\nWidth=%w\nHeight=%h\n', path], function(err, stdout)
-        {
-            if (err)
-                return ok(null);
-            var meta = {};
-            for (const line of stdout.split(/\n/))
+        try {
+            im.identify(['-format', '%[EXIF:*]Compression=%[compression]\nWidth=%w\nHeight=%h\n', path], function(err, stdout)
             {
-                var eq_p = line.indexOf('=');
-                if (eq_p === -1)
-                    continue;
-                var key = line.substr(0, eq_p).replace('/','-'),
-                    value = line.substr(eq_p+1).trim();
-                var p = key.indexOf(':');
-                if (p !== -1)
-                    key = key.substr(p+1);
-                key = key.charAt(0).toLowerCase() + key.slice(1);
-                meta[key] = value;
-            }
-            ok(meta);
-        });
+                if (err)
+                    return ok(null);
+                var meta = {};
+                for (const line of stdout.split(/\n/))
+                {
+                    var eq_p = line.indexOf('=');
+                    if (eq_p === -1)
+                        continue;
+                    var key = line.substr(0, eq_p).replace('/','-'),
+                        value = line.substr(eq_p+1).trim();
+                    var p = key.indexOf(':');
+                    if (p !== -1)
+                        key = key.substr(p+1);
+                    key = key.charAt(0).toLowerCase() + key.slice(1);
+                    meta[key] = value;
+                }
+                ok(meta);
+            });
+        } catch (err) {
+            console.error("readMeta from Imagemagick: ", err);
+            ok(null);
+        }
     });
 }
 
@@ -125,19 +130,27 @@ module.exports.createThumbnail = (fileObj, width, height, quality) => {
             width = height = 420;
         let ratio = Math.min((width / fileObj.meta.width) || 1, (height / fileObj.meta.height) || 1, 1);
         const output = tmp.fileSync({ discardDescriptor: true });
-        im.convert([
-            fileObj.path,
-            '-strip',
-            '-interlace', 'Plane',
-            '-resize', (Math.floor(fileObj.meta.width * ratio)),
-            '-quality', '' + (Math.floor(Math.max(0, Math.min(100, (quality *10))))) + '%',
-            '-sampling-factor', '4:2:0',
-            `JPG:${output.name}`,
-        ], err => {
-            if (err)
-                throw err;
-            ok(output);
-        });
+        try {
+            im.convert([
+                fileObj.path,
+                '-strip',
+                '-interlace', 'Plane',
+                '-resize', (Math.floor(fileObj.meta.width * ratio)),
+                '-quality', '' + (Math.floor(Math.max(0, Math.min(100, (quality *10))))) + '%',
+                '-sampling-factor', '4:2:0',
+                `JPG:${output.name}`,
+            ], err => {
+                if (err) {
+                    console.error("Imagemagick createThumbnail error: ", err);
+                    ko();
+                } else {
+                    ok(output);
+                }
+            });
+        }
+        catch (err) {
+            ko(err);
+        }
     });
 };
 

+ 2 - 2
src/library.js

@@ -1,8 +1,8 @@
 const fs = require('fs');
 const path = require('path');
 const mime = require("mime-types");
-const md5Stats = require('./md5sum.js').stats;
-const md5String = require('./md5sum.js').string;
+const md5Stats = require('craftlabhttpserver/src/md5sum').stats;
+const md5String = require('craftlabhttpserver/src/md5sum.js').string;
 const FileTypeManager = require('./fileTypeManager.js');
 
 const MediaFileModel = require("../model/mediaItem.js").MediaFileModel;

+ 0 - 44
src/md5sum.js

@@ -1,44 +0,0 @@
-
-const crypto = require('crypto');
-const fs = require('fs');
-
-function md5File(path) {
-    return new Promise((ok, ko) => {
-        const readStream = fs.createReadStream(path);
-        let hash = crypto.createHash('md5');
-
-        readStream.on('data', chunk => hash.update(chunk.toString()));
-        readStream.once('end', () => {
-            ok(hash.digest('hex'));
-        });
-        readStream.once('error', () => ok(""));
-    });
-}
-
-function md5String(input) {
-    return crypto.createHash('md5').update(input).digest('hex');
-}
-
-function md5Stats(path) {
-    return new Promise((ok, ko) => {
-        fs.stat(path, (err, st) => {
-            if (err)
-                ok("");
-            else
-                ok(md5String(JSON.stringify({
-                    path: path,
-                    size: st.size,
-                    atimeMs: st.atimeMs,
-                    mtimeMs: st.mtimeMs,
-                    ctimeMs: st.ctimeMs
-                })));
-        });
-    });
-}
-
-module.exports = {
-    file: md5File,
-    string: md5String,
-    stats: md5Stats
-}
-

+ 1 - 171
src/routerUtils.js

@@ -1,173 +1,3 @@
 
-const mime = require('mime-types');
-const path = require('path');
-const fs = require('fs');
-const Security = require('./security.js');
-const CONFIG = require('./config.js');
-
-function RouterUtils(app) {
-    this.app = app;
-}
-
-RouterUtils.prototype.httpResponse = function(res, code, response) {
-    res.writeHead(code);
-    res.end(response);
-    return true;
-}
-
-RouterUtils.prototype.requireLogin =function(req, res) {
-    if (Security.isLoggedUser(req.cookies))
-    {
-        req.loggedUser = Security.getLoggedUser(req.cookies);
-        req.loggedSession = Security.getSessionId(req.cookies);
-        return false;
-    }
-    this.redirect(res, '/login?page='+encodeURIComponent(req.url));
-    return true;
-};
-
-RouterUtils.prototype.onApiRequest = function(req, res) {
-    this.onRequest(req);
-    req.accessList = Security.getAccessList(req.cookies);
-    if (req.accessList === null) {
-        const log = Security.createSession(req);
-        res.setHeader("Set-Cookie", Security.SESSION_COOKIE +'='+log.key);
-        req.accessList = log.accessList;
-    }
-};
-
-RouterUtils.prototype.apiRequireLogin =function(req, res, validTokens) {
-    if (Security.isLoggedUser(req.cookies))
-    {
-        req.loggedUser = Security.getLoggedUser(req.cookies);
-        req.loggedSession = Security.getSessionId(req.cookies);
-        return false;
-    }
-    if (validTokens && req.body?.apiKey && validTokens.indexOf(req.body?.apiKey) >= 0)
-    {
-        req.loggedUser = req.body.apiKey;
-        req.loggedSession = "";
-        return false;
-    }
-    return this.httpResponse(res, 403, "Unauthorized Access");
-};
-
-RouterUtils.prototype.redirect = function(res, url) {
-    res.writeHead(302, { Location: url });
-    res.end();
-}
-
-RouterUtils.prototype.prepareCookie = function(req) {
-    req.cookies = {};
-    let arr = ((req.headers?.cookie || "").split(';').map(i => i.split('=', 2))).forEach(i => { req.cookies[i[0].trim()] = decodeURIComponent(i[1]).trim();});
-}
-
-RouterUtils.prototype.onRequest = function(req) {
-    this.prepareCookie(req);
-}
-
-RouterUtils.prototype.readPostBody = function(req, res) {
-    const now = Math.floor(Date.now() / 1000);
-    return new Promise((ok, ko) => {
-        if (req.headers['content-type'] !== 'application/json') {
-            console.error("Unexpected input from query: wrong Content-Type");
-            ko();
-            return;
-        }
-        let data = null;
-        try {
-            data = JSON.parse(req.body.data);
-        } catch (e) {
-            console.error("Unexpected input from query: invalid JSON");
-            ko();
-            return;
-        }
-        if (!data.time || Math.abs(now - data.time) > 3) {
-            console.error("Unexpected input from query: Invalid time");
-            ko();
-            return;
-        }
-        if (!data.hostname) {
-            console.error("Unexpected input from query: missing hostname");
-            ko();
-            return;
-        }
-        req.data = data;
-        ok();
-    });
-}
-
-RouterUtils.prototype.apiError = function(res) {
-    res.writeHead(400, { "Content-Type": "application/json"});
-    res.end();
-}
-
-RouterUtils.prototype.jsonResponse = function(res, data) {
-    res.writeHead(200, { "Content-Type": "application/json"});
-    if (typeof data !== 'string')
-        data = JSON.stringify(data);
-    res.end(data);
-}
-
-RouterUtils.prototype.onPageNotFound = function(res) {
-    return this.httpResponse(res, 404, "Page not found...");
-}
-
-RouterUtils.prototype.staticServe = async function(res, filePath) {
-    return new Promise((ok, ko) => {
-        try {
-            const stream = fs.createReadStream(filePath);
-            let onError = false;
-            stream.once('error', err => {
-                ko(err);
-                onError = true;
-            });
-            const fileSize = fs.statSync(filePath)?.size || undefined;
-            if (!stream || !fileSize || onError) {
-                console.error("RouterUtils::staticGet", filePath, err);
-                this.httpResponse(res, 500, "Internal Server Error");
-                return ko(err);
-            }
-            res.writeHead(200, {
-                "Content-Type": mime.contentType(path.basename(filePath)),
-                "Content-Length": fileSize
-            });
-            stream.pipe(res);
-            stream.once('end', () => ok());
-        } catch (err) {
-            ko(err);
-        }
-    });
-}
-
-RouterUtils.prototype.staticGet = function(app, url, staticResources) {
-    app.router.get(url, (req, res) => {
-        app.routerUtils.staticServe(res, staticResources).catch(err => {
-            app.routerUtils.onPageNotFound(res);
-        });
-    });
-}
-
-RouterUtils.encodeUrlComponent = function(input) {
-    return btoa(input).replaceAll('=', '-').replaceAll('+', '_');
-}
-
-RouterUtils.prototype.encodeUrlComponent = function(input) {
-    return RouterUtils.encodeUrlComponent(input);
-}
-
-RouterUtils.decodeUrlComponent = function(input) {
-    return atob(input.replaceAll('-', '=').replaceAll('_', '+'));
-}
-
-RouterUtils.prototype.decodeUrlComponent = function(input) {
-    return RouterUtils.decodeUrlComponent(input);
-}
-
-RouterUtils.prototype.commonRenderInfos = function() {
-    return {
-    };
-}
-
-module.exports = { RouterUtils: RouterUtils, encodeUrlComponent: RouterUtils.encodeUrlComponent, decodeUrlComponent: RouterUtils.decodeUrlComponent };
+module.exports = require('craftlabhttpserver/src/routerUtils.js');
 

+ 36 - 84
src/security.js

@@ -1,62 +1,16 @@
 
-const CONFIG = require('./config.js');
-const SESSION_TIME = 2 * 1 * 60 * 60 * 1000; // 2h
-const SESSION_COOKIE = "_sessionId";
 const crypto = require('crypto');
-const ldapjs = require('ldapjs');
-const ldap = ldapjs.createClient({
-    url: [ CONFIG.ldapUrl, CONFIG.ldapUrl ],
-    reconnect: true
-    });
-const MD5 = require('./md5sum.js').string;
-const SessionModel = require('../model/session.js').SessionModel;
+const MD5 = require('craftlabhttpserver/src/md5sum.js').string;
 
-let loggedCache = {};
-
-let ldapReady = new Promise((ok, ko) => {
-    ldap.on("error", (err) => { console.error("LDAP Error: " +err) });
-    ldap.bind(CONFIG.ldapBindDN, CONFIG.ldapBindPwd, (err) => {
-        if (err) {
-            console.error(err);
-            ko(err);
-            throw err;
-        }
-        console.log("LDAP is ready");
-        ok();
-    });
-});
-
-function getSessionId(cookieObject) {
-    return cookieObject?.[SESSION_COOKIE];
-}
-
-function getSessionObj(cookieObject) {
-    let cookie = getSessionId(cookieObject);
-    if (!cookie)
-        return null;
-    let sessionEntry = loggedCache[cookie];
-    const now = (new Date()).getTime();
-    if (!sessionEntry || sessionEntry.expire < now)
-        return null;
-    sessionEntry.expire = now + SESSION_TIME;
-    return sessionEntry;
-}
+module.exports = require('craftlabhttpserver/src/security.js');
 
 function getAccessList(cookieObject) {
-    let session = getSessionObj(cookieObject);
+    let session = module.exports.getSessionObj(cookieObject);
     if (!session)
         return null;
     return session.accessList;
 }
 
-function getRequestIp(req) {
-    return req.headers['x-forwarded-for'] || req.socket.remoteAddress;
-}
-
-function sign(msg) {
-    return crypto.sign('sha256', Buffer.from(msg), decodeKey(CONFIG.privKey)).toString('base64');
-}
-
 function Access() {
 }
 Access.prototype.id = function() { return ""; }
@@ -68,40 +22,38 @@ function LinkAccess(linkId) {
 LinkAccess.prototype = Object.create(Access.prototype);
 LinkAccess.prototype.id = function() { return "LINK_"+this.linkId; }
 
-module.exports = {
-    getAccessList: getAccessList,
-    getRequestIp: getRequestIp,
-    createSession: req => {
-        const now = Date.now();
-        let sessionInfos = {
-            loginDateTime: now,
-            expire: now + SESSION_TIME,
-            accessList: {},
-            random: Math.random(),
-            userAgent: req.headers['user-agent'],
-            ipAddress: getRequestIp(req)
-        };
-        let sessionKey = MD5(JSON.stringify(sessionInfos));
-        sessionInfos.sessionId = sessionKey;
-        loggedCache[sessionKey] = sessionInfos;
-        req.cookies[SESSION_COOKIE] = sessionKey;
-        return { key: sessionKey, accessList: sessionInfos.accessList };
-    },
-    addLinkToSession: (req, linkId) => {
-        let session = getSessionObj(req.cookies);
-        if (!session)
-            return;
-        let accessList = new LinkAccess(linkId);
-        session.accessList[accessList.id()] = accessList;
-        return session.accessList;
-    },
-    removeFromSession: (req, accessId) => {
-        let session = getSessionObj(req.cookies);
-        if (!session)
-            return;
-        delete session.accessList[accessId];
-        return session.accessList;
-    },
-    SESSION_COOKIE: SESSION_COOKIE
+module.exports.getAccessList = getAccessList;
+module.exports.createSession = req => {
+    const now = Date.now();
+    let sessionInfos = {
+        loginDateTime: now,
+        expire: now + module.exports.SESSION_TIME,
+        random: Math.random(),
+        userAgent: req.headers['user-agent'],
+        ipAddress: module.exports.getRequestIp(req),
+        data: {
+            accessList: {}
+        }
+    };
+    let sessionKey = MD5(JSON.stringify(sessionInfos));
+    sessionInfos.sessionId = sessionKey;
+    module.exports.pushSession(sessionKey, sessionInfos);
+    req.cookies[module.exports.SESSION_COOKIE] = sessionKey;
+    return { key: sessionKey, accessList: sessionInfos.data.accessList };
+};
+module.exports.addLinkToSession = (req, linkId) => {
+    let session = getSessionObj(req.cookies);
+    if (!session)
+        return;
+    let accessList = new LinkAccess(linkId);
+    session.accessList[accessList.id()] = accessList;
+    return session.accessList;
+};
+module.exports.removeFromSession = (req, accessId) => {
+    let session = getSessionObj(req.cookies);
+    if (!session)
+        return;
+    delete session.accessList[accessId];
+    return session.accessList;
 };
 

+ 16 - 0
static/public/css/style.css

@@ -40,6 +40,22 @@
     transition: scale 700ms;
 }
 
+#pch-mediaList > .pch-image:hover input,
+    #pch-mediaList.selection > .pch-image input {
+    visibility: visible;
+}
+
+#pch-mediaList > .pch-image input {
+    display: inline-block;
+    visibility: hidden;
+    position: absolute;
+    top: 2rem;
+    left: 2.5rem;
+    height: 2rem;
+    width: 2rem;
+    border-radius: 1rem;
+}
+
 #pch-mediaList > h3, #pch-mediaList > h4 {
     text-align: left;
 }

+ 1 - 1
static/public/js/common.js

@@ -3,7 +3,7 @@ $(() => {
     function RebuildAccess() {
         LoadingTasks.push(() => {
             $.get("/api/access/list", data => {
-                console.log("Access list => ", data);
+                window.ReloadAccessList(data);
             });
         });
     }

+ 26 - 0
static/public/js/medias.js

@@ -70,6 +70,32 @@ class MediaStorage extends EventTarget
         }
     }
 
+    getMediaIndex(media) {
+        return this.medias.indexOf(media);
+    }
+
+    nextMedia(current) {
+        return this.medias[this.getMediaIndex(current) +1];
+    }
+
+    previousMedia(current) {
+        return this.medias[this.getMediaIndex(current) -1];
+    }
+
+    getMediaBetweenIndexes(a, b) {
+        if (a > b)
+            return this.getMediaBetweenIndexes(b, a);
+        return this.medias.slice(a, b +1);
+    }
+
+    getMediaBetween(a, b) {
+        let aIndex = this.medias.indexOf(a);
+        let bIndex = this.medias.indexOf(b);
+        if (aIndex < 0 || bIndex < 0 || aIndex === bIndex)
+            return [];
+        return this.getMediaBetweenIndexes(aIndex, bIndex);
+    }
+
     async getMedia(md5sum) {
         let media = this.medias.find(x => x.md5sum === md5sum);
         if (media)

+ 33 - 0
static/public/js/uiAccess.js

@@ -0,0 +1,33 @@
+
+$(() => {
+
+document.getElementById("menu-login").addEventListener("click", e => {
+    e.preventDefault();
+    document.body.classList.add("login-visible")
+});
+
+window.ReloadAccessList = function(accessList) {
+    let rootNode = document.getElementById("accessListMenu");
+    let items = rootNode.querySelectorAll("li.accessItem");
+    for (let i =0; i < items.length; ++i)
+        items[i].remove();
+    if (accessList.length) {
+        let li = document.createElement("li");
+        li.classList.add("accessItem");
+        li.classList.add("divider");
+        let hr = document.createElement("hr");
+        hr.classList.add("dropdown-divider");
+        li.appendChild(hr);
+        rootNode.appendChild(li);
+    }
+    for (let i of accessList) {
+        let li = document.createElement("li");
+        li.classList.add("accessItem");
+        let a = document.createElement("a");
+        a.innerText = i.name;
+        li.appendChild(a);
+        rootNode.appendChild(li);
+    }
+}
+
+});

+ 95 - 1
static/public/js/uiMedia.js

@@ -1,11 +1,27 @@
 
 $(() => {
+    var fullPageMediaDisplayed = false;
+    var selectedThumbnails = [];
+    var lastKeyboardEvent = null;
+    var lastSelection = null;
+
+    function onItemSelected(mediaItem) {
+        document.getElementById("pch-mediaList").classList.add("selection");
+    }
+
+    function onItemDeselected(mediaItem) {
+        if (!selectedThumbnails.length)
+            document.getElementById("pch-mediaList").classList.remove("selection");
+    }
+
     function buildThumbnail(mediaItem) {
         if (mediaItem.ui)
             return mediaItem.ui;
+        let checkbox = document.createElement("input");
         let img = document.createElement("img");
         let container = document.createElement("li");
         let loadingImg = document.createElement("div");
+        checkbox.type = "checkbox";
         container.classList.add("pch-image");
         container.classList.add("loading");
         loadingImg.classList.add("spinner");
@@ -23,12 +39,59 @@ $(() => {
         container.style.width = `${requestSize.width}px`;
         container.appendChild(loadingImg);
         container.appendChild(img);
+        container.appendChild(checkbox);
+        let setSelectionCheckboxValue = function(media, value) {
+            let checked = media.ui.checkbox.checked;
+            let indexInSelection = selectedThumbnails.indexOf(media.md5sum);
+            if (checked && indexInSelection < 0) {
+                selectedThumbnails.push(media.md5sum);
+                onItemSelected(media);
+                return true;
+            } else if (!checked && indexInSelection >= 0) {
+                selectedThumbnails.splice(indexInSelection, 1);
+                onItemDeselected(media);
+                return true;
+            }
+            return false;
+        }
+        let cascadeSetSelectionCheckboxValue = function(value) {
+            if (!setSelectionCheckboxValue(mediaItem, value))
+                return;
+            if (lastKeyboardEvent?.shiftKey && lastSelection) {
+                let _lastKeyboardEvent = lastKeyboardEvent;
+                lastKeyboardEvent = null;
+                for (let i of MediaStorage.Instance.getMediaBetween(lastSelection, mediaItem)) {
+                    if (i === mediaItem)
+                        continue;
+                    i.ui.checkbox.setAttribute("checked", value);
+                    i.ui.checkbox.checked = value;
+                    setSelectionCheckboxValue(i, value);
+                }
+                lastKeyboardEvent = _lastKeyboardEvent;
+            }
+            lastSelection = mediaItem;
+            console.log(mediaItem);
+        }
         container.addEventListener("click", () => {
+            if (selectedThumbnails.length || lastKeyboardEvent?.ctrlKey) {
+                let value = !checkbox.checked;
+                checkbox.setAttribute("checked", value);
+                checkbox.checked = value;
+                cascadeSetSelectionCheckboxValue(value);
+                return;
+            }
             document.location.hash = mediaItem.md5sum;
         });
+        checkbox.addEventListener("click", evt => {
+            evt.stopPropagation();
+        });
+        checkbox.addEventListener("change", evt => {
+            cascadeSetSelectionCheckboxValue(checkbox.checked);
+        });
         return mediaItem.ui = {
             root: container,
-            img: img
+            img: img,
+            checkbox: checkbox
         };
     }
 
@@ -97,8 +160,21 @@ $(() => {
         });
     }
 
+    function LoadPreviousMedia() {
+        let media = MediaStorage.Instance.previousMedia(fullPageMediaDisplayed);
+        if (media)
+            window.displayMediaFullPage(media);
+    }
+
+    function LoadNextMedia() {
+        let media = MediaStorage.Instance.nextMedia(fullPageMediaDisplayed);
+        if (media)
+            window.displayMediaFullPage(media);
+    }
+
     window.displayMediaFullPage = function(mediaItem) {
         document.getElementById("pch-fullPageMedia").classList.remove("hidden");
+        fullPageMediaDisplayed = mediaItem;
         if (!mediaItem)
             return _displayMediaFullPage("Error", null, {}, null);
         let containerSize = document.getElementById("pch-fullPageMedia").getBoundingClientRect();
@@ -116,6 +192,7 @@ $(() => {
     document.getElementById("pch-fullPageMedia-closeBt")
         .addEventListener("click", () => {
             document.getElementById("pch-fullPageMedia").classList.add("hidden");
+            fullPageMediaDisplayed = false;
             history.pushState({}, '', '#');
         });
     document.getElementById("pch-fullPagePreview").addEventListener("load", () => {
@@ -133,5 +210,22 @@ $(() => {
         link.href = evt.target.dataset.link;
         link.click();
     });
+    document.addEventListener("keyup", evt => {
+        lastKeyboardEvent = evt;
+    });
+    document.addEventListener("keydown", evt => {
+        lastKeyboardEvent = evt;
+        if (!fullPageMediaDisplayed)
+            return;
+        if (evt.keyCode === 37 || evt.keyCode === 38)
+            LoadPreviousMedia();
+        else if (evt.keyCode === 39 || evt.keyCode === 40)
+            LoadNextMedia();
+        else {
+            console.log("Unregistered key event", evt.key, evt.keyCode);
+            return;
+        }
+        evt.preventDefault();
+    });
 });
 

+ 1 - 0
templates/footer.js → templates/_footer.js

@@ -5,6 +5,7 @@ module.exports = `
 <script src="/public/js/taskQueue.js"></script>
 <script src="/public/js/medias.js"></script>
 <script src="/public/js/uiMedia.js"></script>
+<script src="/public/js/uiAccess.js"></script>
 <script src="/public/js/chronology.js"></script>
 <script src="/public/js/common.js"></script>
 </body></html>`;

+ 27 - 0
templates/_fullPageMedia.js

@@ -0,0 +1,27 @@
+
+module.exports = `
+    <div id="pch-fullPageMedia" class="modal hidden" role="dialog"><div class="modal-dialog"><div class="modal-content">
+        <div class="modal-header">
+            <h5 id="pch-fullPageMedia-title" class="modal-title"></h5>
+            <button id="pch-fullPageMedia-closeBt" type="button" class="close" data-dismiss="modal" aria-label="Close">
+                <span aria-hidden="true">&times;</span>
+            </button>
+        </div>
+        <div class="container modal-body">
+            <div class="row">
+                <div class="col col-12 col-xl-9">
+                    <div id="pch-fullPagePreviewContainer" class="pch-image loading">
+                        <div id="pch-fullPagePreviewSpinner" class="spinner"><span class="spinner-grow"></span></div>
+                        <img id="pch-fullPagePreview" src="" class="img-fluid"/>
+                    </div>
+                </div>
+                <div class="col col-12 col-xl-3">
+                    <div>
+                        <button type="button" class="btn btn-primary" id="pch-fullPageDetail-dlButton">Download</button>
+                    </div>
+                    <div id="pch-fullPageDetail"></div>
+                </div>
+            </div>
+        </div>
+    </div>
+    `;

+ 0 - 0
templates/header.js → templates/_header.js


+ 3 - 0
templates/_login.js

@@ -0,0 +1,3 @@
+
+module.exports = `
+`;

+ 8 - 11
templates/menu.js → templates/_menu.js

@@ -1,14 +1,10 @@
 
 module.exports = `
 <body>
-    <nav id="pch-navbar" class="navbar navbar-expand-lg bg-body-tertiary fixed-top">
+    <div class="fixed-top">
+    <nav id="pch-navbar" class="navbar navbar-expand-lg bg-body-tertiary">
         <div class="container-fluid" id="navbarSupportedContent">
-            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
-                <li class="nav-item">
-                    <a class="nav-link active" href="#">Home</a>
-                </li>
-            </ul>
-            <ul class="navbar-nav mb-2 mb-lg-0">
+            <ul class="navbar-nav col justify-content-end">
                 <form class="d-flex" role="search">
                     <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                     <button class="btn btn-outline-success" type="submit">Search</button>
@@ -18,14 +14,15 @@ module.exports = `
                         <i class="bi bi-person-circle"></i>
                     </a>
                     <ul class="dropdown-menu" id="accessListMenu">
-                        <li><a class="dropdown-item" href="#">Action</a></li>
-                        <li><a class="dropdown-item" href="#">Another action</a></li>
-                        <li><hr class="dropdown-divider"></li>
-                        <li><a class="dropdown-item" href="#">Something else here</a></li>
+                        <li id="menu-login"><a class="dropdown-item" href="#">Login</a></li>
                     </ul>
                 </li>
             </ul>
         </div>
     </nav>
+    <nav id="pch-filterbar" class="navbar bg-body-tertiary"><div class="container-fluid">
+        Lorem Ipsum
+    </div></nav>
+    </div>
 `;
 

+ 8 - 27
templates/index.js

@@ -1,34 +1,15 @@
 
-module.exports = require('./header.js') +require('./menu.js')
+module.exports = require('./_header.js') +require('./_menu.js')
     +`
 <div id="pch-page">
     <ul id="pch-mediaList"></ul>
     <div class="spinner" id="pch-infiniteScrollLoading">
         <div class="spinner-grow"></div>
-    </div>
-    <div id="pch-fullPageMedia" class="modal hidden" role="dialog"><div class="modal-dialog"><div class="modal-content">
-        <div class="modal-header">
-            <h5 id="pch-fullPageMedia-title" class="modal-title"></h5>
-            <button id="pch-fullPageMedia-closeBt" type="button" class="close" data-dismiss="modal" aria-label="Close">
-                <span aria-hidden="true">&times;</span>
-            </button>
-        </div>
-        <div class="container modal-body">
-            <div class="row">
-                <div class="col col-12 col-xl-9">
-                    <div id="pch-fullPagePreviewContainer" class="pch-image loading">
-                        <div id="pch-fullPagePreviewSpinner" class="spinner"><span class="spinner-grow"></span></div>
-                        <img id="pch-fullPagePreview" src="" class="img-fluid"/>
-                    </div>
-                </div>
-                <div class="col col-12 col-xl-3">
-                    <div>
-                        <button type="button" class="btn btn-primary" id="pch-fullPageDetail-dlButton">Download</button>
-                    </div>
-                    <div id="pch-fullPageDetail"></div>
-                </div>
-            </div>
-        </div>
-</div></div></div></div>`
-    +require('./footer.js');
+    </div>`
+    +require('./_fullPageMedia.js')
+    +`
+</div>`
+    +require('./_login.js')
+    +`</div></div>`
+    +require('./_footer.js');