feat: migrate from Elysia Eden to Contract-First API (OpenAPI)

This commit is contained in:
bipproduction
2026-02-07 18:12:52 +08:00
parent 6abd32650d
commit f0c317837f
10 changed files with 1855 additions and 36 deletions

View File

@@ -45,6 +45,23 @@ export const apikey = new Elysia({
}
},
{
response: {
200: t.Object({
apiKeys: t.Array(
t.Object({
id: t.String(),
name: t.String(),
key: t.String(),
isActive: t.Boolean(),
expiresAt: t.Union([t.String(), t.Null(), t.Any()]),
createdAt: t.Any(),
updatedAt: t.Any(),
}),
),
}),
401: t.Object({ error: t.String() }),
500: t.Object({ error: t.String() }),
},
detail: {
summary: "Get all API keys",
description: "Get all API keys",
@@ -97,6 +114,21 @@ export const apikey = new Elysia({
name: t.String(),
expiresAt: t.Optional(t.String()), // ISO date string
}),
response: {
200: t.Object({
apiKey: t.Object({
id: t.String(),
name: t.String(),
key: t.String(),
isActive: t.Boolean(),
expiresAt: t.Union([t.String(), t.Null(), t.Any()]),
createdAt: t.Any(),
updatedAt: t.Any(),
}),
}),
401: t.Object({ error: t.String() }),
500: t.Object({ error: t.String() }),
},
detail: {
summary: "Create a new API key",
description: "Create a new API key",
@@ -174,6 +206,22 @@ export const apikey = new Elysia({
isActive: t.Boolean(),
expiresAt: t.Optional(t.Union([t.String(), t.Null()])), // ISO date string or null
}),
response: {
200: t.Object({
apiKey: t.Object({
id: t.String(),
name: t.String(),
key: t.String(),
isActive: t.Boolean(),
expiresAt: t.Union([t.String(), t.Null(), t.Any()]),
createdAt: t.Any(),
updatedAt: t.Any(),
}),
}),
401: t.Object({ error: t.String() }),
403: t.Object({ error: t.String() }),
500: t.Object({ error: t.String() }),
},
detail: {
summary: "Update an API key",
description: "Update an API key",
@@ -225,6 +273,14 @@ export const apikey = new Elysia({
body: t.Object({
id: t.String(),
}),
response: {
200: t.Object({
success: t.Boolean(),
}),
500: t.Object({ error: t.String() }),
403: t.Object({ error: t.String() }),
401: t.Object({ error: t.String() }),
},
detail: {
summary: "Delete an API key",
description: "Delete an API key",

View File

@@ -86,6 +86,11 @@ export function apiMiddleware(app: Elysia) {
}
})
.onBeforeHandle(({ user, set, request }) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/docs")) {
return;
}
if (!user) {
logger.warn(`[AUTH] Unauthorized: ${request.method} ${request.url}`);
set.status = 401;

View File

@@ -68,9 +68,12 @@ function DashboardApikeyComponent() {
const fetchApiKeys = useCallback(async () => {
try {
setLoading(true);
const response = await apiClient.api.apikey.get();
if (response.data) {
setApiKeys((response.data.apiKeys as any) || []);
const { data, error } = await apiClient.GET("/api/apikey/");
if (data) {
setApiKeys((data.apiKeys as any) || []);
}
if (error) {
setError("Failed to load API keys");
}
} catch (err) {
setError("Failed to load API keys");
@@ -92,19 +95,24 @@ function DashboardApikeyComponent() {
try {
setCreating(true);
const response = await apiClient.api.apikey.post({
name: newKeyName,
expiresAt: newKeyExpiresAt
? dayjs(newKeyExpiresAt).toISOString()
: undefined,
const { data, error } = await apiClient.POST("/api/apikey/", {
body: {
name: newKeyName,
expiresAt: newKeyExpiresAt
? dayjs(newKeyExpiresAt).toISOString()
: undefined,
},
});
if (response.data) {
setApiKeys([...apiKeys, response.data.apiKey as any]);
if (data) {
setApiKeys([...apiKeys, data.apiKey as any]);
setNewKeyName("");
setNewKeyExpiresAt(null);
setCreateModalOpen(false);
}
if (error) {
setError("Failed to create API key");
}
} catch (err) {
setError("Failed to create API key");
console.error(err);
@@ -119,18 +127,23 @@ function DashboardApikeyComponent() {
setError("API key ID is required");
return;
}
const response = await apiClient.api.apikey.update.post({
id,
isActive: !currentStatus,
const { data, error } = await apiClient.POST("/api/apikey/update", {
body: {
id,
isActive: !currentStatus,
},
});
if (response.data) {
if (data) {
setApiKeys(
apiKeys.map((key) =>
key.id === id ? { ...key, isActive: !currentStatus } : key,
),
);
}
if (error) {
setError("Failed to update API key status");
}
} catch (err) {
setError("Failed to update API key status");
console.error(err);
@@ -147,12 +160,18 @@ function DashboardApikeyComponent() {
if (!keyToDelete) return;
try {
await apiClient.api.apikey.delete.post({
id: keyToDelete,
const { error } = await apiClient.POST("/api/apikey/delete", {
body: {
id: keyToDelete,
},
});
setApiKeys(apiKeys.filter((key: ApiKey) => key.id !== keyToDelete));
setDeleteModalOpen(false);
setKeyToDelete(null);
if (!error) {
setApiKeys(apiKeys.filter((key: ApiKey) => key.id !== keyToDelete));
setDeleteModalOpen(false);
setKeyToDelete(null);
} else {
setError("Failed to delete API key");
}
} catch (err) {
setError("Failed to delete API key");
console.error(err);

View File

@@ -1,5 +1,5 @@
import { edenTreaty } from "@elysiajs/eden";
import type { ApiApp } from "../index";
import createClient from "openapi-fetch";
import type { paths } from "../../generated/api";
import { VITE_PUBLIC_URL } from "./env";
const baseUrl =
@@ -8,4 +8,7 @@ const baseUrl =
? window.location.origin
: "http://localhost:3000");
export const apiClient = edenTreaty<ApiApp>(baseUrl);
export const apiClient = createClient<paths>({
baseUrl: baseUrl,
credentials: "include",
});