mnemo_cards/mnemo_cards_backend/lib/database/daos/user_dao.dart
Dmitry a36af9d4bd
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
back fix
2026-01-03 17:42:05 +03:00

421 lines
14 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))
..limit(1);
final results = await query.get();
if (results.isEmpty) return null;
final result = results.first;
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) & t.isDeleted.equals(false)))
.getSingleOrNull();
}
/// Получить токен по externalUserId (только активные)
Future<Token?> getTokenByExternalUserId(String externalUserId) async {
final query = select(tokens)
..where(
(t) =>
t.externalUserId.equals(externalUserId) &
t.isDeleted.equals(false),
)
..orderBy([(t) => OrderingTerm.desc(t.created)])
..limit(1);
final results = await query.get();
return results.isNotEmpty ? results.first : null;
}
/// Получить токен пользователя (только активные)
Future<Token?> 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<String> createToken(TokensCompanion token) async {
final inserted = await into(tokens).insertReturning(token);
return inserted.id;
}
/// Удалить токен (hard delete)
Future<void> deleteToken(String token) async {
await (delete(tokens)..where((t) => t.token.equals(token))).go();
}
/// Удалить токен (soft delete)
Future<int> 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<int> 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<int> 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<RefreshToken?> getRefreshTokenByJti(String jti) {
return (select(refreshTokens)
..where((rt) => rt.jti.equals(jti) & rt.isDeleted.equals(false)))
.getSingleOrNull();
}
/// Получить активные refresh токены пользователя (только активные)
Future<List<RefreshToken>> 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<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 токены (hard delete)
Future<void> deleteExpiredRefreshTokens() async {
final now = PgDateTime(DateTime.now());
await (delete(
refreshTokens,
)..where((rt) => rt.expiresAt.isSmallerThanValue(now))).go();
}
/// Удалить истекшие refresh токены (soft delete)
Future<int> 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<TelegramAuthCode?> getAuthCode(String code) {
return (select(db.telegramAuthCodes)
..where((ac) => ac.code.equals(code) & ac.isDeleted.equals(false)))
.getSingleOrNull();
}
/// Удалить код авторизации (soft delete)
Future<void> 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<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))
..limit(1);
final results = await query.get();
return results.isNotEmpty;
}
/// Дать пользователю доступ к паку
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});
}