isundil 4 年之前
父節點
當前提交
fecc821f38

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@ obj/
 *.map
 package-lock.json
 /ACPSBooking/routes/*.js
+/ACPSBooking/serverConfig.js
 /ACPSBooking/src/*.js
 /ACPSBooking/server.js
 /ACPSBooking/node_modules/

+ 23 - 0
ACPSBooking/ACPSBooking.njsproj

@@ -84,12 +84,18 @@
     <Content Include="public\style\bootstrap.rtl.css.map" />
     <Content Include="public\style\bootstrap.rtl.min.css" />
     <Content Include="public\style\bootstrap.rtl.min.css.map" />
+    <Content Include="serverConfig.js">
+      <SubType>Code</SubType>
+    </Content>
     <Content Include="views\error.pug">
       <SubType>Code</SubType>
     </Content>
     <Content Include="views\index.pug">
       <SubType>Code</SubType>
     </Content>
+    <Content Include="views\login.pug">
+      <SubType>Code</SubType>
+    </Content>
     <Content Include="views\template\page.pug" />
     <None Include="server.ts" />
     <Content Include="tsconfig.json" />
@@ -103,6 +109,8 @@
     <Folder Include="public\style\" />
     <Folder Include="routes\" />
     <Folder Include="src\" />
+    <Folder Include="src\DbConnector\" />
+    <Folder Include="src\UserConnector\" />
     <Folder Include="views\" />
     <Folder Include="views\template\" />
   </ItemGroup>
@@ -110,6 +118,21 @@
     <TypeScriptCompile Include="routes\index.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
+    <TypeScriptCompile Include="routes\login.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\AConfigChecker.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\UserConnectorFactory.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
   </ItemGroup>
   <Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
   <ProjectExtensions>

+ 22 - 20
ACPSBooking/package.json

@@ -1,22 +1,24 @@
 {
-  "name": "acpsbooking",
-  "version": "0.0.0",
-  "description": "ACPSBooking",
-  "main": "server.js",
-  "author": {
-    "name": ""
-  },
-  "scripts": {
-    "build": "tsc --build",
-    "clean": "tsc --build --clean",
-    "start": "node server"
-  },
-  "devDependencies": {
-    "@types/node": "^14.14.7",
-    "@types/express": "^4.0.37",
-    "typescript": "^4.0.5",
-    "body-parser": "^1.19.0",
-    "express": "^4.14.0",
-    "pug": "^3.0.2"
-  }
+    "name": "acpsbooking",
+    "version": "0.0.0",
+    "description": "ACPSBooking",
+    "main": "server.js",
+    "author": {
+        "name": ""
+    },
+    "scripts": {
+        "build": "tsc --build",
+        "clean": "tsc --build --clean",
+        "start": "node server"
+    },
+    "dependencies": {
+        "@types/node": "^14.14.7",
+        "@types/express": "^4.0.37",
+        "typescript": "^4.0.5",
+        "body-parser": "^1.19.0",
+        "express": "^4.14.0",
+        "pug": "^3.0.2"
+    },
+    "devDependencies": {
+    }
 }

+ 3 - 4
ACPSBooking/routes/index.ts

@@ -1,11 +1,10 @@
-/*
- * GET users listing.
- */
 import express = require('express');
 const router = express.Router();
 
 router.get('/', (req: express.Request, res: express.Response) => {
-    res.render('index', {});
+    res.render('index', {
+        username: req.mSession.GetUsername()
+    });
 });
 
 export default router;

+ 43 - 0
ACPSBooking/routes/login.ts

