mnemo_cards/mnemo_cards_backend/lib/database/daos/user_dao.dart

439 lines
14 KiB
Dart
Raw Normal View History

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 {
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();
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});
}