managarten/services/mana-core-auth/docs/GIFT_CODES.md
Till-JS 284cd004aa 🩹 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>
2026-02-13 23:54:36 +01:00

6.2 KiB

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

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:

{
  "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)

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

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)

curl "https://auth.mana.how/api/v1/gifts/ABC123"

Response:

{
  "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

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:

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:

{
  "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)

// 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

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:

# Base URL for short links
APP_BASE_URL=https://mana.how

Gift code rules (hardcoded):

const GIFT_CODE_RULES = {
  minCredits: 1,
  maxCredits: 10000,
  maxPortions: 100,
  maxMessageLength: 500,
  maxRiddleQuestionLength: 200,
  defaultExpirationDays: 90,
};