Skip to content

Commit 1cf06db

Browse files
authored
Merge pull request #32 from devxtra-community/shanu
Shanu
2 parents cd069f4 + 75ad5b1 commit 1cf06db

19 files changed

Lines changed: 2188 additions & 49 deletions

File tree

backend/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +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",
14+
"form-data": "^4.0.5",
15+
"mailgun.js": "^12.4.1",
16+
"multer": "^2.0.2",
1317
"pg": "^8.16.3",
1418
"pino": "^10.1.0",
1519
"pino-http": "^11.0.0",
@@ -21,11 +25,13 @@
2125
"@commitlint/config-conventional": "^20.2.0",
2226
"@eslint/js": "^9.39.2",
2327
"@types/express": "^5.0.6",
28+
"@types/multer": "^2.0.0",
2429
"@types/node": "^25.0.3",
2530
"@types/pg": "^8.16.0",
2631
"@typescript-eslint/eslint-plugin": "^8.51.0",
2732
"@typescript-eslint/parser": "^8.51.0",
2833
"eslint": "^9.39.2",
34+
"ts-node": "^10.9.2",
2935
"ts-node-dev": "^2.0.0",
3036
"typescript": "~5.9.3",
3137
"typescript-eslint": "^8.51.0"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Mailgun from 'mailgun.js';
2+
import FormData from 'form-data';
3+
import { logger } from '../utils/logger';
4+
5+
const mailgun = new Mailgun(FormData);
6+
7+
const mg = mailgun.client({
8+
username: 'api',
9+
key: process.env.MAILGUN_API_KEY!,
10+
});
11+
12+
export const sendOtpEmail = async (to: string, otp: string) => {
13+
try {
14+
await mg.messages.create(process.env.MAILGUN_DOMAIN!, {
15+
from: process.env.MAIL_FROM_EMAIL!,
16+
to,
17+
subject: 'Your SocialCode verification code',
18+
text: `Your SocialCode verification code is ${otp}. This code expires in 5 minutes.`,
19+
});
20+
} catch (err) {
21+
logger.error({ err }, 'Error sending OTP email');
22+
throw new Error('Failed to send OTP email');
23+
}
24+
};

backend/src/app.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ import { pinoHttp } from 'pino-http';
33
import { logger } from './utils/logger';
44
import { notFound } from './middleware/notFound';
55
import { errorHandler } from './middleware/errorHandler';
6-
import Healthrouter from './routes/health';
6+
import Healthrouter from './modules/health/health';
7+
import authRouter from './modules/auth/auth.routes';
8+
import userRouter from './modules/user/user.routes';
9+
710
const app = express();
811
app.use(express.json());
912
app.use(
1013
pinoHttp({ logger, autoLogging: { ignore: (req) => req.url === 'health' } }),
1114
);
1215
app.use('/health', Healthrouter);
16+
app.use('/auth', authRouter);
17+
app.use('/user', userRouter);
1318
app.use(notFound);
1419
app.use(errorHandler);
1520
export default app;

backend/src/data-source.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dotenv/config';
22
import { DataSource } from 'typeorm';
33
import { User } from './entities/User';
4+
import { Otp } from './entities/opt';
45
if (!process.env.DATABASE_URL) {
56
throw new Error('DATABASE_URL is not defined');
67
}
@@ -10,6 +11,6 @@ export const appDataSouce = new DataSource({
1011
ssl: {
1112
rejectUnauthorized: false,
1213
},
13-
entities: [User],
14+
entities: [User, Otp],
1415
synchronize: true,
1516
});

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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
} from 'typeorm';
7+
8+
@Entity('otps')
9+
export class Otp {
10+
@PrimaryGeneratedColumn('uuid')
11+
id!: string;
12+
@Column()
13+
email!: string;
14+
@Column()
15+
otp!: string;
16+
@Column({ type: 'timestamp' })
17+
expiresAt!: Date;
18+
@Column({ default: false })
19+
verified!: boolean;
20+
@CreateDateColumn()
21+
createdAt!: Date;
22+
}
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
import { Request, Response, NextFunction } from 'express';
2+
import { logger } from '../utils/logger';
3+
4+
type AppError = {
5+
statusCode?: number;
6+
message?: string;
7+
};
28

39
export const errorHandler = (
410
err: unknown,
511
req: Request,
612
res: Response,
713
_next: NextFunction,
814
) => {
9-
console.error(err);
15+
logger.info('reached inside global error');
16+
const error = err as AppError;
1017

11-
let message = 'Internal Server Error';
18+
const statusCode = error.statusCode ?? 500;
19+
const message =
20+
statusCode === 500
21+
? 'Internal Server Error'
22+
: (error.message ?? 'Something went wrong');
1223

13-
if (err instanceof Error) {
14-
message = err.message;
15-
}
24+
logger.error(
25+
{
26+
err,
27+
path: req.path,
28+
method: req.method,
29+
},
30+
'Unhandled error',
31+
);
1632

17-
res.status(500).json({ message });
33+
res.status(statusCode).json({
34+
success: false,
35+
message,
36+
});
1837
};

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: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { logger } from '../../utils/logger';
3+
import { emailSchema, registerSchema } from './auth.schema';
4+
import { sendOtpEmail } from '../../Services/email.service';
5+
import { generateotp } from '../../utils/otp';
6+
import { appDataSouce } from '../../data-source';
7+
import { Otp } from '../../entities/opt';
8+
import { User } from '../../entities/User';
9+
10+
export const sendOtp = async (
11+
req: Request,
12+
res: Response,
13+
next: NextFunction,
14+
) => {
15+
try {
16+
logger.info('reached');
17+
const result = emailSchema.safeParse(req.body);
18+
if (!result.success) {
19+
return res
20+
.status(400)
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: {
55+
email,
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+
}
67+
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+
});
81+
} catch (err) {
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');
138+
next(err);
139+
}
140+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import express from 'express';
2+
import { sendOtp, verifyotp, register } from './auth.controller';
3+
const authRouter = express.Router();
4+
authRouter.post('/send-otp', sendOtp);
5+
authRouter.post('/verify-otp', verifyotp);
6+
authRouter.post('/register', register);
7+
8+
export default authRouter;

0 commit comments

Comments
 (0)