Skip to content

Commit 229c7f9

Browse files
committed
initialized the auth module
1 parent b1c678b commit 229c7f9

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
Controller,
3+
Post,
4+
Get,
5+
Body,
6+
UseGuards,
7+
HttpCode,
8+
HttpStatus,
9+
Headers,
10+
UnauthorizedException,
11+
} from '@nestjs/common';
12+
import {
13+
ApiTags,
14+
ApiOperation,
15+
ApiResponse,
16+
ApiBearerAuth,
17+
} from '@nestjs/swagger';
18+
import { JwtService } from '@nestjs/jwt';
19+
import { ConfigService } from '@nestjs/config';
20+
import { AuthService } from './auth.service';
21+
import { RegisterDto } from './dto/register.dto';
22+
import { LoginDto } from './dto/login.dto';
23+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
24+
import { CurrentUser } from './decorators/current-user.decorator';
25+
import { User } from '../users/user.entity';
26+
27+
@ApiTags('Auth')
28+
@Controller('auth')
29+
export class AuthController {
30+
constructor(
31+
private readonly authService: AuthService,
32+
private readonly jwtService: JwtService,
33+
private readonly configService: ConfigService,
34+
) {}
35+
36+
@Post('register')
37+
@ApiOperation({ summary: 'Create a new account' })
38+
@ApiResponse({ status: 201, description: 'Account created successfully' })
39+
@ApiResponse({ status: 409, description: 'Email already in use' })
40+
async register(@Body() dto: RegisterDto) {
41+
const { user, tokens } = await this.authService.register(dto);
42+
return {
43+
user: {
44+
id: user.id,
45+
email: user.email,
46+
firstName: user.firstName,
47+
lastName: user.lastName,
48+
role: user.role,
49+
},
50+
...tokens,
51+
};
52+
}
53+
54+
@Post('login')
55+
@HttpCode(HttpStatus.OK)
56+
@ApiOperation({ summary: 'Sign in with email and password' })
57+
@ApiResponse({ status: 200, description: 'Login successful' })
58+
@ApiResponse({ status: 401, description: 'Invalid credentials' })
59+
async login(@Body() dto: LoginDto) {
60+
const { user, tokens } = await this.authService.login(dto);
61+
return {
62+
user: {
63+
id: user.id,
64+
email: user.email,
65+
firstName: user.firstName,
66+
lastName: user.lastName,
67+
role: user.role,
68+
},
69+
...tokens,
70+
};
71+
}
72+
73+
@Post('refresh')
74+
@HttpCode(HttpStatus.OK)
75+
@ApiOperation({ summary: 'Refresh access token using refresh token' })
76+
@ApiResponse({ status: 200, description: 'Tokens refreshed' })
77+
@ApiResponse({ status: 401, description: 'Invalid or expired refresh token' })
78+
async refresh(@Headers('authorization') authHeader: string) {
79+
if (!authHeader?.startsWith('Bearer ')) {
80+
throw new UnauthorizedException('Refresh token required');
81+
}
82+
const refreshToken = authHeader.slice(7);
83+
84+
let payload: { sub: string };
85+
try {
86+
payload = await this.jwtService.verifyAsync(refreshToken, {
87+
secret: this.configService.get<string>('JWT_REFRESH_SECRET', 'change-me-refresh'),
88+
});
89+
} catch {
90+
throw new UnauthorizedException('Session expired. Please log in again.');
91+
}
92+
93+
const tokens = await this.authService.refresh(payload.sub, refreshToken);
94+
return tokens;
95+
}
96+
97+
@Post('logout')
98+
@HttpCode(HttpStatus.NO_CONTENT)
99+
@UseGuards(JwtAuthGuard)
100+
@ApiBearerAuth('JWT-auth')
101+
@ApiOperation({ summary: 'Sign out and invalidate tokens' })
102+
async logout(@CurrentUser() user: User) {
103+
await this.authService.logout(user.id);
104+
}
105+
106+
@Get('me')
107+
@UseGuards(JwtAuthGuard)
108+
@ApiBearerAuth('JWT-auth')
109+
@ApiOperation({ summary: 'Get currently authenticated user' })
110+
me(@CurrentUser() user: User) {
111+
return {
112+
id: user.id,
113+
email: user.email,
114+
firstName: user.firstName,
115+
lastName: user.lastName,
116+
role: user.role,
117+
createdAt: user.createdAt,
118+
};
119+
}
120+
}

