From bc63e85d13056fd7069b48806dc796d08d82e103 Mon Sep 17 00:00:00 2001 From: Quicy <1728550853@qq.com> Date: Thu, 29 Jan 2026 14:59:21 +0800 Subject: [PATCH] feat(auth): implement login API integration with backend --- .env.example | 2 +- packages/api/src/modules/auth/service.ts | 119 ++++++++++++ packages/api/src/modules/user/index.ts | 182 ++++++++++++++++++ packages/shared/src/chatInfo.ts | 8 +- packages/ui/package.json | 5 +- .../ui/src/components/form/FormControl.vue | 17 ++ .../src/components/form/FormDescription.vue | 21 ++ packages/ui/src/components/form/FormItem.vue | 23 +++ packages/ui/src/components/form/FormLabel.vue | 25 +++ .../ui/src/components/form/FormMessage.vue | 23 +++ packages/ui/src/components/form/index.ts | 7 + .../ui/src/components/form/injectionKeys.ts | 4 + .../ui/src/components/form/useFormField.ts | 30 +++ packages/ui/src/components/label/Label.vue | 16 +- packages/ui/src/components/label/index.ts | 2 +- packages/ui/src/index.ts | 1 + packages/web/package.json | 8 +- packages/web/src/components/Sidebar/index.vue | 27 ++- packages/web/src/main.ts | 6 +- packages/web/src/pages/login/index.vue | 182 ++++++++++++------ packages/web/src/router.ts | 2 +- packages/web/src/store/User.ts | 40 ++++ packages/web/src/utils/index.ts | 0 packages/web/src/utils/request.ts | 29 +++ packages/web/tsconfig.app.json | 9 +- packages/web/tsconfig.json | 3 +- packages/web/type.d.ts | 1 + pnpm-lock.yaml | 128 +++++++++++- 28 files changed, 834 insertions(+), 86 deletions(-) create mode 100644 packages/api/src/modules/auth/service.ts create mode 100644 packages/api/src/modules/user/index.ts create mode 100644 packages/ui/src/components/form/FormControl.vue create mode 100644 packages/ui/src/components/form/FormDescription.vue create mode 100644 packages/ui/src/components/form/FormItem.vue create mode 100644 packages/ui/src/components/form/FormLabel.vue create mode 100644 packages/ui/src/components/form/FormMessage.vue create mode 100644 packages/ui/src/components/form/index.ts create mode 100644 packages/ui/src/components/form/injectionKeys.ts create mode 100644 packages/ui/src/components/form/useFormField.ts create mode 100644 packages/web/src/store/User.ts delete mode 100644 packages/web/src/utils/index.ts create mode 100644 packages/web/src/utils/request.ts create mode 100644 packages/web/type.d.ts 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/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 index fb5aaae2..f852f9fa 100644 --- a/packages/shared/src/chatInfo.ts +++ b/packages/shared/src/chatInfo.ts @@ -2,9 +2,13 @@ export interface robot{ description: string time: Date, id: string | number, - type: string + type: string, + action:'robot' } export interface user{ - description: string, time: Date, id: number | string + description: string, + time: Date, + id: number | string, + action:'user' } \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 352848db..f7ede7ae 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "@vee-validate/zod": "^4.15.1", "@vueuse/core": "^14.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -27,7 +28,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/src/components/form/FormControl.vue b/packages/ui/src/components/form/FormControl.vue new file mode 100644 index 00000000..b1bc4bfa --- /dev/null +++ b/packages/ui/src/components/form/FormControl.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/components/form/FormDescription.vue b/packages/ui/src/components/form/FormDescription.vue new file mode 100644 index 00000000..b5a32558 --- /dev/null +++ b/packages/ui/src/components/form/FormDescription.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/ui/src/components/form/FormItem.vue b/packages/ui/src/components/form/FormItem.vue new file mode 100644 index 00000000..18ceb6ac --- /dev/null +++ b/packages/ui/src/components/form/FormItem.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/ui/src/components/form/FormLabel.vue b/packages/ui/src/components/form/FormLabel.vue new file mode 100644 index 00000000..57eae984 --- /dev/null +++ b/packages/ui/src/components/form/FormLabel.vue @@ -0,0 +1,25 @@ + + + diff --git a/packages/ui/src/components/form/FormMessage.vue b/packages/ui/src/components/form/FormMessage.vue new file mode 100644 index 00000000..02290243 --- /dev/null +++ b/packages/ui/src/components/form/FormMessage.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/ui/src/components/form/index.ts b/packages/ui/src/components/form/index.ts new file mode 100644 index 00000000..1eb05f11 --- /dev/null +++ b/packages/ui/src/components/form/index.ts @@ -0,0 +1,7 @@ +export { default as FormControl } from "./FormControl.vue" +export { default as FormDescription } from "./FormDescription.vue" +export { default as FormItem } from "./FormItem.vue" +export { default as FormLabel } from "./FormLabel.vue" +export { default as FormMessage } from "./FormMessage.vue" +export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys" +export { Form, Field as FormField, FieldArray as FormFieldArray } from "vee-validate" diff --git a/packages/ui/src/components/form/injectionKeys.ts b/packages/ui/src/components/form/injectionKeys.ts new file mode 100644 index 00000000..42542a96 --- /dev/null +++ b/packages/ui/src/components/form/injectionKeys.ts @@ -0,0 +1,4 @@ +import type { InjectionKey } from "vue" + +export const FORM_ITEM_INJECTION_KEY + = Symbol() as InjectionKey diff --git a/packages/ui/src/components/form/useFormField.ts b/packages/ui/src/components/form/useFormField.ts new file mode 100644 index 00000000..62d86c26 --- /dev/null +++ b/packages/ui/src/components/form/useFormField.ts @@ -0,0 +1,30 @@ +import { FieldContextKey } from "vee-validate" +import { computed, inject } from "vue" +import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys" + +export function useFormField() { + const fieldContext = inject(FieldContextKey) + const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY) + + if (!fieldContext) + throw new Error("useFormField should be used within ") + + const { name, errorMessage: error, meta } = fieldContext + const id = fieldItemContext + + const fieldState = { + valid: computed(() => meta.valid), + isDirty: computed(() => meta.dirty), + isTouched: computed(() => meta.touched), + error, + } + + return { + id, + name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} diff --git a/packages/ui/src/components/label/Label.vue b/packages/ui/src/components/label/Label.vue index 3799c16f..c3f0c6a1 100644 --- a/packages/ui/src/components/label/Label.vue +++ b/packages/ui/src/components/label/Label.vue @@ -1,13 +1,13 @@