@raketa-cloud/admin 0.0.93 → 0.0.95

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.
package/jest.config.js CHANGED
@@ -11,8 +11,8 @@ const config = {
11
11
  ],
12
12
  coverageThreshold: {
13
13
  global: {
14
- branches: 71,
15
- functions: 74,
14
+ branches: 72,
15
+ functions: 75,
16
16
  lines: 78,
17
17
  statements: 78,
18
18
  },
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "main": "src/index.js",
5
5
  "scripts": {
6
6
  "dev": "npx tailwindcss -i ./src/styles/application.css -o ../../apps/admin/static/application.css --watch",
7
- "test": "jest --coverage --coverageReporters=text --coverageReporters=\"text-summary\""
7
+ "test": "jest --coverage --coverageReporters=html --coverageReporters=\"text-summary\""
8
8
  },
9
9
  "devDependencies": {
10
10
  "@babel/core": "7.24.7",
@@ -102,7 +102,7 @@ const destroy = async (ctx) => {
102
102
  const { errors } = await ctx.client.redirects.delete(id);
103
103
 
104
104
  if (errors) {
105
- throw `Failed to delete redirect #${id}. WTF?`;
105
+ throw `Failed to delete redirect #${id}.`;
106
106
  }
107
107
 
108
108
  return {
@@ -92,7 +92,7 @@ const destroy = async (ctx) => {
92
92
  const { errors } = await ctx.client.tags.delete(id, ctx.locale);
93
93
 
94
94
  if (errors) {
95
- throw `Failed to delete tag #${id}. WTF?`;
95
+ throw `Failed to delete tag #${id}.`;
96
96
  }
97
97
 
98
98
  return {
@@ -106,7 +106,7 @@ const destroy = async (ctx) => {
106
106
 
107
107
  return {
108
108
  headers: {
109
- "HX-redirect": `/${ctx.locale}/${ctx.resourceConfig.categoryTypePlural}`,
109
+ "HX-Redirect": `/${ctx.locale}/${ctx.resourceConfig.categoryTypePlural}`,
110
110
  },
111
111
  json: {},
112
112
  };
@@ -31,7 +31,7 @@ export default class HTMLBuilder {
31
31
  }, "");
32
32
 
33
33
  if (children || nonVoid)
34
- return `<${tag} ${htmlAttributes}>${children}</${tag}>`;
34
+ return `<${tag} ${htmlAttributes}>${children ? children : ""}</${tag}>`;
35
35
 
36
36
  return `<${tag} ${htmlAttributes} />`;
37
37
  }
@@ -7,26 +7,28 @@ import AuthHelper from "./helpers/AuthHelper";
7
7
  import simpleCipher from "./simpleCipher";
8
8
 
9
9
  // Generate a random signing secret on every server restart
10
- const SIGNING_SECRET = randomBytes(64).toString("hex");
10
+ // const SIGNING_SECRET = randomBytes(64).toString("hex");
11
11
  const DELIMITER = ",";
12
12
 
13
- function calculateHash(text) {
14
- return createHmac("sha256", SIGNING_SECRET).update(text).digest("hex");
13
+ function calculateHash(text, config) {
14
+ return createHmac("sha256", config.secrets.signing_secret)
15
+ .update(text)
16
+ .digest("hex");
15
17
  }
16
18
 
17
- function obfuscateToken(token) {
19
+ function obfuscateToken(token, config) {
18
20
  const date = new Date();
19
21
  const timestamp = date.getTime() + 2 * 60000;
20
22
  const timestamped = [token, timestamp].join(DELIMITER);
21
- const hash = calculateHash(timestamped);
23
+ const hash = calculateHash(timestamped, config);
22
24
  const signed = [hash, timestamped].join(DELIMITER);
23
25
 
24
- return Buffer.from(simpleCipher.encrypt(signed)).toString("base64");
26
+ return Buffer.from(simpleCipher.encrypt(signed, config)).toString("base64");
25
27
  }
26
28
 
27
- function extractToken(text) {
29
+ function extractToken(text, config) {
28
30
  const encrypted = Buffer.from(text, "base64").toString("ascii");
29
- const signed = simpleCipher.decrypt(encrypted);
31
+ const signed = simpleCipher.decrypt(encrypted, config);
30
32
  const [hash, token, timestamp] = signed.split(DELIMITER);
31
33
  const tDate = new Date(Number.parseInt(timestamp, 10));
32
34
 
@@ -34,7 +36,7 @@ function extractToken(text) {
34
36
  return null;
35
37
  }
36
38
 
37
- if (hash !== calculateHash([token, timestamp].join(DELIMITER))) {
39
+ if (hash !== calculateHash([token, timestamp].join(DELIMITER), config)) {
38
40
  return null;
39
41
  }
40
42
 
@@ -138,9 +140,7 @@ const setupAuthentication = (app, config) => {
138
140
  return c.redirect("/login");
139
141
  }
140
142
 
141
- const ot = obfuscateToken(token);
142
-
143
- console.log({ ot });
143
+ const ot = obfuscateToken(token, config);
144
144
 
145
145
  return c.redirect(
146
146
  [
@@ -148,7 +148,7 @@ const setupAuthentication = (app, config) => {
148
148
  "?redirect_to=",
149
149
  c.req.query().redirect_to,
150
150
  "&snapshot=",
151
- obfuscateToken(token),
151
+ ot,
152
152
  ].join("")
153
153
  );
154
154
  });
@@ -156,7 +156,7 @@ const setupAuthentication = (app, config) => {
156
156
  app.post("/remote/verify", async (c) => {
157
157
  const { credentials } = await c.req.json();
158
158
 
159
- const token = extractToken(credentials);
159
+ const token = extractToken(credentials, config);
160
160
 
161
161
  if (!token) {
162
162
  return unauthorized();
@@ -1,14 +1,6 @@
1
1
  const ALPHABET =
2
2
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.";
3
3
 
4
- function generateKey() {
5
- const shuffled = ALPHABET.split("")
6
- .sort(() => Math.random() - 0.5)
7
- .join("");
8
-
9
- return shuffled;
10
- }
11
-
12
4
  function generateEncryptionLookup(key) {
13
5
  const lookup = {};
14
6
 
@@ -29,22 +21,34 @@ function generateDecryptionLookup(key) {
29
21
  return lookup;
30
22
  }
31
23
 
24
+ // function generateKey() {
25
+ // const shuffled = ALPHABET.split("")
26
+ // .sort(() => Math.random() - 0.5)
27
+ // .join("");
28
+
29
+ // return shuffled;
30
+ // }
31
+
32
32
  // Scramble lookup maps on every server restart
33
- const CIPHER_KEY = generateKey();
34
- const ENCRYPT_LOOKUP = generateEncryptionLookup(CIPHER_KEY);
35
- const DECRYPT_LOOKUP = generateDecryptionLookup(CIPHER_KEY);
33
+ // const CIPHER_KEY = generateKey();
34
+ // const ENCRYPT_LOOKUP = generateEncryptionLookup(CIPHER_KEY);
35
+ // const DECRYPT_LOOKUP = generateDecryptionLookup(CIPHER_KEY);
36
+
37
+ function encrypt(plaintext, config) {
38
+ const ecryptLookup = generateEncryptionLookup(config.secrets.crypto_secret);
36
39
 
37
- function encrypt(plaintext) {
38
40
  return plaintext
39
41
  .split("")
40
- .map((char) => ENCRYPT_LOOKUP[char] || char)
42
+ .map((char) => ecryptLookup[char] || char)
41
43
  .join("");
42
44
  }
43
45
 
44
- function decrypt(ciphertext) {
46
+ function decrypt(ciphertext, config) {
47
+ const decryptLookup = generateDecryptionLookup(config.secrets.crypto_secret);
48
+
45
49
  return ciphertext
46
50
  .split("")
47
- .map((char) => DECRYPT_LOOKUP[char] || char)
51
+ .map((char) => decryptLookup[char] || char)
48
52
  .join("");
49
53
  }
50
54
 
@@ -0,0 +1,33 @@
1
+ import Logger from "../Logger";
2
+
3
+ describe("Logger", () => {
4
+ describe("error", () => {
5
+ test("Logs an error to the console", () => {
6
+ const logSpy = jest.spyOn(global.console, "error");
7
+
8
+ Logger.error({ foo: "BAR" });
9
+
10
+ expect(logSpy).toHaveBeenCalledWith("[ERROR]", "{ foo: 'BAR' }");
11
+ });
12
+ });
13
+
14
+ describe("info", () => {
15
+ test("Logs info to the console", () => {
16
+ const logSpy = jest.spyOn(global.console, "info");
17
+
18
+ Logger.info({ foo: "BAR" });
19
+
20
+ expect(logSpy).toHaveBeenCalledWith("[INFO]", "{ foo: 'BAR' }");
21
+ });
22
+ });
23
+
24
+ describe("debug", () => {
25
+ test("Logs debug information to the console", () => {
26
+ const logSpy = jest.spyOn(global.console, "debug");
27
+
28
+ Logger.debug({ foo: "BAR" });
29
+
30
+ expect(logSpy).toHaveBeenCalledWith("[DEBUG]", "{ foo: 'BAR' }");
31
+ });
32
+ });
33
+ });
@@ -45,7 +45,7 @@ describe("Redirects", () => {
45
45
  }));
46
46
 
47
47
  await expect(() => RedirectsHandler.destroy(ctx)).rejects.toMatch(
48
- "Failed to delete redirect #1. WTF?"
48
+ "Failed to delete redirect #1."
49
49
  );
50
50
 
51
51
  expect(mockDelete).toHaveBeenCalledWith(1);
@@ -43,7 +43,7 @@ describe("Tags Handler", () => {
43
43
  }));
44
44
 
45
45
  expect(() => TagsHandler.destroy(ctx)).rejects.toMatch(
46
- "Failed to delete tag #1. WTF?"
46
+ "Failed to delete tag #1."
47
47
  );
48
48
 
49
49
  expect(mockedDelete).toHaveBeenCalledWith("1", "en");
@@ -1,34 +1,38 @@
1
- import RedirectsHandler from "../../../handlers/RedirectsHandler";
2
- import Redirects from "../../../cloud/Redirects";
1
+ import CategoriesHandler from "../../../handlers/categories";
2
+ import Categories from "../../../cloud/Categories";
3
3
 
4
4
  import Factory from "../../support/factory";
5
5
 
6
6
  function buildCtx() {
7
7
  return {
8
8
  client: {
9
- redirects: new Redirects(() => ({}), "token"),
9
+ categories: new Categories(() => ({}), "token"),
10
10
  },
11
11
  params: { id: 1 },
12
12
  locale: "en",
13
+ resourceConfig: {
14
+ categoryType: "cat",
15
+ categoryTypePlural: "cats",
16
+ },
13
17
  };
14
18
  }
15
19
 
16
- describe("Redirects", () => {
20
+ describe("Categories", () => {
17
21
  test("destroy successful", async () => {
18
22
  const redirect = Factory.build("redirect");
19
23
  const ctx = buildCtx();
20
24
 
21
25
  const mockDelete = jest
22
- .spyOn(ctx.client.redirects, "delete")
26
+ .spyOn(ctx.client.categories, "delete")
23
27
  .mockImplementation(() => ({ redirect: redirect, errors: null }));
24
28
 
25
- const result = await RedirectsHandler.destroy(ctx);
29
+ const result = await CategoriesHandler.destroy(ctx);
26
30
 
27
- expect(mockDelete).toHaveBeenCalledWith(1);
31
+ expect(mockDelete).toHaveBeenCalledWith(1, "en");
28
32
 
29
33
  expect(result).toEqual({
30
34
  headers: {
31
- "HX-Redirect": "/en/redirects",
35
+ "HX-Redirect": "/en/cats",
32
36
  },
33
37
  json: {},
34
38
  });
@@ -38,16 +42,16 @@ describe("Redirects", () => {
38
42
  const ctx = buildCtx();
39
43
 
40
44
  const mockDelete = jest
41
- .spyOn(ctx.client.redirects, "delete")
45
+ .spyOn(ctx.client.categories, "delete")
42
46
  .mockImplementation(() => ({
43
47
  redirect: null,
44
48
  errors: [{ key: "generalError", message: "Not found" }],
45
49
  }));
46
50
 
47
- await expect(() => RedirectsHandler.destroy(ctx)).rejects.toMatch(
48
- "Failed to delete redirect #1. WTF?"
51
+ await expect(() => CategoriesHandler.destroy(ctx)).rejects.toEqual(
52
+ "Failed to delete category #1."
49
53
  );
50
54
 
51
- expect(mockDelete).toHaveBeenCalledWith(1);
55
+ expect(mockDelete).toHaveBeenCalledWith(1, "en");
52
56
  });
53
57
  });
@@ -0,0 +1,44 @@
1
+ import HTMLBuilder from "../../helpers/HTMLBuilder";
2
+
3
+ describe("HTMLBuilder", () => {
4
+ function normalizeHTML(html) {
5
+ return html.replace(/>\s+</g, "><").replace(/\s+/g, " ").trim();
6
+ }
7
+
8
+ describe("tag", () => {
9
+ test("renders a self closing tag", () => {
10
+ const builder = new HTMLBuilder();
11
+
12
+ const result = builder.tag("img", null, { src: "/some-image.jpg" });
13
+
14
+ expect(normalizeHTML(result)).toEqual(
15
+ normalizeHTML('<img src="/some-image.jpg" />')
16
+ );
17
+ });
18
+
19
+ test("renders a label tag", () => {
20
+ const builder = new HTMLBuilder();
21
+
22
+ const result = builder.tag("label", "Heelo", { for: "some-input-id" });
23
+
24
+ expect(normalizeHTML(result)).toEqual(
25
+ normalizeHTML('<label for="some-input-id">Heelo</label>')
26
+ );
27
+ });
28
+
29
+ test("renders a textarea tag", () => {
30
+ const builder = new HTMLBuilder();
31
+
32
+ const result = builder.tag(
33
+ "textarea",
34
+ undefined,
35
+ { id: "some-input-id", value: "Text?" },
36
+ { nonVoid: true }
37
+ );
38
+
39
+ expect(normalizeHTML(result)).toEqual(
40
+ normalizeHTML('<textarea id="some-input-id" value="Text?"></textarea>')
41
+ );
42
+ });
43
+ });
44
+ });