@raketa-cloud/admin 0.0.3 → 0.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.
Files changed (39) hide show
  1. package/babel.config.js +6 -0
  2. package/bin/tailwind +2 -0
  3. package/package.json +22 -30
  4. package/src/Adapter.js +160 -0
  5. package/src/HonoAdapter.js +248 -0
  6. package/src/RaketaContext.js +46 -0
  7. package/src/helpers/AuthHelper.js +44 -0
  8. package/src/helpers/FormBuilder.js +226 -0
  9. package/src/helpers/HTMLBuilder.js +38 -0
  10. package/src/helpers/Pagination.js +92 -0
  11. package/src/helpers/SortButton.js +102 -0
  12. package/src/helpers/form_for.js +6 -0
  13. package/src/helpers/pagination_helper.js +5 -0
  14. package/src/helpers/sort_button.js +5 -0
  15. package/src/index.js +7 -0
  16. package/src/routeCollection.js +90 -0
  17. package/src/routeMember.js +40 -0
  18. package/src/routeNestedCollection.js +90 -0
  19. package/src/routeNestedMember.js +40 -0
  20. package/src/setGlobalConfig.js +5 -0
  21. package/src/setGlobalContext.js +5 -0
  22. package/src/setupAuthentication.js +99 -0
  23. package/src/styles/application.css +479 -0
  24. package/src/tests/Adapter.test.js +39 -0
  25. package/src/tests/HonoAdapter.test.js +34 -0
  26. package/src/tests/helpers/Pagination.test.js +65 -0
  27. package/src/tests/helpers/SortButton.test.js +85 -0
  28. package/src/tests/routeCollection.test.js +150 -0
  29. package/src/tests/routeMember.test.js +91 -0
  30. package/src/tests/routeNestedCollection.test.js +176 -0
  31. package/src/tests/routeNestedMember.test.js +96 -0
  32. package/src/views/_auth/login.ejs +92 -0
  33. package/src/views/layouts/application.ejs +147 -0
  34. package/src/views/layouts/header.ejs +49 -0
  35. package/src/views/redirects/index.ejs +1 -0
  36. package/tailwind.config.js +39 -0
  37. package/tsconfig.json +15 -0
  38. package/tsconfig.lint.json +8 -0
  39. package/dist/index.js +0 -670
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ presets: [
3
+ ["@babel/preset-env", { targets: { node: "current" } }],
4
+ "@babel/preset-typescript",
5
+ ],
6
+ };
package/bin/tailwind ADDED
@@ -0,0 +1,2 @@
1
+ # npx tailwindcss -i ./src/styles/application.css -o ./dist/application.css --watch
2
+ npx tailwindcss -i ./src/styles/application.css -o ../../apps/admin/static/application.css --watch
package/package.json CHANGED
@@ -1,32 +1,24 @@
1
1
  {
2
- "name": "@raketa-cloud/admin",
3
- "version": "0.0.3",
4
- "private": false,
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "files": ["dist"],
8
- "scripts": {
9
- "build": "tsup src/index.ts --format cjs --loader \".js=jsx\"",
10
- "dev": "tsup src/index.ts --format cjs --loader \".js=jsx\" --watch",
11
- "lint": "biome lint --apply .",
12
- "test": "jest --verbose",
13
- "format": "prettier --write ."
14
- },
15
- "devDependencies": {
16
- "@jest/globals": "^29.7.0",
17
- "@repo/typescript-config": "*",
18
- "@turbo/gen": "^1.11.3",
19
- "@types/jest": "^29.5.8",
20
- "@types/node": "^20.10.6",
21
- "jest": "^29.7.0",
22
- "ts-jest": "^29.1.1",
23
- "tsup": "6.5.0",
24
- "typescript": "^5.3.3"
25
- },
26
- "dependencies": {
27
- "@biomejs/biome": "1.5.3",
28
- "graphql-request": "^6.1.0",
29
- "hono": "^3.12.3",
30
- "prettier": "^3.2.4"
31
- }
2
+ "name": "@raketa-cloud/admin",
3
+ "version": "0.0.5",
4
+ "main": "src/index.js",
5
+ "scripts": {
6
+ "dev": "npx tailwindcss -i ./src/styles/application.css -o ../../apps/admin/static/application.css --watch",
7
+ "test": "jest"
8
+ },
9
+ "devDependencies": {
10
+ "@babel/core": "^7.24.7",
11
+ "@babel/preset-env": "^7.24.7",
12
+ "@babel/preset-typescript": "^7.24.7",
13
+ "@repo/typescript-config": "*",
14
+ "@tailwindcss/forms": "^0.5.7",
15
+ "@tailwindcss/typography": "^0.5.13",
16
+ "@types/node": "^20.11.24",
17
+ "jest": "^29.7.0",
18
+ "tailwindcss": "^3.4.4",
19
+ "typescript": "^5.3.3"
20
+ },
21
+ "dependencies": {
22
+ "ejs": "^3.1.10"
23
+ }
32
24
  }
