Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions apps/api/prisma/migrations/20260308180436_add_teams/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- CreateEnum
CREATE TYPE "TeamRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');

-- CreateEnum
CREATE TYPE "InvitationStatus" AS ENUM ('PENDING', 'ACCEPTED', 'DECLINED', 'EXPIRED');

-- CreateTable
CREATE TABLE "Team" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"avatarUrl" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "TeamMember" (
"id" TEXT NOT NULL,
"teamId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"role" "TeamRole" NOT NULL DEFAULT 'MEMBER',
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "TeamInvitation" (
"id" TEXT NOT NULL,
"teamId" TEXT NOT NULL,
"invitedById" TEXT NOT NULL,
"email" TEXT NOT NULL,
"role" "TeamRole" NOT NULL DEFAULT 'MEMBER',
"status" "InvitationStatus" NOT NULL DEFAULT 'PENDING',
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "TeamInvitation_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "TeamMember_teamId_userId_key" ON "TeamMember"("teamId", "userId");

-- CreateIndex
CREATE UNIQUE INDEX "TeamInvitation_token_key" ON "TeamInvitation"("token");

-- CreateIndex
CREATE INDEX "TeamInvitation_email_idx" ON "TeamInvitation"("email");

-- CreateIndex
CREATE INDEX "TeamInvitation_token_idx" ON "TeamInvitation"("token");

-- AddForeignKey
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "TeamInvitation" ADD CONSTRAINT "TeamInvitation_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "TeamInvitation" ADD CONSTRAINT "TeamInvitation_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
64 changes: 60 additions & 4 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,71 @@ enum Priority {
CRITICAL
}

enum TeamRole {
OWNER
ADMIN
MEMBER
}

enum InvitationStatus {
PENDING
ACCEPTED
DECLINED
EXPIRED
}

model User {
id String @id @default(uuid())
email String @unique
id String @id @default(uuid())
email String @unique
name String?
password String
createdProjects Project[]
assignedTasks Task[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamMemberships TeamMember[]
sentInvitations TeamInvitation[] @relation("SentInvitations")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Team {
id String @id @default(uuid())
name String
description String?
avatarUrl String?
members TeamMember[]
invitations TeamInvitation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model TeamMember {
id String @id @default(uuid())
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role TeamRole @default(MEMBER)
joinedAt DateTime @default(now())

@@unique([teamId, userId])
}

model TeamInvitation {
id String @id @default(uuid())
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
invitedById String
invitedBy User @relation("SentInvitations", fields: [invitedById], references: [id])
email String
role TeamRole @default(MEMBER)
status InvitationStatus @default(PENDING)
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([email])
@@index([token])
}

model Project {
Expand Down
46 changes: 46 additions & 0 deletions apps/api/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,52 @@ async function main() {

console.log(`✅ Создано задач: 8`)

// Создание тестовой команды
console.log('👥 Создание команды...')
const team = await prisma.team.create({
data: {
name: 'Команда разработки',
description: 'Основная команда разработчиков трекера задач',
},
})

// Добавление участников команды
await prisma.teamMember.createMany({
data: [
{
teamId: team.id,
userId: user1.id,
role: 'OWNER',
},
{
teamId: team.id,
userId: user2.id,
role: 'MEMBER',
},
{
teamId: team.id,
userId: user3.id,
role: 'ADMIN',
},
],
})

console.log(`✅ Создана команда "${team.name}" с 3 участниками`)

// Создание тестового приглашения
await prisma.teamInvitation.create({
data: {
teamId: team.id,
invitedById: user1.id,
email: 'newmember@example.com',
role: 'MEMBER',
token: 'test-invitation-token-00000000-0000-0000-0000-000000000001',
expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000), // +48 часов
},
})

console.log('✅ Создано тестовое приглашение')

console.log('✨ Заполнение базы данных завершено успешно!')
}

Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/auth/decorators/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common'
import { ROLES_KEY, type TeamRole } from 'src/common/constants/roles.constants'

export const Roles = (...roles: TeamRole[]) => SetMetadata(ROLES_KEY, roles)
4 changes: 4 additions & 0 deletions apps/api/src/common/constants/roles.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { TEAM_ROLES } from '@repo/types'
export type { TeamRole } from '@repo/types'

export const ROLES_KEY = 'roles'
48 changes: 48 additions & 0 deletions apps/api/src/guards/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import type { Request } from 'express'
import type { User } from 'generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { ROLES_KEY, type TeamRole } from 'src/common/constants/roles.constants'

@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly prisma: PrismaService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<TeamRole[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
])

if (!requiredRoles || requiredRoles.length === 0) {
return true
}

const request = context.switchToHttp().getRequest<Request>()
const user = request.user as User
const teamId = request.params['teamId']

if (!user || !teamId) {
throw new ForbiddenException('Недостаточно прав')
}

const member = await this.prisma.teamMember.findUnique({
where: { teamId_userId: { teamId, userId: user.id } },
})

if (!member || !requiredRoles.includes(member.role as TeamRole)) {
throw new ForbiddenException('Недостаточно прав')
}

return true
}
}
2 changes: 2 additions & 0 deletions packages/types/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export { loginRequestSchema } from './auth/schema/login-request.schema'
export type { LoginRequest } from './auth/schema/login-request.schema'
export { authResponseSchema } from './auth/schema/auth-response.schema'
export type { AuthResponse } from './auth/schema/auth-response.schema'
export { TEAM_ROLES } from './teams/team-role.constants'
export type { TeamRole } from './teams/team-role.constants'
7 changes: 7 additions & 0 deletions packages/types/src/teams/team-role.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const TEAM_ROLES = {
OWNER: 'OWNER',
ADMIN: 'ADMIN',
MEMBER: 'MEMBER',
} as const

export type TeamRole = (typeof TEAM_ROLES)[keyof typeof TEAM_ROLES]
Loading