init studio admin backend

This commit is contained in:
2026-07-01 17:30:04 +09:00
commit 4437d9980d
144 changed files with 44964 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
import { IsOptional, IsString } from 'class-validator';
export class ListUserSessionsQuery {
@IsOptional()
@IsString()
keyword?: string;
@IsOptional()
@IsString()
state?: string;
}
+29
View File
@@ -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);
}
}
+12
View File
@@ -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 {}
+171
View File
@@ -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 '활성';
}