Skip to content

Commit 75ad5b1

Browse files
committed
feat: added upload profile picture feature
1 parent 3f4b37d commit 75ad5b1

14 files changed

Lines changed: 1980 additions & 102 deletions

File tree

backend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
"lint": "eslint"
99
},
1010
"dependencies": {
11+
"@aws-sdk/client-s3": "^3.962.0",
1112
"dotenv": "^17.2.3",
1213
"express": "^5.2.1",
1314
"form-data": "^4.0.5",
1415
"mailgun.js": "^12.4.1",
16+
"multer": "^2.0.2",
1517
"pg": "^8.16.3",
1618
"pino": "^10.1.0",
1719
"pino-http": "^11.0.0",
@@ -23,6 +25,7 @@
2325
"@commitlint/config-conventional": "^20.2.0",
2426
"@eslint/js": "^9.39.2",
2527
"@types/express": "^5.0.6",
28+
"@types/multer": "^2.0.0",
2629
"@types/node": "^25.0.3",
2730
"@types/pg": "^8.16.0",
2831
"@typescript-eslint/eslint-plugin": "^8.51.0",

backend/src/app.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { logger } from './utils/logger';
44
import { notFound } from './middleware/notFound';
55
import { errorHandler } from './middleware/errorHandler';
66
import Healthrouter from './modules/health/health';
7-
import authRouter from './modules/auth/auth.routes'
8-
7+
import authRouter from './modules/auth/auth.routes';
8+
import userRouter from './modules/user/user.routes';
99

1010
const app = express();
1111
app.use(express.json());
@@ -14,6 +14,7 @@ app.use(
1414
);
1515
app.use('/health', Healthrouter);
1616
app.use('/auth', authRouter);
17+
app.use('/user', userRouter);
1718
app.use(notFound);
1819
app.use(errorHandler);
1920
export default app;

backend/src/data-source.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ export const appDataSouce = new DataSource({
1111
ssl: {
1212
rejectUnauthorized: false,
1313
},
14-
entities: [User,Otp],
14+
entities: [User, Otp],
1515
synchronize: true,
1616
});

backend/src/entities/User.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
1-
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
2-
@Entity()
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
} from 'typeorm';
7+
8+
@Entity('users')
39
export class User {
4-
@PrimaryGeneratedColumn()
5-
id!: number;
10+
@PrimaryGeneratedColumn('uuid')
11+
id!: string;
12+
13+
@Column({ unique: true })
14+
email!: string;
15+
616
@Column()
7-
phone!: string;
17+
name!: string;
18+
19+
@Column({ nullable: true })
20+
age?: number;
21+
22+
@Column({ nullable: true })
23+
gender?: string;
24+
25+
@Column('text', { array: true, nullable: true })
26+
interests?: string[];
27+
28+
@Column({ nullable: true })
29+
profileImageUrl?: string;
30+
31+
@CreateDateColumn()
32+
createdAt!: Date;
833
}

