mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 02:06:42 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
53
services/mana-core-auth/src/auth/auth.controller.ts
Normal file
53
services/mana-core-auth/src/auth/auth.controller.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Controller, Post, Body, UseGuards, Req, Ip, Headers } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AuthService } from './auth.service';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser, CurrentUserData } from '../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
async register(
|
||||
@Body() registerDto: RegisterDto,
|
||||
@Ip() ipAddress: string,
|
||||
@Headers('user-agent') userAgent: string,
|
||||
) {
|
||||
return this.authService.register(registerDto, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(
|
||||
@Body() loginDto: LoginDto,
|
||||
@Ip() ipAddress: string,
|
||||
@Headers('user-agent') userAgent: string,
|
||||
) {
|
||||
return this.authService.login(loginDto, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
async refresh(
|
||||
@Body() refreshTokenDto: RefreshTokenDto,
|
||||
@Ip() ipAddress: string,
|
||||
@Headers('user-agent') userAgent: string,
|
||||
) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async logout(@Req() req: Request & { user: CurrentUserData }) {
|
||||
// Extract sessionId from JWT (would need to be added to the CurrentUserData interface)
|
||||
// For now, we'll use a placeholder
|
||||
return this.authService.logout('session-id');
|
||||
}
|
||||
|
||||
@Post('validate')
|
||||
async validate(@Body() body: { token: string }) {
|
||||
return this.authService.validateToken(body.token);
|
||||
}
|
||||
}
|
||||
10
services/mana-core-auth/src/auth/auth.module.ts
Normal file
10
services/mana-core-auth/src/auth/auth.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
291
services/mana-core-auth/src/auth/auth.service.ts
Normal file
291
services/mana-core-auth/src/auth/auth.service.ts
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import { Injectable, UnauthorizedException, ConflictException, BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { eq, and, isNull } from 'drizzle-orm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getDb } from '../db/connection';
|
||||
import { users, passwords, sessions } from '../db/schema';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
|
||||
export interface TokenPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string;
|
||||
sessionId: string;
|
||||
deviceId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
private getDb() {
|
||||
const databaseUrl = this.configService.get<string>('database.url');
|
||||
return getDb(databaseUrl!);
|
||||
}
|
||||
|
||||
async register(registerDto: RegisterDto, ipAddress?: string, userAgent?: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, registerDto.email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
throw new ConflictException('User with this email already exists');
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(registerDto.password, 12);
|
||||
|
||||
// Create user
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
email: registerDto.email.toLowerCase(),
|
||||
name: registerDto.name,
|
||||
role: 'user',
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Store password
|
||||
await db.insert(passwords).values({
|
||||
userId: newUser.id,
|
||||
hashedPassword,
|
||||
});
|
||||
|
||||
// Initialize credit balance (done via trigger or separate service call)
|
||||
// This will be handled by the credits service
|
||||
|
||||
return {
|
||||
id: newUser.id,
|
||||
email: newUser.email,
|
||||
name: newUser.name,
|
||||
createdAt: newUser.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
async login(loginDto: LoginDto, ipAddress?: string, userAgent?: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Find user
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, loginDto.email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// Check if user is soft-deleted
|
||||
if (user.deletedAt) {
|
||||
throw new UnauthorizedException('Account has been deleted');
|
||||
}
|
||||
|
||||
// Get password
|
||||
const [passwordRecord] = await db
|
||||
.select()
|
||||
.from(passwords)
|
||||
.where(eq(passwords.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
if (!passwordRecord) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await bcrypt.compare(loginDto.password, passwordRecord.hashedPassword);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokenData = await this.generateTokens(
|
||||
user.id,
|
||||
user.email,
|
||||
user.role,
|
||||
loginDto.deviceId,
|
||||
loginDto.deviceName,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
},
|
||||
...tokenData,
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string, ipAddress?: string, userAgent?: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Find session by refresh token
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(and(eq(sessions.refreshToken, refreshToken), isNull(sessions.revokedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (!session) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
// Check if refresh token is expired
|
||||
if (new Date() > session.refreshTokenExpiresAt) {
|
||||
throw new UnauthorizedException('Refresh token expired');
|
||||
}
|
||||
|
||||
// Get user
|
||||
const [user] = await db.select().from(users).where(eq(users.id, session.userId)).limit(1);
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
}
|
||||
|
||||
// Revoke old session (refresh token rotation)
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({ revokedAt: new Date() })
|
||||
.where(eq(sessions.id, session.id));
|
||||
|
||||
// Generate new tokens
|
||||
const tokenData = await this.generateTokens(
|
||||
user.id,
|
||||
user.email,
|
||||
user.role,
|
||||
session.deviceId ?? undefined,
|
||||
session.deviceName ?? undefined,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
},
|
||||
...tokenData,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(sessionId: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({ revokedAt: new Date() })
|
||||
.where(eq(sessions.id, sessionId));
|
||||
|
||||
return { message: 'Logged out successfully' };
|
||||
}
|
||||
|
||||
private async generateTokens(
|
||||
userId: string,
|
||||
email: string,
|
||||
role: string,
|
||||
deviceId?: string,
|
||||
deviceName?: string,
|
||||
ipAddress?: string,
|
||||
userAgent?: string,
|
||||
) {
|
||||
const db = this.getDb();
|
||||
|
||||
const privateKeyRaw = this.configService.get<string>('jwt.privateKey');
|
||||
if (!privateKeyRaw) {
|
||||
throw new Error('JWT private key not configured');
|
||||
}
|
||||
const privateKey: string = privateKeyRaw;
|
||||
const accessTokenExpiry = this.configService.get<string>('jwt.accessTokenExpiry') || '15m';
|
||||
const refreshTokenExpiry = this.configService.get<string>('jwt.refreshTokenExpiry') || '7d';
|
||||
const issuer = this.configService.get<string>('jwt.issuer');
|
||||
const audience = this.configService.get<string>('jwt.audience');
|
||||
|
||||
// Generate session ID (must be UUID for database)
|
||||
const sessionId = randomUUID();
|
||||
|
||||
// Create session record
|
||||
const refreshTokenString = nanoid(64);
|
||||
const refreshTokenExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
const accessTokenExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
|
||||
|
||||
await db.insert(sessions).values({
|
||||
id: sessionId,
|
||||
userId,
|
||||
token: sessionId,
|
||||
refreshToken: refreshTokenString,
|
||||
refreshTokenExpiresAt,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
deviceId,
|
||||
deviceName,
|
||||
expiresAt: accessTokenExpiresAt,
|
||||
});
|
||||
|
||||
// Generate JWT payload
|
||||
const tokenPayload: Record<string, unknown> = {
|
||||
sub: userId,
|
||||
email,
|
||||
role,
|
||||
sessionId,
|
||||
...(deviceId && { deviceId }),
|
||||
};
|
||||
|
||||
// Sign access token
|
||||
const accessToken = jwt.sign(tokenPayload, privateKey, {
|
||||
algorithm: 'RS256' as const,
|
||||
expiresIn: accessTokenExpiry as jwt.SignOptions['expiresIn'],
|
||||
...(issuer && { issuer }),
|
||||
...(audience && { audience }),
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: refreshTokenString,
|
||||
expiresIn: 15 * 60, // 15 minutes in seconds
|
||||
tokenType: 'Bearer',
|
||||
};
|
||||
}
|
||||
|
||||
async validateToken(token: string) {
|
||||
try {
|
||||
const publicKey = this.configService.get<string>('jwt.publicKey');
|
||||
if (!publicKey) {
|
||||
throw new Error('JWT public key not configured');
|
||||
}
|
||||
const audience = this.configService.get<string>('jwt.audience');
|
||||
const issuer = this.configService.get<string>('jwt.issuer');
|
||||
|
||||
const payload = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
audience,
|
||||
issuer,
|
||||
}) as TokenPayload;
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
payload,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
17
services/mana-core-auth/src/auth/dto/login.dto.ts
Normal file
17
services/mana-core-auth/src/auth/dto/login.dto.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { IsEmail, IsString, IsOptional } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deviceId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deviceName?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { IsString } from 'class-validator';
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
}
|
||||
16
services/mana-core-auth/src/auth/dto/register.dto.ts
Normal file
16
services/mana-core-auth/src/auth/dto/register.dto.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MaxLength(128)
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
name?: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue