forked from mirrors/claude-code-usage-dashboard
init
This commit is contained in:
154
server/api-client.ts
Normal file
154
server/api-client.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
interface LoginResponse {
|
||||
success: boolean;
|
||||
token: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
interface ApiKeysResponse {
|
||||
success: boolean;
|
||||
data: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
usage: {
|
||||
total: {
|
||||
cost: number;
|
||||
tokens: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheCreateTokens: number;
|
||||
cacheReadTokens: number;
|
||||
requests: number;
|
||||
formattedCost: string;
|
||||
};
|
||||
};
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface KeyIdResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
private username: string;
|
||||
private password: string;
|
||||
private token: string | null = null;
|
||||
private expiresAt: number = 0;
|
||||
private readonly skewMs = 10000; // 10 seconds
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = process.env.BASE_URL || '';
|
||||
this.username = process.env.ADMIN_USERNAME || '';
|
||||
this.password = process.env.ADMIN_PASSWORD || '';
|
||||
|
||||
if (!this.baseUrl || !this.username || !this.password) {
|
||||
throw new Error('Missing required environment variables: BASE_URL, ADMIN_USERNAME, ADMIN_PASSWORD');
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureValidToken(): Promise<void> {
|
||||
if (this.token && Date.now() + this.skewMs < this.expiresAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.login();
|
||||
}
|
||||
|
||||
private async login(): Promise<void> {
|
||||
let retries = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/web/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Login failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as LoginResponse;
|
||||
|
||||
if (!data.success || !data.token) {
|
||||
throw new Error('Login response invalid');
|
||||
}
|
||||
|
||||
this.token = data.token;
|
||||
this.expiresAt = Date.now() + data.expiresIn;
|
||||
return;
|
||||
|
||||
} catch (error) {
|
||||
retries++;
|
||||
if (retries >= maxRetries) {
|
||||
throw new Error(`Login failed after ${maxRetries} retries: ${error}`);
|
||||
}
|
||||
|
||||
// Exponential backoff
|
||||
const delay = Math.pow(2, retries - 1) * 1000;
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentCosts(): Promise<ApiKeysResponse['data']> {
|
||||
await this.ensureValidToken();
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/admin/api-keys?timeRange=all`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch current costs: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ApiKeysResponse;
|
||||
|
||||
if (!data.success || !Array.isArray(data.data)) {
|
||||
throw new Error('Invalid response format from admin/api-keys');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async getKeyId(apiKey: string): Promise<string> {
|
||||
const response = await fetch(`${this.baseUrl}/apiStats/api/get-key-id`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get key ID: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as KeyIdResponse;
|
||||
|
||||
if (!data.success || !data.data?.id) {
|
||||
throw new Error('Invalid API key or response format');
|
||||
}
|
||||
|
||||
return data.data.id;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
280
server/billing-calculator.ts
Normal file
280
server/billing-calculator.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { db } from './database';
|
||||
import { apiClient } from './api-client';
|
||||
|
||||
export interface UserData {
|
||||
id: string;
|
||||
name: string;
|
||||
usage: {
|
||||
total: {
|
||||
cost: number;
|
||||
tokens: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheCreateTokens: number;
|
||||
cacheReadTokens: number;
|
||||
requests: number;
|
||||
formattedCost: string;
|
||||
};
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface PeriodInfo {
|
||||
index: number;
|
||||
startSnapshotId: number | null;
|
||||
startAt: string | null;
|
||||
endAt: string | null;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
export interface UserRanking {
|
||||
id: string;
|
||||
name: string;
|
||||
cost: number;
|
||||
share: number;
|
||||
isMe: boolean;
|
||||
rawStart: UserData | null;
|
||||
rawEnd: UserData | null;
|
||||
periodTokens: number;
|
||||
periodRequests: number;
|
||||
}
|
||||
|
||||
export interface PeriodSummary {
|
||||
period: PeriodInfo;
|
||||
totals: {
|
||||
totalCost: number;
|
||||
userCount: number;
|
||||
};
|
||||
ranking: UserRanking[];
|
||||
}
|
||||
|
||||
export interface UserDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
startCost: number;
|
||||
endCost: number;
|
||||
deltaCost: number;
|
||||
raw: {
|
||||
start: UserData | null;
|
||||
end: UserData | null;
|
||||
};
|
||||
}
|
||||
|
||||
function mapFromDataArray(data: UserData[]): Map<string, UserData> {
|
||||
const m = new Map<string, UserData>();
|
||||
for (const u of data ?? []) m.set(u.id, u);
|
||||
return m;
|
||||
}
|
||||
|
||||
function computePeriodDelta(startData: UserData[], endData: UserData[], meId?: string) {
|
||||
const start = mapFromDataArray(startData);
|
||||
const end = mapFromDataArray(endData);
|
||||
|
||||
const ids = new Set<string>([...Array.from(start.keys()), ...Array.from(end.keys())]);
|
||||
const users: UserRanking[] = [];
|
||||
|
||||
for (const id of Array.from(ids)) {
|
||||
const endU = end.get(id);
|
||||
if (!endU) continue; // 删除用户:不计入
|
||||
|
||||
const startU = start.get(id);
|
||||
const startCost = Number(startU?.usage?.total?.cost ?? 0);
|
||||
const endCost = Number(endU?.usage?.total?.cost ?? 0);
|
||||
const startTokens = Number(startU?.usage?.total?.tokens ?? 0);
|
||||
const endTokens = Number(endU?.usage?.total?.tokens ?? 0);
|
||||
const startRequests = Number(startU?.usage?.total?.requests ?? 0);
|
||||
const endRequests = Number(endU?.usage?.total?.requests ?? 0);
|
||||
|
||||
let delta = endCost - startCost;
|
||||
if (!Number.isFinite(delta) || delta < 0) delta = 0;
|
||||
|
||||
let deltaTokens = endTokens - startTokens;
|
||||
if (!Number.isFinite(deltaTokens) || deltaTokens < 0) deltaTokens = 0;
|
||||
|
||||
let deltaRequests = endRequests - startRequests;
|
||||
if (!Number.isFinite(deltaRequests) || deltaRequests < 0) deltaRequests = 0;
|
||||
|
||||
users.push({
|
||||
id: meId === id ? id : '', // Only include ID for current user
|
||||
name: endU.name || 'User',
|
||||
cost: +delta.toFixed(6),
|
||||
share: 0, // will be calculated below
|
||||
isMe: meId === id,
|
||||
rawStart: start.get(id) ?? null,
|
||||
rawEnd: endU,
|
||||
periodTokens: deltaTokens,
|
||||
periodRequests: deltaRequests
|
||||
});
|
||||
}
|
||||
|
||||
const totalCost = users.reduce((s, u) => s + u.cost, 0);
|
||||
for (const u of users) u.share = totalCost > 0 ? u.cost / totalCost : 0;
|
||||
users.sort((a, b) => b.cost - a.cost);
|
||||
|
||||
const me = meId ? users.find(u => u.id === meId) : null;
|
||||
|
||||
return { users, totalCost: +totalCost.toFixed(6), me };
|
||||
}
|
||||
|
||||
export class BillingCalculator {
|
||||
async getPeriods(): Promise<PeriodInfo[]> {
|
||||
const snapshots = db.getSnapshots();
|
||||
const periods: PeriodInfo[] = [];
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
// No snapshots case: only current period from beginning
|
||||
periods.push({
|
||||
index: 0,
|
||||
startSnapshotId: null,
|
||||
startAt: null,
|
||||
endAt: null,
|
||||
isCurrent: true
|
||||
});
|
||||
} else {
|
||||
// Add current period (last snapshot to now)
|
||||
const lastSnapshot = snapshots[snapshots.length - 1];
|
||||
if (lastSnapshot) {
|
||||
periods.push({
|
||||
index: snapshots.length,
|
||||
startSnapshotId: lastSnapshot.id,
|
||||
startAt: lastSnapshot.created_at,
|
||||
endAt: null,
|
||||
isCurrent: true
|
||||
});
|
||||
}
|
||||
|
||||
// Add first historical period: from beginning to first snapshot
|
||||
const firstSnapshot = snapshots[0];
|
||||
if (firstSnapshot) {
|
||||
periods.push({
|
||||
index: 0,
|
||||
startSnapshotId: null,
|
||||
startAt: null,
|
||||
endAt: firstSnapshot.created_at,
|
||||
isCurrent: false
|
||||
});
|
||||
}
|
||||
|
||||
// Add other historical periods: from snapshot to snapshot
|
||||
for (let i = 1; i < snapshots.length; i++) {
|
||||
const startSnapshot = snapshots[i - 1];
|
||||
const endSnapshot = snapshots[i];
|
||||
|
||||
if (startSnapshot && endSnapshot) {
|
||||
periods.push({
|
||||
index: i,
|
||||
startSnapshotId: startSnapshot.id,
|
||||
startAt: startSnapshot.created_at,
|
||||
endAt: endSnapshot.created_at,
|
||||
isCurrent: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return periods;
|
||||
}
|
||||
|
||||
async getPeriodSummary(periodIndex: number, meId?: string): Promise<PeriodSummary> {
|
||||
const periods = await this.getPeriods();
|
||||
const period = periods.find(p => p.index === periodIndex);
|
||||
|
||||
if (!period) {
|
||||
throw new Error(`Period ${periodIndex} not found`);
|
||||
}
|
||||
|
||||
let startData: UserData[] = [];
|
||||
let endData: UserData[] = [];
|
||||
|
||||
if (period.index === 0) {
|
||||
// First historical period: start from 0, end with first snapshot
|
||||
if (period.endAt) {
|
||||
// There is a snapshot - find it and use it as endData
|
||||
const snapshots = db.getSnapshots();
|
||||
const endSnapshot = snapshots.find(s => s.created_at === period.endAt);
|
||||
if (endSnapshot) {
|
||||
// Handle double JSON encoding issue
|
||||
let rawData = endSnapshot.raw_json;
|
||||
if (typeof rawData === 'string' && rawData.startsWith('"')) {
|
||||
rawData = JSON.parse(rawData);
|
||||
}
|
||||
endData = JSON.parse(rawData);
|
||||
}
|
||||
} else {
|
||||
// No snapshots case: start from 0, end with current data
|
||||
endData = await apiClient.getCurrentCosts();
|
||||
}
|
||||
} else if (period.isCurrent) {
|
||||
// Current period: start from latest snapshot data, end with current data
|
||||
const startSnapshot = db.getSnapshotById(period.startSnapshotId!);
|
||||
if (startSnapshot) {
|
||||
// Handle double JSON encoding issue
|
||||
let rawData = startSnapshot.raw_json;
|
||||
if (typeof rawData === 'string' && rawData.startsWith('"')) {
|
||||
rawData = JSON.parse(rawData);
|
||||
}
|
||||
startData = JSON.parse(rawData);
|
||||
}
|
||||
endData = await apiClient.getCurrentCosts();
|
||||
} else {
|
||||
// Historical period: both start and end from snapshots
|
||||
const startSnapshot = db.getSnapshotById(period.startSnapshotId!);
|
||||
if (startSnapshot) {
|
||||
// Handle double JSON encoding issue
|
||||
let rawData = startSnapshot.raw_json;
|
||||
if (typeof rawData === 'string' && rawData.startsWith('"')) {
|
||||
rawData = JSON.parse(rawData);
|
||||
}
|
||||
startData = JSON.parse(rawData);
|
||||
}
|
||||
|
||||
const snapshots = db.getSnapshots();
|
||||
const endSnapshotIndex = snapshots.findIndex(s => s.created_at === period.endAt);
|
||||
if (endSnapshotIndex !== -1 && snapshots[endSnapshotIndex]) {
|
||||
// Handle double JSON encoding issue
|
||||
let rawData = snapshots[endSnapshotIndex]!.raw_json;
|
||||
if (typeof rawData === 'string' && rawData.startsWith('"')) {
|
||||
rawData = JSON.parse(rawData);
|
||||
}
|
||||
endData = JSON.parse(rawData);
|
||||
}
|
||||
}
|
||||
|
||||
const result = computePeriodDelta(startData, endData, meId);
|
||||
|
||||
return {
|
||||
period: {
|
||||
...period,
|
||||
endAt: period.endAt || new Date().toISOString()
|
||||
},
|
||||
totals: {
|
||||
totalCost: result.totalCost,
|
||||
userCount: result.users.filter(u => u.cost > 0).length
|
||||
},
|
||||
ranking: result.users
|
||||
};
|
||||
}
|
||||
|
||||
async getUserDetail(periodIndex: number, meId: string): Promise<UserDetail> {
|
||||
const summary = await this.getPeriodSummary(periodIndex, meId);
|
||||
const me = summary.ranking.find(u => u.id === meId);
|
||||
|
||||
if (!me) {
|
||||
throw new Error('User not found in this period (possibly deleted)');
|
||||
}
|
||||
|
||||
return {
|
||||
id: me.id,
|
||||
name: me.name,
|
||||
startCost: Number(me.rawStart?.usage?.total?.cost ?? 0),
|
||||
endCost: Number(me.rawEnd?.usage?.total?.cost ?? 0),
|
||||
deltaCost: me.cost,
|
||||
raw: {
|
||||
start: me.rawStart,
|
||||
end: me.rawEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const billingCalculator = new BillingCalculator();
|
||||
83
server/database.ts
Normal file
83
server/database.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
export interface BillingSnapshot {
|
||||
id: number;
|
||||
created_at: string;
|
||||
timezone: string;
|
||||
raw_json: string;
|
||||
}
|
||||
|
||||
export class DatabaseManager {
|
||||
private db: Database;
|
||||
|
||||
constructor(dbPath: string = "./app.db") {
|
||||
this.db = new Database(dbPath);
|
||||
this.initDatabase();
|
||||
}
|
||||
|
||||
private initDatabase() {
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS billing_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL DEFAULT 'Asia/Shanghai',
|
||||
raw_json TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
this.db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_snapshots_created_at ON billing_snapshots(created_at)
|
||||
`);
|
||||
}
|
||||
|
||||
insertSnapshot(rawJson: string, timezone: string = 'Asia/Shanghai'): number {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO billing_snapshots (created_at, timezone, raw_json)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
new Date().toISOString(),
|
||||
timezone,
|
||||
rawJson
|
||||
);
|
||||
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
getSnapshots(): BillingSnapshot[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, created_at, timezone, raw_json
|
||||
FROM billing_snapshots
|
||||
ORDER BY datetime(created_at) ASC
|
||||
`);
|
||||
|
||||
return stmt.all() as BillingSnapshot[];
|
||||
}
|
||||
|
||||
getSnapshotById(id: number): BillingSnapshot | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, created_at, timezone, raw_json
|
||||
FROM billing_snapshots
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
return stmt.get(id) as BillingSnapshot | null;
|
||||
}
|
||||
|
||||
getLatestSnapshot(): BillingSnapshot | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, created_at, timezone, raw_json
|
||||
FROM billing_snapshots
|
||||
ORDER BY datetime(created_at) DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return stmt.get() as BillingSnapshot | null;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new DatabaseManager(process.env.DATABASE_URL || "./app.db");
|
||||
168
server/index.ts
Normal file
168
server/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { apiClient } from './api-client';
|
||||
import { billingCalculator } from './billing-calculator';
|
||||
import indexHtml from '../client/index.html';
|
||||
|
||||
// Middleware to validate API key and get user ID
|
||||
const validateApiKey = async (request: Request): Promise<{valid: boolean, userId?: string, error?: string}> => {
|
||||
const apiKey = request.headers.get('x-api-key');
|
||||
|
||||
if (!apiKey) {
|
||||
return { valid: false, error: 'API key required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = await apiClient.getKeyId(apiKey);
|
||||
return { valid: true, userId };
|
||||
} catch (error) {
|
||||
return { valid: false, error: 'Invalid API key' };
|
||||
}
|
||||
};
|
||||
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
|
||||
Bun.serve({
|
||||
port,
|
||||
routes: {
|
||||
'/': indexHtml,
|
||||
|
||||
'/api/periods': {
|
||||
async GET(req) {
|
||||
const validation = await validateApiKey(req);
|
||||
if (!validation.valid) {
|
||||
return new Response(JSON.stringify({ error: validation.error }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const periods = await billingCalculator.getPeriods();
|
||||
return new Response(JSON.stringify({ periods }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting periods:', error);
|
||||
return new Response(JSON.stringify({ error: 'Failed to get periods' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'/api/periods/:index/summary': {
|
||||
async GET(req: Request) {
|
||||
const validation = await validateApiKey(req);
|
||||
if (!validation.valid) {
|
||||
return new Response(JSON.stringify({ error: validation.error }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const periodIndex = parseInt(url.pathname.split('/')[3] || '0');
|
||||
|
||||
if (isNaN(periodIndex)) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid period index' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const summary = await billingCalculator.getPeriodSummary(periodIndex, validation.userId);
|
||||
return new Response(JSON.stringify(summary), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting period summary:', error);
|
||||
if (error instanceof Error && error.message.includes('not found')) {
|
||||
return new Response(JSON.stringify({ error: 'Period not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: 'Failed to get period summary' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'/api/periods/:index/me': {
|
||||
async GET(req) {
|
||||
const validation = await validateApiKey(req);
|
||||
if (!validation.valid) {
|
||||
return new Response(JSON.stringify({ error: validation.error }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const periodIndex = parseInt(url.pathname.split('/')[3] || '0');
|
||||
|
||||
if (isNaN(periodIndex)) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid period index' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const userDetail = await billingCalculator.getUserDetail(periodIndex, validation.userId!);
|
||||
return new Response(JSON.stringify(userDetail), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting user detail:', error);
|
||||
if (error instanceof Error && error.message.includes('not found')) {
|
||||
return new Response(JSON.stringify({ error: 'User not found in this period (possibly deleted)' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: 'Failed to get user details' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'/api/auth/test': {
|
||||
async POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json() as { apiKey?: string };
|
||||
const { apiKey } = body;
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'API key required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const userId = await apiClient.getKeyId(apiKey);
|
||||
return new Response(JSON.stringify({ success: true, userId }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error testing API key:', error);
|
||||
return new Response(JSON.stringify({ error: 'Invalid API key' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// development: {
|
||||
// hmr: true,
|
||||
// console: true,
|
||||
// }
|
||||
});
|
||||
|
||||
console.log(`Server running on http://localhost:${port}`);
|
||||
Reference in New Issue
Block a user