Some checks failed
ci/woodpecker/push/api Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
307 lines
8.2 KiB
TypeScript
307 lines
8.2 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
Injectable,
|
|
Logger,
|
|
NotFoundException,
|
|
} from "@nestjs/common";
|
|
import { Prisma, WorkspaceMemberRole } from "@prisma/client";
|
|
import { randomUUID } from "node:crypto";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import type { InviteUserDto } from "./dto/invite-user.dto";
|
|
import type { UpdateUserDto } from "./dto/update-user.dto";
|
|
import type { CreateWorkspaceDto } from "./dto/create-workspace.dto";
|
|
import type {
|
|
AdminUserResponse,
|
|
AdminWorkspaceResponse,
|
|
InvitationResponse,
|
|
PaginatedResponse,
|
|
} from "./types/admin.types";
|
|
|
|
@Injectable()
|
|
export class AdminService {
|
|
private readonly logger = new Logger(AdminService.name);
|
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async listUsers(page = 1, limit = 50): Promise<PaginatedResponse<AdminUserResponse>> {
|
|
const skip = (page - 1) * limit;
|
|
|
|
const [users, total] = await Promise.all([
|
|
this.prisma.user.findMany({
|
|
include: {
|
|
workspaceMemberships: {
|
|
include: {
|
|
workspace: { select: { id: true, name: true } },
|
|
},
|
|
},
|
|
},
|
|
orderBy: { createdAt: "desc" },
|
|
skip,
|
|
take: limit,
|
|
}),
|
|
this.prisma.user.count(),
|
|
]);
|
|
|
|
return {
|
|
data: users.map((user) => ({
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
emailVerified: user.emailVerified,
|
|
image: user.image,
|
|
createdAt: user.createdAt,
|
|
deactivatedAt: user.deactivatedAt,
|
|
isLocalAuth: user.isLocalAuth,
|
|
invitedAt: user.invitedAt,
|
|
invitedBy: user.invitedBy,
|
|
workspaceMemberships: user.workspaceMemberships.map((m) => ({
|
|
workspaceId: m.workspaceId,
|
|
workspaceName: m.workspace.name,
|
|
role: m.role,
|
|
joinedAt: m.joinedAt,
|
|
})),
|
|
})),
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
}
|
|
|
|
async inviteUser(dto: InviteUserDto, inviterId: string): Promise<InvitationResponse> {
|
|
const existing = await this.prisma.user.findUnique({
|
|
where: { email: dto.email },
|
|
});
|
|
|
|
if (existing) {
|
|
throw new ConflictException(`User with email ${dto.email} already exists`);
|
|
}
|
|
|
|
if (dto.workspaceId) {
|
|
const workspace = await this.prisma.workspace.findUnique({
|
|
where: { id: dto.workspaceId },
|
|
});
|
|
if (!workspace) {
|
|
throw new NotFoundException(`Workspace ${dto.workspaceId} not found`);
|
|
}
|
|
}
|
|
|
|
const invitationToken = randomUUID();
|
|
const now = new Date();
|
|
|
|
const user = await this.prisma.$transaction(async (tx) => {
|
|
const created = await tx.user.create({
|
|
data: {
|
|
email: dto.email,
|
|
name: dto.name ?? dto.email.split("@")[0] ?? dto.email,
|
|
emailVerified: false,
|
|
invitedBy: inviterId,
|
|
invitationToken,
|
|
invitedAt: now,
|
|
},
|
|
});
|
|
|
|
if (dto.workspaceId) {
|
|
await tx.workspaceMember.create({
|
|
data: {
|
|
workspaceId: dto.workspaceId,
|
|
userId: created.id,
|
|
role: dto.role ?? WorkspaceMemberRole.MEMBER,
|
|
},
|
|
});
|
|
}
|
|
|
|
return created;
|
|
});
|
|
|
|
this.logger.log(`User invited: ${user.email} by ${inviterId}`);
|
|
|
|
return {
|
|
userId: user.id,
|
|
invitationToken,
|
|
email: user.email,
|
|
invitedAt: now,
|
|
};
|
|
}
|
|
|
|
async updateUser(id: string, dto: UpdateUserDto): Promise<AdminUserResponse> {
|
|
const existing = await this.prisma.user.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new NotFoundException(`User ${id} not found`);
|
|
}
|
|
|
|
const data: Prisma.UserUpdateInput = {};
|
|
|
|
if (dto.name !== undefined) {
|
|
data.name = dto.name;
|
|
}
|
|
if (dto.emailVerified !== undefined) {
|
|
data.emailVerified = dto.emailVerified;
|
|
}
|
|
if (dto.preferences !== undefined) {
|
|
data.preferences = dto.preferences as Prisma.InputJsonValue;
|
|
}
|
|
if (dto.deactivatedAt !== undefined) {
|
|
data.deactivatedAt = dto.deactivatedAt ? new Date(dto.deactivatedAt) : null;
|
|
}
|
|
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data,
|
|
include: {
|
|
workspaceMemberships: {
|
|
include: {
|
|
workspace: { select: { id: true, name: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
this.logger.log(`User updated: ${id}`);
|
|
|
|
return {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
emailVerified: user.emailVerified,
|
|
image: user.image,
|
|
createdAt: user.createdAt,
|
|
deactivatedAt: user.deactivatedAt,
|
|
isLocalAuth: user.isLocalAuth,
|
|
invitedAt: user.invitedAt,
|
|
invitedBy: user.invitedBy,
|
|
workspaceMemberships: user.workspaceMemberships.map((m) => ({
|
|
workspaceId: m.workspaceId,
|
|
workspaceName: m.workspace.name,
|
|
role: m.role,
|
|
joinedAt: m.joinedAt,
|
|
})),
|
|
};
|
|
}
|
|
|
|
async deactivateUser(id: string): Promise<AdminUserResponse> {
|
|
const existing = await this.prisma.user.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new NotFoundException(`User ${id} not found`);
|
|
}
|
|
|
|
if (existing.deactivatedAt) {
|
|
throw new BadRequestException(`User ${id} is already deactivated`);
|
|
}
|
|
|
|
const user = await this.prisma.user.update({
|
|
where: { id },
|
|
data: { deactivatedAt: new Date() },
|
|
include: {
|
|
workspaceMemberships: {
|
|
include: {
|
|
workspace: { select: { id: true, name: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
this.logger.log(`User deactivated: ${id}`);
|
|
|
|
return {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
emailVerified: user.emailVerified,
|
|
image: user.image,
|
|
createdAt: user.createdAt,
|
|
deactivatedAt: user.deactivatedAt,
|
|
isLocalAuth: user.isLocalAuth,
|
|
invitedAt: user.invitedAt,
|
|
invitedBy: user.invitedBy,
|
|
workspaceMemberships: user.workspaceMemberships.map((m) => ({
|
|
workspaceId: m.workspaceId,
|
|
workspaceName: m.workspace.name,
|
|
role: m.role,
|
|
joinedAt: m.joinedAt,
|
|
})),
|
|
};
|
|
}
|
|
|
|
async createWorkspace(dto: CreateWorkspaceDto): Promise<AdminWorkspaceResponse> {
|
|
const owner = await this.prisma.user.findUnique({ where: { id: dto.ownerId } });
|
|
if (!owner) {
|
|
throw new NotFoundException(`User ${dto.ownerId} not found`);
|
|
}
|
|
|
|
const workspace = await this.prisma.$transaction(async (tx) => {
|
|
const created = await tx.workspace.create({
|
|
data: {
|
|
name: dto.name,
|
|
ownerId: dto.ownerId,
|
|
settings: dto.settings ? (dto.settings as Prisma.InputJsonValue) : {},
|
|
},
|
|
});
|
|
|
|
await tx.workspaceMember.create({
|
|
data: {
|
|
workspaceId: created.id,
|
|
userId: dto.ownerId,
|
|
role: WorkspaceMemberRole.OWNER,
|
|
},
|
|
});
|
|
|
|
return created;
|
|
});
|
|
|
|
this.logger.log(`Workspace created: ${workspace.id} with owner ${dto.ownerId}`);
|
|
|
|
return {
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
ownerId: workspace.ownerId,
|
|
settings: workspace.settings as Record<string, unknown>,
|
|
createdAt: workspace.createdAt,
|
|
updatedAt: workspace.updatedAt,
|
|
memberCount: 1,
|
|
};
|
|
}
|
|
|
|
async updateWorkspace(
|
|
id: string,
|
|
dto: { name?: string; settings?: Record<string, unknown> }
|
|
): Promise<AdminWorkspaceResponse> {
|
|
const existing = await this.prisma.workspace.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new NotFoundException(`Workspace ${id} not found`);
|
|
}
|
|
|
|
const data: Prisma.WorkspaceUpdateInput = {};
|
|
|
|
if (dto.name !== undefined) {
|
|
data.name = dto.name;
|
|
}
|
|
if (dto.settings !== undefined) {
|
|
data.settings = dto.settings as Prisma.InputJsonValue;
|
|
}
|
|
|
|
const workspace = await this.prisma.workspace.update({
|
|
where: { id },
|
|
data,
|
|
include: {
|
|
_count: { select: { members: true } },
|
|
},
|
|
});
|
|
|
|
this.logger.log(`Workspace updated: ${id}`);
|
|
|
|
return {
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
ownerId: workspace.ownerId,
|
|
settings: workspace.settings as Record<string, unknown>,
|
|
createdAt: workspace.createdAt,
|
|
updatedAt: workspace.updatedAt,
|
|
memberCount: workspace._count.members,
|
|
};
|
|
}
|
|
}
|