Browse Source

Locales, translations, ORM & user connector

isundil 4 years ago
parent
commit
5022bd9daf

+ 3 - 2
.gitignore

@@ -3,8 +3,9 @@ bin/
 obj/
 *.map
 package-lock.json
-/ACPSBooking/routes/*.js
+/ACPSBooking/db.sqlite
 /ACPSBooking/serverConfig.js
-/ACPSBooking/src/*.js
+/ACPSBooking/routes/*.js
+/ACPSBooking/src/**/*.js
 /ACPSBooking/server.js
 /ACPSBooking/node_modules/

+ 44 - 0
ACPSBooking/ACPSBooking.njsproj

@@ -31,6 +31,14 @@
     <DebugSymbols>true</DebugSymbols>
   </PropertyGroup>
   <ItemGroup>
+    <Content Include="locales\en\dashboard.json">
+      <SubType>Code</SubType>
+    </Content>
+    <Content Include="locales\en\index.json">
+      <SubType>Code</SubType>
+    </Content>
+    <Content Include="locales\fr\dashboard.json" />
+    <Content Include="locales\fr\index.json" />
     <Content Include="public\fonts\glyphicons-halflings-regular.eot" />
     <Content Include="public\fonts\glyphicons-halflings-regular.svg" />
     <Content Include="public\fonts\glyphicons-halflings-regular.ttf" />
@@ -87,15 +95,23 @@
     <Content Include="serverConfig.js">
       <SubType>Code</SubType>
     </Content>
+    <Content Include="serverConfig.js.sample" />
     <Content Include="views\error.pug">
       <SubType>Code</SubType>
     </Content>
+    <Content Include="views\dashboard.pug" />
     <Content Include="views\index.pug">
       <SubType>Code</SubType>
     </Content>
     <Content Include="views\login.pug">
       <SubType>Code</SubType>
     </Content>
+    <Content Include="views\setup_user.pug">
+      <SubType>Code</SubType>
+    </Content>
+    <Content Include="views\template\loggedpage.pug">
+      <SubType>Code</SubType>
+    </Content>
     <Content Include="views\template\page.pug" />
     <None Include="server.ts" />
     <Content Include="tsconfig.json" />
@@ -103,6 +119,9 @@
     <Content Include="README.md" />
   </ItemGroup>
   <ItemGroup>
+    <Folder Include="locales\" />
+    <Folder Include="locales\en\" />
+    <Folder Include="locales\fr\" />
     <Folder Include="public\" />
     <Folder Include="public\fonts\" />
     <Folder Include="public\javascript\" />
@@ -110,11 +129,15 @@
     <Folder Include="routes\" />
     <Folder Include="src\" />
     <Folder Include="src\DbConnector\" />
+    <Folder Include="src\models\" />
     <Folder Include="src\UserConnector\" />
     <Folder Include="views\" />
     <Folder Include="views\template\" />
   </ItemGroup>
   <ItemGroup>
+    <TypeScriptCompile Include="routes\accounting.ts" />
+    <TypeScriptCompile Include="routes\trombinoscope.ts" />
+    <TypeScriptCompile Include="routes\calendar.ts" />
     <TypeScriptCompile Include="routes\index.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
@@ -124,12 +147,33 @@
     <TypeScriptCompile Include="src\AConfigChecker.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
+    <TypeScriptCompile Include="src\DbConnector\AbstractDBConnector.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\DbConnector\DBConnectorFactory.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\DbConnector\Query.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\DbConnector\SequelizeConnector.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\models\ApiKey.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\models\User.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
     <TypeScriptCompile Include="src\Security.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
     <TypeScriptCompile Include="src\Session.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
+    <TypeScriptCompile Include="src\UserConnector\Sha1Db.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
     <TypeScriptCompile Include="src\UserConnector\UserConnectorFactory.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>

+ 3 - 0
ACPSBooking/locales/en/dashboard.json

@@ -0,0 +1,3 @@
+{
+	"hello": "Hello {{username}} !"
+}

+ 3 - 0
ACPSBooking/locales/en/index.json

@@ -0,0 +1,3 @@
+{
+	"hello":  "Hello World !"
+}

+ 3 - 0
ACPSBooking/locales/fr/dashboard.json

@@ -0,0 +1,3 @@
+{
+	"hello": "Bonjour {{username}} !"
+}

+ 3 - 0
ACPSBooking/locales/fr/index.json

@@ -0,0 +1,3 @@
+{
+	"hello":  "Bonjour le monde !"
+}

+ 10 - 5
ACPSBooking/package.json

@@ -12,13 +12,18 @@
         "start": "node server"
     },
     "dependencies": {
-        "@types/node": "^14.14.7",
         "@types/express": "^4.0.37",
-        "typescript": "^4.0.5",
+        "@types/node": "^14.14.7",
         "body-parser": "^1.19.0",
         "express": "^4.14.0",
-        "pug": "^3.0.2"
+        "i18n": "^0.13.3",
+        "i18next": "^20.3.2",
+        "i18next-http-middleware": "^3.1.4",
+        "i18next-resources-to-backend": "^1.0.0",
+        "pug": "^3.0.2",
+        "sequelize": "^6.6.4",
+        "sqlite3": "^5.0.2",
+        "typescript": "^4.0.5"
     },
-    "devDependencies": {
-    }
+    "devDependencies": {}
 }

+ 39 - 0
ACPSBooking/routes/accounting.ts

@@ -0,0 +1,39 @@
+import { Request, Response, Router } from "express";
+import DBConnectorFactory from "../src/DbConnector/DBConnectorFactory";
+import { Clause, QueryBuilder } from "../src/DbConnector/Query";
+import UserModel from "../src/models/User";
+
+const router = Router();
+
+router.get('/', (req: Request, res: Response) => {
+    return res.end("accounting dashboard"); // FIXME
+});
+
+function RenderUserAccounting(user: UserModel, req: Request, res: Response) {
+    return res.end(`accounting (user ${user.GetUsername()})`); // FIXME
+}
+
+router.get('/self', (req: Request, res: Response) => {
+    RenderUserAccounting(req.mSession.GetUser(), req, res);
+});
+
+router.get('/user/:userId', async (req: Request, res: Response, next: any) => {
+    const id = parseInt(req.params["userId"] || "", 10);
+    if (isNaN(id)) {
+        let err: any = new Error("Invalid Request");
+        err.status = 400;
+        next(err);
+        return;
+    }
+    const user: UserModel | null = await (await DBConnectorFactory.GetConnector()).FetchOne(new QueryBuilder().Where(Clause.Equal(UserModel.USERID, id)), UserModel);
+    if (user) {
+        RenderUserAccounting(user, req, res);
+    } else {
+        let err: any = new Error("No such entity " +id);
+        err.status = 404;
+        next(err);
+        return;
+	}
+});
+
+export default router;

