- 后端:Bun + Hono + Drizzle ORM + SQLite - 前端:Vue 3 + Naive UI + ECharts - 项目管理:创建项目 + 绑定 Git 仓库 - OKR 系统:目标/关键结果 CRUD + 进度追踪 - Git 同步:Gitea API 自动同步 commit/PR + 作者关联 - 数据看板:项目 OKR 进度 + KR 状态分布 + 代码活动 - 权限体系:admin/manager/developer/viewer 四级 - Docker 部署:docker-compose + nginx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
2.2 KiB
TypeScript
90 lines
2.2 KiB
TypeScript
import { eq } from 'drizzle-orm';
|
|
import { SignJWT } from 'jose';
|
|
import bcrypt from 'bcrypt';
|
|
import { db } from '../db/index';
|
|
import { users } from '../db/schema';
|
|
import { config } from '../config';
|
|
import { AppError } from '../middleware/error-handler';
|
|
|
|
const secret = new TextEncoder().encode(config.JWT_SECRET);
|
|
|
|
export async function login(email: string, password: string) {
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.email, email),
|
|
});
|
|
|
|
if (!user) {
|
|
throw new AppError(40101, 'Invalid email or password', 401);
|
|
}
|
|
|
|
// Check account lockout
|
|
if (user.lockedUntil && user.lockedUntil > new Date()) {
|
|
const retryAfter = Math.ceil((user.lockedUntil.getTime() - Date.now()) / 1000);
|
|
throw new AppError(42300, `Account locked. Try again in ${Math.ceil(retryAfter / 60)} minutes`, 423);
|
|
}
|
|
|
|
// Verify password
|
|
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
if (!isValid) {
|
|
const attempts = (user.loginAttempts || 0) + 1;
|
|
const updateData: Record<string, any> = {
|
|
loginAttempts: attempts,
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
if (attempts >= 5) {
|
|
updateData.lockedUntil = new Date(Date.now() + 15 * 60 * 1000);
|
|
}
|
|
|
|
await db.update(users)
|
|
.set(updateData)
|
|
.where(eq(users.id, user.id));
|
|
|
|
throw new AppError(40101, 'Invalid email or password', 401);
|
|
}
|
|
|
|
// Reset login attempts on success
|
|
await db.update(users)
|
|
.set({ loginAttempts: 0, lockedUntil: null, updatedAt: new Date() })
|
|
.where(eq(users.id, user.id));
|
|
|
|
// Generate JWT
|
|
const token = await new SignJWT({
|
|
sub: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
displayName: user.displayName,
|
|
})
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime('7d')
|
|
.sign(secret);
|
|
|
|
return {
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
displayName: user.displayName,
|
|
email: user.email,
|
|
role: user.role,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function getUserById(userId: string) {
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.id, userId),
|
|
});
|
|
|
|
if (!user) {
|
|
throw new AppError(40401, 'User not found', 404);
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
displayName: user.displayName,
|
|
email: user.email,
|
|
role: user.role,
|
|
};
|
|
}
|