yaml-admin-api 0.0.3 → 0.0.8
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/README.md +243 -0
- package/package.json +11 -1
- package/src/common/util.js +58 -0
- package/src/crud/entity-api-generator.js +437 -0
- package/src/login/auth.js +24 -5
- package/src/upload/localUpload.js +44 -0
- package/src/upload/s3Upload.js +47 -0
- package/src/upload/upload-api-generator.js +248 -0
- package/src/yml-admin-api.js +44 -17
- package/src/crud/api-generator.js +0 -118
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
## yaml-admin-api
|
|
2
|
+
|
|
3
|
+
Build an admin API from a single YAML file. The library reads `admin.yml`, connects to MongoDB, and automatically registers login/auth and CRUD routes to your Express app. It also supports file upload (S3/local) and Excel import/export.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### Install
|
|
8
|
+
```bash
|
|
9
|
+
npm i yaml-admin-api
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Required environment variables
|
|
13
|
+
- `MONGODB_URL`: MongoDB connection string
|
|
14
|
+
- `JWT_SECRET`: JWT signing key
|
|
15
|
+
|
|
16
|
+
Placeholders `${MONGODB_URL}` and `${JWT_SECRET}` inside YAML are resolved at runtime from the environment.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Quick Start (Express)
|
|
21
|
+
```js
|
|
22
|
+
const express = require('express');
|
|
23
|
+
const bodyParser = require('body-parser');
|
|
24
|
+
const cookieParser = require('cookie-parser');
|
|
25
|
+
const { registerRoutes } = require('yaml-admin-api');
|
|
26
|
+
|
|
27
|
+
(async () => {
|
|
28
|
+
const app = express();
|
|
29
|
+
app.use(cookieParser());
|
|
30
|
+
app.use(bodyParser.urlencoded({ extended: true, limit: '30mb' }));
|
|
31
|
+
app.use(bodyParser.json({ limit: '30mb' }));
|
|
32
|
+
|
|
33
|
+
// Provide admin.yml path or a YAML string
|
|
34
|
+
await registerRoutes(app, { yamlPath: './admin.yml' });
|
|
35
|
+
|
|
36
|
+
app.listen(6911, () => console.log('API listening on 6911'));
|
|
37
|
+
})();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Options
|
|
41
|
+
```js
|
|
42
|
+
await registerRoutes(app, {
|
|
43
|
+
yamlPath: './admin.yml', // or yamlString: '<yml text>'
|
|
44
|
+
password: {
|
|
45
|
+
// Override password hashing for fields with type: password (default: sha512)
|
|
46
|
+
encrypt: (plain) => require('crypto').createHash('sha512').update(plain).digest('hex')
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### admin.yml example
|
|
54
|
+
```yaml
|
|
55
|
+
login:
|
|
56
|
+
jwt-secret: ${JWT_SECRET}
|
|
57
|
+
id-password:
|
|
58
|
+
entity: admin
|
|
59
|
+
id-field: email
|
|
60
|
+
password-field: pass
|
|
61
|
+
password-encoding: bcrypt
|
|
62
|
+
bcrypt-salt: 10
|
|
63
|
+
|
|
64
|
+
api-host:
|
|
65
|
+
uri: http://localhost:6911
|
|
66
|
+
web-host:
|
|
67
|
+
uri: http://localhost:6900
|
|
68
|
+
|
|
69
|
+
database:
|
|
70
|
+
mongodb:
|
|
71
|
+
uri: ${MONGODB_URL}
|
|
72
|
+
|
|
73
|
+
upload:
|
|
74
|
+
# For S3
|
|
75
|
+
# s3:
|
|
76
|
+
# access_key_id: ${S3_ACCESS_KEY_ID}
|
|
77
|
+
# secret_access_key: ${S3_SECRET_ACCESS_KEY}
|
|
78
|
+
# region: ${S3_REGION}
|
|
79
|
+
# bucket: ${S3_BUCKET}
|
|
80
|
+
# bucket_private: ${S3_BUCKET_PRIVATE}
|
|
81
|
+
# base_url: http://localhost:6911/upload
|
|
82
|
+
# base_url_private: http://localhost:6911/upload_private
|
|
83
|
+
# For local
|
|
84
|
+
local:
|
|
85
|
+
path: ./upload
|
|
86
|
+
path_private: ./upload_private
|
|
87
|
+
base_url: http://localhost:6911
|
|
88
|
+
|
|
89
|
+
entity:
|
|
90
|
+
member:
|
|
91
|
+
category: 'User'
|
|
92
|
+
label: 'User'
|
|
93
|
+
fields:
|
|
94
|
+
- { name: email, type: string, required: true }
|
|
95
|
+
- { name: pass, type: password, required: true }
|
|
96
|
+
- { name: name, type: string, required: true }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Key field rules
|
|
100
|
+
- Default key is `'_id'` (objectId).
|
|
101
|
+
- To use a custom key, mark the field with `key: true`.
|
|
102
|
+
- `type: integer` + `autogenerate: true` → auto-incrementing ID
|
|
103
|
+
- `type: string` + `autogenerate: true` → UUID
|
|
104
|
+
|
|
105
|
+
Search rules
|
|
106
|
+
- Only fields declared under `entity.<name>.crud.list.search` are searchable in the list API.
|
|
107
|
+
- When `exact: false`, a partial match (regex) is used; integers always compare exactly.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Auth and tokens
|
|
112
|
+
- A JWT is issued on successful login.
|
|
113
|
+
- All protected requests must include header `x-access-token: <JWT>` (query `?token=` and cookie are also accepted).
|
|
114
|
+
|
|
115
|
+
Login endpoints
|
|
116
|
+
```http
|
|
117
|
+
GET /member/login?email={email}&pass={pass}
|
|
118
|
+
POST /member/login { email, pass }
|
|
119
|
+
GET /member/islogin // token verification
|
|
120
|
+
GET /member/logout
|
|
121
|
+
```
|
|
122
|
+
Sample response
|
|
123
|
+
```json
|
|
124
|
+
{ "r": true, "token": "<JWT>", "member": { "id": "...", "email": "...", "name": "..." } }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Entity CRUD endpoints
|
|
130
|
+
All endpoints require authentication.
|
|
131
|
+
|
|
132
|
+
- List: `GET /<entity>`
|
|
133
|
+
- Paging: `_start`, `_end`
|
|
134
|
+
- Sort: `_sort`, `_order` (`ASC`|`DESC`)
|
|
135
|
+
- Field search: `?field=value` (see `crud.list.search` in the schema)
|
|
136
|
+
- Total count header: `X-Total-Count`
|
|
137
|
+
|
|
138
|
+
- Show: `GET /<entity>/:id`
|
|
139
|
+
- Create: `POST /<entity>`
|
|
140
|
+
- ID generation follows field definition (`key`, `type`, `autogenerate`).
|
|
141
|
+
- Response includes `id` (react-admin compatibility).
|
|
142
|
+
- Update: `PUT /<entity>/:id`
|
|
143
|
+
- Key field is not updated.
|
|
144
|
+
- Delete: `DELETE /<entity>/:id`
|
|
145
|
+
- Hard delete by default; customizable hook points exist.
|
|
146
|
+
|
|
147
|
+
Sensitive fields and media
|
|
148
|
+
- Fields with `type: password` are removed from responses and hashed on save.
|
|
149
|
+
- Fields with `type: image|mp4|file` include preview URLs in the response.
|
|
150
|
+
- Private files return secure, short-lived URLs.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### File upload
|
|
155
|
+
Depending on configuration, S3 or local upload APIs are enabled. All upload APIs require authentication.
|
|
156
|
+
|
|
157
|
+
Local upload
|
|
158
|
+
```http
|
|
159
|
+
PUT /api/local/media/upload?ext=jpg // public storage path
|
|
160
|
+
PUT /api/local/media/upload/secure?ext=jpg // private storage path
|
|
161
|
+
```
|
|
162
|
+
Response: `{ r: true, key }`
|
|
163
|
+
|
|
164
|
+
Secure download
|
|
165
|
+
- The server issues temporary URLs like `/local-secure-download?key=...&token=...`.
|
|
166
|
+
- The token is a short-lived (5 min) JWT; clients can use the link directly.
|
|
167
|
+
|
|
168
|
+
S3 upload (pre-signed)
|
|
169
|
+
```http
|
|
170
|
+
GET /api/media/url/put/:ext // public bucket
|
|
171
|
+
GET /api/media/url/secure/put/:ext // private bucket
|
|
172
|
+
POST /api/media/url/secure/init/:ext // multipart init (private)
|
|
173
|
+
POST /api/media/url/secure/part // request part URL
|
|
174
|
+
POST /api/media/url/secure/complete // complete multipart
|
|
175
|
+
POST /api/media/url/secure/abort // abort multipart
|
|
176
|
+
```
|
|
177
|
+
Response includes `upload_url`, storage `key`, etc.
|
|
178
|
+
|
|
179
|
+
Note: S3 usage requires `upload.s3.*` configuration.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Excel export / import
|
|
184
|
+
Declare in the schema to enable endpoints:
|
|
185
|
+
```yaml
|
|
186
|
+
entity:
|
|
187
|
+
member:
|
|
188
|
+
crud:
|
|
189
|
+
list:
|
|
190
|
+
export:
|
|
191
|
+
fields:
|
|
192
|
+
- { name: email }
|
|
193
|
+
- { name: name }
|
|
194
|
+
import:
|
|
195
|
+
fields:
|
|
196
|
+
- { name: email }
|
|
197
|
+
- { name: name }
|
|
198
|
+
upsert: true
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Endpoints
|
|
202
|
+
```http
|
|
203
|
+
POST /excel/<entity>/export // with body { filter }
|
|
204
|
+
POST /excel/<entity>/import // with body { base64 } (Excel file as Base64)
|
|
205
|
+
```
|
|
206
|
+
Export response
|
|
207
|
+
```json
|
|
208
|
+
{ "r": true, "url": "<secure-download-url>" }
|
|
209
|
+
```
|
|
210
|
+
Import response
|
|
211
|
+
```json
|
|
212
|
+
{ "r": true, "msg": "Import success - <n> rows effected" }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Serverless (Lambda) example
|
|
218
|
+
Wrap with `serverless-http` to deploy easily. See `example/api1`.
|
|
219
|
+
```js
|
|
220
|
+
'use strict';
|
|
221
|
+
const serverless = require('serverless-http');
|
|
222
|
+
|
|
223
|
+
exports.handler = async (event, context) => {
|
|
224
|
+
const app = await require('./app');
|
|
225
|
+
const handler = serverless(app);
|
|
226
|
+
return await handler(event, context);
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The sample `serverless.yml` uses region `ap-northeast-2` and runtime `nodejs20.x`.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### FAQ
|
|
235
|
+
- Auth fails: Ensure you send `x-access-token` header. Obtain it from `/member/login`.
|
|
236
|
+
- Need CORS headers: Add an application-level CORS middleware as shown in the example app.
|
|
237
|
+
- Media field has no URL: Response adds preview URLs (`..._preview`). Private files return signed URLs.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### License
|
|
242
|
+
MIT
|
|
243
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaml-admin-api",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "YAML Admin API package",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
"src"
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"publish": "npm publish -w yaml-admin-api --access public"
|
|
13
|
+
},
|
|
11
14
|
"dependencies": {
|
|
15
|
+
"@aws-sdk/client-s3": "^3.596.0",
|
|
16
|
+
"@aws-sdk/client-ses": "^3.855.0",
|
|
17
|
+
"@aws-sdk/s3-presigned-post": "^3.596.0",
|
|
18
|
+
"@aws-sdk/s3-request-presigner": "^3.596.0",
|
|
12
19
|
"bcryptjs": "^3.0.2",
|
|
13
20
|
"jsonwebtoken": "^9.0.2",
|
|
21
|
+
"moment": "^2.30.1",
|
|
14
22
|
"mongodb": "^6.18.0",
|
|
15
23
|
"request": "^2.88.2",
|
|
24
|
+
"uuid": "^11.1.0",
|
|
25
|
+
"xlsx": "^0.18.5",
|
|
16
26
|
"yaml": "^2.8.1"
|
|
17
27
|
}
|
|
18
28
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const genEntityId = async function(db){
|
|
2
|
+
|
|
3
|
+
let ret = await db.collection('counters').findOneAndUpdate(
|
|
4
|
+
{ _id: 'seq' },
|
|
5
|
+
{$inc:{seq:1}},
|
|
6
|
+
{
|
|
7
|
+
new :true,
|
|
8
|
+
upsert:true,
|
|
9
|
+
returnNewDocument:true
|
|
10
|
+
}
|
|
11
|
+
);
|
|
12
|
+
if(!ret?.seq){
|
|
13
|
+
ret = await db.collection('counters').findOneAndUpdate(
|
|
14
|
+
{ _id: 'seq' },
|
|
15
|
+
{$inc:{seq:1}},
|
|
16
|
+
{
|
|
17
|
+
new :true,
|
|
18
|
+
upsert:true,
|
|
19
|
+
returnNewDocument:true
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return ret.seq
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const genEntityIdWithKey = async function(db, key){
|
|
27
|
+
|
|
28
|
+
let ret = await db.collection('counters').findOneAndUpdate(
|
|
29
|
+
{ _id: key },
|
|
30
|
+
{$inc:{seq:1}},
|
|
31
|
+
{
|
|
32
|
+
new :true,
|
|
33
|
+
upsert:true,
|
|
34
|
+
returnNewDocument:true
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if(!ret?.seq){
|
|
39
|
+
ret = await db.collection('counters').findOneAndUpdate(
|
|
40
|
+
{ _id: key },
|
|
41
|
+
{$inc:{seq:1}},
|
|
42
|
+
{
|
|
43
|
+
new :true,
|
|
44
|
+
upsert:true,
|
|
45
|
+
returnNewDocument:true
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return ret.seq
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
genEntityId,
|
|
55
|
+
genEntityIdWithKey
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
const { withConfig } = require('../login/auth.js');
|
|
2
|
+
const { genEntityIdWithKey } = require('../common/util.js');
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
const { ObjectId } = require('mongodb');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const XLSX = require('xlsx');
|
|
7
|
+
const moment = require('moment');
|
|
8
|
+
const { withConfigLocal } = require('../upload/localUpload.js');
|
|
9
|
+
const { withConfigS3 } = require('../upload/s3Upload.js');
|
|
10
|
+
|
|
11
|
+
const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options }) => {
|
|
12
|
+
|
|
13
|
+
const auth = withConfig({ db, jwt_secret: yml.login["jwt-secret"] });
|
|
14
|
+
const api_host = yml["api-host"].uri;
|
|
15
|
+
let isS3 = yml.upload.s3
|
|
16
|
+
let host_image = isS3 ? yml.upload.s3.base_url : yml.upload.local.base_url
|
|
17
|
+
const uploader = yml.upload.s3 ? withConfigS3({
|
|
18
|
+
access_key_id: yml.upload.s3.access_key_id,
|
|
19
|
+
secret_access_key: yml.upload.s3.secret_access_key,
|
|
20
|
+
bucket: yml.upload.s3.bucket,
|
|
21
|
+
bucket_private: yml.upload.s3.bucket_private,
|
|
22
|
+
base_url: yml.upload.s3.base_url,
|
|
23
|
+
}) : withConfigLocal({
|
|
24
|
+
path: yml.upload.local.path,
|
|
25
|
+
path_private: yml.upload.local.path_private,
|
|
26
|
+
base_url: yml.upload.local.base_url,
|
|
27
|
+
api_host,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
let key_field = yml_entity.fields?.find(field => field.key)
|
|
31
|
+
if (!key_field) {
|
|
32
|
+
key_field = {
|
|
33
|
+
name: '_id',
|
|
34
|
+
type: 'objectId',
|
|
35
|
+
key: true,
|
|
36
|
+
autogenerate: true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const generateKey = async () => {
|
|
41
|
+
if (key_field.type == 'integer')
|
|
42
|
+
return await genEntityIdWithKey(db, key_field.name)
|
|
43
|
+
else if (key_field.type == 'string')
|
|
44
|
+
return uuidv4()
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getKeyFromEntity = (entity) => {
|
|
49
|
+
const keyValue = entity[key_field.name]
|
|
50
|
+
if (key_field.type == 'objectId' && keyValue)
|
|
51
|
+
return keyValue.toString()
|
|
52
|
+
return keyValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parseKey = (key) => {
|
|
56
|
+
if (key_field.type == 'integer')
|
|
57
|
+
return parseInt(key)
|
|
58
|
+
else if (key_field.type == 'string')
|
|
59
|
+
return key
|
|
60
|
+
else if (key_field.type == 'objectId')
|
|
61
|
+
return ObjectId.isValid(key) ? new ObjectId(key) : key
|
|
62
|
+
return key
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parseValueByType = (value, field) => {
|
|
66
|
+
const { type, reference_entity, reference_field } = field
|
|
67
|
+
if (type == 'reference') {
|
|
68
|
+
const referenceEntity = yml.entity[reference_entity]
|
|
69
|
+
const referenceField = referenceEntity.fields.find(f => f.name == reference_field)
|
|
70
|
+
console.log('referenceField', referenceField)
|
|
71
|
+
return parseValueByTypeCore(value, referenceField)
|
|
72
|
+
} else {
|
|
73
|
+
return parseValueByTypeCore(value, field)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const parseValueByTypeCore = (value, field) => {
|
|
77
|
+
const { type } = field
|
|
78
|
+
if (type == 'integer')
|
|
79
|
+
return parseInt(value)
|
|
80
|
+
else if (type == 'string')
|
|
81
|
+
return value
|
|
82
|
+
else if (type == 'objectId')
|
|
83
|
+
return ObjectId.isValid(value) ? new ObjectId(value) : value
|
|
84
|
+
return value
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const passwordEncrypt = (value) => {
|
|
88
|
+
if (options?.password?.encrypt) {
|
|
89
|
+
return options.password.encrypt(value)
|
|
90
|
+
} else {
|
|
91
|
+
return crypto.createHash('sha512').update(value).digest('hex')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const addInfo = async (db, list) => {
|
|
96
|
+
let passwordFields = yml_entity.fields.filter(f => f.type == 'password').map(f => f.name)
|
|
97
|
+
list.forEach(m => {
|
|
98
|
+
passwordFields.forEach(f => {
|
|
99
|
+
delete m[f]
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
let mediaFields = yml_entity.fields.filter(f => ['image', 'mp4', 'file'].includes(f.type))
|
|
104
|
+
for(let m of list) {
|
|
105
|
+
for(let field of mediaFields) {
|
|
106
|
+
m[field.name] = await mediaToFront(m[field.name], field.private)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const mediaKeyToFullUrl = async (key, private) => {
|
|
112
|
+
|
|
113
|
+
let url = key
|
|
114
|
+
if(url && !url.startsWith('http'))
|
|
115
|
+
url= host_image + '/' + url
|
|
116
|
+
|
|
117
|
+
if(private) {
|
|
118
|
+
url = await uploader.getUrlSecure(key, auth);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return url
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const mediaToFront = async (media, private) => {
|
|
125
|
+
if (media && typeof media == 'string') {
|
|
126
|
+
media= { src: url }
|
|
127
|
+
media.image_preview = await mediaKeyToFullUrl(url, private)
|
|
128
|
+
} else if (media && typeof media == 'object') {
|
|
129
|
+
let { image, video, src } = media
|
|
130
|
+
let url = image || src
|
|
131
|
+
media.image_preview = await mediaKeyToFullUrl(url, private)
|
|
132
|
+
if (video) {
|
|
133
|
+
media.video_preview = await mediaKeyToFullUrl(video, private)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return media
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
//list
|
|
141
|
+
app.get(`/${entity_name}`, auth.isAuthenticated, async (req, res) => {
|
|
142
|
+
var s = {};
|
|
143
|
+
var _sort = req.query._sort;
|
|
144
|
+
var _order = req.query._order;
|
|
145
|
+
if (_sort != null)
|
|
146
|
+
s[_sort] = (_order == 'ASC' ? 1 : -1);
|
|
147
|
+
|
|
148
|
+
var _end = req.query._end;
|
|
149
|
+
var _start = req.query._start;
|
|
150
|
+
var l = _end - _start;
|
|
151
|
+
|
|
152
|
+
//검색 파라미터
|
|
153
|
+
const f = {};
|
|
154
|
+
yml_entity.fields?.forEach(field => {
|
|
155
|
+
const q = req.query[field.name];
|
|
156
|
+
if (q) {
|
|
157
|
+
const search = yml_entity.crud?.list?.search?.find(m => m.name == field.name)
|
|
158
|
+
if (Array.isArray(q)) {
|
|
159
|
+
f[field.name] = { $in: q.map(v => parseValueByType(v, field)) };
|
|
160
|
+
} else {
|
|
161
|
+
if (search?.exact != false || field.type == 'integer')
|
|
162
|
+
f[field.name] = parseValueByType(q, field)
|
|
163
|
+
else
|
|
164
|
+
f[field.name] = { $regex: ".*" + q + ".*" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
console.log('f', f, req.query)
|
|
170
|
+
|
|
171
|
+
var name = req.query.name;
|
|
172
|
+
if (name == null && req.query.q)
|
|
173
|
+
name = req.query.q;
|
|
174
|
+
if (name != null)
|
|
175
|
+
f['name'] = { $regex: ".*" + name + ".*" };
|
|
176
|
+
f.remove = { $ne: true }
|
|
177
|
+
|
|
178
|
+
//Custom f list Start
|
|
179
|
+
|
|
180
|
+
//Custom f list End
|
|
181
|
+
|
|
182
|
+
const projection = (key_field.name == '_id' ? {} : { _id: false })
|
|
183
|
+
var count = await db.collection(entity_name).find(f).project(projection).sort(s).count();
|
|
184
|
+
let list = await db.collection(entity_name).find(f).project(projection).sort(s).skip(parseInt(_start)).limit(l).toArray()
|
|
185
|
+
list.map(m => {
|
|
186
|
+
m.id = getKeyFromEntity(m)
|
|
187
|
+
})
|
|
188
|
+
//Custom list Start
|
|
189
|
+
|
|
190
|
+
//Custom list End
|
|
191
|
+
await addInfo(db, list)
|
|
192
|
+
res.header('X-Total-Count', count);
|
|
193
|
+
res.json(list);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
const constructEntity = async (req, entityId) => {
|
|
198
|
+
var entity = {};
|
|
199
|
+
|
|
200
|
+
if (entityId)
|
|
201
|
+
entity[key_field.name] = entityId
|
|
202
|
+
|
|
203
|
+
yml_entity.fields.forEach(field => {
|
|
204
|
+
if (!field.key)
|
|
205
|
+
entity[field.name] = req.body[field.name]
|
|
206
|
+
})
|
|
207
|
+
entity['update_date'] = new Date()
|
|
208
|
+
|
|
209
|
+
let passwordFields = yml_entity.fields.filter(f => f.type == 'password').map(f => f.name)
|
|
210
|
+
passwordFields.forEach(f => {
|
|
211
|
+
entity[f] = passwordEncrypt(req.body[f])
|
|
212
|
+
})
|
|
213
|
+
//Custom ConstructEntity Start
|
|
214
|
+
|
|
215
|
+
//Custom ConstructEntity End
|
|
216
|
+
|
|
217
|
+
return entity;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
//create
|
|
221
|
+
app.post(`/${entity_name}`, auth.isAuthenticated, async (req, res) => {
|
|
222
|
+
let entityId
|
|
223
|
+
if (key_field.autogenerate)
|
|
224
|
+
entityId = await generateKey()
|
|
225
|
+
else
|
|
226
|
+
entityId = parseKey(req.body[key_field.name])
|
|
227
|
+
|
|
228
|
+
if (entityId) {
|
|
229
|
+
let f = {}
|
|
230
|
+
f[key_field.name] = entityId
|
|
231
|
+
let already = await db.collection(entity_name).findOne(f)
|
|
232
|
+
if (already)
|
|
233
|
+
return res.status(400).json({ status: 400, statusText: 'error', message: "duplicate key [" + entityId + "]" });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const entity = await constructEntity(req, entityId);
|
|
237
|
+
entity['update_date'] = entity['create_date'] = new Date()
|
|
238
|
+
entity['create_admin_id'] = req.user.id
|
|
239
|
+
|
|
240
|
+
//Custom Create Start
|
|
241
|
+
|
|
242
|
+
//Custom Create End
|
|
243
|
+
|
|
244
|
+
var r = await db.collection(entity_name).insertOne(entity);
|
|
245
|
+
//Custom Create Tail Start
|
|
246
|
+
|
|
247
|
+
//Custom Create Tail End
|
|
248
|
+
|
|
249
|
+
const generatedId = entityId || r.insertedId
|
|
250
|
+
entity.id = (key_field.type == 'objectId') ? generatedId?.toString() : generatedId;
|
|
251
|
+
|
|
252
|
+
res.json(entity);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
//edit
|
|
257
|
+
app.put(`/${entity_name}/:id`, auth.isAuthenticated, async (req, res) => {
|
|
258
|
+
let entityId = parseKey(req.params.id)
|
|
259
|
+
|
|
260
|
+
const entity = await constructEntity(req, entityId);
|
|
261
|
+
entity['update_date'] = new Date()
|
|
262
|
+
// Do not attempt to set the key field during update (immutable `_id` etc.)
|
|
263
|
+
if (entity[key_field.name] !== undefined)
|
|
264
|
+
delete entity[key_field.name]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
//Custom Create Start
|
|
268
|
+
|
|
269
|
+
//Custom Create End
|
|
270
|
+
|
|
271
|
+
let f = {}
|
|
272
|
+
f[key_field.name] = entityId
|
|
273
|
+
|
|
274
|
+
for(let field of yml_entity.fields) {
|
|
275
|
+
if(['mp4', 'image', 'file'].includes(field.type)) {
|
|
276
|
+
let a = entity[field.name]
|
|
277
|
+
delete a.image_preview
|
|
278
|
+
delete a.video_preview
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await db.collection(entity_name).updateOne(f, { $set: entity });
|
|
283
|
+
|
|
284
|
+
//Custom Create Tail Start
|
|
285
|
+
|
|
286
|
+
//Custom Create Tail End
|
|
287
|
+
|
|
288
|
+
// Ensure React-Admin receives an `id` in the response
|
|
289
|
+
entity.id = (key_field.type == 'objectId') ? entityId?.toString() : entityId
|
|
290
|
+
|
|
291
|
+
res.json(entity);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
//view
|
|
295
|
+
app.get(`/${entity_name}/:id`, auth.isAuthenticated, async (req, res) => {
|
|
296
|
+
let f = {}
|
|
297
|
+
f[key_field.name] = parseKey(req.params.id)
|
|
298
|
+
const m = await db.collection(entity_name).findOne(f);
|
|
299
|
+
if (!m)
|
|
300
|
+
return res.status(404).send('Not found');
|
|
301
|
+
|
|
302
|
+
m.id = getKeyFromEntity(m)
|
|
303
|
+
await addInfo(db, [m])
|
|
304
|
+
|
|
305
|
+
res.json(m);
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
//delete
|
|
309
|
+
app.delete(`/${entity_name}/:id`, auth.isAuthenticated, async function (req, res) {
|
|
310
|
+
let f = {}
|
|
311
|
+
f[key_field.name] = parseKey(req.params.id)
|
|
312
|
+
const entity = await db.collection(entity_name).findOne(f);
|
|
313
|
+
if (!entity)
|
|
314
|
+
return res.status(404).send('Not found');
|
|
315
|
+
|
|
316
|
+
entity.id = getKeyFromEntity(entity)
|
|
317
|
+
|
|
318
|
+
let customDelete = false
|
|
319
|
+
let softDelete = false
|
|
320
|
+
//Custom Delete Api Start
|
|
321
|
+
|
|
322
|
+
//Custom Delete Api End
|
|
323
|
+
|
|
324
|
+
if (customDelete)
|
|
325
|
+
;
|
|
326
|
+
else if (softDelete)
|
|
327
|
+
await db.collection(entity_name).updateOne(f, { $set: { remove: true } });
|
|
328
|
+
else
|
|
329
|
+
await db.collection(entity_name).deleteOne(f);
|
|
330
|
+
|
|
331
|
+
res.json(entity);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (yml_entity.crud?.list?.export) {
|
|
335
|
+
app.post(`/excel/${entity_name}/export`, auth.isAuthenticated, async (req, res) => {
|
|
336
|
+
const filename = `${entity_name}_`
|
|
337
|
+
const fields = yml_entity.crud.list.export.fields.map(field => ({
|
|
338
|
+
label: field.name,
|
|
339
|
+
value: field.name,
|
|
340
|
+
}))
|
|
341
|
+
//{ label: '상품', value: row => row.product_list?.map(m=>m.total_name).join(',') },
|
|
342
|
+
|
|
343
|
+
let f = req.body.filter || {}
|
|
344
|
+
const list = await db.collection(entity_name).find(f).project({
|
|
345
|
+
_id: false,
|
|
346
|
+
}).toArray();
|
|
347
|
+
|
|
348
|
+
if (list.length == 0)
|
|
349
|
+
return res.json({ r: false, msg: 'No Data' });
|
|
350
|
+
|
|
351
|
+
await addInfo(db, list)
|
|
352
|
+
|
|
353
|
+
const data = list.map(row => {
|
|
354
|
+
let obj = {};
|
|
355
|
+
fields.forEach(field => {
|
|
356
|
+
obj[field.label] = typeof field.value === 'function' ? field.value(row) : row[field.value];
|
|
357
|
+
});
|
|
358
|
+
return obj;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const worksheet = XLSX.utils.json_to_sheet(data);
|
|
362
|
+
const workbook = XLSX.utils.book_new();
|
|
363
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
364
|
+
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
|
365
|
+
|
|
366
|
+
const currentTime = moment().format('YYYYMMDD_HHmmss');
|
|
367
|
+
const key = `${filename}${currentTime}.xlsx`;
|
|
368
|
+
await uploader.uploadSecure(key, excelBuffer);
|
|
369
|
+
let url = await uploader.getUrlSecure(key, auth);
|
|
370
|
+
return res.json({ r: true, url });
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (yml_entity.crud?.list?.import) {
|
|
375
|
+
app.post(`/excel/${entity_name}/import`, auth.isAuthenticated, async (req, res) => {
|
|
376
|
+
const { base64 } = req.body
|
|
377
|
+
const buf = Buffer.from(base64, 'base64');
|
|
378
|
+
const workbook = XLSX.read(buf, { type: 'buffer' });
|
|
379
|
+
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
380
|
+
let list = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
|
381
|
+
//엑셀 첫번째 행 타이틀 데이터 제거
|
|
382
|
+
let header = list[0]
|
|
383
|
+
list.shift();
|
|
384
|
+
|
|
385
|
+
let upsert = yml_entity.crud.list.import.upsert || true
|
|
386
|
+
|
|
387
|
+
const fields = yml_entity.crud.list.import.fields.map(m => m)
|
|
388
|
+
fields.map(field => {
|
|
389
|
+
let original = yml_entity.fields.find(f => f.name == field.name)
|
|
390
|
+
field.type = original.type
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
let key_field = yml_entity.fields.find(f => f.key)
|
|
394
|
+
let bulk = []
|
|
395
|
+
list.map(m => {
|
|
396
|
+
let f = {}
|
|
397
|
+
|
|
398
|
+
let m_obj = {}
|
|
399
|
+
header.map((h, index) => {
|
|
400
|
+
m_obj[h] = m[index]
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
f[key_field.name] = getKeyFromEntity(m_obj)
|
|
404
|
+
if (!f[key_field.name])
|
|
405
|
+
return
|
|
406
|
+
let entity = {}
|
|
407
|
+
fields.forEach(field => {
|
|
408
|
+
if (field.type == 'integer')
|
|
409
|
+
entity[field.name] = parseInt(m_obj[field.name])
|
|
410
|
+
else if (field.type == 'password')
|
|
411
|
+
entity[field.name] = passwordEncrypt(m_obj[field.name] + '')
|
|
412
|
+
else
|
|
413
|
+
entity[field.name] = m_obj[field.name] + ''
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
bulk.push({
|
|
417
|
+
updateOne: {
|
|
418
|
+
filter: f,
|
|
419
|
+
update: { $set: entity },
|
|
420
|
+
upsert: upsert
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
let result = await db.collection('delivery').bulkWrite(bulk);
|
|
426
|
+
res.json({ r: true, msg: 'Import success - ' + result.upsertedCount + ' rows effected' });
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const generateEntityApi = async ({ app, db, entity_name, entity, yml, options }) => {
|
|
432
|
+
await generateCrud({ app, db, entity_name, yml_entity: entity, yml, options })
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = {
|
|
436
|
+
generateEntityApi
|
|
437
|
+
}
|
package/src/login/auth.js
CHANGED
|
@@ -12,7 +12,25 @@ const withConfig = (config) => {
|
|
|
12
12
|
});
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const genenrateShortToken = () => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
jwt.sign(
|
|
18
|
+
{},
|
|
19
|
+
jwt_secret,
|
|
20
|
+
{
|
|
21
|
+
expiresIn: '5m',
|
|
22
|
+
subject: 'shortToken'
|
|
23
|
+
}, (err, token) => {
|
|
24
|
+
if (err)
|
|
25
|
+
reject(err);
|
|
26
|
+
else
|
|
27
|
+
resolve(token);
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const authenticateSuccess = (req, res, user, next) => {
|
|
16
34
|
jwt.sign(
|
|
17
35
|
user,
|
|
18
36
|
jwt_secret,
|
|
@@ -30,15 +48,15 @@ const withConfig = (config) => {
|
|
|
30
48
|
);
|
|
31
49
|
};
|
|
32
50
|
|
|
33
|
-
const isAuthenticated =
|
|
34
|
-
const token = req.headers['x-access-token'] || req.query.token || req.cookies.token;
|
|
51
|
+
const isAuthenticated = (req, res, next) => {
|
|
35
52
|
|
|
53
|
+
const token = req.headers['x-access-token'] || req.query.token || req.cookies.token;
|
|
36
54
|
if (token == null)
|
|
37
|
-
res.json({ r: false, err: { code: 666 }, msg: '
|
|
55
|
+
res.json({ r: false, err: { code: 666 }, msg: 'No authentication' });
|
|
38
56
|
else
|
|
39
57
|
jwt.verify(token, jwt_secret, (err, decoded) => {
|
|
40
58
|
if (err) {
|
|
41
|
-
res.json({ r: false, err: { code: 666 }, msg: '
|
|
59
|
+
res.json({ r: false, err: { code: 666 }, msg: 'No authentication' });
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
44
62
|
req.user = decoded;
|
|
@@ -78,6 +96,7 @@ const withConfig = (config) => {
|
|
|
78
96
|
return {
|
|
79
97
|
isAuthenticated,
|
|
80
98
|
authenticate,
|
|
99
|
+
genenrateShortToken,
|
|
81
100
|
}
|
|
82
101
|
}
|
|
83
102
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const moment = require('moment')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
|
|
4
|
+
const getUrl = async (Key) => {
|
|
5
|
+
//console.log('getSignedUrl')
|
|
6
|
+
let r = await s3.getSignedUrl('getObject', {
|
|
7
|
+
Bucket:aws_bucket_private,
|
|
8
|
+
Key,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
//console.log(r)
|
|
12
|
+
return r
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const withConfigLocal = ({path, path_private, base_url, api_host}) => {
|
|
16
|
+
|
|
17
|
+
const upload = async (key, stream) => {
|
|
18
|
+
return await fs.writeFileSync(path + '/' + key, stream)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const uploadSecure = async (key, stream) => {
|
|
22
|
+
return await fs.writeFileSync(path_private + '/' + key, stream)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getUrlSecure = async (Key, auth) => {
|
|
26
|
+
let r = `${api_host}/local-secure-download?key=${Key}`
|
|
27
|
+
let shortToken = await auth.genenrateShortToken()
|
|
28
|
+
r += `&token=${shortToken}`
|
|
29
|
+
return r
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
upload,
|
|
34
|
+
uploadSecure,
|
|
35
|
+
getUrl: async (key) => {
|
|
36
|
+
return await getUrl(key)
|
|
37
|
+
},
|
|
38
|
+
getUrlSecure,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
withConfigLocal
|
|
44
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const moment = require('moment')
|
|
2
|
+
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3')
|
|
3
|
+
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const upload = async (key, stream) => {
|
|
7
|
+
if(!key){
|
|
8
|
+
throw new Error('필수값이 없습니다')
|
|
9
|
+
}
|
|
10
|
+
//console.log('uploadExcel')
|
|
11
|
+
return await s3.upload({
|
|
12
|
+
Bucket: aws_bucket_private,
|
|
13
|
+
Key: key,
|
|
14
|
+
Body: stream,
|
|
15
|
+
ACL: 'private',
|
|
16
|
+
Expires: moment().add(10, 'minute').toISOString(),
|
|
17
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
18
|
+
}).promise()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const getUrl = async (Key) => {
|
|
22
|
+
//console.log('getSignedUrl')
|
|
23
|
+
let r = await s3.getSignedUrl('getObject', {
|
|
24
|
+
Bucket:aws_bucket_private,
|
|
25
|
+
Key,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
//console.log(r)
|
|
29
|
+
return r
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const getSecureUrl = async (Key) => {
|
|
33
|
+
//console.log('getSignedUrl')
|
|
34
|
+
let r = await s3.getSignedUrl('getObject', {
|
|
35
|
+
Bucket:aws_bucket_private,
|
|
36
|
+
Key,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
//console.log(r)
|
|
40
|
+
return r
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
upload,
|
|
45
|
+
getUrl,
|
|
46
|
+
getSecureUrl,
|
|
47
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const { withConfig } = require('../login/auth.js');
|
|
2
|
+
const { PutObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand } = require('@aws-sdk/client-s3')
|
|
3
|
+
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
|
|
4
|
+
const { genEntityIdWithKey } = require('../common/util.js');
|
|
5
|
+
const { S3Client } = require('@aws-sdk/client-s3')
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
|
|
8
|
+
const getContentType = (ext) => {
|
|
9
|
+
let contentType = 'image/jpeg'
|
|
10
|
+
if(ext == 'mp4')
|
|
11
|
+
contentType = 'video/mp4'
|
|
12
|
+
else if(ext == 'mov')
|
|
13
|
+
contentType = 'video/quicktime'
|
|
14
|
+
else if(ext == 'png')
|
|
15
|
+
contentType = 'image/png'
|
|
16
|
+
return contentType
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const generateS3UploadApi = async ({ app, db, yml, options }) => {
|
|
21
|
+
const auth = withConfig({ db, jwt_secret: yml.login["jwt-secret"] });
|
|
22
|
+
const { region, access_key_id, secret_access_key, bucket, bucket_private } = yml.upload.s3;
|
|
23
|
+
const getS3 = () => {
|
|
24
|
+
let s3 = new S3Client({
|
|
25
|
+
region: region,
|
|
26
|
+
credentials: {
|
|
27
|
+
accessKeyId: access_key_id,
|
|
28
|
+
secretAccessKey: secret_access_key
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
return s3
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
app.get('/api/media/url/put/:ext', auth.isAuthenticated, async function(req, res){
|
|
35
|
+
let s3 = getS3()
|
|
36
|
+
let {member_no} = req.user;
|
|
37
|
+
let fileName = await genEntityIdWithKey(db, 'file');
|
|
38
|
+
let ext = req.params.ext;
|
|
39
|
+
|
|
40
|
+
let contentType = getContentType(ext)
|
|
41
|
+
let key = `media/${member_no}/${fileName}.${ext}`
|
|
42
|
+
const uploadUrl = await getSignedUrl(s3, new PutObjectCommand({Bucket: aws_bucket_image,
|
|
43
|
+
ContentType: contentType,
|
|
44
|
+
Key: key}), { expiresIn: 300 });
|
|
45
|
+
|
|
46
|
+
let r = {upload_url:uploadUrl, key, fileName:`${fileName}.${ext}`, member_no, contentType}
|
|
47
|
+
console.log(r)
|
|
48
|
+
res.json(r);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.get('/api/media/url/secure/put/:ext', auth.isAuthenticated, async function(req, res){
|
|
52
|
+
let s3 = getS3()
|
|
53
|
+
let {member_no} = req.user;
|
|
54
|
+
let fileName = await genEntityIdWithKey(db, 'file');
|
|
55
|
+
let ext = req.params.ext;
|
|
56
|
+
|
|
57
|
+
let contentType = getContentType(ext)
|
|
58
|
+
let key = `media/${member_no}/${fileName}.${ext}`
|
|
59
|
+
const uploadUrl = await getSignedUrl(s3, new PutObjectCommand({Bucket: aws_bucket_private,
|
|
60
|
+
ContentType: contentType,
|
|
61
|
+
Key: key}), { expiresIn: 300 });
|
|
62
|
+
|
|
63
|
+
let r = {upload_url:uploadUrl, key, fileName:`${fileName}.${ext}`, member_no, contentType}
|
|
64
|
+
|
|
65
|
+
res.json(r);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// request uploadId
|
|
69
|
+
app.get('/api/media/url/secure/init/:ext', auth.isAuthenticated, async function (req, res) {
|
|
70
|
+
let s3 = getS3();
|
|
71
|
+
let member_no = req.user.member_no || req.user.id;
|
|
72
|
+
let fileName = await genEntityIdWithKey(db, 'file');
|
|
73
|
+
let ext = req.params.ext;
|
|
74
|
+
|
|
75
|
+
let key = `media/${member_no}/${fileName}.${ext}`;
|
|
76
|
+
let contentType = getContentType(ext);
|
|
77
|
+
|
|
78
|
+
const createMultipartUpload = await s3.send(new CreateMultipartUploadCommand({
|
|
79
|
+
Bucket: aws_bucket_private,
|
|
80
|
+
Key: key,
|
|
81
|
+
ContentType: contentType
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
res.json({ uploadId: createMultipartUpload.UploadId, key, contentType });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// request presigned url
|
|
88
|
+
app.post('/api/media/url/secure/part', auth.isAuthenticated, async function (req, res) {
|
|
89
|
+
let s3 = getS3();
|
|
90
|
+
let { key, uploadId, partNumber } = req.body;
|
|
91
|
+
|
|
92
|
+
const command = new UploadPartCommand({
|
|
93
|
+
Bucket: aws_bucket_private,
|
|
94
|
+
Key: key,
|
|
95
|
+
UploadId: uploadId,
|
|
96
|
+
PartNumber: partNumber
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300*10 });
|
|
100
|
+
|
|
101
|
+
res.json({ uploadUrl });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// merge file
|
|
105
|
+
app.post('/api/media/url/secure/complete', auth.isAuthenticated, async function (req, res) {
|
|
106
|
+
let s3 = getS3();
|
|
107
|
+
let { key, uploadId, parts } = req.body;
|
|
108
|
+
|
|
109
|
+
parts.sort((a, b) => a.PartNumber - b.PartNumber);
|
|
110
|
+
|
|
111
|
+
await s3.send(new CompleteMultipartUploadCommand({
|
|
112
|
+
Bucket: aws_bucket_private,
|
|
113
|
+
Key: key,
|
|
114
|
+
UploadId: uploadId,
|
|
115
|
+
MultipartUpload: { Parts: parts }
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
res.json({ key });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// 청크파일 삭제
|
|
122
|
+
app.post('/api/media/url/secure/abort', auth.isAuthenticated, async (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const { key, uploadId } = req.body;
|
|
125
|
+
const s3 = getS3();
|
|
126
|
+
|
|
127
|
+
await s3.send(new AbortMultipartUploadCommand({
|
|
128
|
+
Bucket: aws_bucket_private,
|
|
129
|
+
Key: key,
|
|
130
|
+
UploadId: uploadId
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
res.json({ r: true });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.log(error);
|
|
136
|
+
res.status(500).json({ r: false });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.post('/api/media/url/put', auth.isAuthenticated, async function(req, res) {
|
|
141
|
+
let s3 = getS3()
|
|
142
|
+
let {member_no} = req.user
|
|
143
|
+
const {ext_list} = req.body
|
|
144
|
+
|
|
145
|
+
let r = {list:[], r:true}
|
|
146
|
+
for(let ext of ext_list) {
|
|
147
|
+
let fileName = await genEntityIdWithKey(db, 'file')
|
|
148
|
+
let key = `media/${member_no}/${fileName}.${ext}`
|
|
149
|
+
let contentType = getContentType(ext)
|
|
150
|
+
const upload_url = await getSignedUrl(s3, new PutObjectCommand({Bucket: aws_bucket_image,
|
|
151
|
+
ContentType: contentType,
|
|
152
|
+
Key: key}), { expiresIn: 300 });
|
|
153
|
+
|
|
154
|
+
r.list.push({upload_url, key, fileName:`${fileName}.${ext}`, member_no, contentType})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
res.json(r);
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
app.post('/api/media/url/secure/put', auth.isAuthenticated, async function(req, res) {
|
|
161
|
+
let s3 = getS3()
|
|
162
|
+
let {member_no} = req.user
|
|
163
|
+
const {ext_list} = req.body
|
|
164
|
+
|
|
165
|
+
let r = {list:[], r:true}
|
|
166
|
+
for(let ext of ext_list) {
|
|
167
|
+
let fileName = await genEntityIdWithKey(db, 'file')
|
|
168
|
+
let key = `media/${member_no}/${fileName}.${ext}`
|
|
169
|
+
let contentType = getContentType(ext)
|
|
170
|
+
const upload_url = await getSignedUrl(s3, new PutObjectCommand({Bucket: aws_bucket_private,
|
|
171
|
+
ContentType: contentType,
|
|
172
|
+
Key: key}), { expiresIn: 300 });
|
|
173
|
+
|
|
174
|
+
r.list.push({upload_url, key, fileName:`${fileName}.${ext}`, member_no, contentType})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
res.json(r);
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const generateLocalUploadApi = async ({ app, db, yml, options }) => {
|
|
182
|
+
const auth = withConfig({ db, jwt_secret: yml.login["jwt-secret"] });
|
|
183
|
+
const { path, path_private } = yml.upload.local;
|
|
184
|
+
|
|
185
|
+
// Accept raw binary for local upload and stream to disk
|
|
186
|
+
app.put('/api/local/media/upload', auth.isAuthenticated, async function(req, res) {
|
|
187
|
+
let member_no = req.user.member_no || req.user.id;
|
|
188
|
+
let {ext, name} = req.query
|
|
189
|
+
let fileName = await genEntityIdWithKey(db, 'file')
|
|
190
|
+
let key = `media/${member_no}/${fileName}.${ext}`
|
|
191
|
+
|
|
192
|
+
// Ensure directory exists
|
|
193
|
+
const fullPath = `${path}/${key}`;
|
|
194
|
+
fs.mkdirSync(require('path').dirname(fullPath), { recursive: true });
|
|
195
|
+
|
|
196
|
+
const writeStream = fs.createWriteStream(fullPath);
|
|
197
|
+
req.pipe(writeStream);
|
|
198
|
+
writeStream.on('finish', () => {
|
|
199
|
+
res.json({ r: true, key })
|
|
200
|
+
});
|
|
201
|
+
writeStream.on('error', (err) => {
|
|
202
|
+
console.error(err);
|
|
203
|
+
res.status(500).json({ r: false, msg:err.message })
|
|
204
|
+
});
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
app.put('/api/local/media/upload/secure', auth.isAuthenticated, async function(req, res) {
|
|
208
|
+
let member_no = req.user.member_no || req.user.id;
|
|
209
|
+
let {ext, name} = req.query
|
|
210
|
+
|
|
211
|
+
let fileName = await genEntityIdWithKey(db, 'file')
|
|
212
|
+
let key = `media/${member_no}/${fileName}.${ext}`
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Ensure directory exists
|
|
216
|
+
const fullPath = `${path_private}/${key}`;
|
|
217
|
+
fs.mkdirSync(require('path').dirname(fullPath), { recursive: true });
|
|
218
|
+
|
|
219
|
+
const writeStream = fs.createWriteStream(fullPath);
|
|
220
|
+
req.pipe(writeStream);
|
|
221
|
+
writeStream.on('finish', () => {
|
|
222
|
+
res.json({ r: true, key })
|
|
223
|
+
});
|
|
224
|
+
writeStream.on('error', (err) => {
|
|
225
|
+
console.error(err);
|
|
226
|
+
res.status(500).json({ r: false, msg:err.message })
|
|
227
|
+
});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error(e)
|
|
230
|
+
res.status(500).json({ r:false })
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const generateUploadApi = async ({ app, db, yml, options }) => {
|
|
236
|
+
if(yml.upload.s3) {
|
|
237
|
+
await generateS3UploadApi({ app, db, yml, options })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if(yml.upload.local) {
|
|
241
|
+
await generateLocalUploadApi({ app, db, yml, options })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
generateUploadApi
|
|
248
|
+
}
|
package/src/yml-admin-api.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
const { MongoClient } = require('mongodb');
|
|
2
2
|
const fs = require('fs').promises;
|
|
3
3
|
const yaml = require('yaml');
|
|
4
|
-
const { generateEntityApi } = require('./crud/api-generator');
|
|
4
|
+
const { generateEntityApi } = require('./crud/entity-api-generator');
|
|
5
5
|
const { generateLoginApi } = require('./crud/login-api-generator');
|
|
6
|
+
const { withConfig } = require('./login/auth.js');
|
|
7
|
+
const { generateUploadApi } = require('./upload/upload-api-generator');
|
|
8
|
+
|
|
9
|
+
const changeEnv = (yamlString, env = {}) => {
|
|
10
|
+
if (!yamlString) return yamlString;
|
|
11
|
+
const mergedEnv = { ...process.env, ...env };
|
|
12
|
+
return yamlString.replace(/\$\{([A-Z0-9_\.\-]+)\}/g, (match, varName) => {
|
|
13
|
+
console.log('env', varName, mergedEnv[varName]);
|
|
14
|
+
const value = mergedEnv[varName];
|
|
15
|
+
return value !== undefined && value !== null ? String(value) : match;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
async function registerRoutes(app, options = {}) {
|
|
8
|
-
const { yamlPath, yamlString } = options;
|
|
20
|
+
const { yamlPath, yamlString, env } = options;
|
|
9
21
|
let yml;
|
|
10
22
|
if(yamlPath) {
|
|
11
|
-
yml = await readYml(yamlPath);
|
|
23
|
+
yml = await readYml(yamlPath, env);
|
|
12
24
|
} else if(yamlString) {
|
|
13
|
-
|
|
25
|
+
const replaced = changeEnv(yamlString, env);
|
|
26
|
+
yml = yaml.parse(replaced);
|
|
14
27
|
} else {
|
|
15
28
|
let yamlString = await fs.readFile('./admin.yml', 'utf8');
|
|
16
29
|
if(!yamlString) {
|
|
17
30
|
throw new Error('admin.yml is not found. yamlPath or yamlString is required.')
|
|
18
31
|
}
|
|
19
|
-
|
|
20
|
-
yamlString = yamlString
|
|
32
|
+
|
|
33
|
+
yamlString = changeEnv(yamlString, env);
|
|
34
|
+
|
|
21
35
|
yml = yaml.parse(yamlString);
|
|
22
36
|
}
|
|
23
37
|
|
|
@@ -31,27 +45,40 @@ async function registerRoutes(app, options = {}) {
|
|
|
31
45
|
console.log('db connected')
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
|
-
const jwt_secret = yml.login["jwt-secret"]
|
|
35
|
-
app.set('jwt-secret', jwt_secret);
|
|
36
48
|
|
|
37
49
|
await generateLoginApi(app, db, yml)
|
|
38
|
-
entity && Object.keys(entity).forEach(async (
|
|
50
|
+
entity && Object.keys(entity).forEach(async (entity_name) => {
|
|
39
51
|
await generateEntityApi({
|
|
40
52
|
app, db,
|
|
41
|
-
|
|
42
|
-
entity:entity[
|
|
43
|
-
|
|
53
|
+
entity_name,
|
|
54
|
+
entity:entity[entity_name],
|
|
55
|
+
yml,
|
|
56
|
+
options,
|
|
44
57
|
})
|
|
45
58
|
})
|
|
59
|
+
|
|
60
|
+
await generateUploadApi({
|
|
61
|
+
app, db,
|
|
62
|
+
yml,
|
|
63
|
+
options,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
//local secure download api
|
|
67
|
+
const auth = withConfig({ db, jwt_secret: yml.login["jwt-secret"] });
|
|
68
|
+
app.get('/local-secure-download', auth.isAuthenticated, async (req, res) => {
|
|
69
|
+
const {key} = req.query;
|
|
70
|
+
const a = `${yml.upload.local.path_private}/${key}`
|
|
71
|
+
const file = await fs.readFile(a)
|
|
72
|
+
res.setHeader('Content-Disposition', `attachment; filename=${key}`);
|
|
73
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
74
|
+
res.send(file);
|
|
75
|
+
})
|
|
46
76
|
}
|
|
47
77
|
|
|
48
|
-
async function readYml(path) {
|
|
78
|
+
async function readYml(path, env = {}) {
|
|
49
79
|
let yml = await fs.readFile(path, 'utf8');
|
|
50
|
-
yml = yml
|
|
51
|
-
yml = yml.replace('${MONGODB_URL}', process.env.MONGODB_URL);
|
|
52
|
-
|
|
80
|
+
yml = changeEnv(yml, env);
|
|
53
81
|
return yaml.parse(yml);
|
|
54
|
-
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
module.exports = registerRoutes;
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
const isAuthenticated = (jwt_secret) => {
|
|
2
|
-
return async (req, res, next) => {
|
|
3
|
-
const token = req.headers['x-access-token'] || req.query.token || req.cookies.token;
|
|
4
|
-
if (token == null)
|
|
5
|
-
res.json({ r: false, err: { code: 666 }, msg: '로그인 필요' });
|
|
6
|
-
else {
|
|
7
|
-
jwt.verify(token, jwt_secret, (err, decoded) => {
|
|
8
|
-
if (err) {
|
|
9
|
-
res.json({ r: false, err: { code: 666 }, msg: '로그인 필요' });
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
req.user = decoded;
|
|
13
|
-
delete req.user.password;
|
|
14
|
-
next();
|
|
15
|
-
})
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const generateList = async ({app, db, name, entity, jwt_secret}) => {
|
|
21
|
-
//list
|
|
22
|
-
app.get(`/${name}`, isAuthenticated(jwt_secret), async function (req, res) {
|
|
23
|
-
var s = {};
|
|
24
|
-
var _sort = req.query._sort;
|
|
25
|
-
var _order = req.query._order;
|
|
26
|
-
if (_sort != null)
|
|
27
|
-
s[_sort] = (_order == 'ASC' ? 1 : -1);
|
|
28
|
-
|
|
29
|
-
var _end = req.query._end;
|
|
30
|
-
var _start = req.query._start;
|
|
31
|
-
var l = _end - _start;
|
|
32
|
-
|
|
33
|
-
//검색 파라미터
|
|
34
|
-
var f = {};
|
|
35
|
-
var id = req.query.id;
|
|
36
|
-
if (id) {
|
|
37
|
-
if (Array.isArray(id))
|
|
38
|
-
f['id'] = { $in: id.map(m => parseInt(m)) };
|
|
39
|
-
else
|
|
40
|
-
f['id'] = parseInt(id);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ${
|
|
44
|
-
// entity.property.filter(f=>f.search && f.type != 'divider').map(m=>{
|
|
45
|
-
// if(m.type == 'bool') {
|
|
46
|
-
// return `
|
|
47
|
-
// if (req.query.${m.name})
|
|
48
|
-
// f['${m.name}'] = req.query.${m.name} == 'true'
|
|
49
|
-
// `
|
|
50
|
-
// } else if(m.type == 'text') {
|
|
51
|
-
// return `
|
|
52
|
-
// if (req.query.${m.name})
|
|
53
|
-
// f['${m.name}'] = { $regex: ".*" + req.query.${m.name} + ".*" }
|
|
54
|
-
// `
|
|
55
|
-
// } else if(m.type == 'reference') {
|
|
56
|
-
// //reference의 키는 integer인지 string인지 모른다. 그리고 member_no같이 string으로 취급되어야할 integer도 있다
|
|
57
|
-
// return `
|
|
58
|
-
// if (req.query.${m.name})
|
|
59
|
-
// f['${m.name}'] = isNaN(Number(req.query.${m.name}))|| parseInt(req.query.${m.name}) > 1569340492095?req.query.${m.name}:parseInt(req.query.${m.name})
|
|
60
|
-
// `
|
|
61
|
-
// } else if(m.type == 'date') {
|
|
62
|
-
// return `
|
|
63
|
-
// if (req.query.date) {
|
|
64
|
-
// if(req.query.date.replace(/-/g, '').length == 8) {
|
|
65
|
-
// //해당 날짜의 시작Date와 끝Date로 검색
|
|
66
|
-
// f['date'] = {
|
|
67
|
-
// $gte: moment(req.query.date).startOf('day').toDate(),
|
|
68
|
-
// $lte: moment(req.query.date).endOf('day').toDate()
|
|
69
|
-
// }
|
|
70
|
-
// }
|
|
71
|
-
// }
|
|
72
|
-
// `
|
|
73
|
-
// } else {
|
|
74
|
-
// return `
|
|
75
|
-
// if (req.query.${m.name})
|
|
76
|
-
// f['${m.name}'] = req.query.${m.name}
|
|
77
|
-
// `
|
|
78
|
-
// }
|
|
79
|
-
|
|
80
|
-
// }).join('\n')
|
|
81
|
-
// }
|
|
82
|
-
|
|
83
|
-
var name = req.query.name;
|
|
84
|
-
if (name == null && req.query.q)
|
|
85
|
-
name = req.query.q;
|
|
86
|
-
if (name != null)
|
|
87
|
-
f['name'] = { $regex: ".*" + name + ".*" };
|
|
88
|
-
f.remove = { $ne: true }
|
|
89
|
-
|
|
90
|
-
//Custom f list Start
|
|
91
|
-
|
|
92
|
-
//Custom f list End
|
|
93
|
-
|
|
94
|
-
var count = await db.collection('${name}').find(f).project({ _id: false }).sort(s).count();
|
|
95
|
-
let list = await db.collection('${name}').find(f).project({ _id: false }).sort(s).skip(parseInt(_start)).limit(l).toArray()
|
|
96
|
-
console.log('entity', entity)
|
|
97
|
-
list.map(m => {
|
|
98
|
-
m.id = entity.key
|
|
99
|
-
})
|
|
100
|
-
//Custom list Start
|
|
101
|
-
|
|
102
|
-
//Custom list End
|
|
103
|
-
await addInfo(db, list)
|
|
104
|
-
res.header('X-Total-Count', count);
|
|
105
|
-
res.json(list);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const generateEntityApi = async ({app, db, name, entity, jwt_secret}) => {
|
|
110
|
-
const { fields } = entity;
|
|
111
|
-
console.log('generateEntityApi', name)
|
|
112
|
-
|
|
113
|
-
await generateList({app, db, name, entity, jwt_secret})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = {
|
|
117
|
-
generateEntityApi
|
|
118
|
-
}
|