mnemo_cards/mnemo_cards_backend/lib/database/daos/user_dao.dart
Dmitry 306b2fdca7
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
fixes
2025-12-13 17:48:00 +03:00

344 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:drift/drift.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<AppDatabase> with _$UserDaoMixin {
UserDao(super.db);
// ==================== Users ====================
/// Получить пользователя по ID
Future<User?> getUserById(int id) {
return (select(users)..where((u) => u.id.equals(id))).getSingleOrNull();
}
/// Получить пользователя с UserData
Future<UserWithData?> getUserWithDataById(int 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<User?> getUserByEmail(String email) {
return (select(users)..where((u) => u.email.equals(email))).getSingleOrNull();
}
/// Получить пользователя по externalUserId
Future<User?> getUserByExternalId(String externalId) {
return (select(users)
..where((u) => u.externalUserId.equals(externalId))
).getSingleOrNull();
}
/// Создать пользователя
Future<int> createUser(UsersCompanion user) {
return into(users).insert(user);
}
/// Создать пользователя с UserData
Future<int> createUserWithData({
required UsersCompanion user,
required UserDatasCompanion userData,
}) async {
return await transaction(() async {
final userId = await into(users).insert(user);
await into(userDatas).insert(userData.copyWith(userId: Value(userId)));
return userId;
});
}
/// Обновить пользователя
Future<bool> updateUser(User user) {
return update(users).replace(user);
}
/// Обновить пользователя частично
Future<void> 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<void> softDeleteUser(int userId) {
return (update(users)..where((u) => u.id.equals(userId)))
.write(UsersCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
));
}
/// Получить всех пользователей (для админки)
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();
}
/// Подсчитать пользователей
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();
}
/// Получить пользователей с активной подпиской
Stream<List<User>> watchUsersWithActiveSubscription() {
final now = 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<UserData?> getUserData(int userId) {
return (select(userDatas)..where((ud) => ud.userId.equals(userId)))
.getSingleOrNull();
}
/// Создать UserData
Future<int> createUserData(UserDatasCompanion userData) {
return into(userDatas).insert(userData);
}
/// Обновить UserData
Future<bool> updateUserData(UserData userData) {
return update(userDatas).replace(userData);
}
/// Обновить UserData частично
Future<void> 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(DateTime.now())));
}
/// Обновить время последнего визита
Future<void> updateLastOnline(int userId) async {
await (update(userDatas)..where((ud) => ud.userId.equals(userId)))
.write(UserDatasCompanion(
lastTimeOnline: Value(DateTime.now()),
updatedAt: Value(DateTime.now()),
));
}
// ==================== Tokens ====================
/// Получить токен по значению
Future<Token?> getTokenByValue(String tokenValue) {
return (select(tokens)..where((t) => t.token.equals(tokenValue)))
.getSingleOrNull();
}
/// Получить токен по externalUserId
Future<Token?> getTokenByExternalUserId(String externalUserId) {
return (select(tokens)
..where((t) => t.externalUserId.equals(externalUserId))
..orderBy([(t) => OrderingTerm.desc(t.created)])
..limit(1)
).getSingleOrNull();
}
/// Получить токен пользователя
Future<Token?> getTokenByUserId(int userId) {
return (select(tokens)
..where((t) => t.userId.equals(userId))
..where((t) => t.expires.isBiggerThanValue(DateTime.now()))
..orderBy([(t) => OrderingTerm.desc(t.created)])
).getSingleOrNull();
}
/// Создать токен
Future<int> createToken(TokensCompanion token) {
return into(tokens).insert(token);
}
/// Удалить токен
Future<int> deleteToken(int tokenId) {
return (delete(tokens)..where((t) => t.id.equals(tokenId))).go();
}
/// Удалить токен по значению
Future<int> deleteTokenByValue(String tokenValue) {
return (delete(tokens)..where((t) => t.token.equals(tokenValue))).go();
}
/// Удалить истекшие токены
Future<int> deleteExpiredTokens() {
return (delete(tokens)
..where((t) => t.expires.isSmallerThanValue(DateTime.now()))
).go();
}
// ==================== RefreshTokens ====================
/// Получить refresh token по JTI
Future<RefreshToken?> getRefreshTokenByJti(String jti) {
return (select(refreshTokens)..where((rt) => rt.jti.equals(jti)))
.getSingleOrNull();
}
/// Получить активные refresh токены пользователя
Future<List<RefreshToken>> getActiveRefreshTokens(int userId) {
return (select(refreshTokens)
..where((rt) => rt.userId.equals(userId))
..where((rt) => rt.isBlacklisted.equals(false))
..where((rt) => rt.expiresAt.isBiggerThanValue(DateTime.now()))
..orderBy([(rt) => OrderingTerm.desc(rt.createdAt)])
).get();
}
/// Создать refresh token
Future<int> createRefreshToken(RefreshTokensCompanion token) {
return into(refreshTokens).insert(token);
}
/// Отозвать refresh token (blacklist)
Future<void> revokeRefreshToken(int tokenId) {
return (update(refreshTokens)..where((rt) => rt.id.equals(tokenId)))
.write(const RefreshTokensCompanion(
isBlacklisted: Value(true),
));
}
/// Отозвать refresh token по JTI
Future<void> revokeRefreshTokenByJti(String jti) {
return (update(refreshTokens)..where((rt) => rt.jti.equals(jti)))
.write(const RefreshTokensCompanion(
isBlacklisted: Value(true),
));
}
/// Удалить истекшие refresh токены
Future<int> deleteExpiredRefreshTokens() {
return (delete(refreshTokens)
..where((rt) => rt.expiresAt.isSmallerThanValue(DateTime.now()))
).go();
}
// ==================== TelegramAuthCodes ====================
/// Получить код авторизации
Future<TelegramAuthCode?> getAuthCode(String code) {
return (select(db.telegramAuthCodes)..where((ac) => ac.code.equals(code)))
.getSingleOrNull();
}
/// Создать код авторизации
Future<int> createAuthCode(TelegramAuthCodesCompanion code) {
return into(db.telegramAuthCodes).insert(code);
}
/// Отметить код как использованный
Future<void> markAuthCodeAsUsed(String code) {
return (update(db.telegramAuthCodes)..where((ac) => ac.code.equals(code)))
.write(TelegramAuthCodesCompanion(
isUsed: const Value(true),
usedAt: Value(DateTime.now()),
));
}
/// Удалить истекшие коды
Future<int> deleteExpiredAuthCodes() {
return (delete(db.telegramAuthCodes)
..where((ac) => ac.expiresAt.isSmallerThanValue(DateTime.now()))
).go();
}
// ==================== User Packs ====================
/// Получить паки пользователя
Future<List<CardPack>> getUserPacks(int 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<bool> hasPackAccess(int userId, int 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<void> grantPackAccess({
required int userId,
required int packId,
String grantType = 'purchase',
}) async {
await into(userPacks).insert(
UserPacksCompanion.insert(
userId: userId,
packId: packId,
grantType: Value(grantType),
),
mode: InsertMode.insertOrIgnore, // игнорировать если уже есть
);
}
/// Отозвать доступ к паку
Future<void> revokePackAccess(int userId, int 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});
}