mbkauthe 4.9.0 → 5.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.
Files changed (45) hide show
  1. package/README.md +98 -112
  2. package/docs/README.md +33 -0
  3. package/docs/STYLE.md +40 -0
  4. package/docs/diagrams/auth-processes.mmd +120 -0
  5. package/docs/diagrams/c.md +122 -0
  6. package/docs/{env.md → guides/configuration.md} +2 -1
  7. package/docs/{db.md → guides/database.md} +4 -2
  8. package/docs/images/auth-process.svg +102 -1
  9. package/docs/images/auth-processes.svg +1 -0
  10. package/docs/reference/api/authentication.md +105 -0
  11. package/docs/{api.md → reference/api/endpoints.md} +31 -861
  12. package/docs/reference/api/examples.md +251 -0
  13. package/docs/reference/api/middleware.md +239 -0
  14. package/docs/reference/api/operations.md +52 -0
  15. package/docs/reference/api.md +19 -0
  16. package/docs/{error-messages.md → reference/error-codes.md} +2 -0
  17. package/docs/schema/db.sql +328 -0
  18. package/index.js +5 -3
  19. package/lib/config/cookies.js +84 -18
  20. package/lib/config/index.js +3 -1
  21. package/lib/config/tokenScopes.js +1 -1
  22. package/lib/createTable.js +95 -8
  23. package/lib/db/AuthRepository.js +57 -16
  24. package/lib/db/BaseRepository.js +9 -1
  25. package/lib/db/dialects/postgres.js +1 -1
  26. package/lib/main.js +5 -5
  27. package/lib/middleware/auth.js +201 -218
  28. package/lib/middleware/index.js +13 -14
  29. package/lib/middleware/scopeValidator.js +8 -3
  30. package/lib/pool.js +5 -6
  31. package/lib/routes/auth.js +42 -47
  32. package/lib/routes/dbLogs.js +247 -29
  33. package/lib/routes/misc.js +6 -4
  34. package/lib/routes/oauth.js +19 -23
  35. package/lib/utils/dbQueryLogger.js +485 -80
  36. package/lib/utils/errors.js +1 -1
  37. package/lib/utils/logger.js +12 -0
  38. package/lib/utils/timingSafeToken.js +1 -1
  39. package/package.json +4 -3
  40. package/public/main.css +1 -1
  41. package/test.spec.js +515 -48
  42. package/views/pages/dbLogs.handlebars +618 -420
  43. package/docs/auth-processes.mmd +0 -71
  44. package/docs/db.sql +0 -276
  45. /package/docs/{auth-flows.mmd → diagrams/auth-flows.mmd} +0 -0
