@opengis/fastify-table 1.5.8 → 2.0.0
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/dist/config.js +0 -1
- package/dist/index.js +28 -29
- package/dist/server/migrations/oauth.sql.sql +77 -0
- package/dist/server/migrations/properties.sql +1 -1
- package/dist/server/plugins/auth/funcs/authorizeUser.js +63 -0
- package/dist/server/plugins/auth/funcs/checkReferer.js +24 -0
- package/dist/server/plugins/auth/funcs/getQuery.js +127 -0
- package/dist/server/plugins/auth/funcs/jwt.js +63 -0
- package/dist/server/plugins/auth/funcs/logAuth.js +17 -0
- package/dist/server/plugins/auth/funcs/loginFile.js +41 -0
- package/dist/server/plugins/auth/funcs/loginUser.js +45 -0
- package/dist/server/plugins/auth/funcs/sendNotification.js +96 -0
- package/dist/server/plugins/auth/funcs/users.js +2 -0
- package/dist/server/plugins/auth/funcs/verifyPassword.js +30 -0
- package/dist/server/plugins/auth/index.js +110 -0
- package/dist/server/plugins/policy/funcs/checkPolicy.js +50 -100
- package/dist/server/plugins/policy/index.js +2 -2
- package/dist/server/routes/access/controllers/access.group.post.js +3 -3
- package/dist/server/routes/auth/controllers/2factor/generate.js +38 -0
- package/dist/server/routes/auth/controllers/2factor/providers/totp.js +115 -0
- package/dist/server/routes/auth/controllers/2factor/recovery.js +83 -0
- package/dist/server/routes/auth/controllers/2factor/toggle.js +39 -0
- package/dist/server/routes/auth/controllers/2factor/verify.js +68 -0
- package/dist/server/routes/auth/controllers/core/getUserInfo.js +37 -0
- package/dist/server/routes/auth/controllers/core/login.js +21 -0
- package/dist/server/routes/auth/controllers/core/logout.js +10 -0
- package/dist/server/routes/auth/controllers/core/passwordRecovery.js +151 -0
- package/dist/server/routes/auth/controllers/core/registration.js +96 -0
- package/dist/server/routes/auth/controllers/core/updateUserInfo.js +17 -0
- package/dist/server/routes/auth/controllers/euSign/authByData.js +115 -0
- package/dist/server/routes/auth/controllers/jwt/authorize.js +78 -0
- package/dist/server/routes/auth/controllers/jwt/token.js +67 -0
- package/dist/server/routes/auth/controllers/page/login2faTemplate.js +70 -0
- package/dist/server/routes/auth/controllers/page/loginEuSign.js +20 -0
- package/dist/server/routes/auth/controllers/page/loginTemplate.js +45 -0
- package/dist/server/routes/auth/index.js +87 -0
- package/dist/server/routes/util/index.js +9 -5
- package/dist/server.js +62 -0
- package/package.json +20 -8
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import crypt from "apache-crypt";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import config from "../../../../config.js";
|
|
4
|
+
function md5(string) {
|
|
5
|
+
return crypto.createHash("md5").update(string).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
export default async function verifyPassword({ pg, username, password, }) {
|
|
8
|
+
if (!config.pg)
|
|
9
|
+
return;
|
|
10
|
+
if (!username || username === "")
|
|
11
|
+
return { message: "not enough params: username" };
|
|
12
|
+
if (!password || password === "")
|
|
13
|
+
return { message: "not enough params: password" };
|
|
14
|
+
const query = "select * from admin.users where $1 in (login,email,phone) and enabled limit 1";
|
|
15
|
+
const json = await pg.query(query, [username]).then((res) => res.rows[0]);
|
|
16
|
+
if (!json)
|
|
17
|
+
return { message: "user not found" };
|
|
18
|
+
let hash = "";
|
|
19
|
+
for (let i = 0; i < 10; i += 1) {
|
|
20
|
+
hash = md5(password + hash + json.salt);
|
|
21
|
+
}
|
|
22
|
+
hash = crypt(hash, json.salt);
|
|
23
|
+
if (json.password === hash) {
|
|
24
|
+
if (username === "admin" && password === "admin") {
|
|
25
|
+
await pg.query("update admin.users set password='6Z4gu2mG8R' where uid=$1", [json.uid]);
|
|
26
|
+
}
|
|
27
|
+
return { user: json };
|
|
28
|
+
}
|
|
29
|
+
return { message: "Wrong password" };
|
|
30
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// import fp from "fastify-plugin";
|
|
2
|
+
import cookie from "@fastify/cookie";
|
|
3
|
+
import session from "@fastify/session";
|
|
4
|
+
// import fastifyPassport from "@fastify/passport";
|
|
5
|
+
import { Authenticator } from "@fastify/passport";
|
|
6
|
+
import RedisStore from "fastify-session-redis-store";
|
|
7
|
+
import config from "../../../config.js";
|
|
8
|
+
import getRedis from "../redis/funcs/getRedis.js";
|
|
9
|
+
const fastifyPassport = new Authenticator();
|
|
10
|
+
const { prefix = "/api" } = config;
|
|
11
|
+
async function plugin(fastify, opt = config) {
|
|
12
|
+
if (!opt.redis) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
fastify.addHook("onRequest", async (req, reply) => {
|
|
16
|
+
const { pg, hostname, headers, routeOptions } = req;
|
|
17
|
+
const { policy = [] } = routeOptions?.config || {};
|
|
18
|
+
// proxy from old apps to editor, bi etc.
|
|
19
|
+
const validToken = (req.ip === "193.239.152.181" ||
|
|
20
|
+
req.ip === "127.0.0.1" ||
|
|
21
|
+
req.ip?.startsWith?.("192.168.") ||
|
|
22
|
+
config.debug) &&
|
|
23
|
+
req.headers?.token &&
|
|
24
|
+
config.auth?.tokens?.includes?.(headers.token);
|
|
25
|
+
if (validToken && !req?.user?.uid) {
|
|
26
|
+
req.user = {
|
|
27
|
+
uid: req.headers?.uid?.toString?.(),
|
|
28
|
+
user_type: req.headers?.user_type?.toString?.() || "regular",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const isAdmin = process.env.NODE_ENV === "admin" ||
|
|
32
|
+
hostname?.split?.(":")?.shift?.() === config.adminDomain ||
|
|
33
|
+
config.admin ||
|
|
34
|
+
hostname?.startsWith?.("admin");
|
|
35
|
+
const isPublic = Array.isArray(policy)
|
|
36
|
+
? policy.includes("public")
|
|
37
|
+
: policy === "L0";
|
|
38
|
+
// if 2factor is enabled and secondFactor not true => redirect to 2factor login page
|
|
39
|
+
const { secondFactor, passport = {} } = req.session || {}; // base login +
|
|
40
|
+
if (!passport.user?.uid &&
|
|
41
|
+
(config.auth?.disable || config.auth?.user) &&
|
|
42
|
+
!isPublic) {
|
|
43
|
+
req.session = req.session || {};
|
|
44
|
+
req.session.passport = req.session.passport || {}; // ensure passport session exists
|
|
45
|
+
req.session.passport.user = {
|
|
46
|
+
...(config.auth?.user || {}),
|
|
47
|
+
uid: config.auth?.user?.uid?.toString?.() || "1",
|
|
48
|
+
user_rnokpp: config.auth?.user?.rnokpp,
|
|
49
|
+
user_type: config.auth?.user?.type || "regular",
|
|
50
|
+
};
|
|
51
|
+
req.user = req.session.passport.user;
|
|
52
|
+
}
|
|
53
|
+
req.user = req.user === null ? undefined : req.user; // fix for user.uid errors, by default user is null, while with express passport it was {}, unauthorized user does not trigger serializer
|
|
54
|
+
// already done by @fastify/passport serializer / deserializer
|
|
55
|
+
// req.user = passport.user;
|
|
56
|
+
if (config.trace) {
|
|
57
|
+
console.log("req.user?.uid", req.user?.uid, "req.session?.passport?.user?.uid", req.session?.passport?.user?.uid, "config.auth", config.auth);
|
|
58
|
+
}
|
|
59
|
+
// currently 2factor + auth with passwd file not supported
|
|
60
|
+
const ispasswd = !pg?.pk?.["admin.users"] || config.auth?.passwd;
|
|
61
|
+
if (!passport.user?.uid &&
|
|
62
|
+
!config.auth?.disable &&
|
|
63
|
+
isAdmin &&
|
|
64
|
+
!isPublic &&
|
|
65
|
+
!req.url.startsWith(prefix) &&
|
|
66
|
+
!req.url.startsWith("/api") &&
|
|
67
|
+
!req.url.startsWith("/login")) {
|
|
68
|
+
return reply.redirect(`${config?.auth?.redirect || "/login"}` + `?redirect=${req.url}`);
|
|
69
|
+
}
|
|
70
|
+
if (passport.user?.uid &&
|
|
71
|
+
!config.auth?.disable &&
|
|
72
|
+
config.auth?.["2factor"] &&
|
|
73
|
+
isAdmin &&
|
|
74
|
+
(routeOptions?.method || "GET") === "GET" &&
|
|
75
|
+
!secondFactor &&
|
|
76
|
+
!ispasswd) {
|
|
77
|
+
if (![
|
|
78
|
+
"/logout",
|
|
79
|
+
"/2factor",
|
|
80
|
+
"/2factor?recovery=1",
|
|
81
|
+
"/2factor/verify",
|
|
82
|
+
"/2factor/recovery",
|
|
83
|
+
"/user",
|
|
84
|
+
config.auth?.["2factorRedirect"],
|
|
85
|
+
]
|
|
86
|
+
.filter((el) => el)
|
|
87
|
+
.includes(req.url)) {
|
|
88
|
+
return reply.redirect(config.auth?.["2factorRedirect"] || "/2factor");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
});
|
|
93
|
+
await fastify.register(cookie, {
|
|
94
|
+
parseOptions: opt?.auth?.cookieOptions || { secure: false },
|
|
95
|
+
});
|
|
96
|
+
await fastify.register(session, {
|
|
97
|
+
secret: opt?.auth?.secret || "61b820e12858570a4b0633020d4394a17903d9a9",
|
|
98
|
+
cookieName: "session_auth",
|
|
99
|
+
cookie: opt?.auth?.cookieOptions || { secure: false },
|
|
100
|
+
store: new RedisStore({ client: getRedis({ db: 10 }) }),
|
|
101
|
+
});
|
|
102
|
+
// register passport AFTER session is ready
|
|
103
|
+
await fastify.register(fastifyPassport.initialize());
|
|
104
|
+
await fastify.register(fastifyPassport.secureSession());
|
|
105
|
+
// serialize user used to store user info in session store
|
|
106
|
+
fastifyPassport.registerUserSerializer(async (user) => ({ user }));
|
|
107
|
+
// deserialize user used to add user info from session store to req
|
|
108
|
+
fastifyPassport.registerUserDeserializer(async (passport) => passport?.user || passport);
|
|
109
|
+
}
|
|
110
|
+
export default plugin;
|
|
@@ -1,38 +1,53 @@
|
|
|
1
1
|
import { config, logger } from "../../../../utils.js";
|
|
2
2
|
import block from "../sqlInjection.js";
|
|
3
|
-
const { skipCheckPolicyRoutes = [] } = config;
|
|
4
|
-
const skipCheckPolicy = (path) => skipCheckPolicyRoutes.find((el) => path.includes(el));
|
|
5
3
|
export default function checkPolicy(req, reply) {
|
|
6
|
-
const { originalUrl: path, hostname, query, params, headers, method, routeOptions, unittest,
|
|
7
|
-
|
|
4
|
+
const { originalUrl: path, hostname, query, params, headers, method, routeOptions, unittest, // legacy
|
|
5
|
+
} = req;
|
|
6
|
+
// ! skip locally, skip tests
|
|
7
|
+
if (config.local || unittest || process.env.NODE_ENV === "test") {
|
|
8
8
|
return null;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
// ! skip non-API Requests
|
|
11
|
+
const isApi = ["/files/", "/api/", "/api-user/", "/logger", "/file/"].filter((el) => path.includes(el)).length;
|
|
12
|
+
if (!isApi) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const body = req.body
|
|
16
|
+
? JSON.stringify(req?.body || {}).substring(30)
|
|
17
|
+
: undefined;
|
|
11
18
|
const isAdmin = process.env.NODE_ENV === "admin" ||
|
|
12
19
|
hostname.split(":").shift() === config.adminDomain ||
|
|
13
20
|
config.admin ||
|
|
14
21
|
hostname.startsWith("admin");
|
|
15
22
|
const user = req.user || req.session?.passport?.user;
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// const isServer = process.argv[2];
|
|
24
|
+
const { role, referer, policy = [], } = (routeOptions?.config || {});
|
|
25
|
+
const isRole = (role && user?.user_type !== role) ||
|
|
26
|
+
(policy.includes("admin") && user?.user_type !== "admin");
|
|
27
|
+
const allowExtPublic = [".png", ".jpg", ".svg"];
|
|
28
|
+
const ext = path.toLowerCase().substr(-4);
|
|
29
|
+
const isPublic = Array.isArray(policy)
|
|
30
|
+
? policy.includes("public")
|
|
31
|
+
: policy === "L0";
|
|
32
|
+
const requireUser = Array.isArray(policy)
|
|
33
|
+
? policy.includes("user")
|
|
34
|
+
: ["L1", "L2", "L3"].includes(policy);
|
|
35
|
+
const requireReferer = Array.isArray(policy)
|
|
36
|
+
? policy.includes("referer")
|
|
37
|
+
: referer;
|
|
38
|
+
// ! role
|
|
39
|
+
if (isRole) {
|
|
40
|
+
logger.file("policy/role", {
|
|
25
41
|
path,
|
|
26
42
|
method,
|
|
27
43
|
params,
|
|
28
44
|
query,
|
|
29
45
|
body,
|
|
30
|
-
message: "access restricted: not admin",
|
|
31
46
|
uid: user?.uid,
|
|
32
47
|
});
|
|
33
48
|
return reply.status(403).send("access restricted: 0");
|
|
34
49
|
}
|
|
35
|
-
|
|
50
|
+
// ! file injection
|
|
36
51
|
if (JSON.stringify(params || {})?.includes("../") ||
|
|
37
52
|
JSON.stringify(query || {})?.includes("../") ||
|
|
38
53
|
path?.includes("../")) {
|
|
@@ -42,121 +57,57 @@ export default function checkPolicy(req, reply) {
|
|
|
42
57
|
params,
|
|
43
58
|
query,
|
|
44
59
|
body,
|
|
45
|
-
message: "access restricted: 1",
|
|
46
60
|
uid: user?.uid,
|
|
47
61
|
});
|
|
48
62
|
return reply.status(403).send("access restricted: 1");
|
|
49
63
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const ext = path.toLowerCase().substr(-4);
|
|
53
|
-
if (path.includes("files/") && allowExtPublic.includes(ext))
|
|
64
|
+
// ! invalid file extension
|
|
65
|
+
if (path.includes("files/") && allowExtPublic.includes(ext)) {
|
|
54
66
|
return null;
|
|
55
|
-
/* === 2.SQL Injection policy: no-sql === */
|
|
56
|
-
if (!policy.includes("no-sql")) {
|
|
57
|
-
// skip polyline param - data filter (geometry bounds)
|
|
58
|
-
const stopWords = block.filter((el) => path.replace(query.polyline, "").includes(el));
|
|
59
|
-
if (stopWords?.length) {
|
|
60
|
-
logger.file("injection/sql", {
|
|
61
|
-
path,
|
|
62
|
-
method,
|
|
63
|
-
params,
|
|
64
|
-
query,
|
|
65
|
-
body,
|
|
66
|
-
stopWords,
|
|
67
|
-
message: "access restricted: 2",
|
|
68
|
-
uid: user?.uid,
|
|
69
|
-
});
|
|
70
|
-
return reply.status(403).send("access restricted: 2");
|
|
71
|
-
}
|
|
72
67
|
}
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
const validToken = (req.ip === "193.239.152.181" ||
|
|
79
|
-
req.ip === "127.0.0.1" ||
|
|
80
|
-
req.ip?.startsWith?.("192.168.") ||
|
|
81
|
-
config.debug) &&
|
|
82
|
-
req.headers?.token &&
|
|
83
|
-
config.auth?.tokens?.includes?.(headers.token);
|
|
84
|
-
if (validToken && !req?.user?.uid) {
|
|
85
|
-
req.user = {
|
|
86
|
-
uid: req.headers?.uid?.toString?.(),
|
|
87
|
-
user_type: req.ip === "193.239.152.181" || config.debug ? "admin" : "regular",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
/* === policy: public === */
|
|
91
|
-
if (policy.includes("public") ||
|
|
92
|
-
skipCheckPolicy(path) ||
|
|
93
|
-
!config.pg ||
|
|
94
|
-
config.auth?.disable ||
|
|
95
|
-
config.local ||
|
|
96
|
-
config.debug) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
/* === 0. policy: unauthorized access from admin URL === */
|
|
100
|
-
if (!validToken && !user?.uid && isAdmin && !policy.includes("public")) {
|
|
101
|
-
logger.file("policy/unauthorized", {
|
|
68
|
+
// ! sql injection
|
|
69
|
+
const stopWords = block.filter((el) => path.includes(el));
|
|
70
|
+
if (stopWords?.length) {
|
|
71
|
+
logger.file("injection/sql", {
|
|
102
72
|
path,
|
|
103
73
|
method,
|
|
104
74
|
params,
|
|
105
75
|
query,
|
|
106
76
|
body,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ip: req.ip,
|
|
110
|
-
headers,
|
|
111
|
-
message: "unauthorized",
|
|
77
|
+
stopWords,
|
|
78
|
+
uid: user?.uid,
|
|
112
79
|
});
|
|
113
|
-
return reply.status(
|
|
80
|
+
return reply.status(403).send("access restricted: 2");
|
|
114
81
|
}
|
|
115
|
-
|
|
116
|
-
if (!
|
|
117
|
-
!user &&
|
|
118
|
-
policy.includes("user") &&
|
|
119
|
-
!skipCheckPolicy(path)) {
|
|
82
|
+
// ! user required, but not logged in
|
|
83
|
+
if (requireUser && !user) {
|
|
120
84
|
logger.file("policy/user", {
|
|
121
85
|
path,
|
|
122
86
|
method,
|
|
123
87
|
params,
|
|
124
88
|
query,
|
|
125
89
|
body,
|
|
126
|
-
message: "access restricted: 3",
|
|
127
90
|
});
|
|
128
91
|
return reply.status(403).send("access restricted: 3");
|
|
129
92
|
}
|
|
130
|
-
|
|
131
|
-
if (!
|
|
132
|
-
!headers?.referer?.includes?.(hostname) &&
|
|
133
|
-
policy.includes("referer")) {
|
|
93
|
+
// ! referer
|
|
94
|
+
if (requireReferer && !headers?.referer?.includes?.(hostname)) {
|
|
134
95
|
logger.file("policy/referer", {
|
|
135
96
|
path,
|
|
136
97
|
method,
|
|
137
98
|
params,
|
|
138
99
|
query,
|
|
139
100
|
body,
|
|
140
|
-
message: "access restricted: 4",
|
|
141
101
|
uid: user?.uid,
|
|
142
102
|
});
|
|
143
103
|
return reply.status(403).send("access restricted: 4");
|
|
144
104
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
query,
|
|
152
|
-
body,
|
|
153
|
-
message: "access restricted: 5",
|
|
154
|
-
uid: user?.uid,
|
|
155
|
-
});
|
|
156
|
-
return reply.status(403).send("access restricted: 5");
|
|
157
|
-
}*/
|
|
158
|
-
/* === 6. base policy: block non-public api w/ out authorization === */
|
|
159
|
-
if (!validToken && isAdmin && !config.debug && user?.uid && isServer) {
|
|
105
|
+
// ! public / token
|
|
106
|
+
if (isPublic || config.debug) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// ! block any API for admin panel (without authorization)
|
|
110
|
+
if (isAdmin && !config.debug && !user?.uid) {
|
|
160
111
|
logger.file("policy/api", {
|
|
161
112
|
path,
|
|
162
113
|
method,
|
|
@@ -168,6 +119,5 @@ export default function checkPolicy(req, reply) {
|
|
|
168
119
|
});
|
|
169
120
|
return reply.status(403).send("access restricted: 6");
|
|
170
121
|
}
|
|
171
|
-
// console.log(headers);
|
|
172
122
|
return null;
|
|
173
123
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import checkPolicy from
|
|
1
|
+
import checkPolicy from "./funcs/checkPolicy.js";
|
|
2
2
|
async function plugin(fastify) {
|
|
3
|
-
fastify.addHook(
|
|
3
|
+
fastify.addHook("preParsing", async (request, reply) => {
|
|
4
4
|
const resp = checkPolicy(request, reply);
|
|
5
5
|
if (resp) {
|
|
6
6
|
return resp;
|
|
@@ -19,13 +19,13 @@ export default async function accessGroupPost({ pg = pgClients.client, params, u
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
if (routes?.length) {
|
|
22
|
-
const
|
|
22
|
+
const routesDB = await pg
|
|
23
23
|
.query('select array_agg(route_id) as "routesDB" from admin.routes where enabled')
|
|
24
|
-
.then((res1) => res1.rows?.[0] ||
|
|
24
|
+
.then((res1) => res1.rows?.[0]?.routesDB || []);
|
|
25
25
|
await pg.query("delete from admin.role_access where role_id=$1;", [id]);
|
|
26
26
|
const q = "insert into admin.role_access(role_id,route_id,actions) values ($1,$2,$3)";
|
|
27
27
|
await Promise.all(routes
|
|
28
|
-
.filter((el) => routesDB
|
|
28
|
+
.filter((el) => routesDB?.includes?.(el.path) && el.actions)
|
|
29
29
|
.map((el) => pg.query(q, [id, el.path, el.actions])));
|
|
30
30
|
const { rows } = await pg.query(`select a.route_id as path, b.actions as actions from admin.routes a
|
|
31
31
|
left join admin.role_access b on a.route_id=b.route_id
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import config from "../../../../../config.js";
|
|
2
|
+
import pgClients from "../../../../plugins/pg/pgClients.js";
|
|
3
|
+
import { generate } from "./providers/totp.js";
|
|
4
|
+
/**
|
|
5
|
+
* Генерація secret для двохфакторної авторизації користувача
|
|
6
|
+
*
|
|
7
|
+
* @method GET
|
|
8
|
+
* @summary Генерація user secret для двохфакторної авторизації
|
|
9
|
+
* @priority 3
|
|
10
|
+
* @alias generate
|
|
11
|
+
* @type api
|
|
12
|
+
* @tag auth
|
|
13
|
+
* @requires 2fa
|
|
14
|
+
* @errors 500
|
|
15
|
+
* @returns {Number} status Номер помилки
|
|
16
|
+
* @returns {String|Object} error Опис помилки
|
|
17
|
+
* @returns {String|Object} message Повідомлення про успішне виконання або об'єкт з параметрами
|
|
18
|
+
*/
|
|
19
|
+
export default async function generateFunction({ pg = pgClients.client, user = {} }, reply) {
|
|
20
|
+
if (!user?.uid) {
|
|
21
|
+
return reply.status(401).send("unauthorized");
|
|
22
|
+
}
|
|
23
|
+
const { uid } = user;
|
|
24
|
+
if (!config?.auth?.["2factor"]) {
|
|
25
|
+
return reply.status(400).send("2fa not enabled");
|
|
26
|
+
}
|
|
27
|
+
if (!config.pg) {
|
|
28
|
+
return reply.status(400).send("empty pg");
|
|
29
|
+
}
|
|
30
|
+
if (!uid) {
|
|
31
|
+
return reply.status(401).send("access restricted: unauthorized");
|
|
32
|
+
}
|
|
33
|
+
const res = await generate({ pg, uid });
|
|
34
|
+
if (res?.enabled) {
|
|
35
|
+
return reply.status(400).send("already created 2fa");
|
|
36
|
+
}
|
|
37
|
+
return reply.status(200).send(res);
|
|
38
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import qrcode from "qrcode";
|
|
3
|
+
import { authenticator } from "otplib";
|
|
4
|
+
const TYPE = "TOTP";
|
|
5
|
+
const enableSecret = async ({ uid, TYPE, pg }) => {
|
|
6
|
+
await pg.query("update admin.users_social_auth set enabled=true where uid = $1 and social_auth_type = $2", [uid, TYPE]);
|
|
7
|
+
};
|
|
8
|
+
const deleteSecret = async ({ uid, TYPE, pg }) => {
|
|
9
|
+
await pg.query("delete from admin.users_social_auth where uid=$1 and social_auth_type = $2", [uid, TYPE]);
|
|
10
|
+
};
|
|
11
|
+
const getSecret = async ({ uid, TYPE, pg }) => {
|
|
12
|
+
const { social_auth_code: secret, enabled, recoveryCodes, } = await pg
|
|
13
|
+
.query(`select social_auth_code, enabled, social_auth_obj->'codesArray' as "recoveryCodes"
|
|
14
|
+
from admin.users_social_auth
|
|
15
|
+
where uid = $1 and social_auth_type = $2`, [uid, TYPE])
|
|
16
|
+
.then((el) => el.rows?.[0] || {});
|
|
17
|
+
return { secret, enabled, recoveryCodes };
|
|
18
|
+
};
|
|
19
|
+
const addSecret = async ({ uid, secret, TYPE, pg, recoveryCodes, otp, }) => {
|
|
20
|
+
await pg.query(`insert into admin.users_social_auth(uid, social_auth_code, social_auth_type, social_auth_obj, social_auth_url, enabled)
|
|
21
|
+
values($1, $2, $3, $4::json, $5, false)`, [uid, secret, TYPE, { codesArray: recoveryCodes }, otp]);
|
|
22
|
+
};
|
|
23
|
+
const updateSecret = async ({ uid, TYPE, pg, secret, recoveryCodes, otp, }) => {
|
|
24
|
+
const result = await pg
|
|
25
|
+
.query(`update admin.users_social_auth
|
|
26
|
+
set social_auth_code=$3, social_auth_obj=$4::json, social_auth_url=$5
|
|
27
|
+
where uid = $1 and social_auth_type = $2`, [uid, TYPE, secret, { codesArray: recoveryCodes }, otp])
|
|
28
|
+
.then((el) => el.rows?.[0] || {});
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
// return a new secret until it's enabled
|
|
32
|
+
const generate = async ({ uid, pg }) => {
|
|
33
|
+
const { enabled } = await getSecret({ uid, TYPE, pg });
|
|
34
|
+
if (enabled)
|
|
35
|
+
return { enabled };
|
|
36
|
+
const secret = authenticator.generateSecret();
|
|
37
|
+
// console.log('secret', secret, 'length', secret.length, 'token', authenticator.generate(secret), 'verified', authenticator.verify({ secret, token: authenticator.generate(secret) }) );
|
|
38
|
+
const recoveryCodes = [
|
|
39
|
+
crypto.randomUUID(),
|
|
40
|
+
crypto.randomUUID(),
|
|
41
|
+
crypto.randomUUID(),
|
|
42
|
+
crypto.randomUUID(),
|
|
43
|
+
];
|
|
44
|
+
const otp = authenticator.keyuri(uid, "SOFTPRO", secret);
|
|
45
|
+
const qrCodeAsImageSource = await qrcode.toDataURL(otp);
|
|
46
|
+
// no entry in db
|
|
47
|
+
if (enabled === undefined) {
|
|
48
|
+
await addSecret({
|
|
49
|
+
uid,
|
|
50
|
+
secret,
|
|
51
|
+
TYPE,
|
|
52
|
+
pg,
|
|
53
|
+
recoveryCodes,
|
|
54
|
+
otp,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
await updateSecret({
|
|
59
|
+
uid,
|
|
60
|
+
secret,
|
|
61
|
+
TYPE,
|
|
62
|
+
pg,
|
|
63
|
+
recoveryCodes,
|
|
64
|
+
otp,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
qr: qrCodeAsImageSource,
|
|
69
|
+
key: secret,
|
|
70
|
+
otp,
|
|
71
|
+
recoveryCodes,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const verify = async ({ uid, code: token, pg }) => {
|
|
75
|
+
const { secret, enabled, recoveryCodes } = await getSecret({ uid, TYPE, pg });
|
|
76
|
+
// console.debug('secret', secret, 'enabled', enabled, 'verification', 'token', authenticator.generate(secret), authenticator.verify({ token: authenticator.generate(secret), secret }));
|
|
77
|
+
if (!secret) {
|
|
78
|
+
throw new Error("Включіть двофакторну аутентифікацію");
|
|
79
|
+
}
|
|
80
|
+
const isValid = authenticator.verify({ token, secret }) ||
|
|
81
|
+
recoveryCodes.reduce((result, recoveryCode) => result || recoveryCode === token, false);
|
|
82
|
+
if (!isValid) {
|
|
83
|
+
throw new Error("Невірний код");
|
|
84
|
+
}
|
|
85
|
+
return { enabled, recoveryCodes };
|
|
86
|
+
};
|
|
87
|
+
// checks if code is correct
|
|
88
|
+
// delete an entry from db to disable 2fa
|
|
89
|
+
// set the enable flag in db to activate 2fa
|
|
90
|
+
const toggle = async ({ uid, code, pg, enable }) => {
|
|
91
|
+
const { enabled, recoveryCodes } = await verify({
|
|
92
|
+
uid,
|
|
93
|
+
code,
|
|
94
|
+
pg,
|
|
95
|
+
});
|
|
96
|
+
if (enabled === enable) {
|
|
97
|
+
throw new Error("Вже знаходиться у даному стані");
|
|
98
|
+
}
|
|
99
|
+
if (enable) {
|
|
100
|
+
await enableSecret({
|
|
101
|
+
uid,
|
|
102
|
+
TYPE,
|
|
103
|
+
pg,
|
|
104
|
+
});
|
|
105
|
+
return recoveryCodes;
|
|
106
|
+
}
|
|
107
|
+
await deleteSecret({
|
|
108
|
+
uid,
|
|
109
|
+
TYPE,
|
|
110
|
+
pg,
|
|
111
|
+
});
|
|
112
|
+
return "Відключено";
|
|
113
|
+
};
|
|
114
|
+
export { generate, verify, toggle, getSecret, enableSecret, deleteSecret };
|
|
115
|
+
export default null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import config from "../../../../../config.js";
|
|
5
|
+
import getTemplate from "../../../../plugins/table/funcs/getTemplate.js";
|
|
6
|
+
import pgClients from "../../../../plugins/pg/pgClients.js";
|
|
7
|
+
import { handlebars } from "../../../../helpers/index.js";
|
|
8
|
+
import { verify, deleteSecret } from "./providers/totp.js";
|
|
9
|
+
import sendNotification from "../../../../plugins/auth/funcs/sendNotification.js";
|
|
10
|
+
const template = "recovery-codes-email-template";
|
|
11
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
/**
|
|
13
|
+
* Відновлення при втраті доступу до застосунку двохфакторної авторизації
|
|
14
|
+
*
|
|
15
|
+
* @method GET|POST
|
|
16
|
+
* @summary Відновлення при втраті доступу до застосунку двохфакторної авторизації
|
|
17
|
+
* @priority 3
|
|
18
|
+
* @alias recovery
|
|
19
|
+
* @type api
|
|
20
|
+
* @tag auth
|
|
21
|
+
* @requires 2fa
|
|
22
|
+
* @param {Object} body.code Код, який використовується для перевірки у заданому провайдері двофакторної авторизації
|
|
23
|
+
* @errors 500
|
|
24
|
+
* @returns {Number} status Номер помилки
|
|
25
|
+
* @returns {String|Object} error Опис помилки
|
|
26
|
+
* @returns {String|Object} message Повідомлення про успішне виконання або об'єкт з параметрами
|
|
27
|
+
* @returns {String} redirect Шлях до переадресації
|
|
28
|
+
*/
|
|
29
|
+
export default async function recoveryFunction(req, reply) {
|
|
30
|
+
const { pg = pgClients.client, session = {}, body = {} } = req;
|
|
31
|
+
if (!config?.auth?.["2factor"]) {
|
|
32
|
+
return reply.status(400).send("2fa not enabled");
|
|
33
|
+
}
|
|
34
|
+
if (!config.pg) {
|
|
35
|
+
return reply.status(400).send("empty pg");
|
|
36
|
+
}
|
|
37
|
+
const { code } = body;
|
|
38
|
+
const { uid, email } = session.passport?.user || {};
|
|
39
|
+
if (!code) {
|
|
40
|
+
if (!email) {
|
|
41
|
+
return reply.status(404).send("user recovery email not set");
|
|
42
|
+
}
|
|
43
|
+
// return reply.status(400).send('not enough params');
|
|
44
|
+
const customPt = await getTemplate("pt", template);
|
|
45
|
+
const pt = customPt ||
|
|
46
|
+
(await readFile(path.join(dirname, `../templates/pt/${template}.html`), "utf8"));
|
|
47
|
+
const recoveryCodes = await pg
|
|
48
|
+
.query(`select social_auth_obj->'codesArray' as "recoveryCodes" from admin.users_social_auth
|
|
49
|
+
where uid = $1 and social_auth_type = $2`, [uid, "TOTP"])
|
|
50
|
+
.then((el) => el.rows?.[0]?.recoveryCodes || []);
|
|
51
|
+
if (!recoveryCodes?.length) {
|
|
52
|
+
return reply.status(404).send("user recovery code not found");
|
|
53
|
+
// return { message: 'user recovery code not found', status: 404 };
|
|
54
|
+
}
|
|
55
|
+
const html = await handlebars.compile(pt)({
|
|
56
|
+
recoveryCodes: [recoveryCodes[0]],
|
|
57
|
+
domain: `${req.protocol || "https"}://${req.hostname}`,
|
|
58
|
+
});
|
|
59
|
+
await sendNotification({
|
|
60
|
+
pg,
|
|
61
|
+
to: email,
|
|
62
|
+
template: html,
|
|
63
|
+
title: `Recovery code for ${req.hostname} 2-factor authentication`,
|
|
64
|
+
nocache: config.local || config.debug,
|
|
65
|
+
});
|
|
66
|
+
return reply.redirect("/2factor?recovery=1");
|
|
67
|
+
// return reply.status(200).send('recovery code sent to user email');
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// validate recovery code
|
|
71
|
+
await verify({ uid, code, pg });
|
|
72
|
+
// delete old secret
|
|
73
|
+
await deleteSecret({
|
|
74
|
+
pg,
|
|
75
|
+
TYPE: "TOTP",
|
|
76
|
+
uid,
|
|
77
|
+
});
|
|
78
|
+
return reply.redirect("/2factor");
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return reply.status(500).send(err.toString());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import config from '../../../../../config.js';
|
|
2
|
+
import pgClients from '../../../../plugins/pg/pgClients.js';
|
|
3
|
+
import { toggle } from './providers/totp.js';
|
|
4
|
+
/**
|
|
5
|
+
* Включення/виключення двохфакторної авторизації для користувача
|
|
6
|
+
*
|
|
7
|
+
* @method GET
|
|
8
|
+
* @summary Включення/виключення двохфакторної авторизації
|
|
9
|
+
* @priority 2
|
|
10
|
+
* @alias toggle
|
|
11
|
+
* @type api
|
|
12
|
+
* @tag auth
|
|
13
|
+
* @requires 2fa
|
|
14
|
+
* @errors 500
|
|
15
|
+
* @returns {Number} status Номер помилки
|
|
16
|
+
* @returns {String|Object} error Опис помилки
|
|
17
|
+
* @returns {String|Object} message Повідомлення про успішне виконання або об'єкт з параметрами
|
|
18
|
+
*/
|
|
19
|
+
export default async function toggleFunction(req, reply) {
|
|
20
|
+
const { pg = pgClients.client, session = {}, query = {}, } = req;
|
|
21
|
+
const { uid } = session?.passport?.user || {};
|
|
22
|
+
const { code, enable } = query;
|
|
23
|
+
if (!config.pg) {
|
|
24
|
+
return reply.status(400).send('empty pg');
|
|
25
|
+
}
|
|
26
|
+
if (!uid) {
|
|
27
|
+
return reply.status(401).send('access restricted: unauthorized');
|
|
28
|
+
}
|
|
29
|
+
if (!code) {
|
|
30
|
+
return reply.status(400).send('param "code" is required');
|
|
31
|
+
}
|
|
32
|
+
if (!Object.hasOwn(query, 'enable')) {
|
|
33
|
+
return reply.status(400).send('param "enable" is required');
|
|
34
|
+
}
|
|
35
|
+
const data = await toggle({
|
|
36
|
+
pg, code, enable: enable === 'true', uid,
|
|
37
|
+
});
|
|
38
|
+
return reply.status(200).send(data);
|
|
39
|
+
}
|