feat(web): add public chat and thread API types and services

This commit is contained in:
CREDO23 2026-01-26 16:11:55 +02:00
parent aeb0deb21e
commit 9d7259aab9
5 changed files with 170 additions and 2 deletions

View file

@ -0,0 +1,19 @@
import { z } from "zod";
/**
* Toggle public share
*/
export const togglePublicShareRequest = z.object({
thread_id: z.number(),
enabled: z.boolean(),
});
export const togglePublicShareResponse = z.object({
enabled: z.boolean(),
public_url: z.string().nullable(),
share_token: z.string().nullable(),
});
// Type exports
export type TogglePublicShareRequest = z.infer<typeof togglePublicShareRequest>;
export type TogglePublicShareResponse = z.infer<typeof togglePublicShareResponse>;

View file

@ -0,0 +1,61 @@
import { z } from "zod";
/**
* Author info for public chat
*/
export const publicAuthor = z.object({
display_name: z.string().nullable(),
avatar_url: z.string().nullable(),
});
/**
* Message in a public chat
*/
export const publicChatMessage = z.object({
role: z.string(),
content: z.unknown(),
author: publicAuthor.nullable(),
created_at: z.string(),
});
/**
* Thread info for public chat
*/
export const publicChatThread = z.object({
title: z.string(),
created_at: z.string(),
});
/**
* Get public chat
*/
export const getPublicChatRequest = z.object({
share_token: z.string(),
});
export const getPublicChatResponse = z.object({
thread: publicChatThread,
messages: z.array(publicChatMessage),
});
/**
* Clone public chat
*/
export const clonePublicChatRequest = z.object({
share_token: z.string(),
});
export const clonePublicChatResponse = z.object({
status: z.string(),
task_id: z.string(),
message: z.string(),
});
// Type exports
export type PublicAuthor = z.infer<typeof publicAuthor>;
export type PublicChatMessage = z.infer<typeof publicChatMessage>;
export type PublicChatThread = z.infer<typeof publicChatThread>;
export type GetPublicChatRequest = z.infer<typeof getPublicChatRequest>;
export type GetPublicChatResponse = z.infer<typeof getPublicChatResponse>;
export type ClonePublicChatRequest = z.infer<typeof clonePublicChatRequest>;
export type ClonePublicChatResponse = z.infer<typeof clonePublicChatResponse>;

View file

@ -23,7 +23,10 @@ export type RequestOptions = {
class BaseApiService {
baseUrl: string;
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"]; // Add more endpoints as needed
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"];
// Prefixes that don't require auth (checked with startsWith)
noAuthPrefixes: string[] = ["/api/v1/public/"];
// Use a getter to always read fresh token from localStorage
// This ensures the token is always up-to-date after login/logout
@ -84,7 +87,10 @@ class BaseApiService {
}
// Validate the bearer token
if (!this.bearerToken && !this.noAuthEndpoints.includes(url)) {
const isNoAuthEndpoint =
this.noAuthEndpoints.includes(url) ||
this.noAuthPrefixes.some((prefix) => url.startsWith(prefix));
if (!this.bearerToken && !isNoAuthEndpoint) {
throw new AuthenticationError("You are not authenticated. Please login again.");
}

View file

@ -0,0 +1,33 @@
import {
type TogglePublicShareRequest,
type TogglePublicShareResponse,
togglePublicShareRequest,
togglePublicShareResponse,
} from "@/contracts/types/chat-threads.types";
import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
class ChatThreadsApiService {
/**
* Toggle public sharing for a thread.
* Requires authentication.
*/
togglePublicShare = async (
request: TogglePublicShareRequest
): Promise<TogglePublicShareResponse> => {
const parsed = togglePublicShareRequest.safeParse(request);
if (!parsed.success) {
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.patch(
`/api/v1/threads/${parsed.data.thread_id}/public-share`,
togglePublicShareResponse,
{ body: { enabled: parsed.data.enabled } }
);
};
}
export const chatThreadsApiService = new ChatThreadsApiService();

View file

@ -0,0 +1,49 @@
import {
type ClonePublicChatRequest,
type ClonePublicChatResponse,
clonePublicChatRequest,
clonePublicChatResponse,
type GetPublicChatRequest,
type GetPublicChatResponse,
getPublicChatRequest,
getPublicChatResponse,
} from "@/contracts/types/public-chat.types";
import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
class PublicChatApiService {
/**
* Get a public chat by share token.
* No authentication required.
*/
getPublicChat = async (request: GetPublicChatRequest): Promise<GetPublicChatResponse> => {
const parsed = getPublicChatRequest.safeParse(request);
if (!parsed.success) {
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get(`/api/v1/public/${parsed.data.share_token}`, getPublicChatResponse);
};
/**
* Clone a public chat to the user's account.
* Requires authentication.
*/
clonePublicChat = async (request: ClonePublicChatRequest): Promise<ClonePublicChatResponse> => {
const parsed = clonePublicChatRequest.safeParse(request);
if (!parsed.success) {
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.post(
`/api/v1/public/${parsed.data.share_token}/clone`,
clonePublicChatResponse
);
};
}
export const publicChatApiService = new PublicChatApiService();