@@ -0,0 +1,328 @@
1
+ -- imports
2
+
3
+ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
4
+
5
+
6
+ -- Table: Users
7
+ CREATE TABLE IF NOT EXISTS "Users" (
8
+ id integer GENERATED ALWAYS AS IDENTITY NOT NULL,
9
+ "UserName" text,
10
+ "Password" text DEFAULT '12345670'::text,
11
+ "Active" boolean DEFAULT false,
12
+ "Role" text DEFAULT 'NormalUser'::text,
13
+ "HaveMailAccount" boolean DEFAULT false,
14
+ "AllowedApps" jsonb DEFAULT '["Portal", "mbkauthe"]'::jsonb,
15
+ created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
16
+ updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
17
+ last_login timestamp with time zone,
18
+ "PasswordEnc" character varying(255),
19
+ "FullName" character varying(100),
20
+ email text DEFAULT 'support@mbktech.org'::text,
21
+ "Image" text DEFAULT 'https://portal.mbktech.org/icon.svg'::text,
22
+ "Bio" text DEFAULT 'I am ....'::text,
23
+ "SocialAccounts" text DEFAULT '{}'::text,
24
+ "Positions" jsonb DEFAULT '{"Not_Permanent": "Member Is Not Permanent"}'::jsonb,
25
+ CONSTRAINT "Users_pkey" PRIMARY KEY (id),
26
+ CONSTRAINT "Users_UserName_key" UNIQUE ("UserName")
27
+ );
28
+ CREATE INDEX IF NOT EXISTS idx_users_active ON "Users" USING btree ("Active");
29
+ CREATE INDEX IF NOT EXISTS idx_users_allowedapps_gin ON "Users" USING gin ("AllowedApps");
30
+ CREATE INDEX IF NOT EXISTS idx_users_email ON "Users" USING btree (email);
31
+ CREATE INDEX IF NOT EXISTS idx_users_last_login ON "Users" USING btree (last_login);
32
+ CREATE INDEX IF NOT EXISTS idx_users_positions_gin ON "Users" USING gin ("Positions");
33
+ CREATE INDEX IF NOT EXISTS idx_users_role ON "Users" USING btree ("Role");
34
+ CREATE INDEX IF NOT EXISTS idx_users_username_cover ON "Users" USING btree ("UserName") INCLUDE ("Active", "Role");
35
+ COMMENT ON TABLE "Users" IS '[core]';
36
+
37
+ -- Table: ApiTokens
38
+ CREATE TABLE IF NOT EXISTS "ApiTokens" (
39
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
40
+ "UserName" character varying(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
41
+ "Name" character varying(255) NOT NULL,
42
+ "TokenHash" character varying(128) NOT NULL,
43
+ "Prefix" character varying(32) NOT NULL,
44
+ "Permissions" jsonb DEFAULT '{"scope": "read-only", "allowedApps": null}'::jsonb NOT NULL,
45
+ "LastUsed" timestamp with time zone,
46
+ "CreatedAt" timestamp with time zone DEFAULT now(),
47
+ "ExpiresAt" timestamp with time zone,
48
+ CONSTRAINT "ApiTokens_pkey" PRIMARY KEY (id),
49
+ CONSTRAINT "ApiTokens_TokenHash_key" UNIQUE ("TokenHash"),
50
+ CONSTRAINT chk_apitokens_permissions_scope CHECK ((("Permissions" ->> 'scope'::text) = ANY (ARRAY['read-only'::text, 'write'::text]))),
51
+ CONSTRAINT chk_apitokens_name_not_empty CHECK ((length(TRIM(BOTH FROM "Name")) > 0)),
52
+ CONSTRAINT chk_apitokens_expires_future CHECK ((("ExpiresAt" IS NULL) OR ("ExpiresAt" > "CreatedAt")))
53
+ );
54
+ CREATE INDEX IF NOT EXISTS idx_apitokens_expires ON "ApiTokens" USING btree ("ExpiresAt") WHERE ("ExpiresAt" IS NOT NULL);
55
+ CREATE INDEX IF NOT EXISTS idx_apitokens_permissions_gin ON "ApiTokens" USING gin ("Permissions");
56
+ CREATE INDEX IF NOT EXISTS idx_apitokens_permissions_scope ON "ApiTokens" USING btree ((("Permissions" ->> 'scope'::text)));
57
+ CREATE INDEX IF NOT EXISTS idx_apitokens_username_created ON "ApiTokens" USING btree ("UserName", "CreatedAt" DESC);
58
+ COMMENT ON TABLE "ApiTokens" IS '[core]';
59
+
60
+ -- Table: PasswordResets
61
+ CREATE TABLE IF NOT EXISTS "PasswordResets" (
62
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
63
+ "UserName" character varying(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
64
+ "resetToken" text,
65
+ "resetTokenExpires" timestamp with time zone,
66
+ "resetAttempts" integer DEFAULT 0,
67
+ "lastResetAttempt" timestamp with time zone,
68
+ created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
69
+ CONSTRAINT "PasswordResets_pkey" PRIMARY KEY (id)
70
+ );
71
+ CREATE INDEX IF NOT EXISTS idx_password_resets_token ON "PasswordResets" USING btree ("resetToken");
72
+ COMMENT ON TABLE "PasswordResets" IS '[core]';
73
+
74
+ -- Table: Sessions
75
+ CREATE TABLE IF NOT EXISTS "Sessions" (
76
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
77
+ "UserName" character varying(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
78
+ created_at timestamp with time zone DEFAULT now(),
79
+ expires_at timestamp with time zone,
80
+ meta jsonb,
81
+ CONSTRAINT "Sessions_pkey" PRIMARY KEY (id)
82
+ );
83
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires ON "Sessions" USING btree (expires_at) WHERE (expires_at IS NOT NULL);
84
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_created ON "Sessions" USING btree ("UserName", created_at);
85
+ CREATE INDEX IF NOT EXISTS idx_sessions_username_expires ON "Sessions" USING btree ("UserName", expires_at);
86
+ COMMENT ON TABLE "Sessions" IS '[core]';
87
+
88
+ -- Table: TrustedDevices
89
+ CREATE TABLE IF NOT EXISTS "TrustedDevices" (
90
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
91
+ "UserName" character varying(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
92
+ "DeviceToken" character varying(64) NOT NULL,
93
+ "DeviceName" character varying(255),
94
+ "UserAgent" text,
95
+ "IpAddress" character varying(45),
96
+ "CreatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
97
+ "ExpiresAt" timestamp with time zone NOT NULL,
98
+ "LastUsed" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
99
+ CONSTRAINT "TrustedDevices_pkey" PRIMARY KEY (id),
100
+ CONSTRAINT "TrustedDevices_DeviceToken_key" UNIQUE ("DeviceToken")
101
+ );
102
+ CREATE INDEX IF NOT EXISTS idx_trusted_devices_expires ON "TrustedDevices" USING btree ("ExpiresAt");
103
+ CREATE INDEX IF NOT EXISTS idx_trusted_devices_username_expires ON "TrustedDevices" USING btree ("UserName", "ExpiresAt");
104
+ CREATE INDEX IF NOT EXISTS idx_trusted_devices_token_user_expires ON "TrustedDevices" USING btree ("DeviceToken", "UserName", "ExpiresAt");
105
+ COMMENT ON TABLE "TrustedDevices" IS '[core]';
106
+
107
+ -- Table: TwoFA
108
+ CREATE TABLE IF NOT EXISTS "TwoFA" (
109
+ "UserName" text NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
110
+ "TwoFAStatus" boolean DEFAULT false NOT NULL,
111
+ "TwoFASecret" text,
112
+ CONSTRAINT "TwoFA_pkey" PRIMARY KEY ("UserName")
113
+ );
114
+ CREATE INDEX IF NOT EXISTS idx_twofa_username_status ON "TwoFA" USING btree ("UserName", "TwoFAStatus");
115
+ COMMENT ON TABLE "TwoFA" IS '[core]';
116
+
117
+ -- Table: ai_history_chatapi
118
+ CREATE TABLE IF NOT EXISTS ai_history_chatapi (
119
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
120
+ conversation_id uuid DEFAULT uuid_generate_v4() NOT NULL,
121
+ conversation_history jsonb NOT NULL,
122
+ created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
123
+ updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
124
+ username character varying(100) NOT NULL,
125
+ is_deleted boolean DEFAULT false,
126
+ CONSTRAINT ai_history_chatapi_pkey PRIMARY KEY (id)
127
+ );
128
+ CREATE INDEX IF NOT EXISTS idx_chat_deleted ON ai_history_chatapi USING btree (is_deleted);
129
+ CREATE INDEX IF NOT EXISTS idx_chat_history_gin ON ai_history_chatapi USING gin (conversation_history);
130
+ CREATE INDEX IF NOT EXISTS idx_chat_username ON ai_history_chatapi USING btree (username);
131
+ COMMENT ON TABLE "ai_history_chatapi" IS '[core]';
132
+
133
+ -- Table: app_access_requests
134
+ CREATE TABLE IF NOT EXISTS app_access_requests (
135
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
136
+ username character varying(255) NOT NULL,
137
+ app character varying(255) NOT NULL,
138
+ message text,
139
+ status character varying(20) DEFAULT 'pending'::character varying,
140
+ admin_notes text,
141
+ reviewed_by character varying(255),
142
+ created_at timestamp without time zone DEFAULT now(),
143
+ updated_at timestamp without time zone DEFAULT now(),
144
+ CONSTRAINT app_access_requests_status_check CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'approved'::character varying, 'rejected'::character varying])::text[]))),
145
+ CONSTRAINT app_access_requests_pkey PRIMARY KEY (id)
146
+ );
147
+ COMMENT ON TABLE "app_access_requests" IS '[core]';
148
+
149
+ -- Table: plan_upgrade_requests
150
+ CREATE TABLE IF NOT EXISTS plan_upgrade_requests (
151
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
152
+ username character varying(255) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
153
+ curr_role character varying(50) NOT NULL,
154
+ requested_plan character varying(50) NOT NULL,
155
+ req_role character varying(50) NOT NULL,
156
+ reason text NOT NULL,
157
+ experience text,
158
+ portfolio character varying(500),
159
+ linkedin character varying(500),
160
+ github character varying(500),
161
+ additional_info text,
162
+ status character varying(20) DEFAULT 'pending'::character varying,
163
+ admin_notes text,
164
+ reviewed_by character varying(255),
165
+ created_at timestamp without time zone DEFAULT now(),
166
+ updated_at timestamp without time zone DEFAULT now(),
167
+ CONSTRAINT plan_upgrade_requests_status_check CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'approved'::character varying, 'rejected'::character varying])::text[]))),
168
+ CONSTRAINT plan_upgrade_requests_pkey PRIMARY KEY (id)
169
+ );
170
+ CREATE INDEX IF NOT EXISTS idx_created_at ON plan_upgrade_requests USING btree (created_at);
171
+ CREATE INDEX IF NOT EXISTS idx_req_role_status ON plan_upgrade_requests USING btree (req_role, status);
172
+ CREATE INDEX IF NOT EXISTS idx_requested_plan ON plan_upgrade_requests USING btree (requested_plan);
173
+ CREATE INDEX IF NOT EXISTS idx_status ON plan_upgrade_requests USING btree (status);
174
+ CREATE INDEX IF NOT EXISTS idx_status_created_at ON plan_upgrade_requests USING btree (status, created_at DESC);
175
+ CREATE INDEX IF NOT EXISTS idx_username_status ON plan_upgrade_requests USING btree (username, status);
176
+ COMMENT ON TABLE "plan_upgrade_requests" IS '[core]';
177
+
178
+ -- Table: session
179
+ CREATE TABLE IF NOT EXISTS session (
180
+ sid character varying NOT NULL,
181
+ sess json NOT NULL,
182
+ expire timestamp(6) without time zone NOT NULL,
183
+ username text REFERENCES "Users"("UserName") ON DELETE CASCADE,
184
+ last_activity timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
185
+ CONSTRAINT session_pkey PRIMARY KEY (sid)
186
+ );
187
+ CREATE INDEX IF NOT EXISTS idx_session_expire ON session USING btree (expire);
188
+ CREATE INDEX IF NOT EXISTS idx_session_last_activity ON session USING btree (last_activity);
189
+ CREATE INDEX IF NOT EXISTS idx_session_user_id ON session USING btree ((((sess -> 'user'::text) ->> 'id'::text)));
190
+ COMMENT ON TABLE "session" IS '[core]';
191
+
192
+ -- Table: todos
193
+ CREATE TABLE IF NOT EXISTS todos (
194
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
195
+ username character varying(50) NOT NULL REFERENCES "Users"("UserName"),
196
+ title character varying(255) NOT NULL,
197
+ description text,
198
+ completed boolean DEFAULT false,
199
+ type character varying(20) DEFAULT 'personal'::character varying,
200
+ assigned boolean DEFAULT false,
201
+ assigneduser character varying(50) DEFAULT 'none'::character varying,
202
+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
203
+ updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
204
+ CONSTRAINT todos_type_check CHECK (((type)::text = ANY ((ARRAY['personal'::character varying, 'admin'::character varying])::text[]))),
205
+ CONSTRAINT todos_pkey PRIMARY KEY (id)
206
+ );
207
+ CREATE INDEX IF NOT EXISTS idx_todos_assigned ON todos USING btree (assigned);
208
+ CREATE INDEX IF NOT EXISTS idx_todos_assigneduser ON todos USING btree (assigneduser);
209
+ CREATE INDEX IF NOT EXISTS idx_todos_completed ON todos USING btree (completed);
210
+ CREATE INDEX IF NOT EXISTS idx_todos_description ON todos USING btree (description);
211
+ CREATE INDEX IF NOT EXISTS idx_todos_title ON todos USING btree (title);
212
+ CREATE INDEX IF NOT EXISTS idx_todos_type ON todos USING btree (type);
213
+ CREATE INDEX IF NOT EXISTS idx_todos_type_completed ON todos USING btree (type, completed);
214
+ CREATE INDEX IF NOT EXISTS idx_todos_username_type ON todos USING btree (username, type);
215
+ COMMENT ON TABLE "todos" IS '[core]';
216
+
217
+ -- Table: user_github
218
+ CREATE TABLE IF NOT EXISTS user_github (
219
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
220
+ user_name text REFERENCES "Users"("UserName") ON DELETE CASCADE,
221
+ github_id character varying(255),
222
+ github_username character varying(255),
223
+ access_token text,
224
+ created_at timestamp without time zone DEFAULT now(),
225
+ updated_at timestamp without time zone DEFAULT now(),
226
+ installation_id bigint,
227
+ installation_target_type character varying(32),
228
+ CONSTRAINT user_github_pkey PRIMARY KEY (id),
229
+ CONSTRAINT user_github_github_id_key UNIQUE (github_id)
230
+ );
231
+ CREATE INDEX IF NOT EXISTS idx_user_github_user_name ON user_github USING btree (user_name);
232
+ COMMENT ON TABLE "user_github" IS '[core]';
233
+
234
+ -- Table: user_google
235
+ CREATE TABLE IF NOT EXISTS user_google (
236
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
237
+ user_name character varying(50) REFERENCES "Users"("UserName"),
238
+ google_id character varying(255),
239
+ google_email character varying(255),
240
+ access_token text,
241
+ created_at timestamp with time zone DEFAULT now(),
242
+ updated_at timestamp with time zone DEFAULT now(),
243
+ CONSTRAINT user_google_pkey PRIMARY KEY (id),
244
+ CONSTRAINT user_google_google_id_key UNIQUE (google_id)
245
+ );
246
+ COMMENT ON TABLE "user_google" IS '[core]';
247
+
248
+
249
+ -- Table: categories
250
+ CREATE TABLE IF NOT EXISTS categories (
251
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
252
+ name character varying(255) NOT NULL,
253
+ slug character varying(255) NOT NULL,
254
+ description text,
255
+ image_url character varying(255),
256
+ created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
257
+ updated_at timestamp without time zone,
258
+ CONSTRAINT categories_pkey PRIMARY KEY (id),
259
+ CONSTRAINT categories_name_key UNIQUE (name),
260
+ CONSTRAINT categories_slug_key UNIQUE (slug)
261
+ );
262
+ COMMENT ON TABLE "categories" IS '[core]';
263
+
264
+ -- Table: markdowns
265
+ CREATE TABLE IF NOT EXISTS markdowns (
266
+ category character varying(255) NOT NULL,
267
+ filename character varying(255) NOT NULL,
268
+ content text,
269
+ CONSTRAINT markdowns_pkey PRIMARY KEY (category, filename)
270
+ );
271
+ CREATE INDEX IF NOT EXISTS idx_markdowns_category ON markdowns USING btree (category);
272
+ CREATE INDEX IF NOT EXISTS idx_markdowns_filename ON markdowns USING btree (filename);
273
+ COMMENT ON TABLE "markdowns" IS '[core]';
274
+
275
+ -- Table: notifications
276
+ CREATE TABLE IF NOT EXISTS notifications (
277
+ id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
278
+ username character varying(50) NOT NULL,
279
+ type character varying(20) NOT NULL,
280
+ message text NOT NULL,
281
+ is_read boolean DEFAULT false,
282
+ created_at timestamp with time zone DEFAULT (now() AT TIME ZONE 'UTC'::text),
283
+ CONSTRAINT notifications_type_check CHECK (((type)::text = ANY ((ARRAY['info'::character varying, 'warning'::character varying, 'error'::character varying, 'success'::character varying])::text[]))),
284
+ CONSTRAINT notifications_pkey PRIMARY KEY (id)
285
+ );
286
+ CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications USING btree (created_at);
287
+ CREATE INDEX IF NOT EXISTS idx_notifications_username ON notifications USING btree (username);
288
+ COMMENT ON TABLE "notifications" IS '[core]';
289
+
290
+ -- Table: profile_content
291
+ CREATE TABLE IF NOT EXISTS profile_content (
292
+ username character varying(255) NOT NULL,
293
+ filename character varying(255) NOT NULL,
294
+ content text,
295
+ CONSTRAINT profile_content_pkey PRIMARY KEY (username, filename)
296
+ );
297
+ COMMENT ON TABLE "profile_content" IS '[core]';
298
+
299
+ -- Table: website_access_logs
300
+ CREATE TABLE IF NOT EXISTS website_access_logs (
301
+ log_id integer GENERATED BY DEFAULT AS IDENTITY NOT NULL,
302
+ ip_address character varying(50),
303
+ page_url text,
304
+ user_agent text,
305
+ referrer text,
306
+ method character varying(10),
307
+ status_code integer,
308
+ browser_language character varying(255),
309
+ user_id integer,
310
+ action_type character varying(255),
311
+ access_timestamp timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
312
+ username character varying(255),
313
+ session_id text,
314
+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
315
+ CONSTRAINT website_access_logs_pkey PRIMARY KEY (log_id)
316
+ );
317
+ CREATE INDEX IF NOT EXISTS idx_website_access_logs_action_type ON website_access_logs USING btree (action_type);
318
+ CREATE INDEX IF NOT EXISTS idx_website_access_logs_session_id ON website_access_logs USING btree (session_id);
319
+ CREATE INDEX IF NOT EXISTS idx_website_access_logs_timestamp ON website_access_logs USING btree (access_timestamp);
320
+ CREATE INDEX IF NOT EXISTS idx_website_access_logs_user_id ON website_access_logs USING btree (user_id);
321
+ COMMENT ON TABLE "website_access_logs" IS '[core]';
322
+
323
+
324
+ -- Seed users (hash-only). Default passwords are "12345678" for the sample accounts below.
325
+ -- Hashes were generated using mbkauthe's "hashPassword(password, username)" function.
326
+ INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "FullName")
327
+ VALUES ('support', 'b8b10c1c9006d8c30ab81c412463c65ff6dae3293d9bfbaf5fd8e275081d0947f000a828004e2fbd3a8f6ef5a35ae3eddd4c57b00ecab376b12e607a16a57459', 'SuperAdmin', true, false, 'Support User')
328
+ ON CONFLICT ("UserName") DO NOTHING;
package/index.js CHANGED
@@ -5,6 +5,7 @@ import path from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { renderError, renderPage } from "#response.js";
7
7
  import { packageJson } from "#config.js";
