@2byte/tgbot-framework 1.0.16 → 1.0.18
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/package.json +1 -1
- package/src/core/App.ts +60 -27
- package/src/core/BotArtisan.ts +47 -2
- package/src/illumination/Artisan.ts +8 -1
- package/src/illumination/InlineKeyboard.ts +10 -0
- package/src/illumination/Message2Byte.ts +1 -1
- package/src/illumination/RunSectionRoute.ts +4 -0
- package/src/models/AccessKey.ts +114 -0
- package/src/models/Model.ts +3 -3
- package/src/models/index.ts +2 -1
- package/src/user/UserModel.ts +40 -0
- package/src/user/UserStore.ts +19 -0
- package/templates/bot/database/migrations/010_access_keys.sql +12 -0
- package/templates/bot/package.json +1 -1
package/package.json
CHANGED
package/src/core/App.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { Section } from "../illumination/Section";
|
|
9
9
|
import { RunSectionRoute } from "../illumination/RunSectionRoute";
|
|
10
10
|
import { UserModel } from "../user/UserModel";
|
|
11
|
+
import { AccessKey } from "../models/AccessKey";
|
|
11
12
|
import { UserStore } from "../user/UserStore";
|
|
12
13
|
import {
|
|
13
14
|
AppConfig,
|
|
@@ -278,11 +279,16 @@ export class App {
|
|
|
278
279
|
? startPayload.split("key=")[1] || null
|
|
279
280
|
: null;
|
|
280
281
|
}
|
|
281
|
-
|
|
282
|
+
|
|
282
283
|
// Check access by username and register user if not exists
|
|
283
284
|
if (!this.config.userStorage.exists(tgUsername) && !this.rememberUser(tgUsername)) {
|
|
284
285
|
const isAuthByUsername = !this.config.accessPublic && !accessKey;
|
|
285
|
-
|
|
286
|
+
this.debugLog(
|
|
287
|
+
"Access control check. isAuthByUsername:",
|
|
288
|
+
isAuthByUsername,
|
|
289
|
+
"accessKey in start payload:",
|
|
290
|
+
accessKey
|
|
291
|
+
);
|
|
286
292
|
// check access by username for private bots
|
|
287
293
|
if (isAuthByUsername) {
|
|
288
294
|
const requestUsername = this.getTgUsername(ctx);
|
|
@@ -306,9 +312,14 @@ export class App {
|
|
|
306
312
|
const accessKeys =
|
|
307
313
|
this.config.envConfig.BOT_ACCESS_KEYS &&
|
|
308
314
|
this.config.envConfig.BOT_ACCESS_KEYS.split(",").map((key) => key.trim());
|
|
315
|
+
|
|
316
|
+
AccessKey.setDatabase(globalThis.db);
|
|
317
|
+
const checkTempKey = AccessKey.checkKeyValid(accessKey);
|
|
318
|
+
|
|
309
319
|
if (
|
|
310
320
|
accessKeys &&
|
|
311
|
-
accessKeys.every((key) => key.toLowerCase() !== accessKey?.toLowerCase())
|
|
321
|
+
accessKeys.every((key) => key.toLowerCase() !== accessKey?.toLowerCase()) &&
|
|
322
|
+
!checkTempKey
|
|
312
323
|
) {
|
|
313
324
|
return ctx.reply("Access denied. Your access key is not valid.");
|
|
314
325
|
}
|
|
@@ -321,15 +332,18 @@ export class App {
|
|
|
321
332
|
|
|
322
333
|
const userRefIdFromStart = startPayload ? parseInt(startPayload) : 0;
|
|
323
334
|
|
|
324
|
-
await this.registerUser(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
335
|
+
await this.registerUser(
|
|
336
|
+
{
|
|
337
|
+
user_refid: userRefIdFromStart,
|
|
338
|
+
tg_id: ctx.from.id,
|
|
339
|
+
tg_username: tgUsername,
|
|
340
|
+
tg_first_name: ctx.from.first_name || tgUsername,
|
|
341
|
+
tg_last_name: ctx.from.last_name || "",
|
|
342
|
+
role: "user",
|
|
343
|
+
language: ctx.from.language_code || "en",
|
|
344
|
+
},
|
|
345
|
+
accessKey || undefined
|
|
346
|
+
);
|
|
333
347
|
}
|
|
334
348
|
|
|
335
349
|
ctx.user = this.config.userStorage.find(tgUsername);
|
|
@@ -433,7 +447,9 @@ export class App {
|
|
|
433
447
|
) {
|
|
434
448
|
this.messageHandlers.forEach(async (handler: any) => {
|
|
435
449
|
if (ctx.caught) {
|
|
436
|
-
this.debugLog(
|
|
450
|
+
this.debugLog(
|
|
451
|
+
"Message already caught by another handler, skipping remaining handlers."
|
|
452
|
+
);
|
|
437
453
|
return;
|
|
438
454
|
}
|
|
439
455
|
|
|
@@ -447,7 +463,9 @@ export class App {
|
|
|
447
463
|
},
|
|
448
464
|
});
|
|
449
465
|
if (ctx.caught) {
|
|
450
|
-
this.debugLog(
|
|
466
|
+
this.debugLog(
|
|
467
|
+
"Message handler route caught the message, skipping remaining handlers."
|
|
468
|
+
);
|
|
451
469
|
return;
|
|
452
470
|
}
|
|
453
471
|
}
|
|
@@ -459,17 +477,25 @@ export class App {
|
|
|
459
477
|
: handler.constructor?.name || "unknown";
|
|
460
478
|
|
|
461
479
|
if (handlerIsClass && !ctx.caught) {
|
|
462
|
-
this.debugLog(
|
|
480
|
+
this.debugLog(
|
|
481
|
+
`Running message handler class ${nameHandler} for user ${ctx.user.username}`
|
|
482
|
+
);
|
|
463
483
|
await new handler(this).handle(ctx);
|
|
464
484
|
if (ctx.caught) {
|
|
465
|
-
this.debugLog(
|
|
485
|
+
this.debugLog(
|
|
486
|
+
"Message handler class caught the message, skipping remaining handlers."
|
|
487
|
+
);
|
|
466
488
|
return;
|
|
467
489
|
}
|
|
468
490
|
} else if (!handlerIsClass && typeof handler === "function" && !ctx.caught) {
|
|
469
|
-
this.debugLog(
|
|
491
|
+
this.debugLog(
|
|
492
|
+
`Running message handler function ${nameHandler} for user ${ctx.user.username}`
|
|
493
|
+
);
|
|
470
494
|
await handler(ctx);
|
|
471
495
|
if (ctx.caught) {
|
|
472
|
-
this.debugLog(
|
|
496
|
+
this.debugLog(
|
|
497
|
+
"Message handler function caught the message, skipping remaining handlers."
|
|
498
|
+
);
|
|
473
499
|
return;
|
|
474
500
|
}
|
|
475
501
|
}
|
|
@@ -876,7 +902,7 @@ export class App {
|
|
|
876
902
|
}
|
|
877
903
|
sectionClass = this.sectionClasses.get(sectionId) as typeof Section;
|
|
878
904
|
}
|
|
879
|
-
this.debugLog("Using section class:", sectionClass);
|
|
905
|
+
this.debugLog("Using section class:", sectionClass.name);
|
|
880
906
|
|
|
881
907
|
let sectionInstance: Section | undefined;
|
|
882
908
|
|
|
@@ -890,6 +916,10 @@ export class App {
|
|
|
890
916
|
};
|
|
891
917
|
|
|
892
918
|
const createRunnedSection = (instance: Section, route: RunSectionRoute): RunnedSection => {
|
|
919
|
+
if (route.hasCallbackParams()) {
|
|
920
|
+
instance.setCallbackParams(route.getCallbackParams());
|
|
921
|
+
this.debugLog("Set callback params for section instance:", route.getCallbackParams());
|
|
922
|
+
}
|
|
893
923
|
return {
|
|
894
924
|
instance,
|
|
895
925
|
route,
|
|
@@ -1042,10 +1072,13 @@ export class App {
|
|
|
1042
1072
|
return section;
|
|
1043
1073
|
}
|
|
1044
1074
|
|
|
1045
|
-
async registerUser(data: UserRegistrationData): Promise<UserModel | null> {
|
|
1075
|
+
async registerUser(data: UserRegistrationData, accessKey?: string): Promise<UserModel | null> {
|
|
1046
1076
|
try {
|
|
1047
1077
|
const user = await UserModel.register(data);
|
|
1048
1078
|
|
|
1079
|
+
if (accessKey) {
|
|
1080
|
+
AccessKey.markUsed(accessKey, user.id);
|
|
1081
|
+
}
|
|
1049
1082
|
if (this.config.userStorage) {
|
|
1050
1083
|
this.config.userStorage.add(data.tg_username, user);
|
|
1051
1084
|
this.debugLog("User added to storage:", data.tg_username);
|
|
@@ -1063,22 +1096,22 @@ export class App {
|
|
|
1063
1096
|
* @param tgUsername Telegram username of the user to remember. This method checks if the user exists in storage, and if not, tries to fetch it from the database and add to storage. This is useful for cases when user data might be updated in the database and we want to refresh the storage with the latest data.
|
|
1064
1097
|
* @returns A boolean indicating whether the user was successfully remembered (true) or not (false).
|
|
1065
1098
|
*/
|
|
1066
|
-
|
|
1099
|
+
rememberUser(tgUsername: string): boolean {
|
|
1067
1100
|
if (this.config.userStorage && !this.config.userStorage.exists(tgUsername)) {
|
|
1068
|
-
this.debugLog("Warning: Username not found in storage:", tgUsername);
|
|
1069
|
-
this.debugLog("Trying getting to database:", tgUsername);
|
|
1070
|
-
|
|
1101
|
+
this.debugLog("Warning: Remembering, Username not found in storage:", tgUsername);
|
|
1102
|
+
this.debugLog("Remembering, Trying getting to database:", tgUsername);
|
|
1103
|
+
|
|
1071
1104
|
// Try to get user from database and add to storage
|
|
1072
1105
|
UserModel.resolveDb();
|
|
1073
1106
|
const userFromDb = UserModel.findByUsername(tgUsername);
|
|
1074
|
-
|
|
1107
|
+
|
|
1075
1108
|
if (userFromDb) {
|
|
1076
1109
|
this.config.userStorage.add(tgUsername, userFromDb);
|
|
1077
1110
|
this.debugLog("Success: User found in database and added to storage:", tgUsername);
|
|
1078
1111
|
this.debugLog('Success: Remembered user "' + tgUsername + '"');
|
|
1079
1112
|
return true;
|
|
1080
1113
|
} else {
|
|
1081
|
-
this.debugLog("Warning: User not found in database:", tgUsername);
|
|
1114
|
+
this.debugLog("Warning: Remembering, User not found in database:", tgUsername);
|
|
1082
1115
|
}
|
|
1083
1116
|
}
|
|
1084
1117
|
return false;
|
|
@@ -1328,7 +1361,7 @@ export class App {
|
|
|
1328
1361
|
const source = `${colors.bright}${colors.fg.magenta}AppDebug${colors.reset}`;
|
|
1329
1362
|
|
|
1330
1363
|
// Format args: highlight objects, errors, etc.
|
|
1331
|
-
const formattedArgs = args.map(arg => {
|
|
1364
|
+
const formattedArgs = args.map((arg) => {
|
|
1332
1365
|
if (arg instanceof Error) {
|
|
1333
1366
|
return `${colors.fg.red}${arg.stack || arg.message}${colors.reset}`;
|
|
1334
1367
|
}
|
package/src/core/BotArtisan.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
2
|
import { Artisan } from '../illumination/Artisan';
|
|
3
|
+
import { UserModel } from '../user/UserModel';
|
|
4
|
+
import type { Database } from 'bun:sqlite'
|
|
4
5
|
|
|
5
6
|
export interface BotArtisanOptions {
|
|
6
7
|
botName: string;
|
|
7
8
|
sectionsPath?: string;
|
|
9
|
+
db?: Database;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export class BotArtisan {
|
|
@@ -13,7 +15,11 @@ export class BotArtisan {
|
|
|
13
15
|
|
|
14
16
|
constructor(botPath: string, options: BotArtisanOptions) {
|
|
15
17
|
this.options = options;
|
|
16
|
-
this.artisan = new Artisan(botPath);
|
|
18
|
+
this.artisan = new Artisan(botPath, { db: options.db });
|
|
19
|
+
|
|
20
|
+
if (options.db) {
|
|
21
|
+
UserModel.setDatabase(options.db);
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
async run(): Promise<void> {
|
|
@@ -49,6 +55,40 @@ export class BotArtisan {
|
|
|
49
55
|
await this.artisan.listSections();
|
|
50
56
|
break;
|
|
51
57
|
|
|
58
|
+
case 'list:users': {
|
|
59
|
+
const users = UserModel.getAll();
|
|
60
|
+
if (!users.length) {
|
|
61
|
+
console.log(chalk.yellow('No users found.'));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
console.log(chalk.green('Users:'));
|
|
65
|
+
users.forEach(u => {
|
|
66
|
+
console.log(
|
|
67
|
+
`ID: ${u.id} | Username: ${u.tgUsername} | Name: ${u.tgName} | Role: ${u.role}`
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case 'set:admin': {
|
|
74
|
+
if (args.length < 1) {
|
|
75
|
+
console.error(chalk.red('❌ Error: User ID is required'));
|
|
76
|
+
console.log('Usage: artisan set:admin <userId>');
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const userId = Number(args[0]);
|
|
80
|
+
|
|
81
|
+
const user = UserModel.findById(userId);
|
|
82
|
+
|
|
83
|
+
if (!user) {
|
|
84
|
+
console.error(chalk.red(`❌ Error: User with ID ${userId} not found`));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
UserModel.update(userId, { role: 'admin' });
|
|
88
|
+
console.log(chalk.green(`User ${user.tgUsername} (ID: ${userId}) is now admin.`));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
default:
|
|
53
93
|
console.error(chalk.red(`❌ Unknown command: ${command}`));
|
|
54
94
|
this.showHelp();
|
|
@@ -71,10 +111,15 @@ Available commands:
|
|
|
71
111
|
add:method <section> <name> Add a new method to existing section
|
|
72
112
|
list:sections List all sections
|
|
73
113
|
|
|
114
|
+
list:users List all users
|
|
115
|
+
set:admin <userId> Set user role to admin
|
|
116
|
+
|
|
74
117
|
Examples:
|
|
75
118
|
artisan make:section Auth Create new AuthSection
|
|
76
119
|
artisan add:method Auth login Add login method to AuthSection
|
|
77
120
|
artisan list:sections Show all available sections
|
|
121
|
+
artisan list:users Show all users
|
|
122
|
+
artisan set:admin 1 Set user with ID 1 as admin
|
|
78
123
|
`);
|
|
79
124
|
}
|
|
80
125
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import type { Database } from 'bun:sqlite';
|
|
4
|
+
|
|
5
|
+
export type ArtisanOptions = {
|
|
6
|
+
db?: Database;
|
|
7
|
+
};
|
|
3
8
|
|
|
4
9
|
export class Artisan {
|
|
5
10
|
private basePath: string;
|
|
11
|
+
private options: ArtisanOptions = {};
|
|
6
12
|
|
|
7
|
-
constructor(basePath: string) {
|
|
13
|
+
constructor(basePath: string, options: ArtisanOptions) {
|
|
8
14
|
this.basePath = basePath;
|
|
15
|
+
this.options = options;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
/**
|
|
@@ -38,6 +38,16 @@ export class InlineKeyboard {
|
|
|
38
38
|
return this;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
appendArray(rows: any[][]): InlineKeyboard {
|
|
42
|
+
rows.forEach(row => this.append(row));
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
prependArray(rows: any[][]): InlineKeyboard {
|
|
47
|
+
rows.forEach(row => this.prepend(row));
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
prepend(...row: any[]): InlineKeyboard {
|
|
42
52
|
if (!Array.isArray(row)) {
|
|
43
53
|
this.keyboard.unshift([row]);
|
|
@@ -202,7 +202,7 @@ export default class Message2byte {
|
|
|
202
202
|
if (message) {
|
|
203
203
|
if ("media_group_id" in message || "caption" in message) {
|
|
204
204
|
const editMessageCaption = this.editMessageCaption(this.messageValue, this.messageExtra);
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
if (editMessageCaption && "message_id" in editMessageCaption) {
|
|
207
207
|
this.messageId = editMessageCaption.message_id as number;
|
|
208
208
|
}
|
|
@@ -74,6 +74,10 @@ export class RunSectionRoute {
|
|
|
74
74
|
getTriggers(): RunSectionRouteTrigger[] {
|
|
75
75
|
return this.runParams.triggers;
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
hasCallbackParams(): boolean {
|
|
79
|
+
return this.runParams.callbackParams && Array.from(this.runParams.callbackParams).length > 0;
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
getMethod(): string | null {
|
|
79
83
|
return this.runParams.method;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Model } from "./Model";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { UserModel } from "../user/UserModel";
|
|
4
|
+
|
|
5
|
+
export interface AccessKeyData {
|
|
6
|
+
id: number;
|
|
7
|
+
user_id: number;
|
|
8
|
+
key: string;
|
|
9
|
+
used?: number;
|
|
10
|
+
used_user_id?: number | null;
|
|
11
|
+
created_at?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class AccessKey extends Model {
|
|
15
|
+
static tableName = "access_keys";
|
|
16
|
+
|
|
17
|
+
/** Generate a secure random key (hex) */
|
|
18
|
+
static generateKey(lengthBytes = 24): string {
|
|
19
|
+
return crypto.randomBytes(lengthBytes).toString("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Create a new access key and ensure uniqueness */
|
|
23
|
+
static create(data: AccessKeyData): string {
|
|
24
|
+
const { user_id } = data;
|
|
25
|
+
|
|
26
|
+
const key = this.generateKey();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
this.execute(
|
|
30
|
+
`INSERT INTO ${this.tableName} (user_id, key, used, used_user_id) VALUES (?, ?, 0, NULL)`,
|
|
31
|
+
[user_id, key]
|
|
32
|
+
);
|
|
33
|
+
return key;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// If UNIQUE constraint violation, retry generating a new key
|
|
36
|
+
// other errors will bubble up
|
|
37
|
+
const msg = (e && (e as any).message) || "";
|
|
38
|
+
if (!msg.includes("UNIQUE") && !msg.includes("unique")) throw e;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static findByKey(key: string): AccessKeyData | null {
|
|
45
|
+
return this.queryOne(`SELECT * FROM ${this.tableName} WHERE key = ?`, [key]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static findById(id: number): AccessKeyData | null {
|
|
49
|
+
return this.queryOne(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static markUsed(key: string, usedUserId?: number): void {
|
|
53
|
+
this.execute(
|
|
54
|
+
`UPDATE ${this.tableName} SET used = 1, used_user_id = ?, created_at = created_at WHERE key = ?`,
|
|
55
|
+
[usedUserId || null, key]
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static revoke(key: string): void {
|
|
60
|
+
this.execute(`DELETE FROM ${this.tableName} WHERE key = ?`, [key]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static getAll(limit = 100): AccessKeyData[] {
|
|
64
|
+
return this.query(`SELECT * FROM ${this.tableName} ORDER BY created_at DESC LIMIT ?`, [limit]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Get all access keys for a given user */
|
|
68
|
+
static findByUser(userId: number, limit = 100): AccessKeyData[] {
|
|
69
|
+
return this.query(
|
|
70
|
+
`SELECT * FROM ${this.tableName} WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`,
|
|
71
|
+
[userId, limit]
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Return the owner `UserModel` for a given access entry or null */
|
|
76
|
+
static getUserByEntry(entry: AccessKeyData): any | null {
|
|
77
|
+
const userData = this.queryOne(`SELECT * FROM users WHERE id = ?`, [entry.user_id]);
|
|
78
|
+
if (!userData) return null;
|
|
79
|
+
try {
|
|
80
|
+
return UserModel.make(userData);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return userData;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static getUsedUserByEntry(entry: AccessKeyData): any | null {
|
|
87
|
+
if (!entry.used_user_id) return null;
|
|
88
|
+
const userData = this.queryOne(`SELECT * FROM users WHERE id = ?`, [entry.used_user_id]);
|
|
89
|
+
if (!userData) return null;
|
|
90
|
+
try {
|
|
91
|
+
return UserModel.make(userData);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return userData;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Find owner by key string */
|
|
98
|
+
static getUserByKey(key: string): any | null {
|
|
99
|
+
const entry = this.findByKey(key);
|
|
100
|
+
if (!entry) return null;
|
|
101
|
+
return this.getUserByEntry(entry as AccessKeyData);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static checkKeyValid(key: string): boolean {
|
|
105
|
+
const queryKey = this.queryOne(
|
|
106
|
+
`SELECT COUNT(*) as count FROM ${this.tableName} WHERE key = ? AND used = 0`,
|
|
107
|
+
[key]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return queryKey && queryKey.count > 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default AccessKey;
|
package/src/models/Model.ts
CHANGED
|
@@ -29,13 +29,13 @@ export abstract class Model {
|
|
|
29
29
|
return this.db.transaction(callback)();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
public static
|
|
32
|
+
public static paginate(paginateParams: ModelPaginateParams): PaginateResult {
|
|
33
33
|
const { page, route, routeParams, limit, whereSql, whereParams } = paginateParams;
|
|
34
34
|
const offset = (page - 1) * limit;
|
|
35
35
|
const sql = `SELECT * FROM ${this.tableName} ${whereSql} LIMIT ${offset}, ${limit}`;
|
|
36
36
|
|
|
37
|
-
const result =
|
|
38
|
-
const queryTotal =
|
|
37
|
+
const result = this.query(sql, whereParams);
|
|
38
|
+
const queryTotal = this.queryOne(`SELECT COUNT(*) as count FROM ${this.tableName} ${whereSql}`, whereParams);
|
|
39
39
|
const total = queryTotal ? queryTotal.count : 0;
|
|
40
40
|
const totalPages = Math.ceil(total / limit);
|
|
41
41
|
const hasPreviousPage = page > 1;
|
package/src/models/index.ts
CHANGED
package/src/user/UserModel.ts
CHANGED
|
@@ -140,6 +140,20 @@ export class UserModel extends Model {
|
|
|
140
140
|
throw new Error("Error not found user params:" + JSON.stringify(params));
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
static findById(id: number): UserModel | undefined {
|
|
144
|
+
if (this.db) {
|
|
145
|
+
const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
146
|
+
|
|
147
|
+
if (userData) {
|
|
148
|
+
return UserModel.make(userData);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
throw new Error("Database connection is not set.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
static findByUsername(tgUsername: string): UserModel | undefined {
|
|
144
158
|
if (this.db) {
|
|
145
159
|
const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE tg_username = ?`, [tgUsername]);
|
|
@@ -201,6 +215,32 @@ export class UserModel extends Model {
|
|
|
201
215
|
}
|
|
202
216
|
}
|
|
203
217
|
|
|
218
|
+
static update(id: number, attributes: Partial<UserAttributes>): UserModel | undefined {
|
|
219
|
+
if (this.db) {
|
|
220
|
+
const user = this.findById(id);
|
|
221
|
+
if (user) {
|
|
222
|
+
Object.assign(user.attributes, attributes);
|
|
223
|
+
const keys = Object.keys(attributes);
|
|
224
|
+
const values = Object.values(attributes);
|
|
225
|
+
const setString = keys.map(key => `${key} = ?`).join(', ');
|
|
226
|
+
this.db.run(`UPDATE ${this.tableName} SET ${setString} WHERE id = ?`, [...values, id]);
|
|
227
|
+
return user;
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
throw new Error("Database connection is not set.");
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static delete(id: number): boolean {
|
|
236
|
+
if (this.db) {
|
|
237
|
+
const result = this.db.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
238
|
+
return result.changes > 0;
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error("Database connection is not set.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
204
244
|
get username(): string {
|
|
205
245
|
return this.attributes.tg_username;
|
|
206
246
|
}
|
package/src/user/UserStore.ts
CHANGED
|
@@ -116,4 +116,23 @@ export class UserStore {
|
|
|
116
116
|
});
|
|
117
117
|
}, 60 * 1000);
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
deleteByUsername(tgUsername: string): boolean {
|
|
121
|
+
const user = this.usersMap.get(tgUsername);
|
|
122
|
+
if (user) {
|
|
123
|
+
this.usersSession.delete(user);
|
|
124
|
+
return this.usersMap.delete(tgUsername);
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
deleteById(userId: number): boolean {
|
|
130
|
+
const userEntry = Array.from(this.usersMap).find(([_, user]) => user.id === userId);
|
|
131
|
+
if (userEntry) {
|
|
132
|
+
const [tgUsername, user] = userEntry;
|
|
133
|
+
this.usersSession.delete(user);
|
|
134
|
+
return this.usersMap.delete(tgUsername);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
119
138
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- UP
|
|
2
|
+
CREATE TABLE IF NOT EXISTS access_keys (
|
|
3
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
|
+
user_id INTEGER NOT NULL,
|
|
5
|
+
key TEXT NOT NULL UNIQUE,
|
|
6
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
used_user_id INTEGER DEFAULT NULL,
|
|
8
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- DOWN
|
|
12
|
+
DROP TABLE IF EXISTS access_keys;
|