resid pt 3: friendship checks & auth session reads
This commit is contained in:
@@ -18,6 +18,8 @@ CACHE_KEY_PREFIX=friendolls
|
||||
CACHE_DEFAULT_TTL_SECONDS=60
|
||||
CACHE_MAX_TTL_SECONDS=86400
|
||||
CACHE_METRICS_LOG_INTERVAL_MS=60000
|
||||
# Max number of cache keys tracked per invalidation tag
|
||||
CACHE_TAG_MAX_ENTRIES=5000
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=replace-with-strong-random-secret
|
||||
|
||||
@@ -83,6 +83,13 @@ function validateEnvironment(
|
||||
throw new Error('REDIS_CONNECT_TIMEOUT_MS must be a positive number');
|
||||
}
|
||||
|
||||
validateOptionalPositiveNumber(config, 'THROTTLE_TTL');
|
||||
validateOptionalPositiveNumber(config, 'THROTTLE_LIMIT');
|
||||
validateOptionalPositiveNumber(config, 'CACHE_DEFAULT_TTL_SECONDS');
|
||||
validateOptionalPositiveNumber(config, 'CACHE_MAX_TTL_SECONDS');
|
||||
validateOptionalPositiveNumber(config, 'CACHE_METRICS_LOG_INTERVAL_MS');
|
||||
validateOptionalPositiveNumber(config, 'CACHE_TAG_MAX_ENTRIES');
|
||||
|
||||
validateOptionalProvider(config, 'GOOGLE');
|
||||
validateOptionalProvider(config, 'DISCORD');
|
||||
|
||||
@@ -109,6 +116,20 @@ function validateOptionalProvider(
|
||||
}
|
||||
}
|
||||
|
||||
function validateOptionalPositiveNumber(
|
||||
config: Record<string, unknown>,
|
||||
key: string,
|
||||
): void {
|
||||
const value = config[key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(Number(value)) || Number(value) <= 0) {
|
||||
throw new Error(`${key} must be a positive number`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Root Application Module
|
||||
*
|
||||
|
||||
@@ -37,6 +37,14 @@ import {
|
||||
} from './auth.utils';
|
||||
import type { SsoProvider } from './dto/sso-provider';
|
||||
import { UserEvents } from '../users/events/user.events';
|
||||
import { CacheService } from '../common/cache/cache.service';
|
||||
import {
|
||||
authSessionUserTag,
|
||||
authSessionCacheKey,
|
||||
CACHE_NAMESPACE,
|
||||
CACHE_TTL_SECONDS,
|
||||
} from '../common/cache/cache-keys';
|
||||
import { CacheTagsService } from '../common/cache/cache-tags.service';
|
||||
|
||||
interface SsoStateClaims {
|
||||
provider: SsoProvider;
|
||||
@@ -45,6 +53,28 @@ interface SsoStateClaims {
|
||||
typ: 'sso_state';
|
||||
}
|
||||
|
||||
interface AuthSessionWithUser {
|
||||
id: string;
|
||||
refresh_token_hash: string;
|
||||
expires_at: Date;
|
||||
revoked_at: Date | null;
|
||||
provider: 'GOOGLE' | 'DISCORD' | null;
|
||||
user_id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
interface CachedAuthSessionWithUser {
|
||||
id: string;
|
||||
refresh_token_hash: string;
|
||||
expires_at: string;
|
||||
revoked_at: string | null;
|
||||
provider: 'GOOGLE' | 'DISCORD' | null;
|
||||
user_id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
@@ -59,6 +89,8 @@ export class AuthService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly cacheTagsService: CacheTagsService,
|
||||
) {
|
||||
this.jwtSecret = this.configService.get<string>('JWT_SECRET') || '';
|
||||
this.jwtIssuer =
|
||||
@@ -162,7 +194,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
if (session.refresh_token_hash !== refreshTokenHash) {
|
||||
await this.revokeSessionOnReplay(session.id);
|
||||
await this.revokeSessionOnReplay(session.id, session.user_id);
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
@@ -174,7 +206,7 @@ export class AuthService {
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
await this.revokeSessionOnReplay(session.id);
|
||||
await this.revokeSessionOnReplay(session.id, session.user_id);
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
@@ -560,28 +592,34 @@ export class AuthService {
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
private async getSessionWithUser(sessionId: string): Promise<{
|
||||
id: string;
|
||||
refresh_token_hash: string;
|
||||
expires_at: Date;
|
||||
revoked_at: Date | null;
|
||||
provider: 'GOOGLE' | 'DISCORD' | null;
|
||||
user_id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
} | null> {
|
||||
const rows = await this.prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
refresh_token_hash: string;
|
||||
expires_at: Date;
|
||||
revoked_at: Date | null;
|
||||
provider: 'GOOGLE' | 'DISCORD' | null;
|
||||
user_id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
}>
|
||||
>`
|
||||
private async getSessionWithUser(
|
||||
sessionId: string,
|
||||
): Promise<AuthSessionWithUser | null> {
|
||||
const sessionCacheKey = this.getAuthSessionCacheKey(sessionId);
|
||||
const cachedSessionRaw = await this.cacheService.get(sessionCacheKey);
|
||||
|
||||
if (cachedSessionRaw) {
|
||||
try {
|
||||
const cachedSession = JSON.parse(
|
||||
cachedSessionRaw,
|
||||
) as CachedAuthSessionWithUser;
|
||||
return {
|
||||
...cachedSession,
|
||||
expires_at: new Date(cachedSession.expires_at),
|
||||
revoked_at: cachedSession.revoked_at
|
||||
? new Date(cachedSession.revoked_at)
|
||||
: null,
|
||||
};
|
||||
} catch (error) {
|
||||
this.cacheService.recordError(
|
||||
'auth session parse',
|
||||
sessionCacheKey,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await this.prisma.$queryRaw<Array<AuthSessionWithUser>>`
|
||||
SELECT s.id, s.refresh_token_hash, s.expires_at, s.revoked_at, s.provider, s.user_id, u.email, u.roles
|
||||
FROM auth_sessions AS s
|
||||
INNER JOIN users AS u ON u.id = s.user_id
|
||||
@@ -589,7 +627,29 @@ export class AuthService {
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
return rows[0] ?? null;
|
||||
const session = rows[0] ?? null;
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachePayload: CachedAuthSessionWithUser = {
|
||||
...session,
|
||||
expires_at: session.expires_at.toISOString(),
|
||||
revoked_at: session.revoked_at ? session.revoked_at.toISOString() : null,
|
||||
};
|
||||
|
||||
await this.cacheService.set(
|
||||
sessionCacheKey,
|
||||
JSON.stringify(cachePayload),
|
||||
CACHE_TTL_SECONDS.AUTH_SESSION,
|
||||
);
|
||||
await this.cacheTagsService.rememberKeyForTag(
|
||||
CACHE_NAMESPACE.AUTH_SESSION,
|
||||
authSessionUserTag(session.user_id),
|
||||
authSessionCacheKey(session.id),
|
||||
);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private async rotateRefreshSession(
|
||||
@@ -597,6 +657,8 @@ export class AuthService {
|
||||
refreshTokenHash: string,
|
||||
nextRefreshToken: string,
|
||||
): Promise<boolean> {
|
||||
await this.cacheService.del(this.getAuthSessionCacheKey(sessionId));
|
||||
|
||||
const rows = await this.prisma.$queryRaw<Array<{ id: string }>>`
|
||||
UPDATE auth_sessions
|
||||
SET refresh_token_hash = ${sha256(nextRefreshToken)},
|
||||
@@ -610,6 +672,10 @@ export class AuthService {
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
if (rows.length === 1) {
|
||||
await this.cacheService.del(this.getAuthSessionCacheKey(sessionId));
|
||||
}
|
||||
|
||||
return rows.length === 1;
|
||||
}
|
||||
|
||||
@@ -617,6 +683,8 @@ export class AuthService {
|
||||
sessionId: string,
|
||||
refreshTokenHash: string,
|
||||
): Promise<boolean> {
|
||||
await this.cacheService.del(this.getAuthSessionCacheKey(sessionId));
|
||||
|
||||
const rows = await this.prisma.$queryRaw<Array<{ id: string }>>`
|
||||
UPDATE auth_sessions
|
||||
SET revoked_at = NOW(),
|
||||
@@ -628,17 +696,41 @@ export class AuthService {
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
if (rows.length === 1) {
|
||||
await this.cacheService.del(this.getAuthSessionCacheKey(sessionId));
|
||||
}
|
||||
|
||||
return rows.length === 1;
|
||||
}
|
||||
|
||||
private async revokeSessionOnReplay(sessionId: string): Promise<void> {
|
||||
private async revokeSessionOnReplay(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
await this.cacheService.del(this.getAuthSessionCacheKey(sessionId));
|
||||
await this.revokeAllUserSessions(userId);
|
||||
}
|
||||
|
||||
private async revokeAllUserSessions(userId: string): Promise<void> {
|
||||
await this.prisma.$queryRaw<Array<{ id: string }>>`
|
||||
UPDATE auth_sessions
|
||||
SET revoked_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = ${sessionId}
|
||||
WHERE user_id = ${userId}
|
||||
AND revoked_at IS NULL
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
await this.cacheTagsService.invalidateTag(
|
||||
CACHE_NAMESPACE.AUTH_SESSION,
|
||||
authSessionUserTag(userId),
|
||||
);
|
||||
}
|
||||
|
||||
private getAuthSessionCacheKey(sessionId: string): string {
|
||||
return this.cacheService.getNamespacedKey(
|
||||
CACHE_NAMESPACE.AUTH_SESSION,
|
||||
authSessionCacheKey(sessionId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ const DEFAULT_REVOKED_RETENTION_DAYS = 7;
|
||||
const CLEANUP_LOCK_KEY = 'lock:auth:cleanup';
|
||||
const CLEANUP_LOCK_TTL_MS = 55_000;
|
||||
|
||||
const RELEASE_LOCK_SCRIPT = `
|
||||
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`;
|
||||
|
||||
@Injectable()
|
||||
export class AuthCleanupService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(AuthCleanupService.name);
|
||||
@@ -141,10 +149,12 @@ export class AuthCleanupService implements OnModuleInit, OnModuleDestroy {
|
||||
} finally {
|
||||
if (lockAcquired && this.redisClient) {
|
||||
try {
|
||||
const currentLockValue = await this.redisClient.get(CLEANUP_LOCK_KEY);
|
||||
if (currentLockValue === lockToken) {
|
||||
await this.redisClient.del(CLEANUP_LOCK_KEY);
|
||||
}
|
||||
await this.redisClient.eval(
|
||||
RELEASE_LOCK_SCRIPT,
|
||||
1,
|
||||
CLEANUP_LOCK_KEY,
|
||||
lockToken,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Failed to release auth cleanup lock',
|
||||
|
||||
23
src/common/cache/cache-keys.ts
vendored
23
src/common/cache/cache-keys.ts
vendored
@@ -4,6 +4,8 @@ export const CACHE_NAMESPACE = {
|
||||
FRIENDS_LIST: 'friends-list',
|
||||
DOLLS_LIST: 'dolls-list',
|
||||
USERS_SEARCH: 'users-search',
|
||||
FRIENDSHIP_CHECK: 'friendship-check',
|
||||
AUTH_SESSION: 'auth-session',
|
||||
} as const;
|
||||
|
||||
function normalizeKeyPart(value: string | undefined): string {
|
||||
@@ -18,6 +20,8 @@ export const CACHE_TTL_SECONDS = {
|
||||
FRIENDS_LIST: 30,
|
||||
DOLLS_LIST: 30,
|
||||
USERS_SEARCH: 20,
|
||||
FRIENDSHIP_CHECK: 120,
|
||||
AUTH_SESSION: 30,
|
||||
} as const;
|
||||
|
||||
export function friendsListCacheKey(userId: string): string {
|
||||
@@ -56,6 +60,25 @@ export function usersSearchCacheKey(
|
||||
|
||||
export const USERS_SEARCH_GLOBAL_TAG = 'global';
|
||||
|
||||
export function friendshipCheckCacheKey(
|
||||
userId: string,
|
||||
friendId: string,
|
||||
): string {
|
||||
return `${normalizeKeyPart(userId)}:${normalizeKeyPart(friendId)}`;
|
||||
}
|
||||
|
||||
export function friendshipCheckUserTag(userId: string): string {
|
||||
return `user:${normalizeKeyPart(userId)}`;
|
||||
}
|
||||
|
||||
export function authSessionCacheKey(sessionId: string): string {
|
||||
return normalizeKeyPart(sessionId);
|
||||
}
|
||||
|
||||
export function authSessionUserTag(userId: string): string {
|
||||
return `user:${normalizeKeyPart(userId)}`;
|
||||
}
|
||||
|
||||
export function usersSearchUserTag(userId: string): string {
|
||||
return `user:${normalizeKeyPart(userId)}`;
|
||||
}
|
||||
|
||||
42
src/common/cache/cache-tags.service.ts
vendored
42
src/common/cache/cache-tags.service.ts
vendored
@@ -1,11 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CacheService } from './cache.service';
|
||||
import { parsePositiveInteger } from '../config/env.utils';
|
||||
|
||||
const CACHE_TAG_SET_TTL_SECONDS = 86_400;
|
||||
const DEFAULT_CACHE_TAG_MAX_ENTRIES = 5_000;
|
||||
|
||||
@Injectable()
|
||||
export class CacheTagsService {
|
||||
constructor(private readonly cacheService: CacheService) {}
|
||||
private readonly cacheTagMaxEntries: number;
|
||||
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.cacheTagMaxEntries = parsePositiveInteger(
|
||||
this.configService.get<string>('CACHE_TAG_MAX_ENTRIES'),
|
||||
DEFAULT_CACHE_TAG_MAX_ENTRIES,
|
||||
);
|
||||
}
|
||||
|
||||
async rememberKeyForTag(
|
||||
namespace: string,
|
||||
@@ -28,6 +41,11 @@ export class CacheTagsService {
|
||||
redisClient.sadd(tagSetKey, keyWithNamespace),
|
||||
redisClient.expire(tagSetKey, CACHE_TAG_SET_TTL_SECONDS),
|
||||
]);
|
||||
|
||||
const size = await redisClient.scard(tagSetKey);
|
||||
if (size > this.cacheTagMaxEntries) {
|
||||
await this.trimTagSet(tagSetKey, size - this.cacheTagMaxEntries);
|
||||
}
|
||||
} catch (error) {
|
||||
this.cacheService.recordError('tag remember', tagSetKey, error);
|
||||
}
|
||||
@@ -63,4 +81,26 @@ export class CacheTagsService {
|
||||
`${namespace}:${tag}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async trimTagSet(
|
||||
tagSetKey: string,
|
||||
countToDrop: number,
|
||||
): Promise<void> {
|
||||
const redisClient = this.cacheService.getRedisClient();
|
||||
if (!redisClient || countToDrop <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sample = await redisClient.srandmember(tagSetKey, countToDrop);
|
||||
const members = Array.isArray(sample) ? sample : [sample].filter(Boolean);
|
||||
if (members.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await redisClient.srem(tagSetKey, ...members);
|
||||
} catch (error) {
|
||||
this.cacheService.recordError('tag trim', tagSetKey, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/common/cache/index.ts
vendored
1
src/common/cache/index.ts
vendored
@@ -1,3 +1,4 @@
|
||||
export { CacheModule } from './cache.module';
|
||||
export { CacheService } from './cache.service';
|
||||
export { CacheTagsService } from './cache-tags.service';
|
||||
export { RedisThrottlerStorage } from './redis-throttler.storage';
|
||||
|
||||
@@ -6,9 +6,15 @@ import { DollsNotificationService } from './dolls-notification.service';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { WsModule } from '../ws/ws.module';
|
||||
import { FriendsModule } from '../friends/friends.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, AuthModule, forwardRef(() => WsModule)],
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
AuthModule,
|
||||
FriendsModule,
|
||||
forwardRef(() => WsModule),
|
||||
],
|
||||
controllers: [DollsController],
|
||||
providers: [
|
||||
DollsService,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
dollsListOwnerTag,
|
||||
dollsListViewerTag,
|
||||
} from '../common/cache/cache-keys';
|
||||
import { FriendsService } from '../friends/friends.service';
|
||||
|
||||
@Injectable()
|
||||
export class DollsService {
|
||||
@@ -34,16 +35,9 @@ export class DollsService {
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly cacheTagsService: CacheTagsService,
|
||||
private readonly friendsService: FriendsService,
|
||||
) {}
|
||||
|
||||
async getFriendIds(userId: string): Promise<string[]> {
|
||||
const friendships = await this.prisma.friendship.findMany({
|
||||
where: { userId },
|
||||
select: { friendId: true },
|
||||
});
|
||||
return friendships.map((f) => f.friendId);
|
||||
}
|
||||
|
||||
async create(
|
||||
requestingUserId: string,
|
||||
createDollDto: CreateDollDto,
|
||||
@@ -144,8 +138,11 @@ export class DollsService {
|
||||
}
|
||||
|
||||
// If requesting someone else's dolls, check friendship
|
||||
const friendIds = await this.getFriendIds(requestingUserId);
|
||||
if (!friendIds.includes(ownerId)) {
|
||||
const isFriend = await this.friendsService.areFriends(
|
||||
requestingUserId,
|
||||
ownerId,
|
||||
);
|
||||
if (!isFriend) {
|
||||
throw new ForbiddenException('You are not friends with this user');
|
||||
}
|
||||
|
||||
@@ -161,13 +158,9 @@ export class DollsService {
|
||||
}
|
||||
|
||||
async findOne(id: string, requestingUserId: string): Promise<Doll> {
|
||||
const friendIds = await this.getFriendIds(requestingUserId);
|
||||
const accessibleUserIds = [requestingUserId, ...friendIds];
|
||||
|
||||
const doll = await this.prisma.doll.findFirst({
|
||||
where: {
|
||||
id,
|
||||
userId: { in: accessibleUserIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
@@ -178,6 +171,18 @@ export class DollsService {
|
||||
);
|
||||
}
|
||||
|
||||
if (doll.userId !== requestingUserId) {
|
||||
const isFriend = await this.friendsService.areFriends(
|
||||
requestingUserId,
|
||||
doll.userId,
|
||||
);
|
||||
if (!isFriend) {
|
||||
throw new NotFoundException(
|
||||
`Doll with ID ${id} not found or access denied`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return doll;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CacheTagsService } from '../common/cache/cache-tags.service';
|
||||
import {
|
||||
CACHE_NAMESPACE,
|
||||
dollsListViewerTag,
|
||||
friendshipCheckUserTag,
|
||||
friendsListDependsOnUserTag,
|
||||
friendsListOwnerTag,
|
||||
} from '../common/cache/cache-keys';
|
||||
@@ -60,6 +61,14 @@ export class FriendsCacheInvalidationService {
|
||||
CACHE_NAMESPACE.DOLLS_LIST,
|
||||
dollsListViewerTag(secondUserId),
|
||||
),
|
||||
this.cacheTagsService.invalidateTag(
|
||||
CACHE_NAMESPACE.FRIENDSHIP_CHECK,
|
||||
friendshipCheckUserTag(firstUserId),
|
||||
),
|
||||
this.cacheTagsService.invalidateTag(
|
||||
CACHE_NAMESPACE.FRIENDSHIP_CHECK,
|
||||
friendshipCheckUserTag(secondUserId),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import { CacheTagsService } from '../common/cache/cache-tags.service';
|
||||
import {
|
||||
CACHE_NAMESPACE,
|
||||
CACHE_TTL_SECONDS,
|
||||
friendshipCheckCacheKey,
|
||||
friendshipCheckUserTag,
|
||||
friendsListCacheKey,
|
||||
friendsListDependsOnUserTag,
|
||||
friendsListOwnerTag,
|
||||
@@ -378,6 +380,21 @@ export class FriendsService {
|
||||
}
|
||||
|
||||
async areFriends(userId: string, friendId: string): Promise<boolean> {
|
||||
const cacheKey = friendshipCheckCacheKey(userId, friendId);
|
||||
const namespacedKey = this.cacheService.getNamespacedKey(
|
||||
CACHE_NAMESPACE.FRIENDSHIP_CHECK,
|
||||
cacheKey,
|
||||
);
|
||||
const cached = await this.cacheService.get(namespacedKey);
|
||||
|
||||
if (cached === '1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cached === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const friendship = await this.prisma.friendship.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
@@ -385,6 +402,27 @@ export class FriendsService {
|
||||
},
|
||||
});
|
||||
|
||||
return !!friendship;
|
||||
const areFriends = !!friendship;
|
||||
|
||||
await this.cacheService.set(
|
||||
namespacedKey,
|
||||
areFriends ? '1' : '0',
|
||||
CACHE_TTL_SECONDS.FRIENDSHIP_CHECK,
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.cacheTagsService.rememberKeyForTag(
|
||||
CACHE_NAMESPACE.FRIENDSHIP_CHECK,
|
||||
friendshipCheckUserTag(userId),
|
||||
cacheKey,
|
||||
),
|
||||
this.cacheTagsService.rememberKeyForTag(
|
||||
CACHE_NAMESPACE.FRIENDSHIP_CHECK,
|
||||
friendshipCheckUserTag(friendId),
|
||||
cacheKey,
|
||||
),
|
||||
]);
|
||||
|
||||
return areFriends;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user