@@ -0,0 +1,43 @@
+import express = require('express');
+import Security from '../src/Security';
+import { SessionManager } from '../src/Session';
+const router = express.Router();
+
+function requestIsPost(req: express.Request): boolean {
+    return req.method.toUpperCase() === 'POST' &&
+        req.body["redirect"] &&
+        req.body["username"] &&
+        req.body["password"] !== undefined;
+}
+
+function extractRedirection(req: express.Request): string {
+    let redir: string | undefined = undefined;
+
+    if (requestIsPost(req))
+        redir = req.body["redirect"] || undefined;
+    if (!redir && req.query["redirect"]) {
+        if (Array.isArray(req.query["redirect"]))
+            redir = req.query["redirect"][0].toString() || undefined;
+        else
+            redir = req.query["redirect"]?.toString() || undefined;
+	}
+    return redir || "/";
+}
+
+router.all('/', (req: express.Request, res: express.Response) => {
+    let redir = extractRedirection(req);
+    (requestIsPost(req) ?
+        (Security.TryLogin(req.mSession, req.body["username"], req.body["password"]).then(_ => {
+            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
+        });
+    });
+});
+
+export default router;

+ 67 - 2
ACPSBooking/server.ts

@@ -1,12 +1,41 @@
+import { AConfigChecker } from './src/AConfigChecker';
 import * as express from 'express';
 import { AddressInfo } from "net";
 import * as path from 'path';
 import * as bodyParser from 'body-parser';
 import route_index from './routes/index';
+import route_login from './routes/login';
+import Security from './src/Security';
+import { Session, SessionManager } from './src/Session';
+import UserConnectorFactory from './src/UserConnector/UserConnectorFactory';
 
 const debug = require('debug')('my express app');
 const app = express();
 
+declare global {
+    namespace Express {
+        interface Request {
+            mSession: Session
+            mCookies: Map<string, string>
+		}
+	}
+}
+
+// Check config
+(_ => {
+    let cc: AConfigChecker[] = [
+        UserConnectorFactory
+    ];
+    for (let i of cc) {
+        let err = i.CheckConfig();
+        if (err) {
+            err = i.constructor.name + ": " + err;
+            console.error("Configuration error: " + err);
+            throw "Configuration error";
+        }
+    }
+})();
+
 // view engine setup
 app.set('views', [path.join(__dirname, 'views'), path.join(__dirname, 'views/template')]);
 app.set('view engine', 'pug');
@@ -15,10 +44,46 @@ app.use(express.static(path.join(__dirname, 'public')));
 app.use(bodyParser.json());
 app.use(bodyParser.urlencoded({ extended: true }));
 
+// 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());
+    });
+    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);
+            return;
+		}
+    next();
+});
+
+// Annonymous pages
 app.use('/', route_index);
+app.use('/login', route_login);
 
