Skip to content
Draft
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
10 changes: 8 additions & 2 deletions apps/server-nestjs/src/main.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DeploymentModule } from './modules/deployment/deployment.module'
import { EnvironmentModule } from './modules/environment/environment.module'
import { HealthzModule } from './modules/healthz/healthz.module'
import { InfrastructureModule } from './modules/infrastructure/infrastructure.module'
import { KeycloakModule } from './modules/keycloak/keycloak.module'
import { LogModule } from './modules/log/log.module'
import { PluginModule } from './modules/plugin/plugin.module'
import { ProjectBulkModule } from './modules/project-bulk/project-bulk.module'
Expand All @@ -17,7 +18,9 @@ import { ProjectSecretsModule } from './modules/project-secrets/project-secrets.
import { ProjectServicesModule } from './modules/project-services/project-services.module'
import { ProjectModule } from './modules/project/project.module'
import { RepositoryModule } from './modules/repository/repository.module'
import { ServiceChainModule } from './modules/service-chain/service-chain.module'
import { SystemSettingsModule } from './modules/system-settings/system-settings.module'
import { UserTokensModule } from './modules/user-tokens/user-tokens.module'
import { VersionModule } from './modules/version/version.module'
import { getDotenvPaths } from './utils/dotenv.utils'

Expand All @@ -33,18 +36,21 @@ import { getDotenvPaths } from './utils/dotenv.utils'
EnvironmentModule,
HealthzModule,
InfrastructureModule,
KeycloakModule,
LogModule,
PluginModule,
ProjectBulkModule,
ProjectHooksModule,
ProjectMembersModule,
ProjectModule,
ProjectRolesModule,
ProjectSecretsModule,
ProjectServicesModule,
RepositoryModule,
ScheduleModule.forRoot(),
ProjectModule,
ServiceChainModule,
SystemSettingsModule,
ScheduleModule.forRoot(),
UserTokensModule,
VersionModule,
],
controllers: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {
projectContract,
projectRoleContract,
} from '@cpn-console/shared'
import type { ClientInferRequest } from '@ts-rest/core'

export type CreateProjectBody = ClientInferRequest<typeof projectContract.createProject>['body']
export type UpdateProjectBody = ClientInferRequest<typeof projectContract.updateProject>['body']
export type BulkActionProjectBody = ClientInferRequest<typeof projectContract.bulkActionProject>['body']

