@raketa-cloud/admin 0.0.2 → 0.0.4

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.
Files changed (50) hide show
  1. package/babel.config.js +6 -0
  2. package/bin/tailwind +2 -0
  3. package/package.json +22 -29
  4. package/src/Adapter.js +105 -0
  5. package/src/HonoAdapter.js +120 -0
  6. package/src/RaketaContext.js +33 -0
  7. package/src/createNestedResource.js +83 -0
  8. package/src/createResource.js +83 -0
  9. package/src/index.js +3 -0
  10. package/src/styles/application.css +17 -0
  11. package/src/tests/HonoAdapter.test.js +33 -0
  12. package/src/tests/createNestedResource.test.js +150 -0
  13. package/src/tests/createResource.test.js +115 -0
  14. package/src/views/layouts/application.ejs +22 -0
  15. package/src/views/redirects/index.ejs +1 -0
  16. package/tailwind.config.js +8 -0
  17. package/tsconfig.json +14 -14
  18. package/tsconfig.lint.json +8 -0
  19. package/biome.json +0 -6
  20. package/jest.config.js +0 -15
  21. package/src/app.tsx +0 -19
  22. package/src/crud/login.tsx +0 -99
  23. package/src/crud/logout.tsx +0 -14
  24. package/src/crud/tags.tsx +0 -235
  25. package/src/index.ts +0 -1
  26. package/src/lib/dxp/private/tags.ts +0 -106
  27. package/src/lib/dxp/protocol.ts +0 -32
  28. package/src/lib/gateway/login.ts +0 -44
  29. package/src/lib/gateway/protocol.ts +0 -8
  30. package/src/lib/gqlEngine.ts +0 -17
  31. package/src/lib/logger.tsx +0 -149
  32. package/src/middleware/authentication.ts +0 -62
  33. package/src/middleware/http_logger.ts +0 -41
  34. package/src/middleware/logger.ts +0 -9
  35. package/src/shared/Layout.tsx +0 -50
  36. package/tests/integration/authentication/middleware.test.ts +0 -100
  37. package/tests/integration/login/create.test.ts +0 -117
  38. package/tests/integration/login/index.test.ts +0 -16
  39. package/tests/integration/logout/delete.test.ts +0 -17
  40. package/tests/integration/tags/create.test.ts +0 -66
  41. package/tests/integration/tags/delete.test.ts +0 -74
  42. package/tests/integration/tags/edit.test.ts +0 -43
  43. package/tests/integration/tags/index.test.ts +0 -66
  44. package/tests/integration/tags/new.test.ts +0 -16
  45. package/tests/integration/tags/update.test.ts +0 -87
  46. package/tests/support/helpers.ts +0 -53
  47. package/tests/support/setup.js +0 -9
  48. package/turbo/generators/config.ts +0 -30
  49. package/turbo/generators/templates/component.hbs +0 -14
  50. package/turbo.json +0 -9
