@opengis/fastify-table 1.5.9 → 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 (37) hide show
  1. package/dist/index.js +28 -29
  2. package/dist/server/migrations/oauth.sql.sql +77 -0
  3. package/dist/server/plugins/auth/funcs/authorizeUser.js +63 -0
  4. package/dist/server/plugins/auth/funcs/checkReferer.js +24 -0
  5. package/dist/server/plugins/auth/funcs/getQuery.js +127 -0
  6. package/dist/server/plugins/auth/funcs/jwt.js +63 -0
  7. package/dist/server/plugins/auth/funcs/logAuth.js +17 -0
  8. package/dist/server/plugins/auth/funcs/loginFile.js +41 -0
  9. package/dist/server/plugins/auth/funcs/loginUser.js +45 -0
  10. package/dist/server/plugins/auth/funcs/sendNotification.js +96 -0
  11. package/dist/server/plugins/auth/funcs/users.js +2 -0
  12. package/dist/server/plugins/auth/funcs/verifyPassword.js +30 -0
  13. package/dist/server/plugins/auth/index.js +110 -0
  14. package/dist/server/plugins/policy/funcs/checkPolicy.js +33 -62
  15. package/dist/server/plugins/policy/index.js +2 -2
  16. package/dist/server/routes/access/controllers/access.group.post.js +3 -3
  17. package/dist/server/routes/auth/controllers/2factor/generate.js +38 -0
  18. package/dist/server/routes/auth/controllers/2factor/providers/totp.js +115 -0
  19. package/dist/server/routes/auth/controllers/2factor/recovery.js +83 -0
  20. package/dist/server/routes/auth/controllers/2factor/toggle.js +39 -0
  21. package/dist/server/routes/auth/controllers/2factor/verify.js +68 -0
  22. package/dist/server/routes/auth/controllers/core/getUserInfo.js +37 -0
  23. package/dist/server/routes/auth/controllers/core/login.js +21 -0
  24. package/dist/server/routes/auth/controllers/core/logout.js +10 -0
  25. package/dist/server/routes/auth/controllers/core/passwordRecovery.js +151 -0
  26. package/dist/server/routes/auth/controllers/core/registration.js +96 -0
  27. package/dist/server/routes/auth/controllers/core/updateUserInfo.js +17 -0
  28. package/dist/server/routes/auth/controllers/euSign/authByData.js +115 -0
  29. package/dist/server/routes/auth/controllers/jwt/authorize.js +78 -0
  30. package/dist/server/routes/auth/controllers/jwt/token.js +67 -0
  31. package/dist/server/routes/auth/controllers/page/login2faTemplate.js +70 -0
  32. package/dist/server/routes/auth/controllers/page/loginEuSign.js +20 -0
  33. package/dist/server/routes/auth/controllers/page/loginTemplate.js +45 -0
  34. package/dist/server/routes/auth/index.js +87 -0
  35. package/dist/server/routes/util/index.js +9 -5
  36. package/dist/server.js +62 -0
  37. package/package.json +19 -7
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
- import fp from "fastify-plugin";
5
4
  import proxy from "@fastify/http-proxy";
6
5
  import config from "./config.js";
7
6
  const { maxFileSize = 512 } = config;
@@ -16,6 +15,7 @@ import policyPlugin from "./server/plugins/policy/index.js";
16
15
  import metricPlugin from "./server/plugins/metric/index.js";
17
16
  import redisPlugin from "./server/plugins/redis/index.js";
18
17
  import loggerPlugin from "./server/plugins/logger/index.js";
18
+ import authPlugin from "./server/plugins/auth/index.js";
19
19
  // routes
20
20
  import cronRoutes from "./server/routes/cron/index.js";
21
21
  import crudRoutes from "./server/routes/crud/index.js";
@@ -33,6 +33,7 @@ import dblistRoutes from "./server/routes/dblist/index.js";
33
33
  import menuRoutes from "./server/routes/menu/index.js";
34
34
  import templatesRoutes from "./server/routes/templates/index.js";
35
35
  import widgetRoutes from "./server/routes/widget/index.js";
36
+ import authRoutes from "./server/routes/auth/index.js";
36
37
  // core templates && cls
37
38
  const filename = fileURLToPath(import.meta.url);
38
39
  const cwd = path.dirname(filename);
@@ -80,6 +81,7 @@ async function plugin(fastify, opt) {
80
81
  config.templates?.forEach((el) => addTemplateDir(el));
81
82
  addTemplateDir(path.join(cwd, "module/core"));
82
83
  // plugins / utils / funcs
84
+ await authPlugin(fastify, config); // fastify-auth api + hooks integrated to core
83
85
  policyPlugin(fastify);
84
86
  metricPlugin();
85
87
  redisPlugin(fastify);
@@ -96,6 +98,30 @@ async function plugin(fastify, opt) {
96
98
  keyGenerator: (req) => `${req.ip}-${req.raw.url.split("?")[0]}`,
97
99
  });
98
100
  }
101
+ if (config.dblist) {
102
+ dblistRoutes(fastify);
103
+ }
104
+ // routes / api
105
+ cronRoutes(fastify);
106
+ crudRoutes(fastify);
107
+ loggerRoutes(fastify);
108
+ propertiesRoutes(fastify);
109
+ tableRoutes(fastify, opt);
110
+ utilRoutes(fastify);
111
+ accessRoutes(fastify, opt);
112
+ widgetRoutes(fastify, opt);
113
+ menuRoutes(fastify, opt);
114
+ templatesRoutes(fastify, opt);
115
+ authRoutes(fastify, opt); // from fastify-auth
116
+ // fastify.register(import("./server/routes/auth/index.js"));
117
+ // from fastify-file
118
+ await fastify.register(import("@fastify/multipart"), {
119
+ limits: {
120
+ fileSize: maxFileSize * 1024 * 1024,
121
+ },
122
+ }); // content parser, await before adding upload routes
123
+ fastify.register(import("./server/routes/file/index.js"), opt);
124
+ fastify.register(import("./server/routes/grpc/index.js"), opt);
99
125
  config.proxy?.forEach?.((el) => {
100
126
  if (execName === "bun") {
101
127
  fastify.all(`${el.source}/*`, async (req, reply) => {
@@ -140,32 +166,5 @@ async function plugin(fastify, opt) {
140
166
  });
141
167
  }
142
168
  });
143
- if (config.dblist) {
144
- dblistRoutes(fastify);
145
- }
146
- // routes / api
147
- cronRoutes(fastify);
148
- crudRoutes(fastify);
149
- loggerRoutes(fastify);
150
- propertiesRoutes(fastify);
151
- tableRoutes(fastify, opt);
152
- utilRoutes(fastify);
153
- accessRoutes(fastify, opt);
154
- widgetRoutes(fastify, opt);
155
- menuRoutes(fastify, opt);
156
- templatesRoutes(fastify, opt);
157
- // from fastify-file
158
- await fastify.register(import("@fastify/multipart"), {
159
- limits: {
160
- fileSize: maxFileSize * 1024 * 1024,
161
- },
162
- }); // content parser, await before adding upload routes
163
- fastify.register(import("./server/routes/file/index.js"), opt);
164
- fastify.register(import("./server/routes/grpc/index.js"), opt);
165
- fastify.get("/api/test-proxy", {}, (req) => ({
166
- ...(req.headers || {}),
167
- sessionId: req.session?.sessionId,
168
- }));
169
- fastify.get("/api/config", { config: { policy: ["admin", "site"] } }, () => config);
170
169
  }
171
- export default fp(plugin);
170
+ export default plugin;
@@ -0,0 +1,77 @@
1
+ CREATE schema if not exists oauth;
2
+
3
+ CREATE TABLE if not exists oauth.clients (
4
+ client_id text PRIMARY KEY DEFAULT next_id(), -- ID клієнта (публічний ідентифікатор)
5
+ client_secret_hash text, -- Хеш секрету (NULL для public-клієнтів)
6
+ name text NOT NULL, -- Назва застосунку
7
+ type text NOT NULL CHECK (type IN ('public','confidential')),
8
+ token_endpoint_auth_method text NOT NULL CHECK (token_endpoint_auth_method IN ('client_secret_basic','client_secret_post','private_key_jwt','none')),
9
+ owner_user_id text, -- Власник/адміністратор клієнта (посилання на users.id or other id)
10
+
11
+ redirect_uris text[], -- Дозволені redirect_uri
12
+ grant_types text[] CHECK (case when grant_types is not null then grant_types <@ ARRAY['authorization_code','refresh_token','client_credentials','device_code']::text[] else true end),
13
+ require_pkce boolean NOT NULL DEFAULT true,
14
+ scopes text[],
15
+ allowed_cors_origins text[],
16
+ jwks jsonb, -- Вбудований JWK Set (опційно)
17
+
18
+ created_at timestamptz NOT NULL DEFAULT now(),
19
+ updated_at timestamptz NOT NULL DEFAULT now()
20
+ );
21
+
22
+ CREATE TABLE if not exists oauth.tokens (
23
+ id text PRIMARY KEY DEFAULT next_id(),
24
+ token_type text NOT NULL CHECK (token_type IN ('access','refresh')),
25
+ token_hash text NOT NULL UNIQUE, -- Argon2/bcrypt/SCrypt (хеш у застосунку)
26
+ token_hint text, -- останні 6-8 символів для діагностики (необов’язково)
27
+ jti text UNIQUE, -- JWT ID, якщо токен — JWT
28
+ client_id text NOT NULL REFERENCES oauth.clients(client_id) ON DELETE CASCADE,
29
+ user_id text, -- NULL для client_credentials
30
+ issuer text, -- iss
31
+ scopes text[],
32
+ claims jsonb, -- додаткові клейми
33
+ issued_at timestamptz NOT NULL DEFAULT now(),
34
+ expires_at timestamptz NOT NULL,
35
+ revoked_at timestamptz,
36
+ revocation_reason text,
37
+ ip inet -- IP видачі/використання (опційно)
38
+ );
39
+
40
+ COMMENT ON SCHEMA oauth IS 'Schema for OAuth2 / OpenID Connect clients and tokens';
41
+
42
+ -- Comments for oauth.clients
43
+ COMMENT ON TABLE oauth.clients IS 'OAuth 2.0 clients (applications) that can request tokens';
44
+
45
+ COMMENT ON COLUMN oauth.clients.client_id IS 'Client identifier (public ID, generated by next_id())';
46
+ COMMENT ON COLUMN oauth.clients.client_secret_hash IS 'Hashed client secret (NULL for public clients)';
47
+ COMMENT ON COLUMN oauth.clients.name IS 'Name of the application/client';
48
+ COMMENT ON COLUMN oauth.clients.type IS 'Client type: public or confidential';
49
+ COMMENT ON COLUMN oauth.clients.token_endpoint_auth_method IS 'Authentication method at token endpoint (client_secret_basic, client_secret_post, private_key_jwt, none)';
50
+ COMMENT ON COLUMN oauth.clients.owner_user_id IS 'Owner/administrator of the client (reference to users.id or external id)';
51
+ COMMENT ON COLUMN oauth.clients.redirect_uris IS 'Allowed redirect URIs';
52
+ COMMENT ON COLUMN oauth.clients.grant_types IS 'Allowed grant types (authorization_code, refresh_token, client_credentials, device_code)';
53
+ COMMENT ON COLUMN oauth.clients.require_pkce IS 'Whether PKCE is required (default true)';
54
+ COMMENT ON COLUMN oauth.clients.scopes IS 'Allowed OAuth2 scopes';
55
+ COMMENT ON COLUMN oauth.clients.allowed_cors_origins IS 'Allowed CORS origins for browser-based apps';
56
+ COMMENT ON COLUMN oauth.clients.jwks IS 'Embedded JSON Web Key Set (optional)';
57
+ COMMENT ON COLUMN oauth.clients.created_at IS 'Creation timestamp';
58
+ COMMENT ON COLUMN oauth.clients.updated_at IS 'Last update timestamp';
59
+
60
+ -- Comments for oauth.tokens
61
+ COMMENT ON TABLE oauth.tokens IS 'Issued OAuth 2.0 tokens (access or refresh)';
62
+
63
+ COMMENT ON COLUMN oauth.tokens.id IS 'Internal token ID (generated by next_id())';
64
+ COMMENT ON COLUMN oauth.tokens.token_type IS 'Type of token: access or refresh';
65
+ COMMENT ON COLUMN oauth.tokens.token_hash IS 'Secure hash of the token (Argon2/bcrypt/SCrypt)';
66
+ COMMENT ON COLUMN oauth.tokens.token_hint IS 'Optional hint (last 6–8 characters of token) for diagnostics';
67
+ COMMENT ON COLUMN oauth.tokens.jti IS 'JWT ID if token is a JWT (unique)';
68
+ COMMENT ON COLUMN oauth.tokens.client_id IS 'Reference to oauth.clients (issuing client)';
69
+ COMMENT ON COLUMN oauth.tokens.user_id IS 'User ID if bound to user (NULL for client_credentials flow)';
70
+ COMMENT ON COLUMN oauth.tokens.issuer IS 'Token issuer (iss claim)';
71
+ COMMENT ON COLUMN oauth.tokens.scopes IS 'Granted OAuth2 scopes for this token';
72
+ COMMENT ON COLUMN oauth.tokens.claims IS 'Additional claims (JSONB)';
73
+ COMMENT ON COLUMN oauth.tokens.issued_at IS 'Timestamp when issued';
74
+ COMMENT ON COLUMN oauth.tokens.expires_at IS 'Timestamp when token expires';
75
+ COMMENT ON COLUMN oauth.tokens.revoked_at IS 'Timestamp when revoked';
76
+ COMMENT ON COLUMN oauth.tokens.revocation_reason IS 'Reason for revocation (if any)';
77
+ COMMENT ON COLUMN oauth.tokens.ip IS 'IP address of issuance/usage (optional)';
@@ -0,0 +1,63 @@
1
+ import config from "../../../../config.js";
2
+ import logger from "../../logger/getLogger.js";
3
+ import applyHook from "../../hook/funcs/applyHook.js";
4
+ import logAuth from "./logAuth.js";
5
+ /*
6
+ session duration by default
7
+ * 10 hours = 600 minutes w/o keep (remember me checkbox)
8
+ * 30 days = 720 hours = 43200 minutes w/ keep
9
+ */
10
+ const { sessionTimeout = 600, sessionTimeoutKeep = 43200 } = config.auth || {};
11
+ const getIp = (req) => (req.headers?.["x-real-ip"] ||
12
+ req.headers?.["x-forwarded-for"] ||
13
+ req.ip ||
14
+ req.connection?.remoteAddress ||
15
+ "")
16
+ .split(":")
17
+ .pop();
18
+ export default async function authorizeUser(user, req, loginType = "login", expire) {
19
+ if (!user?.uid)
20
+ return "/logout";
21
+ // fastify/passport
22
+ await req.login(user);
23
+ const st = (req.body?.keep || req.query?.keep) === "on"
24
+ ? sessionTimeoutKeep
25
+ : sessionTimeout;
26
+ const maxAge = expire || st * 1000 * 60;
27
+ if (req.session?.cookie) {
28
+ req.session.cookie.maxAge = maxAge;
29
+ }
30
+ logger.file("auth", {
31
+ level: "DEBUG",
32
+ name: loginType,
33
+ response: { uid: user?.uid, name: user.name },
34
+ }, req);
35
+ const ip = getIp(req);
36
+ if (loginType === "login") {
37
+ await logAuth({
38
+ uid: user?.uid,
39
+ type: loginType,
40
+ ip,
41
+ });
42
+ }
43
+ const { href } = (await applyHook("afterAuth", {
44
+ ip,
45
+ user,
46
+ name: loginType,
47
+ referer: req.headers?.referer,
48
+ })) ||
49
+ {};
50
+ await req.session?.save?.();
51
+ if (req.method === "POST") {
52
+ return { message: "ok", status: "200" };
53
+ }
54
+ if (href) {
55
+ return href;
56
+ }
57
+ if (config.auth?.redirectAfter) {
58
+ return config.auth?.redirectAfter;
59
+ }
60
+ const redirectUrl = req.headers?.referer?.match?.(/[?&]redirect=([^&]+)/)?.[1] || "/";
61
+ // logger.debug('login', { redirect, ref: headers?.referer, sid: req.sid });
62
+ return redirectUrl.startsWith("/") ? redirectUrl : "/";
63
+ }
@@ -0,0 +1,24 @@
1
+ import config from "../../../../config.js";
2
+ import getRedis from "../../redis/funcs/getRedis.js";
3
+ const rclient = getRedis();
4
+ async function checkReferer({ req, referer, hostOauth, }) {
5
+ if (config.local || config.debug)
6
+ return false;
7
+ if (!referer && !hostOauth)
8
+ return true;
9
+ if (hostOauth?.includes(req.hostname)) {
10
+ const tokenData = config.redis
11
+ ? JSON.parse((await rclient.get(`auth_social:${req.query.data}`)) || "{}")
12
+ : null;
13
+ if (referer && tokenData?.provider === "google") {
14
+ return !referer.startsWith("https://accounts.google.com");
15
+ }
16
+ return false;
17
+ }
18
+ if (req?.session?.login_referer &&
19
+ !req.session?.login_referer?.includes(req.hostname)) {
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+ export default checkReferer;
@@ -0,0 +1,127 @@
1
+ const insertSocialSQL = `insert into admin.users_social_auth(uid, phone, user_name, sur_name, email, social_auth_id,
2
+ social_auth_type, social_auth_date, social_auth_obj, city, enabled)
3
+ select $9, $1, $2, $3, $4, $5,
4
+ $6, now(), $7, $8, true
5
+ on conflict(social_auth_id,email) do update set
6
+ phone=excluded.phone, user_name=excluded.user_name,
7
+ sur_name=excluded.sur_name, email=excluded.email, social_auth_id=excluded.social_auth_id,
8
+ social_auth_type=excluded.social_auth_type, social_auth_date=excluded.social_auth_date,
9
+ social_auth_obj=excluded.social_auth_obj, city=excluded.city, enabled=excluded.enabled`;
10
+ const insertUserSQL = `insert into admin.users (enabled, phone, user_name, sur_name, father_name, email, user_rnokpp)
11
+ values(true, $1, $2, $3, $4, $5, $6)
12
+ on conflict (user_rnokpp) do update set
13
+ phone=excluded.phone,
14
+ user_name=excluded.user_name,
15
+ sur_name=excluded.sur_name,
16
+ father_name=excluded.father_name,
17
+ email=excluded.email
18
+ returning uid`;
19
+ const updateUserSQL = `update admin.users set
20
+ phone=$1, user_name=$2, sur_name=$3,
21
+ father_name=coalesce($4, father_name), email=$5, social_auth_id=$6,
22
+ social_auth_type=$7 where uid=$8 returning uid`;
23
+ const deleteSocialIdSQL = `delete from admin.users_social_auth where social_auth_id=$1 or email=$2`;
24
+ import config from "../../../../config.js";
25
+ import logger from "../../logger/getLogger.js";
26
+ export default async function getQuery({ pg, data, }) {
27
+ if (typeof data !== "object") {
28
+ throw new Error("invalid param data");
29
+ }
30
+ const { drfocode, // personal eusign code
31
+ edrpoucode, // organization eusign code
32
+ locality: city, middlename, email: emailOriginal, phone, sub, // google account ID
33
+ } = data?.user || data || {};
34
+ // google and id.gov.ua compatibility
35
+ const email = ["", "n/a"].includes(emailOriginal) ? null : emailOriginal;
36
+ const name = data?.user
37
+ ? data?.user?.given_name || data?.user?.givenname
38
+ : data?.givenname;
39
+ const surname = data?.user
40
+ ? data?.user?.family_name || data?.user?.lastname
41
+ : data?.lastname;
42
+ const id = drfocode || edrpoucode || sub;
43
+ if (!id && !email) {
44
+ throw new Error("invalid params: no id or email");
45
+ }
46
+ const authType = data?.type || "govid";
47
+ const isSocialPK = pg.pk?.["admin.users_social_auth"];
48
+ const userId = await pg
49
+ .query(`select uid from admin.users where ${id ? "user_rnokpp=$1" : "email=$1"}`, [id || email])
50
+ .then((el) => el.rows?.[0]?.uid);
51
+ const socialUserId = isSocialPK
52
+ ? await pg
53
+ .query(`select uid from admin.users_social_auth where $1 in (social_auth_id,email) limit 1`, [email || id])
54
+ .then((el) => el.rows?.[0]?.uid)
55
+ : null;
56
+ const client = await pg.connect();
57
+ try {
58
+ await client.query("BEGIN");
59
+ if (userId || socialUserId) {
60
+ // delete old user, prevent duplicates / access level issues
61
+ await client.query("delete from admin.users where $1 in (user_rnokpp, social_auth_id) and uid<>$2", [id, userId || socialUserId]);
62
+ }
63
+ const uid = userId || socialUserId
64
+ ? await client
65
+ .query(updateUserSQL, [
66
+ phone,
67
+ name,
68
+ surname,
69
+ middlename,
70
+ email,
71
+ id,
72
+ authType,
73
+ userId || socialUserId,
74
+ ])
75
+ .then((el) => el.rows?.[0]?.uid)
76
+ : await client
77
+ .query(insertUserSQL, [phone, name, surname, middlename, email, id])
78
+ .then((el) => el.rows?.[0]?.uid);
79
+ const args = [
80
+ phone,
81
+ name,
82
+ surname,
83
+ email,
84
+ id,
85
+ authType,
86
+ data?.user || data,
87
+ city,
88
+ uid,
89
+ ];
90
+ if (isSocialPK) {
91
+ await client.query(deleteSocialIdSQL, [id, email]);
92
+ await client.query(insertSocialSQL, args);
93
+ }
94
+ await client.query("COMMIT");
95
+ logger.file("auth/getQuery", {
96
+ data: config.debug ? data : undefined,
97
+ id,
98
+ email,
99
+ name,
100
+ surname,
101
+ authType,
102
+ uid,
103
+ socialUserId,
104
+ });
105
+ }
106
+ catch (err) {
107
+ await client.query("ROLLBACK");
108
+ logger.file("auth/getQuery/error", {
109
+ error: err.toString(),
110
+ data: config.debug ? data : undefined,
111
+ id,
112
+ email,
113
+ userId,
114
+ socialUserId,
115
+ stack: err.stack,
116
+ });
117
+ throw new Error("Помилка авторизації. Зверніться до адміністратора");
118
+ }
119
+ finally {
120
+ client.release();
121
+ }
122
+ const result = await pg
123
+ .query(`select * from admin.users where uid =(select uid from admin.users_social_auth
124
+ where ${id ? "social_auth_id=$1" : "email=$1"} limit 1)`, [id || email])
125
+ .then((el) => el.rows?.[0] || {});
126
+ return result;
127
+ }
@@ -0,0 +1,63 @@
1
+ import { createHmac, scrypt, randomBytes } from "node:crypto";
2
+ import util from "node:util";
3
+ import config from "../../../../config.js";
4
+ const scryptAsync = util.promisify(scrypt);
5
+ const { jwtSecret = "65450754381cfaf768eeb4bb33326529b48a40ffdb6e15d84dc224dff527166f", } = config.auth || {};
6
+ const jwtHeader = Buffer.from(JSON.stringify({
7
+ alg: "HS256",
8
+ typ: "JWT",
9
+ })).toString("base64");
10
+ export async function scryptHash(code) {
11
+ const salt = randomBytes(16).toString("hex");
12
+ const derived = (await scryptAsync(code, salt, 64)); // 64 bytes
13
+ return `${salt}:${derived.toString("hex")}`;
14
+ }
15
+ export async function scryptVerify(stored, code) {
16
+ const [salt, keyHex] = stored.split(":");
17
+ const derived = (await scryptAsync(code, salt, 64));
18
+ return keyHex === derived.toString("hex");
19
+ }
20
+ export function sign(uid, secret = jwtSecret, exp = 90000) {
21
+ if (typeof uid !== "string")
22
+ throw new Error("uid must be a string");
23
+ if (secret && typeof secret !== "string")
24
+ throw new Error("secret must be a string");
25
+ if (typeof exp !== "number")
26
+ throw new Error("exp must be a number");
27
+ const jwtPayload = Buffer.from(JSON.stringify({
28
+ uid,
29
+ exp,
30
+ created: Date.now(),
31
+ })).toString("base64");
32
+ const jwtEncrypted = [jwtHeader, jwtPayload].join(".");
33
+ const signature = createHmac("sha256", secret)
34
+ .update(jwtEncrypted)
35
+ .digest("base64");
36
+ return `${jwtEncrypted}.${signature}`;
37
+ }
38
+ export function verify(token, secret = jwtSecret) {
39
+ if (!token)
40
+ throw new Error("not enough params: token");
41
+ if (!secret)
42
+ throw new Error("not enough params: secret");
43
+ const split = token.split(".");
44
+ const signature = split[2];
45
+ try {
46
+ const header = JSON.parse(Buffer.from(split[0], "base64").toString());
47
+ const payload = JSON.parse(Buffer.from(split[1], "base64").toString());
48
+ const jwtHeader = Buffer.from(JSON.stringify(header)).toString("base64");
49
+ const jwtPayload = Buffer.from(JSON.stringify(payload)).toString("base64");
50
+ const jwtEncryptedExpected = [jwtHeader, jwtPayload].join(".");
51
+ const expectedSignature = createHmac("sha256", secret)
52
+ .update(jwtEncryptedExpected)
53
+ .digest("base64");
54
+ if (signature === expectedSignature) {
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ catch (err) {
60
+ return false;
61
+ }
62
+ }
63
+ export default null;
@@ -0,0 +1,17 @@
1
+ import pgClients from "../../pg/pgClients.js";
2
+ import dataInsert from "../../crud/funcs/dataInsert.js";
3
+ export default async function logAuth({ uid, type = "login", data, ip, }, pg = pgClients.client) {
4
+ const res = await dataInsert({
5
+ pg,
6
+ table: "log.user_auth",
7
+ uid,
8
+ data: {
9
+ user_id: uid,
10
+ auth_date: new Date().toISOString(),
11
+ auth_type: type,
12
+ auth_data: data,
13
+ ip,
14
+ },
15
+ });
16
+ return res;
17
+ }
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import config from '../../../../config.js';
5
+ import users from './users.js';
6
+ import authorizeUser from './authorizeUser.js';
7
+ export default async function loginFile(req, reply) {
8
+ const { username, password } = req.method === 'POST' ? req.body : req.query;
9
+ const filepath = path.join(process.cwd(), 'passwd');
10
+ if (!users?.length) {
11
+ const fileExists = existsSync(filepath);
12
+ if (!fileExists) {
13
+ req.log.error(req, 'passwd file not exists');
14
+ return { error: 'login error', status: 500 };
15
+ }
16
+ // parse file on start up
17
+ const data = readFileSync(filepath, 'utf8');
18
+ const separator = data.indexOf('\\r\\n') !== -1 ? '\r\n' : '\n';
19
+ const rows = data.split(separator).map((row) => {
20
+ const [name, passwd, usertype = 'regular', uid] = row.split(':');
21
+ return { username: name, password: passwd, usertype, uid };
22
+ });
23
+ rows.forEach((row) => users.push(row));
24
+ }
25
+ // check user / password
26
+ const user = users.find((el) => el.username === username);
27
+ const hashPasswd = createHash('sha1').update(`${password}${user?.salt || ''}`).digest('hex');
28
+ if (!user?.password || user.password !== hashPasswd) {
29
+ const txt = 'Invalid user or password';
30
+ return req.method === 'GET'
31
+ ? reply.status(302).redirect(`/login?confirm=wrong_pass&message=${txt}`)
32
+ : reply.status(400).send({ message: txt });
33
+ }
34
+ const resultUser = {
35
+ uid: user?.uid || config?.auth?.uid || username,
36
+ user_name: username,
37
+ user_type: user.usertype || 'regular',
38
+ };
39
+ const href = await authorizeUser(resultUser, req, 'loginFile');
40
+ reply.redirect(href);
41
+ }
@@ -0,0 +1,45 @@
1
+ import config from "../../../../config.js";
2
+ import pgClients from "../../pg/pgClients.js";
3
+ import logger from "../../logger/getLogger.js";
4
+ import verifyPassword from "./verifyPassword.js";
5
+ import authorizeUser from "./authorizeUser.js";
6
+ export default async function loginUser(req, reply) {
7
+ const { username, password } = (req.method === "POST" ? req.body : req.query) || {};
8
+ const { pg = pgClients.client } = req;
9
+ if (!config.pg) {
10
+ return req.method === "GET"
11
+ ? reply.status(302).redirect("/login?confirm=wrong_pass&message=empty pg")
12
+ : reply.status(400).send({ message: "empty pg" });
13
+ }
14
+ if (!config.redis) {
15
+ return req.method === "GET"
16
+ ? reply
17
+ .status(302)
18
+ .redirect("/login?confirm=wrong_pass&message=empty redis")
19
+ : reply.status(400).send({ message: "empty redis" });
20
+ }
21
+ const { user, message = "invalid user" } = await verifyPassword({
22
+ pg,
23
+ username,
24
+ password,
25
+ }) || {};
26
+ if (!user) {
27
+ return req.method === "GET"
28
+ ? reply
29
+ .status(302)
30
+ .redirect(`/login?confirm=wrong_pass&message=${message}`)
31
+ : reply.status(400).send({ message });
32
+ }
33
+ if (!user && (req.query?.username || req.body?.username)) {
34
+ return req.method === "GET"
35
+ ? reply
36
+ .status(302)
37
+ .redirect(`/login?confirm=wrong_pass&message=${message}`)
38
+ : reply.status(400).send({ message });
39
+ }
40
+ logger.metrics("user.login");
41
+ const href = await authorizeUser(user, req, "login");
42
+ return req.method === "GET"
43
+ ? reply.status(302).redirect(href)
44
+ : reply.status(200).send(href);
45
+ }
@@ -0,0 +1,96 @@
1
+ import nodemailer from "nodemailer";
2
+ import config from "../../../../config.js";
3
+ import { handlebars } from "../../../helpers/index.js";
4
+ import pgClients from "../../pg/pgClients.js";
5
+ import getTemplate from "../../table/funcs/getTemplate.js";
6
+ import getRedis from "../../redis/funcs/getRedis.js";
7
+ import logger from "../../logger/getLogger.js";
8
+ const rclient = getRedis();
9
+ // eslint-disable-next-line max-len, no-control-regex
10
+ const emailReg = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g;
11
+ async function generateNotificationContent({ pg, table, template, id, message, data: data1, }) {
12
+ if (template) {
13
+ const data = table && id && config.pg
14
+ ? await pg
15
+ .query(`select * from ${table} where ${pg.pk[table]}=$1`, [id])
16
+ .then((el) => el.rows?.[0] || {})
17
+ : data1;
18
+ const body = await getTemplate("pt", template);
19
+ const html = handlebars.compile(body || template || "template not found")(data || {});
20
+ return html;
21
+ }
22
+ return message;
23
+ }
24
+ async function sendEmail({ to, from, subject, html, attachments }) {
25
+ if (!to?.length) {
26
+ throw new Error("empty to list");
27
+ }
28
+ const { mailSetting = {} } = config;
29
+ /*= == check service and setting === */
30
+ if (!mailSetting.service) {
31
+ logger.file("notification/warn", {
32
+ to,
33
+ from,
34
+ message: "service is not defined in config",
35
+ });
36
+ return null;
37
+ }
38
+ Object.assign(mailSetting, { rejectUnauthorized: false });
39
+ if (mailSetting.port === 465) {
40
+ Object.assign(mailSetting, { secure: true });
41
+ }
42
+ const transport = nodemailer.createTransport(mailSetting);
43
+ const result = await transport.sendMail({
44
+ from: from || mailSetting.from,
45
+ to,
46
+ subject,
47
+ html,
48
+ attachments,
49
+ });
50
+ return result;
51
+ }
52
+ export default async function notification({ pg = pgClients.client, provider = ["email"], from, to, template, table, message, title, data, id, nocache, }, unittest) {
53
+ if (pg?.readonly) {
54
+ return null;
55
+ }
56
+ const keyTo = `${pg?.options?.database}:mail:${provider[0]}:${to || ""}${id || ""}${table || ""}${title || ""}`;
57
+ const uniqueTo = config.redis ? await rclient.setnx(keyTo, 1) : true;
58
+ if (!uniqueTo && !nocache) {
59
+ logger.file("notification/sent", { keyTo, send: uniqueTo, nocache });
60
+ return `already sent: ${keyTo}`;
61
+ }
62
+ if (config.redis) {
63
+ await rclient.expire(keyTo, 1000 * 600);
64
+ }
65
+ if (!to.length) {
66
+ return null;
67
+ }
68
+ if (!(Array.isArray(provider) && provider.length)) {
69
+ return "notification provider - must be array and not empty";
70
+ }
71
+ const html = await generateNotificationContent({
72
+ pg,
73
+ table,
74
+ template,
75
+ id,
76
+ message,
77
+ data,
78
+ });
79
+ if (provider.includes("email")) {
80
+ const toEmail = Array.isArray(to)
81
+ ? to.map((emails) => emails.match(emailReg)?.join(","))
82
+ : to;
83
+ const result = await sendEmail({
84
+ attachments: [],
85
+ html,
86
+ subject: title,
87
+ from,
88
+ to: unittest && config.mailSetting?.to ? config.mailSetting?.to : toEmail,
89
+ });
90
+ if (config.debug) {
91
+ console.log(result);
92
+ }
93
+ return result;
94
+ }
95
+ return null;
96
+ }
@@ -0,0 +1,2 @@
1
+ const users = [];
2
+ export default users;