mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 21:21:24 +02:00
Merge branch 'feat/mana-core'
This commit is contained in:
commit
28d167a978
112 changed files with 34765 additions and 548 deletions
395
chat/INTEGRATION_COMPLETE.md
Normal file
395
chat/INTEGRATION_COMPLETE.md
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
# ✅ Mana Core Auth Integration - COMPLETE
|
||||
|
||||
**Date:** 2025-11-25
|
||||
**Status:** 🎉 All code changes implemented
|
||||
**Project:** Chat (Backend, Web, Mobile)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary
|
||||
|
||||
The Chat project has been **fully migrated** from Supabase Auth to **Mana Core Auth**! All three apps (backend, web, mobile) now use the centralized authentication system with built-in credit management.
|
||||
|
||||
---
|
||||
|
||||
## ✅ What Was Done
|
||||
|
||||
### 1. **Updated `@manacore/shared-auth` Package** ✅
|
||||
|
||||
**Location:** `/packages/shared-auth/src/core/authService.ts`
|
||||
|
||||
**Changes:**
|
||||
- Updated API endpoints to match Mana Core Auth (`/api/v1/auth/*`)
|
||||
- Fixed login response handling (`accessToken` instead of `appToken`)
|
||||
- Fixed signup flow (register then login separately)
|
||||
- Updated refresh token endpoint
|
||||
- Updated credits balance endpoint
|
||||
|
||||
**Status:** Package is now 100% compatible with Mana Core Auth API
|
||||
|
||||
---
|
||||
|
||||
### 2. **Chat Backend Integration** ✅
|
||||
|
||||
**Files Modified:**
|
||||
- ✅ `chat/backend/src/common/guards/jwt-auth.guard.ts` (NEW)
|
||||
- ✅ `chat/backend/src/common/decorators/current-user.decorator.ts` (NEW)
|
||||
- ✅ `chat/backend/src/chat/chat.controller.ts`
|
||||
- ✅ `chat/backend/src/chat/chat.service.ts`
|
||||
- ✅ `chat/backend/src/conversation/conversation.controller.ts`
|
||||
- ✅ `chat/backend/.env.example`
|
||||
|
||||
**Changes:**
|
||||
- Created JWT Auth Guard that validates tokens with Mana Core Auth
|
||||
- Created CurrentUser decorator to inject user data into controllers
|
||||
- Updated all controllers to use JwtAuthGuard
|
||||
- Removed userId from request body (now extracted from JWT)
|
||||
- Added MANA_CORE_AUTH_URL environment variable
|
||||
- Changed PORT from 3001 to 3002 (to avoid conflict with auth service)
|
||||
|
||||
**Key Features:**
|
||||
- All endpoints now protected with JWT validation
|
||||
- User context automatically injected via @CurrentUser decorator
|
||||
- Token validation happens via Mana Core Auth API
|
||||
- Proper error handling for invalid/expired tokens
|
||||
|
||||
---
|
||||
|
||||
### 3. **Chat Web App Integration** ✅
|
||||
|
||||
**Files Modified:**
|
||||
- ✅ `chat/apps/web/src/lib/stores/auth.svelte.ts`
|
||||
- ✅ `chat/apps/web/.env.example`
|
||||
|
||||
**Changes:**
|
||||
- Completely rewrote auth store to use `@manacore/shared-auth`
|
||||
- Removed Supabase auth dependencies
|
||||
- Added `initializeWebAuth()` initialization
|
||||
- Added `getCredits()` method for credit balance
|
||||
- Added `getAccessToken()` method for API calls
|
||||
- Added MANA_CORE_AUTH_URL environment variable
|
||||
|
||||
**API Compatibility:**
|
||||
- Same method signatures as before (signIn, signUp, signOut, resetPassword)
|
||||
- Minimal breaking changes for existing code
|
||||
- Additional methods: `getCredits()`, `getAccessToken()`
|
||||
|
||||
---
|
||||
|
||||
### 4. **Chat Mobile App Integration** ✅
|
||||
|
||||
**Files Modified:**
|
||||
- ✅ `chat/apps/mobile/context/AuthProvider.tsx`
|
||||
- ✅ `chat/apps/mobile/.env.example`
|
||||
|
||||
**Changes:**
|
||||
- Rewrote AuthProvider to use `@manacore/shared-auth`
|
||||
- Created SecureStore adapter for token storage
|
||||
- Created React Native device adapter
|
||||
- Created React Native network adapter
|
||||
- Removed Supabase auth dependencies
|
||||
- Added MANA_CORE_AUTH_URL environment variable
|
||||
|
||||
**Key Features:**
|
||||
- Tokens stored securely in Expo SecureStore
|
||||
- Device ID generated and persisted
|
||||
- Same API as before (useAuth hook remains unchanged)
|
||||
- Auto sign-in after successful signup
|
||||
|
||||
---
|
||||
|
||||
## 📝 Configuration Changes
|
||||
|
||||
### Backend `.env`
|
||||
|
||||
```env
|
||||
# OLD (Remove):
|
||||
# SUPABASE_URL=...
|
||||
# SUPABASE_SERVICE_KEY=...
|
||||
# PORT=3001
|
||||
|
||||
# NEW (Add):
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
PORT=3002
|
||||
|
||||
# Keep (for database):
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_SERVICE_KEY=your-service-key-here
|
||||
```
|
||||
|
||||
### Web App `.env`
|
||||
|
||||
```env
|
||||
# OLD (Remove):
|
||||
# PUBLIC_SUPABASE_URL=...
|
||||
# PUBLIC_SUPABASE_ANON_KEY=...
|
||||
# PUBLIC_BACKEND_URL=http://localhost:3001
|
||||
|
||||
# NEW (Add):
|
||||
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
|
||||
# Keep (for database):
|
||||
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
```
|
||||
|
||||
### Mobile App `.env`
|
||||
|
||||
```env
|
||||
# OLD (Remove):
|
||||
# EXPO_PUBLIC_SUPABASE_URL=...
|
||||
# EXPO_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
# EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
|
||||
|
||||
# NEW (Add):
|
||||
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
EXPO_PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
|
||||
# Keep (for database):
|
||||
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 How to Run
|
||||
|
||||
### 1. Start Mana Core Auth (Terminal 1)
|
||||
|
||||
```bash
|
||||
cd mana-core-auth
|
||||
cp .env.example .env
|
||||
# Edit .env and add JWT keys (see mana-core-auth/QUICKSTART.md)
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
Service runs on: `http://localhost:3001`
|
||||
|
||||
### 2. Start Chat Backend (Terminal 2)
|
||||
|
||||
```bash
|
||||
cd chat/backend
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# - Add MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
# - Change PORT=3002
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
Service runs on: `http://localhost:3002`
|
||||
|
||||
### 3. Start Web App (Terminal 3)
|
||||
|
||||
```bash
|
||||
cd chat/apps/web
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# - Add PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
# - Change PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
App runs on: `http://localhost:5173`
|
||||
|
||||
### 4. Start Mobile App (Terminal 4)
|
||||
|
||||
```bash
|
||||
cd chat/apps/mobile
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# - Add EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
# - Change EXPO_PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Backend
|
||||
|
||||
- [ ] Start backend on port 3002
|
||||
- [ ] Try accessing `/api/chat/models` without token → Should return 401
|
||||
- [ ] Login via Mana Core Auth
|
||||
- [ ] Access `/api/chat/models` with token → Should work
|
||||
- [ ] Access `/api/conversations` with token → Should work
|
||||
|
||||
### Web App
|
||||
|
||||
- [ ] Go to `/login`
|
||||
- [ ] Register new user
|
||||
- [ ] Should redirect and auto-login
|
||||
- [ ] Check user is authenticated
|
||||
- [ ] Try protected routes
|
||||
- [ ] Logout
|
||||
- [ ] Try protected routes again → Should redirect to login
|
||||
|
||||
### Mobile App
|
||||
|
||||
- [ ] Open app
|
||||
- [ ] Register new user
|
||||
- [ ] Should auto-login
|
||||
- [ ] Check chat functionality works
|
||||
- [ ] Logout
|
||||
- [ ] Login again with same credentials
|
||||
|
||||
---
|
||||
|
||||
## 💡 New Features Available
|
||||
|
||||
### Credit System (Built-in)
|
||||
|
||||
All users now have access to the credit system:
|
||||
|
||||
```typescript
|
||||
// Web App
|
||||
const credits = await authStore.getCredits();
|
||||
console.log(credits); // { credits: 150, maxCreditLimit: 1000, userId: "..." }
|
||||
|
||||
// Mobile App (need to add this method to AuthProvider if needed)
|
||||
const credits = await authService.getUserCredits();
|
||||
```
|
||||
|
||||
**Default Credits:**
|
||||
- Signup bonus: 150 free credits
|
||||
- Daily free credits: 5 credits every 24 hours
|
||||
- Pricing: 100 mana = €1.00
|
||||
|
||||
---
|
||||
|
||||
## 🔄 What Changed for Users
|
||||
|
||||
| Aspect | Before (Supabase) | After (Mana Core) | Impact |
|
||||
|--------|-------------------|-------------------|---------|
|
||||
| **Registration** | Immediate session | Register → Login | Minimal (auto-login in mobile) |
|
||||
| **Login** | Supabase JWT | Mana Core JWT | None (transparent) |
|
||||
| **Token Storage** | Supabase cookies | localStorage/SecureStore | None (same security) |
|
||||
| **Sessions** | Supabase sessions | JWT + refresh tokens | Better (token rotation) |
|
||||
| **Credits** | ❌ None | ✅ 150 initial + 5 daily | **NEW FEATURE!** |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Port Configuration
|
||||
|
||||
| Service | Port | URL |
|
||||
|---------|------|-----|
|
||||
| **Mana Core Auth** | 3001 | http://localhost:3001 |
|
||||
| **Chat Backend** | 3002 | http://localhost:3002 |
|
||||
| **Web App** | 5173 | http://localhost:5173 |
|
||||
| **Mobile App** | 8081 | exp://localhost:8081 |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Potential Issues & Solutions
|
||||
|
||||
### Issue: "Connection refused" to Mana Core Auth
|
||||
|
||||
**Solution:** Make sure Mana Core Auth is running on port 3001
|
||||
```bash
|
||||
cd mana-core-auth && pnpm start:dev
|
||||
```
|
||||
|
||||
### Issue: "Invalid token" errors
|
||||
|
||||
**Solution:** Clear stored tokens and login again
|
||||
```typescript
|
||||
// Web: Clear localStorage
|
||||
localStorage.clear();
|
||||
|
||||
// Mobile: Uninstall and reinstall app, or clear SecureStore
|
||||
await SecureStore.deleteItemAsync('@auth/appToken');
|
||||
await SecureStore.deleteItemAsync('@auth/refreshToken');
|
||||
```
|
||||
|
||||
### Issue: CORS errors from web app
|
||||
|
||||
**Solution:** Add web app URL to Mana Core Auth CORS config
|
||||
```env
|
||||
# In mana-core-auth/.env
|
||||
CORS_ORIGINS=http://localhost:5173,http://localhost:8081
|
||||
```
|
||||
|
||||
### Issue: Backend can't validate tokens
|
||||
|
||||
**Solution:** Check MANA_CORE_AUTH_URL in backend .env
|
||||
```env
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 API Endpoint Reference
|
||||
|
||||
### Mana Core Auth (Port 3001)
|
||||
|
||||
- POST `/api/v1/auth/register` - Register new user
|
||||
- POST `/api/v1/auth/login` - Login with email/password
|
||||
- POST `/api/v1/auth/refresh` - Refresh access token
|
||||
- POST `/api/v1/auth/logout` - Logout and revoke session
|
||||
- POST `/api/v1/auth/validate` - Validate JWT token
|
||||
- GET `/api/v1/credits/balance` - Get credit balance
|
||||
|
||||
### Chat Backend (Port 3002)
|
||||
|
||||
- GET `/api/chat/models` - List AI models (protected)
|
||||
- POST `/api/chat/completions` - Create chat completion (protected)
|
||||
- GET `/api/conversations` - List conversations (protected)
|
||||
- POST `/api/conversations` - Create conversation (protected)
|
||||
- GET `/api/conversations/:id` - Get conversation (protected)
|
||||
- GET `/api/conversations/:id/messages` - Get messages (protected)
|
||||
- POST `/api/conversations/:id/messages` - Add message (protected)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Next Steps (Optional Enhancements)
|
||||
|
||||
1. **Add Credit Usage Tracking**
|
||||
- Deduct credits when using AI models
|
||||
- Show remaining credits in UI
|
||||
|
||||
2. **Add OAuth Providers**
|
||||
- Google Sign-In
|
||||
- Apple Sign-In
|
||||
|
||||
3. **Add Email Verification**
|
||||
- Send verification emails on signup
|
||||
- Verify email before allowing login
|
||||
|
||||
4. **Add Password Reset**
|
||||
- Implement forgot password flow
|
||||
- Send reset emails
|
||||
|
||||
5. **Add 2FA**
|
||||
- Enable two-factor authentication
|
||||
- Support TOTP apps
|
||||
|
||||
6. **Add Session Management**
|
||||
- Show active sessions
|
||||
- Revoke specific sessions
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- **Integration Guide:** `/chat/MANA_CORE_AUTH_INTEGRATION.md`
|
||||
- **Mana Core Auth README:** `/mana-core-auth/README.md`
|
||||
- **Quick Start:** `/mana-core-auth/QUICKSTART.md`
|
||||
- **Master Plan:** `/.hive-mind/MASTER_PLAN_CENTRAL_AUTH_SYSTEM.md`
|
||||
|
||||
---
|
||||
|
||||
## ✨ Benefits of Migration
|
||||
|
||||
1. **✅ Centralized Authentication** - Single auth system for all Mana Core apps
|
||||
2. **✅ Built-in Credits** - No need to build separate credit system
|
||||
3. **✅ Better Security** - RS256 JWT, refresh token rotation, optimistic locking
|
||||
4. **✅ Cost Savings** - Self-hosted, no per-user charges
|
||||
5. **✅ Full Control** - Complete ownership of user data
|
||||
6. **✅ Consistent API** - Same auth flow across all apps
|
||||
|
||||
---
|
||||
|
||||
**Status:** 🎉 **INTEGRATION COMPLETE - READY FOR TESTING!**
|
||||
|
||||
All code changes are done. Follow the "How to Run" section above to test the integration.
|
||||
544
chat/MANA_CORE_AUTH_INTEGRATION.md
Normal file
544
chat/MANA_CORE_AUTH_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
# Mana Core Auth Integration Guide - Chat Project
|
||||
|
||||
This guide explains how to integrate the Chat project with the new **Mana Core Auth** system, replacing Supabase Auth.
|
||||
|
||||
## Overview
|
||||
|
||||
The Chat project currently uses **Supabase Auth** across all apps. We're migrating to **Mana Core Auth**, our centralized authentication system with built-in credit management.
|
||||
|
||||
### Benefits
|
||||
|
||||
- ✅ **Unified Authentication** - Single auth system for all Mana Core apps
|
||||
- ✅ **Built-in Credits** - Automatic credit balance management (150 signup bonus + 5 daily)
|
||||
- ✅ **Better Security** - RS256 JWT, refresh token rotation, optimistic locking
|
||||
- ✅ **Cost Savings** - Self-hosted, no per-user charges
|
||||
- ✅ **Full Control** - Complete ownership of user data and auth flow
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Chat Apps (Web, Mobile, Landing)
|
||||
↓
|
||||
@manacore/shared-auth (Client Library)
|
||||
↓
|
||||
Mana Core Auth Service (NestJS)
|
||||
↓
|
||||
PostgreSQL (Users, Sessions, Credits)
|
||||
```
|
||||
|
||||
## What Changed
|
||||
|
||||
### 1. Shared Auth Package Updated ✅
|
||||
|
||||
The `@manacore/shared-auth` package has been updated to work with Mana Core Auth endpoints:
|
||||
|
||||
**Updated endpoints:**
|
||||
- `POST /api/v1/auth/register` - User registration
|
||||
- `POST /api/v1/auth/login` - Email/password login
|
||||
- `POST /api/v1/auth/refresh` - Token refresh
|
||||
- `POST /api/v1/auth/logout` - Logout
|
||||
- `GET /api/v1/credits/balance` - Get credit balance
|
||||
|
||||
**Response format changes:**
|
||||
- Login returns: `{ accessToken, refreshToken, user, expiresIn, tokenType }`
|
||||
- Credits balance returns: `{ balance, freeCreditsRemaining, totalEarned, totalSpent }`
|
||||
|
||||
## Step-by-Step Integration
|
||||
|
||||
### Step 1: Update Environment Variables
|
||||
|
||||
#### Backend `.env`
|
||||
|
||||
```env
|
||||
# Remove Supabase variables
|
||||
# SUPABASE_URL=...
|
||||
# SUPABASE_SERVICE_KEY=...
|
||||
|
||||
# Add Mana Core Auth URL
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
#### Web App `.env`
|
||||
|
||||
```env
|
||||
# Remove
|
||||
# PUBLIC_SUPABASE_URL=...
|
||||
# PUBLIC_SUPABASE_ANON_KEY=...
|
||||
|
||||
# Add
|
||||
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
#### Mobile App `.env`
|
||||
|
||||
```env
|
||||
# Remove
|
||||
# EXPO_PUBLIC_SUPABASE_URL=...
|
||||
# EXPO_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
|
||||
# Add
|
||||
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
### Step 2: Update Backend (NestJS)
|
||||
|
||||
#### 2.1 Install Dependencies
|
||||
|
||||
```bash
|
||||
cd chat/backend
|
||||
pnpm add jsonwebtoken
|
||||
pnpm add -D @types/jsonwebtoken
|
||||
```
|
||||
|
||||
#### 2.2 Create JWT Auth Guard
|
||||
|
||||
Create `chat/backend/src/common/guards/jwt-auth.guard.ts`:
|
||||
|
||||
```typescript
|
||||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get public key from Mana Core Auth
|
||||
const authUrl = this.configService.get<string>('MANA_CORE_AUTH_URL');
|
||||
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
const { valid, payload } = await response.json();
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
request.user = {
|
||||
userId: payload.sub,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: any): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Update Controllers
|
||||
|
||||
Replace Supabase guards with JWT Auth guard:
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { SupabaseGuard } from './guards/supabase.guard';
|
||||
|
||||
@UseGuards(SupabaseGuard)
|
||||
|
||||
// After
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
```
|
||||
|
||||
### Step 3: Update Web App (SvelteKit)
|
||||
|
||||
#### 3.1 Update Auth Store
|
||||
|
||||
Edit `chat/apps/web/src/lib/stores/auth.svelte.ts`:
|
||||
|
||||
```typescript
|
||||
import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
|
||||
const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
// Initialize Mana Core Auth
|
||||
const { authService, tokenManager } = initializeWebAuth({
|
||||
baseUrl: MANA_AUTH_URL,
|
||||
});
|
||||
|
||||
class AuthStore {
|
||||
user = $state<UserData | null>(null);
|
||||
isLoading = $state(true);
|
||||
|
||||
async initialize() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
this.user = userData;
|
||||
}
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(email: string, password: string) {
|
||||
const result = await authService.signIn(email, password);
|
||||
if (result.success) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
this.user = userData;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async signUp(email: string, password: string) {
|
||||
const result = await authService.signUp(email, password);
|
||||
// After signup, automatically sign in
|
||||
if (result.success) {
|
||||
return this.signIn(email, password);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await authService.signOut();
|
||||
this.user = null;
|
||||
}
|
||||
|
||||
async resetPassword(email: string) {
|
||||
return authService.forgotPassword(email);
|
||||
}
|
||||
}
|
||||
|
||||
export const authStore = new AuthStore();
|
||||
```
|
||||
|
||||
#### 3.2 Update Server Hooks
|
||||
|
||||
Edit `chat/apps/web/src/hooks.server.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Get token from cookies
|
||||
const token = event.cookies.get('auth_token');
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
// Validate token with Mana Core Auth
|
||||
const authUrl = process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { valid, payload } = await response.json();
|
||||
if (valid) {
|
||||
event.locals.user = {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating token:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
```
|
||||
|
||||
### Step 4: Update Mobile App (Expo)
|
||||
|
||||
#### 4.1 Update AuthProvider
|
||||
|
||||
Edit `chat/apps/mobile/context/AuthProvider.tsx`:
|
||||
|
||||
```typescript
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import {
|
||||
createAuthService,
|
||||
createTokenManager,
|
||||
setStorageAdapter,
|
||||
setDeviceAdapter,
|
||||
setNetworkAdapter,
|
||||
type UserData,
|
||||
} from '@manacore/shared-auth';
|
||||
import { createSecureStoreAdapter } from '@manacore/shared-auth/native'; // You may need to create this
|
||||
|
||||
const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
// Initialize auth service
|
||||
const authService = createAuthService({ baseUrl: MANA_AUTH_URL });
|
||||
const tokenManager = createTokenManager(authService);
|
||||
|
||||
type AuthContextType = {
|
||||
user: UserData | null;
|
||||
isLoading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<void>;
|
||||
signUp: (email: string, password: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
resetPassword: (email: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, []);
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function signIn(email: string, password: string) {
|
||||
const result = await authService.signIn(email, password);
|
||||
if (result.success) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
} else {
|
||||
throw new Error(result.error || 'Sign in failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function signUp(email: string, password: string) {
|
||||
const result = await authService.signUp(email, password);
|
||||
if (result.success) {
|
||||
// Auto sign in after signup
|
||||
await signIn(email, password);
|
||||
} else {
|
||||
throw new Error(result.error || 'Sign up failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
await authService.signOut();
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
async function resetPassword(email: string) {
|
||||
const result = await authService.forgotPassword(email);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Password reset failed');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ user, isLoading, signIn, signUp, signOut, resetPassword }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Remove Supabase Dependencies
|
||||
|
||||
#### 5.1 Web App
|
||||
|
||||
```bash
|
||||
cd chat/apps/web
|
||||
pnpm remove @supabase/ssr @supabase/supabase-js
|
||||
```
|
||||
|
||||
Delete or update these files:
|
||||
- `src/lib/services/supabase.ts` (no longer needed)
|
||||
|
||||
#### 5.2 Mobile App
|
||||
|
||||
```bash
|
||||
cd chat/apps/mobile
|
||||
pnpm remove @supabase/supabase-js
|
||||
```
|
||||
|
||||
Delete or update these files:
|
||||
- `utils/supabase.ts` (no longer needed)
|
||||
|
||||
#### 5.3 Backend
|
||||
|
||||
```bash
|
||||
cd chat/backend
|
||||
pnpm remove @supabase/supabase-js
|
||||
```
|
||||
|
||||
### Step 6: Test the Integration
|
||||
|
||||
#### 6.1 Start Mana Core Auth
|
||||
|
||||
```bash
|
||||
# From monorepo root
|
||||
cd mana-core-auth
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
Service should be running at `http://localhost:3001`
|
||||
|
||||
#### 6.2 Start Chat Backend
|
||||
|
||||
```bash
|
||||
cd chat/backend
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
#### 6.3 Start Web App
|
||||
|
||||
```bash
|
||||
cd chat/apps/web
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
#### 6.4 Test Flow
|
||||
|
||||
1. **Register a new user**
|
||||
- Go to `/register`
|
||||
- Enter email and password
|
||||
- Should redirect to login
|
||||
|
||||
2. **Login**
|
||||
- Enter credentials
|
||||
- Should receive tokens and redirect to app
|
||||
|
||||
3. **Check credits**
|
||||
- User should have 150 initial credits
|
||||
- Call `authService.getUserCredits()` to verify
|
||||
|
||||
4. **Protected routes**
|
||||
- Try accessing `/chat` or other protected routes
|
||||
- Should work with valid token
|
||||
|
||||
5. **Logout**
|
||||
- Click logout
|
||||
- Tokens should be cleared
|
||||
- Should redirect to login
|
||||
|
||||
## API Compatibility
|
||||
|
||||
### Mana Core Auth vs Supabase
|
||||
|
||||
| Feature | Supabase Auth | Mana Core Auth | Status |
|
||||
|---------|---------------|----------------|--------|
|
||||
| Email/Password | ✅ | ✅ | Migrated |
|
||||
| OAuth (Google) | ✅ | 🚧 | TODO |
|
||||
| OAuth (Apple) | ✅ | 🚧 | TODO |
|
||||
| Password Reset | ✅ | 🚧 | TODO |
|
||||
| Email Verification | ✅ | 🚧 | TODO |
|
||||
| Credits | ❌ | ✅ | New! |
|
||||
| Session Management | ✅ | ✅ | Migrated |
|
||||
| JWT Tokens | ✅ | ✅ | Migrated |
|
||||
|
||||
## Credits System
|
||||
|
||||
Mana Core Auth includes a built-in credit system:
|
||||
|
||||
```typescript
|
||||
// Get credit balance
|
||||
const credits = await authService.getUserCredits();
|
||||
console.log(credits);
|
||||
// {
|
||||
// credits: 150, // balance + freeCreditsRemaining
|
||||
// maxCreditLimit: 1000,
|
||||
// userId: "user-id"
|
||||
// }
|
||||
```
|
||||
|
||||
### Default Credit Allocation
|
||||
|
||||
- **Signup Bonus:** 150 free credits
|
||||
- **Daily Free:** 5 credits every 24 hours
|
||||
- **Pricing:** 100 mana = €1.00
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" to Mana Core Auth
|
||||
|
||||
**Solution:** Make sure Mana Core Auth is running:
|
||||
```bash
|
||||
cd mana-core-auth
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
### "Invalid token" errors
|
||||
|
||||
**Solution:** Clear stored tokens and login again:
|
||||
```typescript
|
||||
await authService.clearAuthStorage();
|
||||
```
|
||||
|
||||
### CORS errors
|
||||
|
||||
**Solution:** Add Chat app URLs to Mana Core Auth `.env`:
|
||||
```env
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:8081
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Update `@manacore/shared-auth` package
|
||||
2. ⏳ Integrate into Chat backend
|
||||
3. ⏳ Update Chat web app
|
||||
4. ⏳ Update Chat mobile app
|
||||
5. ⏳ Test end-to-end
|
||||
6. 🔜 Add OAuth providers
|
||||
7. 🔜 Add email verification
|
||||
8. 🔜 Add password reset
|
||||
|
||||
## Resources
|
||||
|
||||
- **Mana Core Auth README:** `/mana-core-auth/README.md`
|
||||
- **Shared Auth Package:** `/packages/shared-auth/`
|
||||
- **API Documentation:** `/mana-core-auth/README.md#api-endpoints`
|
||||
- **Quick Start:** `/mana-core-auth/QUICKSTART.md`
|
||||
|
||||
---
|
||||
|
||||
**Status:** 🚧 Integration Guide Complete - Implementation Pending
|
||||
|
||||
**Date:** 2025-11-25
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
# Supabase Konfiguration
|
||||
# Mana Core Auth Configuration
|
||||
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
|
||||
# Supabase Configuration (for database only, not auth)
|
||||
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
|
||||
# Chat Backend API
|
||||
# The backend handles AI API calls securely - no API keys needed in the mobile app
|
||||
EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
|
||||
EXPO_PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
|
|
|
|||
|
|
@ -1,23 +1,99 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session, User } from '@supabase/supabase-js';
|
||||
import { ActivityIndicator, View, Text } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import {
|
||||
createAuthService,
|
||||
createTokenManager,
|
||||
setStorageAdapter,
|
||||
setDeviceAdapter,
|
||||
setNetworkAdapter,
|
||||
createMemoryStorageAdapter,
|
||||
type UserData,
|
||||
} from '@manacore/shared-auth';
|
||||
|
||||
// Definiere den Typ für den Auth-Kontext
|
||||
// Mana Core Auth URL from environment
|
||||
const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
// Create SecureStore adapter for React Native
|
||||
const createSecureStoreAdapter = () => ({
|
||||
async getItem<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const value = await SecureStore.getItemAsync(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async setItem(key: string, value: unknown): Promise<void> {
|
||||
await SecureStore.setItemAsync(key, JSON.stringify(value));
|
||||
},
|
||||
async removeItem(key: string): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
});
|
||||
|
||||
// Create device adapter for React Native
|
||||
const createReactNativeDeviceAdapter = () => {
|
||||
let deviceId: string | null = null;
|
||||
|
||||
return {
|
||||
async getDeviceInfo() {
|
||||
if (!deviceId) {
|
||||
// Try to get stored device ID
|
||||
deviceId = await SecureStore.getItemAsync('@device/id');
|
||||
|
||||
if (!deviceId) {
|
||||
// Generate new device ID
|
||||
deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
await SecureStore.setItemAsync('@device/id', deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
deviceName: 'React Native Device',
|
||||
platform: 'react-native',
|
||||
};
|
||||
},
|
||||
async getStoredDeviceId() {
|
||||
return deviceId || (await SecureStore.getItemAsync('@device/id'));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Create network adapter (basic implementation)
|
||||
const createReactNativeNetworkAdapter = () => ({
|
||||
async isConnected() {
|
||||
return true; // Always assume connected for now
|
||||
},
|
||||
async hasStableConnection() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize adapters
|
||||
setStorageAdapter(createSecureStoreAdapter());
|
||||
setDeviceAdapter(createReactNativeDeviceAdapter());
|
||||
setNetworkAdapter(createReactNativeNetworkAdapter());
|
||||
|
||||
// Create auth service
|
||||
const authService = createAuthService({ baseUrl: MANA_AUTH_URL });
|
||||
const tokenManager = createTokenManager(authService);
|
||||
|
||||
// Auth context type
|
||||
type AuthContextType = {
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
user: UserData | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<{ error: any | null }>;
|
||||
signUp: (email: string, password: string) => Promise<{ error: any | null, data: any | null }>;
|
||||
signUp: (email: string, password: string) => Promise<{ error: any | null; data: any | null }>;
|
||||
signOut: () => Promise<void>;
|
||||
resetPassword: (email: string) => Promise<{ error: any | null }>;
|
||||
};
|
||||
|
||||
// Erstelle den Auth-Kontext
|
||||
// Create auth context
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
// Hook für den Zugriff auf den Auth-Kontext
|
||||
// Hook to access auth context
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
|
|
@ -26,57 +102,51 @@ export const useAuth = () => {
|
|||
return context;
|
||||
};
|
||||
|
||||
// AuthProvider-Komponente
|
||||
// AuthProvider component
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Initialisiere den Auth-Status
|
||||
// Initialize auth state
|
||||
useEffect(() => {
|
||||
// Hole die aktuelle Session
|
||||
const getInitialSession = async () => {
|
||||
const initialize = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Prüfe, ob bereits eine Session existiert
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
|
||||
// Abonniere Änderungen am Auth-Status
|
||||
const { data: { subscription } } = await supabase.auth.onAuthStateChange(
|
||||
(_event, session) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
|
||||
// Check if user is authenticated
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Initialisieren der Auth-Session:', error);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getInitialSession();
|
||||
|
||||
initialize();
|
||||
}, []);
|
||||
|
||||
// Anmelden mit E-Mail und Passwort
|
||||
// Sign in with email and password
|
||||
const signIn = async (email: string, password: string) => {
|
||||
try {
|
||||
console.log('Versuche Anmeldung mit:', email);
|
||||
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase Auth Fehler:', error.message, error.status);
|
||||
return { error };
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Auth Fehler:', result.error);
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
|
||||
console.log('Anmeldung erfolgreich:', data.user?.id);
|
||||
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
|
||||
console.log('Anmeldung erfolgreich:', userData?.userId);
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
console.error('Unerwarteter Fehler beim Anmelden:', error.message || error);
|
||||
|
|
@ -84,55 +154,56 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
};
|
||||
|
||||
// Registrieren mit E-Mail und Passwort
|
||||
// Sign up with email and password
|
||||
const signUp = async (email: string, password: string) => {
|
||||
try {
|
||||
// Registriere den Benutzer mit autoConfirm=true, um die E-Mail-Bestätigung zu umgehen
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
email_confirmed: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!error && data?.user) {
|
||||
// Wenn die Registrierung erfolgreich war, melde den Benutzer direkt an
|
||||
await signIn(email, password);
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { data: null, error: { message: result.error } };
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await signIn(email, password);
|
||||
|
||||
if (signInResult.error) {
|
||||
return { data: null, error: signInResult.error };
|
||||
}
|
||||
|
||||
return { data: user, error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Registrieren:', error);
|
||||
return { data: null, error };
|
||||
}
|
||||
};
|
||||
|
||||
// Abmelden
|
||||
// Sign out
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
await authService.signOut();
|
||||
setUser(null);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abmelden:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Passwort zurücksetzen
|
||||
// Reset password
|
||||
const resetPassword = async (email: string) => {
|
||||
try {
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: 'exp://localhost:8081/reset-password',
|
||||
});
|
||||
return { error };
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
if (!result.success) {
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
// Zeige Ladeindikator während der Initialisierung
|
||||
// Show loading indicator during initialization
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
|
|
@ -142,11 +213,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
// Stelle den Auth-Kontext bereit
|
||||
// Provide auth context
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
session,
|
||||
user,
|
||||
loading,
|
||||
signIn,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# Supabase Configuration (same as mobile app)
|
||||
# Mana Core Auth Configuration
|
||||
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
|
||||
# Supabase Configuration (for database only, not auth)
|
||||
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
|
||||
# Chat Backend API
|
||||
PUBLIC_BACKEND_URL=http://localhost:3001
|
||||
PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
|
|
|
|||
|
|
@ -1,32 +1,24 @@
|
|||
/**
|
||||
* Auth Store - Manages authentication state using Svelte 5 runes
|
||||
* Compatible with Chat mobile app (same Supabase instance)
|
||||
* Now using Mana Core Auth instead of Supabase Auth
|
||||
*/
|
||||
|
||||
import { createSupabaseBrowserClient } from '$lib/services/supabase';
|
||||
import type { Session, User } from '@supabase/supabase-js';
|
||||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
|
||||
|
||||
// Initialize Mana Core Auth
|
||||
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
const { authService, tokenManager } = initializeWebAuth({
|
||||
baseUrl: MANA_AUTH_URL,
|
||||
});
|
||||
|
||||
// State
|
||||
let session = $state<Session | null>(null);
|
||||
let user = $state<User | null>(null);
|
||||
let user = $state<UserData | null>(null);
|
||||
let loading = $state(true);
|
||||
let initialized = $state(false);
|
||||
|
||||
// Create browser client
|
||||
let supabase: ReturnType<typeof createSupabaseBrowserClient> | null = null;
|
||||
|
||||
function getSupabase() {
|
||||
if (!supabase) {
|
||||
supabase = createSupabaseBrowserClient();
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
|
||||
export const authStore = {
|
||||
// Getters
|
||||
get session() {
|
||||
return session;
|
||||
},
|
||||
get user() {
|
||||
return user;
|
||||
},
|
||||
|
|
@ -41,33 +33,21 @@ export const authStore = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Initialize auth state from Supabase session
|
||||
* Initialize auth state from stored tokens
|
||||
*/
|
||||
async initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const sb = getSupabase();
|
||||
|
||||
// Get current session
|
||||
const {
|
||||
data: { session: currentSession },
|
||||
} = await sb.auth.getSession();
|
||||
|
||||
session = currentSession;
|
||||
user = currentSession?.user ?? null;
|
||||
|
||||
// Subscribe to auth changes
|
||||
sb.auth.onAuthStateChange((_event, newSession) => {
|
||||
session = newSession;
|
||||
user = newSession?.user ?? null;
|
||||
});
|
||||
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
}
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth:', error);
|
||||
session = null;
|
||||
user = null;
|
||||
} finally {
|
||||
loading = false;
|
||||
|
|
@ -78,83 +58,98 @@ export const authStore = {
|
|||
* Sign in with email and password
|
||||
*/
|
||||
async signIn(email: string, password: string) {
|
||||
const sb = getSupabase();
|
||||
const { data, error } = await sb.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
try {
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Login failed' };
|
||||
}
|
||||
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
|
||||
session = data.session;
|
||||
user = data.user;
|
||||
return { success: true, error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign up with email and password
|
||||
*/
|
||||
async signUp(email: string, password: string) {
|
||||
const sb = getSupabase();
|
||||
const { data, error } = await sb.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
email_confirmed: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, needsVerification: false };
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
|
||||
}
|
||||
|
||||
// Mana Core Auth requires separate login after signup
|
||||
if (result.needsVerification) {
|
||||
return { success: true, error: null, needsVerification: true };
|
||||
}
|
||||
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await this.signIn(email, password);
|
||||
return { ...signInResult, needsVerification: false };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage, needsVerification: false };
|
||||
}
|
||||
|
||||
// Check if email confirmation is required
|
||||
if (data.user && !data.session) {
|
||||
return { success: true, error: null, needsVerification: true };
|
||||
}
|
||||
|
||||
session = data.session;
|
||||
user = data.user;
|
||||
return { success: true, error: null, needsVerification: false };
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
async signOut() {
|
||||
const sb = getSupabase();
|
||||
await sb.auth.signOut();
|
||||
session = null;
|
||||
user = null;
|
||||
try {
|
||||
await authService.signOut();
|
||||
user = null;
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
// Clear user even if sign out fails
|
||||
user = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send password reset email
|
||||
*/
|
||||
async resetPassword(email: string) {
|
||||
const sb = getSupabase();
|
||||
const { error } = await sb.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/auth/reset-password`,
|
||||
});
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Password reset failed' };
|
||||
}
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
|
||||
return { success: true, error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Set session from server-side data
|
||||
* Get user credit balance
|
||||
*/
|
||||
setSession(newSession: Session | null) {
|
||||
session = newSession;
|
||||
user = newSession?.user ?? null;
|
||||
initialized = true;
|
||||
loading = false;
|
||||
async getCredits() {
|
||||
try {
|
||||
const credits = await authService.getUserCredits();
|
||||
return credits;
|
||||
} catch (error) {
|
||||
console.error('Failed to get credits:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
async getAccessToken() {
|
||||
return await authService.getAppToken();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ AZURE_OPENAI_ENDPOINT=https://your-azure-openai-endpoint.openai.azure.com
|
|||
AZURE_OPENAI_API_KEY=your-api-key-here
|
||||
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||
|
||||
# Supabase Configuration
|
||||
# Mana Core Auth Configuration
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
|
||||
# Supabase Configuration (for database only, not auth)
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_SERVICE_KEY=your-service-key-here
|
||||
|
||||
# Server Configuration
|
||||
PORT=3001
|
||||
PORT=3002
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-errors": "workspace:*",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { isOk } from '@manacore/shared-errors';
|
||||
import { ChatService } from './chat.service';
|
||||
import { ChatCompletionDto, ChatCompletionResponseDto } from './dto/chat-completion.dto';
|
||||
import {
|
||||
ChatCompletionDto,
|
||||
ChatCompletionResponseDto,
|
||||
} from './dto/chat-completion.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import {
|
||||
CurrentUser,
|
||||
CurrentUserData,
|
||||
} from '../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('chat')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ChatController {
|
||||
constructor(private readonly chatService: ChatService) {}
|
||||
|
||||
|
|
@ -14,7 +24,14 @@ export class ChatController {
|
|||
@Post('completions')
|
||||
async createCompletion(
|
||||
@Body() dto: ChatCompletionDto,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<ChatCompletionResponseDto> {
|
||||
return this.chatService.createCompletion(dto);
|
||||
const result = await this.chatService.createCompletion(dto, user.userId);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error; // Caught by AppExceptionFilter
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
type AsyncResult,
|
||||
ok,
|
||||
err,
|
||||
ValidationError,
|
||||
ServiceError,
|
||||
} from '@manacore/shared-errors';
|
||||
import { ChatCompletionDto, ChatCompletionResponseDto } from './dto/chat-completion.dto';
|
||||
|
||||
export interface AIModel {
|
||||
|
|
@ -84,11 +91,23 @@ export class ChatService {
|
|||
return this.availableModels.find((m) => m.id === modelId);
|
||||
}
|
||||
|
||||
async createCompletion(dto: ChatCompletionDto): Promise<ChatCompletionResponseDto> {
|
||||
async createCompletion(
|
||||
dto: ChatCompletionDto,
|
||||
userId?: string,
|
||||
): AsyncResult<ChatCompletionResponseDto> {
|
||||
const model = this.getModelById(dto.modelId);
|
||||
|
||||
if (!model) {
|
||||
throw new BadRequestException(`Model with ID ${dto.modelId} not found`);
|
||||
return err(
|
||||
ValidationError.invalidInput('modelId', `Model ${dto.modelId} not found`),
|
||||
);
|
||||
}
|
||||
|
||||
// Log user context for tracking (optional)
|
||||
if (userId) {
|
||||
this.logger.log(
|
||||
`User ${userId} creating chat completion with model ${dto.modelId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const deployment = model.parameters.deployment;
|
||||
|
|
@ -104,7 +123,8 @@ export class ChatService {
|
|||
};
|
||||
|
||||
// Model-specific parameters
|
||||
const isGPTOModel = deployment.includes('gpt-o') || deployment.includes('gpt-4o');
|
||||
const isGPTOModel =
|
||||
deployment.includes('gpt-o') || deployment.includes('gpt-4o');
|
||||
|
||||
if (!isGPTOModel) {
|
||||
requestBody.max_tokens = maxTokens;
|
||||
|
|
@ -128,7 +148,12 @@ export class ChatService {
|
|||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
this.logger.error(`API error: ${response.status} - ${errorText}`);
|
||||
throw new BadRequestException(`Azure OpenAI API error: ${response.status}`);
|
||||
return err(
|
||||
ServiceError.externalError(
|
||||
'Azure OpenAI',
|
||||
`API error: ${response.status}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
|
@ -137,20 +162,28 @@ export class ChatService {
|
|||
|
||||
if (!messageContent) {
|
||||
this.logger.warn('No message content in response');
|
||||
throw new BadRequestException('No response generated');
|
||||
return err(
|
||||
ServiceError.generationFailed('Azure OpenAI', 'No response generated'),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
return ok({
|
||||
content: messageContent,
|
||||
usage: {
|
||||
prompt_tokens: data.usage?.prompt_tokens || 0,
|
||||
completion_tokens: data.usage?.completion_tokens || 0,
|
||||
total_tokens: data.usage?.total_tokens || 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Error calling Azure OpenAI API', error);
|
||||
throw error;
|
||||
return err(
|
||||
ServiceError.generationFailed(
|
||||
'Azure OpenAI',
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
error instanceof Error ? error : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
chat/backend/src/common/decorators/current-user.decorator.ts
Normal file
15
chat/backend/src/common/decorators/current-user.decorator.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export interface CurrentUserData {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext): CurrentUserData => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
66
chat/backend/src/common/guards/jwt-auth.guard.ts
Normal file
66
chat/backend/src/common/guards/jwt-auth.guard.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get Mana Core Auth URL from config
|
||||
const authUrl =
|
||||
this.configService.get<string>('MANA_CORE_AUTH_URL') ||
|
||||
'http://localhost:3001';
|
||||
|
||||
// Validate token with Mana Core Auth
|
||||
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
const { valid, payload } = await response.json();
|
||||
|
||||
if (!valid || !payload) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
request.user = {
|
||||
userId: payload.sub,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
sessionId: payload.sessionId,
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('Error validating token:', error);
|
||||
throw new UnauthorizedException('Token validation failed');
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: any): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,99 @@
|
|||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { ConversationService } from './conversation.service';
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { isOk } from '@manacore/shared-errors';
|
||||
import {
|
||||
ConversationService,
|
||||
type Conversation,
|
||||
type Message,
|
||||
} from './conversation.service';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import {
|
||||
CurrentUser,
|
||||
CurrentUserData,
|
||||
} from '../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('conversations')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ConversationController {
|
||||
constructor(private readonly conversationService: ConversationService) {}
|
||||
|
||||
@Get()
|
||||
async getConversations(@Query('userId') userId: string) {
|
||||
return this.conversationService.getConversations(userId);
|
||||
async getConversations(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Conversation[]> {
|
||||
const result = await this.conversationService.getConversations(user.userId);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getConversation(@Param('id') id: string) {
|
||||
return this.conversationService.getConversation(id);
|
||||
async getConversation(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Conversation> {
|
||||
// TODO: Add ownership check - ensure conversation belongs to user
|
||||
const result = await this.conversationService.getConversation(id);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@Get(':id/messages')
|
||||
async getMessages(@Param('id') id: string) {
|
||||
return this.conversationService.getMessages(id);
|
||||
async getMessages(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Message[]> {
|
||||
// TODO: Add ownership check - ensure conversation belongs to user
|
||||
const result = await this.conversationService.getMessages(id);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createConversation(
|
||||
@Body() body: { userId: string; modelId: string; title?: string },
|
||||
) {
|
||||
return this.conversationService.createConversation(
|
||||
body.userId,
|
||||
@Body() body: { modelId: string; title?: string },
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Conversation> {
|
||||
const result = await this.conversationService.createConversation(
|
||||
user.userId,
|
||||
body.modelId,
|
||||
body.title,
|
||||
);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@Post(':id/messages')
|
||||
async addMessage(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { sender: 'user' | 'assistant' | 'system'; messageText: string },
|
||||
) {
|
||||
return this.conversationService.addMessage(id, body.sender, body.messageText);
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
): Promise<Message> {
|
||||
// TODO: Add ownership check - ensure conversation belongs to user
|
||||
const result = await this.conversationService.addMessage(
|
||||
id,
|
||||
body.sender,
|
||||
body.messageText,
|
||||
);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
import {
|
||||
type AsyncResult,
|
||||
ok,
|
||||
err,
|
||||
ServiceError,
|
||||
DatabaseError,
|
||||
NotFoundError,
|
||||
} from '@manacore/shared-errors';
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
|
|
@ -23,7 +31,7 @@ export interface Message {
|
|||
@Injectable()
|
||||
export class ConversationService {
|
||||
private readonly logger = new Logger(ConversationService.name);
|
||||
private supabase: SupabaseClient;
|
||||
private supabase: SupabaseClient | null = null;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL');
|
||||
|
|
@ -36,10 +44,9 @@ export class ConversationService {
|
|||
}
|
||||
}
|
||||
|
||||
async getConversations(userId: string): Promise<Conversation[]> {
|
||||
async getConversations(userId: string): AsyncResult<Conversation[]> {
|
||||
if (!this.supabase) {
|
||||
this.logger.warn('Supabase not configured');
|
||||
return [];
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
|
|
@ -51,15 +58,15 @@ export class ConversationService {
|
|||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching conversations', error);
|
||||
throw error;
|
||||
return err(DatabaseError.queryFailed('Failed to fetch conversations'));
|
||||
}
|
||||
|
||||
return data || [];
|
||||
return ok(data || []);
|
||||
}
|
||||
|
||||
async getConversation(id: string): Promise<Conversation | null> {
|
||||
async getConversation(id: string): AsyncResult<Conversation> {
|
||||
if (!this.supabase) {
|
||||
return null;
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
|
|
@ -70,15 +77,18 @@ export class ConversationService {
|
|||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching conversation', error);
|
||||
return null;
|
||||
if (error.code === 'PGRST116') {
|
||||
return err(new NotFoundError('Conversation', id));
|
||||
}
|
||||
return err(DatabaseError.queryFailed('Failed to fetch conversation'));
|
||||
}
|
||||
|
||||
return data;
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
async getMessages(conversationId: string): Promise<Message[]> {
|
||||
async getMessages(conversationId: string): AsyncResult<Message[]> {
|
||||
if (!this.supabase) {
|
||||
return [];
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
|
|
@ -89,19 +99,19 @@ export class ConversationService {
|
|||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching messages', error);
|
||||
throw error;
|
||||
return err(DatabaseError.queryFailed('Failed to fetch messages'));
|
||||
}
|
||||
|
||||
return data || [];
|
||||
return ok(data || []);
|
||||
}
|
||||
|
||||
async createConversation(
|
||||
userId: string,
|
||||
modelId: string,
|
||||
title?: string,
|
||||
): Promise<Conversation> {
|
||||
): AsyncResult<Conversation> {
|
||||
if (!this.supabase) {
|
||||
throw new Error('Supabase not configured');
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
|
|
@ -117,19 +127,19 @@ export class ConversationService {
|
|||
|
||||
if (error) {
|
||||
this.logger.error('Error creating conversation', error);
|
||||
throw error;
|
||||
return err(DatabaseError.queryFailed('Failed to create conversation'));
|
||||
}
|
||||
|
||||
return data;
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
async addMessage(
|
||||
conversationId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string,
|
||||
): Promise<Message> {
|
||||
): AsyncResult<Message> {
|
||||
if (!this.supabase) {
|
||||
throw new Error('Supabase not configured');
|
||||
return err(ServiceError.unavailable('Database'));
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabase
|
||||
|
|
@ -144,7 +154,7 @@ export class ConversationService {
|
|||
|
||||
if (error) {
|
||||
this.logger.error('Error adding message', error);
|
||||
throw error;
|
||||
return err(DatabaseError.queryFailed('Failed to add message'));
|
||||
}
|
||||
|
||||
// Update conversation updated_at
|
||||
|
|
@ -153,6 +163,6 @@ export class ConversationService {
|
|||
.update({ updated_at: new Date().toISOString() })
|
||||
.eq('id', conversationId);
|
||||
|
||||
return data;
|
||||
return ok(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppExceptionFilter } from '@manacore/shared-errors/nestjs';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
|
|
@ -17,6 +18,9 @@ async function bootstrap() {
|
|||
credentials: true,
|
||||
});
|
||||
|
||||
// Global exception filter for standardized error responses
|
||||
app.useGlobalFilters(new AppExceptionFilter());
|
||||
|
||||
// Enable validation
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue