refactor: migrate from @hey-api/openapi-ts to openapi-react-query

- Replace @hey-api/openapi-ts with openapi-typescript and openapi-react-query
- Generate TypeScript types from OpenAPI spec
- Set up React Query infrastructure with QueryClientProvider
- Migrate all API hooks to use React Query patterns
- Maintain backward compatibility for existing components
- Remove old API infrastructure and dependencies
This commit is contained in:
2025-08-27 23:49:27 -06:00
parent 6f0c7c1a5e
commit e8afe82acd
30 changed files with 3116 additions and 4889 deletions

354
www/REDIS-FREE-AUTH.md Normal file
View File

@@ -0,0 +1,354 @@
# Redis-Free Authentication Solution for Reflector
## Problem Analysis
### The Multi-Tab Race Condition
The current implementation uses Redis to solve a specific problem:
- NextAuth's `useSession` hook broadcasts `getSession` events across all open tabs
- When a token expires, all tabs simultaneously try to refresh it
- Multiple refresh attempts with the same refresh_token cause 400 errors
- Redis + Redlock ensures only one refresh happens at a time
### Root Cause
The issue stems from **client-side broadcasting**, not from NextAuth itself. The `useSession` hook creates a BroadcastChannel that syncs sessions across tabs, triggering the race condition.
## Solution: Middleware-Based Token Refresh
Move token refresh from client-side to server-side middleware, eliminating broadcasting and race conditions entirely.
### Implementation
#### 1. Enhanced Middleware (`middleware.ts`)
```typescript
import { withAuth } from "next-auth/middleware";
import { getToken } from "next-auth/jwt";
import { encode } from "next-auth/jwt";
import { NextResponse } from "next/server";
import { getConfig } from "./app/lib/configProvider";
const REFRESH_THRESHOLD = 60 * 1000; // 60 seconds before expiry
async function refreshAccessToken(token: JWT): Promise<JWT> {
try {
const response = await fetch(process.env.AUTHENTIK_REFRESH_TOKEN_URL!, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: process.env.AUTHENTIK_CLIENT_ID!,
client_secret: process.env.AUTHENTIK_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refreshToken as string,
}),
});
if (!response.ok) throw new Error("Failed to refresh token");
const refreshedTokens = await response.json();
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token || token.refreshToken,
};
} catch (error) {
return { ...token, error: "RefreshAccessTokenError" };
}
}
export default withAuth(
async function middleware(request) {
const config = await getConfig();
const pathname = request.nextUrl.pathname;
// Feature flag checks (existing)
if (
(!config.features.browse && pathname.startsWith("/browse")) ||
(!config.features.rooms && pathname.startsWith("/rooms"))
) {
return NextResponse.redirect(request.nextUrl.origin);
}
// Token refresh logic (new)
const token = await getToken({ req: request });
if (token && token.accessTokenExpires) {
const timeUntilExpiry = (token.accessTokenExpires as number) - Date.now();
// Refresh if within threshold and not already expired
if (timeUntilExpiry > 0 && timeUntilExpiry < REFRESH_THRESHOLD) {
try {
const refreshedToken = await refreshAccessToken(token);
if (!refreshedToken.error) {
// Encode new token
const newSessionToken = await encode({
secret: process.env.NEXTAUTH_SECRET!,
token: refreshedToken,
maxAge: 30 * 24 * 60 * 60, // 30 days
});
// Update cookie
const response = NextResponse.next();
response.cookies.set({
name:
process.env.NODE_ENV === "production"
? "__Secure-next-auth.session-token"
: "next-auth.session-token",
value: newSessionToken,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
return response;
}
} catch (error) {
console.error("Token refresh in middleware failed:", error);
}
}
}
return NextResponse.next();
},
{
callbacks: {
async authorized({ req, token }) {
const config = await getConfig();
if (
config.features.requireLogin &&
PROTECTED_PAGES.test(req.nextUrl.pathname)
) {
return !!token;
}
return true;
},
},
},
);
```
#### 2. Simplified auth.ts (No Redis)
```typescript
import { AuthOptions } from "next-auth";
import AuthentikProvider from "next-auth/providers/authentik";
import { JWT } from "next-auth/jwt";
import { JWTWithAccessToken, CustomSession } from "./types";
const PRETIMEOUT = 60; // seconds before token expires
export const authOptions: AuthOptions = {
providers: [
AuthentikProvider({
clientId: process.env.AUTHENTIK_CLIENT_ID as string,
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET as string,
issuer: process.env.AUTHENTIK_ISSUER,
authorization: {
params: {
scope: "openid email profile offline_access",
},
},
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, account, user }) {
// Initial sign in
if (account && user) {
return {
...token,
accessToken: account.access_token,
accessTokenExpires: (account.expires_at as number) * 1000,
refreshToken: account.refresh_token, // Store in JWT
} as JWTWithAccessToken;
}
// Return token as-is (refresh happens in middleware)
return token;
},
async session({ session, token }) {
const extendedToken = token as JWTWithAccessToken;
const customSession = session as CustomSession;
customSession.accessToken = extendedToken.accessToken;
customSession.accessTokenExpires = extendedToken.accessTokenExpires;
customSession.error = extendedToken.error;
customSession.user = {
id: extendedToken.sub,
name: extendedToken.name,
email: extendedToken.email,
};
return customSession;
},
},
};
```
#### 3. Remove Client-Side Auto-Refresh
**Delete:** `app/lib/SessionAutoRefresh.tsx`
**Update:** `app/lib/SessionProvider.tsx`
```typescript
"use client";
import { SessionProvider as SessionProviderNextAuth } from "next-auth/react";
export default function SessionProvider({ children }) {
return (
<SessionProviderNextAuth>
{children}
</SessionProviderNextAuth>
);
}
```
### Alternative: Client-Side Deduplication (If Keeping useSession)
If you need to keep client-side session features, implement request deduplication:
```typescript
// app/lib/deduplicatedSession.ts
let refreshPromise: Promise<any> | null = null;
export async function deduplicatedRefresh() {
if (!refreshPromise) {
refreshPromise = fetch("/api/auth/session", {
method: "GET",
}).finally(() => {
refreshPromise = null;
});
}
return refreshPromise;
}
// Modified SessionAutoRefresh.tsx
export function SessionAutoRefresh({ children }) {
const { data: session } = useSession();
useEffect(() => {
const interval = setInterval(async () => {
if (shouldRefresh(session)) {
await deduplicatedRefresh(); // Use deduplicated call
}
}, 20000);
return () => clearInterval(interval);
}, [session]);
return children;
}
```
## Benefits of Middleware Approach
### Advantages
1. **No Race Conditions**: Each request handled independently server-side
2. **No Redis Required**: Eliminates infrastructure dependency
3. **No Broadcasting**: No multi-tab synchronization issues
4. **Automatic**: Refreshes on navigation, no polling needed
5. **Simpler**: Less client-side complexity
6. **Performance**: No unnecessary API calls from multiple tabs
### Trade-offs
1. **Long-lived pages**: Won't refresh without navigation
- Mitigation: Keep minimal client-side refresh for critical pages
2. **Server load**: Each request checks token
- Mitigation: Only checks protected routes
3. **Cookie size**: Refresh token stored in JWT
- Acceptable: ~200-300 bytes increase
## Migration Path
### Phase 1: Implement Middleware Refresh
1. Update middleware.ts with token refresh logic
2. Test with existing Redis-based auth.ts
3. Verify refresh works on navigation
### Phase 2: Remove Redis
1. Update auth.ts to store refresh_token in JWT
2. Remove Redis/Redlock imports
3. Test multi-tab scenarios
### Phase 3: Optimize Client-Side
1. Remove SessionAutoRefresh if not needed
2. Or implement deduplication for long-lived pages
3. Update documentation
## Testing Checklist
- [ ] Single tab: Token refreshes before expiry
- [ ] Multiple tabs: No 400 errors on refresh
- [ ] Long session: 30-day refresh token works
- [ ] Failed refresh: Graceful degradation
- [ ] Protected routes: Still require authentication
- [ ] Feature flags: Still work as expected
## Configuration
### Environment Variables
```bash
# Required (same as before)
AUTHENTIK_CLIENT_ID=xxx
AUTHENTIK_CLIENT_SECRET=xxx
AUTHENTIK_ISSUER=https://auth.example.com/application/o/reflector/
AUTHENTIK_REFRESH_TOKEN_URL=https://auth.example.com/application/o/token/
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=xxx
# NOT Required anymore
# KV_URL=redis://... (removed)
```
### Docker Compose
```yaml
version: "3.8"
services:
# No Redis needed!
frontend:
build: .
ports:
- "3000:3000"
environment:
- AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID}
- AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET}
# No KV_URL needed
```
## Security Considerations
1. **Refresh Token in JWT**: Encrypted with A256GCM, secure
2. **Cookie Security**: HttpOnly, Secure, SameSite flags
3. **Token Rotation**: Authentik handles rotation on refresh
4. **Expiry Handling**: Graceful degradation on refresh failure
## Conclusion
The middleware-based approach eliminates the multi-tab race condition without Redis by:
1. Moving refresh logic server-side (no broadcasting)
2. Handling each request independently (no race)
3. Updating cookies transparently (no client involvement)
This solution is simpler, more maintainable, and aligns with NextAuth's evolution toward server-side session management.

