add organization management and refine source repository view
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -12,6 +12,7 @@ import { HealthModule } from './health/health.module';
|
||||
import { LauncherModule } from './launcher/launcher.module';
|
||||
import { LicensesModule } from './licenses/licenses.module';
|
||||
import { NoticesModule } from './notices/notices.module';
|
||||
import { OrganizationsModule } from './organizations/organizations.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { RuntimeModule } from './runtime/runtime.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
@@ -37,6 +38,7 @@ import { WoodpeckerModule } from './woodpecker/woodpecker.module';
|
||||
LauncherModule,
|
||||
LicensesModule,
|
||||
NoticesModule,
|
||||
OrganizationsModule,
|
||||
PrismaModule,
|
||||
RuntimeModule,
|
||||
UsersModule,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IsIn, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreateOrganizationDto {
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
name!: string;
|
||||
|
||||
@IsIn(['COMPANY', 'SCHOOL', 'GOVERNMENT', 'LAB', 'OTHER'])
|
||||
type!: 'COMPANY' | 'SCHOOL' | 'GOVERNMENT' | 'LAB' | 'OTHER';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
domain?: string | null;
|
||||
}
|
||||
|
||||
export class UpdateOrganizationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['COMPANY', 'SCHOOL', 'GOVERNMENT', 'LAB', 'OTHER'])
|
||||
type?: 'COMPANY' | 'SCHOOL' | 'GOVERNMENT' | 'LAB' | 'OTHER';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
domain?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
|
||||
import { CreateOrganizationDto, UpdateOrganizationDto } from './dto';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
|
||||
@Controller('organizations')
|
||||
export class OrganizationsController {
|
||||
constructor(private readonly organizationsService: OrganizationsService) {}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.organizationsService.list();
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateOrganizationDto) {
|
||||
return this.organizationsService.create(body);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() body: UpdateOrganizationDto) {
|
||||
return this.organizationsService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.organizationsService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
|
||||
import { OrganizationsController } from './organizations.controller';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthPrismaModule],
|
||||
controllers: [OrganizationsController],
|
||||
providers: [OrganizationsService],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/auth-client';
|
||||
|
||||
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
|
||||
import { CreateOrganizationDto, UpdateOrganizationDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsService {
|
||||
constructor(private readonly authPrisma: AuthPrismaService) {}
|
||||
|
||||
async list() {
|
||||
const organizations = await this.authPrisma.organization.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: [{ type: 'asc' }, { name: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
domain: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { organizations };
|
||||
}
|
||||
|
||||
async create(body: CreateOrganizationDto) {
|
||||
try {
|
||||
const organization = await this.authPrisma.organization.create({
|
||||
data: {
|
||||
name: body.name.trim(),
|
||||
type: body.type,
|
||||
domain: body.domain?.trim() || null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
domain: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { organization };
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2002'
|
||||
) {
|
||||
throw new ConflictException('이미 등록된 조직입니다.');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, body: UpdateOrganizationDto) {
|
||||
try {
|
||||
const organization = await this.authPrisma.organization.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.name !== undefined ? { name: body.name.trim() } : {}),
|
||||
...(body.type !== undefined ? { type: body.type } : {}),
|
||||
...(body.domain !== undefined ? { domain: body.domain?.trim() || null } : {}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
domain: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { organization };
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2025'
|
||||
) {
|
||||
throw new NotFoundException('조직을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2002'
|
||||
) {
|
||||
throw new ConflictException('이미 등록된 조직입니다.');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
try {
|
||||
await this.authPrisma.organization.update({
|
||||
where: { id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2025'
|
||||
) {
|
||||
throw new NotFoundException('조직을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,14 @@ type UserSessionRow = {
|
||||
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()
|
||||
@@ -43,10 +51,37 @@ export class UsersService {
|
||||
},
|
||||
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 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 [
|
||||
{
|
||||
@@ -61,6 +96,7 @@ export class UsersService {
|
||||
expiresAt: null,
|
||||
lastSeen: user.lastLoginAt,
|
||||
state: user.isActive ? '세션 없음' : '잠김',
|
||||
primaryAffiliation,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -77,6 +113,7 @@ export class UsersService {
|
||||
expiresAt: token.expiresAt,
|
||||
lastSeen: user.lastLoginAt ?? token.createdAt,
|
||||
state: getSessionState(user.isActive, token.revokedAt, token.expiresAt, now),
|
||||
primaryAffiliation,
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user