feat: let students customize public profiles
All checks were successful
CI / Validate frontend and API (push) Successful in 1m2s
CI / Build and publish Docker images (push) Successful in 23s

This commit is contained in:
Cauê Faleiros
2026-09-08 14:46:16 -03:00
parent ad736758a7
commit 54f598cd99
7 changed files with 54 additions and 38 deletions

View File

@@ -72,6 +72,7 @@ async function assertVideoAccess(videoId: string, user: { id: string; role: stri
export const mediaRoutes: FastifyPluginAsync = async (app) => {
const manageAccess = { preHandler: app.requireRoles(['instructor', 'admin']) };
const coverAccess = { preHandler: app.authenticate };
app.get('/bunny/config', manageAccess, async () => ({
data: {
@@ -80,14 +81,14 @@ export const mediaRoutes: FastifyPluginAsync = async (app) => {
},
}));
app.get('/covers/config', manageAccess, async () => ({
app.get('/covers/config', coverAccess, async () => ({
data: {
configured: isBunnyStorageConfigured(),
maxUploadBytes: config.BUNNY_COVER_MAX_UPLOAD_MB * 1024 * 1024,
},
}));
app.post('/covers', manageAccess, async (request, reply) => {
app.post('/covers', coverAccess, async (request, reply) => {
const contentType = request.headers['content-type']?.split(';')[0]?.toLowerCase();
const body = request.body;
if (!contentType || !Buffer.isBuffer(body)) return reply.code(400).send({ error: 'Envie um arquivo de imagem.' });

View File

@@ -4,6 +4,7 @@ import { recordAudit } from '../audit.js';
import { pool } from '../db/pool.js';
import { courseSelect } from './courses.js';
const profileParamsSchema = z.object({ profileId: z.string().uuid() });
const instructorParamsSchema = z.object({ instructorId: z.string().uuid() });
const optionalUrl = z.union([z.string().url().max(500), z.literal(''), z.null()]).optional()
@@ -68,25 +69,23 @@ export const profileRoutes: FastifyPluginAsync = async (app) => {
app.patch('/me', { preHandler: app.authenticate }, async (request, reply) => {
const input = profileUpdateSchema.parse(request.body);
const isInstructor = request.user.role === 'instructor' || request.user.role === 'admin';
const result = await pool.query<ProfileRow>(
`update users set
display_name = $2,
avatar_image_url = case when $3::boolean then $4 else avatar_image_url end,
profile_headline = case when $3::boolean then $5 else profile_headline end,
profile_bio = case when $3::boolean then $6 else profile_bio end,
website_url = case when $3::boolean then $7 else website_url end,
linkedin_url = case when $3::boolean then $8 else linkedin_url end,
instagram_url = case when $3::boolean then $9 else instagram_url end,
youtube_url = case when $3::boolean then $10 else youtube_url end,
profile_is_public = case when $3::boolean then coalesce($11, profile_is_public) else profile_is_public end,
avatar_image_url = $3,
profile_headline = $4,
profile_bio = $5,
website_url = $6,
linkedin_url = $7,
instagram_url = $8,
youtube_url = $9,
profile_is_public = coalesce($10, profile_is_public),
updated_at = now()
where id = $1
returning ${profileColumns}, email`,
[
request.user.id,
input.name,
isInstructor,
input.avatarImageUrl,
input.headline,
input.bio,
@@ -103,21 +102,30 @@ export const profileRoutes: FastifyPluginAsync = async (app) => {
return { data: profile };
});
app.get('/instructors/:instructorId', async (request, reply) => {
const { instructorId } = instructorParamsSchema.parse(request.params);
const getPublicProfile = async (profileId: string, reply: { code: (status: number) => { send: (payload: object) => unknown } }, instructorOnly = false) => {
const result = await pool.query<ProfileRow>(
`select ${profileColumns}
from users
where id = $1 and profile_is_public and role in ('instructor', 'admin')`,
[instructorId],
where id = $1 and profile_is_public${instructorOnly ? " and role in ('instructor', 'admin')" : ''}`,
[profileId],
);
const profile = result.rows[0];
if (!profile) return reply.code(404).send({ error: 'Perfil de instrutor não encontrado.' });
if (!profile) return reply.code(404).send({ error: instructorOnly ? 'Perfil de instrutor não encontrado.' : 'Perfil não encontrado ou não está público.' });
const courses = await pool.query(
`${courseSelect()} where c.instructor_id = $1 and c.status = 'published' order by c.published_at desc`,
[instructorId],
);
const canPublishCourses = profile.role === 'instructor' || profile.role === 'admin';
const courses = canPublishCourses
? await pool.query(`${courseSelect()} where c.instructor_id = $1 and c.status = 'published' order by c.published_at desc`, [profileId])
: { rows: [] };
return { data: { ...profile, courses: courses.rows.map(withoutProtectedMedia) } };
};
app.get('/:profileId', async (request, reply) => {
const { profileId } = profileParamsSchema.parse(request.params);
return getPublicProfile(profileId, reply);
});
app.get('/instructors/:instructorId', async (request, reply) => {
const { instructorId } = instructorParamsSchema.parse(request.params);
return getPublicProfile(instructorId, reply, true);
});
};