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
16 changes: 16 additions & 0 deletions PROJECT_KNOWLEDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@
- The project appears to be a marketplace or currency rate tracking application ("OtraAppDelDolarVzla" directory name suggests dollar rates, "AKomo" is the new branding).
- **Recent Focus**: UI/UX enhancements, responsive design, and branding updates (renaming to AKomo).
- **Backend Features**: Includes scripts for synchronizing currency logic (Binance, BCV).
- **Authentication**: Project-based API Key system using SHA-256 hashing.

## Key Management

### Project Creation
Authorized clients (like the mobile app) must have a project entry in Supabase and use an `x-api-key` header.

To create a new project:
```bash
pnpm create:project "Project Name" [--test]
```

### Authentication Logic
- **Header**: `x-api-key`
- **Global Guard**: `ProjectAuthGuard` enforces validation on all routes except those marked with `@IsPublic()`.
- **Database Table**: `projects` (uuid, name, key_hash, prefix, is_active, created_at, last_used_at).

## Rules & Standards
- **Package Manager**: `pnpm` (Global preference). `npm` used specifically for syncing mobile `package-lock.json` if needed.
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"sync:binance": "ts-node src/scripts/sync-binance.ts",
"sync:bcv": "ts-node src/scripts/sync-bcv.ts"
"sync:bcv": "ts-node src/scripts/sync-bcv.ts",
"create:project": "ts-node src/scripts/create-project.ts"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { IsPublic } from './auth/is-public.decorator';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@IsPublic()
@Get()
getHello(): string {
return this.appService.getHello();
Expand Down
12 changes: 11 additions & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@ import { AppService } from './app.service';
import { ExchangeRatesModule } from './exchange-rates/exchange-rates.module';
import { SupabaseModule } from './supabase/supabase.module';
import { BuildsModule } from './builds/builds.module';
import { AuthModule } from './auth/auth.module';
import { APP_GUARD } from '@nestjs/core';
import { ProjectAuthGuard } from './auth/project-auth.guard';

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
SupabaseModule,
ExchangeRatesModule,
BuildsModule,
AuthModule,
],
controllers: [AppController],
providers: [AppService],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: ProjectAuthGuard,
},
],
})
export class AppModule {}
10 changes: 10 additions & 0 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SupabaseModule } from '../supabase/supabase.module';
import { ProjectAuthGuard } from './project-auth.guard';

@Module({
imports: [SupabaseModule],
providers: [ProjectAuthGuard],
exports: [ProjectAuthGuard],
})
export class AuthModule {}
4 changes: 4 additions & 0 deletions backend/src/auth/is-public.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const IsPublic = () => SetMetadata(IS_PUBLIC_KEY, true);
66 changes: 66 additions & 0 deletions backend/src/auth/project-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { createHash } from 'crypto';
import { SupabaseService } from '../supabase/supabase.service';
import { IS_PUBLIC_KEY } from './is-public.decorator';

@Injectable()
export class ProjectAuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private supabaseService: SupabaseService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);

if (isPublic) {
return true;
}

const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'];

if (!apiKey) {
throw new UnauthorizedException('API key is missing');
}

// Hash the incoming key for comparison
const keyHash = createHash('sha256').update(apiKey).digest('hex');

const supabase = this.supabaseService.getClient();
const { data: project, error } = await supabase
.from('projects')
.select('id, name, is_active')
.eq('key_hash', keyHash)
.single();

if (error || !project) {
throw new UnauthorizedException('Invalid API key');
}

if (!project.is_active) {
throw new UnauthorizedException('Project is inactive');
}

// Optional: Attach project info to request
request['project'] = project;

// Update last used timestamp (non-blocking)
supabase
.from('projects')
.update({ last_used_at: new Date().toISOString() })
.eq('id', project.id)
.then();

return true;
}
}
61 changes: 61 additions & 0 deletions backend/src/scripts/create-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../app.module';
import { SupabaseService } from '../supabase/supabase.service';
import { createHash, randomBytes } from 'crypto';

async function bootstrap() {
const args = process.argv.slice(2);
const projectName = args[0];
const type = args[1] === '--test' ? 'test' : 'live';

if (!projectName) {
console.error('Usage: pnpm ts-node src/scripts/create-project.ts "Project Name" [--test]');
process.exit(1);
}

const app = await NestFactory.createApplicationContext(AppModule);
const supabaseService = app.get(SupabaseService);
const supabase = supabaseService.getClient();

try {
// 1. Generate a random secure key
const randomPart = randomBytes(24).toString('base64url');
const prefix = `ak_${type}`;
const rawKey = `${prefix}_${randomPart}`;

// 2. Hash the key for database storage
const keyHash = createHash('sha256').update(rawKey).digest('hex');

// 3. Save to Supabase (assuming the table exists)
const { data, error } = await supabase
.from('projects')
.insert([
{
name: projectName,
key_hash: keyHash,
prefix: prefix,
is_active: true,
},
])
.select();

if (error) {
throw new Error(`Failed to create project: ${error.message}`);
}

console.log('\n--- Project Created Successfully ---');
console.log(`Project Name: ${projectName}`);
console.log(`Prefix: ${prefix}`);
console.log('------------------------------------');
console.log('IMPORTANT: Copy the following API Key. It will NOT be shown again!');
console.log(`\nAPI KEY: ${rawKey}\n`);
console.log('------------------------------------');
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
} finally {
await app.close();
}
}

bootstrap();
Loading