8
+ import { createLogger } from "./lib/utils/logger.js";
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
@@ -12,6 +13,7 @@ const isDevMode = process.env.test === "dev";
12
13
  const DEV_PORT = 5555;
13
14
  const viewsPath = path.join(__dirname, "views");
14
15
  const packageVersion = packageJson.version;
16
+ const logServer = createLogger("server");
15
17
 
16
18
  const app = express();
17
19
 
@@ -58,7 +60,7 @@ const renderDevError = (res, req, code, error, message, page, details) => render
58
60
  });
59
61
 
60
62
  if (isDevMode) {
61
- console.log(`[mbkauthe] Dev mode is enabled. Starting server in dev mode.`);
63
+ logServer(`Dev mode is enabled. Starting server in dev mode.`);
62
64
 
63
65
  app.get(["/dashboard", "/home", "/"], (req, res) => res.redirect("/mbkauthe/"));
64
66
 
@@ -75,12 +77,12 @@ if (isDevMode) {
75
77
  ));
76
78
 
77
79
  app.use((req, res) => {
78
- console.log(`[mbkauthe] Path not found: ${req.method} ${req.url}`);
80
+ logServer(`Path not found: ${req.method} ${req.url}`);
79
81
  renderDevError(res, req, 404, "Not Found", "The requested page was not found.", "/mbkauthe/login");
80
82
  });
