mbkauthe 4.9.0 → 5.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/docs/api.md +29 -178
- package/docs/db.md +1 -1
- package/docs/db.sql +305 -253
- package/index.js +5 -3
- package/lib/config/cookies.js +84 -18
- package/lib/config/index.js +3 -1
- package/lib/config/tokenScopes.js +1 -1
- package/lib/createTable.js +95 -8
- package/lib/db/AuthRepository.js +57 -16
- package/lib/db/BaseRepository.js +9 -1
- package/lib/db/dialects/postgres.js +1 -1
- package/lib/main.js +5 -5
- package/lib/middleware/auth.js +201 -218
- package/lib/middleware/index.js +13 -14
- package/lib/middleware/scopeValidator.js +8 -3
- package/lib/pool.js +5 -6
- package/lib/routes/auth.js +42 -47
- package/lib/routes/dbLogs.js +247 -29
- package/lib/routes/misc.js +6 -4
- package/lib/routes/oauth.js +19 -23
- package/lib/utils/dbQueryLogger.js +485 -80
- package/lib/utils/errors.js +1 -1
- package/lib/utils/logger.js +12 -0
- package/lib/utils/timingSafeToken.js +1 -1
- package/package.json +1 -1
- package/public/main.css +1 -1
- package/test.spec.js +515 -48
- package/views/pages/dbLogs.handlebars +618 -420
package/docs/db.sql
CHANGED
|
@@ -1,276 +1,328 @@
|
|
|
1
|
+
-- imports
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
BEGIN
|
|
4
|
-
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role') THEN
|
|
5
|
-
CREATE TYPE role AS ENUM ('SuperAdmin', 'NormalUser', 'Guest', 'member');
|
|
6
|
-
END IF;
|
|
7
|
-
END
|
|
8
|
-
$$;
|
|
3
|
+
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
9
4
|
|
|
5
|
+
|
|
6
|
+
-- Table: Users
|
|
10
7
|
CREATE TABLE IF NOT EXISTS "Users" (
|
|
11
|
-
id
|
|
12
|
-
"UserName"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"PasswordEnc"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"resetTokenExpires" TimeStamp,
|
|
31
|
-
"resetAttempts" INTEGER DEFAULT '0',
|
|
32
|
-
"lastResetAttempt" TimeStamp WITH TIME ZONE
|
|
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")
|
|
33
27
|
);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
CREATE INDEX IF NOT EXISTS
|
|
37
|
-
CREATE INDEX IF NOT EXISTS
|
|
38
|
-
CREATE INDEX IF NOT EXISTS
|
|
39
|
-
CREATE INDEX IF NOT EXISTS
|
|
40
|
-
CREATE INDEX IF NOT EXISTS
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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")))
|
|
57
53
|
);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
CREATE
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
75
|
-
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
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)
|
|
76
70
|
);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_password_resets_token ON "PasswordResets" USING btree ("resetToken");
|
|
72
|
+
COMMENT ON TABLE "PasswordResets" IS '[core]';
|
|
77
73
|
|
|
78
|
-
--
|
|
79
|
-
CREATE INDEX IF NOT EXISTS idx_user_google_google_id ON user_google (google_id);
|
|
80
|
-
CREATE INDEX IF NOT EXISTS idx_user_google_user_name ON user_google (user_name);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-- Application Sessions table (stores multiple concurrent sessions per user)
|
|
84
|
-
-- Note: this is separate from the express-session store table named "session"
|
|
74
|
+
-- Table: Sessions
|
|
85
75
|
CREATE TABLE IF NOT EXISTS "Sessions" (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
meta
|
|
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)
|
|
91
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]';
|
|
92
87
|
|
|
93
|
-
--
|
|
94
|
-
CREATE
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-- These can enable index-only scans for the exact columns used in auth middleware.
|
|
107
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_id_cover
|
|
108
|
-
ON "Sessions" (id)
|
|
109
|
-
INCLUDE ("UserName", expires_at);
|
|
110
|
-
|
|
111
|
-
CREATE INDEX IF NOT EXISTS idx_users_username_cover
|
|
112
|
-
ON "Users" ("UserName")
|
|
113
|
-
INCLUDE ("Active", "Role");
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
CREATE TABLE IF NOT EXISTS "session" (
|
|
117
|
-
sid VARCHAR(33) PRIMARY KEY NOT NULL,
|
|
118
|
-
sess JSONB NOT NULL,
|
|
119
|
-
expire TimeStamp WITH TIME ZONE NOT NULL,
|
|
120
|
-
username TEXT,
|
|
121
|
-
last_activity TimeStamp WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
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")
|
|
122
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]';
|
|
123
106
|
|
|
124
|
-
--
|
|
125
|
-
CREATE INDEX IF NOT EXISTS idx_session_expire ON "session" ("expire");
|
|
126
|
-
CREATE INDEX IF NOT EXISTS idx_session_user_id ON "session" ((sess->'user'->>'id'));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
107
|
+
-- Table: TwoFA
|
|
130
108
|
CREATE TABLE IF NOT EXISTS "TwoFA" (
|
|
131
|
-
"UserName"
|
|
132
|
-
"TwoFAStatus" boolean NOT NULL,
|
|
133
|
-
"TwoFASecret"
|
|
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")
|
|
134
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]';
|
|
135
148
|
|
|
136
|
-
--
|
|
137
|
-
CREATE
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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)
|
|
152
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]';
|
|
153
233
|
|
|
154
|
-
--
|
|
155
|
-
CREATE
|
|
156
|
-
|
|
157
|
-
|
|
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]';
|
|
158
322
|
|
|
159
323
|
|
|
160
324
|
-- Seed users (hash-only). Default passwords are "12345678" for the sample accounts below.
|
|
161
|
-
-- Hashes were generated using mbkauthe's
|
|
325
|
+
-- Hashes were generated using mbkauthe's "hashPassword(password, username)" function.
|
|
162
326
|
INSERT INTO "Users" ("UserName", "PasswordEnc", "Role", "Active", "HaveMailAccount", "FullName")
|
|
163
327
|
VALUES ('support', 'b8b10c1c9006d8c30ab81c412463c65ff6dae3293d9bfbaf5fd8e275081d0947f000a828004e2fbd3a8f6ef5a35ae3eddd4c57b00ecab376b12e607a16a57459', 'SuperAdmin', true, false, 'Support User')
|
|
164
328
|
ON CONFLICT ("UserName") DO NOTHING;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
-- API Tokens for persistent programmatic access
|
|
168
|
-
CREATE TABLE IF NOT EXISTS "ApiTokens" (
|
|
169
|
-
"id" SERIAL PRIMARY KEY,
|
|
170
|
-
"UserName" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName") ON DELETE CASCADE,
|
|
171
|
-
"Name" VARCHAR(255) NOT NULL CHECK (LENGTH(TRIM("Name")) > 0),
|
|
172
|
-
"TokenHash" VARCHAR(128) NOT NULL UNIQUE,
|
|
173
|
-
"Prefix" VARCHAR(32) NOT NULL,
|
|
174
|
-
"Permissions" JSONB NOT NULL DEFAULT '{"scope":"read-only","allowedApps":null}'::jsonb
|
|
175
|
-
CHECK ("Permissions"->>'scope' IN ('read-only', 'write')),
|
|
176
|
-
"LastUsed" TIMESTAMP WITH TIME ZONE,
|
|
177
|
-
"CreatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
178
|
-
"ExpiresAt" TIMESTAMP WITH TIME ZONE
|
|
179
|
-
CHECK ("ExpiresAt" IS NULL OR "ExpiresAt" > "CreatedAt")
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
-- Basic indexes
|
|
183
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_tokenhash
|
|
184
|
-
ON "ApiTokens" ("TokenHash");
|
|
185
|
-
|
|
186
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_username
|
|
187
|
-
ON "ApiTokens" ("UserName");
|
|
188
|
-
|
|
189
|
-
-- Performance indexes
|
|
190
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_tokenhash_expires
|
|
191
|
-
ON "ApiTokens" ("TokenHash", "ExpiresAt")
|
|
192
|
-
WHERE "ExpiresAt" IS NOT NULL;
|
|
193
|
-
|
|
194
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_username_created
|
|
195
|
-
ON "ApiTokens" ("UserName", "CreatedAt" DESC);
|
|
196
|
-
|
|
197
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_expires
|
|
198
|
-
ON "ApiTokens" ("ExpiresAt")
|
|
199
|
-
WHERE "ExpiresAt" IS NOT NULL;
|
|
200
|
-
|
|
201
|
-
-- JSONB indexes for fast permission queries
|
|
202
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_permissions_gin
|
|
203
|
-
ON "ApiTokens" USING GIN ("Permissions");
|
|
204
|
-
|
|
205
|
-
CREATE INDEX IF NOT EXISTS idx_apitokens_permissions_scope
|
|
206
|
-
ON "ApiTokens" (("Permissions"->>'scope'));
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
-- WebPortal user table
|
|
221
|
-
|
|
222
|
-
-- todos table for user tasks, with indexes for performance optimization
|
|
223
|
-
CREATE TABLE IF NOT EXISTS "todos" (
|
|
224
|
-
"id" SERIAL PRIMARY KEY,
|
|
225
|
-
"username" VARCHAR(50) NOT NULL REFERENCES "Users"("UserName"),
|
|
226
|
-
"title" VARCHAR(255) NOT NULL,
|
|
227
|
-
"description" TEXT,
|
|
228
|
-
"completed" BOOLEAN DEFAULT false,
|
|
229
|
-
"type" VARCHAR(20) DEFAULT 'personal' CHECK ("type" IN ('personal', 'admin')),
|
|
230
|
-
"assigned" BOOLEAN DEFAULT false,
|
|
231
|
-
"assigneduser" VARCHAR(50) DEFAULT 'none',
|
|
232
|
-
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
233
|
-
"updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
CREATE INDEX IF NOT EXISTS idx_todos_username ON "todos" USING BTREE ("username");
|
|
237
|
-
CREATE INDEX IF NOT EXISTS idx_todos_type ON "todos" USING BTREE ("type");
|
|
238
|
-
CREATE INDEX IF NOT EXISTS idx_todos_completed ON "todos" USING BTREE ("completed");
|
|
239
|
-
CREATE INDEX IF NOT EXISTS idx_todos_type_completed ON "todos" USING BTREE ("type", "completed");
|
|
240
|
-
CREATE INDEX IF NOT EXISTS idx_todos_assigned ON "todos" USING BTREE ("assigned");
|
|
241
|
-
CREATE INDEX IF NOT EXISTS idx_todos_assigneduser ON "todos" USING BTREE ("assigneduser");
|
|
242
|
-
CREATE INDEX IF NOT EXISTS idx_todos_username_type ON "todos" USING BTREE ("username", "type");
|
|
243
|
-
CREATE INDEX IF NOT EXISTS idx_todos_title ON "todos" USING BTREE ("title");
|
|
244
|
-
CREATE INDEX IF NOT EXISTS idx_todos_description ON "todos" USING BTREE ("description");
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
-- Plan upgrade requests table
|
|
251
|
-
CREATE TABLE IF NOT EXISTS plan_upgrade_requests (
|
|
252
|
-
id SERIAL PRIMARY KEY,
|
|
253
|
-
username VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
254
|
-
curr_role VARCHAR(50) NOT NULL,
|
|
255
|
-
requested_plan VARCHAR(50) NOT NULL,
|
|
256
|
-
req_role VARCHAR(50) NOT NULL,
|
|
257
|
-
reason TEXT NOT NULL,
|
|
258
|
-
experience TEXT,
|
|
259
|
-
portfolio VARCHAR(500),
|
|
260
|
-
linkedin VARCHAR(500),
|
|
261
|
-
github VARCHAR(500),
|
|
262
|
-
additional_info TEXT,
|
|
263
|
-
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
|
|
264
|
-
admin_notes TEXT,
|
|
265
|
-
reviewed_by VARCHAR(255),
|
|
266
|
-
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
267
|
-
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
CREATE INDEX IF NOT EXISTS idx_username ON plan_upgrade_requests("username");
|
|
271
|
-
CREATE INDEX IF NOT EXISTS idx_status ON plan_upgrade_requests(status);
|
|
272
|
-
CREATE INDEX IF NOT EXISTS idx_created_at ON plan_upgrade_requests(created_at);
|
|
273
|
-
CREATE INDEX IF NOT EXISTS idx_requested_plan ON plan_upgrade_requests(requested_plan);
|
|
274
|
-
CREATE INDEX IF NOT EXISTS idx_status_created_at ON plan_upgrade_requests(status, created_at DESC);
|
|
275
|
-
CREATE INDEX IF NOT EXISTS idx_username_status ON plan_upgrade_requests("username", status);
|
|
276
|
-
CREATE INDEX IF NOT EXISTS idx_req_role_status ON plan_upgrade_requests("req_role", status);
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
logServer(`Server running on http://localhost:${DEV_PORT}`);
|
|
84
86
|
});
|
|
85
87
|
}
|
|
86
88
|
|