feat: Контроллер и сервис пользователей
This commit is contained in:
83
apps/backend/src/controllers/users.controller.ts
Normal file
83
apps/backend/src/controllers/users.controller.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Elysia, t } from 'elysia';
|
||||||
|
import { usersService } from '@/services/users.service';
|
||||||
|
|
||||||
|
export const usersController = new Elysia({ prefix: '/users' })
|
||||||
|
.get(
|
||||||
|
'/by-name',
|
||||||
|
async ({ query, set }) => {
|
||||||
|
try {
|
||||||
|
const name = query.name?.trim();
|
||||||
|
|
||||||
|
const foundUser = await usersService.getByName(name);
|
||||||
|
|
||||||
|
if (!foundUser) {
|
||||||
|
set.status = 404;
|
||||||
|
return { error: 'User not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: foundUser.id,
|
||||||
|
name: foundUser.name,
|
||||||
|
email: foundUser.email,
|
||||||
|
image: foundUser.image,
|
||||||
|
emailVerified: foundUser.emailVerified,
|
||||||
|
createdAt: foundUser.createdAt.toISOString(),
|
||||||
|
updatedAt: foundUser.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
set.status = 500;
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to get user',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String({ minLength: 1 }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
tags: ['Users'],
|
||||||
|
summary: 'Get user by name',
|
||||||
|
description: 'Returns user information by name',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/:id',
|
||||||
|
async ({ params: { id }, set }) => {
|
||||||
|
try {
|
||||||
|
const foundUser = await usersService.getById(id);
|
||||||
|
|
||||||
|
if (!foundUser) {
|
||||||
|
set.status = 404;
|
||||||
|
return { error: 'User not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: foundUser.id,
|
||||||
|
name: foundUser.name,
|
||||||
|
email: foundUser.email,
|
||||||
|
image: foundUser.image,
|
||||||
|
emailVerified: foundUser.emailVerified,
|
||||||
|
createdAt: foundUser.createdAt.toISOString(),
|
||||||
|
updatedAt: foundUser.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
set.status = 500;
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to get user',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
tags: ['Users'],
|
||||||
|
summary: 'Get user by ID',
|
||||||
|
description: 'Returns user information by ID',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
@ -4,6 +4,7 @@ import { auth } from "@/lib/auth";
|
|||||||
import { env } from "./config/env";
|
import { env } from "./config/env";
|
||||||
import { AuthOpenAPI } from "./lib/auth/openapi";
|
import { AuthOpenAPI } from "./lib/auth/openapi";
|
||||||
import { purple } from "./lib/term/color";
|
import { purple } from "./lib/term/color";
|
||||||
|
import { usersController } from "./controllers/users.controller";
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(openapi({
|
.use(openapi({
|
||||||
@ -13,6 +14,7 @@ const app = new Elysia()
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.mount('/auth', auth.handler)
|
.mount('/auth', auth.handler)
|
||||||
|
.use(usersController)
|
||||||
.listen(env.PORT);
|
.listen(env.PORT);
|
||||||
|
|
||||||
const hostname = app.server?.hostname
|
const hostname = app.server?.hostname
|
||||||
@ -27,6 +29,6 @@ console.log(purple`
|
|||||||
/_/
|
/_/
|
||||||
`)
|
`)
|
||||||
|
|
||||||
console.log(` ${purple`started server`} @ ${hostname}:${port}`);
|
console.log(` ${purple`started server`} @ http://${hostname}:${port}`);
|
||||||
console.log(` ${purple`visit scalar`} @ ${hostname}:${port}/openapi`)
|
console.log(` ${purple`visit scalar`} @ http://${hostname}:${port}/openapi`)
|
||||||
|
|
||||||
|
|||||||
90
apps/backend/src/services/redis.service.ts
Normal file
90
apps/backend/src/services/redis.service.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { env } from '@/config/env';
|
||||||
|
import { RedisClient } from 'bun';
|
||||||
|
|
||||||
|
class RedisService {
|
||||||
|
private client: any = null;
|
||||||
|
private isConnected = false;
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (this.isConnected) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.client = new RedisClient(env.REDIS_URL);
|
||||||
|
this.isConnected = true;
|
||||||
|
console.log('Redis connected');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to Redis:', error);
|
||||||
|
if (env.NODE_ENV === 'test') {
|
||||||
|
console.warn('Running without Redis in test mode');
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(key: string): Promise<T | null> {
|
||||||
|
if (!this.isConnected) return null;
|
||||||
|
try {
|
||||||
|
const value = await this.client.get(key);
|
||||||
|
return value ? JSON.parse(value) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Redis GET error for key ${key}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||||
|
if (!this.isConnected) return;
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
if (ttl) {
|
||||||
|
await this.client.setex(key, ttl, serialized);
|
||||||
|
} else {
|
||||||
|
await this.client.set(key, serialized);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Redis SET error for key ${key}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string): Promise<void> {
|
||||||
|
if (!this.isConnected) return;
|
||||||
|
try {
|
||||||
|
await this.client.del(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Redis DEL error for key ${key}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delPattern(pattern: string): Promise<void> {
|
||||||
|
if (!this.isConnected) return;
|
||||||
|
try {
|
||||||
|
const keys = await this.client.keys(pattern);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await this.client.del(...keys);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Redis DEL pattern error for ${pattern}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
if (this.isConnected && this.client) {
|
||||||
|
try {
|
||||||
|
if (typeof this.client.quit === 'function') {
|
||||||
|
await this.client.quit();
|
||||||
|
} else if (typeof this.client.close === 'function') {
|
||||||
|
await this.client.close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error disconnecting Redis:', error);
|
||||||
|
} finally {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redis = new RedisService();
|
||||||
|
|
||||||
60
apps/backend/src/services/users.service.ts
Normal file
60
apps/backend/src/services/users.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { eq, ilike } from 'drizzle-orm';
|
||||||
|
import { db, user } from '@/db';
|
||||||
|
import { redis } from './redis.service';
|
||||||
|
|
||||||
|
class UsersService {
|
||||||
|
private readonly CACHE_TTL = 3600;
|
||||||
|
|
||||||
|
async getById(userId: string): Promise<typeof user.$inferSelect | null> {
|
||||||
|
const cacheKey = `user:${userId}`;
|
||||||
|
const cached = await redis.get<typeof user.$inferSelect>(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const [foundUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(user)
|
||||||
|
.where(eq(user.id, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (foundUser) {
|
||||||
|
await redis.set(cacheKey, foundUser, this.CACHE_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundUser || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByName(name: string): Promise<typeof user.$inferSelect | null> {
|
||||||
|
if (!name || name.trim().length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchName = name.trim();
|
||||||
|
const cacheKey = `user_by_name:${searchName}`;
|
||||||
|
|
||||||
|
const cached = await redis.get<typeof user.$inferSelect>(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
let [foundUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(user)
|
||||||
|
.where(eq(user.name, searchName))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!foundUser) {
|
||||||
|
[foundUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(user)
|
||||||
|
.where(ilike(user.name, searchName))
|
||||||
|
.limit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundUser) {
|
||||||
|
await redis.set(cacheKey, foundUser, this.CACHE_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundUser || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usersService = new UsersService();
|
||||||
|
|
||||||
Reference in New Issue
Block a user