happy-server/sources/app/api/routes/userRoutes.ts
2025-09-19 21:02:50 -07:00

208 lines
6.2 KiB
TypeScript

import { z } from "zod";
import { Fastify } from "../types";
import { db } from "@/storage/db";
import { RelationshipStatus } from "@prisma/client";
import { getPublicUrl } from "@/storage/files";
import { friendAdd } from "@/app/social/friendAdd";
import { Context } from "@/context";
import { friendRemove } from "@/app/social/friendRemove";
import { friendList } from "@/app/social/friendList";
export async function userRoutes(app: Fastify) {
// Get user profile
app.get('/v1/user/:id', {
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
user: UserProfileSchema
}),
404: z.object({
error: z.literal('User not found')
})
}
},
preHandler: app.authenticate
}, async (request, reply) => {
const { id } = request.params;
// Fetch user
const user = await db.account.findUnique({
where: {
id: id
},
include: {
githubUser: true
}
});
if (!user) {
return reply.code(404).send({ error: 'User not found' });
}
// Resolve relationship status
const relationship = await db.userRelationship.findFirst({
where: {
fromUserId: request.userId,
toUserId: id
}
});
const status: RelationshipStatus = relationship?.status || RelationshipStatus.none;
// Build user profile
return reply.send({
user: {
id: user.id,
firstName: user.firstName || '',
lastName: user.lastName,
avatar: user.avatar ? {
path: user.avatar.path,
url: getPublicUrl(user.avatar.path),
width: user.avatar.width,
height: user.avatar.height,
thumbhash: user.avatar.thumbhash
} : null,
username: user.username || (user.githubUser?.profile?.login || ''),
status: status
}
});
});
// Search for user
app.get('/v1/user/search', {
schema: {
querystring: z.object({
query: z.string()
}),
response: {
200: z.object({
user: UserProfileSchema
}),
404: z.object({
error: z.literal('User not found')
})
}
},
preHandler: app.authenticate
}, async (request, reply) => {
const { query } = request.query;
// Search for user by username or GitHub login
const user = await db.account.findFirst({
where: {
username: {
startsWith: query,
mode: 'insensitive'
}
},
include: {
githubUser: true
}
});
if (!user) {
return reply.code(404).send({ error: 'User not found' });
}
// Resolve relationship status
const relationship = await db.userRelationship.findFirst({
where: {
fromUserId: request.userId,
toUserId: user.id
}
});
const status: RelationshipStatus = relationship?.status || RelationshipStatus.none;
return reply.send({
user: {
id: user.id,
firstName: user.firstName || '',
lastName: user.lastName,
avatar: user.avatar ? {
path: user.avatar.path,
url: getPublicUrl(user.avatar.path),
width: user.avatar.width,
height: user.avatar.height,
thumbhash: user.avatar.thumbhash
} : null,
username: user.username || (user.githubUser?.profile?.login || ''),
status: status
}
});
});
// Add friend
app.post('/v1/friends/add', {
schema: {
body: z.object({
uid: z.string()
}),
response: {
200: z.object({
user: UserProfileSchema.nullable()
}),
404: z.object({
error: z.literal('User not found')
})
}
},
preHandler: app.authenticate
}, async (request, reply) => {
const user = await friendAdd(Context.create(request.userId), request.body.uid);
return reply.send({ user });
});
app.post('/v1/friends/remove', {
schema: {
body: z.object({
uid: z.string()
}),
response: {
200: z.object({
user: UserProfileSchema.nullable()
}),
404: z.object({
error: z.literal('User not found')
})
}
},
preHandler: app.authenticate
}, async (request, reply) => {
const user = await friendRemove(Context.create(request.userId), request.body.uid);
return reply.send({ user });
});
app.get('/v1/friends', {
schema: {
response: {
200: z.object({
friends: z.array(UserProfileSchema)
})
}
},
preHandler: app.authenticate
}, async (request, reply) => {
const friends = await friendList(Context.create(request.userId));
return reply.send({ friends });
});
};
// Shared Zod Schemas
const RelationshipStatusSchema = z.enum(['none', 'requested', 'pending', 'friend', 'rejected']);
const UserProfileSchema = z.object({
id: z.string(),
firstName: z.string(),
lastName: z.string().nullable(),
avatar: z.object({
path: z.string(),
url: z.string(),
width: z.number().optional(),
height: z.number().optional(),
thumbhash: z.string().optional()
}).nullable(),
username: z.string(),
status: RelationshipStatusSchema
});