Feat: Refactor postgress

This commit is contained in:
Till-JS 2025-11-27 02:25:37 +01:00
parent 046a0e3fe7
commit 98efa6f6e8
134 changed files with 9459 additions and 1904 deletions

View file

@ -0,0 +1,42 @@
{
"name": "@mana-core/nestjs-integration",
"version": "1.0.0",
"description": "NestJS integration package for Mana Core authentication and credits",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist"
},
"dependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/config": "^3.0.0 || ^4.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
"@nestjs/axios": "^3.0.0 || ^4.0.0",
"axios": "^1.6.0",
"jsonwebtoken": "^9.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/config": "^3.0.0 || ^4.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0"
},
"files": [
"dist"
]
}

View file

@ -0,0 +1,23 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export interface JwtPayload {
sub: string;
email: string;
role?: string;
app_id?: string;
iat?: number;
exp?: number;
}
export const CurrentUser = createParamDecorator(
(data: keyof JwtPayload | undefined, ctx: ExecutionContext): JwtPayload | string => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as JwtPayload;
if (data) {
return user[data] as string;
}
return user;
},
);

View file

@ -0,0 +1,22 @@
import { HttpException, HttpStatus } from '@nestjs/common';
export interface InsufficientCreditsDetails {
requiredCredits: number;
availableCredits: number;
creditType: 'user' | 'app';
operation: string;
}
export class InsufficientCreditsException extends HttpException {
constructor(details: InsufficientCreditsDetails) {
super(
{
statusCode: HttpStatus.PAYMENT_REQUIRED,
error: 'Insufficient Credits',
message: `Not enough credits for ${details.operation}. Required: ${details.requiredCredits}, Available: ${details.availableCredits}`,
details,
},
HttpStatus.PAYMENT_REQUIRED,
);
}
}

View file

@ -0,0 +1,73 @@
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
Inject,
Optional,
} from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@Optional()
@Inject(MANA_CORE_OPTIONS)
private readonly options?: ManaCoreModuleOptions,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No authorization token provided');
}
try {
// Decode the token to extract user information
// The actual verification happens at the Mana Core middleware level
const decoded = jwt.decode(token) as jwt.JwtPayload | null;
if (!decoded || !decoded.sub) {
throw new UnauthorizedException('Invalid token structure');
}
// Attach user info to request
request.user = {
sub: decoded.sub,
email: decoded.email || '',
role: decoded.role || 'user',
app_id: decoded.app_id,
iat: decoded.iat,
exp: decoded.exp,
};
// Store raw token for downstream services
request.accessToken = token;
if (this.options?.debug) {
console.log('[AuthGuard] User authenticated:', decoded.sub);
}
return true;
} catch (error) {
if (this.options?.debug) {
console.error('[AuthGuard] Token validation failed:', error);
}
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const authHeader = request.headers.authorization;
if (!authHeader) {
return undefined;
}
const [type, token] = authHeader.split(' ');
return type === 'Bearer' ? token : undefined;
}
}

View file

@ -0,0 +1,28 @@
// Module
export { ManaCoreModule, MANA_CORE_OPTIONS } from './mana-core.module';
// Interfaces
export {
ManaCoreModuleOptions,
ManaCoreModuleAsyncOptions,
ManaCoreOptionsFactory,
} from './interfaces/mana-core-options.interface';
// Guards
export { AuthGuard } from './guards/auth.guard';
// Decorators
export { CurrentUser, JwtPayload } from './decorators/current-user.decorator';
// Services
export {
CreditClientService,
CreditValidationResult,
CreditBalance,
} from './services/credit-client.service';
// Exceptions
export {
InsufficientCreditsException,
InsufficientCreditsDetails,
} from './exceptions/insufficient-credits.exception';

View file

@ -0,0 +1,20 @@
import { ModuleMetadata, Type } from '@nestjs/common';
export interface ManaCoreModuleOptions {
manaServiceUrl: string;
appId: string;
serviceKey?: string;
signupRedirectUrl?: string;
debug?: boolean;
}
export interface ManaCoreOptionsFactory {
createManaCoreOptions(): Promise<ManaCoreModuleOptions> | ManaCoreModuleOptions;
}
export interface ManaCoreModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<ManaCoreOptionsFactory>;
useClass?: Type<ManaCoreOptionsFactory>;
useFactory?: (...args: any[]) => Promise<ManaCoreModuleOptions> | ManaCoreModuleOptions;
inject?: any[];
}

View file

@ -0,0 +1,87 @@
import { DynamicModule, Module, Global, Provider } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';
import {
ManaCoreModuleOptions,
ManaCoreModuleAsyncOptions,
ManaCoreOptionsFactory,
} from './interfaces/mana-core-options.interface';
import { AuthGuard } from './guards/auth.guard';
import { CreditClientService } from './services/credit-client.service';
export const MANA_CORE_OPTIONS = 'MANA_CORE_OPTIONS';
@Global()
@Module({})
export class ManaCoreModule {
static forRoot(options: ManaCoreModuleOptions): DynamicModule {
return {
module: ManaCoreModule,
imports: [HttpModule],
providers: [
{
provide: MANA_CORE_OPTIONS,
useValue: options,
},
AuthGuard,
CreditClientService,
],
exports: [MANA_CORE_OPTIONS, AuthGuard, CreditClientService],
};
}
static forRootAsync(options: ManaCoreModuleAsyncOptions): DynamicModule {
const asyncProviders = this.createAsyncProviders(options);
return {
module: ManaCoreModule,
imports: [...(options.imports || []), HttpModule],
providers: [...asyncProviders, AuthGuard, CreditClientService],
exports: [MANA_CORE_OPTIONS, AuthGuard, CreditClientService],
};
}
private static createAsyncProviders(
options: ManaCoreModuleAsyncOptions,
): Provider[] {
if (options.useFactory) {
return [
{
provide: MANA_CORE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
},
];
}
const useClass = options.useClass;
const useExisting = options.useExisting;
if (useClass) {
return [
{
provide: MANA_CORE_OPTIONS,
useFactory: async (optionsFactory: ManaCoreOptionsFactory) =>
await optionsFactory.createManaCoreOptions(),
inject: [useClass],
},
{
provide: useClass,
useClass,
},
];
}
if (useExisting) {
return [
{
provide: MANA_CORE_OPTIONS,
useFactory: async (optionsFactory: ManaCoreOptionsFactory) =>
await optionsFactory.createManaCoreOptions(),
inject: [useExisting],
},
];
}
return [];
}
}

View file

@ -0,0 +1,182 @@
import { Injectable, Inject, Optional, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';
export interface CreditValidationResult {
hasCredits: boolean;
availableCredits: number;
requiredCredits?: number;
}
export interface CreditBalance {
balance: number;
freeCreditsRemaining: number;
totalEarned: number;
totalSpent: number;
}
@Injectable()
export class CreditClientService {
private readonly logger = new Logger(CreditClientService.name);
constructor(
@Optional()
@Inject(MANA_CORE_OPTIONS)
private readonly options?: ManaCoreModuleOptions,
@Optional()
private readonly httpService?: HttpService,
) {}
async validateCredits(
userId: string,
operation: string,
requiredAmount: number,
): Promise<CreditValidationResult> {
try {
const balance = await this.getBalance(userId);
const totalAvailable = balance.balance + balance.freeCreditsRemaining;
return {
hasCredits: totalAvailable >= requiredAmount,
availableCredits: totalAvailable,
requiredCredits: requiredAmount,
};
} catch (error) {
this.logger.error(`Failed to validate credits for user ${userId}:`, error);
// In case of error, we allow the operation to proceed
// The actual credit deduction will fail if there are no credits
return {
hasCredits: true,
availableCredits: 0,
requiredCredits: requiredAmount,
};
}
}
async getBalance(userId: string): Promise<CreditBalance> {
if (!this.httpService || !this.options?.manaServiceUrl) {
this.logger.warn('HTTP service or Mana service URL not configured, returning default balance');
return {
balance: 1000,
freeCreditsRemaining: 100,
totalEarned: 0,
totalSpent: 0,
};
}
try {
const response = await firstValueFrom(
this.httpService.get<CreditBalance>(
`${this.options.manaServiceUrl}/credits/balance/${userId}`,
{
headers: {
'X-Service-Key': this.options.serviceKey || '',
'X-App-Id': this.options.appId || '',
},
},
),
);
return response.data;
} catch (error) {
this.logger.error(`Failed to get balance for user ${userId}:`, error);
// Return default balance on error
return {
balance: 1000,
freeCreditsRemaining: 100,
totalEarned: 0,
totalSpent: 0,
};
}
}
async consumeCredits(
userId: string,
operation: string,
amount: number,
description: string,
metadata?: Record<string, any>,
): Promise<boolean> {
if (!this.httpService || !this.options?.manaServiceUrl) {
this.logger.warn('HTTP service or Mana service URL not configured, skipping credit consumption');
return true;
}
try {
await firstValueFrom(
this.httpService.post(
`${this.options.manaServiceUrl}/credits/use`,
{
userId,
amount,
appId: this.options.appId,
description,
metadata: {
operation,
...metadata,
},
},
{
headers: {
'X-Service-Key': this.options.serviceKey || '',
'X-App-Id': this.options.appId || '',
},
},
),
);
if (this.options.debug) {
this.logger.log(`Consumed ${amount} credits for user ${userId}: ${description}`);
}
return true;
} catch (error) {
this.logger.error(`Failed to consume credits for user ${userId}:`, error);
return false;
}
}
async refundCredits(
userId: string,
amount: number,
description: string,
metadata?: Record<string, any>,
): Promise<boolean> {
if (!this.httpService || !this.options?.manaServiceUrl) {
this.logger.warn('HTTP service or Mana service URL not configured, skipping credit refund');
return true;
}
try {
await firstValueFrom(
this.httpService.post(
`${this.options.manaServiceUrl}/credits/refund`,
{
userId,
amount,
appId: this.options.appId,
description,
metadata,
},
{
headers: {
'X-Service-Key': this.options.serviceKey || '',
'X-App-Id': this.options.appId || '',
},
},
),
);
if (this.options.debug) {
this.logger.log(`Refunded ${amount} credits for user ${userId}: ${description}`);
}
return true;
} catch (error) {
this.logger.error(`Failed to refund credits for user ${userId}:`, error);
return false;
}
}
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"lib": ["ES2021"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -15,6 +15,11 @@
"default": "./dist/nestjs/index.js"
}
},
"typesVersions": {
"*": {
"nestjs": ["./dist/nestjs/index.d.ts"]
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"type-check": "tsc --noEmit",