-// catch 404 and forward to error handler
+// 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);
@@ -39,7 +104,7 @@ if (app.get('env') === 'development') {
 
 // production error handler
 // no stacktraces leaked to user
-app.use((err: any, req: any, res: any, next: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
+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,

+ 3 - 0
ACPSBooking/serverConfig.js.sample

@@ -0,0 +1,3 @@
+module.exports = {
+	"UserConnector": "YesMan"
+}

+ 16 - 0
ACPSBooking/src/AConfigChecker.ts

@@ -0,0 +1,16 @@
+
+export abstract class AConfigChecker {
+	abstract CheckConfig(): string | null;
+
+	public ConfigCheckValues(val: string, canBe: string[], fieldName: string, opt: boolean = false): string | 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 null;
+	}
+}

+ 20 - 0
ACPSBooking/src/Security.ts

@@ -0,0 +1,20 @@
+import { Session } from './Session';
+import UserConnectorManager from './UserConnector/UserConnectorFactory';
+
+export default new class {
+	public TryLoginApiKey(apiKey: string): boolean {
+		// FIXME
+		return false;
+	}
+
+	public TryLogin(session: Session, username: string, password: string): Promise<void> {
+		return new Promise((ok, ko) => {
+			UserConnectorManager.GetConnector().Login(username, password).then(_ => {
+				session.Login(username);
+				ok();
+			}).catch(err => {
+				ko(err);
+			});
+		});
+	}
+}();

+ 62 - 0
ACPSBooking/src/Session.ts

@@ -0,0 +1,62 @@
+import { Request, Response } from 'express';
+const crypto = require('crypto');
+
+const START_DATE_TS = (new Date()).getTime();
+const COOKIE_KEY = "sessId";
+const SESSION_TIME = 36000; // 10 min, FIXME config
+
+export class Session {
+	constructor(req: Request) {
+		this.mExpireTs = 0;
+		this.mUsername = null;
+		this.mHash = crypto.createHash('sha1').update(
+			(new Date()).toISOString()
+			+ "__salt__"
+			+ req.connection.remoteAddress
+			+ ""
+			+ req.connection.remotePort
+			+ ""
+			+ START_DATE_TS
+		).digest('hex');
+		this.Ping();
+	}
+
+	public IsValid(): boolean {
+		return ((new Date()).getTime() < this.mExpireTs) &&
+			this.mUsername !== null;
+	}
+
+	public GetUsername(): string {
+		return this.mUsername || "";
+	}
+
+	public Login(username: string) {
+		this.mUsername = username;
+	}
+
+	public GetHash(): string {
+		return this.mHash;
+	}
+
+	public Ping(): void {
+		this.mExpireTs = (new Date()).getTime() + SESSION_TIME;
+	}
+
+	private mExpireTs: number;
+	private mUsername: string | null;
+	private mHash: string;
+}
+
+export var SessionManager = new class {
+	public GetSession(req: Request): Session {
+		const sessionId: string | undefined = req.mCookies.get(COOKIE_KEY);
+		return (sessionId ? this.mStoredSessions.get(sessionId) : undefined) || new Session(req);
+	}
+
+	public Write(res: Response, sess: Session) {
+		res.cookie(COOKIE_KEY, sess.GetHash());
+		this.mStoredSessions.set(sess.GetHash(), sess);
+	}
+
+	private mStoredSessions: Map<string, Session> = new Map();
+}();

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

@@ -0,0 +1,39 @@
+"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

+ 41 - 0
ACPSBooking/src/UserConnector/UserConnectorFactory.ts

@@ -0,0 +1,41 @@
+import { AConfigChecker } from "../AConfigChecker";
+const CONFIG:any = require('../../serverConfig.js');
+
+export interface IUserConnector {
+	Login(username: string, password: string): Promise<void>;
+}
+
+class UserConnectorFactory extends AConfigChecker {
+	public CheckConfig(): string | null {
+		return this.ConfigCheckValues(CONFIG["UserConnector"]?.toLowerCase(), [
+			"yesman",
+			"noman"
+		], "UserConnector", false);
+    }
+
+	public GetConnector(): IUserConnector {
+		if (this.mConnector !== null)
+			return this.mConnector;
+
+		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();
+					}
+				}();
+			case "noman":
+				return this.mConnector = new class implements IUserConnector {
+					Login(_: string, __: string): Promise<void> {
+						return Promise.reject("Denied");
+					}
+				}();
+			default:
+				throw "Not Implemented Exception";
+		}	}
+
+	private mConnector: IUserConnector | null = null;
+}
+
+export default new UserConnectorFactory();

+ 11 - 0
ACPSBooking/src/UserConnector/yesMan.js

@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.YesMan = void 0;
+class YesMan {
+    Login(username, password) {
+        // FIXME check username exists...
+        return Promise.resolve();
+    }
+}
+exports.YesMan = YesMan;
+//# sourceMappingURL=yesMan.js.map

+ 1 - 1
ACPSBooking/tsconfig.json

@@ -7,6 +7,6 @@
     "strict": true
   },
   "exclude": [
-    "node_modules"
+      "node_modules"
   ]
 }

+ 1 - 0
ACPSBooking/views/index.pug

@@ -2,3 +2,4 @@ extends template/page
 
 block content
   p Hello World
+  p="Hello "+username

+ 11 - 0
ACPSBooking/views/login.pug

@@ -0,0 +1,11 @@
+extends template/page
+block content
+  form(method="POST")
+    label
+      span Username
+      input(type="text",value=username,name="username",required)
+    label
+      span Password
+      input(type="password",name="password")
+    input(type="hidden",name="redirect",value=redirect)
+    input(type="submit")