Skip to content

Commit 7d2afac

Browse files
committed
feat: add auth module with Passport strategies, JWT, and OAuth
1 parent 8f530e3 commit 7d2afac

16 files changed

Lines changed: 1092 additions & 22 deletions

‎package.json‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,20 @@
2121
"@nestjs/common": "^10.4.15",
2222
"@nestjs/config": "^3.3.0",
2323
"@nestjs/core": "^10.4.15",
24+
"@nestjs/jwt": "^11.0.2",
25+
"@nestjs/passport": "^11.0.5",
2426
"@nestjs/platform-express": "^10.4.15",
2527
"@nestjs/typeorm": "^11.0.1",
28+
"bcrypt": "^6.0.0",
29+
"class-transformer": "^0.5.1",
30+
"class-validator": "^0.15.1",
31+
"cookie-parser": "^1.4.7",
2632
"ioredis": "^5.10.1",
33+
"passport": "^0.7.0",
34+
"passport-github2": "^0.1.12",
35+
"passport-google-oauth20": "^2.0.0",
36+
"passport-jwt": "^4.0.1",
37+
"passport-local": "^1.0.0",
2738
"pg": "^8.20.0",
2839
"reflect-metadata": "^0.2.2",
2940
"rxjs": "^7.8.1",
@@ -35,9 +46,15 @@
3546
"@nestjs/cli": "^10.4.9",
3647
"@nestjs/schematics": "^10.2.3",
3748
"@nestjs/testing": "^10.4.15",
49+
"@types/bcrypt": "^6.0.0",
50+
"@types/cookie-parser": "^1.4.10",
3851
"@types/express": "^5.0.0",
3952
"@types/jest": "^29.5.14",
4053
"@types/node": "^22.10.5",
54+
"@types/passport-github2": "^1.2.9",
55+
"@types/passport-google-oauth20": "^2.0.17",
56+
"@types/passport-jwt": "^4.0.1",
57+
"@types/passport-local": "^1.0.38",
4158
"husky": "^9.1.7",
4259
"jest": "^29.7.0",
4360
"prettier": "^3.4.2",

‎pnpm-lock.yaml‎

Lines changed: 415 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/app.module.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
33
import { HealthModule } from './health/health.module';
44
import { StorageModule } from './storage/storage.module';
5+
import { AuthModule } from './auth/auth.module';
56
import configuration from './config/configuration';
67

78
@Module({
@@ -11,6 +12,7 @@ import configuration from './config/configuration';
1112
load: [configuration],
1213
}),
1314
StorageModule,
15+
AuthModule,
1416
HealthModule,
1517
],
1618
})