@@ -1,32 +0,0 @@
1
- import { getCookie } from "hono/cookie";
2
- import { env } from "hono/adapter";
3
- import { gqlRequest } from "../gqlEngine";
4
-
5
- export function dxpPublicRequest(query: string, variables: any, honoCtx: any) {
6
- const {
7
- RAKETA_GATEWAY_DISPATCH_URL,
8
- RAKETA_GATEWAY_PROJECT,
9
- RAKETA_DXP_PRIVATE_SERVICE,
10
- } = env(honoCtx);
11
-
12
- return gqlRequest(RAKETA_GATEWAY_DISPATCH_URL, query, variables, {
13
- "x-raketa-project": RAKETA_GATEWAY_PROJECT,
14
- "x-raketa-service": RAKETA_DXP_PRIVATE_SERVICE,
15
- });
16
- }
17
-
18
- export function dxpPrivateRequest(query: string, variables: any, honoCtx: any) {
19
- const token = getCookie(honoCtx, "authToken");
20
-
21
- const {
22
- RAKETA_GATEWAY_DISPATCH_URL,
23
- RAKETA_GATEWAY_PROJECT,
24
- RAKETA_DXP_PRIVATE_SERVICE,
25
- } = env(honoCtx);
26
-
27
- return gqlRequest(RAKETA_GATEWAY_DISPATCH_URL, query, variables, {
28
- "x-raketa-project": RAKETA_GATEWAY_PROJECT,
29
- "x-raketa-service": RAKETA_DXP_PRIVATE_SERVICE,
30
- "x-raketa-identity": token,
31
- });
32
- }
@@ -1,44 +0,0 @@
1
- import { Context } from "hono";
2
- import { env } from "hono/adapter";
3
- import { gql } from "../gqlEngine";
4
- import { gatewayRequest } from "./protocol";
5
-
6
- const LoginDocument = gql`
7
- mutation Login($input: LoginInput!) {
8
- login(input: $input) {
9
- accessToken
10
- errors {
11
- key
12
- message
13
- }
14
- }
15
- }
16
- `;
17
-
18
- const login = {
19
- create: async (email: string, password: string, honoCtx: Context) => {
20
- const {
21
- RAKETA_GATEWAY_PRIMARY_USER_POOL,
22
- RAKETA_GATEWAY_SECONDARY_USER_POOL,
23
- RAKETA_GATEWAY_PROJECT,
24
- } = env(honoCtx);
25
-
26
- return gatewayRequest(
27
- LoginDocument,
28
- {
29
- input: {
30
- email,
31
- password,
32
- userPools: [
33
- RAKETA_GATEWAY_PRIMARY_USER_POOL,
34
- RAKETA_GATEWAY_SECONDARY_USER_POOL,
35
- ].filter(Boolean),
36
- project: RAKETA_GATEWAY_PROJECT,
37
- },
38
- },
39
- honoCtx,
40
- );
41
- },
42
- };
43
-
44
- export default login;
@@ -1,8 +0,0 @@
1
- import { env } from "hono/adapter";
2
- import { gqlRequest } from "../gqlEngine";
3
-
4
- export function gatewayRequest(query: string, variables: any, honoCtx: any) {
5
- const { RAKETA_GATEWAY_AUTH_URL } = env(honoCtx);
6
-
7
- return gqlRequest(RAKETA_GATEWAY_AUTH_URL, query, variables);
8
- }
@@ -1,17 +0,0 @@
1
- import { request, gql } from "graphql-request";
2
-
3
- export function gqlRequest(
4
- url: string,
5
- query: string,
6
- variables = {},
7
- headers = {},
8
- ) {
9
- return request({
10
- url,
11
- document: query,
12
- variables: variables,
13
- requestHeaders: headers,
14
- });
15
- }
16
-
17
- export { gql };
@@ -1,149 +0,0 @@
1
- enum Severity {
2
- Info = 1,
3
- Warn = 2,
4
- Error = 3,
5
- Debug = 4,
6
- }
7
-
8
- type Payload = {
9
- date: Date;
10
- identifier: string;
11
- severity: Severity;
12
- message: string;
13
- meta?: object;
14
- };
15
-
16
- class Fancy {
17
- static SEVERITY = {
18
- [Severity.Info]: "I",
19
- [Severity.Warn]: "W",
20
- [Severity.Error]: "E",
21
- [Severity.Debug]: "D",
22
- };
23
-
24
- private static formatDate(date: Date) {
25
- return date.toISOString().replace("T", " ").split(".")[0];
26
- }
27
-
28
- private static formatSeverity(severity: Severity) {
29
- return Fancy.SEVERITY[severity];
30
- }
31
-
32
- static formatStatus(status: number) {
33
- const out: { [key: string]: string } = {
34
- 5: `\x1b[31m${status}\x1b[0m`,
35
- 4: `\x1b[31m${status}\x1b[0m`,
36
- 3: `\x1b[33m${status}\x1b[0m`,
37
- 2: `\x1b[32m${status}\x1b[0m`,
38
- 1: `\x1b[32m${status}\x1b[0m`,
39
- 0: `${status}`,
40
- };
41
-
42
- const calculateStatus = (status / 100) | 0;
43
-
44
- return out[calculateStatus];
45
- }
46
-
47
- static formatMessage(message: string): string {
48
- if (message.startsWith("-> ")) {
49
- const { verb, path } = message.match(
50
- /(?<verb>GET|POST|DELETE|PUT|PATCH|OPTIONS)\s(?<path>.*)$/i
51
- ).groups;
52
-
53
- return `-> ${verb} \x1b[36m${path}\x1b[0m`;
54
- }
55
-
56
- if (message.startsWith("<- ")) {
57
- const { status } = message.match(/<-\s(?<status>\d{3})\s/i).groups;
58
-
59
- return `${message.replace(
60
- status,
61
- Fancy.formatStatus(parseInt(status))
62
- )}\n`;
63
- }
64
-
65
- return message;
66
- }
67
-
68
- static format({ date, identifier, severity, message }: Payload): string {
69
- return [
70
- `[${Fancy.formatDate(date)}]`,
71
- `(${identifier})`,
72
- `${Fancy.formatSeverity(severity)}:`,
73
- Fancy.formatMessage(message),
74
- ].join(" ");
75
- }
76
- }
77
-
78
- class Structured {
79
- static SEVERITY = {
80
- [Severity.Info]: "info",
81
- [Severity.Warn]: "warning",
82
- [Severity.Error]: "error",
83
- [Severity.Debug]: "debug",
84
- };
85
-
86
- private static formatDate(date: Date) {
87
- return date.toISOString();
88
- }
89
-
90
- private static formatSeverity(severity: Severity) {
91
- return Structured.SEVERITY[severity];
92
- }
93
-
94
- static format({
95
- date,
96
- identifier,
97
- severity,
98
- message,
99
- meta,
100
- }: Payload): string {
101
- return JSON.stringify({
102
- date: Structured.formatDate(date),
103
- identifier,
104
- severity: Structured.formatSeverity(severity),
105
- message,
106
- meta,
107
- });
108
- }
109
- }
110
-
111
- export class Logger {
112
- requestID: string;
113
- formatter: typeof Fancy | typeof Structured;
114
-
115
- constructor(requestID: string, formatter = Fancy) {
116
- this.requestID = requestID;
117
- this.formatter = formatter;
118
- }
119
-
120
- private log(severity: Severity, message: string, meta?: object) {
121
- const date = new Date();
122
-
123
- console.log(
124
- this.formatter.format({
125
- date,
126
- identifier: this.requestID,
127
- severity,
128
- message,
129
- meta,
130
- })
131
- );
132
- }
133
-
134
- info(message: string, meta?: object) {
135
- this.log(Severity.Info, message, meta);
136
- }
137
-
138
- warn(message: string, meta?: object) {
139
- this.log(Severity.Warn, message, meta);
140
- }
141
-
142
- error(message: string, meta?: object) {
143
- this.log(Severity.Error, message, meta);
144
- }
145
-
146
- debug(message: string, meta?: object) {
147
- this.log(Severity.Debug, message, meta);
148
- }
149
- }
@@ -1,62 +0,0 @@
1
- import { Context, Next } from "hono";
2
- import { getCookie } from "hono/cookie";
3
- import { decode } from "hono/jwt";
4
- import { env } from "hono/adapter";
5
-
6
- const EXCLUDED_PATHS = ["/login", "/logout", "/serviceworker.js"];
7
-
8
- function shouldAuthenticate(c: Context) {
9
- return !EXCLUDED_PATHS.includes(c.req.path);
10
- }
11
-
12
- function redirect(location: string) {
13
- return new Response("Not Authorized", {
14
- status: 301,
15
- headers: {
16
- Location: location,
17
- "Set-Cookie": "authToken=; Max-Age=0",
18
- },
19
- });
20
- }
21
-
22
- async function authenticate(c: Context, next: Next) {
23
- const token = getCookie(c, "authToken");
24
-
25
- if (!token) {
26
- return redirect("/login");
27
- }
28
-
29
- const parsedToken = decode(token);
30
- const payload = parsedToken.payload;
31
-
32
- if (payload.exp < (new Date().getTime() + 1) / 1000) {
33
- return redirect("/login");
34
- }
35
-
36
- if (!payload.info.active) {
37
- return redirect("/login?reason=inactive");
38
- }
39
-
40
- const {
41
- RAKETA_GATEWAY_PRIMARY_USER_POOL,
42
- RAKETA_GATEWAY_SECONDARY_USER_POOL,
43
- } = env(c);
44
-
45
- if (
46
- ![RAKETA_GATEWAY_PRIMARY_USER_POOL, RAKETA_GATEWAY_SECONDARY_USER_POOL]
47
- .filter(Boolean)
48
- .includes(payload.info.user_pool_id)
49
- ) {
50
- return redirect("/login");
51
- }
52
-
53
- await next();
54
- }
55
-
56
- export default async (c: Context, next: Next) => {
57
- if (shouldAuthenticate(c)) {
58
- return authenticate(c, next);
59
- }
60
-
61
- await next();
62
- };
@@ -1,41 +0,0 @@
1
- import { Context, Next } from "hono";
2
-
3
- const humanize = (times: string[]) => {
4
- const [delimiter, separator] = [",", "."];
5
-
6
- const orderTimes = times.map((v) =>
7
- v.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, `$1${delimiter}`),
8
- );
9
-
10
- return orderTimes.join(separator);
11
- };
12
-
13
- const time = (start: number) => {
14
- const delta = Date.now() - start;
15
- return humanize([
16
- delta < 1000 ? `${delta}ms` : `${Math.round(delta / 1000)}s`,
17
- ]);
18
- };
19
-
20
- export default async (c: Context, next: Next) => {
21
- const logger = c.get("logger");
22
- const { method } = c.req;
23
-
24
- const start = Date.now();
25
- const url = new URL(c.req.url);
26
- const path = url.href.replace(url.origin, "");
27
-
28
- logger.info(`-> ${method} ${path}`, {
29
- method,
30
- path,
31
- });
32
-
33
- await next();
34
-
35
- const elapsed = time(start);
36
-
37
- logger.info(`<- ${c.res.status} in ${elapsed}`, {
38
- method,
39
- elapsed,
40
- });
41
- };
@@ -1,9 +0,0 @@
1
- import { Context, Next } from "hono";
2
- import { randomBytes } from "crypto";
3
- import { Logger } from "src/lib/logger";
4
-
5
- export default async (c: Context, next: Next) => {
6
- c.set("logger", new Logger(randomBytes(8).toString("hex")));
7
-
8
- await next();
9
- };
@@ -1,50 +0,0 @@
1
- import { Context } from "hono";
2
- import type { FC } from "hono/jsx";
3
- import { getCookie } from "hono/cookie";
4
- import { decode } from "hono/jwt";
5
-
6
- function fetchUser(c: Context) {
7
- const token = getCookie(c, "authToken");
8
-
9
- if (!token) {
10
- return null;
11
- }
12
-
13
- return decode(token).payload.info || null;
14
- }
15
-
16
- const Layout: FC = ({ context, children }) => {
17
- const user = fetchUser(context);
18
-
19
- return (
20
- <html lang="en">
21
- <head>
22
- <meta charset="UTF-8" />
23
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
-
25
- <script src="https://cdn.tailwindcss.com" />
26
- <script src="https://unpkg.com/htmx.org@1.9.6" />
27
-
28
- <title>Administration</title>
29
- </head>
30
-
31
- <body>
32
- {user && (
33
- <div class="flex items-center justify-end gap-4 w-full h-[50px] p-4">
34
- <img
35
- src={user.avatar}
36
- class="rounded-full h-[40px] w-[40px]"
37
- alt="Admin avatar"
38
- />
39
- <button type="button" hx-delete="/logout">
40
- Logout
41
- </button>
42
- </div>
43
- )}
44
- {children}
45
- </body>
46
- </html>
47
- );
48
- };
49
-
50
- export default Layout;
@@ -1,100 +0,0 @@
1
- import { Hono } from "hono";
2
- import { sign } from "hono/jwt";
3
- import { describe, expect, test } from "@jest/globals";
4
- import auth from "src/middleware/authentication";
5
-
6
- const app = new Hono();
7
-
8
- app.use("*", auth);
9
-
10
- async function prepareToken(
11
- userPool: string,
12
- { active = true, date }: { active?: boolean; date: Date },
13
- ) {
14
- return await sign(
15
- {
16
- sub: "1",
17
- exp: (date.getTime() / 1000).toFixed(0),
18
- info: {
19
- user_pool_id: userPool,
20
- active: active,
21
- },
22
- },
23
- "secret",
24
- );
25
- }
26
-
27
- const USER_POOL = "primary-user-pool";
28
-
29
- describe("Authentication middleware", () => {
30
- test("Does not fire on excluded paths", async () => {
31
- let res = await app.request("/login");
32
- expect(res.status).toBe(404);
33
-
34
- res = await app.request("/logout");
35
- expect(res.status).toBe(404);
36
-
37
- res = await app.request("/serviceworker.js");
38
- expect(res.status).toBe(404);
39
- });
40
-
41
- test("Allows access with ok token and logged in user", async () => {
42
- const date = new Date(new Date().getTime() + 1000000);
43
- const token = await prepareToken(USER_POOL, { date });
44
-
45
- const req = new Request("https://example.com/tags", {
46
- method: "GET",
47
- headers: {
48
- Cookie: `authToken=${token}`,
49
- },
50
- });
51
-
52
- const res = await app.request(req);
53
-
54
- expect(res.status).toBe(404);
55
- });
56
-
57
- test("Redirects in case of missing token", async () => {
58
- const res = await app.request("/tags");
59
-
60
- expect(res.status).toBe(301);
61
- expect(res.headers.get("Location")).toBe("/login");
62
- expect(res.headers.get("Set-Cookie")).toBe("authToken=; Max-Age=0");
63
- });
64
-
65
- test("Redirects in case of expired token", async () => {
66
- const date = new Date(new Date().getTime() - 1000000);
67
- const token = await prepareToken(USER_POOL, { date });
68
-
69
- const req = new Request("https://example.com/tags", {
70
- method: "GET",
71
- headers: {
72
- Cookie: `authToken=${token}`,
73
- },
74
- });
75
-
76
- const res = await app.request(req);
77
-
78
- expect(res.status).toBe(301);
79
- expect(res.headers.get("Location")).toBe("/login");
80
- expect(res.headers.get("Set-Cookie")).toBe("authToken=; Max-Age=0");
81
- });
82
-
83
- test("Redirects in case of inactive user", async () => {
84
- const date = new Date(new Date().getTime() + 1000000);
85
- const token = await prepareToken(USER_POOL, { active: false, date });
86
-
87
- const req = new Request("https://example.com/tags", {
88
- method: "GET",
89
- headers: {
90
- Cookie: `authToken=${token}`,
91
- },
92
- });
93
-
94
- const res = await app.request(req);
95
-
96
- expect(res.status).toBe(301);
97
- expect(res.headers.get("Location")).toBe("/login?reason=inactive");
98
- expect(res.headers.get("Set-Cookie")).toBe("authToken=; Max-Age=0");
99
- });
100
- });
@@ -1,117 +0,0 @@
1
- import { sign } from "hono/jwt";
2
- import { describe, expect, test } from "@jest/globals";
3
- import app from "src/crud/login";
4
- import {
5
- mockGraphql,
6
- assertAuthRequestCalled,
7
- assertEmptyBody,
8
- } from "tests/support/helpers";
9
-
10
- const LOGIN_MUTATION = `
11
- mutation Login($input: LoginInput!) {
12
- login(input: $input) {
13
- accessToken
14
- errors {
15
- key
16
- message
17
- }
18
- }
19
- }
20
- `;
21
-
22
- async function prepareToken(
23
- userPool: string,
24
- { active = true, date }: { active?: boolean; date: Date },
25
- ) {
26
- return await sign(
27
- {
28
- sub: "1",
29
- exp: (date.getTime() / 1000).toFixed(0),
30
- info: {
31
- user_pool_id: userPool,
32
- active: active,
33
- },
34
- },
35
- "secret",
36
- );
37
- }
38
-
39
- const USER_POOL = "primary-user-pool";
40
-
41
- describe("Login create endpoint", () => {
42
- test("POST /login with valid credentials", async () => {
43
- const date = new Date();
44
- const token = await prepareToken(USER_POOL, { date });
45
-
46
- const requestsMock = mockGraphql(
47
- Promise.resolve({
48
- login: {
49
- accessToken: token,
50
- errors: null,
51
- },
52
- }),
53
- );
54
-
55
- const formData = new FormData();
56
- formData.set("email", "john@example.com");
57
- formData.set("password", "123");
58
-
59
- const res = await app.request("/login", { method: "POST", body: formData });
60
-
61
- assertAuthRequestCalled(requestsMock, LOGIN_MUTATION, {
62
- input: {
63
- email: "john@example.com",
64
- password: "123",
65
- userPools: [USER_POOL],
66
- project: "1-raketa-gw-priject",
67
- },
68
- });
69
-
70
- expect(res.status).toBe(301);
71
- expect(res.headers.get("Set-Cookie")).toBe(
72
- `authToken=${token}; Path=/; Expires=${date.toUTCString()}`,
73
- );
74
- expect(res.headers.get("Location")).toBe("/tags");
75
- await assertEmptyBody(res);
76
- });
77
- });
78
-
79
- describe("Login create endpoint", () => {
80
- test("POST /login with invalid credentials", async () => {
81
- const requestsMock = mockGraphql(
82
- Promise.resolve({
83
- login: {
84
- accessToken: null,
85
- errors: [
86
- {
87
- key: "email",
88
- message: "Invalid credentials",
89
- },
90
- ],
91
- },
92
- }),
93
- );
94
-
95
- const formData = new FormData();
96
- formData.set("email", "john@example.com");
97
- formData.set("password", "123");
98
-
99
- const res = await app.request("/login", { method: "POST", body: formData });
100
-
101
- assertAuthRequestCalled(requestsMock, LOGIN_MUTATION, {
102
- input: {
103
- email: "john@example.com",
104
- password: "123",
105
- userPools: [USER_POOL],
106
- project: "1-raketa-gw-priject",
107
- },
108
- });
109
-
110
- expect(res.status).toBe(200);
111
- expect(res.headers.get("Set-Cookie")).toBe("authToken=; Max-Age=0; Path=/");
112
-
113
- const content = await res.text();
114
-
115
- expect(content).toContain("Invalid credentials");
116
- });
117
- });
@@ -1,16 +0,0 @@
1
- import { describe, expect, test } from "@jest/globals";
2
- import app from "src/crud/login";
3
-
4
- describe("Login index endpoint", () => {
5
- test("GET /login", async () => {
6
- const res = await app.request("/login");
7
-
8
- expect(res.status).toBe(200);
9
-
10
- const content = await res.text();
11
-
12
- expect(content).toContain("Login");
13
- expect(content).toContain("email");
14
- expect(content).toContain("password");
15
- });
16
- });
@@ -1,17 +0,0 @@
1
- import { describe, expect, test } from "@jest/globals";
2
- import app from "src/crud/logout";
3
- import { assertJSONContent } from "tests/support/helpers";
4
-
5
- describe("Logout delete endpoint", () => {
6
- test("DELETE /logout", async () => {
7
- const res = await app.request("/logout", { method: "DELETE" });
8
-
9
- expect(res.status).toBe(201);
10
- expect(res.headers.get("HX-Redirect")).toBe("/login");
11
- expect(res.headers.get("Set-Cookie")).toBe("authToken=; Max-Age=0; Path=/");
12
-
13
- await assertJSONContent(res, {
14
- message: "Bye",
15
- });
16
- });
17
- });