mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
feat: add web PAT API client
This commit is contained in:
parent
096dea45d4
commit
e5ab0e5342
4 changed files with 146 additions and 66 deletions
30
surfsense_web/contracts/types/pat.types.ts
Normal file
30
surfsense_web/contracts/types/pat.types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const pat = z.object({
|
||||
id: z.number(),
|
||||
label: z.string(),
|
||||
prefix: z.string(),
|
||||
expires_at: z.string().nullable(),
|
||||
last_used_at: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
|
||||
export const createPatRequest = z.object({
|
||||
label: z.string().min(1).max(120),
|
||||
expires_in_days: z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
|
||||
export const createPatResponse = z.object({
|
||||
id: z.number(),
|
||||
label: z.string(),
|
||||
token: z.string(),
|
||||
prefix: z.string(),
|
||||
expires_at: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const listPatsResponse = z.array(pat);
|
||||
export const deletePatResponse = z.void();
|
||||
|
||||
export type PersonalAccessToken = z.infer<typeof pat>;
|
||||
export type CreatePatRequest = z.infer<typeof createPatRequest>;
|
||||
export type CreatedPat = z.infer<typeof createPatResponse>;
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils";
|
||||
|
||||
interface UseApiKeyReturn {
|
||||
apiKey: string | null;
|
||||
isLoading: boolean;
|
||||
copied: boolean;
|
||||
copyToClipboard: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useApiKey(): UseApiKeyReturn {
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Load API key from localStorage
|
||||
const loadApiKey = () => {
|
||||
try {
|
||||
const token = getBearerToken();
|
||||
setApiKey(token);
|
||||
} catch (error) {
|
||||
console.error("Error loading API key:", error);
|
||||
toast.error("Failed to load API key");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a small delay to simulate loading
|
||||
const timer = setTimeout(loadApiKey, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (!apiKey) return;
|
||||
|
||||
const success = await copyToClipboardUtil(apiKey);
|
||||
if (success) {
|
||||
setCopied(true);
|
||||
toast.success("API key copied to clipboard");
|
||||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
|
||||
copyTimerRef.current = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
toast.error("Failed to copy API key");
|
||||
}
|
||||
}, [apiKey]);
|
||||
|
||||
return {
|
||||
apiKey,
|
||||
isLoading,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
};
|
||||
}
|
||||
83
surfsense_web/hooks/use-pats.ts
Normal file
83
surfsense_web/hooks/use-pats.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
CreatePatRequest,
|
||||
CreatedPat,
|
||||
PersonalAccessToken,
|
||||
} from "@/contracts/types/pat.types";
|
||||
import { patsApiService } from "@/lib/apis/pats-api.service";
|
||||
|
||||
export function usePats() {
|
||||
const [tokens, setTokens] = useState<PersonalAccessToken[]>([]);
|
||||
const [createdToken, setCreatedToken] = useState<CreatedPat | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await patsApiService.listPats();
|
||||
setTokens(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to load personal access tokens:", error);
|
||||
toast.error("Failed to load personal access tokens");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const createToken = useCallback(
|
||||
async (request: CreatePatRequest) => {
|
||||
setIsMutating(true);
|
||||
try {
|
||||
const data = await patsApiService.createPat(request);
|
||||
setCreatedToken(data);
|
||||
await refresh();
|
||||
toast.success("Personal access token created");
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create personal access token:", error);
|
||||
toast.error("Failed to create personal access token");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const deleteToken = useCallback(
|
||||
async (id: number) => {
|
||||
setIsMutating(true);
|
||||
try {
|
||||
await patsApiService.deletePat(id);
|
||||
await refresh();
|
||||
toast.success("Personal access token deleted");
|
||||
} catch (error) {
|
||||
console.error("Failed to delete personal access token:", error);
|
||||
toast.error("Failed to delete personal access token");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
return {
|
||||
tokens,
|
||||
createdToken,
|
||||
setCreatedToken,
|
||||
isLoading,
|
||||
isMutating,
|
||||
refresh,
|
||||
createToken,
|
||||
deleteToken,
|
||||
};
|
||||
}
|
||||
33
surfsense_web/lib/apis/pats-api.service.ts
Normal file
33
surfsense_web/lib/apis/pats-api.service.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
type CreatePatRequest,
|
||||
createPatRequest,
|
||||
createPatResponse,
|
||||
deletePatResponse,
|
||||
listPatsResponse,
|
||||
} from "@/contracts/types/pat.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class PatsApiService {
|
||||
listPats = async () => {
|
||||
return baseApiService.get("/api/v1/pats", listPatsResponse);
|
||||
};
|
||||
|
||||
createPat = async (request: CreatePatRequest) => {
|
||||
const parsedRequest = createPatRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post("/api/v1/pats", createPatResponse, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
|
||||
deletePat = async (id: number) => {
|
||||
return baseApiService.delete(`/api/v1/pats/${id}`, deletePatResponse);
|
||||
};
|
||||
}
|
||||
|
||||
export const patsApiService = new PatsApiService();
|
||||
Loading…
Add table
Add a link
Reference in a new issue