‎src/auth/auth.controller.ts‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {
2+
Controller,
3+
Post,
4+
Get,
5+
Patch,
6+
Body,
7+
UseGuards,
8+
Req,
9+
Res,
10+
HttpCode,
11+
HttpStatus,
12+
} from '@nestjs/common';
13+
import { AuthGuard } from '@nestjs/passport';
14+
import { Request, Response } from 'express';
15+
import { AuthService } from './auth.service';
16+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
17+
import { CurrentDeveloper } from './decorators/current-developer.decorator';
18+
19+
@Controller('auth')
20+
export class AuthController {
21+
constructor(private readonly authService: AuthService) {}
22+
23+
@Post('register')
24+
async register(@Body() body: { email: string; password: string; name: string }) {
25+
return this.authService.register(body.email, body.password, body.name);
26+
}
27+
28+
@Post('login')
29+
@HttpCode(HttpStatus.OK)
30+
@UseGuards(AuthGuard('local'))
31+
async login(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
32+
const result = await this.authService.login(req.user as any);
33+
res.cookie('refresh_token', result.refreshToken, {
34+
httpOnly: true,
35+
secure: process.env.NODE_ENV === 'production',
36+
sameSite: 'lax',
37+
maxAge: 30 * 24 * 60 * 60 * 1000,
38+
path: '/auth/refresh',
39+
});
40+
return { accessToken: result.accessToken, developer: result.developer };
41+
}
42+
43+
@Post('refresh')
44+
@HttpCode(HttpStatus.OK)
45+
async refresh(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
46+
const token = req.cookies?.refresh_token || req.body?.refreshToken;
47+
if (!token) {
48+
return { error: 'No refresh token provided' };
49+
}
50+
const result = await this.authService.refresh(token);
51+
res.cookie('refresh_token', result.refreshToken, {
52+
httpOnly: true,
53+
secure: process.env.NODE_ENV === 'production',
54+
sameSite: 'lax',
55+
maxAge: 30 * 24 * 60 * 60 * 1000,
56+
path: '/auth/refresh',
57+
});
58+
return { accessToken: result.accessToken, developer: result.developer };
59+
}
60+
61+
@Post('logout')
62+
@HttpCode(HttpStatus.OK)
63+
async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
64+
const token = req.cookies?.refresh_token || req.body?.refreshToken;
65+
if (token) {
66+
await this.authService.logout(token);
67+
}
68+
res.clearCookie('refresh_token', { path: '/auth/refresh' });
69+
return { message: 'Logged out' };
70+
}
71+
72+
@Get('google')
73+
@UseGuards(AuthGuard('google'))
74+
googleAuth() {
75+
// Passport redirects to Google
76+
}
77+
78+
@Get('google/callback')
79+
@UseGuards(AuthGuard('google'))
80+
async googleCallback(@Req() req: Request, @Res() res: Response) {
81+
const result = await this.authService.login(req.user as any);
82+
const consoleUrl = process.env.CONSOLE_URL || 'http://localhost:3000';
83+
res.cookie('refresh_token', result.refreshToken, {
84+
httpOnly: true,
85+
secure: process.env.NODE_ENV === 'production',
86+
sameSite: 'lax',
87+
maxAge: 30 * 24 * 60 * 60 * 1000,
88+
path: '/auth/refresh',
89+
});
90+
res.redirect(`${consoleUrl}/auth/callback?token=${result.accessToken}`);
91+
}
92+
93+
@Get('github')
94+
@UseGuards(AuthGuard('github'))
95+
githubAuth() {
96+
// Passport redirects to GitHub
97+
}
98+
99+
@Get('github/callback')
100+
@UseGuards(AuthGuard('github'))
101+
async githubCallback(@Req() req: Request, @Res() res: Response) {
102+
const result = await this.authService.login(req.user as any);
103+
const consoleUrl = process.env.CONSOLE_URL || 'http://localhost:3000';
104+
res.cookie('refresh_token', result.refreshToken, {
105+
httpOnly: true,
106+
secure: process.env.NODE_ENV === 'production',
107+
sameSite: 'lax',
108+
maxAge: 30 * 24 * 60 * 60 * 1000,
109+
path: '/auth/refresh',
110+
});
111+
res.redirect(`${consoleUrl}/auth/callback?token=${result.accessToken}`);
112+
}
113+
114+
@Post('forgot-password')
115+
@HttpCode(HttpStatus.OK)
116+
async forgotPassword(@Body() body: { email: string }) {
117+
return this.authService.forgotPassword(body.email);
118+
}
119+
120+
@Post('reset-password')
121+
@HttpCode(HttpStatus.OK)
122+
async resetPassword(@Body() body: { token: string; password: string }) {
123+
return this.authService.resetPassword(body.token, body.password);
124+
}
125+
126+
@Get('me')
127+
@UseGuards(JwtAuthGuard)
128+
async me(@CurrentDeveloper() dev: { id: string }) {
129+
return this.authService.getProfile(dev.id);
130+
}
131+
132+
@Patch('me')
133+
@UseGuards(JwtAuthGuard)
134+
async updateMe(
135+
@CurrentDeveloper() dev: { id: string },
136+
@Body() body: { name?: string; avatarUrl?: string },
137+
) {
138+
return this.authService.updateProfile(dev.id, body);
139+
}
140+
}

‎src/auth/auth.module.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Module } from '@nestjs/common';
2+
import { JwtModule } from '@nestjs/jwt';
3+
import { PassportModule } from '@nestjs/passport';
4+
import { ConfigModule, ConfigService } from '@nestjs/config';
5+
import { TypeOrmModule } from '@nestjs/typeorm';
6+
import { AuthController } from './auth.controller';
7+
import { AuthService } from './auth.service';
8+
import { LocalStrategy } from './strategies/local.strategy';
9+
import { JwtStrategy } from './strategies/jwt.strategy';
10+
import { GoogleStrategy } from './strategies/google.strategy';
11+
import { GitHubStrategy } from './strategies/github.strategy';
12+
import { DeveloperEntity } from '../storage/entities/developer.entity';
13+
import { TeamEntity } from '../storage/entities/team.entity';
14+
import { TeamMemberEntity } from '../storage/entities/team-member.entity';
15+
import { RefreshTokenEntity } from '../storage/entities/refresh-token.entity';
16+
17+
@Module({
18+
imports: [
19+
PassportModule,
20+
JwtModule.registerAsync({
21+
imports: [ConfigModule],
22+
inject: [ConfigService],
23+
useFactory: (config: ConfigService) => ({
24+
secret: config.get<string>('JWT_SECRET'),
25+
signOptions: { expiresIn: '1h' },
26+
}),
27+
}),
28+
TypeOrmModule.forFeature([DeveloperEntity, TeamEntity, TeamMemberEntity, RefreshTokenEntity]),
29+
],
30+
controllers: [AuthController],
31+
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy, GitHubStrategy],
32+
exports: [AuthService],
33+
})
34+
export class AuthModule {}

0 commit comments

Comments
 (0)