Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): updated functionality of API key #114

Merged
merged 14 commits into from
Feb 7, 2024
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
21 changes: 17 additions & 4 deletions .github/workflows/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Unit tests
run: |
pnpm run db:generate-types
pnpm run test:api
pnpm run test:api -- --no-cache

- name: E2E tests
env:
Expand All @@ -57,19 +57,32 @@ jobs:
GITHUB_CALLBACK_URL: dummy
run: |
docker compose up -d
pnpm run e2e:api
pnpm run e2e:api -- --no-cache
docker compose down

- name: Upload coverage reports to Codecov
- name: Upload unit test coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage/apps/api
directory: coverage/api/
with:
flags: api-unit-tests
files: coverage-final.json

- name: Upload e2e test coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
directory: coverage-e2e/api/
with:
flags: api-e2e-tests
files: coverage-final.json

deploy-stage:
runs-on: ubuntu-latest
name: Deploy to stage
if: ${{ github.ref == 'refs/heads/develop' }}
needs: validate

steps:
- name: Checkout
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/web.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ jobs:
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: web

deploy-stage:
runs-on: ubuntu-latest
name: Deploy to stage
if: ${{ github.ref == 'refs/heads/develop' }}
needs: validate

steps:
- name: Checkout
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ node_modules
/.sass-cache
/connect.lock
/coverage
/coverage-e2e
/libpeerconnection.log
npm-debug.log
yarn-error.log
Expand Down
1 change: 1 addition & 0 deletions apps/api/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
},
"test:e2e": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage-e2e/{projectRoot}"],
"options": {
"runInBand": true,
"devServerTarget": "api:serve",
Expand Down
47 changes: 46 additions & 1 deletion apps/api/src/api-key/api-key.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('Api Key Role Controller Tests', () => {
let prisma: PrismaService
let user: User
let apiKey: ApiKey
let apiKeyValue: string

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
Expand Down Expand Up @@ -51,7 +52,8 @@ describe('Api Key Role Controller Tests', () => {
url: '/api-key',
payload: {
name: 'Test Key',
expiresAfter: '24'
expiresAfter: '24',
authorities: ['READ_API_KEY']
},
headers: {
'x-e2e-user-email': user.email
Expand All @@ -63,12 +65,14 @@ describe('Api Key Role Controller Tests', () => {
id: expect.any(String),
name: 'Test Key',
value: expect.stringMatching(/^ks_*/),
authorities: ['READ_API_KEY'],
expiresAt: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
})

apiKey = response.json()
apiKeyValue = response.json().value
})

it('should be able to update the api key', async () => {
Expand All @@ -88,6 +92,7 @@ describe('Api Key Role Controller Tests', () => {
expect(response.json()).toEqual({
id: apiKey.id,
name: 'Updated Test Key',
authorities: ['READ_API_KEY'],
expiresAt: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
Expand All @@ -109,6 +114,7 @@ describe('Api Key Role Controller Tests', () => {
expect(response.json()).toEqual({
id: apiKey.id,
name: 'Updated Test Key',
authorities: ['READ_API_KEY'],
expiresAt: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
Expand All @@ -129,13 +135,52 @@ describe('Api Key Role Controller Tests', () => {
{
id: apiKey.id,
name: 'Updated Test Key',
authorities: ['READ_API_KEY'],
expiresAt: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
}
])
})

it('should be able to get all api keys using the API key', async () => {
const response = await app.inject({
method: 'GET',
url: '/api-key/all/as-user',
headers: {
'x-keyshade-token': apiKeyValue
}
})

expect(response.statusCode).toBe(200)
expect(response.json()).toEqual([
{
id: apiKey.id,
name: 'Updated Test Key',
authorities: ['READ_API_KEY'],
expiresAt: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
}
])
})

it('should not be able to create api key with invalid authorities of API key', async () => {
const response = await app.inject({
method: 'POST',
url: '/api-key',
payload: {
name: 'Test Key',
expiresAfter: '24'
},
headers: {
'x-keyshade-token': apiKeyValue
}
})

expect(response.statusCode).toBe(401)
})

it('should be able to delete the api key', async () => {
const response = await app.inject({
method: 'DELETE',
Expand Down
10 changes: 8 additions & 2 deletions apps/api/src/api-key/controller/api-key.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ import {
UseGuards
} from '@nestjs/common'
import { ApiKeyService } from '../service/api-key.service'
import { User } from '@prisma/client'
import { CurrentUser } from '../../decorators/user.decorator'
import { CreateApiKey } from '../dto/create.api-key/create.api-key'
import { UpdateApiKey } from '../dto/update.api-key/update.api-key'
import { AdminGuard } from '../../auth/guard/admin.guard'
import { AdminGuard } from '../../auth/guard/admin/admin.guard'
import { Authority, User } from '@prisma/client'
import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator'

@Controller('api-key')
export class ApiKeyController {
constructor(private readonly apiKeyService: ApiKeyService) {}

@Post()
@RequiredApiKeyAuthorities(Authority.CREATE_API_KEY)
async createApiKey(@CurrentUser() user: User, @Body() dto: CreateApiKey) {
return this.apiKeyService.createApiKey(user, dto)
}

@Put(':id')
@RequiredApiKeyAuthorities(Authority.UPDATE_API_KEY)
async updateApiKey(
@CurrentUser() user: User,
@Body() dto: UpdateApiKey,
Expand All @@ -35,16 +38,19 @@ export class ApiKeyController {
}

@Delete(':id')
@RequiredApiKeyAuthorities(Authority.DELETE_API_KEY)
async deleteApiKey(@CurrentUser() user: User, @Param('id') id: string) {
return this.apiKeyService.deleteApiKey(user, id)
}

@Get(':id')
@RequiredApiKeyAuthorities(Authority.READ_API_KEY)
async getApiKey(@CurrentUser() user: User, @Param('id') id: string) {
return this.apiKeyService.getApiKeyById(user, id)
}

@Get('all/as-user')
@RequiredApiKeyAuthorities(Authority.READ_API_KEY)
async getApiKeysOfUser(
@CurrentUser() user: User,
@Query('page') page: number = 1,
Expand Down
11 changes: 8 additions & 3 deletions apps/api/src/api-key/dto/create.api-key/create.api-key.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { ApiKey } from '@prisma/client'
import { IsString } from 'class-validator'
import { ApiKey, Authority } from '@prisma/client'
import { IsArray, IsOptional, IsString } from 'class-validator'

export class CreateApiKey {
@IsString()
name: ApiKey['name']

@IsString()
expiresAfter: '24' | '168' | '720' | '8760' | 'never' = 'never'
@IsOptional()
expiresAfter?: '24' | '168' | '720' | '8760' | 'never' = 'never'

@IsArray()
@IsOptional()
authorities?: Authority[] = []
}
17 changes: 16 additions & 1 deletion apps/api/src/api-key/service/api-key.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common'
import { User } from '@prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateApiKey } from '../dto/create.api-key/create.api-key'
import { addHoursToDate } from '../../common/add-hours-to-date'
import { generateApiKey } from '../../common/api-key-generator'
import { toSHA256 } from '../../common/to-sha256'
import { UpdateApiKey } from '../dto/update.api-key/update.api-key'
import { User } from '@prisma/client'

@Injectable()
export class ApiKeyService {
Expand All @@ -20,6 +20,11 @@ export class ApiKeyService {
data: {
name: dto.name,
value: hashedApiKey,
authorities: dto.authorities
? {
set: dto.authorities
}
: [],
expiresAt: addHoursToDate(dto.expiresAfter),
user: {
connect: {
Expand All @@ -31,6 +36,7 @@ export class ApiKeyService {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
Expand All @@ -52,6 +58,11 @@ export class ApiKeyService {
},
data: {
name: dto.name,
authorities: dto.authorities
? {
set: dto.authorities
}
: undefined,
expiresAt: dto.expiresAfter
? addHoursToDate(dto.expiresAfter)
: undefined
Expand All @@ -60,6 +71,7 @@ export class ApiKeyService {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
Expand Down Expand Up @@ -89,6 +101,7 @@ export class ApiKeyService {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
Expand Down Expand Up @@ -127,6 +140,7 @@ export class ApiKeyService {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
Expand Down Expand Up @@ -155,6 +169,7 @@ export class ApiKeyService {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import { PrismaModule } from '../prisma/prisma.module'
import { CommonModule } from '../common/common.module'
import { MailModule } from '../mail/mail.module'
import { APP_GUARD } from '@nestjs/core'
import { AuthGuard } from '../auth/guard/auth.guard'
import { AuthGuard } from '../auth/guard/auth/auth.guard'
import { UserModule } from '../user/user.module'
import { ProjectModule } from '../project/project.module'
import { EnvironmentModule } from '../environment/environment.module'
import { ApiKeyModule } from '../api-key/api-key.module'
import { WorkspaceModule } from '../workspace/workspace.module'
import { WorkspaceRoleModule } from '../workspace-role/workspace-role.module'
import { ApiKeyGuard } from '../auth/guard/api-key/api-key.guard'

@Module({
controllers: [AppController],
Expand All @@ -39,6 +40,10 @@ import { WorkspaceRoleModule } from '../workspace-role/workspace-role.module'
{
provide: APP_GUARD,
useClass: AuthGuard
},
{
provide: APP_GUARD,
useClass: ApiKeyGuard
}
]
})
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/auth/auth.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { User } from '@prisma/client'
import { Authority, User } from '@prisma/client'

export type UserAuthenticatedResponse = User & {
token: string
}

export type AuthenticatedUserContext = User & {
isAuthViaApiKey?: boolean
apiKeyAuthorities?: Set<Authority>
}
7 changes: 7 additions & 0 deletions apps/api/src/auth/guard/api-key/api-key.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ApiKeyGuard } from './api-key.guard'

describe('ApiKeyGuard', () => {
it('should be defined', () => {
expect(new ApiKeyGuard(null)).toBeDefined()
})
})
Loading
Loading