Skip to content
Open
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
41 changes: 41 additions & 0 deletions apps/backend/src/modules/api-keys/api-keys.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Controller, Get, Post, Delete, Body, Param, HttpCode } from '@nestjs/common';
import { ApiKeysService } from './api-keys.service';
import { CreateApiKeyDto, ApiKeyResponseDto, ApiKeyUsageDto } from './dto/api-key.dto';

// NOTE: In production, add @UseGuards(AuthGuard) and extract userId from request
// For now, userId is passed as a path parameter for demo purposes

@Controller('api-keys')
export class ApiKeysController {
constructor(private readonly apiKeysService: ApiKeysService) {}

@Post(':userId')
async createApiKey(
@Param('userId') userId: string,
@Body() dto: CreateApiKeyDto,
): Promise<ApiKeyResponseDto & { key: string }> {
return this.apiKeysService.createApiKey(userId, dto);
}

@Get(':userId')
async getApiKeys(@Param('userId') userId: string): Promise<ApiKeyResponseDto[]> {
return this.apiKeysService.getApiKeys(userId);
}

@Delete(':userId/:keyId')
@HttpCode(204)
async revokeApiKey(
@Param('userId') userId: string,
@Param('keyId') keyId: string,
): Promise<void> {
return this.apiKeysService.revokeApiKey(userId, keyId);
}

@Get(':userId/:keyId/usage')
async getApiKeyUsage(
@Param('userId') userId: string,
@Param('keyId') keyId: string,
): Promise<ApiKeyUsageDto> {
return this.apiKeysService.getApiKeyUsage(userId, keyId);
}
}
10 changes: 10 additions & 0 deletions apps/backend/src/modules/api-keys/api-keys.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ApiKeysController } from './api-keys.controller';
import { ApiKeysService } from './api-keys.service';

@Module({
controllers: [ApiKeysController],
providers: [ApiKeysService],
exports: [ApiKeysService],
})
export class ApiKeysModule {}
133 changes: 133 additions & 0 deletions apps/backend/src/modules/api-keys/api-keys.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { randomBytes, createHash } from 'crypto';
import { CreateApiKeyDto, ApiKeyResponseDto, ApiKeyUsageDto } from './dto/api-key.dto';

@Injectable()
export class ApiKeysService {
private prisma: PrismaClient;

constructor() {
this.prisma = new PrismaClient();
}

private generateKey(): string {
return `sk_${randomBytes(32).toString('hex')}`;
}

private hashKey(key: string): string {
return createHash('sha256').update(key).digest('hex');
}

private getKeyPreview(key: string): string {
return key.slice(-4);
}

async createApiKey(
userId: string,
dto: CreateApiKeyDto,
): Promise<ApiKeyResponseDto & { key: string }> {
const key = this.generateKey();
const keyHash = this.hashKey(key);

const apiKey = await this.prisma.apiKey.create({
data: {
userId,
name: dto.name,
key: keyHash,
keyHash,
},
});

return {
id: apiKey.id,
name: apiKey.name,
key,
keyPreview: this.getKeyPreview(key),
createdAt: apiKey.createdAt,
lastUsedAt: apiKey.lastUsedAt,
revokedAt: apiKey.revokedAt,
};
}

async getApiKeys(userId: string): Promise<ApiKeyResponseDto[]> {
const apiKeys = await this.prisma.apiKey.findMany({
where: {
userId,
revokedAt: null,
},
orderBy: {
createdAt: 'desc',
},
});

return apiKeys.map(key => ({
id: key.id,
name: key.name,
keyPreview: key.keyHash.slice(-4),
createdAt: key.createdAt,
lastUsedAt: key.lastUsedAt,
revokedAt: key.revokedAt,
}));
}

async revokeApiKey(userId: string, keyId: string): Promise<void> {
const apiKey = await this.prisma.apiKey.findUnique({
where: { id: keyId },
});

if (!apiKey) {
throw new NotFoundException(`API key not found`);
}

if (apiKey.userId !== userId) {
throw new ConflictException(`Unauthorized access to API key`);
}

await this.prisma.apiKey.update({
where: { id: keyId },
data: { revokedAt: new Date() },
});
}

async validateAndUpdateUsage(key: string): Promise<string | null> {
const keyHash = this.hashKey(key);

const apiKey = await this.prisma.apiKey.findUnique({
where: { keyHash },
});

if (!apiKey || apiKey.revokedAt) {
return null;
}

await this.prisma.apiKey.update({
where: { id: apiKey.id },
data: { lastUsedAt: new Date() },
});

return apiKey.userId;
}

async getApiKeyUsage(userId: string, keyId: string): Promise<ApiKeyUsageDto> {
const apiKey = await this.prisma.apiKey.findUnique({
where: { id: keyId },
});

if (!apiKey || apiKey.userId !== userId) {
throw new NotFoundException(`API key not found`);
}

// Placeholder for actual usage tracking
const requestCount = 0;

return {
id: apiKey.id,
name: apiKey.name,
keyPreview: apiKey.keyHash.slice(-4),
createdAt: apiKey.createdAt,
lastUsedAt: apiKey.lastUsedAt,
requestCount,
};
}
}
22 changes: 22 additions & 0 deletions apps/backend/src/modules/api-keys/dto/api-key.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class CreateApiKeyDto {
name: string;

Check failure on line 2 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'name' has no initializer and is not definitely assigned in the constructor.

Check failure on line 2 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'name' has no initializer and is not definitely assigned in the constructor.

Check failure on line 2 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'name' has no initializer and is not definitely assigned in the constructor.
}

