mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat(manadeck): complete Supabase removal from frontend and backend
- Frontend: Updated deckStore to use fetch API calls to backend - Frontend: Removed supabase.ts utility and @supabase/supabase-js dependency - Backend: Removed SupabaseService and supabase.service.ts - Backend: Updated validation schema to use DATABASE_URL - Backend: Updated health controller to remove Supabase check - Backend: Marked AI generation endpoint as not implemented (was using Edge Functions) Phase 4 of PostgreSQL/Drizzle migration complete. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1530efa936
commit
be9df4aa85
10 changed files with 87 additions and 553 deletions
|
|
@ -28,7 +28,6 @@
|
|||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"axios": "^1.7.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { AppService } from './app.service';
|
|||
import { ApiController } from './controllers/api.controller';
|
||||
import { PublicController } from './controllers/public.controller';
|
||||
import { HealthController } from './controllers/health.controller';
|
||||
import { SupabaseService } from './services/supabase.service';
|
||||
import { validationSchema } from './config/validation.schema';
|
||||
import {
|
||||
DatabaseModule,
|
||||
|
|
@ -65,7 +64,6 @@ import {
|
|||
],
|
||||
providers: [
|
||||
AppService,
|
||||
SupabaseService,
|
||||
// Database repositories
|
||||
DeckRepository,
|
||||
CardRepository,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ export const validationSchema = Joi.object({
|
|||
MANA_SUPABASE_SECRET_KEY: Joi.string().optional(),
|
||||
SIGNUP_REDIRECT_URL: Joi.string().uri().optional(),
|
||||
|
||||
// Your app's database
|
||||
SUPABASE_URL: Joi.string().uri().required(),
|
||||
SUPABASE_ANON_KEY: Joi.string().required(),
|
||||
SUPABASE_SERVICE_KEY: Joi.string().required(), // Required for edge functions
|
||||
// PostgreSQL Database
|
||||
DATABASE_URL: Joi.string().required(),
|
||||
|
||||
// JWT
|
||||
JWT_SECRET: Joi.string().optional(),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Logger, BadRequestException, Req } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Logger, BadRequestException, NotImplementedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
|
||||
import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
|
||||
import { CreditClientService } from '@mana-core/nestjs-integration';
|
||||
import { CreditOperationType, getCreditCost, getOperationDescription } from '../config/credit-operations';
|
||||
import { SupabaseService } from '../services/supabase.service';
|
||||
import { DeckRepository, CardRepository, UserStatsRepository } from '../database';
|
||||
|
||||
@Controller('api')
|
||||
|
|
@ -13,7 +12,6 @@ export class ApiController {
|
|||
|
||||
constructor(
|
||||
private readonly creditClient: CreditClientService,
|
||||
private readonly supabaseService: SupabaseService,
|
||||
private readonly deckRepository: DeckRepository,
|
||||
private readonly cardRepository: CardRepository,
|
||||
private readonly userStatsRepository: UserStatsRepository,
|
||||
|
|
@ -150,121 +148,15 @@ export class ApiController {
|
|||
}
|
||||
|
||||
@Post('decks/generate')
|
||||
async generateDeckWithAI(@CurrentUser() user: any, @Body() requestData: any, @Req() req: any) {
|
||||
this.logger.log(`Generating AI deck for user: ${user.sub}`);
|
||||
async generateDeckWithAI(@CurrentUser() user: any, @Body() requestData: any) {
|
||||
this.logger.log(`AI deck generation requested by user: ${user.sub}`);
|
||||
|
||||
const { prompt, deckTitle, deckDescription, cardCount = 10, cardTypes, difficulty, tags } = requestData;
|
||||
|
||||
// Validate required fields
|
||||
if (!prompt || !deckTitle) {
|
||||
throw new BadRequestException({
|
||||
error: 'validation_failed',
|
||||
message: 'prompt and deckTitle are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (cardCount < 1 || cardCount > 50) {
|
||||
throw new BadRequestException({
|
||||
error: 'validation_failed',
|
||||
message: 'cardCount must be between 1 and 50',
|
||||
});
|
||||
}
|
||||
|
||||
const operationType = CreditOperationType.AI_DECK_GENERATION;
|
||||
const creditCost = getCreditCost(operationType);
|
||||
|
||||
try {
|
||||
// 1. Pre-flight credit validation
|
||||
const validation = await this.creditClient.validateCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
);
|
||||
|
||||
if (!validation.hasCredits) {
|
||||
this.logger.warn(
|
||||
`User ${user.sub} has insufficient credits for AI deck generation. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
);
|
||||
|
||||
throw new BadRequestException({
|
||||
error: 'insufficient_credits',
|
||||
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
requiredCredits: creditCost,
|
||||
availableCredits: validation.availableCredits,
|
||||
operation: getOperationDescription(operationType),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Get the Mana token from the request
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
throw new BadRequestException({
|
||||
error: 'authentication_failed',
|
||||
message: 'No authorization token found',
|
||||
});
|
||||
}
|
||||
const manaToken = authHeader.replace('Bearer ', '');
|
||||
|
||||
// 3. Call the edge function via Supabase
|
||||
const result = await this.supabaseService.generateDeckWithAI(
|
||||
user.sub,
|
||||
{
|
||||
prompt,
|
||||
deckTitle,
|
||||
deckDescription,
|
||||
cardCount,
|
||||
cardTypes,
|
||||
difficulty,
|
||||
tags,
|
||||
},
|
||||
manaToken,
|
||||
);
|
||||
|
||||
// 4. Check if the edge function was successful
|
||||
if (!result.success) {
|
||||
throw new BadRequestException({
|
||||
error: 'deck_generation_failed',
|
||||
message: result.error || 'Failed to generate deck',
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Success - Consume credits
|
||||
await this.creditClient.consumeCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
`Generated AI deck: ${deckTitle}`,
|
||||
{
|
||||
deckId: result.deck?.id,
|
||||
deckTitle,
|
||||
cardCount: result.deck?.card_count,
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(`AI deck generated successfully for user ${user.sub}. ${creditCost} credits consumed.`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: user.sub,
|
||||
deck: result.deck,
|
||||
cards: result.cards,
|
||||
creditsUsed: creditCost,
|
||||
message: result.message || 'Deck generated successfully with AI',
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's already a BadRequestException, rethrow it
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log other errors
|
||||
this.logger.error(`Error generating AI deck for user ${user.sub}:`, error);
|
||||
throw new BadRequestException({
|
||||
error: 'deck_generation_failed',
|
||||
message: error.message || 'Failed to generate deck with AI',
|
||||
});
|
||||
}
|
||||
// TODO: Implement AI deck generation with a self-hosted solution
|
||||
// This endpoint previously used Supabase Edge Functions which are no longer available
|
||||
throw new NotImplementedException({
|
||||
error: 'not_implemented',
|
||||
message: 'AI deck generation is currently being migrated to a new infrastructure. Please check back later.',
|
||||
});
|
||||
}
|
||||
|
||||
@Put('decks/:id')
|
||||
|
|
|
|||
|
|
@ -20,11 +20,10 @@ export class HealthController {
|
|||
@HealthCheck()
|
||||
check() {
|
||||
const manaServiceUrl = this.configService.get<string>('MANA_SERVICE_URL')!;
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
|
||||
return this.health.check([
|
||||
() => this.http.pingCheck('mana-core', manaServiceUrl),
|
||||
() => this.http.pingCheck('supabase', `${supabaseUrl}/rest/v1/`),
|
||||
// PostgreSQL health is checked via database module initialization
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ async function bootstrap() {
|
|||
// Debug: Log environment variables before validation
|
||||
logger.log('=== Environment Variables Debug ===');
|
||||
logger.log(`APP_ID: ${process.env.APP_ID ? process.env.APP_ID.substring(0, 20) + '...' : 'NOT SET'}`);
|
||||
logger.log(`SUPABASE_URL: ${process.env.SUPABASE_URL || 'NOT SET'}`);
|
||||
logger.log(`DATABASE_URL: ${process.env.DATABASE_URL ? '[SET]' : 'NOT SET'}`);
|
||||
logger.log(`MANA_SERVICE_URL: ${process.env.MANA_SERVICE_URL || 'NOT SET'}`);
|
||||
logger.log('===================================');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,283 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
@Injectable()
|
||||
export class SupabaseService {
|
||||
private readonly logger = new Logger(SupabaseService.name);
|
||||
private supabase: SupabaseClient;
|
||||
private supabaseServiceRole: SupabaseClient;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
const supabaseAnonKey = this.configService.get<string>('SUPABASE_ANON_KEY')!;
|
||||
const supabaseServiceKey = this.configService.get<string>('SUPABASE_SERVICE_KEY');
|
||||
|
||||
// Client for public operations
|
||||
this.supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Client for service-level operations (optional, only if service key is provided)
|
||||
if (supabaseServiceKey) {
|
||||
this.supabaseServiceRole = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('Supabase service initialized');
|
||||
}
|
||||
|
||||
// Get client with user's token for RLS
|
||||
getClientWithUserToken(token: string): SupabaseClient {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
const supabaseAnonKey = this.configService.get<string>('SUPABASE_ANON_KEY')!;
|
||||
|
||||
return createClient(supabaseUrl, supabaseAnonKey, {
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Example methods for deck operations
|
||||
async getUserDecks(userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching user decks:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createDeck(userId: string, deckData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.insert({
|
||||
...deckData,
|
||||
user_id: userId,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error creating deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateDeck(deckId: string, userId: string, deckData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.update({
|
||||
...deckData,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.eq('id', deckId)
|
||||
.eq('user_id', userId) // Ensure user owns the deck
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error updating deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async deleteDeck(deckId: string, userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { error } = await client
|
||||
.from('decks')
|
||||
.delete()
|
||||
.eq('id', deckId)
|
||||
.eq('user_id', userId); // Ensure user owns the deck
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error deleting deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Example methods for card operations
|
||||
async getUserCards(userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('cards')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching user cards:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createCard(userId: string, cardData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('cards')
|
||||
.insert({
|
||||
...cardData,
|
||||
user_id: userId,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error creating card:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Public methods (no auth required)
|
||||
async getFeaturedDecks(limit = 10) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('decks')
|
||||
.select('*')
|
||||
.eq('is_featured', true)
|
||||
.eq('is_public', true)
|
||||
.limit(limit)
|
||||
.order('featured_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching featured decks:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getLeaderboard(limit = 10) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('user_stats')
|
||||
.select('*')
|
||||
.order('total_wins', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching leaderboard:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getDeckTemplates(category?: string) {
|
||||
let query = this.supabase
|
||||
.from('deck_templates')
|
||||
.select('*')
|
||||
.eq('is_active', true);
|
||||
|
||||
if (category) {
|
||||
query = query.eq('category', category);
|
||||
}
|
||||
|
||||
const { data, error } = await query.order('popularity', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching deck templates:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Service-level operations (using service role key)
|
||||
async adminGetAllUsers() {
|
||||
if (!this.supabaseServiceRole) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabaseServiceRole.auth.admin.listUsers();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching all users:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Edge Function invocations
|
||||
async generateDeckWithAI(
|
||||
userId: string,
|
||||
requestData: {
|
||||
prompt: string;
|
||||
deckTitle: string;
|
||||
deckDescription?: string;
|
||||
cardCount?: number;
|
||||
cardTypes?: string[];
|
||||
difficulty?: string;
|
||||
tags?: string[];
|
||||
},
|
||||
manaToken: string,
|
||||
) {
|
||||
if (!this.supabaseServiceRole) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
|
||||
this.logger.log(`Invoking generate-deck edge function for user ${userId}`);
|
||||
|
||||
const { data, error } = await this.supabaseServiceRole.functions.invoke(
|
||||
'generate-deck',
|
||||
{
|
||||
body: requestData,
|
||||
headers: {
|
||||
Authorization: `Bearer ${manaToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error invoking generate-deck edge function:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue