mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 01:26:42 +02:00
Feat: Refactor postgress
This commit is contained in:
parent
046a0e3fe7
commit
98efa6f6e8
134 changed files with 9459 additions and 1904 deletions
42
packages/mana-core-nestjs-integration/package.json
Normal file
42
packages/mana-core-nestjs-integration/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
28
packages/mana-core-nestjs-integration/src/index.ts
Normal file
28
packages/mana-core-nestjs-integration/src/index.ts
Normal 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';
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
packages/mana-core-nestjs-integration/tsconfig.json
Normal file
21
packages/mana-core-nestjs-integration/tsconfig.json
Normal 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"]
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue