import 'package:drift/drift.dart'; import 'package:drift_postgres/drift_postgres.dart'; 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 with _$UserDaoMixin { UserDao(super.db); // ==================== Users ==================== /// Получить пользователя по ID Future getUserById(String id) { return (select(users)..where((u) => u.id.equals(id))).getSingleOrNull(); } /// Получить пользователя с UserData Future getUserWithDataById(String id) async { final query = select(users).join([ leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)), ])..where(users.id.equals(id)); final result = await query.getSingleOrNull(); if (result == null) return null; return UserWithData( user: result.readTable(users), userData: result.readTableOrNull(userDatas), ); } /// Получить пользователя по email Future getUserByEmail(String email) { return (select(users)..where((u) => u.email.equals(email))).getSingleOrNull(); } /// Получить пользователя по externalUserId Future getUserByExternalId(String externalId) { return (select(users) ..where((u) => u.externalUserId.equals(externalId)) ).getSingleOrNull(); } /// Создать пользователя Future createUser(UsersCompanion user) async { final inserted = await into(users).insertReturning(user); return inserted.id; } /// Создать пользователя с UserData Future createUserWithData({ required UsersCompanion user, required UserDatasCompanion userData, }) async { return await transaction(() async { final inserted = await into(users).insertReturning(user); final userId = inserted.id; await into(userDatas).insert(userData.copyWith(userId: Value(userId))); return userId; }); } /// Обновить пользователя Future updateUser(User user) { return update(users).replace(user); } /// Обновить пользователя частично Future updateUserPartial(UsersCompanion updates) { final userId = updates.id.value; if (userId == null) throw ArgumentError('User ID is required'); return (update(users)..where((u) => u.id.equals(userId))).write(updates); } /// Удалить пользователя (soft delete) Future softDeleteUser(String userId) { return (update(users)..where((u) => u.id.equals(userId))) .write(UsersCompanion( isDeleted: const Value(true), updatedAt: Value(PgDateTime(DateTime.now())), )); } /// Получить всех пользователей (для админки) Future> 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(); } /// Подсчитать пользователей Future 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(); } /// Получить пользователей с активной подпиской Stream> watchUsersWithActiveSubscription() { final now = PgDateTime(DateTime.now()); final query = select(users).join([ innerJoin( db.userSubscriptions, db.userSubscriptions.userId.equalsExp(users.id) & db.userSubscriptions.start.isSmallerThanValue(now) & db.userSubscriptions.finish.isBiggerThanValue(now), ), ])..where(users.isDeleted.equals(false)); return query.watch().map((rows) => rows.map((row) => row.readTable(users)).toList() ); } // ==================== UserData ==================== /// Получить UserData пользователя Future getUserData(String userId) { return (select(userDatas)..where((ud) => ud.userId.equals(userId))) .getSingleOrNull(); } /// Создать UserData Future createUserData(UserDatasCompanion userData) async { final inserted = await into(userDatas).insertReturning(userData); return inserted.id; } /// Обновить UserData Future updateUserData(UserData userData) { return update(userDatas).replace(userData); } /// Обновить UserData частично Future updateUserDataPartial(UserDatasCompanion updates) { final userId = updates.userId.value; if (userId == null) throw ArgumentError('User ID is required'); return (update(userDatas)..where((ud) => ud.userId.equals(userId))) .write(updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now())))); } /// Обновить время последнего визита Future updateLastOnline(String userId) async { final now = PgDateTime(DateTime.now()); await (update(userDatas)..where((ud) => ud.userId.equals(userId))) .write(UserDatasCompanion( lastTimeOnline: Value(now), updatedAt: Value(now), )); } // ==================== Tokens ==================== /// Получить токен по значению (только активные) Future getTokenByValue(String tokenValue) { return (select(tokens) ..where((t) => t.token.equals(tokenValue) & t.isDeleted.equals(false)) ).getSingleOrNull(); } /// Получить токен по externalUserId (только активные) Future getTokenByExternalUserId(String externalUserId) { return (select(tokens) ..where((t) => t.externalUserId.equals(externalUserId) & t.isDeleted.equals(false)) ..orderBy([(t) => OrderingTerm.desc(t.created)]) ..limit(1) ).getSingleOrNull(); } /// Получить токен пользователя (только активные) Future getTokenByUserId(String userId) { return (select(tokens) ..where((t) => t.userId.equals(userId) & t.isDeleted.equals(false)) ..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now()))) ..orderBy([(t) => OrderingTerm.desc(t.created)]) ).getSingleOrNull(); } /// Создать токен Future createToken(TokensCompanion token) async { final inserted = await into(tokens).insertReturning(token); return inserted.id; } /// Удалить токен (hard delete) Future deleteToken(String token) async { await (delete(tokens)..where((t) => t.token.equals(token))).go(); } /// Удалить токен (soft delete) Future softDeleteToken(String tokenId) { return (update(tokens)..where((t) => t.id.equals(tokenId))) .write(TokensCompanion( isDeleted: const Value(true), deletedAt: Value(PgDateTime(DateTime.now())), )); } /// Удалить токен по значению (soft delete) Future softDeleteTokenByValue(String tokenValue) { return (update(tokens)..where((t) => t.token.equals(tokenValue))) .write(TokensCompanion( isDeleted: const Value(true), deletedAt: Value(PgDateTime(DateTime.now())), )); } /// Удалить истекшие токены (soft delete) Future softDeleteExpiredTokens() { final now = PgDateTime(DateTime.now()); return (update(tokens) ..where((t) => t.expires.isSmallerThanValue(now) & t.isDeleted.equals(false)) ).write(TokensCompanion( isDeleted: const Value(true), deletedAt: Value(now), )); } // ==================== RefreshTokens ==================== /// Получить refresh token по JTI (только активные) Future getRefreshTokenByJti(String jti) { return (select(refreshTokens) ..where((rt) => rt.jti.equals(jti) & rt.isDeleted.equals(false)) ).getSingleOrNull(); } /// Получить активные refresh токены пользователя (только активные) Future> getActiveRefreshTokens(String userId) { return (select(refreshTokens) ..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(); } /// Создать refresh token Future createRefreshToken(RefreshTokensCompanion token) async { final inserted = await into(refreshTokens).insertReturning(token); return inserted.id; } /// Отозвать refresh token (blacklist) Future revokeRefreshToken(String tokenId) { return (update(refreshTokens)..where((rt) => rt.id.equals(tokenId))) .write(const RefreshTokensCompanion( isBlacklisted: Value(true), )); } /// Отозвать refresh token по JTI Future revokeRefreshTokenByJti(String jti) { return (update(refreshTokens)..where((rt) => rt.jti.equals(jti))) .write(const RefreshTokensCompanion( isBlacklisted: Value(true), )); } /// Удалить истекшие refresh токены (hard delete) Future deleteExpiredRefreshTokens() async { final now = PgDateTime(DateTime.now()); await (delete(refreshTokens) ..where((rt) => rt.expiresAt.isSmallerThanValue(now)) ).go(); } /// Удалить истекшие refresh токены (soft delete) Future softDeleteExpiredRefreshTokens() { final now = PgDateTime(DateTime.now()); return (update(refreshTokens) ..where((rt) => rt.expiresAt.isSmallerThanValue(now) & rt.isDeleted.equals(false)) ).write(RefreshTokensCompanion( isDeleted: const Value(true), deletedAt: Value(now), )); } // ==================== TelegramAuthCodes ==================== /// Получить код авторизации (только активные) Future getAuthCode(String code) { return (select(db.telegramAuthCodes) ..where((ac) => ac.code.equals(code) & ac.isDeleted.equals(false)) ).getSingleOrNull(); } /// Удалить код авторизации (soft delete) Future softDeleteAuthCode(String codeId) { return (update(db.telegramAuthCodes)..where((ac) => ac.id.equals(codeId))) .write(TelegramAuthCodesCompanion( isDeleted: const Value(true), deletedAt: Value(PgDateTime(DateTime.now())), )); } /// Создать код авторизации Future createAuthCode(TelegramAuthCodesCompanion code) async { final inserted = await into(db.telegramAuthCodes).insertReturning(code); return inserted.id; } /// Отметить код как использованный Future markAuthCodeAsUsed(String code) { return (update(db.telegramAuthCodes)..where((ac) => ac.code.equals(code))) .write(TelegramAuthCodesCompanion( isUsed: const Value(true), usedAt: Value(PgDateTime(DateTime.now())), )); } /// Удалить истекшие коды Future deleteExpiredAuthCodes() { return (delete(db.telegramAuthCodes) ..where((ac) => ac.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now()))) ).go(); } // ==================== User Packs ==================== /// Получить паки пользователя Future> getUserPacks(String userId) async { final query = select(cardPacks).join([ innerJoin( userPacks, userPacks.packId.equalsExp(cardPacks.id) & userPacks.userId.equals(userId), ), ]); return query.map((row) => row.readTable(cardPacks)).get(); } /// Проверить, есть ли у пользователя доступ к паку Future hasPackAccess(String userId, String packId) async { final query = select(userPacks) ..where((up) => up.userId.equals(userId) & up.packId.equals(packId)); final result = await query.getSingleOrNull(); return result != null; } /// Дать пользователю доступ к паку Future grantPackAccess({ required String userId, required String packId, String grantType = 'purchase', }) async { await into(userPacks).insert( UserPacksCompanion.insert( userId: userId, packId: packId, grantType: Value(grantType), ), mode: InsertMode.insertOrIgnore, // игнорировать если уже есть ); } /// Отозвать доступ к паку Future revokePackAccess(String userId, String packId) async { await (delete(userPacks) ..where((up) => up.userId.equals(userId) & up.packId.equals(packId)) ).go(); } } /// Вспомогательный класс для User с UserData class UserWithData { final User user; final UserData? userData; UserWithData({required this.user, this.userData}); }