backend/src/entities/opt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class Otp {
1515
otp!: string;
1616
@Column({ type: 'timestamp' })
1717
expiresAt!: Date;
18-
@Column({default:false})
18+
@Column({ default: false })
1919
verified!: boolean;
2020
@CreateDateColumn()
2121
createdAt!: Date;

backend/src/middleware/upload.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import multer from 'multer';
2+
3+
export const upload = multer({
4+
storage: multer.memoryStorage(),
5+
limits: {
6+
fileSize: 5 * 1024 * 1024,
7+
},
8+
});
Lines changed: 117 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Request, Response, NextFunction } from 'express';
22
import { logger } from '../../utils/logger';
3-
import { emailSchema } from './auth.schema';
3+
import { emailSchema, registerSchema } from './auth.schema';
44
import { sendOtpEmail } from '../../Services/email.service';
55
import { generateotp } from '../../utils/otp';
66
import { appDataSouce } from '../../data-source';
77
import { Otp } from '../../entities/opt';
8+
import { User } from '../../entities/User';
9+
810
export const sendOtp = async (
911
req: Request,
1012
res: Response,
@@ -16,22 +18,123 @@ export const sendOtp = async (
1618
if (!result.success) {
1719
return res
1820
.status(400)
19-
.json({ message: 'validation vailed', error: result.error?.format() });}
20-
const otpRep = appDataSouce.getRepository(Otp)
21-
const otpcode = generateotp()
22-
logger.debug({otpcode},"otp is")
23-
const email = result.data?.email;
24-
await sendOtpEmail(email.toString(),otpcode.toString())
25-
await otpRep.delete({email})
26-
await otpRep.save({
21+
.json({ message: 'validation vailed', error: result.error?.format() });
22+
}
23+
const otpRep = appDataSouce.getRepository(Otp);
24+
const otpcode = generateotp();
25+
logger.debug({ otpcode }, 'otp is');
26+
const email = result.data?.email;
27+
await sendOtpEmail(email.toString(), otpcode.toString());
28+
await otpRep.delete({ email });
29+
await otpRep.save({
30+
email,
31+
otp: otpcode.toString(),
32+
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
33+
});
34+
res.status(200).json({ message: 'otp sent success fully ', success: true });
35+
} catch (err) {
36+
logger.error('error in register');
37+
next(err);
38+
}
39+
};
40+
41+
export const verifyotp = async (
42+
req: Request,
43+
res: Response,
44+
next: NextFunction,
45+
) => {
46+
try {
47+
const { email, otp } = req.body;
48+
if (!otp || !email) {
49+
return res.status(400).json({ message: ' otp and email are required' });
50+
}
51+
52+
const otpRepo = appDataSouce.getRepository(Otp);
53+
const otpRecord = await otpRepo.findOne({
54+
where: {
2755
email,
28-
otp:otpcode.toString(),
29-
expiresAt: new Date(Date.now()+5*60*1000)
56+
verified: false,
57+
},
58+
order: {
59+
createdAt: 'DESC',
60+
},
61+
});
62+
if (!otpRecord) {
63+
return res.status(400).json({
64+
messge: 'Ivalid or expired OTP',
65+
});
66+
}
3067

31-
})
32-
res.status(200).json({ message: "otp sent success fully ",success:true });
68+
if (otpRecord.expiresAt < new Date()) {
69+
return res.status(400).json({ message: 'OTP has expired' });
70+
}
71+
if (otpRecord.otp != otp.toString()) {
72+
return res.status(400).json({ message: 'Invalid OTP' });
73+
}
74+
otpRecord.verified = true;
75+
await otpRepo.save(otpRecord);
76+
res.status(200).json({
77+
message: 'OTP verified successfully',
78+
success: true,
79+
otpId: otpRecord.id,
80+
});
3381
} catch (err) {
34-
logger.error('error in register');
82+
logger.error({ err }, 'error in verify-otp');
83+
next(err);
84+
}
85+
};
86+
87+
export const register = async (
88+
req: Request,
89+
res: Response,
90+
next: NextFunction,
91+
) => {
92+
try {
93+
const result = registerSchema.safeParse(req.body);
94+
if (!result.success) {
95+
return res.status(400).json({
96+
message: 'invalid registation data',
97+
errors: result.error.format(),
98+
});
99+
}
100+
const { otpId, name, age, gender, interests } = result.data;
101+
if (!otpId) {
102+
return res.status(400).json({ message: 'otpid requuired' });
103+
}
104+
const otpRepo = appDataSouce.getRepository(Otp);
105+
const userRepo = appDataSouce.getRepository(User);
106+
107+
const otpRecord = await otpRepo.findOne({
108+
where: { id: otpId },
109+
});
110+
if (!otpRecord) {
111+
return res.status(400).json({ message: 'invalid registration ' });
112+
}
113+
if (!otpRecord.verified) {
114+
return res.status(400).json({
115+
message: 'OTP not verified',
116+
});
117+
}
118+
119+
const email = otpRecord.email;
120+
121+
const existingUser = await userRepo.findOne({
122+
where: { email },
123+
});
124+
125+
if (existingUser) {
126+
return res.status(409).json({ message: 'User already exists' });
127+
}
128+
129+
const user = userRepo.create({ email, name, age, gender, interests });
130+
131+
await userRepo.save(user);
132+
await otpRepo.delete({ id: otpId });
133+
return res
134+
.status(201)
135+
.json({ message: 'User registered successfully', success: true });
136+
} catch (err) {
137+
logger.error({ err }, 'error in register');
35138
next(err);
36139
}
37140
};
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import express from 'express';
2-
import { sendOtp } from './auth.controller';
2+
import { sendOtp, verifyotp, register } from './auth.controller';
33
const authRouter = express.Router();
44
authRouter.post('/send-otp', sendOtp);
5+
authRouter.post('/verify-otp', verifyotp);
6+
authRouter.post('/register', register);
57

68
export default authRouter;

backend/src/modules/auth/auth.schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,11 @@ import { z } from 'zod';
22
export const emailSchema = z.object({
33
email: z.string().email('Invalid email'),
44
});
5+
6+
export const registerSchema = z.object({
7+
otpId: z.string().uuid(),
8+
name: z.string().min(1, 'name is required'),
9+
age: z.number().int().positive().optional(),
10+
gender: z.enum(['male', 'female', 'other']).optional(),
11+
interests: z.array(z.string()).optional(),
12+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { NextFunction, Request, Response } from 'express';
2+
import { PutObjectCommand } from '@aws-sdk/client-s3';
3+
import { r2 } from '../../utils/r2';
4+
import { appDataSouce } from '../../data-source';
5+
import { User } from '../../entities/User';
6+
import { logger } from '../../utils/logger';
7+
export const uploadAvatar = async (
8+
req: Request,
9+
res: Response,
10+
next: NextFunction,
11+
) => {
12+
try {
13+
if (!req.file) {
14+
return res.status(400).json({ message: 'Avatar is required' });
15+
}
16+
const userId = req.body.userId; // this is temp
17+
const file = req.file;
18+
const key = `avatars/${userId}-${Date.now()}`;
19+
await r2.send(
20+
new PutObjectCommand({
21+
Bucket: process.env.R2_BUCKET_NAME,
22+
Key: key,
23+
Body: file.buffer,
24+
ContentType: file.mimetype,
25+
}),
26+
);
27+
const imageUrl = `${process.env.R2_ENDPOINT}/${process.env.R2_BUCKET_NAME}/${key}`;
28+
const userRepo = appDataSouce.getRepository(User);
29+
await userRepo.update(userId, { profileImageUrl: imageUrl });
30+
res
31+
.status(200)
32+
.json({ message: 'profile picture uploaded', url: imageUrl });
33+
} catch (err) {
34+
logger.error({ err }, 'error in upload image');
35+
next(err);
36+
}
37+
};

0 commit comments

Comments
 (0)