Files
studio-admin-be/src/users/users.service.ts
T
chyoo ca35c30f87
ci/woodpecker/push/woodpecker Pipeline was successful
add avatar user list
2026-07-09 11:20:23 +09:00

245 lines
6.1 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { ListUserSessionsQuery } from './dto';
type UserSessionRow = {
id: string;
sessionId: string | null;
userId: string;
user: string;
email: string;
avatarUrl: string | null;
role: string;
session: string;
createdAt: Date | null;
expiresAt: Date | null;
lastSeen: Date | null;
state: string;
primaryAffiliation: {
id: string;
organizationId: string;
organizationName: string;
organizationType: string;
department: string | null;
position: string | null;
} | null;
};
@Injectable()
export class UsersService {
constructor(
private readonly authPrisma: AuthPrismaService,
private readonly configService: ConfigService,
) {}
async listSessions(query: ListUserSessionsQuery) {
const keyword = query.keyword?.trim();
const now = new Date();
const users = await this.authPrisma.user.findMany({
where: keyword
? {
OR: [
{ email: { contains: keyword, mode: 'insensitive' } },
{ name: { contains: keyword, mode: 'insensitive' } },
],
}
: undefined,
orderBy: [{ lastLoginAt: 'desc' }, { createdAt: 'desc' }],
include: {
refreshTokens: {
where: {
revokedAt: null,
expiresAt: { gt: now },
},
orderBy: { createdAt: 'desc' },
},
affiliations: {
where: {
OR: [{ endsAt: null }, { endsAt: { gt: now } }],
},
orderBy: [{ isPrimary: 'desc' }, { createdAt: 'desc' }],
take: 1,
include: {
organization: {
select: {
id: true,
name: true,
type: true,
},
},
},
},
},
});
const rows: UserSessionRow[] = users.flatMap((user): UserSessionRow[] => {
const avatarUrl = getUserAvatarUrl(
user.id,
user.profileImageMime,
this.configService.get<string>('AUTH_PUBLIC_URL')
?? this.configService.get<string>('ATNMY_AUTH_URL')
?? this.configService.get<string>('AUTH_SERVER_URL'),
);
const primaryAffiliation = user.affiliations[0]
? {
id: user.affiliations[0].id,
organizationId: user.affiliations[0].organizationId,
organizationName: user.affiliations[0].organization.name,
organizationType: user.affiliations[0].organization.type,
department: user.affiliations[0].department,
position: user.affiliations[0].position,
}
: null;
if (user.refreshTokens.length === 0) {
return [
{
id: `user-${user.id}`,
sessionId: null,
userId: user.id,
user: user.name || user.email,
email: user.email,
avatarUrl,
role: user.role,
session: '세션 없음',
createdAt: null,
expiresAt: null,
lastSeen: user.lastLoginAt,
state: user.isActive ? '세션 없음' : '잠김',
primaryAffiliation,
},
];
}
return user.refreshTokens.map((token) => ({
id: token.id,
sessionId: token.id,
userId: user.id,
user: user.name || user.email,
email: user.email,
avatarUrl,
role: user.role,
session: token.tokenHash.slice(0, 10),
createdAt: token.createdAt,
expiresAt: token.expiresAt,
lastSeen: user.lastLoginAt ?? token.createdAt,
state: getSessionState(user.isActive, token.revokedAt, token.expiresAt, now),
primaryAffiliation,
}));
});
const filteredRows = query.state
? rows.filter((row) => row.state === query.state)
: rows;
return {
sessions: filteredRows.map((row, index) => ({
...row,
no: filteredRows.length - index,
})),
};
}
async lockUser(userId: string) {
const user = await this.authPrisma.user.findUnique({
where: { id: userId },
select: { id: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다.');
}
await this.authPrisma.$transaction([
this.authPrisma.user.update({
where: { id: userId },
data: { isActive: false },
}),
this.authPrisma.refreshToken.updateMany({
where: { userId, revokedAt: null },
data: { revokedAt: new Date() },
}),
]);
return { ok: true };
}
async unlockUser(userId: string) {
const user = await this.authPrisma.user.findUnique({
where: { id: userId },
select: { id: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다.');
}
await this.authPrisma.user.update({
where: { id: userId },
data: { isActive: true },
});
return { ok: true };
}
async terminateSession(sessionId: string) {
const session = await this.authPrisma.refreshToken.findUnique({
where: { id: sessionId },
select: { id: true, userId: true },
});
if (!session) {
throw new NotFoundException('세션을 찾을 수 없습니다.');
}
await this.authPrisma.refreshToken.updateMany({
where: {
userId: session.userId,
revokedAt: null,
expiresAt: { gt: new Date() },
},
data: { revokedAt: new Date() },
});
return { ok: true };
}
}
function getUserAvatarUrl(
userId: string,
mimeType?: string | null,
authBaseUrl?: string | null,
) {
if (!mimeType || !authBaseUrl) {
return null;
}
const normalizedBaseUrl = authBaseUrl.replace(/\/+$/, '');
return `${normalizedBaseUrl}/auth/users/${encodeURIComponent(userId)}/avatar`;
}
function getSessionState(
isUserActive: boolean,
revokedAt: Date | null,
expiresAt: Date,
now: Date,
) {
if (!isUserActive) {
return '잠김';
}
if (revokedAt) {
return '종료';
}
if (expiresAt < now) {
return '만료';
}
return '유효토큰';
}