@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.
Files changed (39) hide show
  1. package/dist/config.js +0 -1
  2. package/dist/index.js +28 -29
  3. package/dist/server/migrations/oauth.sql.sql +77 -0
  4. package/dist/server/migrations/properties.sql +1 -1
  5. package/dist/server/plugins/auth/funcs/authorizeUser.js +63 -0
  6. package/dist/server/plugins/auth/funcs/checkReferer.js +24 -0
  7. package/dist/server/plugins/auth/funcs/getQuery.js +127 -0
  8. package/dist/server/plugins/auth/funcs/jwt.js +63 -0
  9. package/dist/server/plugins/auth/funcs/logAuth.js +17 -0
  10. package/dist/server/plugins/auth/funcs/loginFile.js +41 -0
  11. package/dist/server/plugins/auth/funcs/loginUser.js +45 -0
  12. package/dist/server/plugins/auth/funcs/sendNotification.js +96 -0
  13. package/dist/server/plugins/auth/funcs/users.js +2 -0
  14. package/dist/server/plugins/auth/funcs/verifyPassword.js +30 -0
  15. package/dist/server/plugins/auth/index.js +110 -0
  16. package/dist/server/plugins/policy/funcs/checkPolicy.js +50 -100
  17. package/dist/server/plugins/policy/index.js +2 -2
  18. package/dist/server/routes/access/controllers/access.group.post.js +3 -3
  19. package/dist/server/routes/auth/controllers/2factor/generate.js +38 -0
  20. package/dist/server/routes/auth/controllers/2factor/providers/totp.js +115 -0
  21. package/dist/server/routes/auth/controllers/2factor/recovery.js +83 -0
  22. package/dist/server/routes/auth/controllers/2factor/toggle.js +39 -0
  23. package/dist/server/routes/auth/controllers/2factor/verify.js +68 -0
  24. package/dist/server/routes/auth/controllers/core/getUserInfo.js +37 -0
  25. package/dist/server/routes/auth/controllers/core/login.js +21 -0
  26. package/dist/server/routes/auth/controllers/core/logout.js +10 -0
  27. package/dist/server/routes/auth/controllers/core/passwordRecovery.js +151 -0
  28. package/dist/server/routes/auth/controllers/core/registration.js +96 -0
  29. package/dist/server/routes/auth/controllers/core/updateUserInfo.js +17 -0
  30. package/dist/server/routes/auth/controllers/euSign/authByData.js +115 -0
  31. package/dist/server/routes/auth/controllers/jwt/authorize.js +78 -0
  32. package/dist/server/routes/auth/controllers/jwt/token.js +67 -0
  33. package/dist/server/routes/auth/controllers/page/login2faTemplate.js +70 -0
  34. package/dist/server/routes/auth/controllers/page/loginEuSign.js +20 -0
  35. package/dist/server/routes/auth/controllers/page/loginTemplate.js +45 -0
  36. package/dist/server/routes/auth/index.js +87 -0
  37. package/dist/server/routes/util/index.js +9 -5
  38. package/dist/server.js +62 -0
  39. package/package.json +20 -8
@@ -0,0 +1,2 @@
1
+ const users = [];
2
+ export default users;
@@ -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, } = req;
7
- if (config.local || unittest || config.env === "test") {
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
- const body = JSON.stringify(req?.body || {}).substring(30);
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 isUser = config?.debug || !!user;
17
- const isServer = process.argv[2];
18
- const { policy = [] } = (routeOptions?.config ||
19
- {});
20
- /*= == 0.Check superadmin access === */
21
- if (policy.includes("admin") &&
22
- user?.user_type !== "admin" &&
23
- !config.auth?.disable) {
24
- logger.file("policy/access", {
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
- /*= == 1.File injection === */
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
- /* === 1.1 File === */
51
- const allowExtPublic = [".png", ".jpg", ".svg"];
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
- /* policy: skip if not API */
74
- const isApi = ["/files/", "/api/", "/api-user/", "/logger", "/file/"].filter((el) => path.includes(el)).length;
75
- if (!isApi) {
76
- return null;
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
- token: headers?.token,
108
- userId: headers?.uid,
109
- ip: req.ip,
110
- headers,
111
- message: "unauthorized",
77
+ stopWords,
78
+ uid: user?.uid,
112
79
  });
113
- return reply.status(401).send("unauthorized");
80
+ return reply.status(403).send("access restricted: 2");
114
81
  }
115
- /* === 3. policy: user === */
116
- if (!validToken &&
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
- /* === 4. policy: referer === */
131
- if (!validToken &&
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
- /* === 5. policy: site auth === */
146
- /* if (!validToken && !policy.includes("site") && !isAdmin) {
147
- logger.file("policy/site", {
148
- path,
149
- method,
150
- params,
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 './funcs/checkPolicy.js';
1
+ import checkPolicy from "./funcs/checkPolicy.js";
2
2
  async function plugin(fastify) {
3
- fastify.addHook('preParsing', async (request, reply) => {
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 { routesDB = [] } = await pg
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.includes(el.path) && el.actions)
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
+ }