@raketa-cloud/admin 0.0.4 → 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 (33) hide show
  1. package/package.json +2 -2
  2. package/src/Adapter.js +61 -6
  3. package/src/HonoAdapter.js +191 -63
  4. package/src/RaketaContext.js +13 -0
  5. package/src/helpers/AuthHelper.js +44 -0
  6. package/src/helpers/FormBuilder.js +226 -0
  7. package/src/helpers/HTMLBuilder.js +38 -0
  8. package/src/helpers/Pagination.js +92 -0
  9. package/src/helpers/SortButton.js +102 -0
  10. package/src/helpers/form_for.js +6 -0
  11. package/src/helpers/pagination_helper.js +5 -0
  12. package/src/helpers/sort_button.js +5 -0
  13. package/src/index.js +6 -2
  14. package/src/{createResource.js → routeCollection.js} +13 -6
  15. package/src/routeMember.js +40 -0
  16. package/src/{createNestedResource.js → routeNestedCollection.js} +12 -5
  17. package/src/routeNestedMember.js +40 -0
  18. package/src/setGlobalConfig.js +5 -0
  19. package/src/setGlobalContext.js +5 -0
  20. package/src/setupAuthentication.js +99 -0
  21. package/src/styles/application.css +465 -3
  22. package/src/tests/Adapter.test.js +39 -0
  23. package/src/tests/HonoAdapter.test.js +1 -0
  24. package/src/tests/helpers/Pagination.test.js +65 -0
  25. package/src/tests/helpers/SortButton.test.js +85 -0
  26. package/src/tests/{createResource.test.js → routeCollection.test.js} +51 -16
  27. package/src/tests/routeMember.test.js +91 -0
  28. package/src/tests/{createNestedResource.test.js → routeNestedCollection.test.js} +36 -10
  29. package/src/tests/routeNestedMember.test.js +96 -0
  30. package/src/views/_auth/login.ejs +92 -0
  31. package/src/views/layouts/application.ejs +144 -19
  32. package/src/views/layouts/header.ejs +49 -0
  33. package/tailwind.config.js +32 -1
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "main": "src/index.js",
5
5
  "scripts": {
6
- "dev": "bin/tailwind",
6
+ "dev": "npx tailwindcss -i ./src/styles/application.css -o ../../apps/admin/static/application.css --watch",
7
7
  "test": "jest"
8
8
  },
