diff --git a/surfsense_web/contracts/types/chat-threads.types.ts b/surfsense_web/contracts/types/chat-threads.types.ts new file mode 100644 index 000000000..e5ca183bd --- /dev/null +++ b/surfsense_web/contracts/types/chat-threads.types.ts @@ -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; +export type TogglePublicShareResponse = z.infer; diff --git a/surfsense_web/contracts/types/public-chat.types.ts b/surfsense_web/contracts/types/public-chat.types.ts new file mode 100644 index 000000000..709bedcb7 --- /dev/null +++ b/surfsense_web/contracts/types/public-chat.types.ts @@ -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; +export type PublicChatMessage = z.infer; +export type PublicChatThread = z.infer; +export type GetPublicChatRequest = z.infer; +export type GetPublicChatResponse = z.infer; +export type ClonePublicChatRequest = z.infer; +export type ClonePublicChatResponse = z.infer; diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index dcff4768b..a87d4deaf 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -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."); } diff --git a/surfsense_web/lib/apis/chat-threads-api.service.ts b/surfsense_web/lib/apis/chat-threads-api.service.ts new file mode 100644 index 000000000..9ad241c42 --- /dev/null +++ b/surfsense_web/lib/apis/chat-threads-api.service.ts @@ -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 => { + 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(); diff --git a/surfsense_web/lib/apis/public-chat-api.service.ts b/surfsense_web/lib/apis/public-chat-api.service.ts new file mode 100644 index 000000000..52a7c1363 --- /dev/null +++ b/surfsense_web/lib/apis/public-chat-api.service.ts @@ -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 => { + 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 => { + 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();