backend/src/auth/auth.module.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Module } from '@nestjs/common';
2+
import { JwtModule } from '@nestjs/jwt';
3+
import { PassportModule } from '@nestjs/passport';
4+
import { AuthController } from './auth.controller';
5+
import { AuthService } from './auth.service';
6+
import { JwtStrategy } from './strategies/jwt.strategy';
7+
import { UsersModule } from '../users/users.module';
8+
9+
@Module({
10+
imports: [
11+
UsersModule,
12+
PassportModule,
13+
JwtModule.register({}),
14+
],
15+
controllers: [AuthController],
16+
providers: [AuthService, JwtStrategy],
17+
exports: [AuthService, JwtModule],
18+
})
19+
export class AuthModule {}

backend/src/auth/auth.service.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
Injectable,
3+
ConflictException,
4+
UnauthorizedException,
5+
} from '@nestjs/common';
6+
import { JwtService } from '@nestjs/jwt';
7+
import { ConfigService } from '@nestjs/config';
8+
import * as bcrypt from 'bcrypt';
9+
import { UsersService } from '../users/users.service';
10+
import { RegisterDto } from './dto/register.dto';
11+
import { LoginDto } from './dto/login.dto';
12+
import { User } from '../users/user.entity';
13+
14+
export interface AuthTokens {
15+
accessToken: string;
16+
refreshToken: string;
17+
}
18+
19+
@Injectable()
20+
export class AuthService {
21+
constructor(
22+
private readonly usersService: UsersService,
23+
private readonly jwtService: JwtService,
24+
private readonly configService: ConfigService,
25+
) {}
26+
27+
async register(dto: RegisterDto): Promise<{ user: User; tokens: AuthTokens }> {
28+
const existing = await this.usersService.findByEmail(dto.email);
29+
if (existing) {
30+
throw new ConflictException('An account with this email already exists');
31+
}
32+
33+
const hashedPassword = await bcrypt.hash(dto.password, 12);
34+
const user = await this.usersService.create({
35+
email: dto.email.toLowerCase(),
36+
firstName: dto.firstName,
37+
lastName: dto.lastName,
38+
password: hashedPassword,
39+
});
40+
41+
const tokens = await this.signTokens(user);
42+
await this.storeRefreshToken(user.id, tokens.refreshToken);
43+
44+
return { user, tokens };
45+
}
46+
47+
async login(dto: LoginDto): Promise<{ user: User; tokens: AuthTokens }> {
48+
const user = await this.usersService.findByEmail(dto.email.toLowerCase());
49+
if (!user) {
50+
throw new UnauthorizedException('Invalid email or password');
51+
}
52+
53+
const passwordMatch = await bcrypt.compare(dto.password, user.password);
54+
if (!passwordMatch) {
55+
throw new UnauthorizedException('Invalid email or password');
56+
}
57+
58+
const tokens = await this.signTokens(user);
59+
await this.storeRefreshToken(user.id, tokens.refreshToken);
60+
61+
return { user, tokens };
62+
}
63+
64+
async refresh(userId: string, incomingRefreshToken: string): Promise<AuthTokens> {
65+
const user = await this.usersService.findByRefreshToken(userId);
66+
if (!user || !user.refreshToken) {
67+
throw new UnauthorizedException('Session expired. Please log in again.');
68+
}
69+
70+
const tokenMatch = await bcrypt.compare(incomingRefreshToken, user.refreshToken);
71+
if (!tokenMatch) {
72+
throw new UnauthorizedException('Session expired. Please log in again.');
73+
}
74+
75+
const tokens = await this.signTokens(user);
76+
await this.storeRefreshToken(user.id, tokens.refreshToken);
77+
return tokens;
78+
}
79+
80+
async logout(userId: string): Promise<void> {
81+
await this.usersService.updateRefreshToken(userId, null);
82+
}
83+
84+
private async signTokens(user: User): Promise<AuthTokens> {
85+
const payload = { sub: user.id, email: user.email, role: user.role };
86+
87+
const [accessToken, refreshToken] = await Promise.all([
88+
this.jwtService.signAsync(payload, {
89+
secret: this.configService.get<string>('JWT_SECRET', 'change-me-in-env'),
90+
expiresIn: '15m',
91+
}),
92+
this.jwtService.signAsync(payload, {
93+
secret: this.configService.get<string>('JWT_REFRESH_SECRET', 'change-me-refresh'),
94+
expiresIn: '7d',
95+
}),
96+
]);
97+
98+
return { accessToken, refreshToken };
99+
}
100+
101+
private async storeRefreshToken(userId: string, token: string): Promise<void> {
102+
const hashed = await bcrypt.hash(token, 10);
103+
await this.usersService.updateRefreshToken(userId, hashed);
104+
}
105+
}

0 commit comments

Comments
 (0)