2025-12-13 13:27:05 +00:00
|
|
|
|
import 'package:drift/drift.dart';
|
2025-12-13 23:35:14 +00:00
|
|
|
|
import 'package:drift_postgres/drift_postgres.dart';
|
2025-12-13 13:27:05 +00:00
|
|
|
|
import '../database.dart';
|
|
|
|
|
|
import '../tables/users.dart';
|
|
|
|
|
|
import '../tables/auth.dart';
|
|
|
|
|
|
import '../tables/packs.dart';
|
|
|
|
|
|
import '../tables/relations.dart';
|
|
|
|
|
|
|
|
|
|
|
|
part 'user_dao.g.dart';
|
|
|
|
|
|
|
|
|
|
|
|
@DriftAccessor(tables: [Users, UserDatas, Tokens, RefreshTokens, UserPacks])
|
|
|
|
|
|
class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|
|
|
|
|
UserDao(super.db);
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// ==================== Users ====================
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить пользователя по ID
|
2026-01-06 21:11:44 +00:00
|
|
|
|
Future<User?> getUserById(String id) async {
|
|
|
|
|
|
final query = select(users)
|
|
|
|
|
|
..where((u) => u.id.equals(id))
|
|
|
|
|
|
..limit(1);
|
|
|
|
|
|
|
|
|
|
|
|
final results = await query.get();
|
|
|
|
|
|
return results.isNotEmpty ? results.first : null;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить пользователя с UserData
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<UserWithData?> getUserWithDataById(String id) async {
|
2026-01-08 13:02:47 +00:00
|
|
|
|
final query =
|
|
|
|
|
|
select(users).join([
|
|
|
|
|
|
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
|
|
|
|
|
|
])
|
|
|
|
|
|
..where(users.id.equals(id))
|
|
|
|
|
|
..limit(1);
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2026-01-03 14:42:05 +00:00
|
|
|
|
final results = await query.get();
|
|
|
|
|
|
if (results.isEmpty) return null;
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2026-01-03 14:42:05 +00:00
|
|
|
|
final result = results.first;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
return UserWithData(
|
|
|
|
|
|
user: result.readTable(users),
|
|
|
|
|
|
userData: result.readTableOrNull(userDatas),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить пользователя по email
|
|
|
|
|
|
Future<User?> getUserByEmail(String email) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (select(
|
|
|
|
|
|
users,
|
|
|
|
|
|
)..where((u) => u.email.equals(email))).getSingleOrNull();
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Получить пользователя по externalUserId
|
|
|
|
|
|
Future<User?> getUserByExternalId(String externalId) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (select(
|
|
|
|
|
|
users,
|
|
|
|
|
|
)..where((u) => u.externalUserId.equals(externalId))).getSingleOrNull();
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Создать пользователя
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<String> createUser(UsersCompanion user) async {
|
|
|
|
|
|
final inserted = await into(users).insertReturning(user);
|
|
|
|
|
|
return inserted.id;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Создать пользователя с UserData
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<String> createUserWithData({
|
2025-12-13 13:27:05 +00:00
|
|
|
|
required UsersCompanion user,
|
|
|
|
|
|
required UserDatasCompanion userData,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
return await transaction(() async {
|
2025-12-13 20:55:50 +00:00
|
|
|
|
final inserted = await into(users).insertReturning(user);
|
|
|
|
|
|
final userId = inserted.id;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
await into(userDatas).insert(userData.copyWith(userId: Value(userId)));
|
|
|
|
|
|
return userId;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Обновить пользователя
|
|
|
|
|
|
Future<bool> updateUser(User user) {
|
|
|
|
|
|
return update(users).replace(user);
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Обновить пользователя частично
|
|
|
|
|
|
Future<void> updateUserPartial(UsersCompanion updates) {
|
|
|
|
|
|
final userId = updates.id.value;
|
|
|
|
|
|
if (userId == null) throw ArgumentError('User ID is required');
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
return (update(users)..where((u) => u.id.equals(userId))).write(updates);
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Удалить пользователя (soft delete)
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<void> softDeleteUser(String userId) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(users)..where((u) => u.id.equals(userId))).write(
|
|
|
|
|
|
UsersCompanion(
|
|
|
|
|
|
isDeleted: const Value(true),
|
|
|
|
|
|
updatedAt: Value(PgDateTime(DateTime.now())),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить всех пользователей (для админки)
|
|
|
|
|
|
Future<List<User>> getAllUsers({
|
|
|
|
|
|
int? limit,
|
|
|
|
|
|
int? offset,
|
|
|
|
|
|
bool includeDeleted = false,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
final query = select(users);
|
|
|
|
|
|
if (!includeDeleted) {
|
|
|
|
|
|
query.where((u) => u.isDeleted.equals(false));
|
|
|
|
|
|
}
|
|
|
|
|
|
query.orderBy([(u) => OrderingTerm.desc(u.createdAt)]);
|
|
|
|
|
|
if (limit != null) {
|
|
|
|
|
|
query.limit(limit, offset: offset);
|
|
|
|
|
|
}
|
|
|
|
|
|
return query.get();
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Подсчитать пользователей
|
|
|
|
|
|
Future<int> countUsers({bool includeDeleted = false}) async {
|
|
|
|
|
|
final countExpr = users.id.count();
|
|
|
|
|
|
final query = selectOnly(users)..addColumns([countExpr]);
|
|
|
|
|
|
if (!includeDeleted) {
|
|
|
|
|
|
query.where(users.isDeleted.equals(false));
|
|
|
|
|
|
}
|
|
|
|
|
|
return await query.map((row) => row.read(countExpr)!).getSingle();
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить пользователей с активной подпиской
|
|
|
|
|
|
Stream<List<User>> watchUsersWithActiveSubscription() {
|
2025-12-13 23:35:14 +00:00
|
|
|
|
final now = PgDateTime(DateTime.now());
|
2025-12-13 14:48:00 +00:00
|
|
|
|
final query = select(users).join([
|
|
|
|
|
|
innerJoin(
|
|
|
|
|
|
db.userSubscriptions,
|
|
|
|
|
|
db.userSubscriptions.userId.equalsExp(users.id) &
|
2025-12-20 18:26:15 +00:00
|
|
|
|
db.userSubscriptions.start.isSmallerThanValue(now) &
|
|
|
|
|
|
db.userSubscriptions.finish.isBiggerThanValue(now),
|
2025-12-13 14:48:00 +00:00
|
|
|
|
),
|
|
|
|
|
|
])..where(users.isDeleted.equals(false));
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
|
|
|
|
|
return query.watch().map(
|
|
|
|
|
|
(rows) => rows.map((row) => row.readTable(users)).toList(),
|
2025-12-13 14:48:00 +00:00
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// ==================== UserData ====================
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить UserData пользователя
|
2026-01-03 14:48:57 +00:00
|
|
|
|
Future<UserData?> getUserData(String userId) async {
|
2026-01-06 21:20:43 +00:00
|
|
|
|
print('🔍 UserDao.getUserData: userId=$userId');
|
2026-01-08 13:02:47 +00:00
|
|
|
|
print(
|
|
|
|
|
|
'🔍 UserDao.getUserData: SQL: SELECT * FROM user_datas WHERE user_id = ? LIMIT 1',
|
|
|
|
|
|
);
|
2026-01-03 14:48:57 +00:00
|
|
|
|
final query = select(userDatas)
|
|
|
|
|
|
..where((ud) => ud.userId.equals(userId))
|
|
|
|
|
|
..limit(1);
|
|
|
|
|
|
|
|
|
|
|
|
final results = await query.get();
|
2026-01-06 21:20:43 +00:00
|
|
|
|
print('✅ UserDao.getUserData: Result count=${results.length}');
|
2026-01-03 14:48:57 +00:00
|
|
|
|
return results.isNotEmpty ? results.first : null;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Создать UserData
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<String> createUserData(UserDatasCompanion userData) async {
|
|
|
|
|
|
final inserted = await into(userDatas).insertReturning(userData);
|
|
|
|
|
|
return inserted.id;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Обновить UserData
|
|
|
|
|
|
Future<bool> updateUserData(UserData userData) {
|
|
|
|
|
|
return update(userDatas).replace(userData);
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Обновить UserData частично
|
|
|
|
|
|
Future<void> updateUserDataPartial(UserDatasCompanion updates) {
|
|
|
|
|
|
final userId = updates.userId.value;
|
|
|
|
|
|
if (userId == null) throw ArgumentError('User ID is required');
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
|
|
|
|
|
return (update(userDatas)..where((ud) => ud.userId.equals(userId))).write(
|
|
|
|
|
|
updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Обновить время последнего визита
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<void> updateLastOnline(String userId) async {
|
2025-12-13 23:35:14 +00:00
|
|
|
|
final now = PgDateTime(DateTime.now());
|
2025-12-20 18:26:15 +00:00
|
|
|
|
await (update(userDatas)..where((ud) => ud.userId.equals(userId))).write(
|
|
|
|
|
|
UserDatasCompanion(lastTimeOnline: Value(now), updatedAt: Value(now)),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// ==================== Tokens ====================
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Получить токен по значению (только активные)
|
2026-01-06 21:11:44 +00:00
|
|
|
|
Future<Token?> getTokenByValue(String tokenValue) async {
|
|
|
|
|
|
final query = select(tokens)
|
|
|
|
|
|
..where((t) => t.token.equals(tokenValue) & t.isDeleted.equals(false))
|
|
|
|
|
|
..limit(1);
|
|
|
|
|
|
|
|
|
|
|
|
final results = await query.get();
|
|
|
|
|
|
return results.isNotEmpty ? results.first : null;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Получить токен по externalUserId (только активные)
|
2026-01-03 14:42:05 +00:00
|
|
|
|
Future<Token?> getTokenByExternalUserId(String externalUserId) async {
|
|
|
|
|
|
final query = select(tokens)
|
|
|
|
|
|
..where(
|
|
|
|
|
|
(t) =>
|
2026-01-08 13:02:47 +00:00
|
|
|
|
t.externalUserId.equals(externalUserId) & t.isDeleted.equals(false),
|
2026-01-03 14:42:05 +00:00
|
|
|
|
)
|
|
|
|
|
|
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
|
|
|
|
|
..limit(1);
|
|
|
|
|
|
|
|
|
|
|
|
final results = await query.get();
|
|
|
|
|
|
return results.isNotEmpty ? results.first : null;
|
2025-12-13 14:48:00 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Получить токен пользователя (только активные)
|
2026-01-06 21:11:44 +00:00
|
|
|
|
Future<Token?> getTokenByUserId(String userId) async {
|
|
|
|
|
|
final query = select(tokens)
|
|
|
|
|
|
..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false))
|
2026-01-08 13:02:47 +00:00
|
|
|
|
..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
|
2026-01-06 21:11:44 +00:00
|
|
|
|
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
|
|
|
|
|
..limit(1);
|
|
|
|
|
|
|
|
|
|
|
|
final results = await query.get();
|
|
|
|
|
|
return results.isNotEmpty ? results.first : null;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Создать токен
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<String> createToken(TokensCompanion token) async {
|
|
|
|
|
|
final inserted = await into(tokens).insertReturning(token);
|
|
|
|
|
|
return inserted.id;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 21:10:06 +00:00
|
|
|
|
/// Удалить токен (hard delete)
|
|
|
|
|
|
Future<void> deleteToken(String token) async {
|
|
|
|
|
|
await (delete(tokens)..where((t) => t.token.equals(token))).go();
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Удалить токен (soft delete)
|
|
|
|
|
|
Future<int> softDeleteToken(String tokenId) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(tokens)..where((t) => t.id.equals(tokenId))).write(
|
|
|
|
|
|
TokensCompanion(
|
|
|
|
|
|
isDeleted: const Value(true),
|
|
|
|
|
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Удалить токен по значению (soft delete)
|
|
|
|
|
|
Future<int> softDeleteTokenByValue(String tokenValue) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(tokens)..where((t) => t.token.equals(tokenValue))).write(
|
|
|
|
|
|
TokensCompanion(
|
|
|
|
|
|
isDeleted: const Value(true),
|
|
|
|
|
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Удалить истекшие токены (soft delete)
|
|
|
|
|
|
Future<int> softDeleteExpiredTokens() {
|
2025-12-14 21:10:06 +00:00
|
|
|
|
final now = PgDateTime(DateTime.now());
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(tokens)..where(
|
|
|
|
|
|
(t) => t.expires.isSmallerThanValue(now) & t.isDeleted.equals(false),
|
|
|
|
|
|
))
|
|
|
|
|
|
.write(
|
|
|
|
|
|
TokensCompanion(isDeleted: const Value(true), deletedAt: Value(now)),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// ==================== RefreshTokens ====================
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Получить refresh token по JTI (только активные)
|
2025-12-13 13:27:05 +00:00
|
|
|
|
Future<RefreshToken?> getRefreshTokenByJti(String jti) {
|
2025-12-14 20:42:38 +00:00
|
|
|
|
return (select(refreshTokens)
|
2025-12-20 18:26:15 +00:00
|
|
|
|
..where((rt) => rt.jti.equals(jti) & rt.isDeleted.equals(false)))
|
|
|
|
|
|
.getSingleOrNull();
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Получить активные refresh токены пользователя (только активные)
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<List<RefreshToken>> getActiveRefreshTokens(String userId) {
|
2025-12-13 13:27:05 +00:00
|
|
|
|
return (select(refreshTokens)
|
2025-12-20 18:26:15 +00:00
|
|
|
|
..where((rt) => rt.userId.equals(userId) & rt.isDeleted.equals(false))
|
|
|
|
|
|
..where((rt) => rt.isBlacklisted.equals(false))
|
|
|
|
|
|
..where(
|
|
|
|
|
|
(rt) => rt.expiresAt.isBiggerThanValue(PgDateTime(DateTime.now())),
|
|
|
|
|
|
)
|
|
|
|
|
|
..orderBy([(rt) => OrderingTerm.desc(rt.createdAt)]))
|
|
|
|
|
|
.get();
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Создать refresh token
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<String> createRefreshToken(RefreshTokensCompanion token) async {
|
|
|
|
|
|
final inserted = await into(refreshTokens).insertReturning(token);
|
|
|
|
|
|
return inserted.id;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Отозвать refresh token (blacklist)
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<void> revokeRefreshToken(String tokenId) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(refreshTokens)..where((rt) => rt.id.equals(tokenId))).write(
|
|
|
|
|
|
const RefreshTokensCompanion(isBlacklisted: Value(true)),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Отозвать refresh token по JTI
|
|
|
|
|
|
Future<void> revokeRefreshTokenByJti(String jti) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(refreshTokens)..where((rt) => rt.jti.equals(jti))).write(
|
|
|
|
|
|
const RefreshTokensCompanion(isBlacklisted: Value(true)),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 21:10:06 +00:00
|
|
|
|
/// Удалить истекшие refresh токены (hard delete)
|
|
|
|
|
|
Future<void> deleteExpiredRefreshTokens() async {
|
|
|
|
|
|
final now = PgDateTime(DateTime.now());
|
2025-12-20 18:26:15 +00:00
|
|
|
|
await (delete(
|
|
|
|
|
|
refreshTokens,
|
|
|
|
|
|
)..where((rt) => rt.expiresAt.isSmallerThanValue(now))).go();
|
2025-12-14 21:10:06 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Удалить истекшие refresh токены (soft delete)
|
|
|
|
|
|
Future<int> softDeleteExpiredRefreshTokens() {
|
2025-12-14 21:10:06 +00:00
|
|
|
|
final now = PgDateTime(DateTime.now());
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(refreshTokens)..where(
|
|
|
|
|
|
(rt) =>
|
|
|
|
|
|
rt.expiresAt.isSmallerThanValue(now) & rt.isDeleted.equals(false),
|
|
|
|
|
|
))
|
|
|
|
|
|
.write(
|
|
|
|
|
|
RefreshTokensCompanion(
|
|
|
|
|
|
isDeleted: const Value(true),
|
|
|
|
|
|
deletedAt: Value(now),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// ==================== TelegramAuthCodes ====================
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Получить код авторизации (только активные)
|
2025-12-13 13:27:05 +00:00
|
|
|
|
Future<TelegramAuthCode?> getAuthCode(String code) {
|
2025-12-14 20:42:38 +00:00
|
|
|
|
return (select(db.telegramAuthCodes)
|
2025-12-20 18:26:15 +00:00
|
|
|
|
..where((ac) => ac.code.equals(code) & ac.isDeleted.equals(false)))
|
|
|
|
|
|
.getSingleOrNull();
|
2025-12-14 20:42:38 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-14 20:42:38 +00:00
|
|
|
|
/// Удалить код авторизации (soft delete)
|
|
|
|
|
|
Future<void> softDeleteAuthCode(String codeId) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(
|
|
|
|
|
|
db.telegramAuthCodes,
|
|
|
|
|
|
)..where((ac) => ac.id.equals(codeId))).write(
|
|
|
|
|
|
TelegramAuthCodesCompanion(
|
|
|
|
|
|
isDeleted: const Value(true),
|
|
|
|
|
|
deletedAt: Value(PgDateTime(DateTime.now())),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Создать код авторизации
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<String> createAuthCode(TelegramAuthCodesCompanion code) async {
|
|
|
|
|
|
final inserted = await into(db.telegramAuthCodes).insertReturning(code);
|
|
|
|
|
|
return inserted.id;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Отметить код как использованный
|
|
|
|
|
|
Future<void> markAuthCodeAsUsed(String code) {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (update(
|
|
|
|
|
|
db.telegramAuthCodes,
|
|
|
|
|
|
)..where((ac) => ac.code.equals(code))).write(
|
|
|
|
|
|
TelegramAuthCodesCompanion(
|
|
|
|
|
|
isUsed: const Value(true),
|
|
|
|
|
|
usedAt: Value(PgDateTime(DateTime.now())),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Удалить истекшие коды
|
|
|
|
|
|
Future<int> deleteExpiredAuthCodes() {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
return (delete(db.telegramAuthCodes)..where(
|
|
|
|
|
|
(ac) => ac.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now())),
|
|
|
|
|
|
))
|
|
|
|
|
|
.go();
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// ==================== User Packs ====================
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Получить паки пользователя
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<List<CardPack>> getUserPacks(String userId) async {
|
2025-12-13 13:27:05 +00:00
|
|
|
|
final query = select(cardPacks).join([
|
|
|
|
|
|
innerJoin(
|
|
|
|
|
|
userPacks,
|
|
|
|
|
|
userPacks.packId.equalsExp(cardPacks.id) &
|
2025-12-20 18:26:15 +00:00
|
|
|
|
userPacks.userId.equals(userId),
|
2025-12-13 13:27:05 +00:00
|
|
|
|
),
|
|
|
|
|
|
]);
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
return query.map((row) => row.readTable(cardPacks)).get();
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Проверить, есть ли у пользователя доступ к паку
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<bool> hasPackAccess(String userId, String packId) async {
|
2026-01-06 21:20:43 +00:00
|
|
|
|
print('🔍 UserDao.hasPackAccess: userId=$userId, packId=$packId');
|
2026-01-08 13:02:47 +00:00
|
|
|
|
print(
|
|
|
|
|
|
'🔍 UserDao.hasPackAccess: SQL: SELECT * FROM user_packs WHERE user_id = ? AND pack_id = ? LIMIT 1',
|
|
|
|
|
|
);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
final query = select(userPacks)
|
2026-01-03 14:25:32 +00:00
|
|
|
|
..where((up) => up.userId.equals(userId) & up.packId.equals(packId))
|
|
|
|
|
|
..limit(1);
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2026-01-03 14:25:32 +00:00
|
|
|
|
final results = await query.get();
|
2026-01-08 13:02:47 +00:00
|
|
|
|
print(
|
|
|
|
|
|
'✅ UserDao.hasPackAccess: Result count=${results.length}, hasAccess=${results.isNotEmpty}',
|
|
|
|
|
|
);
|
2026-01-03 14:25:32 +00:00
|
|
|
|
return results.isNotEmpty;
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Дать пользователю доступ к паку
|
|
|
|
|
|
Future<void> grantPackAccess({
|
2025-12-13 20:55:50 +00:00
|
|
|
|
required String userId,
|
|
|
|
|
|
required String packId,
|
2025-12-13 13:27:05 +00:00
|
|
|
|
String grantType = 'purchase',
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
await into(userPacks).insert(
|
|
|
|
|
|
UserPacksCompanion.insert(
|
|
|
|
|
|
userId: userId,
|
|
|
|
|
|
packId: packId,
|
|
|
|
|
|
grantType: Value(grantType),
|
|
|
|
|
|
),
|
2025-12-20 18:26:15 +00:00
|
|
|
|
mode: InsertMode.insertOrIgnore, // игнорировать если уже есть
|
2025-12-13 13:27:05 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
/// Отозвать доступ к паку
|
2025-12-13 20:55:50 +00:00
|
|
|
|
Future<void> revokePackAccess(String userId, String packId) async {
|
2025-12-20 18:26:15 +00:00
|
|
|
|
await (delete(
|
|
|
|
|
|
userPacks,
|
|
|
|
|
|
)..where((up) => up.userId.equals(userId) & up.packId.equals(packId))).go();
|
2025-12-13 13:27:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Вспомогательный класс для User с UserData
|
|
|
|
|
|
class UserWithData {
|
|
|
|
|
|
final User user;
|
|
|
|
|
|
final UserData? userData;
|
2025-12-20 18:26:15 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
UserWithData({required this.user, this.userData});
|
|
|
|
|
|
}
|