mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
🩹 fix(auth): fix gift code route prefix and JWT issuer validation
- Remove duplicate route prefix in GiftsController (was /api/v1/api/v1/gifts) - Fix JwtAuthGuard to use JWT_ISSUER as fallback when BASE_URL is not set - Add comprehensive GIFT_CODES.md documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0485ce4b07
commit
284cd004aa
3 changed files with 263 additions and 9 deletions
257
services/mana-core-auth/docs/GIFT_CODES.md
Normal file
257
services/mana-core-auth/docs/GIFT_CODES.md
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
# Gift Code System
|
||||
|
||||
User-generated gift codes for sharing credits across the Mana ecosystem.
|
||||
|
||||
## Overview
|
||||
|
||||
Users can create gift codes to share credits with others. The system supports various modes:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `simple` | Single-use, one recipient |
|
||||
| `personalized` | Restricted to specific email/Matrix ID |
|
||||
| `split` | Divided into portions (e.g., 100 credits / 5 = 20 each) |
|
||||
| `first_come` | First N users get full amount |
|
||||
| `riddle` | Requires correct answer to redeem |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
Base URL: `/api/v1/gifts`
|
||||
|
||||
| Method | Endpoint | Auth | Description |
|
||||
|--------|----------|------|-------------|
|
||||
| GET | `/:code` | - | Get gift code info (public preview) |
|
||||
| POST | `/` | JWT | Create new gift code |
|
||||
| POST | `/:code/redeem` | JWT | Redeem a gift code |
|
||||
| GET | `/me/created` | JWT | List codes you created |
|
||||
| GET | `/me/received` | JWT | List gifts you received |
|
||||
| DELETE | `/:id` | JWT | Cancel code & refund unclaimed |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create Gift Code
|
||||
|
||||
```bash
|
||||
curl -X POST "https://auth.mana.how/api/v1/gifts" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"credits": 100,
|
||||
"message": "Happy Birthday!",
|
||||
"type": "simple"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"code": "ABC123",
|
||||
"url": "https://mana.how/g/ABC123",
|
||||
"totalCredits": 100,
|
||||
"creditsPerPortion": 100,
|
||||
"totalPortions": 1,
|
||||
"type": "simple",
|
||||
"expiresAt": "2026-05-14T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Create Split Gift (5 portions)
|
||||
|
||||
```bash
|
||||
curl -X POST "https://auth.mana.how/api/v1/gifts" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"credits": 100,
|
||||
"type": "split",
|
||||
"portions": 5,
|
||||
"message": "Share this with friends!"
|
||||
}'
|
||||
```
|
||||
|
||||
Each recipient gets 20 credits (100 / 5).
|
||||
|
||||
### Create Riddle Gift
|
||||
|
||||
```bash
|
||||
curl -X POST "https://auth.mana.how/api/v1/gifts" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"credits": 50,
|
||||
"type": "riddle",
|
||||
"riddleQuestion": "What is the capital of Germany?",
|
||||
"riddleAnswer": "Berlin"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Gift Info (Public)
|
||||
|
||||
```bash
|
||||
curl "https://auth.mana.how/api/v1/gifts/ABC123"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"code": "ABC123",
|
||||
"type": "simple",
|
||||
"status": "active",
|
||||
"creditsPerPortion": 100,
|
||||
"totalPortions": 1,
|
||||
"remainingPortions": 1,
|
||||
"message": "Happy Birthday!",
|
||||
"hasRiddle": false,
|
||||
"isPersonalized": false,
|
||||
"expiresAt": "2026-05-14T00:00:00Z",
|
||||
"creatorName": "John Doe"
|
||||
}
|
||||
```
|
||||
|
||||
### Redeem Gift Code
|
||||
|
||||
```bash
|
||||
curl -X POST "https://auth.mana.how/api/v1/gifts/ABC123/redeem" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
For riddle gifts, include the answer:
|
||||
```bash
|
||||
curl -X POST "https://auth.mana.how/api/v1/gifts/ABC123/redeem" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"answer": "Berlin"}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"creditsReceived": 100,
|
||||
"newBalance": 250,
|
||||
"message": "Happy Birthday!"
|
||||
}
|
||||
```
|
||||
|
||||
## Matrix Bot Commands
|
||||
|
||||
### German
|
||||
```
|
||||
!geschenk 50 # Simple gift
|
||||
!geschenk 100 /5 # Split into 5 portions
|
||||
!geschenk 50 ?="Berlin" # With riddle
|
||||
!geschenk 50 "Viel Spass!" # With message
|
||||
!einloesen ABC123 # Redeem code
|
||||
!einloesen ABC123 Berlin # Redeem with riddle answer
|
||||
!meine-geschenke # List your gifts
|
||||
```
|
||||
|
||||
### English
|
||||
```
|
||||
!gift 50
|
||||
!gift 100 /5
|
||||
!gift 50 ?="Berlin"
|
||||
!redeem ABC123
|
||||
!my-gifts
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Tables
|
||||
|
||||
**gifts.gift_codes**
|
||||
- `id` - UUID primary key
|
||||
- `code` - Unique 6-char code (e.g., "ABC123")
|
||||
- `short_url` - Full URL (e.g., "mana.how/g/ABC123")
|
||||
- `creator_id` - FK to auth.users
|
||||
- `total_credits` - Reserved amount
|
||||
- `credits_per_portion` - Credits per redemption
|
||||
- `total_portions` - Number of portions
|
||||
- `claimed_portions` - Portions already redeemed
|
||||
- `type` - simple|personalized|split|first_come|riddle
|
||||
- `status` - active|depleted|expired|cancelled|refunded
|
||||
- `target_email` - For personalized gifts
|
||||
- `target_matrix_id` - For personalized gifts
|
||||
- `riddle_question` - Question text
|
||||
- `riddle_answer_hash` - bcrypt hash of answer
|
||||
- `message` - Optional message
|
||||
- `expires_at` - Expiration timestamp
|
||||
- `reservation_transaction_id` - FK to credits.transactions
|
||||
|
||||
**gifts.gift_redemptions**
|
||||
- `id` - UUID primary key
|
||||
- `gift_code_id` - FK to gift_codes
|
||||
- `redeemer_user_id` - FK to auth.users
|
||||
- `status` - success|failed_wrong_answer|failed_wrong_user|...
|
||||
- `credits_received` - Amount credited
|
||||
- `portion_number` - Which portion was claimed
|
||||
- `credit_transaction_id` - FK to credits.transactions
|
||||
- `source_app_id` - 'matrix-bot', 'web', etc.
|
||||
|
||||
### Transaction Types
|
||||
|
||||
Credits schema includes gift-related transaction types:
|
||||
- `gift_reserve` - Credits reserved when creating gift
|
||||
- `gift_release` - Credits returned when cancelling gift
|
||||
- `gift_receive` - Credits received when redeeming gift
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Web Apps (SvelteKit)
|
||||
```typescript
|
||||
// Fetch gift info
|
||||
const response = await fetch(`/api/v1/gifts/${code}`);
|
||||
const giftInfo = await response.json();
|
||||
|
||||
// Redeem
|
||||
const result = await fetch(`/api/v1/gifts/${code}/redeem`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify({ answer: riddleAnswer })
|
||||
});
|
||||
```
|
||||
|
||||
### Matrix Bots
|
||||
```typescript
|
||||
import { GiftService } from '@manacore/bot-services';
|
||||
import { handleGiftCommand } from '@manacore/matrix-bot-common';
|
||||
|
||||
// In bot command handler
|
||||
if (isGiftCommand(command)) {
|
||||
return handleGiftCommand(this, roomId, userId, command, args);
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile Apps (Expo)
|
||||
Same REST API, use fetch or axios with JWT token.
|
||||
|
||||
## Security
|
||||
|
||||
- Gift codes use 6-char alphanumeric codes (no ambiguous chars)
|
||||
- Riddle answers are bcrypt hashed
|
||||
- Row-level locking prevents race conditions
|
||||
- Credits are reserved atomically when creating gifts
|
||||
- Personalized gifts verify email or Matrix ID
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables:
|
||||
```env
|
||||
# Base URL for short links
|
||||
APP_BASE_URL=https://mana.how
|
||||
```
|
||||
|
||||
Gift code rules (hardcoded):
|
||||
```typescript
|
||||
const GIFT_CODE_RULES = {
|
||||
minCredits: 1,
|
||||
maxCredits: 10000,
|
||||
maxPortions: 100,
|
||||
maxMessageLength: 500,
|
||||
maxRiddleQuestionLength: 200,
|
||||
defaultExpirationDays: 90,
|
||||
};
|
||||
```
|
||||
|
|
@ -43,9 +43,10 @@ export class JwtAuthGuard implements CanActivate {
|
|||
}
|
||||
|
||||
// IMPORTANT: Match Better Auth signing config exactly (better-auth.config.ts)
|
||||
// Signing uses: issuer = BASE_URL, audience = JWT_AUDIENCE || 'manacore'
|
||||
const baseUrl = this.configService.get<string>('BASE_URL') || 'http://localhost:3001';
|
||||
const issuer = baseUrl; // Better Auth uses BASE_URL as issuer for OIDC compatibility
|
||||
// Better Auth uses: issuer = BASE_URL || JWT_ISSUER || 'http://localhost:3001'
|
||||
const baseUrl = this.configService.get<string>('BASE_URL');
|
||||
const jwtIssuer = this.configService.get<string>('jwt.issuer');
|
||||
const issuer = baseUrl || jwtIssuer || 'http://localhost:3001';
|
||||
const audience = this.configService.get<string>('jwt.audience') || 'manacore';
|
||||
|
||||
const { payload } = await jwtVerify(token, this.jwks, {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { CreateGiftDto } from './dto/create-gift.dto';
|
|||
import { RedeemGiftDto } from './dto/redeem-gift.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('api/v1/gifts')
|
||||
@Controller('gifts')
|
||||
export class GiftsController {
|
||||
constructor(private readonly giftCodeService: GiftCodeService) {}
|
||||
|
||||
|
|
@ -50,11 +50,7 @@ export class GiftsController {
|
|||
@Post(':code/redeem')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async redeemGift(
|
||||
@Request() req: any,
|
||||
@Param('code') code: string,
|
||||
@Body() dto: RedeemGiftDto
|
||||
) {
|
||||
async redeemGift(@Request() req: any, @Param('code') code: string, @Body() dto: RedeemGiftDto) {
|
||||
const userId = req.user.sub;
|
||||
const userEmail = req.user.email;
|
||||
// Matrix ID would be passed in the request if coming from a Matrix bot
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue