Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@ FIREBASE_APP_ID=YOUR_FIREBASE_APP_ID
FIREBASE_MEASUREMENT_ID=YOUR_FIREBASE_MEASUREMENT_ID
```

Google Cloud Console (Google OAuth Client ID)
1. Application type: Web Application
2. Authorized JavaScript origins:
- http://localhost:5173 (local development)
- Any other frontend URI you use
3. Redirect URIs: Not needed for this setup

```bash
GOOGLE_CLIENT_ID=YOUR_GOOGLE_OAuth_CLIENT_ID
```

c. Install dependencies and run the server:

```bash
Expand All @@ -243,6 +254,13 @@ npm start

### 3\. Set up the Frontends (`/user`, `/employee`, `/admin`):

> **Note:** The following `.env` configuration is **only for the `/user`**:

```bash
VITE_GOOGLE_CLIENT_ID=YOUR_GOOGLE_OAuth_CLIENT_ID (Same as Backend)
```


> Repeat the following steps for each frontend directory (`user`, `employee`, and `admin`) in a **separate terminal**.

a. Navigate to a frontend directory:
Expand Down
160 changes: 158 additions & 2 deletions backend/controllers/userController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import userModel from "../models/userModel.js";
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import { OAuth2Client } from "google-auth-library";

const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); // clien id needed for google login

const registerUser = async(req,res) =>{
const {name,email,mobile,password,gender,country,state} = req.body;
Expand Down Expand Up @@ -31,7 +34,8 @@ const registerUser = async(req,res) =>{
state,
mobile,
gender,
password : hashedPassword
password : hashedPassword,
isComplete: true, // Mark local registration as complete
})

await newUser.save();
Expand Down Expand Up @@ -62,6 +66,23 @@ const loginUser = async(req,res) =>{
})
}

// Check if the user registered via Google
if (user.googleId) {
return res.status(400).json({
success: false,
message:
"This account uses Google login. Please use Google or mobile OTP.",
});
}

// Ensure the user has completed registration
if (!user.isComplete) {
return res.json({
success: false,
message: "Please complete your registration first",
});
}

const isMatch = await bcrypt.compare(password,user.password)
if(!isMatch){
return res.json({
Expand Down Expand Up @@ -100,6 +121,141 @@ const loginUser = async(req,res) =>{
}
}

// Google Login Handler
const googleLogin = async (req, res) => {

const { id_token, isRegistering } = req.body;
try {

const ticket = await client.verifyIdToken({
idToken: id_token,
audience: process.env.GOOGLE_CLIENT_ID,
});

const data = ticket.getPayload();
if (data.aud !== process.env.GOOGLE_CLIENT_ID) {
return res
.status(401)
.json({ success: false, message: "Invalid audience" });
}

if (data.exp * 1000 < Date.now()) {
return res.status(401).json({ success: false, message: "Token expired" });
}

const { sub: googleId, email, name } = data;
let existingUser = await userModel.findOne({ email });

if (existingUser && !existingUser.googleId) {
if (isRegistering) {
return res
.status(400)
.json({
success: false,
message: "User already exists. Please login instead.",
});
}
return res.status(400).json({
success: false,
message: "This account uses email login. Please use your email & password or mobile OTP.",
});
}

let user = await userModel.findOne({ googleId });

if (user) {
// Extra safety: check required fields along with isComplete
if (
user.isComplete &&
user.mobile &&
user.gender &&
user.state &&
user.country
) {
if (isRegistering) {
return res.status(400).json({
success: false,
message: "User already exists. Please login instead.",
});
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: "1h",
});

return res.json({
success: true,
message: "Logged in successfully",
token,
user,
});
} else {
// Partial registration / missing info
return res.json({ success: true, user, incomplete: true });
}
} else {
if (isRegistering) {
// Create new user only during registration
user = new userModel({
name,
email,
googleId,
provider: "google",
isComplete: false,
});
await user.save();
return res.json({ success: true, user, incomplete: true });
} else {
// During login, do NOT create user
return res
.status(404)
.json({ success: false, message: "User not found" });
}
}
} catch (error) {
return res
.status(500)
.json({ success: false, message: "Google authentication failed" });
}
};

// Complete Registration for Google Users
const completeGoogleRegistration = async (req, res) => {
const { googleId, mobile, gender, state, country } = req.body;
try {
const user = await userModel.findOne({ googleId });
if (!user) return res.status(404).json({ message: "User not found" });

const isExistMobile = await userModel.findOne({
mobile,
_id: { $ne: user._id },
});

if (isExistMobile) {
return res.json({
success: false,
message: "Mobile number already exists",
});
}

user.mobile = mobile;
user.gender = gender;
user.state = state;
user.country = country;
user.isComplete = true;

await user.save();

return res.json({
success: true,
message: "Registration completed successfully. Please login to continue.",
});
} catch (error) {
return res
.status(500)
.json({ success: false, message: "Failed to complete registration" });
}
};

const getSingleUser = async(req,res) =>{
const id = req.params.id;
try {
Expand Down Expand Up @@ -144,4 +300,4 @@ const getAllUser = async(req,res) =>{
}
}

export {registerUser,loginUser ,getAllUser ,getSingleUser}
export {registerUser,loginUser ,googleLogin, completeGoogleRegistration,getAllUser ,getSingleUser}
61 changes: 45 additions & 16 deletions backend/models/userModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,55 @@ const userSchema = new mongoose.Schema({
required:true,
unique:true
},
password:{
type:String,
required:true
password: {
type: String,
required: function () {
return this.provider === "local";
},
},
gender:{
type:String,
required:true
googleId: {
type: String,
unique: true,
sparse: true,
required: function () {
return this.provider === "google";
},
},
mobile:{
type:Number,
required:true,
unique:true
provider: {
type: String,
enum: ["local", "google"],
default: "local",
},
state:{
type:String,
required:true
isComplete: {
type: Boolean,
default: function () {
return this.provider === "local"; // local users are always complete
},
},
country:{
type:String,
required:true
gender: {
type: String,
required: function () {
return this.isComplete;
},
},
mobile: {
type: Number,
unique: true,
required: function () {
return this.isComplete;
},
},
state: {
type: String,
required: function () {
return this.isComplete;
},
},
country: {
type: String,
required: function () {
return this.isComplete;
},
},
otp: { type: String },
otp_expiry: { type: Date },
Expand Down
Loading