Skip to content

YeboID Services — Deep Dive

All business logic in YeboID is encapsulated in services. This document covers every service method with parameters, return types, and implementation details.


Table of Contents

  1. Database Service (db.ts)
  2. OTP Service (otp.ts)
  3. Token Service (token.ts)
  4. Handle Service (handle.ts)

Database Service

File: api/src/services/db.ts

Simple Prisma client singleton for database access.

typescript
import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient();

Usage

typescript
import { prisma } from '../services/db.js';

// In any route or service
const user = await prisma.user.findUnique({ where: { phone } });

OTP Service

File: api/src/services/otp.ts

Handles OTP generation, storage, verification, and SMS delivery via YeboLink.

Configuration

Env VariableDefaultDescription
OTP_EXPIRY_MINUTES5OTP validity period
OTP_LENGTH6Number of digits
YEBOLINK_API_URLhttps://api.yebolink.comSMS API URL
YEBOLINK_API_KEYRequired for production

otpService.sendOtp(phone)

Generates and sends an OTP to the given phone number.

Parameters:

ParamTypeDescription
phonestringE.164 format phone (+26878422613)

Returns:

typescript
Promise<{ expiresIn: number }> // expiresIn in seconds

Implementation:

typescript
async sendOtp(phone: string): Promise<{ expiresIn: number }> {
  // Generate OTP
  const code = generateOtp(); // Random 6-digit string
  const expiresAt = new Date(Date.now() + OTP_EXPIRY_MINUTES * 60 * 1000);
  
  // Find existing user (optional link)
  const user = await prisma.user.findUnique({ where: { phone } });
  
  // Invalidate old OTPs for this phone
  await prisma.otpCode.updateMany({
    where: { phone, verified: false },
    data: { verified: true }
  });
  
  // Store new OTP
  await prisma.otpCode.create({
    data: {
      phone,
      code,
      expiresAt,
      userId: user?.id
    }
  });
  
  // Send SMS via YeboLink
  await sendSms(phone, `Your Yebo code is: ${code}`);
  
  return { expiresIn: OTP_EXPIRY_MINUTES * 60 };
}

Notes:

  • Old unverified OTPs are invalidated before creating new one
  • If YEBOLINK_API_KEY is not set, OTP is logged to console (dev mode)

otpService.verifyOtp(phone, code)

Verifies an OTP code for the given phone number.

Parameters:

ParamTypeDescription
phonestringE.164 format phone
codestring6-digit OTP code

Returns:

typescript
Promise<boolean> // true if valid, false otherwise

Implementation:

typescript
async verifyOtp(phone: string, code: string): Promise<boolean> {
  const otp = await prisma.otpCode.findFirst({
    where: {
      phone,
      code,
      verified: false,
      expiresAt: { gt: new Date() }
    }
  });
  
  if (!otp) return false;
  
  // Mark as verified (can't reuse)
  await prisma.otpCode.update({
    where: { id: otp.id },
    data: { verified: true }
  });
  
  return true;
}

Notes:

  • OTP must be unverified and not expired
  • Once verified, OTP cannot be reused

Helper: sendSms(to, message)

Internal function to send SMS via YeboLink API.

typescript
async function sendSms(to: string, message: string): Promise<void> {
  const apiUrl = process.env.YEBOLINK_API_URL || 'https://api.yebolink.com';
  const apiKey = process.env.YEBOLINK_API_KEY;
  
  if (!apiKey) {
    console.log(`[DEV] SMS to ${to}: ${message}`);
    return;
  }
  
  try {
    const response = await fetch(`${apiUrl}/api/v1/sms/send`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
      },
      body: JSON.stringify({ to, message })
    });
    
    if (!response.ok) {
      console.error('Failed to send SMS:', await response.text());
    }
  } catch (error) {
    console.error('SMS error:', error);
  }
}

Token Service

File: api/src/services/token.ts

Handles JWT access tokens and refresh token management with rotation.

Configuration

Env VariableDefaultDescription
JWT_SECRETdev-secret-change-meMUST change in production
JWT_ACCESS_EXPIRY15mAccess token lifetime

Constants:

  • REFRESH_EXPIRY_DAYS: 7 days

Types

typescript
export interface TokenPayload {
  userId: string;      // UUID
  type: 'access' | 'refresh';
}

tokenService.generateTokens(userId)

Generates a new access/refresh token pair.

Parameters:

ParamTypeDescription
userIdstringUser's UUID

Returns:

typescript
Promise<{
  accessToken: string;    // JWT
  refreshToken: string;   // Random 128-char hex string
  expiresIn: number;      // 900 (15 min in seconds)
}>

Implementation:

typescript
async generateTokens(userId: string): Promise<{
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}> {
  // Access token (short-lived JWT)
  const accessToken = jwt.sign(
    { userId, type: 'access' } as TokenPayload,
    JWT_SECRET,
    { expiresIn: ACCESS_EXPIRY }
  );
  
  // Refresh token (long-lived, stored in DB)
  const refreshToken = crypto.randomBytes(64).toString('hex');
  const expiresAt = new Date(Date.now() + REFRESH_EXPIRY_DAYS * 24 * 60 * 60 * 1000);
  
  await prisma.refreshToken.create({
    data: {
      token: refreshToken,
      userId,
      expiresAt
    }
  });
  
  return {
    accessToken,
    refreshToken,
    expiresIn: 15 * 60 // 15 minutes
  };
}

tokenService.verifyAccessToken(token)

Verifies a JWT access token locally.

Parameters:

ParamTypeDescription
tokenstringJWT access token

Returns:

typescript
TokenPayload | null // null if invalid/expired

Implementation:

typescript
verifyAccessToken(token: string): TokenPayload | null {
  try {
    const payload = jwt.verify(token, JWT_SECRET) as TokenPayload;
    if (payload.type !== 'access') return null;
    return payload;
  } catch {
    return null;
  }
}

tokenService.refreshTokens(refreshToken)

Rotates refresh token and issues new access token.

Parameters:

ParamTypeDescription
refreshTokenstringExisting refresh token

Returns:

typescript
Promise<{
  accessToken: string;
  refreshToken: string;  // NEW token (old one is revoked)
  expiresIn: number;
} | null> // null if invalid/expired/revoked

Implementation:

typescript
async refreshTokens(refreshToken: string): Promise<{...} | null> {
  // Find valid refresh token
  const stored = await prisma.refreshToken.findUnique({
    where: { token: refreshToken }
  });
  
  if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
    return null;
  }
  
  // Revoke old token (rotation)
  await prisma.refreshToken.update({
    where: { id: stored.id },
    data: { revokedAt: new Date() }
  });
  
  // Generate new tokens
  return this.generateTokens(stored.userId);
}

Security Notes:

  • Token rotation: Old refresh token is revoked when used
  • If a refresh token is used twice, it indicates theft — both uses fail after first

tokenService.revokeRefreshToken(refreshToken)

Revokes a specific refresh token (used for logout).

Parameters:

ParamTypeDescription
refreshTokenstringToken to revoke

Returns:

typescript
Promise<void>

Implementation:

typescript
async revokeRefreshToken(refreshToken: string): Promise<void> {
  await prisma.refreshToken.updateMany({
    where: { token: refreshToken, revokedAt: null },
    data: { revokedAt: new Date() }
  });
}

Handle Service

File: api/src/services/handle.ts

Validates and manages @handle identifiers.

Reserved Words

typescript
const RESERVED_WORDS = [
  'admin', 'support', 'help', 'yebo', 'yeboid', 'official',
  'api', 'www', 'app', 'mail', 'ftp', 'blog', 'shop',
  'yebojobs', 'yeboshops', 'yebolearn', 'yebolink', 'yebona', 'yebosafe',
];

handleService.validateHandle(handle, excludeUserId?)

Validates a handle for availability and format.

Parameters:

ParamTypeDescription
handlestringHandle to validate
excludeUserIdstring?Exclude this user (for profile updates)

Returns:

typescript
Promise<{ 
  available: boolean; 
  reason?: string;  // Reason if not available
}>

Implementation:

typescript
async validateHandle(
  handle: string, 
  excludeUserId?: string
): Promise<{ available: boolean; reason?: string }> {
  // Normalize
  const normalized = handle.toLowerCase().trim();
  
  // Format validation
  if (normalized.length < 3) {
    return { available: false, reason: 'Handle must be at least 3 characters' };
  }
  
  if (normalized.length > 30) {
    return { available: false, reason: 'Handle must be 30 characters or less' };
  }
  
  if (!/^[a-z0-9_]+$/.test(normalized)) {
    return { 
      available: false, 
      reason: 'Handle can only contain letters, numbers, and underscores' 
    };
  }
  
  if (normalized.startsWith('_') || normalized.endsWith('_')) {
    return { 
      available: false, 
      reason: 'Handle cannot start or end with underscore' 
    };
  }
  
  // Reserved words check
  if (RESERVED_WORDS.includes(normalized)) {
    return { available: false, reason: 'This handle is reserved' };
  }
  
  // Check reserved handles table
  const reserved = await prisma.reservedHandle.findUnique({
    where: { handle: normalized }
  });
  
  if (reserved) {
    return { available: false, reason: 'This handle is reserved' };
  }
  
  // Check if taken by another user
  const existing = await prisma.user.findUnique({
    where: { handle: normalized }
  });
  
  if (existing && existing.id !== excludeUserId) {
    return { available: false, reason: 'This handle is already taken' };
  }
  
  return { available: true };
}

Validation Rules:

  1. 3-30 characters
  2. Lowercase letters, numbers, underscores only
  3. Cannot start or end with underscore
  4. Not in hardcoded reserved words
  5. Not in ReservedHandle table
  6. Not taken by another user

handleService.generateSuggestions(base)

Generates alternative handle suggestions.

Parameters:

ParamTypeDescription
basestringBase name to generate from

Returns:

typescript
string[] // Array of suggested handles

Implementation:

typescript
generateSuggestions(base: string): string[] {
  const normalized = base.toLowerCase().replace(/[^a-z0-9]/g, '');
  const suggestions: string[] = [];
  
  // Add random numbers
  for (let i = 1; i <= 3; i++) {
    suggestions.push(`${normalized}${Math.floor(Math.random() * 1000)}`);
  }
  
  // Add underscores
  if (normalized.length > 3) {
    suggestions.push(`${normalized}_`);
    suggestions.push(`_${normalized}`);
  }
  
  return suggestions;
}

Example Output:

javascript
handleService.generateSuggestions('laslie')
// ['laslie742', 'laslie128', 'laslie395', 'laslie_', '_laslie']

Service Exports

All services are exported as singleton objects:

typescript
// otp.ts
export const otpService = { sendOtp, verifyOtp };

// token.ts  
export const tokenService = { 
  generateTokens, 
  verifyAccessToken, 
  refreshTokens, 
  revokeRefreshToken 
};

// handle.ts
export const handleService = { 
  validateHandle, 
  generateSuggestions 
};

// db.ts
export const prisma = new PrismaClient();

One chat. Everything done.