export class ApiKeyResponseDto {
id: string;

Check failure on line 6 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'id' has no initializer and is not definitely assigned in the constructor.

Check failure on line 6 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'id' has no initializer and is not definitely assigned in the constructor.

Check failure on line 6 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'id' has no initializer and is not definitely assigned in the constructor.
name: string;

Check failure on line 7 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'name' has no initializer and is not definitely assigned in the constructor.

Check failure on line 7 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'name' has no initializer and is not definitely assigned in the constructor.

Check failure on line 7 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'name' has no initializer and is not definitely assigned in the constructor.
key?: string; // Only returned on creation
keyPreview: string; // Last 4 chars

Check failure on line 9 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'keyPreview' has no initializer and is not definitely assigned in the constructor.

Check failure on line 9 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'keyPreview' has no initializer and is not definitely assigned in the constructor.

Check failure on line 9 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'keyPreview' has no initializer and is not definitely assigned in the constructor.
createdAt: Date;

Check failure on line 10 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'createdAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 10 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'createdAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 10 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'createdAt' has no initializer and is not definitely assigned in the constructor.
lastUsedAt: Date | null;

Check failure on line 11 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'lastUsedAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 11 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'lastUsedAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 11 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'lastUsedAt' has no initializer and is not definitely assigned in the constructor.
revokedAt: Date | null;

Check failure on line 12 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'revokedAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 12 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'revokedAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 12 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'revokedAt' has no initializer and is not definitely assigned in the constructor.
}

export class ApiKeyUsageDto {
id: string;

Check failure on line 16 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'id' has no initializer and is not definitely assigned in the constructor.

Check failure on line 16 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'id' has no initializer and is not definitely assigned in the constructor.

Check failure on line 16 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'id' has no initializer and is not definitely assigned in the constructor.
name: string;

Check failure on line 17 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'name' has no initializer and is not definitely assigned in the constructor.

Check failure on line 17 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'name' has no initializer and is not definitely assigned in the constructor.

Check failure on line 17 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'name' has no initializer and is not definitely assigned in the constructor.
keyPreview: string;

Check failure on line 18 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'keyPreview' has no initializer and is not definitely assigned in the constructor.

Check failure on line 18 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'keyPreview' has no initializer and is not definitely assigned in the constructor.

Check failure on line 18 in apps/backend/src/modules/api-keys/dto/api-key.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'keyPreview' has no initializer and is not definitely assigned in the constructor.
createdAt: Date;
lastUsedAt: Date | null;
requestCount: number;
}
10 changes: 10 additions & 0 deletions apps/backend/src/modules/api-keys/interfaces/api-key.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface IApiKey {
id: string;
userId: string;
name: string;
key: string;
keyHash: string;
lastUsedAt: Date | null;
revokedAt: Date | null;
createdAt: Date;
}
62 changes: 62 additions & 0 deletions apps/dashboard/src/app/settings/api-keys/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use client';

