Skip to content

Commit 9fd65c3

Browse files
Moved auth files.
1 parent 5b06978 commit 9fd65c3

16 files changed

+591
-0
lines changed

src/auth/auth.controller.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Body, Controller, Post, HttpCode, HttpStatus, BadRequestException } from '@nestjs/common';
2+
import { AuthService } from './auth.service';
3+
import { GoogleAuthInput } from './dto/google-auth.input';
4+
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
5+
import { ApiPaginationQuery } from '../common/decorators/api-nested-query.decorator';
6+
7+
@ApiTags('Auth')
8+
@Controller('auth')
9+
export class AuthController {
10+
constructor(private readonly authService: AuthService) {}
11+
12+
/**
13+
* Fetch new jobs from Reddit and Web3Career, store them, and send notifications
14+
*/
15+
@Post('google')
16+
@ApiOperation({
17+
summary: 'Fetch new jobs from Reddit and Web3Career',
18+
description: 'Fetches new jobs from both sources, stores them, and sends notifications.',
19+
})
20+
@ApiParam({ name: 'idToken', description: 'Google ID Token', type: String })
21+
@ApiResponse({ status: 200, description: 'Jobs fetched and notifications sent.' })
22+
@HttpCode(HttpStatus.OK)
23+
async googleAuth(@Body() googleAuthInput: GoogleAuthInput) {
24+
const { idToken, buildType } = googleAuthInput;
25+
26+
if (!idToken) {
27+
throw new BadRequestException('ID token is required');
28+
}
29+
30+
return this.authService.googleAuth(idToken, buildType);
31+
}
32+
}

src/auth/auth.module.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Module } from '@nestjs/common';
2+
import { JwtModule } from '@nestjs/jwt';
3+
import { PassportModule } from '@nestjs/passport';
4+
import { PasswordService } from './password.service';
5+
import { GqlAuthGuard } from './gql-auth.guard';
6+
import { AuthService } from './auth.service';
7+
import { AuthResolver } from './auth.resolver';
8+
import { AuthController } from './auth.controller';
9+
//import { JwtStrategy } from './jwt.strategy';
10+
import { SecurityConfig } from '../common/configs/config.interface';
11+
import { RedisModule } from '../common/redis/redis.module';
12+
import { ConfigModule } from '../common/configs/config.module';
13+
import { ConfigService } from '../common/configs/config.service';
14+
15+
@Module({
16+
imports: [
17+
PassportModule.register({ defaultStrategy: 'jwt' }),
18+
RedisModule.forRoot(),
19+
ConfigModule,
20+
JwtModule.registerAsync({
21+
useFactory: (configService: ConfigService) => {
22+
return {
23+
secret: process.env.JWT_ACCESS_SECRET || 'nestjsPrismaAccessSecret',
24+
signOptions: {
25+
expiresIn: process.env.JWT_EXPIRATION_TIME || '60m',
26+
},
27+
};
28+
},
29+
inject: [ConfigService],
30+
}),
31+
],
32+
providers: [AuthService, AuthResolver, GqlAuthGuard, PasswordService], //JwtStrategy
33+
controllers: [AuthController],
34+
exports: [GqlAuthGuard],
35+
})
36+
export class AuthModule {}

src/auth/auth.resolver.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Resolver, Mutation, Args, Parent, ResolveField } from '@nestjs/graphql';
2+
import { AuthService } from './auth.service';
3+
import { Auth } from './models/auth.model';
4+
import { Token } from './models/token.model';
5+
import { LoginInput } from './dto/login.input';
6+
import { SignupInput } from './dto/signup.input';
7+
import { RefreshTokenInput } from './dto/refresh-token.input';
8+
import { User } from '../users/models/user.model';
9+
10+
@Resolver(() => Auth)
11+
export class AuthResolver {
12+
constructor(private readonly authService: AuthService) {}
13+
14+
@Mutation(() => Auth)
15+
async signup(@Args('data') data: SignupInput) {
16+
data.email = data.email.toLowerCase();
17+
const { accessToken, refreshToken } = await this.authService.createUser(data);
18+
return {
19+
accessToken,
20+
refreshToken,
21+
};
22+
}
23+
24+
@Mutation(() => Auth)
25+
async login(@Args('data') { email, password }: LoginInput) {
26+
const { accessToken, refreshToken } = await this.authService.login(email.toLowerCase(), password);
27+
28+
return {
29+
accessToken,
30+
refreshToken,
31+
};
32+
}
33+
34+
@Mutation(() => Token)
35+
refreshToken(@Args() { token }: RefreshTokenInput) {
36+
return this.authService.refreshToken(token);
37+
}
38+
39+
@ResolveField('user', () => User)
40+
async user(@Parent() auth: Auth) {
41+
return await this.authService.getUserFromToken(auth.accessToken);
42+
}
43+
}