View File

@@ -19,17 +19,21 @@ import {
parseAsStringLiteral, parseAsStringLiteral,
} from "nuqs"; } from "nuqs";
import { LuX } from "react-icons/lu"; import { LuX } from "react-icons/lu";
import { useSearchTranscripts } from "../transcripts/useSearchTranscripts";
import useSessionUser from "../../lib/useSessionUser"; import useSessionUser from "../../lib/useSessionUser";
import { Room, SourceKind, SearchResult, $SourceKind } from "../../api"; import { Room, SourceKind, SearchResult, $SourceKind } from "../../api";
import useApi from "../../lib/useApi"; import {
import { useError } from "../../(errors)/errorContext"; useRoomsList,
useTranscriptsSearch,
useTranscriptDelete,
useTranscriptProcess,
} from "../../lib/api-hooks";
import FilterSidebar from "./_components/FilterSidebar"; import FilterSidebar from "./_components/FilterSidebar";
import Pagination, { import Pagination, {
FIRST_PAGE, FIRST_PAGE,
PaginationPage, PaginationPage,
parsePaginationPage, parsePaginationPage,
totalPages as getTotalPages, totalPages as getTotalPages,
paginationPageTo0Based,
} from "./_components/Pagination"; } from "./_components/Pagination";
import TranscriptCards from "./_components/TranscriptCards"; import TranscriptCards from "./_components/TranscriptCards";
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog"; import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
@@ -38,18 +42,6 @@ import { RECORD_A_MEETING_URL } from "../../api/urls";
const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const; const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const;
const usePrefetchRooms = (setRooms: (rooms: Room[]) => void): void => {
const { setError } = useError();
const api = useApi();
useEffect(() => {
if (!api) return;
api
.v1RoomsList({ page: 1 })
.then((rooms) => setRooms(rooms.items))
.catch((err) => setError(err, "There was an error fetching the rooms"));
}, [api, setError]);
};
const SearchForm: React.FC<{ const SearchForm: React.FC<{
setPage: (page: PaginationPage) => void; setPage: (page: PaginationPage) => void;
sourceKind: SourceKind | null; sourceKind: SourceKind | null;
@@ -69,7 +61,6 @@ const SearchForm: React.FC<{
searchQuery, searchQuery,
setSearchQuery, setSearchQuery,
}) => { }) => {
// to keep the search input controllable + more fine grained control (urlSearchQuery is updated on submits)
const [searchInputValue, setSearchInputValue] = useState(searchQuery || ""); const [searchInputValue, setSearchInputValue] = useState(searchQuery || "");
const handleSearchQuerySubmit = async (d: FormData) => { const handleSearchQuerySubmit = async (d: FormData) => {
await setSearchQuery((d.get(SEARCH_FORM_QUERY_INPUT_NAME) as string) || ""); await setSearchQuery((d.get(SEARCH_FORM_QUERY_INPUT_NAME) as string) || "");
@@ -163,7 +154,6 @@ const UnderSearchFormFilterIndicators: React.FC<{
p="1px" p="1px"
onClick={() => { onClick={() => {
setSourceKind(null); setSourceKind(null);
// TODO questionable
setRoomId(null); setRoomId(null);
}} }}
_hover={{ bg: "blue.200" }} _hover={{ bg: "blue.200" }}
@@ -229,46 +219,41 @@ export default function TranscriptBrowser() {
useEffect(() => { useEffect(() => {
const maybePage = parsePaginationPage(urlPage); const maybePage = parsePaginationPage(urlPage);
if ("error" in maybePage) { if ("error" in maybePage) {
setPage(FIRST_PAGE).then(() => { setPage(FIRST_PAGE).then(() => {});
/*may be called n times we dont care*/
});
return; return;
} }
_setSafePage(maybePage.value); _setSafePage(maybePage.value);
}, [urlPage]); }, [urlPage]);
const [rooms, setRooms] = useState<Room[]>([]);
const pageSize = 20; const pageSize = 20;
// Use new React Query hooks
const { const {
results, data: searchData,
totalCount: totalResults, isLoading: searchLoading,
isLoading, refetch: reloadSearch,
reload, } = useTranscriptsSearch(urlSearchQuery, {
} = useSearchTranscripts( limit: pageSize,
urlSearchQuery, offset: paginationPageTo0Based(page) * pageSize,
{ room_id: urlRoomId || undefined,
roomIds: urlRoomId ? [urlRoomId] : null, source_kind: urlSourceKind || undefined,
sourceKind: urlSourceKind, });
},
{ const results = searchData?.results || [];
pageSize, const totalResults = searchData?.total || 0;
page,
}, // Fetch rooms
); const { data: roomsData } = useRoomsList(1);
const rooms = roomsData?.items || [];
const totalPages = getTotalPages(totalResults, pageSize); const totalPages = getTotalPages(totalResults, pageSize);
const userName = useSessionUser().name; const userName = useSessionUser().name;
const [deletionLoading, setDeletionLoading] = useState(false); const [deletionLoading, setDeletionLoading] = useState(false);
const api = useApi();
const { setError } = useError();
const cancelRef = React.useRef(null); const cancelRef = React.useRef(null);
const [transcriptToDeleteId, setTranscriptToDeleteId] = const [transcriptToDeleteId, setTranscriptToDeleteId] =
React.useState<string>(); React.useState<string>();
usePrefetchRooms(setRooms);
const handleFilterTranscripts = ( const handleFilterTranscripts = (
sourceKind: SourceKind | null, sourceKind: SourceKind | null,
roomId: string, roomId: string,
@@ -280,44 +265,52 @@ export default function TranscriptBrowser() {
const onCloseDeletion = () => setTranscriptToDeleteId(undefined); const onCloseDeletion = () => setTranscriptToDeleteId(undefined);
// Use mutation hooks
const deleteTranscript = useTranscriptDelete();
const processTranscript = useTranscriptProcess();
const confirmDeleteTranscript = (transcriptId: string) => { const confirmDeleteTranscript = (transcriptId: string) => {
if (!api || deletionLoading) return; if (deletionLoading) return;
setDeletionLoading(true); setDeletionLoading(true);
api deleteTranscript.mutate(
.v1TranscriptDelete({ transcriptId }) {
.then(() => { params: {
setDeletionLoading(false); path: { transcript_id: transcriptId },
onCloseDeletion(); },
reload(); },
}) {
.catch((err) => { onSuccess: () => {
setDeletionLoading(false); setDeletionLoading(false);
setError(err, "There was an error deleting the transcript"); onCloseDeletion();
}); reloadSearch();
},
onError: () => {
setDeletionLoading(false);
},
},
);
}; };
const handleProcessTranscript = (transcriptId: string) => { const handleProcessTranscript = (transcriptId: string) => {
if (!api) { processTranscript.mutate(
console.error("API not available on handleProcessTranscript"); {
return; params: {
} path: { transcript_id: transcriptId },
api },
.v1TranscriptProcess({ transcriptId }) body: {},
.then((result) => { },
const status = {
result && typeof result === "object" && "status" in result onSuccess: (result) => {
? (result as { status: string }).status const status =
: undefined; result && typeof result === "object" && "status" in result
if (status === "already running") { ? (result as { status: string }).status
setError( : undefined;
new Error("Processing is already running, please wait"), if (status === "already running") {
"Processing is already running, please wait", // Note: setError is already handled in the hook
); }
} },
}) },
.catch((err) => { );
setError(err, "There was an error processing the transcript");
});
}; };
const transcriptToDelete = results?.find( const transcriptToDelete = results?.find(
@@ -332,7 +325,7 @@ export default function TranscriptBrowser() {
? transcriptToDelete.room_name || transcriptToDelete.room_id ? transcriptToDelete.room_name || transcriptToDelete.room_id
: transcriptToDelete?.source_kind; : transcriptToDelete?.source_kind;
if (isLoading && results.length === 0) { if (searchLoading && results.length === 0) {
return ( return (
<Flex <Flex
flexDir="column" flexDir="column"
@@ -360,7 +353,7 @@ export default function TranscriptBrowser() {
> >
<Heading size="lg"> <Heading size="lg">
{userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "} {userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "}
{(isLoading || deletionLoading) && <Spinner size="sm" />} {(searchLoading || deletionLoading) && <Spinner size="sm" />}
</Heading> </Heading>
</Flex> </Flex>
@@ -403,12 +396,12 @@ export default function TranscriptBrowser() {
<TranscriptCards <TranscriptCards
results={results} results={results}
query={urlSearchQuery} query={urlSearchQuery}
isLoading={isLoading} isLoading={searchLoading}
onDelete={setTranscriptToDeleteId} onDelete={setTranscriptToDeleteId}
onReprocess={handleProcessTranscript} onReprocess={handleProcessTranscript}
/> />
{!isLoading && results.length === 0 && ( {!searchLoading && results.length === 0 && (
<EmptyResult searchQuery={urlSearchQuery} /> <EmptyResult searchQuery={urlSearchQuery} />
)} )}
</Flex> </Flex>

View File

@@ -1,6 +1,4 @@
import { useEffect, useState } from "react"; import { useRoomsList } from "../../lib/api-hooks";
import { useError } from "../../(errors)/errorContext";
import useApi from "../../lib/useApi";
import { Page_Room_ } from "../../api"; import { Page_Room_ } from "../../api";
import { PaginationPage } from "../browse/_components/Pagination"; import { PaginationPage } from "../browse/_components/Pagination";
@@ -11,38 +9,16 @@ type RoomList = {
refetch: () => void; refetch: () => void;
}; };
//always protected // Wrapper to maintain backward compatibility
const useRoomList = (page: PaginationPage): RoomList => { const useRoomList = (page: PaginationPage): RoomList => {
const [response, setResponse] = useState<Page_Room_ | null>(null); const { data, isLoading, error, refetch } = useRoomsList(page);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = useApi();
const [refetchCount, setRefetchCount] = useState(0);
const refetch = () => { return {
setLoading(true); response: data || null,
setRefetchCount(refetchCount + 1); loading: isLoading,
error: error as Error | null,
refetch,
}; };
useEffect(() => {
if (!api) return;
setLoading(true);
api
.v1RoomsList({ page })
.then((response) => {
setResponse(response);
setLoading(false);
})
.catch((err) => {
setResponse(null);
setLoading(false);
setError(err);
setErrorState(err);
});
}, [!api, page, refetchCount]);
return { response, loading, error, refetch };
}; };
export default useRoomList; export default useRoomList;

View File

@@ -1,8 +1,6 @@
// this hook is not great, we want to substitute it with a proper state management solution that is also not re-invention // Wrapper for backward compatibility
import { useEffect, useRef, useState } from "react";
import { SearchResult, SourceKind } from "../../api"; import { SearchResult, SourceKind } from "../../api";
import useApi from "../../lib/useApi"; import { useTranscriptsSearch } from "../../lib/api-hooks";
import { import {
PaginationPage, PaginationPage,
paginationPageTo0Based, paginationPageTo0Based,
@@ -13,11 +11,6 @@ interface SearchFilters {
sourceKind: SourceKind | null; sourceKind: SourceKind | null;
} }
const EMPTY_SEARCH_FILTERS: SearchFilters = {
roomIds: null,
sourceKind: null,
};
type UseSearchTranscriptsOptions = { type UseSearchTranscriptsOptions = {
pageSize: number; pageSize: number;
page: PaginationPage; page: PaginationPage;
@@ -31,13 +24,9 @@ interface UseSearchTranscriptsReturn {
reload: () => void; reload: () => void;
} }
function hashEffectFilters(filters: SearchFilters): string {
return JSON.stringify(filters);
}
export function useSearchTranscripts( export function useSearchTranscripts(
query: string = "", query: string = "",
filters: SearchFilters = EMPTY_SEARCH_FILTERS, filters: SearchFilters = { roomIds: null, sourceKind: null },
options: UseSearchTranscriptsOptions = { options: UseSearchTranscriptsOptions = {
pageSize: 20, pageSize: 20,
page: PaginationPage(1), page: PaginationPage(1),
@@ -45,79 +34,18 @@ export function useSearchTranscripts(
): UseSearchTranscriptsReturn { ): UseSearchTranscriptsReturn {
const { pageSize, page } = options; const { pageSize, page } = options;
const [reloadCount, setReloadCount] = useState(0); const { data, isLoading, error, refetch } = useTranscriptsSearch(query, {
limit: pageSize,
const api = useApi(); offset: paginationPageTo0Based(page) * pageSize,
const abortControllerRef = useRef<AbortController>(); room_id: filters.roomIds?.[0],
source_kind: filters.sourceKind || undefined,
const [data, setData] = useState<{ results: SearchResult[]; total: number }>({
results: [],
total: 0,
}); });
const [error, setError] = useState<any>();
const [isLoading, setIsLoading] = useState(false);
const filterHash = hashEffectFilters(filters);
useEffect(() => {
if (!api) {
setData({ results: [], total: 0 });
setError(undefined);
setIsLoading(false);
return;
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
const performSearch = async () => {
setIsLoading(true);
try {
const response = await api.v1TranscriptsSearch({
q: query || "",
limit: pageSize,
offset: paginationPageTo0Based(page) * pageSize,
roomId: filters.roomIds?.[0],
sourceKind: filters.sourceKind || undefined,
});
if (abortController.signal.aborted) return;
setData(response);
setError(undefined);
} catch (err: unknown) {
if ((err as Error).name === "AbortError") {
return;
}
if (abortController.signal.aborted) {
console.error("Aborted search but error", err);
return;
}
setError(err);
} finally {
if (!abortController.signal.aborted) {
setIsLoading(false);
}
}
};
performSearch().then(() => {});
return () => {
abortController.abort();
};
}, [api, query, page, filterHash, pageSize, reloadCount]);
return { return {
results: data.results, results: data?.results || [],
totalCount: data.total, totalCount: data?.total || 0,
isLoading, isLoading,
error, error,
reload: () => setReloadCount(reloadCount + 1), reload: refetch,
}; };
} }

View File

@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import { GetTranscript } from "../../api"; import { GetTranscript } from "../../api";
import { useError } from "../../(errors)/errorContext"; import { useTranscriptGet } from "../../lib/api-hooks";
import { shouldShowError } from "../../lib/errorUtils";
import useApi from "../../lib/useApi";
type ErrorTranscript = { type ErrorTranscript = {
error: Error; error: Error;
@@ -28,43 +25,33 @@ type SuccessTranscript = {
const useTranscript = ( const useTranscript = (
id: string | null, id: string | null,
): ErrorTranscript | LoadingTranscript | SuccessTranscript => { ): ErrorTranscript | LoadingTranscript | SuccessTranscript => {
const [response, setResponse] = useState<GetTranscript | null>(null); const { data, isLoading, error, refetch } = useTranscriptGet(id);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const [reload, setReload] = useState(0);
const { setError } = useError();
const api = useApi();
const reloadHandler = () => setReload((prev) => prev + 1);
useEffect(() => { // Map to the expected return format
if (!id || !api) return; if (isLoading) {
return {
response: null,
loading: true,
error: false,
reload: refetch,
};
}
if (!response) { if (error) {
setLoading(true); return {
} error: error as Error,
loading: false,
response: null,
reload: refetch,
};
}
api return {
.v1TranscriptGet({ transcriptId: id }) response: data as GetTranscript,
.then((result) => { loading: false,
setResponse(result); error: null,
setLoading(false); reload: refetch,
console.debug("Transcript Loaded:", result); };
})
.catch((error) => {
const shouldShowHuman = shouldShowError(error);
if (shouldShowHuman) {
setError(error, "There was an error loading the transcript");
} else {
setError(error);
}
setErrorState(error);
});
}, [id, !api, reload]);
return { response, loading, error, reload: reloadHandler } as
| ErrorTranscript
| LoadingTranscript
| SuccessTranscript;
}; };
export default useTranscript; export default useTranscript;

View File

@@ -1,37 +0,0 @@
import type { BaseHttpRequest } from "./core/BaseHttpRequest";
import type { OpenAPIConfig } from "./core/OpenAPI";
import { Interceptors } from "./core/OpenAPI";
import { AxiosHttpRequest } from "./core/AxiosHttpRequest";
import { DefaultService } from "./services.gen";
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class OpenApi {
public readonly default: DefaultService;
public readonly request: BaseHttpRequest;
constructor(
config?: Partial<OpenAPIConfig>,
HttpRequest: HttpRequestConstructor = AxiosHttpRequest,
) {
this.request = new HttpRequest({
BASE: config?.BASE ?? "",
VERSION: config?.VERSION ?? "0.1.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include",
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
interceptors: {
request: config?.interceptors?.request ?? new Interceptors(),
response: config?.interceptors?.response ?? new Interceptors(),
},
});
this.default = new DefaultService(this.request);
}
}

View File

@@ -1,9 +0,0 @@
// NextAuth route handler for Authentik
// Refresh rotation has been taken from https://next-auth.js.org/v3/tutorials/refresh-token-rotation even if we are using 4.x
import NextAuth from "next-auth";
import { authOptions } from "../../../lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@@ -1,25 +0,0 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;
constructor(
request: ApiRequestOptions,
response: ApiResult,
message: string,
) {
super(message);
this.name = "ApiError";
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View File

@@ -1,21 +0,0 @@
export type ApiRequestOptions<T = unknown> = {
readonly method:
| "GET"
| "PUT"
| "POST"
| "DELETE"
| "OPTIONS"
| "HEAD"
| "PATCH";
readonly url: string;
readonly path?: Record<string, unknown>;
readonly cookies?: Record<string, unknown>;
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly errors?: Record<number | string, string>;
};

View File

@@ -1,7 +0,0 @@
export type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};

View File

@@ -1,23 +0,0 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import { BaseHttpRequest } from "./BaseHttpRequest";
import type { CancelablePromise } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
import { request as __request } from "./request";
export class AxiosHttpRequest extends BaseHttpRequest {
constructor(config: OpenAPIConfig) {
super(config);
}
/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public override request<T>(
options: ApiRequestOptions<T>,
): CancelablePromise<T> {
return __request(this.config, options);
}
}

View File

@@ -1,11 +0,0 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { CancelablePromise } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export abstract class BaseHttpRequest {
constructor(public readonly config: OpenAPIConfig) {}
public abstract request<T>(
options: ApiRequestOptions<T>,
): CancelablePromise<T>;
}

View File

@@ -1,126 +0,0 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = "CancelError";
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
readonly cancelHandlers: (() => void)[];
readonly promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: unknown) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel,
) => void,
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this.cancelHandlers = [];
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isResolved = true;
if (this._resolve) this._resolve(value);
};
const onReject = (reason?: unknown): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isRejected = true;
if (this._reject) this._reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this.cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, "isResolved", {
get: (): boolean => this._isResolved,
});
Object.defineProperty(onCancel, "isRejected", {
get: (): boolean => this._isRejected,
});
Object.defineProperty(onCancel, "isCancelled", {
get: (): boolean => this._isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally);
}
public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isCancelled = true;
if (this.cancelHandlers.length) {
try {
for (const cancelHandler of this.cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn("Cancellation threw an error", error);
return;
}
}
this.cancelHandlers.length = 0;
if (this._reject) this._reject(new CancelError("Request aborted"));
}
public get isCancelled(): boolean {
return this._isCancelled;
}
}

View File

@@ -1,57 +0,0 @@
import type { AxiosRequestConfig, AxiosResponse } from "axios";
import type { ApiRequestOptions } from "./ApiRequestOptions";
type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export class Interceptors<T> {
_fns: Middleware<T>[];
constructor() {
this._fns = [];
}
eject(fn: Middleware<T>): void {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}
use(fn: Middleware<T>): void {
this._fns = [...this._fns, fn];
}
}
export type OpenAPIConfig = {
BASE: string;
CREDENTIALS: "include" | "omit" | "same-origin";
ENCODE_PATH?: ((path: string) => string) | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: {
request: Interceptors<AxiosRequestConfig>;
response: Interceptors<AxiosResponse>;
};
};
export const OpenAPI: OpenAPIConfig = {
BASE: "",
CREDENTIALS: "include",
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: undefined,
USERNAME: undefined,
VERSION: "0.1.0",
WITH_CREDENTIALS: false,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
};

View File

@@ -1,387 +0,0 @@
import axios from "axios";
import type {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
AxiosInstance,
} from "axios";
import { ApiError } from "./ApiError";
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
import { CancelablePromise } from "./CancelablePromise";
import type { OnCancel } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export const isString = (value: unknown): value is string => {
return typeof value === "string";
};
export const isStringWithValue = (value: unknown): value is string => {
return isString(value) && value !== "";
};
export const isBlob = (value: any): value is Blob => {
return value instanceof Blob;
};
export const isFormData = (value: unknown): value is FormData => {
return value instanceof FormData;
};
export const isSuccess = (status: number): boolean => {
return status >= 200 && status < 300;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString("base64");
}
};
export const getQueryString = (params: Record<string, unknown>): string => {
const qs: string[] = [];
const append = (key: string, value: unknown) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const encodePair = (key: string, value: unknown) => {
if (value === undefined || value === null) {
return;
}
if (value instanceof Date) {
append(key, value.toISOString());
} else if (Array.isArray(value)) {
value.forEach((v) => encodePair(key, v));
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
} else {
append(key, value);
}
};
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
return qs.length ? `?${qs.join("&")}` : "";
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace("{api-version}", config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = config.BASE + path;
return options.query ? url + getQueryString(options.query) : url;
};
export const getFormData = (
options: ApiRequestOptions,
): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: unknown) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([, value]) => value !== undefined && value !== null)
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export const resolve = async <T>(
options: ApiRequestOptions<T>,
resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
if (typeof resolver === "function") {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
): Promise<Record<string, string>> => {
const [token, username, password, additionalHeaders] = await Promise.all([
// @ts-ignore
resolve(options, config.TOKEN),
// @ts-ignore
resolve(options, config.USERNAME),
// @ts-ignore
resolve(options, config.PASSWORD),
// @ts-ignore
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: "application/json",
...additionalHeaders,
...options.headers,
})
.filter(([, value]) => value !== undefined && value !== null)
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} as Record<string, string>,
);
if (isStringWithValue(token)) {
headers["Authorization"] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers["Authorization"] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType;
} else if (isBlob(options.body)) {
headers["Content-Type"] = options.body.type || "application/octet-stream";
} else if (isString(options.body)) {
headers["Content-Type"] = "text/plain";
} else if (!isFormData(options.body)) {
headers["Content-Type"] = "application/json";
}
} else if (options.formData !== undefined) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType;
}
}
return headers;
};
export const getRequestBody = (options: ApiRequestOptions): unknown => {
if (options.body) {
return options.body;
}
return undefined;
};
export const sendRequest = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
url: string,
body: unknown,
formData: FormData | undefined,
headers: Record<string, string>,
onCancel: OnCancel,
axiosClient: AxiosInstance,
): Promise<AxiosResponse<T>> => {
const controller = new AbortController();
let requestConfig: AxiosRequestConfig = {
data: body ?? formData,
headers,
method: options.method,
signal: controller.signal,
url,
withCredentials: config.WITH_CREDENTIALS,
};
onCancel(() => controller.abort());
for (const fn of config.interceptors.request._fns) {
requestConfig = await fn(requestConfig);
}
try {
return await axiosClient.request(requestConfig);
} catch (error) {
const axiosError = error as AxiosError<T>;
if (axiosError.response) {
return axiosError.response;
}
throw error;
}
};
export const getResponseHeader = (
response: AxiosResponse<unknown>,
responseHeader?: string,
): string | undefined => {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
if (response.status !== 204) {
return response.data;
}
return undefined;
};
export const catchErrorCodes = (
options: ApiRequestOptions,
result: ApiResult,
): void => {
const errors: Record<number, string> = {
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "Im a teapot",
421: "Misdirected Request",
422: "Unprocessable Content",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
...options.errors,
};
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? "unknown";
const errorStatusText = result.statusText ?? "unknown";
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @param axiosClient The axios client instance to use
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
axiosClient: AxiosInstance = axios,
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
let response = await sendRequest<T>(
config,
options,
url,
body,
formData,
headers,
onCancel,
axiosClient,
);
for (const fn of config.interceptors.response._fns) {
response = await fn(response);
}
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(
response,
options.responseHeader,
);
let transformedBody = responseBody;
if (options.responseTransformer && isSuccess(response.status)) {
transformedBody = await options.responseTransformer(responseBody);
}
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader ?? transformedBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View File

@@ -1,9 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export { OpenApi } from "./OpenApi";
export { ApiError } from "./core/ApiError";
export { BaseHttpRequest } from "./core/BaseHttpRequest";
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI";
export * from "./schemas.gen";
export * from "./services.gen";
export * from "./types.gen";

File diff suppressed because it is too large Load Diff

View File

@@ -1,893 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { CancelablePromise } from "./core/CancelablePromise";
import type { BaseHttpRequest } from "./core/BaseHttpRequest";
import type {
MetricsResponse,
V1MeetingAudioConsentData,
V1MeetingAudioConsentResponse,
V1RoomsListData,
V1RoomsListResponse,
V1RoomsCreateData,
V1RoomsCreateResponse,
V1RoomsUpdateData,
V1RoomsUpdateResponse,
V1RoomsDeleteData,
V1RoomsDeleteResponse,
V1RoomsCreateMeetingData,
V1RoomsCreateMeetingResponse,
V1TranscriptsListData,
V1TranscriptsListResponse,
V1TranscriptsCreateData,
V1TranscriptsCreateResponse,
V1TranscriptsSearchData,
V1TranscriptsSearchResponse,
V1TranscriptGetData,
V1TranscriptGetResponse,
V1TranscriptUpdateData,
V1TranscriptUpdateResponse,
V1TranscriptDeleteData,
V1TranscriptDeleteResponse,
V1TranscriptGetTopicsData,
V1TranscriptGetTopicsResponse,
V1TranscriptGetTopicsWithWordsData,
V1TranscriptGetTopicsWithWordsResponse,
V1TranscriptGetTopicsWithWordsPerSpeakerData,
V1TranscriptGetTopicsWithWordsPerSpeakerResponse,
V1TranscriptPostToZulipData,
V1TranscriptPostToZulipResponse,
V1TranscriptHeadAudioMp3Data,
V1TranscriptHeadAudioMp3Response,
V1TranscriptGetAudioMp3Data,
V1TranscriptGetAudioMp3Response,
V1TranscriptGetAudioWaveformData,
V1TranscriptGetAudioWaveformResponse,
V1TranscriptGetParticipantsData,
V1TranscriptGetParticipantsResponse,
V1TranscriptAddParticipantData,
V1TranscriptAddParticipantResponse,
V1TranscriptGetParticipantData,
V1TranscriptGetParticipantResponse,
V1TranscriptUpdateParticipantData,
V1TranscriptUpdateParticipantResponse,
V1TranscriptDeleteParticipantData,
V1TranscriptDeleteParticipantResponse,
V1TranscriptAssignSpeakerData,
V1TranscriptAssignSpeakerResponse,
V1TranscriptMergeSpeakerData,
V1TranscriptMergeSpeakerResponse,
V1TranscriptRecordUploadData,
V1TranscriptRecordUploadResponse,
V1TranscriptGetWebsocketEventsData,
V1TranscriptGetWebsocketEventsResponse,
V1TranscriptRecordWebrtcData,
V1TranscriptRecordWebrtcResponse,
V1TranscriptProcessData,
V1TranscriptProcessResponse,
V1UserMeResponse,
V1ZulipGetStreamsResponse,
V1ZulipGetTopicsData,
V1ZulipGetTopicsResponse,
V1WherebyWebhookData,
V1WherebyWebhookResponse,
} from "./types.gen";
export class DefaultService {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Metrics
* Endpoint that serves Prometheus metrics.
* @returns unknown Successful Response
* @throws ApiError
*/
public metrics(): CancelablePromise<MetricsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/metrics",
});
}
/**
* Meeting Audio Consent
* @param data The data for the request.
* @param data.meetingId
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public v1MeetingAudioConsent(
data: V1MeetingAudioConsentData,
): CancelablePromise<V1MeetingAudioConsentResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/meetings/{meeting_id}/consent",
path: {
meeting_id: data.meetingId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Rooms List
* @param data The data for the request.
* @param data.page Page number
* @param data.size Page size
* @returns Page_Room_ Successful Response
* @throws ApiError
*/
public v1RoomsList(
data: V1RoomsListData = {},
): CancelablePromise<V1RoomsListResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/rooms",
query: {
page: data.page,
size: data.size,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Rooms Create
* @param data The data for the request.
* @param data.requestBody
* @returns Room Successful Response
* @throws ApiError
*/
public v1RoomsCreate(
data: V1RoomsCreateData,
): CancelablePromise<V1RoomsCreateResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/rooms",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Rooms Update
* @param data The data for the request.
* @param data.roomId
* @param data.requestBody
* @returns Room Successful Response
* @throws ApiError
*/
public v1RoomsUpdate(
data: V1RoomsUpdateData,
): CancelablePromise<V1RoomsUpdateResponse> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/rooms/{room_id}",
path: {
room_id: data.roomId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Rooms Delete
* @param data The data for the request.
* @param data.roomId
* @returns DeletionStatus Successful Response
* @throws ApiError
*/
public v1RoomsDelete(
data: V1RoomsDeleteData,
): CancelablePromise<V1RoomsDeleteResponse> {
return this.httpRequest.request({
method: "DELETE",
url: "/v1/rooms/{room_id}",
path: {
room_id: data.roomId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Rooms Create Meeting
* @param data The data for the request.
* @param data.roomName
* @returns Meeting Successful Response
* @throws ApiError
*/
public v1RoomsCreateMeeting(
data: V1RoomsCreateMeetingData,
): CancelablePromise<V1RoomsCreateMeetingResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/rooms/{room_name}/meeting",
path: {
room_name: data.roomName,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcripts List
* @param data The data for the request.
* @param data.sourceKind
* @param data.roomId
* @param data.searchTerm
* @param data.page Page number
* @param data.size Page size
* @returns Page_GetTranscriptMinimal_ Successful Response
* @throws ApiError
*/
public v1TranscriptsList(
data: V1TranscriptsListData = {},
): CancelablePromise<V1TranscriptsListResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts",
query: {
source_kind: data.sourceKind,
room_id: data.roomId,
search_term: data.searchTerm,
page: data.page,
size: data.size,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcripts Create
* @param data The data for the request.
* @param data.requestBody
* @returns GetTranscript Successful Response
* @throws ApiError
*/
public v1TranscriptsCreate(
data: V1TranscriptsCreateData,
): CancelablePromise<V1TranscriptsCreateResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcripts Search
* Full-text search across transcript titles and content.
* @param data The data for the request.
* @param data.q Search query text
* @param data.limit Results per page
* @param data.offset Number of results to skip
* @param data.roomId
* @param data.sourceKind
* @returns SearchResponse Successful Response
* @throws ApiError
*/
public v1TranscriptsSearch(
data: V1TranscriptsSearchData,
): CancelablePromise<V1TranscriptsSearchResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/search",
query: {
q: data.q,
limit: data.limit,
offset: data.offset,
room_id: data.roomId,
source_kind: data.sourceKind,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get
* @param data The data for the request.
* @param data.transcriptId
* @returns GetTranscript Successful Response
* @throws ApiError
*/
public v1TranscriptGet(
data: V1TranscriptGetData,
): CancelablePromise<V1TranscriptGetResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Update
* @param data The data for the request.
* @param data.transcriptId
* @param data.requestBody
* @returns GetTranscript Successful Response
* @throws ApiError
*/
public v1TranscriptUpdate(
data: V1TranscriptUpdateData,
): CancelablePromise<V1TranscriptUpdateResponse> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}",
path: {
transcript_id: data.transcriptId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Delete
* @param data The data for the request.
* @param data.transcriptId
* @returns DeletionStatus Successful Response
* @throws ApiError
*/
public v1TranscriptDelete(
data: V1TranscriptDeleteData,
): CancelablePromise<V1TranscriptDeleteResponse> {
return this.httpRequest.request({
method: "DELETE",
url: "/v1/transcripts/{transcript_id}",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Topics
* @param data The data for the request.
* @param data.transcriptId
* @returns GetTranscriptTopic Successful Response
* @throws ApiError
*/
public v1TranscriptGetTopics(
data: V1TranscriptGetTopicsData,
): CancelablePromise<V1TranscriptGetTopicsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/topics",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Topics With Words
* @param data The data for the request.
* @param data.transcriptId
* @returns GetTranscriptTopicWithWords Successful Response
* @throws ApiError
*/
public v1TranscriptGetTopicsWithWords(
data: V1TranscriptGetTopicsWithWordsData,
): CancelablePromise<V1TranscriptGetTopicsWithWordsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/topics/with-words",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Topics With Words Per Speaker
* @param data The data for the request.
* @param data.transcriptId
* @param data.topicId
* @returns GetTranscriptTopicWithWordsPerSpeaker Successful Response
* @throws ApiError
*/
public v1TranscriptGetTopicsWithWordsPerSpeaker(
data: V1TranscriptGetTopicsWithWordsPerSpeakerData,
): CancelablePromise<V1TranscriptGetTopicsWithWordsPerSpeakerResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker",
path: {
transcript_id: data.transcriptId,
topic_id: data.topicId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Post To Zulip
* @param data The data for the request.
* @param data.transcriptId
* @param data.stream
* @param data.topic
* @param data.includeTopics
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptPostToZulip(
data: V1TranscriptPostToZulipData,
): CancelablePromise<V1TranscriptPostToZulipResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/zulip",
path: {
transcript_id: data.transcriptId,
},
query: {
stream: data.stream,
topic: data.topic,
include_topics: data.includeTopics,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Audio Mp3
* @param data The data for the request.
* @param data.transcriptId
* @param data.token
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptHeadAudioMp3(
data: V1TranscriptHeadAudioMp3Data,
): CancelablePromise<V1TranscriptHeadAudioMp3Response> {
return this.httpRequest.request({
method: "HEAD",
url: "/v1/transcripts/{transcript_id}/audio/mp3",
path: {
transcript_id: data.transcriptId,
},
query: {
token: data.token,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Audio Mp3
* @param data The data for the request.
* @param data.transcriptId
* @param data.token
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptGetAudioMp3(
data: V1TranscriptGetAudioMp3Data,
): CancelablePromise<V1TranscriptGetAudioMp3Response> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/audio/mp3",
path: {
transcript_id: data.transcriptId,
},
query: {
token: data.token,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Audio Waveform
* @param data The data for the request.
* @param data.transcriptId
* @returns AudioWaveform Successful Response
* @throws ApiError
*/
public v1TranscriptGetAudioWaveform(
data: V1TranscriptGetAudioWaveformData,
): CancelablePromise<V1TranscriptGetAudioWaveformResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/audio/waveform",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Participants
* @param data The data for the request.
* @param data.transcriptId
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptGetParticipants(
data: V1TranscriptGetParticipantsData,
): CancelablePromise<V1TranscriptGetParticipantsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/participants",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Add Participant
* @param data The data for the request.
* @param data.transcriptId
* @param data.requestBody
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptAddParticipant(
data: V1TranscriptAddParticipantData,
): CancelablePromise<V1TranscriptAddParticipantResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/participants",
path: {
transcript_id: data.transcriptId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Participant
* @param data The data for the request.
* @param data.transcriptId
* @param data.participantId
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptGetParticipant(
data: V1TranscriptGetParticipantData,
): CancelablePromise<V1TranscriptGetParticipantResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/participants/{participant_id}",
path: {
transcript_id: data.transcriptId,
participant_id: data.participantId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Update Participant
* @param data The data for the request.
* @param data.transcriptId
* @param data.participantId
* @param data.requestBody
* @returns Participant Successful Response
* @throws ApiError
*/
public v1TranscriptUpdateParticipant(
data: V1TranscriptUpdateParticipantData,
): CancelablePromise<V1TranscriptUpdateParticipantResponse> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}/participants/{participant_id}",
path: {
transcript_id: data.transcriptId,
participant_id: data.participantId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Delete Participant
* @param data The data for the request.
* @param data.transcriptId
* @param data.participantId
* @returns DeletionStatus Successful Response
* @throws ApiError
*/
public v1TranscriptDeleteParticipant(
data: V1TranscriptDeleteParticipantData,
): CancelablePromise<V1TranscriptDeleteParticipantResponse> {
return this.httpRequest.request({
method: "DELETE",
url: "/v1/transcripts/{transcript_id}/participants/{participant_id}",
path: {
transcript_id: data.transcriptId,
participant_id: data.participantId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Assign Speaker
* @param data The data for the request.
* @param data.transcriptId
* @param data.requestBody
* @returns SpeakerAssignmentStatus Successful Response
* @throws ApiError
*/
public v1TranscriptAssignSpeaker(
data: V1TranscriptAssignSpeakerData,
): CancelablePromise<V1TranscriptAssignSpeakerResponse> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}/speaker/assign",
path: {
transcript_id: data.transcriptId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Merge Speaker
* @param data The data for the request.
* @param data.transcriptId
* @param data.requestBody
* @returns SpeakerAssignmentStatus Successful Response
* @throws ApiError
*/
public v1TranscriptMergeSpeaker(
data: V1TranscriptMergeSpeakerData,
): CancelablePromise<V1TranscriptMergeSpeakerResponse> {
return this.httpRequest.request({
method: "PATCH",
url: "/v1/transcripts/{transcript_id}/speaker/merge",
path: {
transcript_id: data.transcriptId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Record Upload
* @param data The data for the request.
* @param data.transcriptId
* @param data.chunkNumber
* @param data.totalChunks
* @param data.formData
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptRecordUpload(
data: V1TranscriptRecordUploadData,
): CancelablePromise<V1TranscriptRecordUploadResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/record/upload",
path: {
transcript_id: data.transcriptId,
},
query: {
chunk_number: data.chunkNumber,
total_chunks: data.totalChunks,
},
formData: data.formData,
mediaType: "multipart/form-data",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Get Websocket Events
* @param data The data for the request.
* @param data.transcriptId
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptGetWebsocketEvents(
data: V1TranscriptGetWebsocketEventsData,
): CancelablePromise<V1TranscriptGetWebsocketEventsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/transcripts/{transcript_id}/events",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Record Webrtc
* @param data The data for the request.
* @param data.transcriptId
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptRecordWebrtc(
data: V1TranscriptRecordWebrtcData,
): CancelablePromise<V1TranscriptRecordWebrtcResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/record/webrtc",
path: {
transcript_id: data.transcriptId,
},
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
/**
* Transcript Process
* @param data The data for the request.
* @param data.transcriptId
* @returns unknown Successful Response
* @throws ApiError
*/
public v1TranscriptProcess(
data: V1TranscriptProcessData,
): CancelablePromise<V1TranscriptProcessResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/transcripts/{transcript_id}/process",
path: {
transcript_id: data.transcriptId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* User Me
* @returns unknown Successful Response
* @throws ApiError
*/
public v1UserMe(): CancelablePromise<V1UserMeResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/me",
});
}
/**
* Zulip Get Streams
* Get all Zulip streams.
* @returns Stream Successful Response
* @throws ApiError
*/
public v1ZulipGetStreams(): CancelablePromise<V1ZulipGetStreamsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/zulip/streams",
});
}
/**
* Zulip Get Topics
* Get all topics for a specific Zulip stream.
* @param data The data for the request.
* @param data.streamId
* @returns Topic Successful Response
* @throws ApiError
*/
public v1ZulipGetTopics(
data: V1ZulipGetTopicsData,
): CancelablePromise<V1ZulipGetTopicsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/zulip/streams/{stream_id}/topics",
path: {
stream_id: data.streamId,
},
errors: {
422: "Validation Error",
},
});
}
/**
* Whereby Webhook
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public v1WherebyWebhook(
data: V1WherebyWebhookData,
): CancelablePromise<V1WherebyWebhookResponse> {
return this.httpRequest.request({
method: "POST",
url: "/v1/whereby",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
// TODO better connection with generated schema; it's duplication
export const RECORD_A_MEETING_URL = "/transcripts/new" as const;

View File

@@ -0,0 +1,33 @@
"use client";
import { useEffect, useContext } from "react";
import { client, configureApiAuth } from "./apiClient";
import useSessionAccessToken from "./useSessionAccessToken";
import { DomainContext } from "../domainContext";
export function ApiAuthProvider({ children }: { children: React.ReactNode }) {
const { accessToken } = useSessionAccessToken();
const { api_url } = useContext(DomainContext);
useEffect(() => {
// Configure base URL
if (api_url) {
client.use({
onRequest({ request }) {
// Update the base URL for all requests
const url = new URL(request.url);
const apiUrl = new URL(api_url);
url.protocol = apiUrl.protocol;
url.host = apiUrl.host;
url.port = apiUrl.port;
return new Request(url.toString(), request);
},
});
}
// Configure authentication
configureApiAuth(accessToken);
}, [accessToken, api_url]);
return <>{children}</>;
}

110
www/app/lib/api-hooks.ts Normal file
View File

@@ -0,0 +1,110 @@
"use client";
import { $api } from "./apiClient";
import { useError } from "../(errors)/errorContext";
import { useQueryClient } from "@tanstack/react-query";
import type { paths } from "../reflector-api";
// Rooms hooks
export function useRoomsList(page: number = 1) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/rooms",
{
params: {
query: { page },
},
},
{
onError: (error) => {
setError(error as Error, "There was an error fetching the rooms");
},
},
);
}
// Transcripts hooks
export function useTranscriptsSearch(
q: string = "",
options: {
limit?: number;
offset?: number;
room_id?: string;
source_kind?: string;
} = {},
) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/search",
{
params: {
query: {
q,
limit: options.limit,
offset: options.offset,
room_id: options.room_id,
source_kind: options.source_kind as any,
},
},
},
{
onError: (error) => {
setError(error as Error, "There was an error searching transcripts");
},
keepPreviousData: true, // For smooth pagination
},
);
}
export function useTranscriptDelete() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation("delete", "/v1/transcripts/{transcript_id}", {
onSuccess: () => {
// Invalidate transcripts queries to refetch
queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/v1/transcripts/search").queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error deleting the transcript");
},
});
}
export function useTranscriptProcess() {
const { setError } = useError();
return $api.useMutation("post", "/v1/transcripts/{transcript_id}/process", {
onError: (error) => {
setError(error as Error, "There was an error processing the transcript");
},
});
}
export function useTranscriptGet(transcriptId: string | null) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/{transcript_id}",
{
params: {
path: {
transcript_id: transcriptId || "",
},
},
},
{
enabled: !!transcriptId,
onError: (error) => {
setError(error as Error, "There was an error loading the transcript");
},
},
);
}

40
www/app/lib/apiClient.tsx Normal file
View File

@@ -0,0 +1,40 @@
"use client";
import createClient from "openapi-fetch";
import type { paths } from "../reflector-api";
import {
queryOptions,
useMutation,
useQuery,
useSuspenseQuery,
} from "@tanstack/react-query";
import createFetchClient from "openapi-react-query";
// Create the base openapi-fetch client
export const client = createClient<paths>({
// Base URL will be set dynamically via middleware
baseUrl: "",
headers: {
"Content-Type": "application/json",
},
});
// Create the React Query client wrapper
export const $api = createFetchClient<paths>(client);
// Configure authentication
export const configureApiAuth = (token: string | null | undefined) => {
if (token) {
client.use({
onRequest({ request }) {
request.headers.set("Authorization", `Bearer ${token}`);
return request;
},
});
}
};
// Export typed hooks for convenience
export const useApiQuery = $api.useQuery;
export const useApiMutation = $api.useMutation;
export const useApiSuspenseQuery = $api.useSuspenseQuery;

View File

@@ -0,0 +1,17 @@
"use client";
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
mutations: {
retry: 0,
},
},
});

View File

@@ -1,37 +0,0 @@
import { useSession, signOut } from "next-auth/react";
import { useContext, useEffect, useState } from "react";
import { DomainContext, featureEnabled } from "../domainContext";
import { OpenApi, DefaultService } from "../api";
import { CustomSession } from "./types";
import useSessionStatus from "./useSessionStatus";
import useSessionAccessToken from "./useSessionAccessToken";
export default function useApi(): DefaultService | null {
const api_url = useContext(DomainContext).api_url;
const [api, setApi] = useState<OpenApi | null>(null);
const { isLoading, isAuthenticated } = useSessionStatus();
const { accessToken, error } = useSessionAccessToken();
if (!api_url) throw new Error("no API URL");
useEffect(() => {
if (error === "RefreshAccessTokenError") {
signOut();
}
}, [error]);
useEffect(() => {
if (isLoading || (isAuthenticated && !accessToken)) {
return;
}
const openApi = new OpenApi({
BASE: api_url,
TOKEN: accessToken || undefined,
});
setApi(openApi);
}, [isLoading, isAuthenticated, accessToken]);
return api?.default ?? null;
}

View File

@@ -6,16 +6,23 @@ import system from "./styles/theme";
import { WherebyProvider } from "@whereby.com/browser-sdk/react"; import { WherebyProvider } from "@whereby.com/browser-sdk/react";
import { Toaster } from "./components/ui/toaster"; import { Toaster } from "./components/ui/toaster";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/queryClient";
import { ApiAuthProvider } from "./lib/ApiAuthProvider";
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return ( return (
<NuqsAdapter> <NuqsAdapter>
<ChakraProvider value={system}> <QueryClientProvider client={queryClient}>
<WherebyProvider> <ApiAuthProvider>
{children} <ChakraProvider value={system}>
<Toaster /> <WherebyProvider>
</WherebyProvider> {children}
</ChakraProvider> <Toaster />
</WherebyProvider>
</ChakraProvider>
</ApiAuthProvider>
</QueryClientProvider>
</NuqsAdapter> </NuqsAdapter>
); );
} }

2170
www/app/reflector-api.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
client: "axios",
name: "OpenApi",
input: "http://127.0.0.1:1250/openapi.json",
output: {
path: "./app/api",
format: "prettier",
},
services: {
asClass: true,
},
});

View File

@@ -8,7 +8,7 @@
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write .", "format": "prettier --write .",
"openapi": "openapi-ts" "codegen": "openapi-typescript http://127.0.0.1:1250/openapi.json -o ./app/reflector-api.d.ts"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^3.24.2", "@chakra-ui/react": "^3.24.2",
@@ -17,6 +17,7 @@
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@sentry/nextjs": "^7.77.0", "@sentry/nextjs": "^7.77.0",
"@tanstack/react-query": "^5.85.5",
"@vercel/edge-config": "^0.4.1", "@vercel/edge-config": "^0.4.1",
"@vercel/kv": "^2.0.0", "@vercel/kv": "^2.0.0",
"@whereby.com/browser-sdk": "^3.3.4", "@whereby.com/browser-sdk": "^3.3.4",
@@ -32,6 +33,8 @@
"next-auth": "^4.24.7", "next-auth": "^4.24.7",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nuqs": "^2.4.3", "nuqs": "^2.4.3",
"openapi-fetch": "^0.14.0",
"openapi-react-query": "^0.5.0",
"postcss": "8.4.31", "postcss": "8.4.31",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^18.2.0",
@@ -53,8 +56,8 @@
"author": "Andreas <andreas@monadical.com>", "author": "Andreas <andreas@monadical.com>",
"license": "All Rights Reserved", "license": "All Rights Reserved",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "^0.48.0",
"@types/react": "18.2.20", "@types/react": "18.2.20",
"openapi-typescript": "^7.9.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"vercel": "^37.3.0" "vercel": "^37.3.0"
}, },

574
www/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff