@es-labs/jslib 0.0.1
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/CHANGELOG.md +4 -0
- package/README.md +42 -0
- package/__test__/services.test.js +32 -0
- package/auth/index.js +226 -0
- package/auth/keyv.js +23 -0
- package/auth/knex.js +29 -0
- package/auth/redis.js +23 -0
- package/comms/email.js +123 -0
- package/comms/nexmo.js +44 -0
- package/comms/telegram.js +43 -0
- package/comms/telegram2/inbound.js +314 -0
- package/comms/telegram2/outbound.js +574 -0
- package/comms/webpush.js +60 -0
- package/config.js +37 -0
- package/express/controller/auth/oauth.js +39 -0
- package/express/controller/auth/oidc.js +87 -0
- package/express/controller/auth/own.js +100 -0
- package/express/controller/auth/saml.js +74 -0
- package/express/upload.js +48 -0
- package/index.js +1 -0
- package/iso/README.md +4 -0
- package/iso/__tests__/csv-utils.spec.js +128 -0
- package/iso/__tests__/datetime.spec.js +101 -0
- package/iso/__tests__/fetch.spec.js +270 -0
- package/iso/csv-utils.js +206 -0
- package/iso/datetime.js +103 -0
- package/iso/fetch.js +129 -0
- package/iso/fetch2.js +180 -0
- package/iso/log-filter.js +17 -0
- package/iso/sleep.js +6 -0
- package/iso/ws.js +63 -0
- package/node/oss-files/oss-uploader-client-fetch.js +258 -0
- package/node/oss-files/oss-uploader-client-fetch.md +31 -0
- package/node/oss-files/oss-uploader-client.js +219 -0
- package/node/oss-files/oss-uploader-server.js +199 -0
- package/node/oss-files/oss-uploader-usage.js +121 -0
- package/node/oss-files/oss-uploader-usage.md +34 -0
- package/node/oss-files/s3-uploader-client.js +217 -0
- package/node/oss-files/s3-uploader-server.js +123 -0
- package/node/oss-files/s3-uploader-usage.js +77 -0
- package/node/oss-files/s3-uploader-usage.md +34 -0
- package/package.json +53 -0
- package/packageInfo.js +9 -0
- package/services/ali.js +279 -0
- package/services/aws.js +194 -0
- package/services/db/__tests__/keyv.spec.js +31 -0
- package/services/db/keyv.js +14 -0
- package/services/db/knex.js +67 -0
- package/services/db/redis.js +51 -0
- package/services/index.js +57 -0
- package/services/mq/README.md +8 -0
- package/services/websocket.js +139 -0
- package/t4t/README.md +1 -0
- package/traps.js +20 -0
- package/utils/__tests__/aes.spec.js +52 -0
- package/utils/aes.js +23 -0
- package/web/UI.md +71 -0
- package/web/bwc-autocomplete.js +211 -0
- package/web/bwc-combobox.js +343 -0
- package/web/bwc-fileupload.js +87 -0
- package/web/bwc-loading-overlay.js +54 -0
- package/web/bwc-t4t-form.js +511 -0
- package/web/bwc-table.js +756 -0
- package/web/fetch.js +129 -0
- package/web/i18n.js +24 -0
- package/web/idle.js +49 -0
- package/web/parse-jwt.js +15 -0
- package/web/pwa.js +84 -0
- package/web/sign-pad.js +164 -0
- package/web/t4t-fe.js +164 -0
- package/web/util.js +126 -0
- package/web/web-cam.js +182 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { authFns, createToken, setTokensToHeader } from '../../../auth/index.js'
|
|
2
|
+
|
|
3
|
+
const { AUTH_ERROR_URL } = process.env
|
|
4
|
+
const OAUTH_OPTIONS = JSON.parse(process.env.OAUTH_OPTIONS || null) || {}
|
|
5
|
+
// set callback URL on github to <schema://host:port>/api/oauth/callback
|
|
6
|
+
// initiated from browser - window.location.replace('https://github.com/login/oauth/authorize?scope=user:email&client_id=XXXXXXXXXXXXXXXXXXXX')
|
|
7
|
+
|
|
8
|
+
// /callback
|
|
9
|
+
export const callbackOAuth = async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const { code, state } = req.query
|
|
12
|
+
const result = await fetch(OAUTH_OPTIONS.URL, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ client_id: OAUTH_OPTIONS.CLIENT_ID, client_secret: OAUTH_OPTIONS.CLIENT_SECRET, code, state }),
|
|
16
|
+
})
|
|
17
|
+
const data = await result.json();
|
|
18
|
+
if (data.access_token) {
|
|
19
|
+
const resultUser = await fetch(OAUTH_OPTIONS.USER_URL, {
|
|
20
|
+
method: 'GET',
|
|
21
|
+
headers: { Authorization: `token ${data.access_token}`, },
|
|
22
|
+
})
|
|
23
|
+
const oauthUser = await resultUser.json();
|
|
24
|
+
const oauthId = oauthUser[OAUTH_OPTIONS.USER_ID] // github id, email
|
|
25
|
+
|
|
26
|
+
const user = await authFns.findUser({ [OAUTH_OPTIONS.FIND_ID]: oauthId }) // match github id (or email?) with our user in our application
|
|
27
|
+
if (!user) return res.status(401).json({ message: 'Unauthorized' })
|
|
28
|
+
|
|
29
|
+
const { id, groups } = user
|
|
30
|
+
const tokens = await createToken({ id, groups })
|
|
31
|
+
setTokensToHeader(res, tokens)
|
|
32
|
+
return res.redirect(OAUTH_OPTIONS.CALLBACK + '#' + tokens.access_token + ';' + tokens.refresh_token + ';' + JSON.stringify(tokens.user_meta)) // use url fragment...
|
|
33
|
+
}
|
|
34
|
+
return res.status(401).json({ message: 'Missing Token' })
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error('github auth err', e)
|
|
37
|
+
return AUTH_ERROR_URL ? res.redirect(AUTH_ERROR_URL) : res.status(401).json({ error: 'NOT Authenticated' })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken'
|
|
2
|
+
import { createToken, setTokensToHeader } from '../../../auth/index.js'
|
|
3
|
+
|
|
4
|
+
const { AUTH_ERROR_URL } = process.env
|
|
5
|
+
const OIDC_OPTIONS = JSON.parse(process.env.OIDC_OPTIONS || null) || {}
|
|
6
|
+
|
|
7
|
+
const AUTH_URL = OIDC_OPTIONS ? `${OIDC_OPTIONS.URL}/auth?` : ''
|
|
8
|
+
const TOKEN_URL = OIDC_OPTIONS ? `${OIDC_OPTIONS.URL}/token` : ''
|
|
9
|
+
|
|
10
|
+
// payload.append('client_secret', OIDC_OPTIONS.CLIENT_SECRET) // if keycloak client access type is confidential
|
|
11
|
+
|
|
12
|
+
// TBD - verify endpoint for keycloak
|
|
13
|
+
// https://stackoverflow.com/questions/48274251/keycloak-access-token-validation-end-point
|
|
14
|
+
// curl --location --request POST 'https://HOST_KEYCLOAK/realms/master/protocol/openid-connect/token/introspect' \
|
|
15
|
+
// --header 'Content-Type: application/x-www-form-urlencoded' \
|
|
16
|
+
// --data-urlencode 'client_id=oficina-virtual' \
|
|
17
|
+
// --data-urlencode 'client_secret=4ZeE2v' \
|
|
18
|
+
// --data-urlencode
|
|
19
|
+
|
|
20
|
+
// GET /login
|
|
21
|
+
export const login = async (req, res) => {
|
|
22
|
+
// &scope=openid%20offline_access // get id token and refresh token
|
|
23
|
+
// &redirect_uri=ourApp%3A%2Fcallback
|
|
24
|
+
// &state=237c671a-29d7-11eb-adc1-0242ac120002
|
|
25
|
+
const payload = new URLSearchParams()
|
|
26
|
+
payload.append('response_type', 'code')
|
|
27
|
+
payload.append('client_id', OIDC_OPTIONS.CLIENT_ID)
|
|
28
|
+
if (OIDC_OPTIONS.CLIENT_SECRET) payload.append('client_secret', OIDC_OPTIONS.CLIENT_SECRET)
|
|
29
|
+
// payload.append('redirect_uri', 'http://127.0.0.1:3000/api/oidc/auth')
|
|
30
|
+
// payload.append('state', 'c45566104a3c7676c1cb92c33b19ab9bd91180c6')
|
|
31
|
+
res.redirect(AUTH_URL + payload.toString())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// GET /auth
|
|
35
|
+
// grant_type=authorization_code for now
|
|
36
|
+
export const auth = async (req, res) => { // callback
|
|
37
|
+
try {
|
|
38
|
+
const { code, session_state } = req.query
|
|
39
|
+
let payload = `grant_type=authorization_code&code=${code}`
|
|
40
|
+
+ `&redirect_uri=${OIDC_OPTIONS.CALLBACK}&client_id=${OIDC_OPTIONS.CLIENT_ID}`
|
|
41
|
+
if (OIDC_OPTIONS.CLIENT_SECRET) payload += `&client_secret=${OIDC_OPTIONS.CLIENT_SECRET}`
|
|
42
|
+
// add offline_access to get refresh token
|
|
43
|
+
const result = await fetch(TOKEN_URL, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Accept': 'application/json',
|
|
47
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
48
|
+
},
|
|
49
|
+
body: payload,
|
|
50
|
+
})
|
|
51
|
+
const data = await result.json();
|
|
52
|
+
let { access_token, refresh_token, ...user_meta } = data
|
|
53
|
+
if (OIDC_OPTIONS.REISSUE) {
|
|
54
|
+
const user = jwt.decode(access_token)
|
|
55
|
+
user.id = user[OIDC_OPTIONS.ID_NAME]
|
|
56
|
+
user.groups = user.resource_access.account.roles.join(',')
|
|
57
|
+
// TODO able to detect revoked?
|
|
58
|
+
const tokens = await createToken(user)
|
|
59
|
+
access_token = tokens.access_token
|
|
60
|
+
refresh_token = tokens.refresh_token
|
|
61
|
+
}
|
|
62
|
+
return res.redirect(OIDC_OPTIONS.CALLBACK + '#' + access_token + ';' + refresh_token + ';' + JSON.stringify(user_meta))
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log(e)
|
|
65
|
+
return AUTH_ERROR_URL ? res.redirect(AUTH_ERROR_URL) : res.status(401).json({ error: 'NOT Authenticated' })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// GET /refresh
|
|
70
|
+
export const refresh = async (req, res) => {
|
|
71
|
+
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
72
|
+
const payload = new URLSearchParams()
|
|
73
|
+
payload.append('grant_type', 'refresh_token')
|
|
74
|
+
payload.append('refresh_token', req.cookies?.refresh_token || req.header('refresh_token') || req.query?.refresh_token)
|
|
75
|
+
payload.append('client_id', OIDC_OPTIONS.CLIENT_ID)
|
|
76
|
+
// add offline_access to get refresh token
|
|
77
|
+
const result = await fetch(TOKEN_URL, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...headers },
|
|
80
|
+
body: JSON.stringify(payload),
|
|
81
|
+
})
|
|
82
|
+
const data = await result.json();
|
|
83
|
+
const { access_token, refresh_token } = data
|
|
84
|
+
const tokens = { access_token, refresh_token }
|
|
85
|
+
setTokensToHeader(res, tokens)
|
|
86
|
+
res.json(tokens)
|
|
87
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// own authentication
|
|
2
|
+
import jwt from 'jsonwebtoken'
|
|
3
|
+
import bcrypt from 'bcryptjs'
|
|
4
|
+
import * as otplib from 'otplib';
|
|
5
|
+
// import { authenticator } from 'otplib/authenticator';
|
|
6
|
+
|
|
7
|
+
import { getSecret, createToken, setTokensToHeader, authFns } from '../../../auth/index.js'
|
|
8
|
+
const {
|
|
9
|
+
COOKIE_HTTPONLY,
|
|
10
|
+
AUTH_USER_FIELD_LOGIN,
|
|
11
|
+
AUTH_USER_FIELD_PASSWORD,
|
|
12
|
+
AUTH_USER_FIELD_GAKEY,
|
|
13
|
+
AUTH_USER_FIELD_ID_FOR_JWT,
|
|
14
|
+
JWT_REFRESH_STORE,
|
|
15
|
+
USE_OTP,
|
|
16
|
+
JWT_ALG
|
|
17
|
+
} = process.env
|
|
18
|
+
|
|
19
|
+
const logout = async (req, res) => {
|
|
20
|
+
let id = null
|
|
21
|
+
try {
|
|
22
|
+
let access_token = null
|
|
23
|
+
let tmp = req.cookies?.Authorization || req.header('Authorization') || req.query?.Authorization
|
|
24
|
+
access_token = tmp.split(' ')[1]
|
|
25
|
+
const result = jwt.decode(access_token)
|
|
26
|
+
id = result.id
|
|
27
|
+
jwt.verify(access_token, getSecret('verify', 'access'), { algorithm: [JWT_ALG] }) // throw if expired or invalid
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (e.name !== 'TokenExpiredError') id = null
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
if (id) {
|
|
33
|
+
await authFns.revokeRefreshToken(id) // clear
|
|
34
|
+
if (COOKIE_HTTPONLY) {
|
|
35
|
+
res.clearCookie('refresh_token')
|
|
36
|
+
res.clearCookie('Authorization')
|
|
37
|
+
}
|
|
38
|
+
return res.status(200).json({ message: 'Logged Out' })
|
|
39
|
+
}
|
|
40
|
+
} catch (e) { }
|
|
41
|
+
return res.status(500).json()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const refresh = async (req, res) => {
|
|
45
|
+
// refresh logic all done in authUser
|
|
46
|
+
return res.status(401).json({ message: 'Error token revoked' })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const login = async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const user = await authFns.findUser({
|
|
53
|
+
[AUTH_USER_FIELD_LOGIN]: req.body[AUTH_USER_FIELD_LOGIN]
|
|
54
|
+
})
|
|
55
|
+
const password = req.body[AUTH_USER_FIELD_PASSWORD]
|
|
56
|
+
if (!user) return res.status(401).json({ message: 'Incorrect credentials...' })
|
|
57
|
+
if (!bcrypt.compareSync(password, user[AUTH_USER_FIELD_PASSWORD])) return res.status(401).json({ message: 'Incorrect credentials' })
|
|
58
|
+
if (user.revoked) return res.status(401).json({ message: 'Revoked credentials' })
|
|
59
|
+
const id = user[AUTH_USER_FIELD_ID_FOR_JWT]
|
|
60
|
+
if (!id) return res.status(401).json({ message: 'Authorization Format Error' })
|
|
61
|
+
if (USE_OTP) {
|
|
62
|
+
// Currently supports only Google Authenticator
|
|
63
|
+
// Fido2 can be added in future
|
|
64
|
+
return res.status(200).json({ otp: id })
|
|
65
|
+
}
|
|
66
|
+
const tokens = await createToken(user) // 5 minute expire for login
|
|
67
|
+
setTokensToHeader(res, tokens)
|
|
68
|
+
return res.status(200).json(tokens)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.log('login err', e.toString())
|
|
71
|
+
}
|
|
72
|
+
return res.status(500).json()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const otp = async (req, res) => { // need to be authentication, body { id: '', pin: '123456' }
|
|
76
|
+
try {
|
|
77
|
+
const { id, pin } = req.body
|
|
78
|
+
const user = await authFns.findUser({ id })
|
|
79
|
+
if (user) {
|
|
80
|
+
const gaKey = user[AUTH_USER_FIELD_GAKEY]
|
|
81
|
+
// if (USE_OTP !== 'TEST' ? otplib.authenticator.check(pin, gaKey) : String(pin) === '111111') { // NOTE: expiry will be determined by authenticator itself
|
|
82
|
+
if (USE_OTP !== 'TEST' ? authenticator.check(pin, gaKey) : String(pin) === '111111') { // NOTE: expiry will be determined by authenticator itself
|
|
83
|
+
const tokens = await createToken(user)
|
|
84
|
+
setTokensToHeader(res, tokens)
|
|
85
|
+
return res.status(200).json(tokens)
|
|
86
|
+
} else {
|
|
87
|
+
return res.status(401).json({ message: 'Error token wrong pin' })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) { console.log('otp err', e.toString()) }
|
|
91
|
+
return res.status(401).json({ message: 'Error token revoked' })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
logout,
|
|
97
|
+
refresh,
|
|
98
|
+
login,
|
|
99
|
+
otp,
|
|
100
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// working SAML ADFS example
|
|
2
|
+
// no refresh token, issue own OAuth2 like JWT server
|
|
3
|
+
|
|
4
|
+
import { SAML } from '@node-saml/node-saml'
|
|
5
|
+
import { createToken, setTokensToHeader } from '../../../auth/index.js'
|
|
6
|
+
|
|
7
|
+
const { SAML_OPTIONS, SAML_JWT_MAP, SAML_CERTIFICATE, SAML_PRIVATE_KEY, AUTH_ERROR_URL } = process.env
|
|
8
|
+
const samlJwtMap = JSON.parse(SAML_JWT_MAP || null)
|
|
9
|
+
const samlOptions = JSON.parse(SAML_OPTIONS || null)
|
|
10
|
+
|
|
11
|
+
if (samlOptions) {
|
|
12
|
+
if (SAML_CERTIFICATE) samlOptions.privateCert = SAML_CERTIFICATE
|
|
13
|
+
if (SAML_PRIVATE_KEY) {
|
|
14
|
+
samlOptions.privateKey = SAML_PRIVATE_KEY
|
|
15
|
+
samlOptions.decryptionPvk = SAML_PRIVATE_KEY
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const saml = samlOptions ? new SAML(samlOptions) : null
|
|
20
|
+
|
|
21
|
+
// /login
|
|
22
|
+
export const login = async (req, res) => {
|
|
23
|
+
// return res.redirect('/' + token...) // for faking, bypass real callback
|
|
24
|
+
// console.debug(req.header('referer'), req.query.RelayState)
|
|
25
|
+
// getAuthorizeUrlAsync(RelayState: string, host: string | undefined, options: AuthOptions)
|
|
26
|
+
const authUrl = await saml?.getAuthorizeUrlAsync(req.query.RelayState) // validatePostResponseAsync (calls..., processValidlySignedPostRequestAsync)
|
|
27
|
+
res.redirect(authUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// POST /callback
|
|
31
|
+
export const auth = async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const parsedResponse = await saml?.validatePostResponseAsync(req.body)
|
|
34
|
+
|
|
35
|
+
// TEST
|
|
36
|
+
// console.log(typeof parsedResponse, parsedResponse)
|
|
37
|
+
// res.json({ message: 'testing node-saml ok', parsedResponse })
|
|
38
|
+
|
|
39
|
+
// Callback
|
|
40
|
+
try {
|
|
41
|
+
const TO = req.body.RelayState
|
|
42
|
+
const authenticated = !parsedResponse.loggedOut
|
|
43
|
+
const user = {
|
|
44
|
+
id: parsedResponse.profile[samlJwtMap.id], // id: req.user.nameID, // string
|
|
45
|
+
groups: parsedResponse.profile[samlJwtMap.groups], // groups: req.user.Role, // comma seperated string or array or object...
|
|
46
|
+
}
|
|
47
|
+
if (!TO) {
|
|
48
|
+
// if no RelayState, then it is a test
|
|
49
|
+
return res.status(200).json({
|
|
50
|
+
authenticated,
|
|
51
|
+
user
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
// console.log(TO, user, authenticated, samlJwtMap, parsedResponse['Role'])
|
|
55
|
+
if (authenticated) {
|
|
56
|
+
const tokens = await createToken(user)
|
|
57
|
+
setTokensToHeader(res, tokens)
|
|
58
|
+
return res.redirect(TO + '#' + tokens.access_token + ';' + tokens.refresh_token + ';' + JSON.stringify(tokens.user_meta)) // use url fragment...
|
|
59
|
+
} else {
|
|
60
|
+
return AUTH_ERROR_URL ? res.redirect(AUTH_ERROR_URL) : res.status(401).json({ error: 'NOT Authenticated' })
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// return AUTH_ERROR_URL ? res.redirect(AUTH_ERROR_URL) : res.status(500).json({ error: e.toString() })
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log('SAML callback error', e)
|
|
67
|
+
res.json({
|
|
68
|
+
message: e.toString(),
|
|
69
|
+
note: 'Currently it always triggers invalid document signature fix is on the way'
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
res.send("ok");
|
|
73
|
+
}
|
|
74
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// import path from 'path'
|
|
2
|
+
// path.extname('index.html')
|
|
3
|
+
// returns '.html'
|
|
4
|
+
// req.file / req.files[index]
|
|
5
|
+
// {
|
|
6
|
+
// fieldname: 'kycfile',
|
|
7
|
+
// originalname: 'todo.txt',
|
|
8
|
+
// encoding: '7bit',
|
|
9
|
+
// mimetype: 'text/plain',
|
|
10
|
+
// destination: 'uploads/',
|
|
11
|
+
// filename: 'kycfile-1582238409067',
|
|
12
|
+
// path: 'uploads\\kycfile-1582238409067',
|
|
13
|
+
// size: 110
|
|
14
|
+
// }
|
|
15
|
+
|
|
16
|
+
import multer from 'multer'
|
|
17
|
+
|
|
18
|
+
const memoryUpload = (options) => multer( Object.assign({
|
|
19
|
+
storage: multer.memoryStorage(),
|
|
20
|
+
limits: { files: 1, fileSize: 500000 }
|
|
21
|
+
}, options) )
|
|
22
|
+
|
|
23
|
+
// TBD
|
|
24
|
+
|
|
25
|
+
const storageUpload = ({ folder, options }) => {
|
|
26
|
+
// validate binary file type... using npm file-type?
|
|
27
|
+
// https://dev.to/ayanabilothman/file-type-validation-in-multer-is-not-safe-3h8l
|
|
28
|
+
// const fileFilter = (req, file, cb) => {
|
|
29
|
+
// if (['image/png', 'image/jpeg'].includes(file.mimetype)) {
|
|
30
|
+
// cb(null, true);
|
|
31
|
+
// } else {
|
|
32
|
+
// cb(new Error('Invalid file type!'), false)
|
|
33
|
+
// }
|
|
34
|
+
// }
|
|
35
|
+
return multer(Object.assign({
|
|
36
|
+
storage: multer.diskStorage({
|
|
37
|
+
// fileFilter
|
|
38
|
+
destination: (req, file, cb) => cb(null, folder),
|
|
39
|
+
filename: (req, file, cb) => cb(null, file.originalname) // file.fieldname, file.originalname
|
|
40
|
+
}),
|
|
41
|
+
limits: { files: 1, fileSize: 8000000 }
|
|
42
|
+
}, options))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
memoryUpload,
|
|
47
|
+
storageUpload,
|
|
48
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// placeholder
|
package/iso/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
arrayToCSVRow,
|
|
6
|
+
jsonToCSVHeader,
|
|
7
|
+
jsonToCSVRow,
|
|
8
|
+
jsonToCsv,
|
|
9
|
+
parseAndValidateCsv,
|
|
10
|
+
csvToJson,
|
|
11
|
+
} from '../csv-utils.js';
|
|
12
|
+
|
|
13
|
+
describe.skip('csv-utils.js', () => {
|
|
14
|
+
describe('arrayToCSVRow', () => {
|
|
15
|
+
it('should convert array of primitives to CSV row', () => {
|
|
16
|
+
assert.strictEqual(arrayToCSVRow([1, 'a', true]), '1,a,true');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should escape commas, quotes, and newlines in strings', () => {
|
|
20
|
+
assert.strictEqual(arrayToCSVRow(['a,b', 'c"d', 'e\nf']), '"a,b","c""d","e\nf"');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should stringify and escape objects', () => {
|
|
24
|
+
assert.strictEqual(arrayToCSVRow([{ x: 1, y: 'z' }]), '"{""x"":1,""y"":""z""}"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle null and undefined as empty fields', () => {
|
|
28
|
+
assert.strictEqual(arrayToCSVRow([null, undefined, 'foo']), ',,foo');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('jsonToCSVHeader', () => {
|
|
33
|
+
it('should convert object keys to CSV header', () => {
|
|
34
|
+
assert.strictEqual(jsonToCSVHeader({ a: 1, b: 2 }), 'a,b');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('jsonToCSVRow', () => {
|
|
39
|
+
it('should convert object values to CSV row', () => {
|
|
40
|
+
assert.strictEqual(jsonToCSVRow({ a: 1, b: 'foo' }), '1,foo');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should escape values as needed', () => {
|
|
44
|
+
assert.strictEqual(jsonToCSVRow({ a: 'a,b', b: 'c"d' }), '"a,b","c""d"');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('jsonToCsv', () => {
|
|
49
|
+
it('should convert array of objects to CSV', () => {
|
|
50
|
+
const arr = [
|
|
51
|
+
{ a: 1, b: 'foo' },
|
|
52
|
+
{ a: 2, b: 'bar' },
|
|
53
|
+
];
|
|
54
|
+
assert.strictEqual(jsonToCsv(arr, ',', '\n', true), 'a,b\n1,foo\n2,bar\n');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should throw on column mismatch unless ignoreColumnMismatch is true', () => {
|
|
58
|
+
const arr = [
|
|
59
|
+
{ a: 1, b: 'foo' },
|
|
60
|
+
{ a: 2 },
|
|
61
|
+
];
|
|
62
|
+
assert.throws(() => jsonToCsv(arr));
|
|
63
|
+
assert.doesNotThrow(() => jsonToCsv(arr, ',', '\n', true));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should not add missing columns when ignoreColumnMismatch is true', () => {
|
|
67
|
+
const arr = [
|
|
68
|
+
{ a: 1, b: 'foo' },
|
|
69
|
+
{ a: 2 },
|
|
70
|
+
];
|
|
71
|
+
assert.strictEqual(jsonToCsv(arr, ',', '\n', true), 'a,b\n1,foo\n2\n');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('csvToJson', () => {
|
|
76
|
+
it('should convert CSV to JSON array of objects', () => {
|
|
77
|
+
const csv = 'a,b\n1,foo\n2,bar\n';
|
|
78
|
+
const result = csvToJson({ _text: csv });
|
|
79
|
+
assert.deepStrictEqual(result, [
|
|
80
|
+
{ a: '1', b: 'foo' },
|
|
81
|
+
{ a: '2', b: 'bar' }
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle quoted fields with commas and newlines', () => {
|
|
86
|
+
const csv = 'a,b\n1,"hello, world"\n2,"line1\nline2"\n';
|
|
87
|
+
const result = csvToJson({ _text: csv });
|
|
88
|
+
assert.strictEqual(result[0].b, 'hello, world');
|
|
89
|
+
assert.strictEqual(result[1].b, 'line1\nline2');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw on column mismatch unless ignoreColumnMismatch is true', () => {
|
|
93
|
+
const csv = 'a,b\n1,2,3\n';
|
|
94
|
+
assert.throws(() => csvToJson({ _text: csv }));
|
|
95
|
+
assert.doesNotThrow(() => csvToJson({ _text: csv, ignoreColumnMismatch: true }));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('parseAndValidateCsv', () => {
|
|
100
|
+
it('should validate well-formed CSV', () => {
|
|
101
|
+
const csv = 'a,b\n1,2\n3,4\n';
|
|
102
|
+
const result = parseAndValidateCsv(csv);
|
|
103
|
+
assert.strictEqual(result.valid, true);
|
|
104
|
+
assert.deepStrictEqual(result.rows, [['a', 'b'], ['1', '2'], ['3', '4']]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should detect unclosed quotes', () => {
|
|
108
|
+
const csv = 'a,b\n"1,2\n3,4\n';
|
|
109
|
+
const result = parseAndValidateCsv(csv);
|
|
110
|
+
assert.strictEqual(result.valid, false);
|
|
111
|
+
assert.match(result.reason, /Unclosed quoted field/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should detect corrupted JSON in fields', () => {
|
|
115
|
+
const csv = 'a,b\n{"x":1},[1,2\n';
|
|
116
|
+
const result = parseAndValidateCsv(csv);
|
|
117
|
+
assert.strictEqual(result.valid, false);
|
|
118
|
+
assert.match(result.reason, /Corrupted JSON/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should validate JSON data', () => {
|
|
122
|
+
const csv = 'a,b\n1,"{ ""aa"": ""123"" }"\n';
|
|
123
|
+
const result = parseAndValidateCsv(csv);
|
|
124
|
+
assert.strictEqual(result.valid, true);
|
|
125
|
+
assert.deepStrictEqual(result.rows, [['a', 'b'], ['1', '{ "aa": "123" }']]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
dateStrAddDay,
|
|
5
|
+
dateStrAddDayISO,
|
|
6
|
+
getLocaleDateTimeTzISO,
|
|
7
|
+
getLocaleDateISO,
|
|
8
|
+
getLocaleTimeISO,
|
|
9
|
+
getTzOffsetISO,
|
|
10
|
+
getYmdhmsUtc,
|
|
11
|
+
getDayOfWeek
|
|
12
|
+
} from '../datetime.js';
|
|
13
|
+
|
|
14
|
+
describe.skip('datetime.js', () => {
|
|
15
|
+
describe('dateStrAddDay', () => {
|
|
16
|
+
it('should add days to ISO date string and return [YYYY, MM, DD]', () => {
|
|
17
|
+
const result = dateStrAddDay('2023-01-01T00:00:00Z', 2);
|
|
18
|
+
assert.deepStrictEqual(result, ['2023', '01', '03']);
|
|
19
|
+
});
|
|
20
|
+
it('should default to 0 days', () => {
|
|
21
|
+
const result = dateStrAddDay('2023-01-01T00:00:00Z');
|
|
22
|
+
assert.deepStrictEqual(result, ['2023', '01', '01']);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('dateStrAddDayISO', () => {
|
|
27
|
+
it('should add days and return ISO string', () => {
|
|
28
|
+
const result = dateStrAddDayISO('2023-01-01T00:00:00Z', 1);
|
|
29
|
+
assert.ok(result.startsWith('2023-01-02'));
|
|
30
|
+
});
|
|
31
|
+
it('should default to 0 days', () => {
|
|
32
|
+
const result = dateStrAddDayISO('2023-01-01T00:00:00Z');
|
|
33
|
+
assert.ok(result.startsWith('2023-01-01'));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getLocaleDateTimeTzISO', () => {
|
|
38
|
+
it('should return formatted date-time with timezone', () => {
|
|
39
|
+
const dt = '2023-10-24T11:40:15Z';
|
|
40
|
+
const tz = 'Asia/Singapore';
|
|
41
|
+
const result = getLocaleDateTimeTzISO(dt, tz);
|
|
42
|
+
assert.match(result, /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} GMT\+8/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('getLocaleDateISO', () => {
|
|
47
|
+
it('should return local date in ISO format', () => {
|
|
48
|
+
const dt = '2023-10-24T11:40:15Z';
|
|
49
|
+
const tz = 'Asia/Singapore';
|
|
50
|
+
const result = getLocaleDateISO(dt, tz);
|
|
51
|
+
assert.match(result, /\d{4}-\d{2}-\d{2}/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getLocaleTimeISO', () => {
|
|
56
|
+
it('should return local time in ISO format', () => {
|
|
57
|
+
const dt = '2023-10-24T11:40:15Z';
|
|
58
|
+
const tz = 'Asia/Singapore';
|
|
59
|
+
const result = getLocaleTimeISO(dt, tz);
|
|
60
|
+
assert.match(result, /\d{2}:\d{2}:\d{2}/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('getTzOffsetISO', () => {
|
|
65
|
+
it('should return timezone offset in ISO format', () => {
|
|
66
|
+
const date = new Date('2023-01-01T00:00:00Z');
|
|
67
|
+
const result = getTzOffsetISO(date);
|
|
68
|
+
assert.match(result, /^[+-]\d{2}:\d{2}$/);
|
|
69
|
+
});
|
|
70
|
+
it('should default to current date', () => {
|
|
71
|
+
const result = getTzOffsetISO();
|
|
72
|
+
assert.match(result, /^[+-]\d{2}:\d{2}$/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('getYmdhmsUtc', () => {
|
|
77
|
+
it('should return UTC timestamp in YYYYMMDD_HHmmssZ format', () => {
|
|
78
|
+
const date = new Date('2023-01-01T12:34:56Z');
|
|
79
|
+
const result = getYmdhmsUtc(date);
|
|
80
|
+
assert.strictEqual(result, '20230101_123456Z');
|
|
81
|
+
});
|
|
82
|
+
it('should default to current date', () => {
|
|
83
|
+
const result = getYmdhmsUtc();
|
|
84
|
+
assert.match(result, /\d{8}_\d{6}Z/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('getDayOfWeek', () => {
|
|
89
|
+
it('should return correct day index for UTC', () => {
|
|
90
|
+
const date = '2023-01-01T00:00:00Z'; // Sunday
|
|
91
|
+
const result = getDayOfWeek(date);
|
|
92
|
+
assert.strictEqual(result, 0);
|
|
93
|
+
});
|
|
94
|
+
it('should return correct day index for timezone', () => {
|
|
95
|
+
const date = '2023-01-01T00:00:00Z';
|
|
96
|
+
const tz = 'Asia/Singapore';
|
|
97
|
+
const result = getDayOfWeek(date, tz);
|
|
98
|
+
assert.ok(result >= 0 && result <= 6);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|