export type CreateProjectRoleBody = ClientInferRequest<typeof projectRoleContract.createProjectRole>['body']
export type PatchProjectRolesBody = ClientInferRequest<typeof projectRoleContract.patchProjectRoles>['body']
8 changes: 8 additions & 0 deletions apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ describe('pluginModule', () => {
vi.stubEnv('USE_HARBOR', 'true')
vi.stubEnv('USE_NEXUS', 'true')
vi.stubEnv('USE_SONARQUBE', 'true')
vi.stubEnv('DB_URL', 'postgres://db')
vi.stubEnv('KEYCLOAK_DOMAIN', 'keycloak.test')
vi.stubEnv('KEYCLOAK_REALM', 'test')
vi.stubEnv('KEYCLOAK_CLIENT_ID', 'client')
vi.stubEnv('KEYCLOAK_CLIENT_SECRET', 'secret')
vi.stubEnv('KEYCLOAK_ADMIN', 'admin')
vi.stubEnv('KEYCLOAK_ADMIN_PASSWORD', 'pw')
vi.stubEnv('KEYCLOAK_REDIRECT_URI', 'https://console.test/')
vi.stubEnv('VAULT_TOKEN', 'test-token')
vi.stubEnv('VAULT_URL', 'https://vault.test/')
vi.stubEnv('GITLAB_TOKEN', 'test-token')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FastifyRequest } from 'fastify'
import type { UserContext } from '../infrastructure/auth/auth-user.decorator'
import type { BulkActionProjectBody } from '../infrastructure/clean-types/controller-body.types'
import { projectContract } from '@cpn-console/shared'
import { Body, Controller, HttpCode, HttpStatus, Inject, Logger, Post, Req, UseGuards } from '@nestjs/common'
import { AuthUser } from '../infrastructure/auth/auth-user.decorator'
Expand All @@ -21,7 +22,7 @@ export class ProjectBulkController {
@UseGuards(UserGuard)
@RequireAdminPermission('Manage')
async bulkAction(
@Body(new ZodValidationPipe(projectContract.bulkActionProject.body)) body: typeof projectContract.bulkActionProject.body._type,
@Body(new ZodValidationPipe(projectContract.bulkActionProject.body)) body: BulkActionProjectBody,
@AuthUser() user: UserContext,
@Req() request: FastifyRequest,
): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { projectContract } from '@cpn-console/shared'
import type { BulkActionProjectBody } from '../infrastructure/clean-types/controller-body.types'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { trace } from '@opentelemetry/api'
import { PrismaService } from '../infrastructure/database/prisma.service'
Expand All @@ -19,7 +19,7 @@ export class ProjectBulkService {

@StartActiveSpan()
async bulkAction(
data: typeof projectContract.bulkActionProject.body._type,
data: BulkActionProjectBody,
requestorUserId?: string,
requestId?: string,
): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CreateProjectRoleBody, PatchProjectRolesBody } from '../infrastructure/clean-types/controller-body.types'
import type { ProjectContext } from '../infrastructure/permission/project/project.guard'
import { projectRoleContract } from '@cpn-console/shared'
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Inject, Param, Patch, Post, UseGuards } from '@nestjs/common'
Expand Down Expand Up @@ -30,7 +31,7 @@ export class ProjectRolesController {
@RequireProjectLocked(false)
@RequireProjectPermission('ManageRoles')
async create(
@Body(new ZodValidationPipe(projectRoleContract.createProjectRole.body)) body: typeof projectRoleContract.createProjectRole.body._type,
@Body(new ZodValidationPipe(projectRoleContract.createProjectRole.body)) body: CreateProjectRoleBody,
@Project() project: ProjectContext,
) {
return this.projectRoles.create(project.id, body)
Expand All @@ -42,7 +43,7 @@ export class ProjectRolesController {
@RequireProjectLocked(false)
@RequireProjectPermission('ManageRoles')
async update(
@Body(new ZodValidationPipe(projectRoleContract.patchProjectRoles.body)) body: typeof projectRoleContract.patchProjectRoles.body._type,
@Body(new ZodValidationPipe(projectRoleContract.patchProjectRoles.body)) body: PatchProjectRolesBody,
@Project() project: ProjectContext,
) {
return this.projectRoles.update(project.id, body)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { projectContract } from '@cpn-console/shared'
import type { Prisma, ProjectMembers, User } from '@prisma/client'
import type { CreateProjectBody } from '../infrastructure/clean-types/controller-body.types'
import type { ProjectContext } from '../infrastructure/permission/project/project.guard'
import type { projectSelect, ProjectWithDetails } from './project-queries.utils'
import { PROJECT_PERMS } from '@cpn-console/shared'
Expand Down Expand Up @@ -138,7 +139,7 @@ export function makeProjectWithMembersResult(
return { ...project, members } as ProjectWithMembers
}

export function makeCreateProjectBody(overrides: Partial<typeof projectContract.createProject.body._type> = {}): typeof projectContract.createProject.body._type {
export function makeCreateProjectBody(overrides: Partial<CreateProjectBody> = {}): CreateProjectBody {
Comment thread
shikanime marked this conversation as resolved.
return {
name: faker.string.alphanumeric({ length: faker.number.int({ min: 2, max: 20 }) }).toLowerCase(),
description: faker.lorem.sentence(),
Expand Down
5 changes: 3 additions & 2 deletions apps/server-nestjs/src/modules/project/project.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProjectV2 } from '@cpn-console/shared'
import type { FastifyRequest } from 'fastify'
import type { UserContext } from '../infrastructure/auth/auth-user.decorator'
import type { CreateProjectBody, UpdateProjectBody } from '../infrastructure/clean-types/controller-body.types'
import type { ProjectContext } from '../infrastructure/permission/project/project.guard'
import { AdminAuthorized, projectContract } from '@cpn-console/shared'
import { Body, Controller, Delete, ForbiddenException, Get, HttpCode, HttpStatus, Inject, Post, Put, Query, Req, UseGuards } from '@nestjs/common'
Expand Down Expand Up @@ -47,7 +48,7 @@ export class ProjectController {
@UseGuards(UserGuard)
@RequireAdminPermission('ManageProjects')
async create(
@Body(new ZodValidationPipe(projectContract.createProject.body)) body: typeof projectContract.createProject.body._type,
@Body(new ZodValidationPipe(projectContract.createProject.body)) body: CreateProjectBody,
@AuthUser() user: UserContext,
@Req() request: FastifyRequest,
): Promise<ProjectV2> {
Expand All @@ -69,7 +70,7 @@ export class ProjectController {
@RequireProjectStatus('initializing', 'created', 'failed', 'warning')
@RequireProjectPermission('Manage')
async update(
@Body(new ZodValidationPipe(projectContract.updateProject.body)) data: typeof projectContract.updateProject.body._type,
@Body(new ZodValidationPipe(projectContract.updateProject.body)) data: UpdateProjectBody,
@Project() project: ProjectContext,
@AuthUser() user: UserContext,
@Req() request: FastifyRequest,
Expand Down
7 changes: 4 additions & 3 deletions apps/server-nestjs/src/modules/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { projectContract } from '@cpn-console/shared'
import type { ConfigType } from '@nestjs/config'
import type { Prisma } from '@prisma/client'
import type { UserContext } from '../infrastructure/auth/auth-user.decorator'
import type { CreateProjectBody, UpdateProjectBody } from '../infrastructure/clean-types/controller-body.types'
import type { ProjectDataExport, ProjectUpdateContext, ProjectWithDetails } from './project-queries.utils'
import { AdminAuthorized } from '@cpn-console/shared'
import { BadRequestException, ForbiddenException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'
Expand Down Expand Up @@ -90,7 +91,7 @@ export class ProjectService {

@StartActiveSpan()
async create(
body: typeof projectContract.createProject.body._type,
body: CreateProjectBody,
requestorUserId: string,
requestId?: string,
): Promise<ProjectWithDetails> {
Expand Down Expand Up @@ -134,7 +135,7 @@ export class ProjectService {
}

async update(
body: typeof projectContract.updateProject.body._type,
body: UpdateProjectBody,
user: UserContext,
projectId: string,
requestId?: string,
Expand Down Expand Up @@ -266,7 +267,7 @@ export class ProjectService {
}

private stripProjectUpdateBody(
body: typeof projectContract.updateProject.body._type,
body: UpdateProjectBody,
user: UserContext,
project: ProjectUpdateContext,
): { effectiveData: Record<string, unknown>, strippedKeys: string[] } {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Prisma } from '@prisma/client'
import type { DeepMockProxy } from 'vitest-mock-extended'
import { faker } from '@faker-js/faker'
import { beforeEach, describe, expect, it } from 'vitest'
import { mockDeep } from 'vitest-mock-extended'
import {
createUserToken,
listUserTokens,
userTokenSelect,
} from './user-tokens-queries.utils'

describe('user-tokens-queries.utils', () => {
let tx: DeepMockProxy<Prisma.TransactionClient>

beforeEach(() => {
tx = mockDeep<Prisma.TransactionClient>()
})

describe('listUserTokens', () => {
it('scopes by userId and uses the shared select', async () => {
const userId = faker.string.uuid()
tx.personalAccessToken.findMany.mockResolvedValue([])

await listUserTokens(tx, userId)

expect(tx.personalAccessToken.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { userId }, select: userTokenSelect }),
)
})
})

describe('createUserToken', () => {
it('creates a personal access token with the provided fields', async () => {
const data = {
name: faker.word.noun(),
expirationDate: faker.date.future(),
hash: faker.string.alphanumeric(64),
userId: faker.string.uuid(),
}

await createUserToken(tx, data)

expect(tx.personalAccessToken.create).toHaveBeenCalledWith({ data, select: userTokenSelect })
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Prisma } from '@prisma/client'

export const userTokenOwnerSelect = {
id: true,
email: true,
firstName: true,
lastName: true,
type: true,
} satisfies Prisma.UserSelect

export const userTokenSelect = {
id: true,
name: true,
lastUse: true,
expirationDate: true,
status: true,
createdAt: true,
userId: true,
owner: {
select: userTokenOwnerSelect,
},
} satisfies Prisma.PersonalAccessTokenSelect

export type UserTokenRecord = Prisma.PersonalAccessTokenGetPayload<{
select: typeof userTokenSelect
}>

export function listUserTokens(tx: Prisma.TransactionClient, userId: string) {
return tx.personalAccessToken.findMany({
where: { userId },
orderBy: [{ status: 'asc' }, { createdAt: 'asc' }],
select: userTokenSelect,
})
}

export function createUserToken(tx: Prisma.TransactionClient, data: {
name: string
expirationDate: Date
hash: string
userId: string
}) {
return tx.personalAccessToken.create({
data: {
name: data.name,
hash: data.hash,
expirationDate: data.expirationDate,
userId: data.userId,
},
select: userTokenSelect,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { UserContext } from '../infrastructure/auth/auth-user.decorator'
import type { CreatePersonalAccessTokenBody } from './user-tokens.utils'
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Inject, Param, ParseUUIDPipe, Post, UseGuards } from '@nestjs/common'
import { AuthUser } from '../infrastructure/auth/auth-user.decorator'
import { RequireUserType } from '../infrastructure/permission/user/user-type.decorator'
import { UserGuard } from '../infrastructure/permission/user/user.guard'
import { ZodValidationPipe } from '../infrastructure/pipe/zod-validation.pipe'
import { UserTokensService } from './user-tokens.service'
import { CreatePersonalAccessTokenBodySchema } from './user-tokens.utils'

@Controller('api/v1/user/tokens')
@UseGuards(UserGuard)
@RequireUserType('human')
export class UserTokensController {
constructor(@Inject(UserTokensService) private readonly service: UserTokensService) {}

@Get()
async list(@AuthUser() user: UserContext) {
return this.service.list(user.userId)
}

@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Body(new ZodValidationPipe(CreatePersonalAccessTokenBodySchema)) data: CreatePersonalAccessTokenBody,
@AuthUser() user: UserContext,
) {
return this.service.create(data, user.userId)
}

@Delete(':tokenId')
@HttpCode(HttpStatus.NO_CONTENT)
async delete(
@Param('tokenId', ParseUUIDPipe) tokenId: string,
@AuthUser() user: UserContext,
): Promise<void> {
return this.service.delete(tokenId, user.userId)
}
}
12 changes: 12 additions & 0 deletions apps/server-nestjs/src/modules/user-tokens/user-tokens.module.ts
Comment thread
shikanime marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { InfrastructureModule } from '../infrastructure/infrastructure.module'
import { UserTokensController } from './user-tokens.controller'
import { UserTokensService } from './user-tokens.service'

@Module({
imports: [InfrastructureModule],
controllers: [UserTokensController],
providers: [UserTokensService],
exports: [UserTokensService],
})
export class UserTokensModule {}
Loading
Loading