Browse Source
- Implemented GET, POST, PUT, and DELETE endpoints for managing LLM models and providers. - Added user authorization checks to ensure only authorized users can access or modify resources. - Enhanced provider and model retrieval functions to include user-specific data. - Updated database service functions to handle user IDs for better data isolation. - Introduced error handling for invalid inputs and unauthorized actions.acas
31 changed files with 3672 additions and 1691 deletions
@ -0,0 +1,241 @@ |
|||
<script setup lang="ts"> |
|||
import type { NavItem } from '~/components/admin/AdminSidebarNav.vue' |
|||
|
|||
definePageMeta({ |
|||
layout: false |
|||
}) |
|||
const { user, clear } = useAuthSession() |
|||
|
|||
const settingsNav: NavItem[] = [ |
|||
{ |
|||
label: '模型配置', |
|||
to: '/settings/llm-config', |
|||
icon: 'lucide:brain-circuit', |
|||
}, |
|||
] |
|||
|
|||
const logout = async () => { |
|||
await clear() |
|||
navigateTo("/") |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="settings-layout"> |
|||
<div class="settings-container"> |
|||
<aside class="settings-sidebar"> |
|||
<div class="sidebar-header"> |
|||
<NuxtLink to="/settings/llm-config" class="brand-link"> |
|||
<span class="brand-icon"> |
|||
<Icon name="lucide:settings-2" /> |
|||
</span> |
|||
<span class="brand-name">个人设置</span> |
|||
</NuxtLink> |
|||
</div> |
|||
|
|||
<AdminSidebarNav :nav="settingsNav" /> |
|||
|
|||
<div class="sidebar-footer"> |
|||
<NuxtLink to="/" class="home-link"> |
|||
<Icon name="lucide:home" /> |
|||
返回首页 |
|||
</NuxtLink> |
|||
<NuxtLink is="div" class="user-section" v-if="user"> |
|||
<div class="user-avatar"> |
|||
{{ user.username?.charAt(0).toUpperCase() }} |
|||
</div> |
|||
<div class="user-info"> |
|||
<span class="user-name">{{ user.nickname || user.username }}</span> |
|||
<span class="user-role">{{ user.role === "user" ? "普通用户" : "管理员" }}</span> |
|||
</div> |
|||
<button class="logout-btn" @click.stop.prevent="logout" title="退出登录"> |
|||
<Icon name="lucide:log-out" /> |
|||
</button> |
|||
</NuxtLink> |
|||
</div> |
|||
</aside> |
|||
|
|||
<main class="settings-main"> |
|||
<NuxtPage /> |
|||
</main> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.settings-layout { |
|||
min-height: 100vh; |
|||
background: var(--color-canvas); |
|||
} |
|||
|
|||
.settings-container { |
|||
display: flex; |
|||
} |
|||
|
|||
.settings-sidebar { |
|||
width: 260px; |
|||
flex-shrink: 0; |
|||
background: var(--color-surface-dark); |
|||
display: flex; |
|||
flex-direction: column; |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
bottom: 0; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.sidebar-header { |
|||
padding: 20px 16px 16px; |
|||
border-bottom: 1px solid rgba(255, 255, 255, 0.06); |
|||
} |
|||
|
|||
.brand-link { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.brand-icon { |
|||
width: 32px; |
|||
height: 32px; |
|||
border-radius: 8px; |
|||
background: var(--color-primary); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.brand-icon :deep(svg) { |
|||
width: 18px; |
|||
height: 18px; |
|||
color: var(--color-on-primary); |
|||
} |
|||
|
|||
.brand-name { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
color: var(--color-on-dark); |
|||
letter-spacing: -0.01em; |
|||
} |
|||
|
|||
.sidebar-footer { |
|||
padding: 16px 12px; |
|||
border-top: 1px solid rgba(255, 255, 255, 0.06); |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.home-link { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
padding: 8px 10px; |
|||
border-radius: 6px; |
|||
text-decoration: none; |
|||
color: var(--color-on-dark-soft); |
|||
font-size: 13px; |
|||
font-weight: 500; |
|||
transition: all 0.15s ease; |
|||
} |
|||
|
|||
.home-link:hover { |
|||
background: rgba(255, 255, 255, 0.06); |
|||
color: var(--color-on-dark); |
|||
} |
|||
|
|||
.home-link :deep(svg) { |
|||
width: 16px; |
|||
height: 16px; |
|||
} |
|||
|
|||
.user-section { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
padding: 8px; |
|||
border-radius: 8px; |
|||
background: rgba(255, 255, 255, 0.03); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.user-avatar { |
|||
width: 32px; |
|||
height: 32px; |
|||
border-radius: 6px; |
|||
background: var(--color-primary); |
|||
color: var(--color-on-primary); |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.user-info { |
|||
flex: 1; |
|||
min-width: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 1px; |
|||
} |
|||
|
|||
.user-name { |
|||
font-size: 13px; |
|||
font-weight: 500; |
|||
color: var(--color-on-dark); |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.user-role { |
|||
font-size: 11px; |
|||
color: var(--color-on-dark-soft); |
|||
} |
|||
|
|||
.logout-btn { |
|||
width: 32px; |
|||
height: 32px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: transparent; |
|||
border: none; |
|||
border-radius: 6px; |
|||
color: var(--color-on-dark-soft); |
|||
cursor: pointer; |
|||
transition: all 0.15s ease; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.logout-btn:hover { |
|||
background: rgba(198, 69, 69, 0.2); |
|||
color: #c64545; |
|||
} |
|||
|
|||
.logout-btn :deep(svg) { |
|||
width: 16px; |
|||
height: 16px; |
|||
} |
|||
|
|||
.settings-main { |
|||
flex: 1; |
|||
margin-left: 260px; |
|||
min-height: 100vh; |
|||
background: var(--color-canvas); |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.settings-sidebar { |
|||
display: none; |
|||
} |
|||
|
|||
.settings-main { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
</style> |
|||
Binary file not shown.
@ -0,0 +1,24 @@ |
|||
DROP INDEX `llm_providers_name_unique`;--> statement-breakpoint |
|||
DROP INDEX `llm_providers_slug_unique`;--> statement-breakpoint |
|||
DROP INDEX `idx_llm_provider_slug`;--> statement-breakpoint |
|||
ALTER TABLE `llm_providers` ADD `user_id` integer REFERENCES users(id);--> statement-breakpoint |
|||
UPDATE `llm_providers` SET `user_id` = (SELECT MIN(id) FROM `users`) WHERE `user_id` IS NULL;--> statement-breakpoint |
|||
CREATE TABLE `_llm_providers_tmp` ( |
|||
`id` integer PRIMARY KEY AUTOINCREMENT, |
|||
`user_id` integer NOT NULL REFERENCES users(id), |
|||
`name` text(100) NOT NULL, |
|||
`slug` text(100) NOT NULL, |
|||
`base_url` text(500), |
|||
`parse_mode` text DEFAULT 'openai' NOT NULL, |
|||
`api_key` text, |
|||
`status` text DEFAULT 'active' NOT NULL, |
|||
`description` text, |
|||
`created_at` integer NOT NULL, |
|||
`updated_at` integer NOT NULL |
|||
);--> statement-breakpoint |
|||
INSERT INTO `_llm_providers_tmp` SELECT `id`, `user_id`, `name`, `slug`, `base_url`, `parse_mode`, `api_key`, `status`, `description`, `created_at`, `updated_at` FROM `llm_providers`;--> statement-breakpoint |
|||
DROP TABLE `llm_providers`;--> statement-breakpoint |
|||
ALTER TABLE `_llm_providers_tmp` RENAME TO `llm_providers`;--> statement-breakpoint |
|||
CREATE UNIQUE INDEX `idx_llm_provider_user_slug` ON `llm_providers` (`user_id`,`slug`);--> statement-breakpoint |
|||
CREATE INDEX `idx_llm_provider_user` ON `llm_providers` (`user_id`);--> statement-breakpoint |
|||
CREATE INDEX `idx_llm_provider_status` ON `llm_providers` (`status`); |
|||
File diff suppressed because it is too large
@ -1,14 +1,14 @@ |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
import { requireUser } from "#server/utils/context"; |
|||
import { listModelsByProvider } from "#server/service/llm"; |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
await requireAdmin(event); |
|||
const user = await requireUser(event); |
|||
|
|||
const providerId = Number(getRouterParam(event, "providerId")); |
|||
if (!providerId) { |
|||
return R.throwError(400, "无效的供应商ID", null); |
|||
} |
|||
|
|||
const result = await listModelsByProvider(providerId); |
|||
const result = await listModelsByProvider(providerId, user.id); |
|||
return R.success(result); |
|||
}); |
|||
@ -1,14 +1,14 @@ |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
import { requireUser } from "#server/utils/context"; |
|||
import { deleteModel } from "#server/service/llm"; |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
await requireAdmin(event); |
|||
const user = await requireUser(event); |
|||
|
|||
const id = Number(getRouterParam(event, "id")); |
|||
if (!id) { |
|||
return R.throwError(400, "无效的模型ID", null); |
|||
} |
|||
|
|||
await deleteModel(id); |
|||
await deleteModel(id, user.id); |
|||
return R.success(null); |
|||
}); |
|||
@ -1,14 +1,14 @@ |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
import { requireUser } from "#server/utils/context"; |
|||
import { deleteProvider } from "#server/service/llm"; |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
await requireAdmin(event); |
|||
const user = await requireUser(event); |
|||
|
|||
const id = Number(getRouterParam(event, "id")); |
|||
if (!id) { |
|||
return R.throwError(400, "无效的供应商ID", null); |
|||
} |
|||
|
|||
await deleteProvider(id); |
|||
await deleteProvider(id, user.id); |
|||
return R.success(null); |
|||
}); |
|||
@ -1,15 +1,15 @@ |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
import { requireUser } from "#server/utils/context"; |
|||
import { getProviderWithModels } from "#server/service/llm"; |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
await requireAdmin(event); |
|||
const user = await requireUser(event); |
|||
|
|||
const id = Number(getRouterParam(event, "id")); |
|||
if (!id) { |
|||
return R.throwError(400, "无效的供应商ID", null); |
|||
} |
|||
|
|||
const result = await getProviderWithModels(id); |
|||
const result = await getProviderWithModels(id, user.id); |
|||
if (!result) { |
|||
return R.throwError(404, "供应商不存在", null); |
|||
} |
|||
@ -1,15 +1,15 @@ |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
import { requireUser } from "#server/utils/context"; |
|||
import { getProviderById } from "#server/service/llm"; |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
await requireAdmin(event); |
|||
const user = await requireUser(event); |
|||
|
|||
const id = Number(getRouterParam(event, "id")); |
|||
if (!id) { |
|||
return R.throwError(400, "无效的供应商 ID", null); |
|||
} |
|||
|
|||
const provider = await getProviderById(id); |
|||
const provider = await getProviderById(id, user.id); |
|||
if (!provider) { |
|||
return R.throwError(404, "供应商不存在", null); |
|||
} |
|||
Loading…
Reference in new issue