package/src/Adapter.js ADDED
@@ -0,0 +1,160 @@
1
+ const nodePath = require("node:path");
2
+ import { existsSync } from "node:fs";
3
+ import ejs from "ejs";
4
+ import FormBuilder from "./helpers/FormBuilder";
5
+ import HTMLBuilder from "./helpers/HTMLBuilder";
6
+ import form_for from "./helpers/form_for";
7
+ import pagination from "./helpers/pagination_helper";
8
+ import sort_button from "./helpers/sort_button";
9
+
10
+ function escapeHtml(string) {
11
+ var matchHtmlRegExp = /["'&<>]/;
12
+
13
+ var str = "" + string;
14
+ var match = matchHtmlRegExp.exec(str);
15
+
16
+ if (!match) {
17
+ return str;
18
+ }
19
+
20
+ var escape;
21
+ var html = "";
22
+ var index = 0;
23
+ var lastIndex = 0;
24
+
25
+ for (index = match.index; index < str.length; index++) {
26
+ switch (str.charCodeAt(index)) {
27
+ case 34: // "
28
+ escape = "&quot;";
29
+ break;
30
+ case 38: // &
31
+ escape = "&amp;";
32
+ break;
33
+ case 39: // '
34
+ escape = "&#39;";
35
+ break;
36
+ case 60: // <
37
+ escape = "&lt;";
38
+ break;
39
+ case 62: // >
40
+ escape = "&gt;";
41
+ break;
42
+ default:
43
+ continue;
44
+ }
45
+
46
+ if (lastIndex !== index) {
47
+ html += str.substring(lastIndex, index);
48
+ }
49
+
50
+ lastIndex = index + 1;
51
+ html += escape;
52
+ }
53
+
54
+ return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
55
+ }
56
+
57
+ const renderError = (message, stack) => {
58
+ const errorCSS = `
59
+ * { box-sizing: border-box; margin: 0; padding: 0; }
60
+ html { font-family: sans-serif; font-size: 16px; padding: 2rem; background-color: pink; }
61
+ h1 { margin-bottom: 1rem; color: FireBrick; }
62
+ .fail-squid { font-size: 7rem; }
63
+ pre { padding: 1.5rem 2rem; border-radius: 0.5rem; background-color: black; color: white; line-height: 1.5; font-size: 1.25rem; }
64
+ `;
65
+ return `<style>${errorCSS}</style><div class="fail-squid">🦑</div><h1>${message}</h1><pre>${escapeHtml(stack)}</pre>`;
66
+ };
67
+
68
+ const Logger = {
69
+ error: (msg) => {
70
+ console.error(`\x1b[31m🦑 ${msg}\x1b[0m`);
71
+ },
72
+ };
73
+
74
+ function putIn(result, key, value) {
75
+ const segments = key.split(/[\[\]]+/).filter(Boolean);
76
+ let current = result;
77
+
78
+ segments.forEach((segment, idx) => {
79
+ if (idx === segments.length - 1) {
80
+ current[segment] = value;
81
+ } else {
82
+ if (!current[segment]) {
83
+ current[segment] = {};
84
+ }
85
+
86
+ current = current[segment];
87
+ }
88
+ });
89
+ }
90
+
91
+ export default class Adapter {
92
+ constructor(router) {
93
+ this.router = router;
94
+ }
95
+
96
+ routeCollection(_resource, _mapping) {
97
+ throw new Error("Implement in an adapter");
98
+ }
99
+
100
+ routeNestedCollection(_resource, _mapping) {
101
+ throw new Error("Implement in an adapter");
102
+ }
103
+
104
+ routeMember(_resource, _mapping) {
105
+ throw new Error("Implement in an adapter");
106
+ }
107
+
108
+ routeNestedMember(_resource, _mapping) {
109
+ throw new Error("Implement in an adapter");
110
+ }
111
+
112
+ getActionPath(resource, action) {
113
+ const packagePath = `${nodePath.dirname(__filename)}/views/${resource}/${action}.ejs`;
114
+ const appPath = `${process.cwd()}/src/views/${resource}/${action}.ejs`;
115
+
116
+ return existsSync(appPath) ? appPath : packagePath;
117
+ }
118
+
119
+ getHelpers() {
120
+ const t = new HTMLBuilder();
121
+ const f = new FormBuilder();
122
+
123
+ return { t, f, form_for, sort_button, pagination };
124
+ }
125
+
126
+ async renderLayout(assigns, body) {
127
+ const layoutTemplate = `${nodePath.dirname(__filename)}/views/layouts/application.ejs`;
128
+ return await ejs.renderFile(layoutTemplate, {
129
+ ...assigns,
130
+ body,
131
+ });
132
+ }
133
+
134
+ async renderAction({ views, action, assigns }) {
135
+ const fullTemplatePath = this.getActionPath(views, action);
136
+ const renderAssigns = { ...this.getHelpers(), ...assigns };
137
+
138
+ try {
139
+ const rendered = await ejs.renderFile(fullTemplatePath, renderAssigns);
140
+ const fullPage = await this.renderLayout(renderAssigns, rendered);
141
+
142
+ return fullPage;
143
+ } catch (e) {
144
+ const error = `Error rendering:\n ${fullTemplatePath}`;
145
+ Logger.error(`${error}\n\n${e.stack}`);
146
+
147
+ return renderError(error, e.stack);
148
+ }
149
+ }
150
+
151
+ parseFormBody(body) {
152
+ const result = {};
153
+
154
+ for (const [key, value] of Object.entries(body)) {
155
+ putIn(result, key, value);
156
+ }
157
+
158
+ return { ...result };
159
+ }
160
+ }
@@ -0,0 +1,248 @@
1
+ import Adapter from "./Adapter";
2
+ import RaketaContext from "./RaketaContext";
3
+ import setupAuthentication from "./setupAuthentication";
4
+
5
+ async function parseBody(req) {
6
+ switch (req.header("Content-type")) {
7
+ case "multipart/form-data":
8
+ case "application/x-www-form-urlencoded":
9
+ return req.parseBody();
10
+ case "application/json":
11
+ return req.json();
12
+ case "text/plain":
13
+ return req.text();
14
+ default:
15
+ // That is an ok default for now
16
+ return req.parseBody();
17
+ }
18
+ }
19
+
20
+ function bodyToParams(req, parsedBody, adapter) {
21
+ switch (req.header("Content-type")) {
22
+ case "multipart/form-data":
23
+ case "application/x-www-form-urlencoded":
24
+ return adapter.parseFormBody(parsedBody);
25
+ default:
26
+ return parsedBody;
27
+ }
28
+ }
29
+
30
+ export default class HonoAdapter extends Adapter {
31
+ constructor(router) {
32
+ super(router);
33
+ this.globalCtx = (ctx) => ctx;
34
+ this.router = router;
35
+ this.globalConfig = {};
36
+
37
+ // TODO: Pass in a path to redirect to after login?
38
+ setupAuthentication(router, {
39
+ remote: {
40
+ domain: "localhost",
41
+ host: "http://localhost:3000",
42
+ cookie_key: "cms-starter-slim",
43
+ },
44
+ });
45
+ }
46
+
47
+ setGlobalContext(globalCtx) {
48
+ this.globalCtx = globalCtx;
49
+ }
50
+
51
+ setGlobalConfig(globalConfigCallback) {
52
+ globalConfigCallback(this.globalConfig);
53
+ }
54
+
55
+ async createContext(router, next, verb, resource, action) {
56
+ const body = await parseBody(router.req);
57
+
58
+ return new RaketaContext({
59
+ router,
60
+ res: next,
61
+ req: router.req,
62
+ method: verb,
63
+ headers: router.req.header(),
64
+ rawPath: router.req.routePath,
65
+ path: router.req.path,
66
+ url: router.req.url,
67
+ action,
68
+ params: router.req.param(),
69
+ body,
70
+ resourceParams: bodyToParams(router.req, body, this),
71
+ query: this.formatQuery(router.req.queries()),
72
+ resource,
73
+ assigns: {},
74
+ authentication: router.get("authentication"),
75
+ getGlobalCtx: this.globalCtx,
76
+ });
77
+ }
78
+
79
+ async createAction([
80
+ verb,
81
+ path,
82
+ resource,
83
+ views,
84
+ action,
85
+ handler,
86
+ before_filter,
87
+ after_filter,
88
+ ]) {
89
+ const method = this.getMethod(verb);
90
+
91
+ [...Object.keys(this.globalConfig.locales), ""].forEach((currentLocale) => {
92
+ const completedPath = `/${currentLocale}${path}`;
93
+
94
+ method(completedPath, async (routerCtx, next) => {
95
+ const ctx = await this.createContext(
96
+ routerCtx,
97
+ next,
98
+ verb,
99
+ resource,
100
+ action
101
+ );
102
+
103
+ ctx.locales = this.globalConfig.locales;
104
+ ctx.defaultLocale = this.globalConfig.defaultLocale;
105
+ ctx.locale = currentLocale;
106
+
107
+ const _context = {
108
+ locales: ctx.locales,
109
+ locale: ctx.locale,
110
+ defaultLocale: ctx.defaultLocale,
111
+ path: ctx.req.path,
112
+ query: ctx.query || {},
113
+ };
114
+
115
+ console.info(
116
+ `\x1b[35m[${ctx.method}] ${ctx.rawPath}\x1b[0m -> ${resource}/${action} | ${JSON.stringify(ctx.params)}`
117
+ );
118
+
119
+ const assigns = {
120
+ beforeFilterAssigns: {},
121
+ actionAssigns: {},
122
+ afterFilterAssigns: {},
123
+ };
124
+
125
+ // Initialize global context
126
+ ctx.initGlobalContext();
127
+
128
+ // Process before filters
129
+ if (before_filter) {
130
+ const {
131
+ redirect,
132
+ json,
133
+ assigns: beforeFilterAssigns = {},
134
+ } = before_filter(ctx) || {};
135
+
136
+ if (redirect) return routerCtx.redirect(redirect);
137
+
138
+ if (json) {
139
+ return routerCtx.json(json);
140
+ }
141
+
142
+ assigns.beforeFilterAssigns = beforeFilterAssigns;
143
+ }
144
+
145
+ // Process action
146
+ const {
147
+ headers,
148
+ redirect,
149
+ json,
150
+ assigns: actionAssigns = {},
151
+ } = (await handler(ctx)) || {};
152
+ assigns.actionAssigns = actionAssigns;
153
+
154
+ this.applyHeaders(headers, routerCtx);
155
+
156
+ if (redirect) return routerCtx.redirect(redirect);
157
+
158
+ // Process after filters
159
+ if (after_filter) {
160
+ const {
161
+ redirect,
162
+ json,
163
+ assigns: afterFilterAssigns = {},
164
+ } = after_filter(ctx) || {};
165
+
166
+ if (redirect) return routerCtx.redirect(redirect);
167
+
168
+ if (json) {
169
+ return routerCtx.json(json);
170
+ }
171
+
172
+ assigns.afterFilterAssigns = afterFilterAssigns;
173
+ }
174
+
175
+ const fullAssigns = {
176
+ ...ctx.assigns,
177
+ ...assigns.beforeFilterAssigns,
178
+ ...assigns.actionAssigns,
179
+ ...assigns.afterFilterAssigns,
180
+ _context: _context,
181
+ };
182
+
183
+ if (json) {
184
+ return routerCtx.json(json);
185
+ }
186
+
187
+ const rendered = this.renderAction({
188
+ views,
189
+ action,
190
+ assigns: fullAssigns,
191
+ });
192
+
193
+ return routerCtx.html(rendered);
194
+ });
195
+ });
196
+ }
197
+
198
+ routeCollection(mapping) {
199
+ mapping.forEach((handlerMap) => this.createAction(handlerMap));
200
+ }
201
+
202
+ routeNestedCollection(mapping) {
203
+ mapping.forEach((handlerMap) => this.createAction(handlerMap));
204
+ }
205
+
206
+ routeMember(mapping) {
207
+ mapping.forEach((handlerMap) => this.createAction(handlerMap));
208
+ }
209
+
210
+ routeNestedMember(mapping) {
211
+ mapping.forEach((handlerMap) => this.createAction(handlerMap));
212
+ }
213
+
214
+ getMethod(verb) {
215
+ return (
216
+ {
217
+ GET: this.router.get,
218
+ POST: this.router.post,
219
+ PUT: this.router.put,
220
+ DELETE: this.router.delete,
221
+ }[verb] || this.router.get
222
+ );
223
+ }
224
+
225
+ applyHeaders(headers, router) {
226
+ if (!headers) {
227
+ return;
228
+ }
229
+
230
+ for (const [key, value] of Object.entries(headers)) {
231
+ router.header(key, value);
232
+ }
233
+ }
234
+
235
+ formatQuery(queries) {
236
+ const result = {};
237
+
238
+ for (const [key, value] of Object.entries(queries)) {
239
+ if (value.length === 1) {
240
+ result[key] = value[0];
241
+ } else {
242
+ result[key] = value;
243
+ }
244
+ }
245
+
246
+ return { ...result };
247
+ }
248
+ }
@@ -0,0 +1,46 @@
1
+ export default class RaketaContext {
2
+ constructor({
3
+ router,
4
+ res,
5
+ req,
6
+ method,
7
+ headers,
8
+ rawPath,
9
+ path,
10
+ url,
11
+ action,
12
+ params,
13
+ query,
14
+ body,
15
+ resourceParams,
16
+ resource,
17
+ assigns,
18
+ authentication,
19
+ getGlobalCtx,
20
+ }) {
21
+ this.router = router;
22
+ this.res = res;
23
+ this.req = req;
24
+ this.method = method;
25
+ this.headers = headers;
26
+ this.rawPath = rawPath;
27
+ this.path = path;
28
+ this.url = url;
29
+ this.action = action;
30
+ this.params = params;
31
+ this.query = query;
32
+ this.body = body;
33
+ this.resource = resource;
34
+ this.resourceParams = resourceParams;
35
+ this.assigns = assigns;
36
+ this.authentication = authentication;
37
+ this.getGlobalCtx = getGlobalCtx;
38
+ }
39
+
40
+ initGlobalContext() {
41
+ const global = this.getGlobalCtx(this);
42
+ for (const key in global) {
43
+ this[key] = global[key];
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,44 @@
1
+ const AuthHelper = {
2
+ cookie_key: "auth_cookie",
3
+
4
+ parseJWT: (token) => {
5
+ return JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
6
+ },
7
+
8
+ verifyJWT: (token) => {
9
+ const { exp } = AuthHelper.parseJWT(token);
10
+ if (!exp) return false;
11
+
12
+ const now = new Date();
13
+
14
+ if (exp > now.getTime() / 1000) return true;
15
+
16
+ return false;
17
+ },
18
+
19
+ isAuthenticated: ({ path, token }) => {
20
+ const ALLOWED_PATHS = ["/login", "/auth/google/callback"];
21
+
22
+ if (ALLOWED_PATHS.includes(path)) return true;
23
+
24
+ if (token && AuthHelper.verifyJWT(token)) return true;
25
+
26
+ return false;
27
+ },
28
+
29
+ getGoogleURL: () => {
30
+ const {
31
+ GATEWAY_SOCIAL_AUTH_URL: url,
32
+ GATEWAY_PROJECT: project,
33
+ GATEWAY_PRIMARY_USER_POOL: user_pool,
34
+ ADMIN_LOGIN_CALLBACK_URL: callback_url,
35
+ } = process.env;
36
+
37
+ if ([url, project, user_pool, callback_url].join() === "")
38
+ throw new Error("Invalid ENVs for authentication configuration");
39
+
40
+ return `${url}/${project}/${user_pool}/google?return_to=${callback_url}`;
41
+ },
42
+ };
43
+
44
+ export default AuthHelper;