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"; |
import { listModelsByProvider } from "#server/service/llm"; |
||||
|
|
||||
export default defineWrappedResponseHandler(async (event) => { |
export default defineWrappedResponseHandler(async (event) => { |
||||
await requireAdmin(event); |
const user = await requireUser(event); |
||||
|
|
||||
const providerId = Number(getRouterParam(event, "providerId")); |
const providerId = Number(getRouterParam(event, "providerId")); |
||||
if (!providerId) { |
if (!providerId) { |
||||
return R.throwError(400, "无效的供应商ID", null); |
return R.throwError(400, "无效的供应商ID", null); |
||||
} |
} |
||||
|
|
||||
const result = await listModelsByProvider(providerId); |
const result = await listModelsByProvider(providerId, user.id); |
||||
return R.success(result); |
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"; |
import { deleteModel } from "#server/service/llm"; |
||||
|
|
||||
export default defineWrappedResponseHandler(async (event) => { |
export default defineWrappedResponseHandler(async (event) => { |
||||
await requireAdmin(event); |
const user = await requireUser(event); |
||||
|
|
||||
const id = Number(getRouterParam(event, "id")); |
const id = Number(getRouterParam(event, "id")); |
||||
if (!id) { |
if (!id) { |
||||
return R.throwError(400, "无效的模型ID", null); |
return R.throwError(400, "无效的模型ID", null); |
||||
} |
} |
||||
|
|
||||
await deleteModel(id); |
await deleteModel(id, user.id); |
||||
return R.success(null); |
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"; |
import { deleteProvider } from "#server/service/llm"; |
||||
|
|
||||
export default defineWrappedResponseHandler(async (event) => { |
export default defineWrappedResponseHandler(async (event) => { |
||||
await requireAdmin(event); |
const user = await requireUser(event); |
||||
|
|
||||
const id = Number(getRouterParam(event, "id")); |
const id = Number(getRouterParam(event, "id")); |
||||
if (!id) { |
if (!id) { |
||||
return R.throwError(400, "无效的供应商ID", null); |
return R.throwError(400, "无效的供应商ID", null); |
||||
} |
} |
||||
|
|
||||
await deleteProvider(id); |
await deleteProvider(id, user.id); |
||||
return R.success(null); |
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"; |
import { getProviderWithModels } from "#server/service/llm"; |
||||
|
|
||||
export default defineWrappedResponseHandler(async (event) => { |
export default defineWrappedResponseHandler(async (event) => { |
||||
await requireAdmin(event); |
const user = await requireUser(event); |
||||
|
|
||||
const id = Number(getRouterParam(event, "id")); |
const id = Number(getRouterParam(event, "id")); |
||||
if (!id) { |
if (!id) { |
||||
return R.throwError(400, "无效的供应商ID", null); |
return R.throwError(400, "无效的供应商ID", null); |
||||
} |
} |
||||
|
|
||||
const result = await getProviderWithModels(id); |
const result = await getProviderWithModels(id, user.id); |
||||
if (!result) { |
if (!result) { |
||||
return R.throwError(404, "供应商不存在", null); |
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"; |
import { getProviderById } from "#server/service/llm"; |
||||
|
|
||||
export default defineWrappedResponseHandler(async (event) => { |
export default defineWrappedResponseHandler(async (event) => { |
||||
await requireAdmin(event); |
const user = await requireUser(event); |
||||
|
|
||||
const id = Number(getRouterParam(event, "id")); |
const id = Number(getRouterParam(event, "id")); |
||||
if (!id) { |
if (!id) { |
||||
return R.throwError(400, "无效的供应商 ID", null); |
return R.throwError(400, "无效的供应商 ID", null); |
||||
} |
} |
||||
|
|
||||
const provider = await getProviderById(id); |
const provider = await getProviderById(id, user.id); |
||||
if (!provider) { |
if (!provider) { |
||||
return R.throwError(404, "供应商不存在", null); |
return R.throwError(404, "供应商不存在", null); |
||||
} |
} |
||||
Loading…
Reference in new issue