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
6 changes: 5 additions & 1 deletion backend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
}],
},
};
39 changes: 39 additions & 0 deletions backend/src/api-keys/api-key.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
} from 'typeorm';

@Entity('api_keys')
export class ApiKey {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
userId: string;

@Column()
name: string;

@Column()
keyHash: string;

@Column({ length: 8 })
prefix: string;

@Column('text', { array: true, default: [] })
scopes: string[];

@Column({ nullable: true, type: 'timestamp' })
lastUsedAt: Date;

@Column({ nullable: true, type: 'timestamp' })
expiresAt: Date;

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;
}
41 changes: 41 additions & 0 deletions backend/src/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,
Param,
Body,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiKeysService } from './api-keys.service';

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

@Post()
create(
@Body() body: { name: string; scopes: string[]; expiresAt?: Date },
@Req() req: any,
) {
return this.apiKeysService.create(
req.user?.id,
body.name,
body.scopes,
body.expiresAt,
);
}

@Get()
findAll(@Req() req: any) {
return this.apiKeysService.findByUser(req.user?.id);
}

@Delete(':id')
revoke(@Param('id') id: string, @Req() req: any) {
return this.apiKeysService.revoke(id, req.user?.id);
}
}
13 changes: 13 additions & 0 deletions backend/src/api-keys/api-keys.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApiKey } from './api-key.entity';
import { ApiKeysService } from './api-keys.service';
import { ApiKeysController } from './api-keys.controller';

@Module({
imports: [TypeOrmModule.forFeature([ApiKey])],
controllers: [ApiKeysController],
providers: [ApiKeysService],
exports: [ApiKeysService],
})
export class ApiKeysModule {}
65 changes: 65 additions & 0 deletions backend/src/api-keys/api-keys.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import * as bcrypt from 'bcrypt';
import { ApiKey } from './api-key.entity';

@Injectable()
export class ApiKeysService {
constructor(
@InjectRepository(ApiKey)
private readonly repo: Repository<ApiKey>,
) {}

async create(
userId: string,
name: string,
scopes: string[],
expiresAt?: Date,
) {
const rawKey = `ak_${crypto.randomBytes(32).toString('hex')}`;
const prefix = rawKey.substring(3, 11);
const keyHash = await bcrypt.hash(rawKey, 10);
const entity = this.repo.create({
userId,
name,
keyHash,
prefix,
scopes,
expiresAt,
});
const saved = await this.repo.save(entity);
return {
id: saved.id,
name: saved.name,
prefix: saved.prefix,
scopes: saved.scopes,
key: rawKey,
createdAt: saved.createdAt,
};
}

async findByUser(userId: string) {
return this.repo.find({
where: { userId, isActive: true },
select: [
'id',
'name',
'prefix',
'scopes',
'lastUsedAt',
'expiresAt',
'createdAt',
],
});
}

async revoke(id: string, userId: string) {
const key = await this.repo.findOne({ where: { id, userId } });
if (!key) throw new NotFoundException('API key not found');
key.isActive = false;
await this.repo.save(key);
return { message: 'API key revoked' };
}
}
18 changes: 18 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { ContractsModule } from './contracts/contracts.module';
import { LicensesModule } from './licenses/licenses.module';
import { PurchaseOrdersModule } from './purchase-orders/purchase-orders.module';
import { TasksModule } from './tasks/tasks.module';
import { ApiKeysModule } from './api-keys/api-keys.module';
import { StellarModule } from './stellar/stellar.module';
import { NotificationModule } from './notifications/notification.module';

@Module({
Expand Down Expand Up @@ -76,6 +78,20 @@ import { NotificationModule } from './notifications/notification.module';
},
}),

ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),

I18nModule.forRoot({
fallbackLanguage: 'en',
loaderOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
},
resolvers: [
{ use: QueryResolver, options: ['lang'] },
AcceptLanguageResolver,
],
}),

AssetsExtendedModule,
AssetsOpsModule,
CheckinModule,
Expand All @@ -94,6 +110,8 @@ import { NotificationModule } from './notifications/notification.module';
InventoryModule,
VendorsModule,
DashboardModule,
ApiKeysModule,
StellarModule,
NotificationModule,
],
controllers: [AppController],
Expand Down
2 changes: 1 addition & 1 deletion backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
Expand Down
1 change: 0 additions & 1 deletion backend/src/auth/providers/two-factor.provider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class TwoFactorProvider {
async generateSecret(_email: string) {}
Expand Down
3 changes: 2 additions & 1 deletion backend/src/common/filters/global-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {
: 'Internal server error';

this.logger.error(
`${request.method} ${request.url} - ${exception instanceof Error ? exception.stack : String(exception)}`,
`${request.method} - ${request.url}`,
exception instanceof Error ? exception.stack : String(exception),
);

response.status(status).json({
Expand Down
6 changes: 3 additions & 3 deletions backend/src/contracts/contracts.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
Expand All @@ -22,13 +22,13 @@ export class ContractsService {
);
}

async create(dto: CreateContractDto, userId?: string): Promise<Contract> {
async create(dto: CreateContractDto, _userId?: string): Promise<Contract> {
const prefix = this.configService.get<string>('CONTRACT_ID_PREFIX', 'CTR');
const contractId = `${prefix}-${this.nextNumber++}`;
const contract = this.contractRepository.create({
...dto,
contractId,
createdById: userId,
createdById: _userId,
});
return this.contractRepository.save(contract);
}
Expand Down
6 changes: 3 additions & 3 deletions backend/src/licenses/licenses.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { License } from './entities/license.entity';
Expand All @@ -13,12 +13,12 @@ export class LicensesService {
private readonly licenseRepository: Repository<License>,
) {}

async create(dto: CreateLicenseDto, userId?: string): Promise<License> {
async create(dto: CreateLicenseDto, _userId?: string): Promise<License> {
const licenseKey = `LIC-${Date.now()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
const license = this.licenseRepository.create({
...dto,
licenseKey,
createdById: userId,
createdById: _userId,
});
return this.licenseRepository.save(license);
}
Expand Down
4 changes: 2 additions & 2 deletions backend/src/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';

Expand Down Expand Up @@ -175,7 +175,7 @@ export class MailService {
const render = templates[template];
if (!render) {
this.logger.warn(`Unknown email template: ${template}`);
return `<p>Email notification</p>`;
return `<p>Unknown template: ${template}</p>`;
}
return render(context);
}
Expand Down
94 changes: 94 additions & 0 deletions backend/src/stellar/stellar.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
Controller,
Get,
Post,
Body,
Param,
UseGuards,
Req,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { StellarService } from './stellar.service';

@Controller('stellar')
@UseGuards(AuthGuard('jwt'))
export class StellarController {
constructor(
private readonly stellarService: StellarService,
@InjectDataSource() private readonly dataSource: DataSource,
) {}

@Post('assets/register')
async registerAsset(
@Body() body: { assetId: string; metadata?: Record<string, string> },
) {
return this.stellarService.registerAsset(body.assetId, body.metadata ?? {});
}

@Get('assets/:id')
async getAsset(@Param('id') id: string) {
const rows = await this.dataSource.query<{ id: string }[]>(
'SELECT id, "assetId", name FROM assets WHERE id = $1',
[id],
);
return rows[0] ?? null;
}

@Post('assets/:id/transfer')
async transferAsset(
@Param('id') id: string,
@Body() body: { toAddress: string; amount: number },
) {
return this.stellarService.transferAsset(id, body.toAddress, body.amount);
}

@Post('assets/:id/tokenize')
async tokenizeAsset(
@Param('id') id: string,
@Body() body: { totalSupply: number; tokenName: string },
) {
return this.stellarService.tokenizeAsset(
id,
body.totalSupply,
body.tokenName,
);
}

@Get('assets/:id/tokens')
async getTokens(@Param('id') id: string, @Req() req: any) {
const balance = await this.stellarService.getTokenBalance(
id,
String(req.user?.id),
);
return { assetId: id, balance };
}

@Post('assets/:id/tokens/transfer')
async transferTokens(
@Param('id') id: string,
@Body() body: { toAddress: string; amount: number },
@Req() req: any,
) {
return this.stellarService.transferTokens(
id,
String(req.user?.id),
body.toAddress,
body.amount,
);
}

@Post('assets/:id/tokens/lock')
async lockTokens(@Param('id') id: string, @Body() body: { amount: number }) {
return this.stellarService.lockTokens(id, body.amount);
}

@Post('assets/:id/tokens/unlock')
async unlockTokens(
@Param('id') id: string,
@Body() body: { amount: number },
) {
return this.stellarService.unlockTokens(id, body.amount);
}
}
Loading
Loading