diff --git a/.env.example b/.env.example index 020338d3..12b0de80 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ # PostgreSQL database connection URL # Format: postgresql://username:password@host:port/database # Example: postgresql://postgres:password@localhost:5432/memohome -DATABASE_URL=postgresql://username:password@localhost:5432/database_name +DATABASE_URL=postgresql://postgres:1234@localhost:5432/database_name # ================================== diff --git a/.vscode/settings.json b/.vscode/settings.json index d0746e5f..ee2a1804 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,14 @@ { "editor.insertSpaces": true, "editor.detectIndentation": false, - + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + "editor.tabSize": 2, - + "[typescript]": { "editor.tabSize": 2, "editor.insertSpaces": true @@ -33,4 +38,3 @@ "editor.insertSpaces": true } } - diff --git a/package.json b/package.json index 147ac39e..52c506a0 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,5 @@ "@algolia/client-search" ] } - } + } } diff --git a/packages/api/src/modules/auth/service.ts b/packages/api/src/modules/auth/service.ts new file mode 100644 index 00000000..b8e91f3b --- /dev/null +++ b/packages/api/src/modules/auth/service.ts @@ -0,0 +1,119 @@ +import { db } from '@memoh/db' +import { users, settings } from '@memoh/db/schema' +import { eq } from 'drizzle-orm' + +/** + * 验证用户凭据 + * 优先检查是否为 ROOT 用户,否则查询数据库 + */ +export const validateUser = async (username: string, password: string) => { + // 检查是否为 ROOT 用户 + const rootUser = process.env.ROOT_USER + const rootPassword = process.env.ROOT_USER_PASSWORD + + let userId: string | null = null + + if (rootUser && rootPassword && username === rootUser) { + if (password === rootPassword) { + // 检查 root 用户是否存在于数据库中 + const [existingUser] = await db + .select() + .from(users) + .where(eq(users.username, rootUser)) + + userId = existingUser?.id + + if (!existingUser) { + // 为 root 用户创建数据库记录 + // 使用占位符密码哈希,因为实际密码在环境变量中 + + const [newUser] = await db + .insert(users) + .values({ + username: rootUser, + passwordHash: 'ENV_BASED_AUTH', // 占位符,实际使用环境变量验证 + role: 'admin', + displayName: 'Root User', + email: null, + avatarUrl: null, + isActive: true, + }) + .onConflictDoNothing() // 避免并发创建导致的冲突 + .returning({ + id: users.id, + }) + + userId = newUser.id + } + + // 检查 root 用户的 settings 是否存在,不存在则创建 + const [existingSettings] = await db + .select() + .from(settings) + .where(eq(settings.userId, userId)) + + if (!existingSettings) { + // 为 root 用户创建默认 settings + await db + .insert(settings) + .values({ + userId: userId, + defaultChatModel: null, + defaultEmbeddingModel: null, + defaultSummaryModel: null, + maxContextLoadTime: 60, + language: 'Same as user input', + }) + .onConflictDoNothing() // 避免并发创建导致的冲突 + } + + // 返回 ROOT 用户信息 + return { + id: userId, + username: rootUser, + role: 'admin' as const, + displayName: 'Root User', + } + } + return null + } + + // 查询数据库中的用户(使用 username 而不是 id) + const [user] = await db + .select() + .from(users) + .where(eq(users.username, username)) + + if (!user) { + return null + } + + // 验证密码 (这里使用简单的 Bun.password.verify) + const isValid = await Bun.password.verify(password, user.passwordHash) + + if (!isValid) { + return null + } + + // 检查账户是否激活 + if (!user.isActive) { + return null + } + + // 更新最后登录时间 + await db + .update(users) + .set({ + lastLoginAt: new Date(), + }) + .where(eq(users.id, user.id)) + + return { + id: user.id, + username: user.username, + role: user.role, + displayName: user.displayName || user.username, + email: user.email, + } +} + diff --git a/packages/api/src/modules/user/index.ts b/packages/api/src/modules/user/index.ts new file mode 100644 index 00000000..02ab1488 --- /dev/null +++ b/packages/api/src/modules/user/index.ts @@ -0,0 +1,182 @@ +import Elysia from 'elysia' +import { adminMiddleware } from '../../middlewares' +import { + GetUserByIdModel, + CreateUserModel, + UpdateUserModel, + DeleteUserModel, + UpdatePasswordModel, +} from './model' +import { + getUsers, + getUserById, + createUser, + updateUser, + deleteUser, + updateUserPassword, +} from './service' + +export const userModule = new Elysia({ + prefix: '/user', +}) + // 使用管理员中间件保护所有路由 + .use(adminMiddleware) + // Get all users + .get('/', async ({ query }) => { + try { + const page = parseInt(query.page as string) || 1 + const limit = parseInt(query.limit as string) || 10 + const sortBy = query.sortBy as string || 'createdAt' + const sortOrder = (query.sortOrder as string) || 'desc' + + const result = await getUsers({ + page, + limit, + sortBy, + sortOrder: sortOrder as 'asc' | 'desc', + }) + + return { + success: true, + ...result, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch users', + } + } + }) + // Get user by ID + .get('/:id', async ({ params, set }) => { + try { + const { id } = params + const user = await getUserById(id) + + if (!user) { + set.status = 404 + return { + success: false, + error: 'User not found', + } + } + + return { + success: true, + data: user, + } + } catch (error) { + set.status = 500 + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch user', + } + } + }, GetUserByIdModel) + // Create new user + .post('/', async ({ body, set }) => { + try { + const newUser = await createUser(body) + set.status = 201 + return { + success: true, + data: newUser, + } + } catch (error) { + if (error instanceof Error && ( + error.message.includes('already exists') + )) { + set.status = 409 + } else { + set.status = 500 + } + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create user', + } + } + }, CreateUserModel) + // Update user + .put('/:id', async ({ params, body, set }) => { + try { + const { id } = params + const updatedUser = await updateUser(id, body) + + if (!updatedUser) { + set.status = 404 + return { + success: false, + error: 'User not found', + } + } + + return { + success: true, + data: updatedUser, + } + } catch (error) { + if (error instanceof Error && error.message.includes('already exists')) { + set.status = 409 + } else { + set.status = 500 + } + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update user', + } + } + }, UpdateUserModel) + // Delete user + .delete('/:id', async ({ params, set }) => { + try { + const { id } = params + const deletedUser = await deleteUser(id) + + if (!deletedUser) { + set.status = 404 + return { + success: false, + error: 'User not found', + } + } + + return { + success: true, + data: deletedUser, + } + } catch (error) { + set.status = 500 + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete user', + } + } + }, DeleteUserModel) + // Update user password + .patch('/:id/password', async ({ params, body, set }) => { + try { + const { id } = params + const updatedUser = await updateUserPassword(id, body.password) + + if (!updatedUser) { + set.status = 404 + return { + success: false, + error: 'User not found', + } + } + + return { + success: true, + data: updatedUser, + message: 'Password updated successfully', + } + } catch (error) { + set.status = 500 + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update password', + } + } + }, UpdatePasswordModel) + diff --git a/packages/shared/src/chatInfo.ts b/packages/shared/src/chatInfo.ts new file mode 100644 index 00000000..3a11d13b --- /dev/null +++ b/packages/shared/src/chatInfo.ts @@ -0,0 +1,15 @@ +export interface robot{ + description: string + time: Date, + id: string | number, + type: string, + action: 'robot', + state:'thinking'|'generate'|'complete' +} + +export interface user{ + description: string, + time: Date, + id: number | string, + action:'user' +} \ No newline at end of file diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index c16f5c97..f23cc870 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,5 @@ export * from './model' export * from './schedule' export * from './platform' -export * from './mcp' \ No newline at end of file +export * from './mcp' +export * from './chatInfo' diff --git a/packages/shared/src/mcp.ts b/packages/shared/src/mcp.ts index b9b4e662..da2993b1 100644 --- a/packages/shared/src/mcp.ts +++ b/packages/shared/src/mcp.ts @@ -28,3 +28,21 @@ export type MCPConnection = | StdioMCPConnection | HTTPMCPConnection | SSEMCPConnection + + +export interface MCPListItem{ + id: string; + type: string; + name: string; + config: { + cwd: string; + env: Record; + args: string[]; + type: string; + command: string; + }; + active: boolean; + user: string; + createdAt: string; + updatedAt: string; +} \ No newline at end of file diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts index e0a788c4..6ba4d6a5 100644 --- a/packages/shared/src/model.ts +++ b/packages/shared/src/model.ts @@ -63,3 +63,18 @@ export interface ChatModel extends BaseModel { } export type Model = EmbeddingModel | ChatModel + + +// 表格当中model的类型 +export interface ModelTable { + apiKey: string, + baseUrl: string, + clientType: 'OpenAI' | 'Anthropic' | 'Google', + modelId: string, + name: string, + type: 'chat' | 'embedding', + id: string, + defaultChatModel: boolean, + defaultEmbeddingModel: boolean, + defaultSummaryModel: boolean +} \ No newline at end of file diff --git a/packages/ui/components.json b/packages/ui/components.json new file mode 100644 index 00000000..f5879d25 --- /dev/null +++ b/packages/ui/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "new-york", + "typescript": true, + "tailwind": { + "config": "", + "css": "src/style.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "#/components", + "composables": "#/composables", + "utils": "#/lib/utils", + "ui": "#/components", + "lib": "#/lib" + }, + "iconLibrary": "lucide" +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 73d22b52..1a7cc6c3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,7 +4,8 @@ "private": true, "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./style.css": "./src/style.css" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -19,6 +20,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "@tanstack/vue-table": "^8.21.3", + "@vee-validate/zod": "^4.15.1", "@vueuse/core": "^14.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -26,7 +29,9 @@ "reka-ui": "^2.7.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", - "vue-sonner": "^2.0.9" + "vee-validate": "^4.15.1", + "vue-sonner": "^2.0.9", + "zod": "3.25.76" }, "peerDependencies": { "vue": "^3.5.26" diff --git a/packages/ui/scripts/gen-entry.ts b/packages/ui/scripts/gen-entry.ts new file mode 100644 index 00000000..20ba008a --- /dev/null +++ b/packages/ui/scripts/gen-entry.ts @@ -0,0 +1,43 @@ +import fs from 'fs' +import path from 'path' + +const rootDir = path.resolve(import.meta.dirname, '../src') +const readDir = path.resolve(rootDir, './components') +const outputDir = path.resolve(rootDir, './index.ts') + +async function readDirName(){ + const pathList:Awaited = await new Promise((resolve, reject) => { + fs.readdir(readDir, (err, data) => { + if (err) { + reject(err) + } + resolve(data) + }) + }) + return pathList +} + +async function writeExportFile(pathList: string[]) { + const pathListStr = pathList.map(fileName => { + return `export * from './components/${fileName}/index'` + }) + await new Promise((resolve, reject) => { + fs.writeFile(outputDir, pathListStr.join('\r\n'), (err) => { + if (err) { + reject(err) + } + resolve(undefined) + }) + }) +} + +async function generate() { + try { + const list = await readDirName() + writeExportFile(list) + } catch(error) { + console.error(error) + } +} + +generate() \ No newline at end of file diff --git a/packages/ui/src/components/badge/Badge.vue b/packages/ui/src/components/badge/Badge.vue index 00e982a4..f255e9cc 100644 --- a/packages/ui/src/components/badge/Badge.vue +++ b/packages/ui/src/components/badge/Badge.vue @@ -1,18 +1,18 @@