This commit is contained in:
2025-10-03 15:43:22 +05:00
commit 531e2d2e7e
54 changed files with 2943 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
// Экспорт всех auth сервисов и типов
export * from "./types";
export * from "./user";
export * from "./session";
export * from "./login";
export * from "./register";
+51
View File
@@ -0,0 +1,51 @@
import { userService } from "./user";
import { sessionService } from "./session";
import type { LoginResult, SessionMetadata } from "./types";
/**
* Сервис авторизации
*/
export const loginService = {
/**
* Авторизация пользователя
*/
async login(
email: string,
password: string,
metadata: SessionMetadata
): Promise<LoginResult | null> {
// Найти пользователя по email
const user = await userService.findByEmail(email);
if (!user) {
return null;
}
// Проверить пароль
const isPasswordValid = await userService.verifyPassword(
password,
user.password
);
if (!isPasswordValid) {
return null;
}
// Создать новую сессию
const { sessionId, accessToken } = await sessionService.create(user.id, metadata);
// Вычислить дату истечения токена
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
return {
user: userService.sanitize(user),
session: {
id: sessionId,
token: accessToken,
expiresAt: expiresAt.toISOString(),
},
};
},
};
+48
View File
@@ -0,0 +1,48 @@
import { userService } from "./user";
import type { RegisterData, UserResponse } from "./types";
// Роль по умолчанию для новых пользователей
const DEFAULT_ROLE_NAME = "manager" as const;
/**
* Сервис регистрации
*/
export const registerService = {
/**
* Регистрация нового пользователя
* @param data - данные для регистрации
* @param callerRole - роль вызывающего пользователя (если авторизован)
*/
async register(
data: RegisterData,
callerRole?: string
): Promise<UserResponse | null> {
// Проверить, существует ли пользователь
const existingUser = await userService.findByEmail(data.email);
if (existingUser) {
return null;
}
// Захешировать пароль
const hashedPassword = await userService.hashPassword(data.password);
// Определить роль для нового пользователя
// Только администраторы могут указывать кастомную роль
const role =
callerRole === "admin" && data.role
? data.role
: DEFAULT_ROLE_NAME;
// Создать пользователя
const newUser = await userService.create({
email: data.email,
password: hashedPassword,
fullName: data.fullName,
role,
});
return userService.sanitize(newUser);
},
};
+80
View File
@@ -0,0 +1,80 @@
import { eq } from "drizzle-orm";
import { SignJWT } from "jose";
import db from "../../db";
import { authSessions } from "../../db/schema/authSessions";
import type { SessionMetadata } from "./types";
// JWT секрет (лучше вынести в .env)
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "your-secret-key-change-this"
);
const TOKEN_EXPIRATION_DAYS = 7;
/**
* Сервис для работы с сессиями
*/
export const sessionService = {
/**
* Создать новую сессию
*/
async create(
userId: string,
metadata: SessionMetadata
): Promise<{ sessionId: string; accessToken: string }> {
const sessionId = crypto.randomUUID();
// Создать JWT токен с id сессии
const accessToken = await new SignJWT({ id: sessionId })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(`${TOKEN_EXPIRATION_DAYS}d`)
.sign(JWT_SECRET);
// Захешировать токен через bcrypt для хранения в БД
const accessTokenHash = await Bun.password.hash(accessToken, {
algorithm: "bcrypt",
});
// Вычислить дату истечения
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + TOKEN_EXPIRATION_DAYS);
// Сохранить сессию в БД
await db.insert(authSessions).values({
id: sessionId,
userId,
accessTokenHash,
userAgent: metadata.userAgent,
ipAddress: metadata.ipAddress,
expiresAt,
});
return { sessionId, accessToken };
},
/**
* Отозвать сессию (logout)
*/
async revoke(sessionId: string): Promise<void> {
await db
.update(authSessions)
.set({ revokedAt: new Date() })
.where(eq(authSessions.id, sessionId));
},
/**
* Получить сессию по ID
*/
async findById(sessionId: string) {
const session = (
await db
.select()
.from(authSessions)
.where(eq(authSessions.id, sessionId))
.limit(1)
)[0];
return session || null;
},
};
+38
View File
@@ -0,0 +1,38 @@
// Типы для auth сервисов
export type RoleName = "admin" | "director" | "manager";
export type UserResponse = {
id: string;
email: string;
fullName: string;
role: RoleName;
createdAt: Date;
};
export type LoginResult = {
user: UserResponse;
session: {
id: string;
token: string;
expiresAt: string;
};
};
export type RegisterData = {
email: string;
password: string;
fullName: string;
role?: RoleName; // Опциональное поле, по умолчанию используется "manager"
};
export type LoginData = {
email: string;
password: string;
};
export type SessionMetadata = {
userAgent: string | null;
ipAddress: string | null;
};
+83
View File
@@ -0,0 +1,83 @@
import { eq } from "drizzle-orm";
import db from "../../db";
import { users } from "../../db/schema/users";
import type { User } from "../../db/schema/users";
import type { RoleName, UserResponse } from "./types";
/**
* Сервис для работы с пользователями
*/
export const userService = {
/**
* Получить пользователя по email
*/
async findByEmail(email: string): Promise<User | null> {
const user = (
await db.select().from(users).where(eq(users.email, email)).limit(1)
)[0];
return user || null;
},
/**
* Получить пользователя по ID
*/
async findById(userId: string): Promise<User | null> {
const user = (
await db.select().from(users).where(eq(users.id, userId)).limit(1)
)[0];
return user || null;
},
/**
* Создать нового пользователя
*/
async create(data: {
email: string;
password: string;
fullName: string;
role: RoleName;
}): Promise<User> {
const newUser = (
await db
.insert(users)
.values({
email: data.email,
password: data.password,
fullName: data.fullName,
role: data.role,
})
.returning()
)[0];
return newUser;
},
/**
* Проверить пароль пользователя
*/
async verifyPassword(password: string, hash: string): Promise<boolean> {
return await Bun.password.verify(password, hash);
},
/**
* Захешировать пароль
*/
async hashPassword(password: string): Promise<string> {
return await Bun.password.hash(password, { algorithm: "bcrypt" });
},
/**
* Убрать пароль из объекта пользователя
*/
sanitize(user: User): UserResponse {
return {
id: user.id,
email: user.email,
fullName: user.fullName,
role: user.role,
createdAt: user.createdAt,
};
},
};