Skip to content

Commit ed5e592

Browse files
committed
Merge branch 'main' into chore-readme-ci-update
2 parents 859a6f8 + 34a86c6 commit ed5e592

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4946
-881
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ models/
1616

1717
*/**/database.sqlite
1818
./backend/src/database.sqlite
19-
.codefox
19+
.codefox
20+
21+
.env

backend/.env

Lines changed: 0 additions & 5 deletions
This file was deleted.

backend/.env.development

Lines changed: 0 additions & 5 deletions
This file was deleted.

backend/.env.example

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Server Configuration
2+
PORT=8080
3+
4+
# DEV PROD OR TEST
5+
NODE_ENV="DEV"
6+
# JWT Configuration
7+
JWT_SECRET="your_jwt_secret_here"
8+
JWT_REFRESH="your_jwt_refresh_secret_here"
9+
SALT_ROUNDS=10
10+
11+
# OpenAI Configuration
12+
OPENAI_BASE_URI="http://localhost:3001"
13+
14+
# S3/Cloudflare R2 Configuration (Optional)
15+
# If not provided, local file storage will be used
16+
S3_ACCESS_KEY_ID="your_s3_access_key_id" # Must be 32 characters for Cloudflare R2
17+
S3_SECRET_ACCESS_KEY="your_s3_secret_access_key"
18+
S3_REGION="auto" # Use 'auto' for Cloudflare R2
19+
S3_BUCKET_NAME="your_bucket_name"
20+
S3_ENDPOINT="https://<account_id>.r2.cloudflarestorage.com" # Cloudflare R2 endpoint
21+
S3_ACCOUNT_ID="your_cloudflare_account_id" # Your Cloudflare account ID
22+
S3_PUBLIC_URL="https://pub-xxx.r2.dev" # Your R2 public bucket URL
23+
24+
# mail
25+
# Set to false to disable all email functionality
26+
MAIL_ENABLED=false
27+
28+
MAIL_HOST=smtp.example.com
29+
MAIL_USER=user@example.com
30+
MAIL_PASSWORD=topsecret
31+
MAIL_FROM=noreply@example.com
32+
MAIL_PORT=587
33+
MAIL_DOMAIN=your_net
34+

backend/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ log-*/
5555

5656

5757
# Backend
58-
/backend/package-lock.json
58+
/backend/package-lock.json
59+
.env

backend/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
},
2929
"dependencies": {
3030
"@apollo/server": "^4.11.0",
31+
"@aws-sdk/client-s3": "^3.758.0",
3132
"@huggingface/hub": "latest",
3233
"@huggingface/transformers": "latest",
34+
"@nestjs-modules/mailer": "^2.0.2",
3335
"@nestjs/apollo": "^12.2.0",
3436
"@nestjs/axios": "^3.0.3",
3537
"@nestjs/common": "^10.0.0",
@@ -45,6 +47,7 @@
4547
"@types/toposort": "^2.0.7",
4648
"axios": "^1.7.7",
4749
"bcrypt": "^5.1.1",
50+
"class-transformer": "^0.5.1",
4851
"class-validator": "^0.14.1",
4952
"dotenv": "^16.4.7",
5053
"eslint-plugin-unused-imports": "^4.1.4",
@@ -53,9 +56,11 @@
5356
"gpt-3-encoder": "^1.1.4",
5457
"graphql": "^16.9.0",
5558
"graphql-subscriptions": "^2.0.0",
59+
"graphql-upload-minimal": "^1.6.1",
5660
"graphql-ws": "^5.16.0",
5761
"lodash": "^4.17.21",
5862
"markdown-to-txt": "^2.0.1",
63+
"nodemailer": "^6.10.0",
5964
"normalize-path": "^3.0.0",
6065
"openai": "^4.77.0",
6166
"p-queue-es5": "^6.0.2",

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { AppResolver } from './app.resolver';
1515
import { APP_INTERCEPTOR } from '@nestjs/core';
1616
import { LoggingInterceptor } from 'src/interceptor/LoggingInterceptor';
1717
import { PromptToolModule } from './prompt-tool/prompt-tool.module';
18+
import { MailModule } from './mail/mail.module';
1819

1920
// TODO(Sma1lboy): move to a separate file
2021
function isProduction(): boolean {
@@ -47,6 +48,7 @@ function isProduction(): boolean {
4748
TokenModule,
4849
ChatModule,
4950
PromptToolModule,
51+
MailModule,
5052
TypeOrmModule.forFeature([User]),
5153
],
5254
providers: [

backend/src/auth/auth.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { User } from 'src/user/user.model';
99
import { AuthResolver } from './auth.resolver';
1010
import { RefreshToken } from './refresh-token/refresh-token.model';
1111
import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module';
12+
import { MailModule } from 'src/mail/mail.module';
1213

1314
@Module({
1415
imports: [
@@ -23,6 +24,7 @@ import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module';
2324
inject: [ConfigService],
2425
}),
2526
JwtCacheModule,
27+
MailModule,
2628
],
2729
providers: [AuthService, AuthResolver],
2830
exports: [AuthService, JwtModule],

backend/src/auth/auth.resolver.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ export class RefreshTokenResponse {
1818
refreshToken: string;
1919
}
2020

21+
@ObjectType()
22+
export class EmailConfirmationResponse {
23+
@Field()
24+
message: string;
25+
26+
@Field({ nullable: true })
27+
success?: boolean;
28+
}
29+
2130
@Resolver()
2231
export class AuthResolver {
2332
constructor(private readonly authService: AuthService) {}
@@ -33,4 +42,11 @@ export class AuthResolver {
3342
): Promise<RefreshTokenResponse> {
3443
return this.authService.refreshToken(refreshToken);
3544
}
45+
46+
@Mutation(() => EmailConfirmationResponse)
47+
async confirmEmail(
48+
@Args('token') token: string,
49+
): Promise<EmailConfirmationResponse> {
50+
return this.authService.confirmEmail(token);
51+
}
3652
}

backend/src/auth/auth.service.ts

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,127 @@ import { Role } from './role/role.model';
1919
import { RefreshToken } from './refresh-token/refresh-token.model';
2020
import { randomUUID } from 'crypto';
2121
import { compare, hash } from 'bcrypt';
22-
import { RefreshTokenResponse } from './auth.resolver';
22+
import {
23+
EmailConfirmationResponse,
24+
RefreshTokenResponse,
25+
} from './auth.resolver';
26+
import { MailService } from 'src/mail/mail.service';
2327

2428
@Injectable()
2529
export class AuthService {
30+
private readonly isMailEnabled: boolean;
31+
2632
constructor(
2733
@InjectRepository(User)
2834
private userRepository: Repository<User>,
2935
private jwtService: JwtService,
3036
private jwtCacheService: JwtCacheService,
3137
private configService: ConfigService,
38+
private mailService: MailService,
3239
@InjectRepository(Menu)
3340
private menuRepository: Repository<Menu>,
3441
@InjectRepository(Role)
3542
private roleRepository: Repository<Role>,
3643
@InjectRepository(RefreshToken)
3744
private refreshTokenRepository: Repository<RefreshToken>,
38-
) {}
45+
) {
46+
// Read the MAIL_ENABLED environment variable, default to 'true'
47+
this.isMailEnabled =
48+
this.configService.get<string>('MAIL_ENABLED', 'true').toLowerCase() ===
49+
'true';
50+
}
51+
52+
async confirmEmail(token: string): Promise<EmailConfirmationResponse> {
53+
try {
54+
const payload = await this.jwtService.verifyAsync(token);
55+
56+
// Check if payload has the required email field
57+
if (!payload || !payload.email) {
58+
return {
59+
message: 'Invalid token format',
60+
success: false,
61+
};
62+
}
63+
64+
// Find user and update
65+
const user = await this.userRepository.findOne({
66+
where: { email: payload.email },
67+
});
68+
69+
if (user && !user.isEmailConfirmed) {
70+
user.isEmailConfirmed = true;
71+
await this.userRepository.save(user);
72+
73+
return {
74+
message: 'Email confirmed successfully!',
75+
success: true,
76+
};
77+
}
78+
79+
return {
80+
message: 'Email already confirmed or user not found.',
81+
success: false,
82+
};
83+
} catch (error) {
84+
return {
85+
message: 'Invalid or expired token',
86+
success: false,
87+
};
88+
}
89+
}
90+
91+
async sendVerificationEmail(user: User): Promise<EmailConfirmationResponse> {
92+
// Generate confirmation token
93+
const verifyToken = this.jwtService.sign(
94+
{ email: user.email },
95+
{ expiresIn: '30m' },
96+
);
97+
98+
// Send confirmation email
99+
await this.mailService.sendConfirmationEmail(user.email, verifyToken);
100+
101+
// update user last time send email time
102+
user.lastEmailSendTime = new Date();
103+
await this.userRepository.save(user);
104+
105+
return {
106+
message: 'Verification email sent successfully!',
107+
success: true,
108+
};
109+
}
110+
111+
async resendVerificationEmail(email: string) {
112+
const user = await this.userRepository.findOne({
113+
where: { email },
114+
});
115+
116+
if (!user) {
117+
throw new Error('User not found');
118+
}
119+
120+
if (user.isEmailConfirmed) {
121+
return { message: 'Email already confirmed!' };
122+
}
123+
124+
// Check if a cooldown period has passed (e.g., 1 minute)
125+
const cooldownPeriod = 1 * 60 * 1000; // 1 minute in milliseconds
126+
if (
127+
user.lastEmailSendTime &&
128+
new Date().getTime() - user.lastEmailSendTime.getTime() < cooldownPeriod
129+
) {
130+
const timeLeft = Math.ceil(
131+
(cooldownPeriod -
132+
(new Date().getTime() - user.lastEmailSendTime.getTime())) /
133+
1000,
134+
);
135+
return {
136+
message: `Please wait ${timeLeft} seconds before requesting another email`,
137+
success: false,
138+
};
139+
}
140+
141+
return this.sendVerificationEmail(user);
142+
}
39143

40144
async register(registerUserInput: RegisterUserInput): Promise<User> {
41145
const { username, email, password } = registerUserInput;
@@ -50,13 +154,31 @@ export class AuthService {
50154
}
51155

52156
const hashedPassword = await hash(password, 10);
53-
const newUser = this.userRepository.create({
54-
username,
55-
email,
56-
password: hashedPassword,
57-
});
58157

59-
return this.userRepository.save(newUser);
158+
let newUser;
159+
if (this.isMailEnabled) {
160+
newUser = this.userRepository.create({
161+
username,
162+
email,
163+
password: hashedPassword,
164+
isEmailConfirmed: false,
165+
});
166+
} else {
167+
newUser = this.userRepository.create({
168+
username,
169+
email,
170+
password: hashedPassword,
171+
isEmailConfirmed: true,
172+
});
173+
}
174+
175+
await this.userRepository.save(newUser);
176+
177+
if (this.isMailEnabled) {
178+
await this.sendVerificationEmail(newUser);
179+
}
180+
181+
return newUser;
60182
}
61183

62184
async login(loginUserInput: LoginUserInput): Promise<RefreshTokenResponse> {
@@ -70,6 +192,10 @@ export class AuthService {
70192
throw new UnauthorizedException('Invalid credentials');
71193
}
72194

195+
if (!user.isEmailConfirmed) {
196+
throw new Error('Email not confirmed. Please check your inbox.');
197+
}
198+
73199
const isPasswordValid = await compare(password, user.password);
74200

75201
if (!isPasswordValid) {
@@ -113,6 +239,7 @@ export class AuthService {
113239
return false;
114240
}
115241
}
242+
116243
async logout(token: string): Promise<boolean> {
117244
try {
118245
await this.jwtService.verifyAsync(token);

0 commit comments

Comments
 (0)