@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.
- package/package.json +2 -2
- package/src/Adapter.js +61 -6
- package/src/HonoAdapter.js +191 -63
- package/src/RaketaContext.js +13 -0
- package/src/helpers/AuthHelper.js +44 -0
- package/src/helpers/FormBuilder.js +226 -0
- package/src/helpers/HTMLBuilder.js +38 -0
- package/src/helpers/Pagination.js +92 -0
- package/src/helpers/SortButton.js +102 -0
- package/src/helpers/form_for.js +6 -0
- package/src/helpers/pagination_helper.js +5 -0
- package/src/helpers/sort_button.js +5 -0
- package/src/index.js +6 -2
- package/src/{createResource.js → routeCollection.js} +13 -6
- package/src/routeMember.js +40 -0
- package/src/{createNestedResource.js → routeNestedCollection.js} +12 -5
- package/src/routeNestedMember.js +40 -0
- package/src/setGlobalConfig.js +5 -0
- package/src/setGlobalContext.js +5 -0
- package/src/setupAuthentication.js +99 -0
- package/src/styles/application.css +465 -3
- package/src/tests/Adapter.test.js +39 -0
- package/src/tests/HonoAdapter.test.js +1 -0
- package/src/tests/helpers/Pagination.test.js +65 -0
- package/src/tests/helpers/SortButton.test.js +85 -0
- package/src/tests/{createResource.test.js → routeCollection.test.js} +51 -16
- package/src/tests/routeMember.test.js +91 -0
- package/src/tests/{createNestedResource.test.js → routeNestedCollection.test.js} +36 -10
- package/src/tests/routeNestedMember.test.js +96 -0
- package/src/views/_auth/login.ejs +92 -0
- package/src/views/layouts/application.ejs +144 -19
- package/src/views/layouts/header.ejs +49 -0
- 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.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "
|
|
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
|
-
|
|
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, {
|
|
128
|
+
return await ejs.renderFile(layoutTemplate, {
|
|
129
|
+
...assigns,
|
|
130
|
+
body,
|
|
131
|
+
});
|
|
88
132
|
}
|
|
89
133
|
|
|
90
|
-
async renderAction({
|
|
91
|
-
const fullTemplatePath = this.getActionPath(
|
|
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,
|
|
95
|
-
const fullPage = await this.renderLayout(
|
|
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
|
}
|
package/src/HonoAdapter.js
CHANGED
|
@@ -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
|
|
23
|
-
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
action
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/RaketaContext.js
CHANGED
|
@@ -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;
|