import { getTokenProperties } from "@components/protected-route"; import { fetch } from "@tauri-apps/plugin-http"; import { type InferOutput, array, null_, literal, nullable, parse, pipe, safeParse, strictObject, string, transform, union, uuid, } from "valibot"; type BaseRequestParams = Partial<{ path: string; body: RequestInit["body"]; method: "GET" | "POST" | "DELETE"; }>; // export const base = "https://haystack.johncosta.tech"; export const base = "http://192.168.1.199:3040"; const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => { return new Request(`${base}/${path}`, { body, method, }); }; const refreshTokenValidator = strictObject({ access: string(), }) export const getAccessToken = async (): Promise => { let accessToken = localStorage.getItem("access")?.toString(); const refreshToken = localStorage.getItem("refresh")?.toString(); if (accessToken == null && refreshToken == null) { throw new Error("you are not logged in") } // FIX: Check what getTokenProperties returns const tokenProps = getTokenProperties(accessToken!); // If tokenProps.exp is a number (seconds), convert to milliseconds: const expiryTime = typeof tokenProps.exp === 'number' ? tokenProps.exp * 1000 // Convert seconds to milliseconds : tokenProps.exp.getTime(); // Already a Date object const isValidAccessToken = accessToken != null && expiryTime > Date.now(); console.log('Token check:', { expiryTime: new Date(expiryTime), now: new Date(), isValid: isValidAccessToken, timeLeft: (expiryTime - Date.now()) / 1000 + 's' }); if (!isValidAccessToken) { console.log('Refreshing token...'); const newAccessToken = await fetch(getBaseRequest({ path: 'auth/refresh', method: "POST", body: JSON.stringify({ refresh: refreshToken, }) })).then(r => r.json()); const { access } = parse(refreshTokenValidator, newAccessToken); localStorage.setItem("access", access); accessToken = access; } return accessToken!; } const getBaseAuthorizedRequest = async ({ path, body, method, }: BaseRequestParams): Promise => { const accessToken = await getAccessToken(); return new Request(`${base}/${path}`, { headers: { Authorization: `Bearer ${accessToken}`, }, body, method, }); }; export const sendImageFile = async ( imageName: string, file: File, ): Promise> => { const request = await getBaseAuthorizedRequest({ path: `images/${imageName}`, body: file, method: "POST", }); request.headers.set("Content-Type", "application/oclet-stream"); const res = await fetch(request).then((res) => res.json()); const parsedRes = safeParse(imageValidator, res); if (!parsedRes.success) { console.log(parsedRes.issues) throw new Error(JSON.stringify(parsedRes.issues)); } return parsedRes.output; }; export const deleteImage = async ( imageID: string ): Promise => { const request = await getBaseAuthorizedRequest({ path: `images/${imageID}`, method: "DELETE", }); await fetch(request); } export const deleteImageFromStack = async (listID: string, imageID: string): Promise => { const request = await getBaseAuthorizedRequest({ path: `stacks/${listID}/${imageID}`, method: "DELETE", }); await fetch(request); } export const deleteStackItem = async ( stackID: string, schemaItemID: string, ): Promise => { const request = await getBaseAuthorizedRequest({ path: `stacks/${stackID}/${schemaItemID}`, method: "DELETE", }); await fetch(request); } export const deleteStack = async (listID: string): Promise => { const request = await getBaseAuthorizedRequest({ path: `stacks/${listID}`, method: "DELETE", }); await fetch(request); } export class ImageLimitReached extends Error { constructor() { super(); } } export const sendImage = async ( imageName: string, base64Image: string, ): Promise> => { const request = await getBaseAuthorizedRequest({ path: `images/${imageName}`, body: base64Image, method: "POST", }); request.headers.set("Content-Type", "application/base64"); const rawRes = await fetch(request); if (!rawRes.ok && rawRes.status == 429) { throw new ImageLimitReached() } const res = await rawRes.json(); const parsedRes = safeParse(imageValidator, res); if (!parsedRes.success) { console.log("Parsing issues: ", parsedRes.issues) throw new Error(JSON.stringify(parsedRes.issues)); } return parsedRes.output; }; const imageValidator = strictObject({ ID: pipe(string(), uuid()), CreatedAt: string(), UserID: pipe(string(), uuid()), Description: string(), Image: null_(), ImageName: string(), Status: union([literal('not-started'), literal('in-progress'), literal('complete')]), }) const userImageValidator = strictObject({ ...imageValidator.entries, ImageStacks: pipe(nullable(array( strictObject({ ID: pipe(string(), uuid()), ImageID: pipe(string(), uuid()), StackID: pipe(string(), uuid()), }), )), transform(l => l ?? [])), }); const stackItem = strictObject({ ID: pipe(string(), uuid()), ImageID: pipe(string(), uuid()), SchemaItemID: pipe(string(), uuid()), Value: string(), }) const stackImage = strictObject({ ID: pipe(string(), uuid()), ImageID: pipe(string(), uuid()), StackID: pipe(string(), uuid()), Items: pipe(nullable(array(stackItem)), transform(l => l ?? [])), }); const stackSchemaItem = strictObject({ ID: pipe(string(), uuid()), StackID: pipe(string(), uuid()), Description: string(), Item: string(), Value: string(), }) const stackValidator = strictObject({ ID: pipe(string(), uuid()), CreatedAt: string(), UserID: pipe(string(), uuid()), Description: string(), Status: union([literal('not-started'), literal('in-progress'), literal('complete')]), Name: string(), Images: pipe(nullable(array(stackImage)), transform(l => l ?? [])), SchemaItems: array(stackSchemaItem), }); export type Stack = InferOutput; const imageRequestValidator = strictObject({ UserImages: array(userImageValidator), Stacks: array(stackValidator), }); export type JustTheImageWhatAreTheseNames = InferOutput< typeof userImageValidator >[]; export const getUserImages = async (): Promise< InferOutput > => { const request = await getBaseAuthorizedRequest({ path: "images" }); const res = await fetch(request).then((res) => res.json()); console.log("Backend response: ", res); const parsedRes = safeParse(imageRequestValidator, res); if (!parsedRes.success) { console.log("Schema error: ", parsedRes.issues) throw new Error(JSON.stringify(parsedRes.issues)); } return parsedRes.output; }; export const postLogin = async (email: string): Promise => { const request = getBaseRequest({ path: "auth/login", body: JSON.stringify({ email }), method: "POST", }); await fetch(request); }; const codeValidator = strictObject({ access: string(), refresh: string(), }); export const postCode = async ( email: string, code: string, ): Promise> => { const request = getBaseRequest({ path: "auth/code", body: JSON.stringify({ email, code }), method: "POST", }); const res = await fetch(request).then((res) => res.json()); const parsedRes = safeParse(codeValidator, res); if (!parsedRes.success) { console.log("Schema error: ", parsedRes.issues) throw new Error(JSON.stringify(parsedRes.issues)); } return parsedRes.output; }; export class ReachedStackLimit extends Error { constructor() { super(); } } export const createStack = async ( title: string, description: string, ): Promise => { const request = await getBaseAuthorizedRequest({ path: "stacks", method: "POST", body: JSON.stringify({ title, description }), }); request.headers.set("Content-Type", "application/json"); const res = await fetch(request); if (!res.ok && res.status == 429) { throw new ReachedStackLimit(); } };