Files
sexy.pivoine.art/packages/frontend/src/lib/services.ts
Valknar XXX 036fc35c9a fix: add custom endpoint for getVideoBySlug to bypass permissions
- Add GET /sexy/videos/:slug endpoint in bundle that bypasses Directus permissions
- Simplify getVideoBySlug to use the new custom endpoint instead of direct API call
- Fixes "Video not found" error on production for public video pages
- Custom endpoint uses database query like other public endpoints (/sexy/models, etc)
- Ensures videos are accessible to unauthenticated users while respecting upload_date

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:13:56 +01:00

625 lines
14 KiB
TypeScript

import { getDirectusInstance, directusApiUrl } from "$lib/directus";
import {
readItems,
registerUser,
updateMe,
readMe,
registerUserVerify,
passwordRequest,
passwordReset,
customEndpoint,
readFolders,
deleteFile,
uploadFiles,
createComment,
readComments,
aggregate,
} from "@directus/sdk";
import type { Analytics, Article, Model, Recording, Stats, User, Video, VideoLikeStatus, VideoLikeResponse, VideoPlayResponse } from "$lib/types";
import { PUBLIC_URL } from "$env/static/public";
import { logger } from "$lib/logger";
// Helper to log API calls
async function loggedApiCall<T>(
operationName: string,
operation: () => Promise<T>,
context?: Record<string, unknown>
): Promise<T> {
const startTime = Date.now();
try {
logger.debug(`🔄 API: ${operationName}`, { context });
const result = await operation();
const duration = Date.now() - startTime;
logger.info(`✅ API: ${operationName} succeeded`, { duration, context });
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`❌ API: ${operationName} failed`, {
duration,
context,
error: error instanceof Error ? error : new Error(String(error)),
});
throw error;
}
}
const userFields = [
"*",
{
avatar: ["*"],
policies: ["*", { policy: ["name", "id"] }],
role: ["*", { policies: [{ policy: ["name", "id"] }] }],
},
];
export async function isAuthenticated(token: string) {
return loggedApiCall(
"isAuthenticated",
async () => {
try {
const directus = getDirectusInstance(fetch);
directus.setToken(token);
const user = await directus.request(
readMe({
fields: userFields,
}),
);
return { authenticated: true, user };
} catch {
return { authenticated: false };
}
},
{ hasToken: !!token },
);
}
export async function register(
email: string,
password: string,
firstName: string,
lastName: string,
) {
return loggedApiCall(
"register",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(
registerUser(email, password, {
verification_url: `${PUBLIC_URL || "http://localhost:3000"}/signup/verify`,
first_name: firstName,
last_name: lastName,
}),
);
},
{ email, firstName, lastName },
);
}
export async function verify(token: string, fetch?: typeof globalThis.fetch) {
return loggedApiCall(
"verify",
async () => {
const directus = fetch
? getDirectusInstance((args) => fetch(args, { redirect: "manual" }))
: getDirectusInstance(fetch);
return directus.request(registerUserVerify(token));
},
{ hasToken: !!token },
);
}
export async function login(email: string, password: string) {
return loggedApiCall(
"login",
async () => {
const directus = getDirectusInstance(fetch);
return directus.login({ email, password });
},
{ email },
);
}
export async function logout() {
return loggedApiCall("logout", async () => {
const directus = getDirectusInstance(fetch);
return directus.logout();
});
}
export async function requestPassword(email: string) {
return loggedApiCall(
"requestPassword",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(
passwordRequest(email, `${PUBLIC_URL || "http://localhost:3000"}/password/reset`),
);
},
{ email },
);
}
export async function resetPassword(token: string, password: string) {
return loggedApiCall(
"resetPassword",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(passwordReset(token, password));
},
{ hasToken: !!token },
);
}
export async function getArticles(fetch?: typeof globalThis.fetch) {
return loggedApiCall("getArticles", async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Article[]>(
customEndpoint({
method: "GET",
path: "/sexy/articles",
}),
);
});
}
export async function getArticleBySlug(
slug: string,
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"getArticleBySlug",
async () => {
const directus = getDirectusInstance(fetch);
return directus
.request<Article[]>(
readItems("sexy_articles", {
fields: ["*", "author.*"],
filter: { slug: { _eq: slug } },
}),
)
.then((articles) => {
if (articles.length === 0) {
throw new Error("Article not found");
}
return articles[0];
});
},
{ slug },
);
}
export async function getVideos(fetch?: typeof globalThis.fetch) {
return loggedApiCall("getVideos", async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Video[]>(
customEndpoint({
method: "GET",
path: "/sexy/videos",
}),
);
});
}
export async function getVideosForModel(id, fetch?: typeof globalThis.fetch) {
return loggedApiCall(
"getVideosForModel",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Video[]>(
customEndpoint({
method: "GET",
path: `/sexy/videos?model_id=${id}`,
}),
);
},
{ modelId: id },
);
}
export async function getFeaturedVideos(
limit: number,
fetchFn: typeof globalThis.fetch = globalThis.fetch,
) {
return loggedApiCall(
"getFeaturedVideos",
async () => {
const url = `${PUBLIC_URL}/api/sexy/videos?featured=true&limit=${limit}`;
const response = await fetchFn(url);
if (!response.ok) {
throw new Error(`Failed to fetch featured videos: ${response.statusText}`);
}
return (await response.json()) as Video[];
},
{ limit },
);
}
export async function getVideoBySlug(
slug: string,
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"getVideoBySlug",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Video>(
customEndpoint({
method: "GET",
path: `/sexy/videos/${slug}`,
}),
);
},
{ slug },
);
}
export async function getModels(fetch?: typeof globalThis.fetch) {
return loggedApiCall("getModels", async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Model[]>(
customEndpoint({
method: "GET",
path: "/sexy/models",
}),
);
});
}
export async function getFeaturedModels(
limit = 3,
fetchFn: typeof globalThis.fetch = globalThis.fetch,
) {
return loggedApiCall(
"getFeaturedModels",
async () => {
const url = `${PUBLIC_URL}/api/sexy/models?featured=true&limit=${limit}`;
const response = await fetchFn(url);
if (!response.ok) {
throw new Error(`Failed to fetch featured models: ${response.statusText}`);
}
return (await response.json()) as Model[];
},
{ limit },
);
}
export async function getModelBySlug(
slug: string,
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"getModelBySlug",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Model>(
customEndpoint({
method: "GET",
path: `/sexy/models/${slug}`,
}),
);
},
{ slug },
);
}
export async function updateProfile(user: Partial<User>) {
return loggedApiCall(
"updateProfile",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<User>(updateMe(user as never));
},
{ userId: user.id },
);
}
export async function getStats(fetch?: typeof globalThis.fetch) {
return loggedApiCall("getStats", async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Stats>(
customEndpoint({
path: "/sexy/stats",
}),
);
});
}
export async function getFolders(fetch?: typeof globalThis.fetch) {
return loggedApiCall("getFolders", async () => {
const directus = getDirectusInstance(fetch);
return directus.request(readFolders());
});
}
export async function removeFile(id: string) {
return loggedApiCall(
"removeFile",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(deleteFile(id));
},
{ fileId: id },
);
}
export async function uploadFile(data: FormData) {
return loggedApiCall("uploadFile", async () => {
const directus = getDirectusInstance(fetch);
return directus.request(uploadFiles(data));
});
}
export async function createCommentForVideo(item: string, comment: string) {
return loggedApiCall(
"createCommentForVideo",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(
createComment({
collection: "sexy_videos",
item,
comment,
}),
);
},
{ videoId: item, commentLength: comment.length },
);
}
export async function getCommentsForVideo(
item: string,
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"getCommentsForVideo",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(
readComments({
fields: ["*", { user_created: ["*"] }],
filter: { collection: "sexy_videos", item },
sort: ["-date_created"],
}),
);
},
{ videoId: item },
);
}
export async function countCommentsForModel(
user_created: string,
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"countCommentsForModel",
async () => {
const directus = getDirectusInstance(fetch);
return directus
.request<[{ count: number }]>(
aggregate("directus_comments", {
aggregate: {
count: "*",
},
query: {
filter: { user_created },
},
}),
)
.then((result) => result[0].count);
},
{ userId: user_created },
);
}
export async function getItemsByTag(
category: "video" | "article" | "model",
tag: string,
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"getItemsByTag",
async () => {
switch (category) {
case "video":
return getVideos(fetch);
case "model":
return getModels(fetch);
case "article":
return getArticles(fetch);
}
},
{ category, tag },
);
}
export async function getRecordings(fetch?: typeof globalThis.fetch) {
return loggedApiCall(
"getRecordings",
async () => {
const directus = getDirectusInstance(fetch);
const response = await directus.request<Recording[]>(
customEndpoint({
method: "GET",
path: "/sexy/recordings",
}),
);
return response;
},
{},
);
}
export async function createRecording(
recording: {
title: string;
description?: string;
duration: number;
events: unknown[];
device_info: unknown[];
tags?: string[];
status?: string;
},
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"createRecording",
async () => {
const directus = getDirectusInstance(fetch);
const response = await directus.request<Recording>(
customEndpoint({
method: "POST",
path: "/sexy/recordings",
body: JSON.stringify(recording),
headers: {
"Content-Type": "application/json",
},
}),
);
return response;
},
{ title: recording.title, eventCount: recording.events.length },
);
}
export async function deleteRecording(id: string) {
return loggedApiCall(
"deleteRecording",
async () => {
const directus = getDirectusInstance();
await directus.request(
customEndpoint({
method: "DELETE",
path: `/sexy/recordings/${id}`,
}),
);
},
{ id },
);
}
export async function getRecording(id: string, fetch?: typeof globalThis.fetch) {
return loggedApiCall(
"getRecording",
async () => {
const directus = getDirectusInstance(fetch);
const response = await directus.request<Recording>(
customEndpoint({
method: "GET",
path: `/sexy/recordings/${id}`,
}),
);
return response;
},
{ id },
);
}
export async function likeVideo(videoId: string) {
return loggedApiCall(
"likeVideo",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<VideoLikeResponse>(
customEndpoint({
method: "POST",
path: `/sexy/videos/${videoId}/like`,
})
);
},
{ videoId }
);
}
export async function unlikeVideo(videoId: string) {
return loggedApiCall(
"unlikeVideo",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<VideoLikeResponse>(
customEndpoint({
method: "DELETE",
path: `/sexy/videos/${videoId}/like`,
})
);
},
{ videoId }
);
}
export async function getVideoLikeStatus(videoId: string, fetch?: typeof globalThis.fetch) {
return loggedApiCall(
"getVideoLikeStatus",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<VideoLikeStatus>(
customEndpoint({
method: "GET",
path: `/sexy/videos/${videoId}/like-status`,
})
);
},
{ videoId }
);
}
export async function recordVideoPlay(videoId: string, sessionId?: string) {
return loggedApiCall(
"recordVideoPlay",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<VideoPlayResponse>(
customEndpoint({
method: "POST",
path: `/sexy/videos/${videoId}/play`,
body: JSON.stringify({ session_id: sessionId }),
headers: { "Content-Type": "application/json" },
})
);
},
{ videoId }
);
}
export async function updateVideoPlay(
videoId: string,
playId: string,
durationWatched: number,
completed: boolean
) {
return loggedApiCall(
"updateVideoPlay",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request(
customEndpoint({
method: "PATCH",
path: `/sexy/videos/${videoId}/play/${playId}`,
body: JSON.stringify({
duration_watched: durationWatched,
completed,
}),
headers: { "Content-Type": "application/json" },
})
);
},
{ videoId, playId, durationWatched, completed }
);
}
export async function getAnalytics(fetch?: typeof globalThis.fetch) {
return loggedApiCall(
"getAnalytics",
async () => {
const directus = getDirectusInstance(fetch);
return directus.request<Analytics>(
customEndpoint({
method: "GET",
path: "/sexy/analytics",
})
);
},
{}
);
}