src/auth/auth.service.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { PrismaService } from 'nestjs-prisma';
2+
import { Prisma, User } from '@prisma/client';
3+
import {
4+
Injectable,
5+
NotFoundException,
6+
BadRequestException,
7+
ConflictException,
8+
UnauthorizedException,
9+
} from '@nestjs/common';
10+
import { JwtService } from '@nestjs/jwt';
11+
import { PasswordService } from './password.service';
12+
import { SignupInput } from './dto/signup.input';
13+
import { Token } from './models/token.model';
14+
import { OAuth2Client } from 'google-auth-library';
15+
16+
@Injectable()
17+
export class AuthService {
18+
private googleClient: OAuth2Client;
19+
20+
constructor(
21+
private readonly jwtService: JwtService,
22+
private readonly prisma: PrismaService,
23+
private readonly passwordService: PasswordService
24+
) {
25+
// Initialize Google OAuth2 client
26+
this.googleClient = new OAuth2Client();
27+
}
28+
29+
async createUser(payload: SignupInput): Promise<Token> {
30+
const hashedPassword = await this.passwordService.hashPassword(payload.password);
31+
32+
try {
33+
const user = await this.prisma.user.create({
34+
data: {
35+
...payload,
36+
role: 'USER',
37+
},
38+
});
39+
40+
return this.generateTokens({
41+
userId: user.id,
42+
});
43+
} catch (e) {
44+
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
45+
throw new ConflictException(`Email ${payload.email} already used.`);
46+
}
47+
throw new Error(e);
48+
}
49+
}
50+
51+
async login(wallet: string, password: string): Promise<Token> {
52+
console.log('login');
53+
const user = await this.prisma.user.findUnique({ where: { wallet } });
54+
55+
if (!user) {
56+
throw new NotFoundException(`No user found for email: ${wallet}`);
57+
}
58+
59+
const passwordValid = await this.passwordService.validatePassword(password, 'password login disabled');
60+
61+
if (!passwordValid) {
62+
throw new BadRequestException('Invalid password');
63+
}
64+
65+
return this.generateTokens({
66+
userId: user.id,
67+
});
68+
}
69+
70+
validateUser(userId: number): Promise<User> {
71+
return this.prisma.user.findUnique({ where: { id: userId } });
72+
}
73+
74+
getUserFromToken(token: string): Promise<User> {
75+
const id = this.jwtService.decode(token)['userId'];
76+
return this.prisma.user.findUnique({ where: { id } });
77+
}
78+
79+
generateTokens(payload: { userId: number }): Token {
80+
return {
81+
accessToken: this.generateAccessToken(payload),
82+
refreshToken: this.generateRefreshToken(payload),
83+
};
84+
}
85+
86+
private generateAccessToken(payload: { userId: number }): string {
87+
return this.jwtService.sign(payload);
88+
}
89+
90+
private generateRefreshToken(payload: { userId: number }): string {
91+
return this.jwtService.sign(payload, {
92+
secret: process.env.JWT_REFRESH_TOKEN_SECRET || 'nestjsPrismaRefreshSecret',
93+
expiresIn: process.env.JWT_REFRESH_EXPIRATION_TIME || '60m',
94+
});
95+
}
96+
97+
refreshToken(token: string) {
98+
try {
99+
const { userId } = this.jwtService.verify(token, {
100+
secret: process.env.JWT_REFRESH_TOKEN_SECRET || 'nestjsPrismaRefreshSecret',
101+
});
102+
103+
return this.generateTokens({
104+
userId,
105+
});
106+
} catch (e) {
107+
throw new UnauthorizedException();
108+
}
109+
}
110+
111+
async googleAuth(idToken: string, buildType?: string): Promise<Token> {
112+
try {
113+
// Determine which client ID to use based on build type
114+
const clientId =
115+
buildType === 'development' ? process.env.GOOGLE_WEB_CLIENT_ID_DEV : process.env.GOOGLE_WEB_CLIENT_ID;
116+
117+
if (!clientId) {
118+
console.error(`Missing Google OAuth client ID for build type: ${buildType}`);
119+
throw new UnauthorizedException('OAuth configuration error');
120+
}
121+
122+
console.log(`🔍 Verifying Google token for ${buildType || 'production'} build`);
123+
console.log(`🔑 Using client ID: ${clientId.substring(0, 20)}...`);
124+
125+
// Verify the Google ID token with the appropriate client ID
126+
const ticket = await this.googleClient.verifyIdToken({
127+
idToken,
128+
audience: clientId,
129+
});
130+
131+
const payload = ticket.getPayload();
132+
if (!payload) {
133+
throw new UnauthorizedException('Invalid Google token');
134+
}
135+
136+
const { email, name, picture, given_name, family_name, sub: googleId } = payload;
137+
138+
if (!email) {
139+
throw new UnauthorizedException('Email not provided by Google');
140+
}
141+
142+
console.log(`✅ Google token verified successfully for ${email}`);
143+
144+
// Find or create user based on email or googleId
145+
let user = await this.prisma.user.findFirst({
146+
where: {
147+
OR: [{ email }, { googleId }],
148+
},
149+
});
150+
151+
if (!user) {
152+
// Create new user with Google info
153+
user = await this.prisma.user.create({
154+
data: {
155+
email,
156+
username: name || '', // Google display name (full name)
157+
firstname: given_name || '',
158+
lastname: family_name || '',
159+
profilePicture: picture || null,
160+
googleId,
161+
role: 'USER',
162+
password: '', // No password for Google auth
163+
is_active: true,
164+
},
165+
});
166+
console.log(`👤 Created new user: ${email}`);
167+
} else {
168+
// Update existing user with latest Google info
169+
user = await this.prisma.user.update({
170+
where: { id: user.id },
171+
data: {
172+
username: name || user.username,
173+
profilePicture: picture || user.profilePicture,
174+
googleId: googleId || user.googleId,
175+
// Update name fields if they weren't set before
176+
firstname: user.firstname || given_name || '',
177+
lastname: user.lastname || family_name || '',
178+
},
179+
});
180+
console.log(`👤 Updated existing user: ${email}`);
181+
}
182+
183+
// Generate and return tokens
184+
return this.generateTokens({
185+
userId: user.id,
186+
});
187+
} catch (error) {
188+
console.error('Google auth error:', error);
189+
190+
// More specific error handling
191+
if (error.message?.includes('audience')) {
192+
throw new UnauthorizedException('OAuth client ID mismatch - check your build configuration');
193+
}
194+
195+
throw new UnauthorizedException('Google authentication failed');
196+
}
197+
}
198+
}

