Skip to content

Commit 14bfcde

Browse files
authored
Implementing 2 Factor Authentication (#407)
1 parent 5aea393 commit 14bfcde

File tree

6 files changed

+261
-212
lines changed

6 files changed

+261
-212
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import invitationSchema from './schema/invitation.schema'
5353
import TableViewInvitationResolver from './resolvers/TableViewInvitationResolver'
5454
import eventSchema from './schema/event.schema'
5555
import './utils/cron-jobs/team-jobs'
56+
import faMutation from './resolvers/2fa.resolvers';
5657

5758
const PORT: number = parseInt(process.env.PORT!) || 4000
5859

@@ -93,6 +94,7 @@ export const resolvers = mergeResolvers([
9394

9495
invitationResolvers,
9596
TableViewInvitationResolver,
97+
faMutation,
9698
])
9799

98100
async function startApolloServer(

src/models/user.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,24 @@ const userSchema = new Schema(
9595
type: Boolean,
9696
default: true,
9797
},
98+
twoFactorAuth: {
99+
type: Boolean,
100+
default: false,
101+
},
102+
twoFactorSecret: {
103+
type: String,
104+
105+
},
106+
oneTimeCode: {
107+
type: String,
108+
code: String,
109+
required: false
110+
},
111+
oneTimeCodeExpiresAt: {
112+
type: Date,
113+
required: false
114+
},
115+
98116
},
99117

100118
{

src/resolvers/2fa.resolvers.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { AuthenticationError } from 'apollo-server-errors'
2+
import mongoose from 'mongoose'
3+
4+
import jwt from 'jsonwebtoken';
5+
import { generateTokenUserExists } from '../helpers/user.helpers';
6+
import { sendEmail } from '../utils/sendEmail';
7+
import { verifyOtpToken } from '../utils/2WayAuthentication';
8+
import { GraphQLError } from 'graphql';
9+
import { User } from '../models/user';
10+
11+
interface Enable2FAInput {
12+
email: string
13+
}
14+
15+
interface Disable2FAInput {
16+
email: string
17+
}
18+
19+
const SECRET: string = process.env.SECRET ?? 'test_secret'
20+
const resolvers = {
21+
Mutation: {
22+
enableTwoFactorAuth: async (_: any, { email }: Enable2FAInput) => {
23+
try {
24+
const UserModel = mongoose.model('User')
25+
const user = await UserModel.findOne({ email })
26+
27+
if (!user) {
28+
throw new Error('User not found')
29+
}
30+
31+
if (user.twoFactorAuth) {
32+
// 2FA is already enabled for this user
33+
return 'Two-factor authentication is already enabled for this user.'
34+
}
35+
36+
user.twoFactorAuth = true
37+
38+
await user.save()
39+
40+
41+
await sendEmail(
42+
email,
43+
' Two-Factor Authentication enabled ',
44+
'Two-Factor Authentication has been enabled on your account next time . You will be asked to verify your account with an OTP code',
45+
null,
46+
process.env.ADMIN_EMAIL,
47+
process.env.ADMIN_PASS
48+
)
49+
return 'Two-factor authentication enabled.'
50+
} catch (error) {
51+
console.error('Enable 2FA Error:', error)
52+
// Add this for more detailed error logging
53+
throw new Error('Failed to enable two-factor authentication')
54+
}
55+
},
56+
disableTwoFactorAuth: async (_: any, { email }: Disable2FAInput) => {
57+
try {
58+
const UserModel = mongoose.model('User')
59+
const user = await UserModel.findOne({ email })
60+
61+
if (!user) {
62+
throw new Error('User not found')
63+
}
64+
65+
// Disable 2FA by clearing the secret and one-time code
66+
user.twoFactorSecret = null
67+
user.twoFactorAuth = false
68+
user.oneTimeCode = null
69+
70+
await user.save()
71+
72+
return 'Two-factor authentication disabled.'
73+
} catch (error) {
74+
throw new Error('Failed to disable two-factor authentication')
75+
}
76+
},
77+
78+
79+
loginWithTwoFactorAuthentication: async (
80+
_: any,
81+
{ id, email, otp, TwoWayVerificationToken }: { id?: string; email?: string; otp: string; TwoWayVerificationToken: string }
82+
) => {
83+
84+
// Verify OTP
85+
const isValidOtp = verifyOtpToken(TwoWayVerificationToken, otp);
86+
87+
if (!isValidOtp) {
88+
throw new GraphQLError('Invalid OTP. Please try again.');
89+
}
90+
91+
// Fetch user by either ID or email
92+
let user: any;
93+
if (id) {
94+
user = await User.findById(id);
95+
} else if (email) {
96+
user = await User.findOne({ email });
97+
}
98+
99+
// Check if user was found
100+
if (!user) {
101+
throw new GraphQLError('User not found.');
102+
}
103+
104+
// Generate JWT token
105+
const token = jwt.sign(
106+
{ userId: user._id, role: user._doc?.role || 'user' },
107+
SECRET,
108+
{ expiresIn: '2h' }
109+
);
110+
111+
112+
113+
return {
114+
token,
115+
user: user.toJSON(),
116+
117+
message: 'Logged in successfully',
118+
};
119+
},
120+
121+
},
122+
};
123+
124+
125+
export default resolvers

0 commit comments

Comments
 (0)