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
Database Service
File: api/src/services/db.ts
Simple Prisma client singleton for database access.
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();Usage
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 Variable | Default | Description |
|---|---|---|
OTP_EXPIRY_MINUTES | 5 | OTP validity period |
OTP_LENGTH | 6 | Number of digits |
YEBOLINK_API_URL | https://api.yebolink.com | SMS API URL |
YEBOLINK_API_KEY | — | Required for production |
otpService.sendOtp(phone)
Generates and sends an OTP to the given phone number.
Parameters:
| Param | Type | Description |
|---|---|---|
phone | string | E.164 format phone (+26878422613) |
Returns:
Promise<{ expiresIn: number }> // expiresIn in secondsImplementation:
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_KEYis not set, OTP is logged to console (dev mode)
otpService.verifyOtp(phone, code)
Verifies an OTP code for the given phone number.
Parameters:
| Param | Type | Description |
|---|---|---|
phone | string | E.164 format phone |
code | string | 6-digit OTP code |
Returns:
Promise<boolean> // true if valid, false otherwiseImplementation:
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.
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 Variable | Default | Description |
|---|---|---|
JWT_SECRET | dev-secret-change-me | MUST change in production |
JWT_ACCESS_EXPIRY | 15m | Access token lifetime |
Constants:
REFRESH_EXPIRY_DAYS: 7 days
Types
export interface TokenPayload {
userId: string; // UUID
type: 'access' | 'refresh';
}tokenService.generateTokens(userId)
Generates a new access/refresh token pair.
Parameters:
| Param | Type | Description |
|---|---|---|
userId | string | User's UUID |
Returns:
Promise<{
accessToken: string; // JWT
refreshToken: string; // Random 128-char hex string
expiresIn: number; // 900 (15 min in seconds)
}>Implementation:
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:
| Param | Type | Description |
|---|---|---|
token | string | JWT access token |
Returns:
TokenPayload | null // null if invalid/expiredImplementation:
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:
| Param | Type | Description |
|---|---|---|
refreshToken | string | Existing refresh token |
Returns:
Promise<{
accessToken: string;
refreshToken: string; // NEW token (old one is revoked)
expiresIn: number;
} | null> // null if invalid/expired/revokedImplementation:
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:
| Param | Type | Description |
|---|---|---|
refreshToken | string | Token to revoke |
Returns:
Promise<void>Implementation:
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
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:
| Param | Type | Description |
|---|---|---|
handle | string | Handle to validate |
excludeUserId | string? | Exclude this user (for profile updates) |
Returns:
Promise<{
available: boolean;
reason?: string; // Reason if not available
}>Implementation:
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:
- 3-30 characters
- Lowercase letters, numbers, underscores only
- Cannot start or end with underscore
- Not in hardcoded reserved words
- Not in
ReservedHandletable - Not taken by another user
handleService.generateSuggestions(base)
Generates alternative handle suggestions.
Parameters:
| Param | Type | Description |
|---|---|---|
base | string | Base name to generate from |
Returns:
string[] // Array of suggested handlesImplementation:
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:
handleService.generateSuggestions('laslie')
// ['laslie742', 'laslie128', 'laslie395', 'laslie_', '_laslie']Service Exports
All services are exported as singleton objects:
// 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();