81
83
 
82
84
  app.listen(DEV_PORT, () => {
83
- console.log(`[mbkauthe] Server running on http://localhost:${DEV_PORT}`);
85
+ logServer(`Server running on http://localhost:${DEV_PORT}`);
84
86
  });
85
87
  }
86
88
 
@@ -14,6 +14,60 @@ const getEncryptionKey = () => {
14
14
  return crypto.createHash('sha256').update(COOKIE_ENCRYPTION_KEY).digest();
15
15
  };
16
16
 
17
+ const getSigningKey = () => {
18
+ return crypto.createHash('sha256').update(`${COOKIE_ENCRYPTION_KEY}:cookie-signing`).digest();
19
+ };
20
+
21
+ const encodePayload = (data) => {
22
+ return Buffer.from(JSON.stringify(data), 'utf8').toString('base64url');
23
+ };
24
+
25
+ const decodePayload = (encoded) => {
26
+ return JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
27
+ };
28
+
29
+ const signCookiePayload = (encodedPayload) => {
30
+ return crypto.createHmac('sha256', getSigningKey()).update(encodedPayload).digest('hex');
31
+ };
32
+
33
+ const verifyCookieSignature = (encodedPayload, signature) => {
34
+ if (!encodedPayload || !signature || typeof encodedPayload !== 'string' || typeof signature !== 'string') {
35
+ return false;
36
+ }
37
+
38
+ const expected = signCookiePayload(encodedPayload);
39
+ const expectedBuffer = Buffer.from(expected, 'hex');
40
+ const actualBuffer = Buffer.from(signature, 'hex');
41
+
42
+ return expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer);
43
+ };
44
+
45
+ const createSignedCookiePayload = (data) => {
46
+ try {
47
+ const payload = encodePayload(data);
48
+ return {
49
+ payload,
50
+ signature: signCookiePayload(payload)
51
+ };
52
+ } catch (error) {
53
+ console.error(`[mbkauthe] Cookie signing error:`, error);
54
+ return null;
55
+ }
56
+ };
57
+
58
+ const parseSignedCookiePayload = (signedPayload) => {
59
+ try {
60
+ if (!signedPayload || !verifyCookieSignature(signedPayload.payload, signedPayload.signature)) {
61
+ return null;
62
+ }
63
+
64
+ return decodePayload(signedPayload.payload);
65
+ } catch (error) {
66
+ console.error(`[mbkauthe] Cookie signature verification error:`, error);
67
+ return null;
68
+ }
69
+ };
70
+
17
71
  // Encrypt and sign cookie payload
