@entity-access/server-pages 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ export default class XNode {
2
+
3
+ public static create(
4
+ // eslint-disable-next-line @typescript-eslint/ban-types
5
+ name: string | Function,
6
+ attribs: Record<string, any>,
7
+ ... nodes: (XNode | string)[]) {
8
+ if (typeof name === "function") {
9
+ return name(attribs ?? {}, ... nodes);
10
+ }
11
+ return new XNode(name, attribs, nodes);
12
+ }
13
+
14
+ private constructor(
15
+ public readonly name: string,
16
+ public readonly attributes: Record<string, any>,
17
+ public readonly children: (XNode | string)[]
18
+ ) {
19
+
20
+ }
21
+
22
+ public render(nest = "") {
23
+ const { name, attributes } = this;
24
+ const children = [];
25
+ let a = "";
26
+ if (attributes) {
27
+ for (const key in attributes) {
28
+ if (Object.prototype.hasOwnProperty.call(attributes, key)) {
29
+ const element = attributes[key];
30
+ a += ` ${key}=${JSON.stringify(element)}`;
31
+ }
32
+ }
33
+ }
34
+ if (this.children) {
35
+ for (const child of this.children) {
36
+ if (typeof child === "string") {
37
+ children.push(child);
38
+ continue;
39
+ }
40
+ if (!child) {
41
+ continue;
42
+ }
43
+ children.push(child.render(nest + "\t"));
44
+ }
45
+ }
46
+ if (!name) {
47
+ return `\n${nest}\t${children.join("\n\t")}`;
48
+ }
49
+ return `${nest}<${name}${a}>\n${nest}\t${children.join("\n\t")}\n${nest}</${name}>`;
50
+ }
51
+
52
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type SessionUser from "./core/SessionUser.ts";
2
+
3
+ export {};
4
+
5
+ declare global {
6
+ namespace Express {
7
+ // eslint-disable-next-line @typescript-eslint/naming-convention
8
+ export interface Request {
9
+ /**
10
+ * user will always be set, even in case if it is an anonymous request.
11
+ * This is to track actual views.
12
+ */
13
+ user: SessionUser;
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,137 @@
1
+ import Inject, { RegisterSingleton, ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
2
+ import TokenService, { IAuthCookie } from "./TokenService.js";
3
+ import TimedCache from "@entity-access/entity-access/dist/common/cache/TimedCache.js";
4
+ import { BaseDriver } from "@entity-access/entity-access/dist/drivers/base/BaseDriver.js";
5
+ import { Request, Response } from "express";
6
+ import cluster from "cluster";
7
+ import SessionUser from "../core/SessionUser.js";
8
+ import UserSessionProvider from "./UserSessionProvider.js";
9
+
10
+ /**
11
+ * This will track userID,cookie pair so we can
12
+ * clear the logged in user's file permissions
13
+ */
14
+ const userCookies = new Map<number,string>();
15
+ const sessionCache = new TimedCache<string, Partial<SessionUser>>();
16
+
17
+ let cookieName = null;
18
+
19
+ const tagForCache = Symbol("tagForCache");
20
+
21
+ const cacheFR = new FinalizationRegistry<number>((heldValue) => {
22
+ userCookies.delete(heldValue);
23
+ });
24
+
25
+ @RegisterSingleton
26
+ export default class CookieService {
27
+
28
+ @Inject
29
+ private tokenService: TokenService;
30
+
31
+ @Inject
32
+ private userSessionProvider: UserSessionProvider;
33
+
34
+ constructor() {
35
+ process.on("message", (msg: any) => {
36
+ if (msg.type === "cookie-service-clear-cache") {
37
+ this.clearCache(msg.userID, false);
38
+ }
39
+ });
40
+ }
41
+
42
+ public clearCache(userID: number, broadcast = true) {
43
+ const cookie = userCookies.get(userID);
44
+ if (cookie) {
45
+ sessionCache.delete(cookie);
46
+ }
47
+ if (!broadcast) {
48
+ return;
49
+ }
50
+ const clearMessage = {
51
+ type: "cookie-service-clear-cache",
52
+ userID
53
+ };
54
+ if (cluster.isWorker) {
55
+ process.send(clearMessage);
56
+ } else {
57
+ if(cluster.workers) {
58
+ for (const key in cluster.workers) {
59
+ if (Object.prototype.hasOwnProperty.call(cluster.workers, key)) {
60
+ const element = cluster.workers[key];
61
+ element.send(clearMessage);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ async createSessionUser(req: Request, resp: Response) {
69
+ cookieName ??= this.tokenService.authCookieName;
70
+ const sessionCookie = req.cookies[cookieName];
71
+ req.user = await this.createSessionUserFromCookie(sessionCookie, req.ip, resp);
72
+ }
73
+
74
+ async createSessionUserFromCookie(cookie: string, ip: string, resp?: Response) {
75
+ const user = new SessionUser(resp, cookieName, this.tokenService);
76
+ try {
77
+ user.ipAddress = ip;
78
+ if (cookie) {
79
+ const userInfo = await this.getVerifiedUser(cookie);
80
+ if (userInfo?.sessionID) {
81
+ user.sessionID = userInfo.sessionID;
82
+ }
83
+ if (userInfo?.userID) {
84
+ user[tagForCache] = userInfo;
85
+ for (const key in userInfo) {
86
+ if (Object.prototype.hasOwnProperty.call(userInfo, key)) {
87
+ const element = userInfo[key];
88
+ user[key] = element;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return user;
94
+ } catch (error) {
95
+ console.error(error);
96
+ return user;
97
+ }
98
+ }
99
+
100
+ private getVerifiedUser(cookie: string): Promise<Partial<SessionUser>> {
101
+
102
+ return sessionCache.getOrCreateAsync(cookie, async (k) => {
103
+
104
+ const parsedCookie = JSON.parse(cookie) as IAuthCookie;
105
+ if (!parsedCookie.id) {
106
+ return {};
107
+ }
108
+
109
+ if(!await this.tokenService.verifyContent(parsedCookie, false)) {
110
+ return {};
111
+ }
112
+
113
+ if (!parsedCookie.active) {
114
+ return {
115
+ sessionID: parsedCookie.id
116
+ };
117
+ }
118
+
119
+ const r = await this.createUserInfo(parsedCookie.id, cookie);
120
+ return r;
121
+ });
122
+ }
123
+
124
+ private async createUserInfo(id: number, cookie: string) {
125
+ const r = await this.userSessionProvider.getUserSession(id);
126
+ if (r === null) {
127
+ return {};
128
+ }
129
+ if (r.expiry.getTime() < Date.now() || r.invalid) {
130
+ return {};
131
+ }
132
+ cacheFR.register(r, r.userID);
133
+ userCookies.set(r.userID, cookie);
134
+ return r;
135
+ }
136
+
137
+ }
@@ -0,0 +1,62 @@
1
+ import { RegisterSingleton } from "@entity-access/entity-access/dist/di/di.js";
2
+ import DateTime from "@entity-access/entity-access/dist/types/DateTime.js";
3
+ import { generateKeyPair } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ export interface IAuthKey {
8
+ publicKey: string,
9
+ privateKey: string,
10
+ expires: DateTime
11
+ }
12
+
13
+
14
+ @RegisterSingleton
15
+ export default class KeyProvider {
16
+
17
+ public keyPath: string = "/data/keys";
18
+
19
+ private key: IAuthKey;
20
+
21
+ public async getKeys() {
22
+ if (this.key) {
23
+ return [this.key];
24
+ }
25
+ // check if we have key stored
26
+ const keyPath = this.keyPath;
27
+ if (!existsSync(keyPath)) {
28
+ mkdirSync(keyPath, { recursive: true});
29
+ }
30
+ const file = join(keyPath, "cookie-key.json");
31
+ if (existsSync(file)) {
32
+ this.key = JSON.parse(readFileSync(file, "utf-8"));
33
+ return [this.key];
34
+ }
35
+ this.key = await this.generateKey(null);
36
+ writeFileSync(file, JSON.stringify(this.key, undefined, 2));
37
+ return [this.key];
38
+ }
39
+
40
+ private generateKey(expires: DateTime) {
41
+ return new Promise<IAuthKey>((resolve, reject) => {
42
+ generateKeyPair('rsa', {
43
+ modulusLength: 2048,
44
+ publicKeyEncoding: {
45
+ type: 'spki',
46
+ format: 'pem'
47
+ },
48
+ privateKeyEncoding: {
49
+ type: 'pkcs8',
50
+ format: 'pem'
51
+ }
52
+ },
53
+ (error, publicKey, privateKey) => {
54
+ resolve({
55
+ publicKey,
56
+ privateKey,
57
+ expires
58
+ });
59
+ });
60
+ });
61
+ }
62
+ }
@@ -0,0 +1,80 @@
1
+ import Inject, { RegisterSingleton } from "@entity-access/entity-access/dist/di/di.js";
2
+ import DateTime from "@entity-access/entity-access/dist/types/DateTime.js";
3
+ import { createSign, createVerify, generateKeyPair } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import KeyProvider from "./KeyProvider.js";
7
+
8
+ export interface IAuthCookie {
9
+ id: number;
10
+ expiry: Date;
11
+ sign: string;
12
+ version: string;
13
+ active?: boolean;
14
+ }
15
+
16
+ export interface IAuthKey {
17
+ publicKey: string,
18
+ privateKey: string,
19
+ expires: DateTime
20
+ }
21
+
22
+ export type ISignedContent<T> = T & {
23
+ sign: string;
24
+ };
25
+
26
+ @RegisterSingleton
27
+ export default class TokenService {
28
+
29
+ public authCookieName = "ea-c1";
30
+
31
+ public shareCookieName = "ea-ca1";
32
+
33
+ @Inject
34
+ private keyProvider: KeyProvider;
35
+
36
+ public async getAuthToken(authCookie: Omit<IAuthCookie, "sign">): Promise<{ cookieName: string, cookie: string}> {
37
+ const cookie = await this.signContent(authCookie);
38
+ return { cookieName: this.authCookieName, cookie: JSON.stringify(cookie) };
39
+ }
40
+
41
+ public async signContent<T>(content: T): Promise<ISignedContent<T>> {
42
+ const [key] = await this.keyProvider.getKeys();
43
+ const sign = this.sign(JSON.stringify(content), key);
44
+ return { ... content, sign};
45
+ }
46
+
47
+ public async verifyContent<T>(content: ISignedContent<T>, fail = true) {
48
+ const { sign , ... c } = content;
49
+ const keys = await this.keyProvider.getKeys();
50
+ for (const iterator of keys) {
51
+ if(this.verify(JSON.stringify(c), sign, iterator, false)) {
52
+ return true;
53
+ }
54
+ }
55
+ if (fail) {
56
+ throw new Error("Signature verification failed");
57
+ }
58
+ }
59
+
60
+ private sign(content: string, key: IAuthKey) {
61
+ const sign = createSign("SHA256");
62
+ sign.write(content);
63
+ sign.end();
64
+ return sign.sign(key.privateKey, "hex");
65
+ }
66
+
67
+ private verify(content: string | Buffer, signature: string, key: IAuthKey, fail = true) {
68
+ const verify = createVerify("SHA256");
69
+ verify.write(content);
70
+ verify.end();
71
+ if(verify.verify(key.publicKey, signature, "hex")) {
72
+ return true;
73
+ }
74
+ if (fail) {
75
+ throw new Error("Invalid signature");
76
+ }
77
+ }
78
+
79
+
80
+ }
@@ -0,0 +1,13 @@
1
+ import { RegisterSingleton } from "@entity-access/entity-access/dist/di/di.js";
2
+ import SessionUser from "../core/SessionUser.js";
3
+
4
+ @RegisterSingleton
5
+ export default class UserSessionProvider {
6
+
7
+ async getUserSession(id: number): Promise<Partial<SessionUser>> {
8
+ return {
9
+ userID: 1,
10
+ }
11
+ }
12
+
13
+ }
@@ -0,0 +1,6 @@
1
+ import Page from "../../../../Page.js";
2
+
3
+ export default function(this: Page) {
4
+ console.log(this.childPath);
5
+ return this.json({ childPath: this.childPath });
6
+ }
@@ -0,0 +1,20 @@
1
+ import Content from "../../Content.js";
2
+ import Page from "../../Page.js";
3
+ import HtmlDocument from "../../html/HtmlDocument.js";
4
+ import XNode from "../../html/XNode.js";
5
+
6
+ export default function(this: Page) {
7
+
8
+ const [child] = this.childPath;
9
+ if (child) {
10
+ if (!/^index\.htm/i.test(child)) {
11
+ return this.notFound();
12
+ }
13
+ }
14
+
15
+ return Content.html(<HtmlDocument>
16
+ <body>
17
+ Test 1
18
+ </body>
19
+ </HtmlDocument>);
20
+ }
package/test.js ADDED
@@ -0,0 +1,12 @@
1
+ import { ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
2
+ import ServerPages from "./dist/ServerPages.js";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+
7
+ const sp = ServerPages.create();
8
+ sp.registerRoutes(join(dirname( fileURLToPath(import.meta.url)), "./dist/tests/logger"));
9
+
10
+ const app = sp.build();
11
+
12
+ app.listen(8080);
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react",
4
+ "target": "ES2021",
5
+ "module":"NodeNext",
6
+ "incremental": true,
7
+ "sourceMap": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "outDir": "dist",
11
+ "skipDefaultLibCheck": true,
12
+ "skipLibCheck": true,
13
+ "experimentalDecorators": true,
14
+ "emitDecoratorMetadata": true,
15
+ "jsxFactory": "XNode.create",
16
+ "lib": [
17
+ "ES2018",
18
+ "ES2021.WeakRef",
19
+ "esnext.disposable",
20
+ "ES2021.String",
21
+ "ES2022.Object"
22
+ ]
23
+ },
24
+ "include": [
25
+ "src/**/*"
26
+ ],
27
+ "exclude": [
28
+ "node_modules",
29
+ "tests"
30
+ ]
31
+ }