mnemo_cards/mnemo_cards_backend/lib/database/daos/user_dao.dart
Dmitry 1ab5071345
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (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
f
2025-12-14 02:35:14 +03:00

352 lines
12 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 '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<AppDatabase> with _$UserDaoMixin {
UserDao(super.db);
// ==================== Users ====================
/// Получить пользователя по ID
Future<User?> getUserById(String id) {
return (select(users)..where((u) => u.id.equals(id))).getSingleOrNull();
}
/// Получить пользователя с UserData
Future<UserWithData?> 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<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<String> createUser(UsersCompanion user) async {
final inserted = await into(users).insertReturning(user);
return inserted.id;
}
/// Создать пользователя с UserData
Future<String> 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<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(String userId) {
return (update(users)..where((u) => u.id.equals(userId)))
.write(UsersCompanion(
isDeleted: const Value(true),
updatedAt: Value(PgDateTime(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 = 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<UserData?> getUserData(String userId) {
return (select(userDatas)..where((ud) => ud.userId.equals(userId)))
.getSingleOrNull();
}
/// Создать UserData
Future<String> createUserData(UserDatasCompanion userData) async {
final inserted = await into(userDatas).insertReturning(userData);
return inserted.id;
}
/// Обновить 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(PgDateTime(DateTime.now()))));
}
/// Обновить время последнего визита
Future<void> 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<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(String userId) {
return (select(tokens)
..where((t) => t.userId.equals(userId))
..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
..orderBy([(t) => OrderingTerm.desc(t.created)])
).getSingleOrNull();
}
/// Создать токен
Future<String> createToken(TokensCompanion token) async {
final inserted = await into(tokens).insertReturning(token);
return inserted.id;
}
/// Удалить токен
Future<int> deleteToken(String 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(PgDateTime(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(String userId) {
return (select(refreshTokens)
..where((rt) => rt.userId.equals(userId))
..where((rt) => rt.isBlacklisted.equals(false))
..where((rt) => rt.expiresAt.isBiggerThanValue(PgDateTime(DateTime.now())))
..orderBy([(rt) => OrderingTerm.desc(rt.createdAt)])
).get();
}
/// Создать refresh token
Future<String> createRefreshToken(RefreshTokensCompanion token) async {
final inserted = await into(refreshTokens).insertReturning(token);
return inserted.id;
}
/// Отозвать refresh token (blacklist)
Future<void> revokeRefreshToken(String 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(PgDateTime(DateTime.now())))
).go();
}
// ==================== TelegramAuthCodes ====================
/// Получить код авторизации
Future<TelegramAuthCode?> getAuthCode(String code) {
return (select(db.telegramAuthCodes)..where((ac) => ac.code.equals(code)))
.getSingleOrNull();
}
/// Создать код авторизации
Future<String> createAuthCode(TelegramAuthCodesCompanion code) async {
final inserted = await into(db.telegramAuthCodes).insertReturning(code);
return inserted.id;
}
/// Отметить код как использованный
Future<void> 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<int> deleteExpiredAuthCodes() {
return (delete(db.telegramAuthCodes)
..where((ac) => ac.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now())))
).go();
}
// ==================== User Packs ====================
/// Получить паки пользователя
Future<List<CardPack>> 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<bool> 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<void> 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<void> 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});
}