Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions prisma/schema/creator.prisma
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
// prisma/schema/creator.prisma

model CreatorProfile {
id String @id @default(cuid())
userId String @unique
handle String @unique
displayName String
bio String?
avatarUrl String?
perkSummary String?
isVerified Boolean @default(false)
perks Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
userId String @unique
handle String @unique
displayName String
bio String?
avatarUrl String?
perkSummary String?
isVerified Boolean @default(false)
perks Json?
followersCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
priceSnapshot CreatorPriceSnapshot?
priceHistory CreatorPriceHistory[]
posts CreatorPost[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
priceSnapshot CreatorPriceSnapshot?
priceHistory CreatorPriceHistory[]
posts CreatorPost[]
followers Follow[]
}

model CreatorPost {
Expand Down
14 changes: 14 additions & 0 deletions prisma/schema/follow.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// prisma/schema/follow.prisma

model Follow {
id String @id @default(cuid())
followerAddress String
creatorId String
createdAt DateTime @default(now())

creator CreatorProfile @relation(fields: [creatorId], references: [id], onDelete: Cascade)

@@unique([followerAddress, creatorId])
@@index([creatorId])
@@index([followerAddress])
}
28 changes: 28 additions & 0 deletions src/modules/creator/creator.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
getCreatorProfileHandler,
upsertCreatorProfileHandler,
} from './creator-profile.handlers';
import {
httpFollowCreator,
httpUnfollowCreator,
} from './follow.handlers';
import { ROOT as CREATORS_ROOT } from '../../constants/creator.constants';
import { cacheControl } from '../../middlewares/cache-control.middleware';
import { CREATOR_PUBLIC_ROUTE_CACHE_PRESETS } from '../../constants/creator-public-cache.constants';
Expand Down Expand Up @@ -74,4 +78,28 @@ router.all('/:creatorId/profile', (_req, res) => {
res.set('Allow', 'GET, PUT').sendStatus(405);
});

/**
* @route POST /api/v1/creators/:creatorId/follow
* @desc Follow a creator (idempotent)
* @access Requires Stellar signature verification
*/
router.post(
'/:creatorId/follow',
validateCreatorParam('creatorId'),
requireStellarSignature(),
httpFollowCreator
);

/**
* @route DELETE /api/v1/creators/:creatorId/follow
* @desc Unfollow a creator (idempotent)
* @access Requires Stellar signature verification
*/
router.delete(
'/:creatorId/follow',
validateCreatorParam('creatorId'),
requireStellarSignature(),
httpUnfollowCreator
);

export default router;
99 changes: 99 additions & 0 deletions src/modules/creator/follow.handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Request, Response } from 'express';
import { logger } from '../../utils/logger.utils';
import {
sendSuccess,
sendError,
sendNotFound,
} from '../../utils/api-response.utils';
import { ErrorCode } from '../../constants/error.constants';
import { followCreator, unfollowCreator } from './follow.service';
import { creatorProfileExists } from './creator-profile.service';

export async function httpFollowCreator(
req: Request<{ creatorId: string }>,
res: Response
): Promise<void> {
try {
const creatorId = String(req.params.creatorId);
const walletAddress = (req as any).walletAddress;

if (!walletAddress) {
sendError(
res,
401,
ErrorCode.UNAUTHORIZED,
'Wallet address is required'
);
return;
}

const exists = await creatorProfileExists(creatorId);
if (!exists) {
sendNotFound(res, 'Creator');
return;
}

const result = await followCreator(creatorId, walletAddress);
const statusCode = result.action === 'followed' ? 201 : 200;
sendSuccess(res, result, statusCode);
} catch (error) {
logger.error(
{
type: 'follow_handler_error',
handler: 'httpFollowCreator',
error,
},
'Error following creator'
);
sendError(
res,
500,
ErrorCode.INTERNAL_ERROR,
'Failed to follow creator'
);
}
}

export async function httpUnfollowCreator(
req: Request<{ creatorId: string }>,
res: Response
): Promise<void> {
try {
const creatorId = String(req.params.creatorId);
const walletAddress = (req as any).walletAddress;

if (!walletAddress) {
sendError(
res,
401,
ErrorCode.UNAUTHORIZED,
'Wallet address is required'
);
return;
}

const exists = await creatorProfileExists(creatorId);
if (!exists) {
sendNotFound(res, 'Creator');
return;
}

const result = await unfollowCreator(creatorId, walletAddress);
sendSuccess(res, result, 200);
} catch (error) {
logger.error(
{
type: 'unfollow_handler_error',
handler: 'httpUnfollowCreator',
error,
},
'Error unfollowing creator'
);
sendError(
res,
500,
ErrorCode.INTERNAL_ERROR,
'Failed to unfollow creator'
);
}
}
Loading
Loading