src/auth/dto/google-auth.input.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
2+
import { InputType, Field } from '@nestjs/graphql';
3+
4+
@InputType()
5+
export class GoogleAuthInput {
6+
@Field()
7+
@IsString()
8+
@IsNotEmpty()
9+
idToken: string;
10+
11+
@Field({ nullable: true })
12+
@IsString()
13+
@IsOptional()
14+
@IsIn(['development', 'production'])
15+
buildType?: string;
16+
}

src/auth/dto/jwt.dto.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface JwtDto {
2+
userId: number;
3+
/**
4+
* Issued at
5+
*/
6+
iat: number;
7+
/**
8+
* Expiration time
9+
*/
10+
exp: number;
11+
}

src/auth/dto/login.input.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
2+
import { InputType, Field } from '@nestjs/graphql';
3+
4+
@InputType()
5+
export class LoginInput {
6+
@Field()
7+
@IsEmail()
8+
email: string;
9+
10+
@Field()
11+
@IsNotEmpty()
12+
@MinLength(8)
13+
password: string;
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ArgsType, Field } from '@nestjs/graphql';
2+
import { IsJWT, IsNotEmpty } from 'class-validator';
3+
import { GraphQLJWT } from 'graphql-scalars';
4+
5+
@ArgsType()
6+
export class RefreshTokenInput {
7+
@IsNotEmpty()
8+
@IsJWT()
9+
@Field(() => GraphQLJWT)
10+
token: string;
11+
}

src/auth/dto/signup.input.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
2+
import { InputType, Field } from '@nestjs/graphql';
3+
4+
@InputType()
5+
export class SignupInput {
6+
@Field()
7+
@IsEmail()
8+
email: string;
9+
10+
@Field()
11+
@IsNotEmpty()
12+
@MinLength(8)
13+
password: string;
14+
15+
@Field({ nullable: true })
16+
firstname?: string;
17+
18+
@Field({ nullable: true })
19+
lastname?: string;
20+
}

0 commit comments

Comments
 (0)