+ 9 - 0
ACPSBooking/routes/calendar.ts

@@ -0,0 +1,9 @@
+import { Request, Response, Router } from "express";
+
+const router = Router();
+
+router.get('/', (req: Request, res: Response) => {
+    return res.end("calendar"); // FIXME
+});
+
+export default router;

+ 18 - 4
ACPSBooking/routes/index.ts

@@ -1,10 +1,24 @@
-import express = require('express');
-const router = express.Router();
+import { Request, Response, Router } from "express";
 
-router.get('/', (req: express.Request, res: express.Response) => {
-    res.render('index', {
+const router = Router();
+
+function RenderDashboard(req: Request, res: Response) {
+    res.renderWithTranslations('dashboard', 'dashboard', {
+        t: req.t,
         username: req.mSession.GetUsername()
     });
+}
+
+function RenderIndex(req: Request, res: Response) {
+    res.renderWithTranslations('index', 'index', {
+        t: (text: string) => {
+            return req.t(text);
+        }
+    });
+}
+
+router.get('/', (req: Request, res: Response) => {
+    return req.mSession.IsValid() ? RenderDashboard(req, res) : RenderIndex(req, res);
 });
 
 export default router;

+ 18 - 10
ACPSBooking/routes/login.ts

@@ -24,19 +24,27 @@ function extractRedirection(req: express.Request): string {
     return redir || "/";
 }
 
-router.all('/', (req: express.Request, res: express.Response) => {
+router.all('/', async (req: express.Request, res: express.Response) => {
     let redir = extractRedirection(req);
-    (requestIsPost(req) ?
-        (Security.TryLogin(req.mSession, req.body["username"], req.body["password"]).then(_ => {
+    let loginFailed = false;
+
+    if (requestIsPost(req)) {
+        try {
+            await Security.TryLogin(req.mSession, req.body["username"], req.body["password"]);
+        }
+        catch (err) {
+            loginFailed = true;
+        }
+        if (!loginFailed) {
             SessionManager.Write(res, req.mSession);
             res.redirect(302, redir);
-        }).catch(_ => Promise.resolve(true))) : Promise.resolve(false))
-    .then(loginFailed => {
-        res.render('login', {
-            username: req.mSession.GetUsername(),
-            failed: loginFailed,
-            redirect: redir
-        });
+            return;
+        }
+	}
+    res.render('login', {
+        username: req.mSession.GetUsername(),
+        failed: loginFailed,
+        redirect: redir
     });
 });
 

+ 13 - 0
ACPSBooking/routes/trombinoscope.ts

@@ -0,0 +1,13 @@
+import { Request, Response, Router } from "express";
+
+const router = Router();
+
+router.get('/', (req: Request, res: Response) => {
+    return res.end("trombi"); // FIXME
+});
+
+router.get('/instructors', (req: Request, res: Response) => {
+    return res.end("trombi (instructors filter)"); // FIXME
+});
+
+export default router;

+ 158 - 78
ACPSBooking/server.ts

@@ -3,11 +3,18 @@ import * as express from 'express';
 import { AddressInfo } from "net";
 import * as path from 'path';
 import * as bodyParser from 'body-parser';
+const i18next = require('i18next'); // FIXME import from
+const i18nextMiddleware = require('i18next-http-middleware'); // FIXME import from
+const resourcesToBackend = require('i18next-resources-to-backend'); // FIXME import from
 import route_index from './routes/index';
 import route_login from './routes/login';
+import route_calendar from './routes/calendar';
+import route_trombinoscope from './routes/trombinoscope';
+import route_accounting from './routes/accounting';
 import Security from './src/Security';
 import { Session, SessionManager } from './src/Session';
-import UserConnectorFactory from './src/UserConnector/UserConnectorFactory';
+import UserConnectorFactory, { IUserConnector } from './src/UserConnector/UserConnectorFactory';
+import DBConnectorFactory from './src/DbConnector/DBConnectorFactory';
 
 const debug = require('debug')('my express app');
 const app = express();
@@ -17,103 +24,176 @@ declare global {
         interface Request {
             mSession: Session
             mCookies: Map<string, string>
+            t: (s: string) => string // i18n translation function
+        }
+
+        interface Response {
+            i18next: any
+            renderWithTranslations: (translationNs: string, renderView: string, args: any) => Promise<void>
 		}
 	}
 }
 
 // Check config
-(_ => {
+(async _ => {
+    await i18next.use(i18nextMiddleware.LanguageDetector)
+        .use(resourcesToBackend((language: string, namespace: string, callback: Function) => {
+            import(`./locales/${language}/${namespace}.json`)
+                .then((resources) => {
+                    callback(null, resources)
+                })
+                .catch((error) => {
+                    callback(error, null)
+                })
+        }))
+        .init({
+            preload: ['fr', 'en'],
+            nonExplicitSupportedLngs: true,
+            lowerCaseLng: true
+        });
+
     let cc: AConfigChecker[] = [
-        UserConnectorFactory
+        UserConnectorFactory,
+        await DBConnectorFactory.GetConnector()
     ];
     for (let i of cc) {
-        let err = i.CheckConfig();
-        if (err) {
-            err = i.constructor.name + ": " + err;
-            console.error("Configuration error: " + err);
+        let err: Promise<void> | Error | null = i.CheckConfig();
+        let errResult: Error | null = null;
+        try {
+            if (err instanceof Promise)
+                await err;
+            else
+                errResult = err;
+        }
+        catch (_err) {
+            errResult = _err;
+        }
+        if (errResult !== null) {
+            console.error("Configuration error: " + i.constructor.name +": "+ errResult.message);
             throw "Configuration error";
         }
     }
-})();
 
-// view engine setup
-app.set('views', [path.join(__dirname, 'views'), path.join(__dirname, 'views/template')]);
-app.set('view engine', 'pug');
+    // view engine setup
+    app.set('views', [path.join(__dirname, 'views'), path.join(__dirname, 'views/template')]);
+    app.set('view engine', 'pug');
 
-app.use(express.static(path.join(__dirname, 'public')));
-app.use(bodyParser.json());
-app.use(bodyParser.urlencoded({ extended: true }));
+    app.use(express.static(path.join(__dirname, 'public')));
+    app.use(bodyParser.json());
+    app.use(bodyParser.urlencoded({ extended: true }));
+    app.use(i18nextMiddleware.handle(i18next, {
+        ignoreRoutes: [] // or function(req, res, options, i18next) { /* return true to ignore */ }
+    }));
 
-// Security and session setup
-app.use((req, _, next) => {
-    req.mCookies = new Map();
-    (req.headers?.cookie || "").split(";").forEach(i => {
-        let keyValue = i.split("=", 2);
-        req.mCookies.set(keyValue[0].trim(), keyValue[1].trim());
+    // Security and session setup
+    app.use((req: express.Request, res: express.Response, next) => {
+        res.i18next = i18next;
+        res.renderWithTranslations = async (translationNs: string, renderView: string, args: any) => {
+            await i18next.loadNamespaces(translationNs);
+            i18next.setDefaultNamespace(translationNs);
+            args = args || {};
+            args["t"] = req.t;
+            res.render(renderView, args);
+        };
+
+        req.mCookies = new Map();
+        (req.headers?.cookie || "").split(";").forEach(i => {
+            let keyValue = i.split("=", 2);
+            req.mCookies.set(keyValue[0].trim(), keyValue[1].trim());
+        });
+        req.mSession = SessionManager.GetSession(req);
+        if (req.mSession.IsValid())
+            req.mSession.Ping();
+        if (req.query["API_KEY"]) {
+            Security.TryLoginApiKey(req.query["API_KEY"].toString()).then(user => {
+                req.mSession.Login(user);
+            })
+                .catch(e => {
+                    let err: any = new Error("Access denied");
+                    err['status'] = 403;
+                    next(err);
+                    return;
+                });
+        }
+        next();
     });
-    req.mSession = SessionManager.GetSession(req);
-    if (req.mSession.IsValid())
-        req.mSession.Ping();
-    if (req.query["API_KEY"])
-        if (!Security.TryLoginApiKey(req.query["API_KEY"].toString())) {
-            let err: any = new Error("Access denied");
-            err['status'] = 403;
-            next(err);
+
+    let HasAccount = await UserConnectorFactory.GetConnector().HasUsers();
+
+    // Anonymous pages
+    !HasAccount && app.use(async (req, res, next) => {
+        HasAccount = HasAccount || await UserConnectorFactory.GetConnector().HasUsers();
+        if (HasAccount) {
+            next();
+            return;
+        }
+        let connector: IUserConnector = UserConnectorFactory.GetConnector();
+        if (!connector.CanCreateAccount()) {
+            res.end("No account defined, and cannot create account from Connector Backend");
             return;
 		}
-    next();
-});
-
-// Annonymous pages
-app.use('/', route_index);
-app.use('/login', route_login);
-
-// Login check
-app.use((req, res, next) => {
-    if (!req.mSession.IsValid()) {
-        const URL = '/login?redirect=' + encodeURIComponent(req.url);
-        res.status(302);
-        res.setHeader("Location", URL);
-        res.send("<!DOCTYPE html><html><body><a href='" + URL + "'>" + URL + "</a><script>document.location.href='" + URL + "';</script></html>");
-    } else {
-        next();
-	}
-});
-// Any following routes need login
-app.use('/logged', route_index);
-
-// catch 404 and forward to error handler
-app.use((_, __, next) => {
-    const err: any = new Error('Not Found');
-    err['status'] = 404;
-    next(err);
-});
-// error handlers
-
-// development error handler
-// will print stacktrace
-if (app.get('env') === 'development') {
-    app.use((err: any, req: Express.Request, res: any, next: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
-        res.status(err['status'] || 500);
+        if (req.method.toUpperCase() === "POST" && (req.body["username"] || "").trim().length && (req.body["password"] || "").trim().length) {
+            let user = await connector.CreateAccount(req.body["username"].trim(), req.body["password"].trim());
+            req.mSession.Login(user);
+            HasAccount = true;
+            SessionManager.Write(res, req.mSession);
+            res.redirect(302, "/");
+            return;
+        }
+        if (req.method.toUpperCase() === "GET") {
+            res.render('setup_user');
+		}
+    });
+    app.use('/', route_index);
+    app.use('/login', route_login);
+
+    // Login check
+    app.use((req, res, next) => {
+        if (!req.mSession.IsValid())
+            res.redirect(302, '/login?redirect=' + encodeURIComponent(req.url));
+        else
+            next();
+    });
+    // Any following routes need login
+    app.use('/calendar', route_calendar);
+    app.use('/trombinoscope', route_trombinoscope);
+    app.use('/accounting', route_accounting);
+
+    // catch 404 and forward to error handler
+    app.use((_, __, next) => {
+        const err: any = new Error('Not Found');
+        err['status'] = 404;
+        next(err);
+    });
+    // error handlers
+
+    // development error handler
+    // will print stacktrace
+    if (app.get('env') === 'development') {
+        app.use((err: any, req: Express.Request, res: any, next: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
+            err && console.error(err);
+            res.status(err['status'] || 500);
+            res.render('error', {
+                message: err.message,
+                error: err
+            });
+        });
+    }
+
+    // production error handler
+    // no stacktraces leaked to user
+    app.use((err: any, _: any, res: any, __: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
+        err && console.error(err);
+        res.status(err.status || 500);
         res.render('error', {
             message: err.message,
-            error: err
+            error: {}
         });
     });
-}
-
-// production error handler
-// no stacktraces leaked to user
-app.use((err: any, _: any, res: any, __: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
-    res.status(err.status || 500);
-    res.render('error', {
-        message: err.message,
-        error: {}
-    });
-});
 
-app.set('port', process.env.port || 1337);
+    app.set('port', process.env.port || 1337);
 
-const server = app.listen(app.get('port'), function () {
-    debug(`Express server listening on port ${(server.address() as AddressInfo).port}`);
-});
+    const server = app.listen(app.get('port'), function () {
+        debug(`Express server listening on port ${(server.address() as AddressInfo).port}`);
+    });
+})();

+ 7 - 1
ACPSBooking/serverConfig.js.sample

@@ -1,3 +1,9 @@
 module.exports = {
-	"UserConnector": "YesMan"
+	/* required, connector to use for user management. Can be one of yesman, noman, sha1db */
+	"UserConnector": "YesMan",
+
+	/* required, connector to use for database. Can be one of sqlite, mysql */
+	"DatabaseConnector": "SQLite",
+	/* Optional, :memory:, relative or absolute path to a sqlite db file (sqlite only). Default is ./db.sqlite */
+	"SQLite_FilePath": "./db.sqlite"
 }

+ 3 - 3
ACPSBooking/src/AConfigChecker.ts

@@ -1,15 +1,15 @@
 
 export abstract class AConfigChecker {
-	abstract CheckConfig(): string | null;
+	abstract CheckConfig(): Promise<void> | Error | null;
 
-	public ConfigCheckValues(val: string, canBe: string[], fieldName: string, opt: boolean = false): string | null {
+	public ConfigCheckValues(val: string, canBe: string[], fieldName: string, opt: boolean = false): Error | null {
 		if (!val && opt)
 			return null;
 		if (!val || canBe.indexOf(val) < 0) {
 			let err = "Invalid value `" + val + "' for field `" + fieldName + "`.\n" +
 				"Values can be: \n";
 			canBe.forEach(i => err += `\t${i}\n`);
-			return err;
+			return new Error(err);
 		}
 		return null;
 	}

+ 77 - 0
ACPSBooking/src/DbConnector/AbstractDBConnector.ts

@@ -0,0 +1,77 @@
+import { AConfigChecker } from "../AConfigChecker";
+import { QueryBuilder, IEntity } from "./Query";
+
+export abstract class ADbConnector extends AConfigChecker {
+	protected abstract FetchFree(query: QueryBuilder, onObject: (obj: Map<string, string>) => void, done: (error: Error | null) => void): void;
+	public abstract InsertOne<T extends IEntity>(entity: T): Promise<T>;
+
+	public FetchAllWhile<T extends IEntity>(query: QueryBuilder, blueprint: new () => T, fnc: (obj: T) => void, done: (error: Error | null) => void) {
+		this.FetchFree(query
+			.SelectEntity(new blueprint()), i => {
+			let result = new blueprint();
+			result.FromDb(i);
+			fnc(result as T);
+		}, done);
+	}
+
+	public FetchAll<T extends IEntity>(query: QueryBuilder, blueprint: new() => T): Promise<T[]> {
+		return new Promise((ok, ko) => {
+			let resultArr: T[] = new Array();
+			this.FetchFree(query.SelectEntity(new blueprint()), i => {
+				let result = new blueprint();
+				result.FromDb(i);
+				resultArr.push(result as T)
+			}, err => {
+				if (err)
+					ko(err);
+				else
+					ok(resultArr);
+			});
+		});
+	}
+
+	public Count<T extends IEntity>(query: QueryBuilder, blueprint: new () => T): Promise<number> {
+		return new Promise((ok, ko) => {
+			let count = 0;
+			this.FetchAllWhile(query, blueprint, i => count++, (err: Error | null) => {
+				if (err)
+					ko(err);
+				else
+					ok(count);
+			});
+		});
+	}
+
+	public HasEntry<T extends IEntity>(blueprint: new () => T): Promise<boolean> {
+		return this.Count(new QueryBuilder().From(new blueprint().GetTableName()).Limit(1), blueprint).then(i => !!i);
+	}
+
+	public FetchOne<T extends IEntity>(query: QueryBuilder, blueprint: new () => T): Promise<T | null> {
+		return new Promise((ok, ko) => {
+			let count = 0;
+			this.FetchFree(query.Limit(1).SelectEntity(new blueprint()), i => {
+				if (count === 1) {
+					console.error("WTF query limit 1 returning more than 1 result");
+					console.trace();
+				} else if (!count) {
+					let result = new blueprint();
+					result.FromDb(i);
+					ok(result as T);
+				}
+				count++;
+			}, err => {
+				if (err)
+					ko(err);
+				if (!count)
+					ok(null);
+			});
+		});
+	}
+
+	protected async GetTables(): Promise<IEntity[]> {
+		return [
+			new (await import('../models/ApiKey')).default(),
+			new (await import('../models/User')).default(),
+		];
+	}
+}

+ 13 - 0
ACPSBooking/src/DbConnector/DBConnectorFactory.ts

@@ -0,0 +1,13 @@
+import { ADbConnector } from "./AbstractDBConnector";
+
+class DbConnectorFactory {
+	public async GetConnector(): Promise<ADbConnector> {
+		if (this.mConnector !== null)
+			return this.mConnector;
+		return this.mConnector = new (await import("./SequelizeConnector")).default();
+	}
+
+	private mConnector: ADbConnector | null = null;
+}
+
+export default new DbConnectorFactory();

+ 182 - 0
ACPSBooking/src/DbConnector/Query.ts

@@ -0,0 +1,182 @@
+import { assert } from "console";
+
+export enum FieldType {
+	eNumber,
+	eString,
+	eDate
+}
+
+export abstract class AField {
+	public constructor(et: string, name: string, type: FieldType, nullable: boolean = false, fieldSize: number | undefined = undefined) {
+		this.mName = name;
+		this.mType = type;
+		this.mFieldSize = fieldSize;
+		this.mTableName = et;
+		this.mNullable = nullable;
+	}
+
+	abstract GetDefault(): any;
+
+	public readonly mTableName: string;
+	public readonly mName: string;
+	public readonly mType: FieldType;
+	public readonly mFieldSize: number | undefined;
+	public readonly mNullable: boolean;
+}
+
+export class Field<T> extends AField {
+	public constructor(tableName: string, name: string, type: FieldType, defaultValue: T, nullable: boolean = false, fieldSize: number | undefined = undefined) {
+		super(tableName, name, type, nullable, fieldSize);
+		this.mDefault = defaultValue;
+	}
+
+	public GetDefault() { return this.mDefault; }
+	public static readonly DATE_NOW: number = -1000;
+
+	public readonly mDefault: T;
+}
+
+export class Constraints {
+	public pushUniqueFields(fields: AField[]) {
+		this.mUniqueKeys.push(fields);
+	}
+	public pushUniqueField(field: AField) {
+		this.mUniqueKeys.push([field]);
+	}
+	public mPrimaryKeys: AField[] | null = null;
+	public mIncrements: AField[] = new Array();
+	public mUniqueKeys: AField[][] = new Array();
+}
+
+export interface IEntity {
+	FromDb(dbObj: Map<string, string>): IEntity;
+	ToDb(): Map<string, string>;
+	GetTableName(): string;
+	GetTableFields(): AField[];
+	GetTableConstraints(): Constraints;
+}
+
+export enum ClauseOperator {
+	eAnd,
+	eOr,
+	eNot,
+	eEqual,
+	eNotEqual,
+	eNull,
+	eNotNull,
+	eLowerThan,
+	eLowerEqThan,
+	eGreaterThan,
+	eGreaterEqThan
+}
+
+export class Clause {
+	private constructor(oper: ClauseOperator, firstLeg: String | number | Date | AField | Clause | Clause[], secondLeg: String | number | Date | AField | Clause | undefined = undefined) {
+		this.mOperator = oper;
+		this.mFirstLeg = firstLeg;
+		this.mSecondLeg = secondLeg;
+	}
+
+	public static Or(...legs: Clause[]): Clause { return new Clause(ClauseOperator.eOr, legs); }
+	public static And(...legs: Clause[]): Clause { return new Clause(ClauseOperator.eAnd, legs); }
+	public static Not(firstLeg: Clause): Clause { return new Clause(ClauseOperator.eNot, firstLeg); }
+	public static Equal(firstLeg: String | number | Date | AField, secondLeg: String | number | Date | AField): Clause { return new Clause(ClauseOperator.eEqual, firstLeg, secondLeg); }
+
+	public readonly mOperator: ClauseOperator;
+	public readonly mFirstLeg: String | number | Date | AField | Clause | Clause[];
+	public readonly mSecondLeg: String | number | Date | AField | Clause | undefined;
+}
+
+export enum OrderDirection {
+	eAcs,
+	eDesc,
+	eNullsFirst
+}
+
+export class Order {
+	private constructor(field: AField, dir: OrderDirection) {
+		this.mField = field;
+		this.mDirection = dir;
+	}
+
+	public static Ascendant(field: AField): Order {
+		return new Order(field, OrderDirection.eAcs);
+	}
+
+	public static Descendant(field: AField): Order {
+		return new Order(field, OrderDirection.eDesc);
+	}
+
+	public static NullsFirst(field: AField): Order {
+		return new Order(field, OrderDirection.eNullsFirst);
+	}
+
+	public readonly mField: AField;
+	public readonly mDirection: OrderDirection;
+}
+
+export class QueryBuilder {
+	public Limit(count: number, from: number | undefined = undefined): QueryBuilder {
+		this.mCount = count;
+		this.mFrom = from;
+		return this;
+	}
+
+	public Where(wh: Clause): QueryBuilder {
+		assert(!this.mWhere);
+		this.mWhere = wh;
+		return this;
+	}
+
+	public Select(fields: string[]): QueryBuilder {
+		for (let i of fields) this.mSelect.push(i);
+		return this;
+	}
+
+	public From(tableName: string): QueryBuilder {
+		this.mTableName = tableName;
+		return this;
+	}
+
+	public SelectEntity(entity: IEntity): QueryBuilder {
+		entity.GetTableFields().forEach(i => this.mSelect.push(i.mName));
+		if (!this.mTableName)
+			this.mTableName = entity.GetTableName();
+		return this;
+	}
+
+	public GetSelect(): string[] {
+		return this.mSelect;
+	}
+
+	public GetTableName(): string {
+		if (!this.mTableName)
+			throw new Error("No table name supplied");
+		return this.mTableName;
+	}
+
+	public GetClause(): Clause | null {
+		return this.mWhere;
+	}
+
+	public HasClause(): boolean {
+		return !!this.mWhere;
+	}
+
+	public OrderBy(dir: Order): QueryBuilder {
+		this.mOrder.push(dir);
+		return this;
+	}
+
+	public GetOrderBy(): Order[] { return this.mOrder; }
+
+	public GetLimit(): number | undefined { return this.mCount; }
+	public GetOffset(): number | undefined { return this.mFrom; }
+
+	private mOrder: Order[] = new Array();
+	private mCount: number | undefined;
+	private mFrom: number | undefined;
+	private mWhere: Clause | null = null;
+	private mSelect: string[] = new Array();
+	private mTableName: string | null = null;
+}

+ 210 - 0
ACPSBooking/src/DbConnector/SequelizeConnector.ts

@@ -0,0 +1,210 @@
+import { QueryBuilder, AField, Clause, ClauseOperator, OrderDirection, IEntity, FieldType, Field } from "./Query";
+import { ADbConnector } from "./AbstractDBConnector";
+import UserModel from "../models/User";
+const { Op, Sequelize, DataTypes } = require('sequelize');
+const CONFIG = require('../../serverConfig.js');
+
+export default class SequelizeConnector extends ADbConnector {
+    protected FetchFree(query: QueryBuilder, onObject: (obj: Map<string, string>) => void, done: (error: Error | null) => void): void {
+        this.Open().then(async () => {
+            const model = this.mModels.get(query.GetTableName());
+            if (!model) {
+                done(new Error("Unknown table " + query.GetTableName()));
+                return;
+            }
+            let order: any = undefined;
+            for (let o of query.GetOrderBy()) {
+                let dir: string;
+                switch (o.mDirection) {
+                    case OrderDirection.eDesc:
+                        dir = "DESC";
+                        break;
+                    case OrderDirection.eNullsFirst:
+                        dir = "NULLS FIRST";
+                        break;
+                    default:
+                        dir = "ASC";
+                        break;
+				}
+                order.push([o.mField.mName, dir]);
+            }
+            let result = await model.findAll({
+                where: this.BuildWhereCondition(query.GetClause()),
+                raw: true,
+                attributes: query.GetSelect(),
+                limit: query.GetLimit(),
+                offset: query.GetOffset(),
+                order: order
+            });
+            result.forEach((i: any) => {
+                let iMap = new Map<string, string>();
+                for (let key in i)
+                    iMap.set(key, i[key]);
+                onObject(iMap)
+            });
+            done(null);
+        }).catch(err => done(err));
+    }
+
+    public async InsertOne<T extends IEntity>(entity: T): Promise<T> {
+        await this.Open();
+        let model = this.mModels.get(entity.GetTableName());
+        if (!model)
+            return Promise.reject(new Error("Unknown table " + entity.GetTableName()));
+        let obj: any = {};
+        for (let [i, j] of entity.ToDb())
+            obj[i] = j;
+        return model.create(obj).then((user: any) => new UserModel().FromDb(user));
+	}
+
+    public CheckConfig(): Promise<void>|Error|null {
+        if (!this.CheckPath())
+            return new Error("Database: Cannot open db");
+        return null;
+    }
+
+    public Count<T extends IEntity>(query: QueryBuilder, blueprint: new () => T): Promise<number> {
+        return this.Open().then(_ => {
+            const model = this.mModels.get(query.GetTableName());
+            if (!model)
+                return Promise.reject(new Error("Unknown table " + query.GetTableName()));
+            return model.count({
+                where: this.BuildWhereCondition(query.GetClause()),
+                limit: query.GetLimit(),
+                offset: query.GetOffset()
+            });
+        });
+    }
+
+    public CheckPath(): boolean {
+        if (this.IsOpen())
+            return true;
+        return !!(CONFIG["Database"]);
+    }
+
+    private BuildWhereCondition(clause: Clause|null): any {
+        if (!clause)
+            return undefined;
+        switch (clause.mOperator) {
+            case ClauseOperator.eNot:
+                return {
+                    [Op.not]: [this.BuildWhereCondition(clause.mFirstLeg as Clause)]
+                };
+            case ClauseOperator.eAnd:
+                return {
+                    [Op.and]: (clause.mFirstLeg as Clause[] || []).map(i => this.BuildWhereCondition(i))
+                };
+            case ClauseOperator.eOr:
+                return {
+                    [Op.or]: (clause.mFirstLeg as Clause[] || []).map(i => this.BuildWhereCondition(i))
+                };
+            case ClauseOperator.eGreaterEqThan:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.gte]: clause.mSecondLeg as number
+                };
+                return cond;
+            case ClauseOperator.eGreaterThan:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.gt]: clause.mSecondLeg as number
+                };
+                return cond;
+            case ClauseOperator.eLowerEqThan:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.lte]: clause.mSecondLeg as number
+                };
+                return cond;
+            case ClauseOperator.eLowerThan:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.lt]: clause.mSecondLeg as number
+                };
+                return cond;
+            case ClauseOperator.eEqual:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.eq]: clause.mSecondLeg
+                };
+                return cond;
+            case ClauseOperator.eNotEqual:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.neq]: clause.mSecondLeg
+                };
+                return cond;
+            case ClauseOperator.eNull:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.is]: null
+                };
+                return cond;
+            case ClauseOperator.eNotNull:
+                var cond: any = {};
+                cond[(clause.mFirstLeg as AField)?.mName || ""] = {
+                    [Op.not]: null
+                };
+                return cond;
+        }
+        return;
+	}
+
+    private PrepareTable(table: IEntity): void {
+        const tableName = table.GetTableName();
+        let def: any = {};
+
+        for (let i of table.GetTableFields()) {
+            let type;
+            switch (i.mType) {
+                case FieldType.eNumber:
+                    type = DataTypes.INTEGER;
+                    break;
+                case FieldType.eString:
+                    type = DataTypes.STRING;
+                    break;
+                case FieldType.eDate:
+                    type = DataTypes.DATE;
+                    break;
+            }
+            let defaultValue = i.GetDefault();
+            if (defaultValue === Field.DATE_NOW && i.mType === FieldType.eDate)
+                defaultValue = DataTypes.NOW;
+            def[i.mName] = {
+                type: type,
+                allowNull: i.mNullable,
+                defaultValue: defaultValue
+            };
+        }
+        for (let i of table.GetTableConstraints().mPrimaryKeys || [])
+            def[i.mName].primaryKey = true;
+        for (let i of table.GetTableConstraints().mIncrements || [])
+            def[i.mName].autoIncrement = true;
+        table.GetTableConstraints().mUniqueKeys.forEach((val, i) => {
+            const keyId = `uniq_${tableName}_${i}"`;
+            for (let f of val)
+                def[f.mName].unique = keyId;
+        });
+        this.mModels.set(tableName, this.mConnector.define(tableName, def, {
+            modelName: tableName,
+            freezeTableName: true
+         }));
+    }
+
+    private async Open(): Promise<void> {
+        if (this.IsOpen())
+            return;
+        this.mConnector = new Sequelize(CONFIG["Database"]);
+        let tables = await this.GetTables();
+        for (let table of tables)
+            this.PrepareTable(table);
+        await this.mConnector.sync({ alter: true });
+    }
+
+    public IsOpen(): boolean {
+        return !!this.mConnector;
+    }
+
+    private mModels: Map<string, any> = new Map();
+    private mConnector: typeof Sequelize | null = null;
+}

+ 22 - 5
ACPSBooking/src/Security.ts

@@ -1,16 +1,33 @@
+import { QueryBuilder, Clause } from './DbConnector/Query';
+import DBConnectorFactory from './DbConnector/DBConnectorFactory';
+import ApiKeyModel from './models/ApiKey';
+import UserModel from './models/User';
 import { Session } from './Session';
 import UserConnectorManager from './UserConnector/UserConnectorFactory';
 
 export default new class {
-	public TryLoginApiKey(apiKey: string): boolean {
-		// FIXME
-		return false;
+	public async TryLoginApiKey(apiKey: string): Promise<UserModel> {
+		let db = await DBConnectorFactory.GetConnector();
+		return new Promise((ok, ko) => {
+			db.FetchOne(new QueryBuilder().Where(Clause.Equal(ApiKeyModel.KEY, apiKey)), ApiKeyModel)
+				.then(key => {
+					return key ?
+						db.FetchOne(new QueryBuilder().Where(Clause.Equal(UserModel.USERID, key.GetUserId())), UserModel) :
+						Promise.reject("No such key");
+				})
+				.then(user => {
+					if (user)
+						ok(user);
+					else
+						return Promise.reject("No such User");
+				}).catch(ko);
+		});
 	}
 
 	public TryLogin(session: Session, username: string, password: string): Promise<void> {
 		return new Promise((ok, ko) => {
-			UserConnectorManager.GetConnector().Login(username, password).then(_ => {
-				session.Login(username);
+			UserConnectorManager.GetConnector().Login(username, password).then(user => {
+				session.Login(user);
 				ok();
 			}).catch(err => {
 				ko(err);

+ 15 - 7
ACPSBooking/src/Session.ts

@@ -1,14 +1,15 @@
 import { Request, Response } from 'express';
+import UserModel from './models/User';
 const crypto = require('crypto');
 
 const START_DATE_TS = (new Date()).getTime();
 const COOKIE_KEY = "sessId";
-const SESSION_TIME = 36000; // 10 min, FIXME config
+const SESSION_TIME = 36000000; // 10 min, FIXME config
 
 export class Session {
 	constructor(req: Request) {
 		this.mExpireTs = 0;
-		this.mUsername = null;
+		this.mUser = null;
 		this.mHash = crypto.createHash('sha1').update(
 			(new Date()).toISOString()
 			+ "__salt__"
@@ -23,15 +24,22 @@ export class Session {
 
 	public IsValid(): boolean {
 		return ((new Date()).getTime() < this.mExpireTs) &&
-			this.mUsername !== null;
+			this.mUser !== null;
+	}
+
+	public GetUser(): UserModel {
+		if (this.mUser)
+			return this.mUser;
+		throw new Error("Nullptr exception");
 	}
 
 	public GetUsername(): string {
-		return this.mUsername || "";
+		return this.mUser?.GetUsername() || "";
 	}
 
-	public Login(username: string) {
-		this.mUsername = username;
+	public Login(user: UserModel) {
+		this.mUser = user;
+		this.Ping();
 	}
 
 	public GetHash(): string {
@@ -43,7 +51,7 @@ export class Session {
 	}
 
 	private mExpireTs: number;
-	private mUsername: string | null;
+	private mUser: UserModel | null;
 	private mHash: string;
 }
 

+ 28 - 0
ACPSBooking/src/UserConnector/Sha1Db.ts

@@ -0,0 +1,28 @@
+import { QueryBuilder, Clause } from "../DbConnector/Query";
+import DBConnectorFactory from "../DbConnector/DBConnectorFactory";
+import UserModel from "../models/User";
+import { IUserConnector } from "./UserConnectorFactory";
+const crypto = require('crypto');
+
+export default class Sha1DbConnector implements IUserConnector {
+	private Hash(pass: string): string {
+		return crypto.createHash('sha256').update(
+			"__salt__"
+			+ pass
+		).digest('hex');
+	}
+
+	public async Login(username: string, password: string): Promise<UserModel> {
+		return (await DBConnectorFactory.GetConnector()).FetchOne(new QueryBuilder()
+			.Where(Clause.And(
+				Clause.Equal(UserModel.USERNAME, username),
+				Clause.Equal(UserModel.AUTHATTR, this.Hash(password))
+			)), UserModel)
+			.then(userEntity => userEntity ? Promise.resolve(userEntity) : Promise.reject("Denied"));
+	}
+	public async HasUsers() { return await (await DBConnectorFactory.GetConnector()).HasEntry(UserModel); }
+	public CanCreateAccount(): boolean { return true; }
+	public async CreateAccount(username: string, password: string): Promise<UserModel> {
+		return (await DBConnectorFactory.GetConnector()).InsertOne(new UserModel().SetUsername(username).SetAuth(this.Hash(password)));
+	}
+}

+ 0 - 39
ACPSBooking/src/UserConnector/UserConnectorFactory.js

@@ -1,39 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-const AConfigChecker_1 = require("../AConfigChecker");
-const CONFIG = require('../../serverConfig.js');
-class UserConnectorFactory extends AConfigChecker_1.AConfigChecker {
-    constructor() {
-        super(...arguments);
-        this.mConnector = null;
-    }
-    CheckConfig() {
-        var _a;
-        return this.ConfigCheckValues((_a = CONFIG["UserConnector"]) === null || _a === void 0 ? void 0 : _a.toLowerCase(), [
-            "yesman",
-            "noman"
-        ], "UserConnector", false);
-    }
-    GetConnector() {
-        if (this.mConnector !== null)
-            return this.mConnector;
-        switch ((CONFIG["UserConnector"] || "").toLowerCase()) {
-            case "yesman":
-                return this.mConnector = new class {
-                    Login(_, __) {
-                        return Promise.resolve();
-                    }
-                }();
-            case "noman":
-                return this.mConnector = new class {
-                    Login(_, __) {
-                        return Promise.reject("Denied");
-                    }
-                }();
-            default:
-                throw "Not Implemented Exception";
-        }
-    }
-}
-exports.default = new UserConnectorFactory();
-//# sourceMappingURL=UserConnectorFactory.js.map

+ 29 - 8
ACPSBooking/src/UserConnector/UserConnectorFactory.ts

@@ -1,15 +1,23 @@
 import { AConfigChecker } from "../AConfigChecker";
+import { QueryBuilder, Clause } from "../DbConnector/Query";
+import DBConnectorFactory from "../DbConnector/DBConnectorFactory";
+import UserModel from "../models/User";
+import Sha1DbConnector from "./Sha1Db";
 const CONFIG:any = require('../../serverConfig.js');
 
 export interface IUserConnector {
-	Login(username: string, password: string): Promise<void>;
+	Login(username: string, password: string): Promise<UserModel>;
+	CanCreateAccount(): boolean;
+	CreateAccount(username: string, password: string): Promise<UserModel>;
+	HasUsers(): Promise<boolean>;
 }
 
 class UserConnectorFactory extends AConfigChecker {
-	public CheckConfig(): string | null {
+	public CheckConfig(): Promise<void> | Error | null {
 		return this.ConfigCheckValues(CONFIG["UserConnector"]?.toLowerCase(), [
 			"yesman",
-			"noman"
+			"noman",
+			"sha1db"
 		], "UserConnector", false);
     }
 
@@ -20,19 +28,32 @@ class UserConnectorFactory extends AConfigChecker {
 		switch ((CONFIG["UserConnector"] || "").toLowerCase()) {
 			case "yesman":
 				return this.mConnector = new class implements IUserConnector {
-					Login(username: string, __: string): Promise<void> {
-						// FIXME check that username exists
-						return Promise.resolve();
+					public async Login(username: string, __: string): Promise<UserModel> {
+						return (await DBConnectorFactory.GetConnector()).FetchOne(new QueryBuilder().Where(Clause.Equal(UserModel.USERNAME, username)), UserModel)
+							.then(userEntity => userEntity ? Promise.resolve(userEntity) : Promise.reject("Denied"));
+					}
+					public async HasUsers() { return await (await DBConnectorFactory.GetConnector()).HasEntry(UserModel); }
+					public CanCreateAccount() { return true; }
+					public async CreateAccount(username: string, password: string): Promise<UserModel> {
+						return (await DBConnectorFactory.GetConnector()).InsertOne(new UserModel().SetUsername(username));
 					}
 				}();
+
 			case "noman":
 				return this.mConnector = new class implements IUserConnector {
-					Login(_: string, __: string): Promise<void> {
+					public Login(_: string, __: string): Promise<UserModel> {
 						return Promise.reject("Denied");
 					}
+					public async HasUsers() { return true; }
+					public CanCreateAccount() { return false; }
+					public async CreateAccount(username: string, password: string): Promise<UserModel> { throw new Error("Not Implemented"); }
 				}();
+
+			case "sha1db":
+				return this.mConnector = new Sha1DbConnector();
+
 			default:
-				throw "Not Implemented Exception";
+				throw new Error("Not Implemented Exception");
 		}	}
 
 	private mConnector: IUserConnector | null = null;

+ 45 - 0
ACPSBooking/src/models/ApiKey.ts

@@ -0,0 +1,45 @@
+import { Constraints, Field, FieldType, IEntity } from "../DbConnector/Query";
+const TABLE_NAME = "apiKey";
+
+export default class ApiKeyModel implements IEntity {
+    public FromDb(dbObj: Map<string, string>): ApiKeyModel {
+        this.mUserId = parseInt(dbObj.get(ApiKeyModel.USERID.mName) || "0", 10) || ApiKeyModel.USERID.mDefault;
+        this.mKey = dbObj.get(ApiKeyModel.KEY.mName) || ApiKeyModel.KEY.mDefault;
+        this.mComment = dbObj.get(ApiKeyModel.COMMENT.mName) || ApiKeyModel.COMMENT.mDefault;
+        return this;
+    }
+
+    public ToDb(): Map<string, string> {
+        let result = new Map();
+        result.set(ApiKeyModel.USERID.mName, this.mUserId);
+        result.set(ApiKeyModel.KEY.mName, this.mKey);
+        result.set(ApiKeyModel.COMMENT.mName, this.mComment);
+        return result;
+    }
+
+    public GetUserId(): number {
+        return this.mUserId;
+    }
+
+    public GetTableName() {
+        return TABLE_NAME;
+    }
+
+    public GetTableFields() {
+        return [ApiKeyModel.USERID, ApiKeyModel.KEY, ApiKeyModel.COMMENT];
+    }
+
+    public GetTableConstraints() {
+        let result = new Constraints();
+        result.mPrimaryKeys = [ApiKeyModel.KEY];
+        result.pushUniqueFields([ApiKeyModel.USERID, ApiKeyModel.COMMENT]);
+        return result;
+	}
+
+    private mUserId: number =0;
+    private mKey: string = "";
+    private mComment: string = "";
+    public static readonly USERID: Field<number> = new Field<number>(TABLE_NAME, "userid", FieldType.eNumber, 0, false, 16);
+    public static readonly KEY: Field<string> = new Field<string>(TABLE_NAME, "key", FieldType.eString, "", false, 256);
+    public static readonly COMMENT: Field<string> = new Field<string>(TABLE_NAME, "comment", FieldType.eString, "", false, 256);
+}

+ 60 - 0
ACPSBooking/src/models/User.ts

@@ -0,0 +1,60 @@
+import { Constraints, Field, FieldType, IEntity } from "../DbConnector/Query";
+const TABLE_NAME = "users";
+
+export default class UserModel implements IEntity {
+    public FromDb(dbObj: Map<string, string>): UserModel {
+        this.mUsername = dbObj.get(UserModel.USERNAME.mName) || UserModel.USERNAME.mDefault;
+        this.mUserId = parseInt(dbObj.get(UserModel.USERID.mName) || "0", 10) || UserModel.USERID.mDefault;
+        this.mAuthAttr = dbObj.get(UserModel.AUTHATTR.mName) || UserModel.AUTHATTR.mDefault;
+        return this;
+    }
+
+    public ToDb(): Map<string, string> {
+        let result = new Map();
+        result.set(UserModel.USERNAME.mName, this.mUsername);
+        result.set(UserModel.USERID.mName, this.mUserId || undefined);
+        result.set(UserModel.AUTHATTR.mName, this.mAuthAttr);
+        return result;
+	}
+
+    public GetTableName() {
+        return TABLE_NAME;
+    }
+
+    public static GetTableName() {
+        return TABLE_NAME;
+    }
+
+    public GetTableFields() {
+        return [UserModel.USERID, UserModel.USERNAME, UserModel.AUTHATTR];
+	}
+
+    public GetUsername(): string {
+        return this.mUsername;
+    }
+
+    public SetUsername(username: string): UserModel {
+        this.mUsername = username;
+        return this;
+    }
+
+    public SetAuth(auth: string): UserModel {
+        this.mAuthAttr = auth;
+        return this;
+	}
+
+    public GetTableConstraints(): Constraints {
+        let result = new Constraints();
+        result.mPrimaryKeys = [UserModel.USERID];
+        result.pushUniqueField(UserModel.USERNAME);
+        result.mIncrements.push(UserModel.USERID);
+        return result;
+	}
+
+    private mUserId: number = 0;
+    private mUsername: string = "";
+    private mAuthAttr: string = "";
+    public static readonly USERID: Field<number> = new Field<number>(TABLE_NAME, "userId", FieldType.eNumber, 0, false, 16);
+    public static readonly USERNAME: Field<string> = new Field<string>(TABLE_NAME, "username", FieldType.eString, "", false, 16);
+    public static readonly AUTHATTR: Field<string> = new Field<string>(TABLE_NAME, "auth", FieldType.eString, "", false, 256);
+}

+ 4 - 0
ACPSBooking/views/dashboard.pug

@@ -0,0 +1,4 @@
+extends template/loggedpage
+
+block append content
+  p=t("hello", { username: username })

+ 2 - 2
ACPSBooking/views/index.pug

@@ -1,5 +1,5 @@
 extends template/page
 
 block content
-  p Hello World
-  p="Hello "+username
+  p=t("hello")
+  

+ 12 - 0
ACPSBooking/views/setup_user.pug

@@ -0,0 +1,12 @@
+extends template/page
+
+block content
+    div Create user account
+    form(method="post",action="#")
+      label
+        span username
+        input(type="text",name="username",required)
+      label
+        span password
+        input(type="password",name="password",required)
+      input(type="submit")

+ 7 - 0
ACPSBooking/views/template/loggedpage.pug

@@ -0,0 +1,7 @@
+extends page
+
+block content
+  menu
+    ul
+      li Dashboard
+      li menu2

+ 1 - 0
ACPSBooking/views/template/page.pug

@@ -4,5 +4,6 @@ html
     title
     link(rel='stylesheet', href='/style/bootstrap.min.css')
     script(src='/javascript/bootstrap.bundle.min.js')
+    block extraHead
   body
     block content

+ 30 - 1
package-lock.json

@@ -1,3 +1,32 @@
 {
-  "lockfileVersion": 1
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "@babel/runtime": {
+      "version": "7.14.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
+      "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
+      "requires": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
+    "i18next": {
+      "version": "20.3.2",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.3.2.tgz",
+      "integrity": "sha512-e8CML2R9Ng2sSQOM80wb/PrM2j8mDm84o/T4Amzn9ArVyNX5/ENWxxAXkRpZdTQNDaxKImF93Wep4mAoozFrKw==",
+      "requires": {
+        "@babel/runtime": "^7.12.0"
+      }
+    },
+    "i18next-http-middleware": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/i18next-http-middleware/-/i18next-http-middleware-3.1.4.tgz",
+      "integrity": "sha512-OVjxnw1w9RqhrOBSsXBXbk/7kzpLu+YvaL0LZnLHTCzjs8dMG8/BuoaDxi1l30T1lUfmweb+er/xwRHV3lp0RQ=="
+    },
+    "regenerator-runtime": {
+      "version": "0.13.7",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
+      "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
+    }
+  }
 }