init
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
// Экспорт всех auth сервисов и типов
|
||||
export * from "./types";
|
||||
export * from "./user";
|
||||
export * from "./session";
|
||||
export * from "./login";
|
||||
export * from "./register";
|
||||
@@ -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(),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user