init studio admin backend
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class ListUserSessionsQuery {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
state?: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Controller, Delete, Get, Param, Patch, Query } from '@nestjs/common';
|
||||
|
||||
import { ListUserSessionsQuery } from './dto';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get('sessions')
|
||||
listSessions(@Query() query: ListUserSessionsQuery) {
|
||||
return this.usersService.listSessions(query);
|
||||
}
|
||||
|
||||
@Patch(':userId/lock')
|
||||
lockUser(@Param('userId') userId: string) {
|
||||
return this.usersService.lockUser(userId);
|
||||
}
|
||||
|
||||
@Patch(':userId/unlock')
|
||||
unlockUser(@Param('userId') userId: string) {
|
||||
return this.usersService.unlockUser(userId);
|
||||
}
|
||||
|
||||
@Delete('sessions/:sessionId')
|
||||
terminateSession(@Param('sessionId') sessionId: string) {
|
||||
return this.usersService.terminateSession(sessionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthPrismaModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
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;
|
||||
role: string;
|
||||
session: string;
|
||||
createdAt: Date | null;
|
||||
expiresAt: Date | null;
|
||||
lastSeen: Date | null;
|
||||
state: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly authPrisma: AuthPrismaService) {}
|
||||
|
||||
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: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const rows: UserSessionRow[] = users.flatMap((user): UserSessionRow[] => {
|
||||
if (user.refreshTokens.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: `user-${user.id}`,
|
||||
sessionId: null,
|
||||
userId: user.id,
|
||||
user: user.name || user.email,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
session: '세션 없음',
|
||||
createdAt: null,
|
||||
expiresAt: null,
|
||||
lastSeen: user.lastLoginAt,
|
||||
state: user.isActive ? '세션 없음' : '잠김',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return user.refreshTokens.map((token) => ({
|
||||
id: token.id,
|
||||
sessionId: token.id,
|
||||
userId: user.id,
|
||||
user: user.name || user.email,
|
||||
email: user.email,
|
||||
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),
|
||||
}));
|
||||
});
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new NotFoundException('세션을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
await this.authPrisma.refreshToken.update({
|
||||
where: { id: sessionId },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionState(
|
||||
isUserActive: boolean,
|
||||
revokedAt: Date | null,
|
||||
expiresAt: Date,
|
||||
now: Date,
|
||||
) {
|
||||
if (!isUserActive) {
|
||||
return '잠김';
|
||||
}
|
||||
|
||||
if (revokedAt) {
|
||||
return '종료';
|
||||
}
|
||||
|
||||
if (expiresAt < now) {
|
||||
return '만료';
|
||||
}
|
||||
|
||||
return '활성';
|
||||
}
|
||||
Reference in New Issue
Block a user