import { useState, useEffect } from 'react';
import { ApiKeyList } from '@/components/api-keys/ApiKeyList';
import { CreateKeyModal } from '@/components/api-keys/CreateKeyModal';
import { useApiKeys } from '@/hooks/useApiKeys';

export default function ApiKeysPage() {
const [showCreateModal, setShowCreateModal] = useState(false);
const [userId, setUserId] = useState('');
const { keys, loading, error, createKey, revokeKey, refetch } = useApiKeys(userId);

// Get userId from session/context in production
useEffect(() => {
// Placeholder: In production, get from auth context/session
const tempUserId = localStorage.getItem('userId') || 'user-123';
setUserId(tempUserId);
}, []);

const handleCreateKey = async (name: string) => {
await createKey(name);
setShowCreateModal(false);
refetch();
};

const handleRevokeKey = async (keyId: string) => {
if (confirm('Are you sure you want to revoke this API key? This action cannot be undone.')) {
await revokeKey(keyId);
refetch();
}
};

return (
<div className="max-w-4xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">API Keys</h1>
<p className="text-gray-400">Manage your API keys for programmatic access to Sentinel</p>
</div>

<div className="mb-6">
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
Create New Key
</button>
</div>

{error && (
<div className="p-4 bg-red-950/20 border border-red-900/60 text-red-400 rounded-lg mb-6">
{error}
</div>
)}

<ApiKeyList keys={keys} loading={loading} onRevoke={handleRevokeKey} />

{showCreateModal && (
<CreateKeyModal onClose={() => setShowCreateModal(false)} onCreate={handleCreateKey} />
)}
</div>
);
}
77 changes: 77 additions & 0 deletions apps/dashboard/src/components/api-keys/ApiKeyList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client';

interface ApiKey {
id: string;
name: string;
keyPreview: string;
createdAt: string;
lastUsedAt: string | null;
revokedAt: string | null;
}

interface ApiKeyListProps {
keys: ApiKey[];
loading: boolean;
onRevoke: (keyId: string) => void;
}

export function ApiKeyList({ keys, loading, onRevoke }: ApiKeyListProps) {
if (loading) {
return (
<div className="bg-gray-900 border border-gray-800 rounded-lg p-8 text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<p className="text-gray-400 mt-4">Loading API keys...</p>
</div>
);
}

if (keys.length === 0) {
return (
<div className="bg-gray-900 border border-gray-800 rounded-lg p-8 text-center">
<p className="text-gray-400">No API keys created yet</p>
<p className="text-gray-500 text-sm mt-2">Create your first API key to get started</p>
</div>
);
}

return (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-800">
<th className="px-4 py-3 text-left text-gray-300 font-semibold">Name</th>
<th className="px-4 py-3 text-left text-gray-300 font-semibold">Key</th>
<th className="px-4 py-3 text-left text-gray-300 font-semibold">Created</th>
<th className="px-4 py-3 text-left text-gray-300 font-semibold">Last Used</th>
<th className="px-4 py-3 text-left text-gray-300 font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{keys.map(key => (
<tr
key={key.id}
className="border-b border-gray-800 hover:bg-gray-800/50 transition-colors"
>
<td className="px-4 py-4 text-white">{key.name}</td>
<td className="px-4 py-4 text-gray-400 font-mono text-sm">****{key.keyPreview}</td>
<td className="px-4 py-4 text-gray-400 text-sm">
{new Date(key.createdAt).toLocaleDateString()}
</td>
<td className="px-4 py-4 text-gray-400 text-sm">
{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : 'Never'}
</td>
<td className="px-4 py-4">
<button
onClick={() => onRevoke(key.id)}
className="px-3 py-1 bg-red-900/20 hover:bg-red-900/40 text-red-400 rounded text-sm font-medium transition-colors"
>
Revoke
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Loading
Loading