18
72
  const encryptCookiePayload = (data) => {
19
73
  try {
@@ -160,33 +214,46 @@ export { getCookieOptions, getClearCookieOptions };
160
214
  const parseAccountList = (raw, req) => {
161
215
  if (!raw) return [];
162
216
  try {
163
- // First, decrypt the cookie payload
164
217
  const parsed = JSON.parse(raw);
165
- const decrypted = decryptCookiePayload(parsed);
218
+ let data = parseSignedCookiePayload(parsed);
219
+ let isLegacyEncryptedCookie = false;
220
+
221
+ // Backward compatibility for previously encrypted account-list cookies.
222
+ if (!data && parsed.iv && parsed.authTag && parsed.data) {
223
+ data = decryptCookiePayload(parsed);
224
+ isLegacyEncryptedCookie = true;
225
+ }
166
226
 
167
- if (!decrypted || !decrypted.accounts || !decrypted.fingerprint) {
227
+ if (!data || !data.accounts || !data.fingerprint) {
168
228
  return [];
169
229
  }
170
230
 
171
231
  // Verify fingerprint matches current request
172
232
  const currentFingerprint = generateFingerprint(req);
173
- if (decrypted.fingerprint !== currentFingerprint) {
233
+ if (data.fingerprint !== currentFingerprint) {
174
234
  console.warn(`[mbkauthe] Cookie fingerprint mismatch - possible cookie theft attempt`);
175
235
  return [];
176
236
  }
177
237
 
178
- const accounts = decrypted.accounts;
238
+ const accounts = data.accounts;
179
239
  if (!Array.isArray(accounts)) return [];
180
240
 
181
241
  // Accept only minimal safe fields
182
242
  return accounts
183
243
  .filter(item => item && typeof item === 'object')
184
- .map(item => ({
185
- sessionId: typeof item.sessionId === 'string' ? item.sessionId : null,
186
- username: typeof item.username === 'string' ? item.username : null,
187
- fullName: typeof item.fullName === 'string' ? item.fullName : null,
188
- image: typeof item.image === 'string' ? item.image : null
189
- }))
244
+ .map(item => {
245
+ const rawSessionId = typeof item.sessionId === 'string' ? item.sessionId : null;
246
+ const sessionId = isLegacyEncryptedCookie
247
+ ? rawSessionId
248
+ : decryptSessionId(rawSessionId);
249
+
250
+ return {
251
+ sessionId,
252
+ username: typeof item.username === 'string' ? item.username : null,
253
+ fullName: typeof item.fullName === 'string' ? item.fullName : null,
254
+ image: typeof item.image === 'string' ? item.image : null
255
+ };
256
+ })
190
257
  .filter(item => item.sessionId && item.username)
191
258
  .slice(0, MAX_REMEMBERED_ACCOUNTS);
192
259
  } catch (error) {
@@ -200,7 +267,7 @@ const writeAccountList = (res, list, req) => {
200
267
 
201
268
  // Clean and limit fields to safe values (limit image URL length)
202
269
  const cleaned = sanitized.map(item => ({
203
- sessionId: item && item.sessionId ? item.sessionId : null,
270
+ sessionId: item && item.sessionId ? encryptSessionId(item.sessionId) : null,
204
271
  username: item && item.username ? item.username : null,
205
272
  fullName: item && item.fullName ? item.fullName : null,
206
273
  image: (item && typeof item.image === 'string' && item.image.length <= 2048) ? item.image : null
@@ -212,14 +279,13 @@ const writeAccountList = (res, list, req) => {
212
279
  fingerprint: generateFingerprint(req)
213
280
  };
214
281
 
215
- // Encrypt the payload
216
- const encrypted = encryptCookiePayload(payload);
217
- if (!encrypted) {
218
- console.error(`[mbkauthe] Failed to encrypt account list cookie`);
282
+ const signed = createSignedCookiePayload(payload);
283
+ if (!signed) {
284
+ console.error(`[mbkauthe] Failed to sign account list cookie`);
219
285
  return;
220
286
  }
221
287
 
222
- res.cookie(ACCOUNT_LIST_COOKIE, JSON.stringify(encrypted), cachedCookieOptions);
288
+ res.cookie(ACCOUNT_LIST_COOKIE, JSON.stringify(signed), cachedCookieOptions);
223
289
  };
224
290
 
225
291
  export const readAccountListFromCookie = (req) => {
@@ -243,4 +309,4 @@ export const removeAccountFromCookie = (req, res, sessionId) => {
243
309
 
244
310
  export const clearAccountListCookie = (res) => {
245
311
  res.clearCookie(ACCOUNT_LIST_COOKIE, cachedClearCookieOptions);
246
- };
312
+ };
@@ -1,7 +1,9 @@
1
1
  import dotenv from "dotenv";
2
2
  import { createRequire } from "module";
3
+ import { createLogger } from "../utils/logger.js";
3
4
 
4
5
  dotenv.config();
6
+ const logConfig = createLogger("config");
5
7
 
6
8
  // Comprehensive validation function
7
9
  function validateConfiguration() {
@@ -215,7 +217,7 @@ function validateConfiguration() {
215
217
  configParts.push(`defaults: ${usedDefaults.length} keys`);
216
218
  }
217
219
  const configSummary = configParts.length > 0 ? ` (${configParts.join(', ')})` : '';
218
- console.log(`[mbkauthe] Configuration loaded${configSummary}`);
220
+ logConfig(`Configuration loaded${configSummary}`);
219
221
  return mbkautheVar;
220
222
  }
221
223
 
@@ -56,4 +56,4 @@ export function getAvailableScopes() {
56
56
  name: value.name,
57
57
  description: value.description
58
58
  }));
59
- }
59
+ }
@@ -2,32 +2,119 @@
2
2
  import { readFile } from "fs/promises";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { performance } from "perf_hooks";
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
 
9
10
  async function main() {
10
- const schemaPath = path.resolve(__dirname, "../docs/db.sql");
11
+ const startTime = performance.now();
12
+
13
+ console.log("[mbkauthe] Starting schema creation...");
14
+
15
+ const schemaPath = path.resolve(__dirname, "../docs/schema/db.sql");
16
+
17
+ console.log(`[mbkauthe] Schema file: ${schemaPath}`);
18
+
11
19
  const schemaSql = await readFile(schemaPath, "utf8");
12
20
 
21
+ console.log(
22
+ `[mbkauthe] Schema loaded (${Buffer.byteLength(schemaSql, "utf8")} bytes)`
23
+ );
24
+
25
+ const statementCount = schemaSql
26
+ .split(";")
27
+ .map(s => s.trim())
28
+ .filter(Boolean).length;
29
+
30
+ console.log(
31
+ `[mbkauthe] Detected approximately ${statementCount} SQL statements`
32
+ );
33
+
13
34
  try {
35
+ console.log("[mbkauthe] Testing database connection...");
36
+
37
+ const ping = await dblogin.query("SELECT version()");
38
+
39
+ console.log(
40
+ `[mbkauthe] Connected to PostgreSQL`
41
+ );
42
+
43
+ console.log(
44
+ `[mbkauthe] PostgreSQL version: ${ping.rows[0].version}`
45
+ );
46
+
47
+ console.log("[mbkauthe] Applying schema...");
48
+
49
+ const queryStart = performance.now();
50
+
14
51
  const res = await dblogin.query(schemaSql);
15
- console.log(`[mbkauthe] Schema applied successfully.`);
52
+
53
+ const queryDuration = (
54
+ performance.now() - queryStart
55
+ ).toFixed(2);
56
+
57
+ console.log(
58
+ `[mbkauthe] Schema applied successfully in ${queryDuration} ms`
59
+ );
60
+
61
+ console.log(
62
+ `[mbkauthe] Command: ${res.command ?? "MULTI"}`
63
+ );
64
+
65
+ console.log(
66
+ `[mbkauthe] Row count: ${res.rowCount ?? 0}`
67
+ );
68
+
16
69
  if (res?.rows?.length) {
17
- console.log(`[mbkauthe] Rows returned by schema query:`, res.rows);
18
- } else {
19
- console.log(`[mbkauthe] No rows returned by schema query (expected). If you want to verify table creation, query the database separately.`);
70
+ console.log(
71
+ `[mbkauthe] Returned rows: ${res.rows.length}`
72
+ );
20
73
  }
74
+
21
75
  } catch (err) {
22
76
  const IGNORE_CODES = ["42710", "42P07"];
23
- if (err && typeof err.code === "string" && IGNORE_CODES.includes(err.code)) {
24
- console.warn(`[mbkauthe] Schema already exists (ignored):`, err.message);
77
+
78
+ if (
79
+ err &&
80
+ typeof err.code === "string" &&
81
+ IGNORE_CODES.includes(err.code)
82
+ ) {
83
+ console.warn(
84
+ `[mbkauthe] Schema object already exists (ignored)`
85
+ );
86
+
87
+ console.warn(`Code: ${err.code}`);
88
+ console.warn(`Message: ${err.message}`);
25
89
  } else {
26
- console.error(`[mbkauthe] Failed to apply schema:`, err);
90
+ console.error(
91
+ `[mbkauthe] Failed to apply schema`
92
+ );
93
+
94
+ console.error("Code:", err.code);
95
+ console.error("Severity:", err.severity);
96
+ console.error("Position:", err.position);
97
+ console.error("Table:", err.table);
98
+ console.error("Column:", err.column);
99
+ console.error("Constraint:", err.constraint);
100
+ console.error("Detail:", err.detail);
101
+ console.error("Hint:", err.hint);
102
+ console.error("Message:", err.message);
103
+
27
104
  process.exitCode = 1;
28
105
  }
29
106
  } finally {
107
+ console.log("[mbkauthe] Closing database connection...");
108
+
30
109
  await dblogin.end();
110
+
111
+ const totalDuration = (
112
+ performance.now() - startTime
113
+ ).toFixed(2);
114
+
115
+ console.log(
116
+ `[mbkauthe] Finished in ${totalDuration} ms`
117
+ );
31
118
  }
32
119
  }
33
120