9
9
  "devDependencies": {
package/src/Adapter.js CHANGED
@@ -1,6 +1,11 @@
1
1
  const nodePath = require("node:path");
2
2
  import { existsSync } from "node:fs";
3
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";
4
9
 
5
10
  function escapeHtml(string) {
6
11
  var matchHtmlRegExp = /["'&<>]/;
@@ -66,12 +71,41 @@ const Logger = {
66
71
  },
67
72
  };
68
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
+
69
91
  export default class Adapter {
70
92
  constructor(router) {
71
93
  this.router = router;
72
94
  }
73
95
 
74
- createResource(_resource, _mapping) {
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) {
75
109
  throw new Error("Implement in an adapter");
76
110
  }
77
111
 
@@ -82,17 +116,28 @@ export default class Adapter {
82
116
  return existsSync(appPath) ? appPath : packagePath;
83
117
  }
84
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
+
85
126
  async renderLayout(assigns, body) {
86
127
  const layoutTemplate = `${nodePath.dirname(__filename)}/views/layouts/application.ejs`;
87
- return await ejs.renderFile(layoutTemplate, { ...assigns, body });
128
+ return await ejs.renderFile(layoutTemplate, {
129
+ ...assigns,
130
+ body,
131
+ });
88
132
  }
89
133
 
90
- async renderAction({ resource, action, assigns }) {
91
- const fullTemplatePath = this.getActionPath(resource, action);
134
+ async renderAction({ views, action, assigns }) {
135
+ const fullTemplatePath = this.getActionPath(views, action);
136
+ const renderAssigns = { ...this.getHelpers(), ...assigns };
92
137
 
93
138
  try {
94
- const rendered = await ejs.renderFile(fullTemplatePath, assigns);
95
- const fullPage = await this.renderLayout(assigns, rendered);
139
+ const rendered = await ejs.renderFile(fullTemplatePath, renderAssigns);
140
+ const fullPage = await this.renderLayout(renderAssigns, rendered);
96
141
 
97
142
  return fullPage;
98
143
  } catch (e) {
@@ -102,4 +147,14 @@ export default class Adapter {
102
147
  return renderError(error, e.stack);
103
148
  }
104
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
+ }
105
160
  }
@@ -1,13 +1,60 @@
1
1
  import Adapter from "./Adapter";
2
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
+ }
3
29
 
4
30
  export default class HonoAdapter extends Adapter {
5
31
  constructor(router) {
6
32
  super(router);
33
+ this.globalCtx = (ctx) => ctx;
7
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);
8
53
  }
9
54
 
10
55
  async createContext(router, next, verb, resource, action) {
56
+ const body = await parseBody(router.req);
57
+
11
58
  return new RaketaContext({
12
59
  router,
13
60
  res: next,
@@ -19,17 +66,21 @@ export default class HonoAdapter extends Adapter {
19
66
  url: router.req.url,
20
67
  action,
21
68
  params: router.req.param(),
22
- body: await router.req.parseBody(),
23
- query: router.req.query(),
69
+ body,
70
+ resourceParams: bodyToParams(router.req, body, this),
71
+ query: this.formatQuery(router.req.queries()),
24
72
  resource,
25
73
  assigns: {},
74
+ authentication: router.get("authentication"),
75
+ getGlobalCtx: this.globalCtx,
26
76
  });
27
77
  }
28
78
 
29
- createAction([
79
+ async createAction([
30
80
  verb,
31
81
  path,
32
82
  resource,
83
+ views,
33
84
  action,
34
85
  handler,
35
86
  before_filter,
@@ -37,73 +88,126 @@ export default class HonoAdapter extends Adapter {
37
88
  ]) {
38
89
  const method = this.getMethod(verb);
39
90
 
40
- method(path, async (router, next) => {
41
- const ctx = await this.createContext(
42
- router,
43
- next,
44
- verb,
45
- resource,
46
- action
47
- );
48
-
49
- console.info(
50
- `\x1b[35m[${ctx.method}] ${ctx.rawPath}\x1b[0m -> ${resource}/${action} | ${JSON.stringify(ctx.params)}`
51
- );
52
-
53
- const assigns = {
54
- beforeFilterAssigns: {},
55
- actionAssigns: {},
56
- afterFilterAssigns: {},
57
- };
58
-
59
- // Process before filters
60
- if (before_filter) {
61
- const { redirect, assigns: beforeFilterAssigns = {} } =
62
- before_filter(ctx) || {};
63
-
64
- if (redirect) return router.redirect(redirect);
65
-
66
- assigns.beforeFilterAssigns = beforeFilterAssigns;
67
- }
68
-
69
- // Process action
70
- const { redirect, assigns: actionAssigns = {} } = handler(ctx) || {};
71
- assigns.actionAssigns = actionAssigns;
72
-
73
- if (redirect) return router.redirect(redirect);
74
-
75
- // Process after filters
76
- if (after_filter) {
77
- const { redirect, assigns: afterFilterAssigns = {} } =
78
- after_filter(ctx) || {};
79
-
80
- if (redirect) return router.redirect(redirect);
81
-
82
- assigns.afterFilterAssigns = afterFilterAssigns;
83
- }
84
-
85
- const fullAssigns = {
86
- ...ctx.assigns,
87
- ...assigns.beforeFilterAssigns,
88
- ...assigns.actionAssigns,
89
- ...assigns.afterFilterAssigns,
90
- };
91
-
92
- const rendered = this.renderAction({
93
- resource,
94
- action,
95
- assigns: fullAssigns,
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);
96
194
  });
97
-
98
- return router.html(rendered);
99
195
  });
100
196
  }
101
197
 
102
- createResource(mapping) {
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) {
103
207
  mapping.forEach((handlerMap) => this.createAction(handlerMap));
104
208
  }
105
209
 
106
- createNestedResource(mapping) {
210
+ routeNestedMember(mapping) {
107
211
  mapping.forEach((handlerMap) => this.createAction(handlerMap));
108
212
  }
109
213
 
@@ -117,4 +221,28 @@ export default class HonoAdapter extends Adapter {
117
221
  }[verb] || this.router.get
118
222
  );
119
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
+ }
120
248
  }
@@ -12,8 +12,11 @@ export default class RaketaContext {
12
12
  params,
13
13
  query,
14
14
  body,
15
+ resourceParams,
15
16
  resource,
16
17
  assigns,
18
+ authentication,
19
+ getGlobalCtx,
17
20
  }) {
18
21
  this.router = router;
19
22
  this.res = res;
@@ -28,6 +31,16 @@ export default class RaketaContext {
28
31
  this.query = query;
29
32
  this.body = body;
30
33
  this.resource = resource;
34
+ this.resourceParams = resourceParams;
31
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
+ }
32
45
  }
33
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;