feat: full api server

This commit is contained in:
Acbox
2026-01-10 21:55:39 +08:00
parent e60c0bb0d7
commit 661d742750
40 changed files with 3587 additions and 157 deletions
+121 -1
View File
@@ -1,5 +1,125 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { AgentStreamModel } from './model'
import { createAgentStream } from './service'
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
import { getSettings } from '../settings/service'
import { ChatModel, EmbeddingModel } from '@memohome/shared'
export const agentModule = new Elysia({
prefix: '/agent',
})
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
return {
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
// Stream agent conversation
.post('/stream', async ({ user, body, set }) => {
try {
// Get user's model configurations and settings
const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([
getChatModel(user.userId),
getEmbeddingModel(user.userId),
getSummaryModel(user.userId),
getSettings(user.userId),
])
if (!chatModel || !embeddingModel || !summaryModel) {
set.status = 400
return {
success: false,
error: 'Model configuration not found. Please configure your models in settings.',
}
}
// Use body params if provided, otherwise use settings, otherwise use defaults
const maxContextLoadTime = body.maxContextLoadTime
?? userSettings?.maxContextLoadTime
?? 60
const language = body.language
?? userSettings?.language
?? 'Same as user input'
// Create agent
const agent = await createAgentStream({
userId: user.userId,
chatModel: chatModel.model as ChatModel,
embeddingModel: embeddingModel.model as EmbeddingModel,
summaryModel: summaryModel.model as ChatModel,
maxContextLoadTime,
language,
})
// Set headers for Server-Sent Events
set.headers['Content-Type'] = 'text/event-stream'
set.headers['Cache-Control'] = 'no-cache'
set.headers['Connection'] = 'keep-alive'
// Create a stream
const stream = new ReadableStream({
async start(controller) {
try {
const encoder = new TextEncoder()
// Send events as they come
for await (const event of agent.ask(body.message)) {
const data = JSON.stringify(event)
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
}
// Send done event
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
controller.close()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
const errorData = JSON.stringify({
type: 'error',
error: errorMessage
})
controller.enqueue(new TextEncoder().encode(`data: ${errorData}\n\n`))
controller.close()
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to process request',
}
}
}, AgentStreamModel)
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod'
export const AgentStreamModel = {
body: z.object({
message: z.string().min(1, 'Message is required'),
// Optional overrides - if not provided, will use settings
maxContextLoadTime: z.number().int().min(1).max(1440).optional(),
language: z.string().optional(),
}),
}
export type AgentStreamInput = z.infer<typeof AgentStreamModel['body']>
+63
View File
@@ -0,0 +1,63 @@
import { createAgent } from '@memohome/agent'
import { createMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory'
import { ChatModel, EmbeddingModel } from '@memohome/shared'
// Type for messages passed to onFinish callback
type MessageType = Record<string, unknown>
export interface CreateAgentStreamParams {
userId: string
chatModel: ChatModel
embeddingModel: EmbeddingModel
summaryModel: ChatModel
maxContextLoadTime: number
language?: string
onFinish?: (messages: MessageType[]) => Promise<void>
}
export async function createAgentStream(params: CreateAgentStreamParams) {
const {
userId,
chatModel,
embeddingModel,
summaryModel,
maxContextLoadTime,
language,
onFinish,
} = params
// Create memory instance
const memoryInstance = createMemory({
summaryModel,
embeddingModel,
})
// Create agent
const agent = createAgent({
model: chatModel,
maxContextLoadTime,
language: language || 'Same as user input',
onReadMemory: async (from: Date, to: Date) => {
return await filterByTimestamp(from, to, userId)
},
onSearchMemory: async (query: string) => {
const results = await memoryInstance.searchMemory(query, userId)
return results
},
onFinish: async (messages: MessageType[]) => {
// Save conversation to memory
const memoryUnit: MemoryUnit = {
messages: messages as unknown as MemoryUnit['messages'],
timestamp: new Date(),
user: userId,
}
await memoryInstance.addMemory(memoryUnit)
// Call custom onFinish handler if provided
await onFinish?.(messages)
},
})
return agent
}
+135
View File
@@ -0,0 +1,135 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { LoginModel } from './model'
import { validateUser } from './service'
export const authModule = new Elysia({
prefix: '/auth',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
// Login endpoint
.post('/login', async ({ body, jwt, set }) => {
try {
const user = await validateUser(body.username, body.password)
if (!user) {
set.status = 401
return {
success: false,
error: 'Invalid username or password',
}
}
// 使用 JWT 插件生成 token
const token = await jwt.sign({
userId: user.id,
username: user.username,
role: user.role,
})
return {
success: true,
data: {
token,
user: {
id: user.id,
username: user.username,
role: user.role,
displayName: user.displayName,
email: user.email,
},
},
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to login',
}
}
}, LoginModel)
// Verify token endpoint
.get('/verify', async ({ bearer, jwt, set }) => {
try {
if (!bearer) {
set.status = 401
return {
success: false,
error: 'No bearer token provided',
}
}
// 使用 JWT 插件验证 token
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
return {
success: false,
error: 'Invalid or expired token',
}
}
return {
success: true,
data: {
userId: payload.userId,
username: payload.username,
role: payload.role,
},
}
} catch {
set.status = 401
return {
success: false,
error: 'Invalid or expired token',
}
}
})
// Get current user info
.get('/me', async ({ bearer, jwt, set }) => {
try {
if (!bearer) {
set.status = 401
return {
success: false,
error: 'No bearer token provided',
}
}
// 使用 JWT 插件验证 token
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
return {
success: false,
error: 'Invalid or expired token',
}
}
return {
success: true,
data: {
userId: payload.userId,
username: payload.username,
role: payload.role,
},
}
} catch {
set.status = 401
return {
success: false,
error: 'Invalid or expired token',
}
}
})
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod'
const LoginSchema = z.object({
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
})
export type LoginInput = z.infer<typeof LoginSchema>
export const LoginModel = {
body: LoginSchema,
}
+86
View File
@@ -0,0 +1,86 @@
import { db } from '@memohome/db'
import { users, settings } from '@memohome/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
if (rootUser && rootPassword && username === rootUser) {
if (password === rootPassword) {
// 检查 root 用户的 settings 是否存在,不存在则创建
const [existingSettings] = await db
.select()
.from(settings)
.where(eq(settings.userId, 'root'))
if (!existingSettings) {
// 为 root 用户创建默认 settings
await db
.insert(settings)
.values({
userId: 'root',
defaultChatModel: null,
defaultEmbeddingModel: null,
defaultSummaryModel: null,
maxContextLoadTime: 60,
language: 'Same as user input',
})
.onConflictDoNothing() // 避免并发创建导致的冲突
}
// 返回 ROOT 用户信息
return {
id: 'root',
username: rootUser,
role: 'admin' as const,
displayName: 'Root User',
}
}
return null
}
// 查询数据库中的用户
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 !== 'true') {
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,
}
}
+3 -1
View File
@@ -1,3 +1,5 @@
export * from './agent'
export * from './auth'
export * from './model'
export * from './settings'
export * from './settings'
export * from './user'
+43 -6
View File
@@ -1,4 +1,6 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { messageModule } from './message'
import { AddMemoryModel, SearchMemoryModel } from './model'
import { addMemory, searchMemory } from './service'
@@ -7,31 +9,66 @@ import { MemoryUnit } from '@memohome/memory'
export const memoryModule = new Elysia({
prefix: '/memory',
})
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
return {
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
.use(messageModule)
// Add memory
.post('/', async ({ body }) => {
// Add memory for current user
.post('/', async ({ user, body, set }) => {
try {
const result = await addMemory(body as unknown as MemoryUnit)
const memoryUnit: MemoryUnit = {
...body,
user: user.userId,
}
const result = await addMemory(memoryUnit)
return {
success: true,
data: result,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to add memory',
}
}
}, AddMemoryModel)
// Search memory
.get('/search', async ({ query }) => {
// Search memory for current user
.get('/search', async ({ user, query, set }) => {
try {
const results = await searchMemory(query.query, query.userId)
const results = await searchMemory(query.query, user.userId)
return {
success: true,
data: results,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to search memory',
@@ -1,22 +1,70 @@
import Elysia from 'elysia'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model'
import { getMemoryMessages, getMemoryMessagesFilter } from './service'
export const messageModule = new Elysia({
prefix: '/message',
})
.get('/', async ({ query }) => {
const units = await getMemoryMessages(query)
return {
success: true,
units,
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
}, GetMemoryMessageModel)
.get('/filter', async ({ query }) => {
const units = await getMemoryMessagesFilter(query)
return {
units,
success: true,
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
// Get messages for current user (paginated)
.get('/', async ({ user, query, set }) => {
try {
const units = await getMemoryMessages(user.userId, query)
return {
success: true,
data: units,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch messages',
}
}
}, GetMemoryMessageModel)
// Get messages by date range for current user
.get('/filter', async ({ user, query, set }) => {
try {
const units = await getMemoryMessagesFilter(user.userId, query)
return {
success: true,
data: units,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to filter messages',
}
}
}, GetMemoryMessageFilterModel)
@@ -2,16 +2,14 @@ import { z } from 'zod'
export const GetMemoryMessageModel = {
query: z.object({
limit: z.string().transform(Number).default(10),
page: z.string().transform(Number).default(1),
userId: z.string(),
limit: z.coerce.number().default(10),
page: z.coerce.number().default(1),
}),
}
export const GetMemoryMessageFilterModel = {
query: z.object({
from: z.date(),
to: z.date(),
userId: z.string(),
from: z.coerce.date(),
to: z.coerce.date(),
}),
}
@@ -3,13 +3,13 @@ import { history } from '@memohome/db/schema'
import { eq, desc, and, gte, lte, asc } from 'drizzle-orm'
export const getMemoryMessages = async (
userId: string,
query: {
limit: number
page: number
userId: string
}
) => {
const { limit, page, userId } = query
const { limit, page } = query
const results = await db
.select()
.from(history)
@@ -22,13 +22,13 @@ export const getMemoryMessages = async (
}
export const getMemoryMessagesFilter = async (
userId: string,
query: {
from: Date
to: Date
userId: string
}
) => {
const { from, to, userId } = query
const { from, to } = query
const results = await db
.select()
.from(history)
+4 -6
View File
@@ -1,20 +1,18 @@
import { z } from 'zod'
// MemoryUnit schema
const MemoryUnitSchema = z.object({
messages: z.array(z.object()),
// MemoryUnit schema (without user field, will be added from auth)
const MemoryUnitBodySchema = z.object({
messages: z.array(z.any()),
timestamp: z.coerce.date(),
user: z.string(),
})
export const AddMemoryModel = {
body: MemoryUnitSchema,
body: MemoryUnitBodySchema,
}
export const SearchMemoryModel = {
query: z.object({
query: z.string().min(1, 'Search query is required'),
userId: z.string().min(1, 'User ID is required'),
}),
}
+44 -57
View File
@@ -1,25 +1,47 @@
import Elysia from 'elysia'
import {
GetSettingsModel,
CreateSettingsModel,
UpdateSettingsModel,
} from './model'
import {
getSettings,
createSettings,
updateSettings,
upsertSettings,
} from './service'
import { bearer } from '@elysiajs/bearer'
import { jwt } from '@elysiajs/jwt'
import { UpdateSettingsModel } from './model'
import { getSettings, upsertSettings } from './service'
export const settingsModule = new Elysia({
prefix: '/settings',
})
// Get user settings
.get('/:userId', async ({ params }) => {
.use(
jwt({
name: 'jwt',
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
exp: process.env.JWT_EXPIRES_IN || '7d',
})
)
.use(bearer())
.derive(async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error('No bearer token provided')
}
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
throw new Error('Invalid or expired token')
}
return {
user: {
userId: payload.userId as string,
username: payload.username as string,
role: payload.role as string,
},
}
})
// Get current user's settings
.get('/', async ({ user, set }) => {
try {
const { userId } = params
const userSettings = await getSettings(userId)
const userSettings = await getSettings(user.userId)
if (!userSettings) {
set.status = 404
return {
success: false,
error: 'Settings not found',
@@ -30,62 +52,27 @@ export const settingsModule = new Elysia({
data: userSettings,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch settings',
}
}
}, GetSettingsModel)
// Create new settings
.post('/', async ({ body }) => {
})
// Update or create current user's settings
.put('/', async ({ user, body, set }) => {
try {
const newSettings = await createSettings(body)
const result = await upsertSettings(user.userId, body)
return {
success: true,
data: newSettings,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create settings',
}
}
}, CreateSettingsModel)
// Update settings
.put('/:userId', async ({ params, body }) => {
try {
const { userId } = params
const updatedSettings = await updateSettings(userId, body)
if (!updatedSettings) {
return {
success: false,
error: 'Settings not found',
}
}
return {
success: true,
data: updatedSettings,
data: result,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update settings',
}
}
}, UpdateSettingsModel)
// Upsert settings (create or update)
.patch('/', async ({ body }) => {
try {
const result = await upsertSettings(body)
return {
success: true,
data: result,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to upsert settings',
}
}
}, CreateSettingsModel)
+5 -21
View File
@@ -1,32 +1,16 @@
import { z } from 'zod'
const SettingsSchema = z.object({
userId: z.string().min(1, 'User ID is required'),
const SettingsBodySchema = z.object({
defaultChatModel: z.string().uuid().nullable().optional(),
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
defaultSummaryModel: z.string().uuid().nullable().optional(),
maxContextLoadTime: z.number().int().min(1).max(1440).optional(), // 1 minute to 24 hours
language: z.string().optional(),
})
export type SettingsInput = z.infer<typeof SettingsSchema>
export const GetSettingsModel = {
params: z.object({
userId: z.string(),
}),
}
export const CreateSettingsModel = {
body: SettingsSchema,
}
export type SettingsInput = z.infer<typeof SettingsBodySchema>
export const UpdateSettingsModel = {
params: z.object({
userId: z.string(),
}),
body: z.object({
defaultChatModel: z.string().uuid().nullable().optional(),
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
defaultSummaryModel: z.string().uuid().nullable().optional(),
}),
body: SettingsBodySchema,
}
+22 -35
View File
@@ -11,51 +11,38 @@ export const getSettings = async (userId: string) => {
return result
}
export const createSettings = async (data: SettingsInput) => {
const [newSettings] = await db
.insert(settings)
.values({
userId: data.userId,
defaultChatModel: data.defaultChatModel || null,
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
defaultSummaryModel: data.defaultSummaryModel || null,
})
.returning()
return newSettings
}
export const upsertSettings = async (userId: string, data: SettingsInput) => {
const updateData: Record<string, unknown> = {}
if (data.defaultChatModel !== undefined) {
updateData.defaultChatModel = data.defaultChatModel
}
if (data.defaultEmbeddingModel !== undefined) {
updateData.defaultEmbeddingModel = data.defaultEmbeddingModel
}
if (data.defaultSummaryModel !== undefined) {
updateData.defaultSummaryModel = data.defaultSummaryModel
}
if (data.maxContextLoadTime !== undefined) {
updateData.maxContextLoadTime = data.maxContextLoadTime
}
if (data.language !== undefined) {
updateData.language = data.language
}
export const updateSettings = async (
userId: string,
data: Partial<Omit<SettingsInput, 'userId'>>
) => {
const [updatedSettings] = await db
.update(settings)
.set({
defaultChatModel: data.defaultChatModel,
defaultEmbeddingModel: data.defaultEmbeddingModel,
defaultSummaryModel: data.defaultSummaryModel,
})
.where(eq(settings.userId, userId))
.returning()
return updatedSettings
}
export const upsertSettings = async (data: SettingsInput) => {
const [result] = await db
.insert(settings)
.values({
userId: data.userId,
userId: userId,
defaultChatModel: data.defaultChatModel || null,
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
defaultSummaryModel: data.defaultSummaryModel || null,
maxContextLoadTime: data.maxContextLoadTime || 60,
language: data.language || 'Same as user input',
})
.onConflictDoUpdate({
target: settings.userId,
set: {
defaultChatModel: data.defaultChatModel,
defaultEmbeddingModel: data.defaultEmbeddingModel,
defaultSummaryModel: data.defaultSummaryModel,
},
set: updateData,
})
.returning()
return result
+171
View File
@@ -0,0 +1,171 @@
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 () => {
try {
const userList = await getUsers()
return {
success: true,
data: userList,
}
} 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)
+63
View File
@@ -0,0 +1,63 @@
import { z } from 'zod'
// 用户角色枚举
const UserRoleSchema = z.enum(['admin', 'member'])
// 创建用户的 Schema
const CreateUserSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters').max(50),
email: z.string().email('Invalid email format').optional(),
password: z.string().min(6, 'Password must be at least 6 characters'),
role: UserRoleSchema.default('member'),
displayName: z.string().optional(),
avatarUrl: z.string().url('Invalid URL format').optional(),
})
// 更新用户的 Schema
const UpdateUserSchema = z.object({
email: z.string().email('Invalid email format').optional(),
role: UserRoleSchema.optional(),
displayName: z.string().optional(),
avatarUrl: z.string().url('Invalid URL format').optional(),
isActive: z.enum(['true', 'false']).optional(),
})
// 更新密码的 Schema
const UpdatePasswordSchema = z.object({
password: z.string().min(6, 'Password must be at least 6 characters'),
})
export type CreateUserInput = z.infer<typeof CreateUserSchema>
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>
export type UpdatePasswordInput = z.infer<typeof UpdatePasswordSchema>
export const GetUserByIdModel = {
params: z.object({
id: z.string().uuid('Invalid user ID format'),
}),
}
export const CreateUserModel = {
body: CreateUserSchema,
}
export const UpdateUserModel = {
params: z.object({
id: z.string().uuid('Invalid user ID format'),
}),
body: UpdateUserSchema,
}
export const DeleteUserModel = {
params: z.object({
id: z.string().uuid('Invalid user ID format'),
}),
}
export const UpdatePasswordModel = {
params: z.object({
id: z.string().uuid('Invalid user ID format'),
}),
body: UpdatePasswordSchema,
}
+213
View File
@@ -0,0 +1,213 @@
import { db } from '@memohome/db'
import { users, settings } from '@memohome/db/schema'
import { eq } from 'drizzle-orm'
import type { CreateUserInput, UpdateUserInput } from './model'
/**
*
*/
export const getUsers = async () => {
const userList = await db
.select({
id: users.id,
username: users.username,
email: users.email,
role: users.role,
displayName: users.displayName,
avatarUrl: users.avatarUrl,
isActive: users.isActive,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
lastLoginAt: users.lastLoginAt,
})
.from(users)
.orderBy(users.createdAt)
return userList
}
/**
* ID
*/
export const getUserById = async (id: string) => {
const [user] = await db
.select({
id: users.id,
username: users.username,
email: users.email,
role: users.role,
displayName: users.displayName,
avatarUrl: users.avatarUrl,
isActive: users.isActive,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
lastLoginAt: users.lastLoginAt,
})
.from(users)
.where(eq(users.id, id))
return user
}
/**
*
*/
export const createUser = async (data: CreateUserInput) => {
// 检查用户名是否已存在
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.username, data.username))
if (existingUser) {
throw new Error('Username already exists')
}
// 检查邮箱是否已存在(如果提供了邮箱)
if (data.email) {
const [existingEmail] = await db
.select()
.from(users)
.where(eq(users.email, data.email))
if (existingEmail) {
throw new Error('Email already exists')
}
}
// 加密密码
const passwordHash = await Bun.password.hash(data.password)
// 创建用户
const [newUser] = await db
.insert(users)
.values({
username: data.username,
email: data.email || null,
passwordHash,
role: data.role || 'member',
displayName: data.displayName || null,
avatarUrl: data.avatarUrl || null,
})
.returning({
id: users.id,
username: users.username,
email: users.email,
role: users.role,
displayName: users.displayName,
avatarUrl: users.avatarUrl,
isActive: users.isActive,
createdAt: users.createdAt,
})
// 自动创建用户的 settings 条目(使用默认值)
await db
.insert(settings)
.values({
userId: newUser.id,
defaultChatModel: null,
defaultEmbeddingModel: null,
defaultSummaryModel: null,
maxContextLoadTime: 60,
language: 'Same as user input',
})
return newUser
}
/**
*
*/
export const updateUser = async (id: string, data: UpdateUserInput) => {
// 检查用户是否存在
const existingUser = await getUserById(id)
if (!existingUser) {
return null
}
// 如果更新邮箱,检查邮箱是否已被其他用户使用
if (data.email) {
const [emailUser] = await db
.select()
.from(users)
.where(eq(users.email, data.email))
if (emailUser && emailUser.id !== id) {
throw new Error('Email already exists')
}
}
// 更新用户
const [updatedUser] = await db
.update(users)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(users.id, id))
.returning({
id: users.id,
username: users.username,
email: users.email,
role: users.role,
displayName: users.displayName,
avatarUrl: users.avatarUrl,
isActive: users.isActive,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
lastLoginAt: users.lastLoginAt,
})
return updatedUser
}
/**
*
*/
export const deleteUser = async (id: string) => {
// 检查用户是否存在
const existingUser = await getUserById(id)
if (!existingUser) {
return null
}
const [deletedUser] = await db
.delete(users)
.where(eq(users.id, id))
.returning({
id: users.id,
username: users.username,
})
return deletedUser
}
/**
*
*/
export const updateUserPassword = async (id: string, password: string) => {
// 检查用户是否存在
const existingUser = await getUserById(id)
if (!existingUser) {
return null
}
// 加密新密码
const passwordHash = await Bun.password.hash(password)
// 更新密码
const [updatedUser] = await db
.update(users)
.set({
passwordHash,
updatedAt: new Date(),
})
.where(eq(users.id, id))
.returning({
id: users.id,
username: users.username,
})
return updatedUser
}