@harshankur/viewcounter 3.0.0 → 3.1.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/.env.example +20 -0
- package/README.md +189 -14
- package/admin/apple-touch-icon.png +0 -0
- package/admin/assets/world-map.json +1 -0
- package/admin/css/admin.css +1875 -0
- package/admin/favicon.ico +0 -0
- package/admin/favicon.svg +9 -0
- package/admin/icon-192.png +0 -0
- package/admin/icon-512.png +0 -0
- package/admin/index.html +89 -0
- package/admin/js/api.js +100 -0
- package/admin/js/charts.js +502 -0
- package/admin/js/clamp.js +41 -0
- package/admin/js/constants.js +150 -0
- package/admin/js/dom.js +83 -0
- package/admin/js/format.js +79 -0
- package/admin/js/i18n.js +80 -0
- package/admin/js/insights.js +192 -0
- package/admin/js/listbox.js +144 -0
- package/admin/js/logs.js +167 -0
- package/admin/js/main.js +235 -0
- package/admin/js/modal.js +171 -0
- package/admin/js/table.js +134 -0
- package/admin/js/theme.js +72 -0
- package/admin/js/toast.js +47 -0
- package/admin/js/viewDialogs.js +208 -0
- package/admin/js/views.js +685 -0
- package/admin/locales/en.json +394 -0
- package/admin/site.webmanifest +20 -0
- package/config/index.js +83 -0
- package/constants.js +215 -2
- package/db/AdminRepository.js +562 -0
- package/db/DatabaseManager.js +94 -20
- package/db/LogRepository.js +217 -0
- package/db/adminSchema.js +244 -0
- package/db/retention.js +97 -0
- package/index.js +39 -6
- package/middleware/adminAuth.js +204 -0
- package/middleware/adminValidation.js +253 -0
- package/package.json +17 -10
- package/routes/admin.js +438 -0
- package/routes/analytics.js +11 -2
- package/utils/cookieUtils.js +47 -0
- package/utils/errorUtils.js +35 -0
package/constants.js
CHANGED
|
@@ -14,9 +14,11 @@ const APP_SLUG = 'viewcounter';
|
|
|
14
14
|
|
|
15
15
|
const HTTP_STATUS = {
|
|
16
16
|
OK: 200,
|
|
17
|
+
NO_CONTENT: 204,
|
|
17
18
|
BAD_REQUEST: 400,
|
|
18
19
|
UNAUTHORIZED: 401,
|
|
19
20
|
FORBIDDEN: 403,
|
|
21
|
+
NOT_FOUND: 404,
|
|
20
22
|
UNPROCESSABLE_ENTITY: 422,
|
|
21
23
|
TOO_MANY_REQUESTS: 429,
|
|
22
24
|
INTERNAL_SERVER_ERROR: 500,
|
|
@@ -46,6 +48,10 @@ const FIELD_MAX_LENGTH = {
|
|
|
46
48
|
DEVICE_TYPE: 20,
|
|
47
49
|
SESSION_ID: 64,
|
|
48
50
|
EVENT_TYPE: 50,
|
|
51
|
+
/** Free-text admin annotation on a single view. */
|
|
52
|
+
NOTE: 1000,
|
|
53
|
+
/** CHAR(36): the canonical textual form of a UUID. */
|
|
54
|
+
UUID: 36,
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
/** Bounds for user-supplied pagination and range parameters. */
|
|
@@ -83,7 +89,9 @@ const DATABASE = {
|
|
|
83
89
|
QUERY_TIMEOUT_MS: 5_000,
|
|
84
90
|
CONNECT_TIMEOUT_MS: 10_000,
|
|
85
91
|
DEFAULT_PORT: 3306,
|
|
86
|
-
SCHEMA_VERSION: '
|
|
92
|
+
SCHEMA_VERSION: 'admin_schema_v4',
|
|
93
|
+
/** Rows given a public_id per statement when backfilling an old table. */
|
|
94
|
+
BACKFILL_BATCH_SIZE: 500,
|
|
87
95
|
};
|
|
88
96
|
|
|
89
97
|
const SERVER = {
|
|
@@ -117,6 +125,194 @@ const PRIVACY = {
|
|
|
117
125
|
MIN_ADMIN_KEY_LENGTH: 32,
|
|
118
126
|
};
|
|
119
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Admin UI and API.
|
|
130
|
+
*
|
|
131
|
+
* The admin tier is a separate credential from both read keys and
|
|
132
|
+
* ADMIN_API_KEYS (SECURITY.md §3): it can read, edit, and delete every app's
|
|
133
|
+
* data, which neither of the other tiers may do.
|
|
134
|
+
*/
|
|
135
|
+
const ADMIN = {
|
|
136
|
+
/** Where the UI and its API are mounted. */
|
|
137
|
+
PATH_PREFIX: '/admin',
|
|
138
|
+
/** Relative to PATH_PREFIX. */
|
|
139
|
+
API_PATH: '/api',
|
|
140
|
+
SESSION_COOKIE: 'vc_admin_session',
|
|
141
|
+
CSRF_HEADER: 'x-csrf-token',
|
|
142
|
+
/** Long enough that the login rate limit makes guessing hopeless. */
|
|
143
|
+
MIN_PASSWORD_LENGTH: 16,
|
|
144
|
+
/** Longest submitted password even looked at; bounds the comparison cost. */
|
|
145
|
+
MAX_PASSWORD_INPUT_LENGTH: 1024,
|
|
146
|
+
SESSION_TOKEN_BYTES: 32,
|
|
147
|
+
CSRF_TOKEN_BYTES: 32,
|
|
148
|
+
/** Signed out after this long without a request. */
|
|
149
|
+
SESSION_IDLE_TIMEOUT_MS: 30 * 60 * 1000,
|
|
150
|
+
/** Signed out after this long regardless of activity. */
|
|
151
|
+
SESSION_ABSOLUTE_TIMEOUT_MS: 12 * 60 * 60 * 1000,
|
|
152
|
+
/** Oldest sessions are evicted beyond this, bounding memory. */
|
|
153
|
+
MAX_SESSIONS: 50,
|
|
154
|
+
LOGIN_RATE_LIMIT_WINDOW_MS: 15 * 60 * 1000,
|
|
155
|
+
/** Failed attempts per IP per window. Successful logins do not count. */
|
|
156
|
+
LOGIN_RATE_LIMIT_MAX: 5,
|
|
157
|
+
RATE_LIMIT_WINDOW_MS: 60 * 1000,
|
|
158
|
+
/** Requests per IP per window across the whole admin surface. */
|
|
159
|
+
RATE_LIMIT_MAX: 600,
|
|
160
|
+
/** Upper bound on the rows one batch operation may touch. */
|
|
161
|
+
MAX_BATCH_IDS: 500,
|
|
162
|
+
/**
|
|
163
|
+
* JSON body ceiling for the admin API. A full batch of MAX_BATCH_IDS
|
|
164
|
+
* UUIDs is about 20 kB on its own, above the public endpoints' limit.
|
|
165
|
+
*/
|
|
166
|
+
MAX_BODY_BYTES: 64 * 1024,
|
|
167
|
+
PAGE_SIZES: [25, 50, 100],
|
|
168
|
+
PAGE_SIZE_DEFAULT: 50,
|
|
169
|
+
PAGE_MAX: 100_000,
|
|
170
|
+
SEARCH_MAX_LENGTH: 200,
|
|
171
|
+
DEFAULT_TRASH_RETENTION_DAYS: 30,
|
|
172
|
+
MAX_TRASH_RETENTION_DAYS: 3650,
|
|
173
|
+
DEFAULT_VIEW_LOG_RETENTION_DAYS: 90,
|
|
174
|
+
MAX_VIEW_LOG_RETENTION_DAYS: 3650,
|
|
175
|
+
/** Rows removed per statement when pruning the view log, so no delete holds locks for long. */
|
|
176
|
+
VIEW_LOG_PRUNE_BATCH_SIZE: 5000,
|
|
177
|
+
/** How often expired trash and old view-log entries are checked for. */
|
|
178
|
+
RETENTION_INTERVAL_MS: 60 * 60 * 1000,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Date ranges an admin listing and its analysis can be limited to, mapped to
|
|
183
|
+
* a number of days. `all` has no lower bound. Only these values reach SQL.
|
|
184
|
+
*/
|
|
185
|
+
const ADMIN_RANGE = {
|
|
186
|
+
WEEK: '7d',
|
|
187
|
+
MONTH: '30d',
|
|
188
|
+
QUARTER: '90d',
|
|
189
|
+
YEAR: '1y',
|
|
190
|
+
ALL: 'all',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const ADMIN_RANGE_DAYS = {
|
|
194
|
+
[ADMIN_RANGE.WEEK]: 7,
|
|
195
|
+
[ADMIN_RANGE.MONTH]: 30,
|
|
196
|
+
[ADMIN_RANGE.QUARTER]: 90,
|
|
197
|
+
[ADMIN_RANGE.YEAR]: 365,
|
|
198
|
+
[ADMIN_RANGE.ALL]: null,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Time-series bucket for the admin analysis, chosen from the span of the data
|
|
203
|
+
* actually in the filtered set, so a chart never has thousands of points.
|
|
204
|
+
*/
|
|
205
|
+
const TREND_BUCKET = {
|
|
206
|
+
DAY: 'day',
|
|
207
|
+
WEEK: 'week',
|
|
208
|
+
MONTH: 'month',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** Largest span, in days, charted per day and per week. */
|
|
212
|
+
const TREND_BUCKET_MAX_DAYS = {
|
|
213
|
+
[TREND_BUCKET.DAY]: 92,
|
|
214
|
+
[TREND_BUCKET.WEEK]: 731,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** Rows per breakdown returned by the admin analysis; the rest is "other". */
|
|
218
|
+
const ANALYSIS_TOP_N = 8;
|
|
219
|
+
|
|
220
|
+
/** Which rows an admin listing returns. */
|
|
221
|
+
const VIEW_STATUS = {
|
|
222
|
+
ACTIVE: 'active',
|
|
223
|
+
DELETED: 'deleted',
|
|
224
|
+
ALL: 'all',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Filter on whether an admin has edited a row's content. */
|
|
228
|
+
const MODIFIED_FILTER = {
|
|
229
|
+
ANY: 'any',
|
|
230
|
+
MODIFIED: 'modified',
|
|
231
|
+
UNMODIFIED: 'unmodified',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const SORT_ORDER = {
|
|
235
|
+
ASC: 'asc',
|
|
236
|
+
DESC: 'desc',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Columns an admin listing may be sorted by, mapped from the API name to the
|
|
241
|
+
* column. Sorting interpolates the column, so only these values can reach SQL.
|
|
242
|
+
*/
|
|
243
|
+
const ADMIN_SORT_COLUMNS = {
|
|
244
|
+
timestamp: 'timestamp',
|
|
245
|
+
page: 'page_path',
|
|
246
|
+
country: 'country',
|
|
247
|
+
deviceSize: 'devicesize',
|
|
248
|
+
eventType: 'event_type',
|
|
249
|
+
source: 'source_type',
|
|
250
|
+
browser: 'browser',
|
|
251
|
+
modifiedAt: 'admin_modified_at',
|
|
252
|
+
deletedAt: 'deleted_at',
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Content fields an admin may edit, mapped from the API name to the column.
|
|
257
|
+
*
|
|
258
|
+
* Deliberately excludes everything that records who or when: the masked IP,
|
|
259
|
+
* visitor hash, timestamp, country, and every User-Agent-derived field. An
|
|
260
|
+
* edit can correct what was viewed, never fabricate who viewed it or when.
|
|
261
|
+
* `referrerDomain` and `sourceType` are not editable directly; they are
|
|
262
|
+
* re-derived whenever `referrer` changes, so they can never disagree with it.
|
|
263
|
+
*/
|
|
264
|
+
const EDITABLE_FIELDS = {
|
|
265
|
+
pagePath: 'page_path',
|
|
266
|
+
pageTitle: 'page_title',
|
|
267
|
+
referrer: 'referrer',
|
|
268
|
+
deviceSize: 'devicesize',
|
|
269
|
+
eventType: 'event_type',
|
|
270
|
+
eventData: 'event_data',
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/** Every entry in the admin operation log is one of these. */
|
|
274
|
+
const ADMIN_ACTION = {
|
|
275
|
+
LOGIN_SUCCEEDED: 'login_succeeded',
|
|
276
|
+
LOGIN_FAILED: 'login_failed',
|
|
277
|
+
LOGOUT: 'logout',
|
|
278
|
+
VIEWS_EDITED: 'views_edited',
|
|
279
|
+
NOTE_SET: 'note_set',
|
|
280
|
+
NOTE_CLEARED: 'note_cleared',
|
|
281
|
+
VIEWS_DELETED: 'views_deleted',
|
|
282
|
+
VIEWS_RESTORED: 'views_restored',
|
|
283
|
+
VIEWS_PURGED: 'views_purged',
|
|
284
|
+
TRASH_AUTO_PURGED: 'trash_auto_purged',
|
|
285
|
+
VIEW_LOG_PRUNED: 'view_log_pruned',
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Machine-readable error codes returned by the admin API. The UI maps each one
|
|
290
|
+
* to a translated message, so no server-side English reaches the screen.
|
|
291
|
+
*/
|
|
292
|
+
const ADMIN_ERROR_CODE = {
|
|
293
|
+
UNAUTHENTICATED: 'UNAUTHENTICATED',
|
|
294
|
+
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
|
295
|
+
TOO_MANY_ATTEMPTS: 'TOO_MANY_ATTEMPTS',
|
|
296
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
297
|
+
CSRF_REJECTED: 'CSRF_REJECTED',
|
|
298
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
299
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
300
|
+
SERVER_ERROR: 'SERVER_ERROR',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** Which write endpoint a view-register-log entry came through. */
|
|
304
|
+
const VIEW_LOG_SOURCE = {
|
|
305
|
+
REGISTER_VIEW: 'registerView',
|
|
306
|
+
EVENT: 'event',
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/** Service-owned tables. All carry the reserved `_` prefix. */
|
|
310
|
+
const ADMIN_LOG_TABLE = '_admin_log';
|
|
311
|
+
const VIEW_LOG_TABLE = '_view_log';
|
|
312
|
+
|
|
313
|
+
/** Canonical UUID text form, any version. Admin row IDs are validated with it. */
|
|
314
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
315
|
+
|
|
120
316
|
/** Recognised event types. `pageview` is the only one the server itself emits. */
|
|
121
317
|
const EVENT_TYPE = {
|
|
122
318
|
PAGEVIEW: 'pageview',
|
|
@@ -180,7 +376,7 @@ const SCOPE_ALL = '*';
|
|
|
180
376
|
*/
|
|
181
377
|
const APP_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
182
378
|
|
|
183
|
-
/** Reserved prefix for the service's own tables (`_migrations`, `_apps
|
|
379
|
+
/** Reserved prefix for the service's own tables (`_migrations`, `_apps`, logs). */
|
|
184
380
|
const RESERVED_TABLE_PREFIX = '_';
|
|
185
381
|
|
|
186
382
|
/** Internal registry of dynamically provisioned apps. */
|
|
@@ -213,6 +409,23 @@ module.exports = {
|
|
|
213
409
|
DATABASE,
|
|
214
410
|
SERVER,
|
|
215
411
|
PRIVACY,
|
|
412
|
+
ADMIN,
|
|
413
|
+
VIEW_STATUS,
|
|
414
|
+
ADMIN_RANGE,
|
|
415
|
+
ADMIN_RANGE_DAYS,
|
|
416
|
+
TREND_BUCKET,
|
|
417
|
+
TREND_BUCKET_MAX_DAYS,
|
|
418
|
+
ANALYSIS_TOP_N,
|
|
419
|
+
MODIFIED_FILTER,
|
|
420
|
+
SORT_ORDER,
|
|
421
|
+
ADMIN_SORT_COLUMNS,
|
|
422
|
+
EDITABLE_FIELDS,
|
|
423
|
+
ADMIN_ACTION,
|
|
424
|
+
ADMIN_ERROR_CODE,
|
|
425
|
+
VIEW_LOG_SOURCE,
|
|
426
|
+
ADMIN_LOG_TABLE,
|
|
427
|
+
VIEW_LOG_TABLE,
|
|
428
|
+
UUID_PATTERN,
|
|
216
429
|
EVENT_TYPE,
|
|
217
430
|
TREND_PERIOD,
|
|
218
431
|
TREND_PERIODS,
|