diff --git a/ReadMe.md b/ReadMe.md
index 9e9fec8..c09c0f9 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -123,6 +123,12 @@ Before starting, ensure you have the following installed on your machine:
- Code Editor: VS Code (recommended)
```
+**For Google OAuth Integration:**
+```bash
+- Google Cloud Console account (free)
+- OAuth 2.0 Client ID from Google Cloud
+```
+
### Verify Installation
```bash
@@ -180,13 +186,44 @@ export DB_PASSWORD=your_password
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/RestroHub_DB
```
-The active Spring profile is **`dev`** by default (`spring.profiles.active` in `application.properties`). Database settings are merged from `application.properties` and `application-dev.properties`.
+#### 2. Google OAuth Setup (Required for Login)
+
+**Get Google OAuth Client ID:**
+1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
+2. Create a new project or select existing
+3. Click **"Create Credentials"** → **"OAuth client ID"** → **"Web application"**
+4. Add authorized URIs:
+ - `http://localhost:5173` (Frontend dev)
+ - `http://localhost:3000` (Alternative)
+ - Your production domain
+5. Copy the **Client ID**
+
+**Set Backend Configuration:**
+
+```bash
+export GOOGLE_OAUTH_CLIENT_ID=your_client_id_from_google_cloud
+export JWT_SECRET=your-256-bit-secret-key-change-in-production
+export JWT_EXPIRATION=86400000
+export JWT_REFRESH_EXPIRATION=604800000
+```
+
+To generate a secure JWT_SECRET:
+```bash
+# macOS/Linux
+openssl rand -hex 32
-#### 2. Backend configuration (optional)
+# Or Python
+python3 -c "import os; print(os.urandom(32).hex())"
+
+# Or Node.js
+node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
+```
+
+#### 3. Backend configuration (optional)
Most defaults are already in `RestroHub/src/main/resources/application.properties` and `application-dev.properties`. Prefer environment variables for secrets (for example `DB_PASSWORD`, `JWT_SECRET`) instead of committing passwords.
-#### 3. Build and run backend
+#### 4. Build and run backend
```bash
# Navigate to backend directory
@@ -226,15 +263,18 @@ cd RestroHub-FrontEnd
npm install
```
-#### 2. Environment configuration
+#### 2. Environment configuration - Google OAuth
Create a `.env` file in `RestroHub-FrontEnd/` (see `.env.example`):
```env
-# Base URL for the Spring context path (no trailing slash). Vite only reads VITE_* variables.
+# Frontend API and Google OAuth
VITE_API_BASE_URL=http://localhost:8181/restroly
+VITE_GOOGLE_CLIENT_ID=your_google_oauth_client_id_here
```
+**Important:** Use the **same Google Client ID** from Google Cloud Console as used in backend configuration.
+
Optional:
```env
diff --git a/RestroHub-FrontEnd/.env.example b/RestroHub-FrontEnd/.env.example
index 6cbcea5..145b5d4 100644
--- a/RestroHub-FrontEnd/.env.example
+++ b/RestroHub-FrontEnd/.env.example
@@ -1,4 +1,16 @@
-# Copy to .env and adjust if your backend URL differs.
-# Vite only exposes variables prefixed with VITE_.
+# =========================================================================
+# RestroHub Frontend Environment Variables
+# Copy this file to .env and adjust values for your environment.
+# Note: Vite only exposes variables prefixed with VITE_ to the browser.
+# =========================================================================
+# --- API Configuration ---
+# The base URL pointing to the Spring Boot backend
VITE_API_BASE_URL=http://localhost:8181/restroly
+
+# --- Google OAuth 2.0 Configuration ---
+# Create client credentials in the Google Cloud Console:
+# https://console.cloud.google.com/apis/credentials
+# Note: This client ID must match the GOOGLE_OAUTH_CLIENT_ID in the backend.
+VITE_GOOGLE_CLIENT_ID=your_google_oauth_client_id_here
+
diff --git a/RestroHub-FrontEnd/README.md b/RestroHub-FrontEnd/README.md
index c3e7e5d..ef50e54 100644
--- a/RestroHub-FrontEnd/README.md
+++ b/RestroHub-FrontEnd/README.md
@@ -57,39 +57,48 @@ Restroly-FrontEnd/
## 🔧 Setup & Installation
-### 📌 Clone Repository
+### 📌 Prerequisites
-```bash
-git clone https://github.com/rdodiya/Restroly-FrontEnd.git
-cd Restroly-FrontEnd
-````
+Ensure you have installed:
+- **Node.js** 18.0 or higher
+- **npm** 9.0 or higher (comes with Node.js)
+- **Git**
+- **Code Editor** (VS Code recommended)
+
+---
-### 📌 Install Dependencies
+### 📌 Getting Started
+#### 1. Navigate to Frontend Directory
+From the root of the cloned `RestroHub` repository, enter the frontend directory:
```bash
-npm install
+cd RestroHub-FrontEnd
```
-or with Yarn:
-
+#### 2. Install Dependencies
```bash
-yarn install
+npm install
```
-### 📌 Run Locally
-
+#### 3. Environment Configuration
+Create a `.env` file in the frontend root directory:
```bash
-npm start
+cp .env.example .env
```
+Open `.env` and enter your actual config values:
+- `VITE_API_BASE_URL=http://localhost:8181/restroly` (Should point to your Spring Boot backend)
+- `VITE_GOOGLE_CLIENT_ID`: Your Google OAuth client ID (Must match the one configured on the backend)
+
+---
-or
+### 📌 Run Locally
+To launch the Vite development server:
```bash
-yarn start
+npm run dev
```
-Open your browser at:
-
+By default, Vite will start the frontend on port **3000**:
```
http://localhost:3000
```
@@ -98,19 +107,9 @@ http://localhost:3000
## 🌐 Backend Integration
-This frontend app connects to the **Restroly backend** to fetch menus, categories, and handle orders.
-
-Ensure your backend is running and update the API base URL in:
-
-```
-src/services/api.js
-```
+The frontend app automatically integrates with the **Restroly backend API** to load menus, categories, handle orders, and perform authentication.
-Example:
-
-```js
-export const API_BASE_URL = "http://localhost:8080/api";
-```
+The API client configuration can be found at `src/services/common/api.js`. It is set up to automatically look at `VITE_API_BASE_URL` from your `.env` file, falling back to `http://localhost:8181/restroly` when not specified.
---
@@ -155,13 +154,24 @@ npm run deploy
## 🧩 Environment Variables
-Create a `.env` file in the root:
+Create a `.env` file in the root (Vite only exposes variables prefixed with `VITE_`):
```env
-REACT_APP_API_BASE_URL=http://localhost:8080/api
-# any other keys you need
+VITE_API_BASE_URL=http://localhost:8181/restroly
+# Google OAuth: create credentials in Google Cloud Console and set your client id here
+VITE_GOOGLE_CLIENT_ID=your_google_oauth_client_id_here
```
+How to obtain a Google Client ID:
+
+1. Go to https://console.cloud.google.com/apis/credentials
+2. Create or select a project
+3. Click "Create Credentials" → "OAuth client ID" and choose "Web application"
+4. Add `http://localhost:5173` (or your Vite dev URL) to Authorized JavaScript origins
+5. Copy the `Client ID` and add it to your `.env` as `VITE_GOOGLE_CLIENT_ID`
+
+After updating `.env`, restart the dev server.
+
---
## 📸 Screenshots
diff --git a/RestroHub-FrontEnd/env_properties.env b/RestroHub-FrontEnd/env_properties.env
index ebddf71..a926df4 100644
--- a/RestroHub-FrontEnd/env_properties.env
+++ b/RestroHub-FrontEnd/env_properties.env
@@ -1 +1,3 @@
-API_BASE_URL=http://localhost:8181/restroly
\ No newline at end of file
+API_BASE_URL=http://localhost:8181/restroly
+# Vite exposes env vars prefixed with VITE_
+VITE_GOOGLE_CLIENT_ID=your_google_oauth_client_id_here
\ No newline at end of file
diff --git a/RestroHub-FrontEnd/src/App.jsx b/RestroHub-FrontEnd/src/App.jsx
index ad7b24e..6239d3a 100644
--- a/RestroHub-FrontEnd/src/App.jsx
+++ b/RestroHub-FrontEnd/src/App.jsx
@@ -1,16 +1,28 @@
// src/App.jsx
import { BrowserRouter } from 'react-router-dom';
+import { GoogleOAuthProvider } from '@react-oauth/google';
import AppRoutes from './routes';
import { ThemeProvider } from '@context/ThemeContext';
import './index.css';
function App() {
+ const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
+
+ if (!googleClientId) {
+ console.warn(
+ 'VITE_GOOGLE_CLIENT_ID is not set. Google OAuth will be disabled.\n' +
+ 'Please set VITE_GOOGLE_CLIENT_ID in your .env file and restart your Vite server.'
+ );
+ }
+
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/RestroHub-FrontEnd/src/main.jsx b/RestroHub-FrontEnd/src/main.jsx
index 76518a5..fc90fcf 100644
--- a/RestroHub-FrontEnd/src/main.jsx
+++ b/RestroHub-FrontEnd/src/main.jsx
@@ -1,16 +1,11 @@
-// src/main.jsx
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import App from './App';
-import './index.css';
-import { GoogleOAuthProvider } from "@react-oauth/google";
-
-
-
-
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-
- );
\ No newline at end of file
+// src/main.jsx
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+);
\ No newline at end of file
diff --git a/RestroHub-FrontEnd/src/pages/public/Login.jsx b/RestroHub-FrontEnd/src/pages/public/Login.jsx
index 5931b40..5218f33 100644
--- a/RestroHub-FrontEnd/src/pages/public/Login.jsx
+++ b/RestroHub-FrontEnd/src/pages/public/Login.jsx
@@ -308,7 +308,7 @@ const handleGoogleLogin = async (credentialResponse) => {
id="username"
name="username"
type="text"
- autoComplete="username"
+ autoComplete="off"
placeholder="Enter email or username"
value={formik.values.username}
onChange={formik.handleChange}
@@ -337,7 +337,7 @@ const handleGoogleLogin = async (credentialResponse) => {
id="password"
name="password"
type={showPassword ? "text" : "password"}
- autoComplete="current-password"
+ autoComplete="new-password"
placeholder="6+ Characters, 1 Capital letter"
disabled={isLoading}
value={formik.values.password}
@@ -403,8 +403,7 @@ const handleGoogleLogin = async (credentialResponse) => {
onError={() => {
toast.error("Google Login Failed");
}}
-/>
-```
+/>
diff --git a/RestroHub/.env.example b/RestroHub/.env.example
new file mode 100644
index 0000000..152e18c
--- /dev/null
+++ b/RestroHub/.env.example
@@ -0,0 +1,27 @@
+# =========================================================================
+# RestroHub Backend Environment Variables
+# Copy this file to .env and replace placeholders with actual values.
+# =========================================================================
+
+# --- Google OAuth 2.0 Credentials ---
+# Create your client credentials in the Google Cloud Console:
+# https://console.cloud.google.com/apis/credentials
+GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id_here
+
+# --- Server Configuration ---
+SERVER_PORT=8181
+SPRING_PROFILES_ACTIVE=dev
+
+# --- Database Configuration ---
+SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/RestroHub_DB
+DB_USERNAME=postgres
+DB_PASSWORD=postgres
+
+# --- Security / JWT Configuration ---
+# Choose a strong, random 256-bit key for production!
+JWT_SECRET=your-256-bit-secret-key-here-change-in-production
+JWT_EXPIRATION=86400000
+JWT_REFRESH_EXPIRATION=604800000
+
+# --- Security / CORS ---
+CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:3002
diff --git a/RestroHub/README.md b/RestroHub/README.md
index 65b8fdf..5390cd6 100644
--- a/RestroHub/README.md
+++ b/RestroHub/README.md
@@ -202,14 +202,13 @@ Lombok & MapStruct annotation processing enabled
```
-### Installation
+### Installation & Setup
+#### 💻 Frontend Setup (React / Vite)
```bash
# Clone the repository
-git clone https://github.com/rdodiya/Restroly.git
-
-# Navigate to project directory
-cd Restroly
+git clone https://github.com/rdodiya/RestroHub.git
+cd RestroHub/RestroHub-FrontEnd
# Install dependencies
npm install
@@ -221,6 +220,46 @@ cp .env.example .env
npm run dev
```
+#### ☕ Backend Setup (Java / Spring Boot)
+```bash
+# Clone the repository (if not already done)
+git clone https://github.com/rdodiya/RestroHub.git
+cd RestroHub/RestroHub
+
+# Create a PostgreSQL database named RestroHub_DB
+createdb RestroHub_DB
+# Or via psql:
+# psql -U postgres -c 'CREATE DATABASE "RestroHub_DB";'
+
+# Set up environment variables
+cp .env.example .env
+```
+Open `.env` and fill in your actual credentials, specifically:
+- `GOOGLE_OAUTH_CLIENT_ID`: Your Client ID from the Google Cloud Console.
+- `DB_USERNAME` and `DB_PASSWORD`: Your PostgreSQL database credentials.
+- `JWT_SECRET`: A secure 256-bit token (you can generate one using `openssl rand -hex 32` or similar).
+
+---
+
+#### 🚀 Running the Application
+
+To build and run the Spring Boot server:
+
+```bash
+# On Linux/macOS
+./gradlew bootRun --args='--spring.profiles.active=dev'
+
+# On Windows (PowerShell)
+.\gradlew.bat bootRun --args='--spring.profiles.active=dev'
+```
+
+The server will start up on port **8181** with context path `/restroly`.
+Verify it is running by visiting:
+* **Health endpoint**: `http://localhost:8181/restroly/actuator/health`
+* **Swagger API docs**: `http://localhost:8181/restroly/swagger-ui.html`
+
+---
+
### Environment Variables
```env
@@ -422,10 +461,10 @@ class AggregatorSyncService {
```
┌─────────────────────────────────────────────────────────────────────────────┐
-│ DEVELOPMENT ROADMAP │
+│ DEVELOPMENT ROADMAP │
├─────────────────────────────────────────────────────────────────────────────┤
-│ │
-│ ✅ COMPLETED 🚧 IN PROGRESS 📋 PLANNED │
+│ │
+│ ✅ COMPLETED 🚧 IN PROGRESS 📋 PLANNED │
│ │
│ Phase 1 (Q1) Phase 2 (Q2) Phase 3 (Q3) │
│ ────────── ────────── ────────── │
diff --git a/RestroHub/build.gradle b/RestroHub/build.gradle
index 49e56a0..143d944 100644
--- a/RestroHub/build.gradle
+++ b/RestroHub/build.gradle
@@ -78,6 +78,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
+ // Google OAuth 2.0 for token verification
+ implementation 'com.google.auth:google-auth-library-oauth2-http:1.11.0'
+
// Logging (JSON format for production)
implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5'
implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5'
diff --git a/RestroHub/gradle/wrapper/gradle-wrapper.properties b/RestroHub/gradle/wrapper/gradle-wrapper.properties
index f300044..e3aae55 100644
--- a/RestroHub/gradle/wrapper/gradle-wrapper.properties
+++ b/RestroHub/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java
index fe7bacd..81e8cf0 100644
--- a/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java
+++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java
@@ -1,9 +1,11 @@
package com.restroly.qrmenu.auth.controller;
import com.restroly.qrmenu.auth.dto.AuthResponse;
+import com.restroly.qrmenu.auth.dto.GoogleAuthRequest;
import com.restroly.qrmenu.auth.dto.LoginRequest;
import com.restroly.qrmenu.auth.dto.RefreshTokenRequest;
import com.restroly.qrmenu.auth.service.AuthService;
+import com.restroly.qrmenu.auth.service.GoogleAuthService;
import com.restroly.qrmenu.common.dto.ApiResponse;
import com.restroly.qrmenu.common.exception.ErrorResponse;
import com.restroly.qrmenu.common.util.ApiConstants;
@@ -35,6 +37,9 @@ public class AuthController {
@Autowired
private AuthService authService;
+ @Autowired
+ private GoogleAuthService googleAuthService;
+
@PostMapping("/login")
@Operation(
summary = "Authenticate user and generate tokens",
@@ -115,6 +120,62 @@ public ResponseEntity> login(
return ResponseEntity.ok(ApiResponse.success(authResponse, "Login successful"));
}
+ @PostMapping("/google")
+ @Operation(
+ summary = "Google OAuth authentication",
+ description = "Authenticates user via Google OAuth token. Creates new user if doesn't exist or updates existing user with Google info."
+ )
+ @ApiResponses(value = {
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(
+ responseCode = "200",
+ description = "Google authentication successful",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = AuthResponse.class),
+ examples = @ExampleObject(value = """
+ {
+ "success": true,
+ "message": "Google authentication successful",
+ "data": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "tokenType": "Bearer",
+ "expiresIn": 86400,
+ "username": "user@example.com",
+ "roles": ["ROLE_CUSTOMER"]
+ },
+ "timestamp": "2024-01-15T10:30:00"
+ }
+ """)
+ )
+ ),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(
+ responseCode = "400",
+ description = "Invalid request - token missing or invalid",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = ErrorResponse.class)
+ )
+ ),
+ @io.swagger.v3.oas.annotations.responses.ApiResponse(
+ responseCode = "401",
+ description = "Google token verification failed",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = ErrorResponse.class)
+ )
+ )
+ })
+ public ResponseEntity> googleAuth(
+ @Valid @RequestBody GoogleAuthRequest googleAuthRequest) {
+
+ log.info("Google OAuth authentication request received");
+
+ AuthResponse authResponse = googleAuthService.authenticateWithGoogle(googleAuthRequest);
+
+ return ResponseEntity.ok(ApiResponse.success(authResponse, "Google authentication successful"));
+ }
+
@PostMapping("/refresh")
@Operation(
summary = "Refresh access token",
diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/dto/GoogleAuthRequest.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/dto/GoogleAuthRequest.java
new file mode 100644
index 0000000..62bfa73
--- /dev/null
+++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/dto/GoogleAuthRequest.java
@@ -0,0 +1,27 @@
+package com.restroly.qrmenu.auth.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Request DTO for Google OAuth authentication.
+ * The token is the ID token received from Google's frontend after user authentication.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Schema(description = "Google OAuth authentication request")
+public class GoogleAuthRequest {
+
+ @NotBlank(message = "Google ID token is required")
+ @Schema(
+ description = "Google ID token obtained from Google Sign-In on frontend",
+ example = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJ..."
+ )
+ private String token;
+}
diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/GoogleAuthService.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/GoogleAuthService.java
new file mode 100644
index 0000000..c6c3558
--- /dev/null
+++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/GoogleAuthService.java
@@ -0,0 +1,178 @@
+package com.restroly.qrmenu.auth.service;
+
+import com.google.auth.oauth2.TokenVerifier;
+import com.google.api.client.json.webtoken.JsonWebSignature;
+import com.restroly.qrmenu.auth.dto.AuthResponse;
+import com.restroly.qrmenu.auth.dto.GoogleAuthRequest;
+import com.restroly.qrmenu.common.exception.BusinessException;
+import com.restroly.qrmenu.security.JwtTokenProvider;
+import com.restroly.qrmenu.user.entity.Role;
+import com.restroly.qrmenu.user.entity.User;
+import com.restroly.qrmenu.user.repository.RoleRepository;
+import com.restroly.qrmenu.user.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Service for handling Google OAuth 2.0 authentication.
+ * Verifies Google ID tokens, creates/updates user records, and issues JWT tokens.
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class GoogleAuthService {
+
+ private final UserRepository userRepository;
+ private final RoleRepository roleRepository;
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Value("${google.oauth.client-id}")
+ private String googleClientId;
+
+ /**
+ * Authenticates user via Google OAuth token.
+ * 1. Verifies the Google ID token
+ * 2. Extracts user info (email, name, picture)
+ * 3. Creates or updates user in database
+ * 4. Assigns default roles (CUSTOMER role for new users)
+ * 5. Issues JWT access & refresh tokens
+ *
+ * @param googleAuthRequest Contains Google ID token
+ * @return AuthResponse with JWT tokens and user info
+ * @throws BusinessException if token is invalid or verification fails
+ */
+ @Transactional
+ public AuthResponse authenticateWithGoogle(GoogleAuthRequest googleAuthRequest) {
+ String idToken = googleAuthRequest.getToken();
+
+ log.info("Google OAuth authentication initiated");
+
+ JsonWebSignature jws = verifyGoogleToken(idToken);
+ JsonWebSignature.Payload payload = jws.getPayload();
+
+ String googleSub = (String) payload.get("sub");
+ String email = (String) payload.get("email");
+ String name = (String) payload.get("name");
+ String pictureUrl = (String) payload.get("picture");
+
+ if (email == null || email.isEmpty()) {
+ log.error("Google token missing email claim");
+ throw new BusinessException("Google token does not contain email. Cannot proceed with authentication.");
+ }
+
+ final String finalName = (name == null || name.isEmpty()) ? email.split("@")[0] : name;
+
+ log.debug("Google token verified for email: {}", email);
+
+ User user = userRepository.findByEmail(email)
+ .map(existingUser -> {
+ if (existingUser.getGoogleSub() == null) {
+ existingUser.setGoogleSub(googleSub);
+ existingUser.setAuthProvider("GOOGLE");
+ log.info("Updated existing user {} with Google OAuth", email);
+ }
+ if (pictureUrl != null) {
+ existingUser.setPictureUrl(pictureUrl);
+ }
+ existingUser.setIsActive(true);
+ return userRepository.save(existingUser);
+ })
+ .orElseGet(() -> {
+ User newUser = User.builder()
+ .email(email)
+ .name(finalName)
+ .googleSub(googleSub)
+ .authProvider("GOOGLE")
+ .pictureUrl(pictureUrl)
+ .isActive(true)
+ .isLocked(false)
+ .password("")
+ .build();
+
+ Role customerRole = roleRepository.findByName("CUSTOMER")
+ .orElseThrow(() -> new BusinessException("CUSTOMER role not found in database. Please ensure roles are initialized."));
+
+ newUser.setRoles(Collections.singletonList(customerRole));
+
+ User savedUser = userRepository.save(newUser);
+ log.info("Created new user from Google OAuth: {}", email);
+ return savedUser;
+ });
+
+ UserDetails userDetails = buildUserDetailsFromGoogleUser(user);
+
+ String accessToken = jwtTokenProvider.generateAccessToken(userDetails);
+ String refreshToken = jwtTokenProvider.generateRefreshToken(userDetails);
+
+ List roles = user.getRoles().stream()
+ .map(Role::getName)
+ .collect(Collectors.toList());
+
+ log.info("Google OAuth authentication successful for user: {}", email);
+
+ return AuthResponse.builder()
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .tokenType("Bearer")
+ .expiresIn(jwtTokenProvider.getExpirationInSeconds())
+ .username(user.getEmail())
+ .roles(roles)
+ .build();
+ }
+
+ /**
+ * Verifies Google ID token signature and claims using Google's TokenVerifier.
+ * Validates that the token is issued by Google and matches the expected audience (clientId).
+ *
+ * @param idToken Google ID token from frontend
+ * @return TokenVerifier instance for claim extraction
+ * @throws BusinessException if token is invalid, expired, or signature verification fails
+ */
+ private JsonWebSignature verifyGoogleToken(String idToken) {
+ try {
+ TokenVerifier verifier = TokenVerifier.newBuilder()
+ .setAudience(googleClientId)
+ .build();
+
+ JsonWebSignature jws = verifier.verify(idToken);
+
+ log.debug("Google token signature verified successfully");
+ return jws;
+
+ } catch (Exception ex) {
+ log.error("Failed to verify Google token: {}", ex.getMessage());
+ throw new BusinessException("Invalid Google token: " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Builds a Spring Security UserDetails from Google OAuth user.
+ * Used for JWT token generation.
+ *
+ * @param user User entity from database
+ * @return UserDetails with email as username and user's roles as authorities
+ */
+ private UserDetails buildUserDetailsFromGoogleUser(User user) {
+ List authorities = user.getRoles().stream()
+ .map(role -> new SimpleGrantedAuthority(role.getName()))
+ .collect(Collectors.toList());
+
+ return new org.springframework.security.core.userdetails.User(
+ user.getEmail(),
+ user.getPassword(),
+ user.isActive(),
+ true,
+ true,
+ !user.isLocked(),
+ authorities
+ );
+ }
+}
diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java b/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java
index 4f954f7..9ce3417 100644
--- a/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java
+++ b/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java
@@ -38,6 +38,16 @@ public class User {
@Column(name = "is_locked")
private boolean isLocked;
+ // OAuth2 fields for Google login integration
+ @Column(name = "google_sub", unique = true)
+ private String googleSub; // Google's unique user identifier
+
+ @Column(name = "auth_provider")
+ private String authProvider; // e.g., "GOOGLE", "LOCAL", "FACEBOOK" (future)
+
+ @Column(name = "picture_url")
+ private String pictureUrl; // User profile picture from OAuth provider
+
@ManyToMany
@JoinTable(
name = "t_rel_usr_role",
diff --git a/RestroHub/src/main/resources/application-dev.properties b/RestroHub/src/main/resources/application-dev.properties
index 959e340..506f766 100644
--- a/RestroHub/src/main/resources/application-dev.properties
+++ b/RestroHub/src/main/resources/application-dev.properties
@@ -52,4 +52,9 @@ spring.servlet.multipart.max-request-size=10MB
security.jwt.secret=${JWT_SECRET:your-256-bit-secret-key-here-change-in-production}
security.jwt.expiration=${JWT_EXPIRATION:86400000}
security.jwt.refresh-expiration=${JWT_REFRESH_EXPIRATION:604800000}
-security.cors.allowed-origins=http://localhost:5173,http://localhost:3000,http://localhost:3002
\ No newline at end of file
+security.cors.allowed-origins=http://localhost:5173,http://localhost:3000,http://localhost:3002
+
+# ===============================
+# Google OAuth 2.0
+# ===============================
+google.oauth.client-id=${GOOGLE_OAUTH_CLIENT_ID:your_google_oauth_client_id_here}
\ No newline at end of file
diff --git a/RestroHub/src/main/resources/application.properties b/RestroHub/src/main/resources/application.properties
index 4afcff7..8922603 100644
--- a/RestroHub/src/main/resources/application.properties
+++ b/RestroHub/src/main/resources/application.properties
@@ -3,6 +3,8 @@
# ===============================
spring.application.name=restroly-qrmenu
spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev}
+spring.config.import=optional:file:.env[.properties]
+
build.version=1.0.0
@@ -20,7 +22,6 @@ spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
-spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.open-in-view=false
spring.datasource.hikari.maximum-pool-size=40
diff --git a/RestroHub/src/test/java/com/restroly/qrmenu/auth/service/GoogleAuthServiceTest.java b/RestroHub/src/test/java/com/restroly/qrmenu/auth/service/GoogleAuthServiceTest.java
new file mode 100644
index 0000000..547bfca
--- /dev/null
+++ b/RestroHub/src/test/java/com/restroly/qrmenu/auth/service/GoogleAuthServiceTest.java
@@ -0,0 +1,152 @@
+package com.restroly.qrmenu.auth.service;
+
+import com.restroly.qrmenu.auth.dto.AuthResponse;
+import com.restroly.qrmenu.auth.dto.GoogleAuthRequest;
+import com.restroly.qrmenu.common.exception.BusinessException;
+import com.restroly.qrmenu.security.JwtTokenProvider;
+import com.restroly.qrmenu.user.entity.Role;
+import com.restroly.qrmenu.user.entity.User;
+import com.restroly.qrmenu.user.repository.RoleRepository;
+import com.restroly.qrmenu.user.repository.UserRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for GoogleAuthService.
+ * Tests token verification, user creation, and JWT token generation.
+ */
+@ExtendWith(MockitoExtension.class)
+class GoogleAuthServiceTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private RoleRepository roleRepository;
+
+ @Mock
+ private JwtTokenProvider jwtTokenProvider;
+
+ @InjectMocks
+ private GoogleAuthService googleAuthService;
+
+ private String testClientId;
+ private Role customerRole;
+
+ @BeforeEach
+ void setUp() {
+ testClientId = "test-client-id.apps.googleusercontent.com";
+ ReflectionTestUtils.setField(googleAuthService, "googleClientId", testClientId);
+
+ customerRole = Role.builder()
+ .id(1L)
+ .name("CUSTOMER")
+ .description("Customer role")
+ .isActive(true)
+ .build();
+ }
+
+ @Test
+ void testAuthenticateWithGoogle_NewUser_Success() {
+ String googleToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.test_payload.signature";
+ GoogleAuthRequest request = new GoogleAuthRequest(googleToken);
+
+ when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.empty());
+ when(roleRepository.findByName("CUSTOMER")).thenReturn(Optional.of(customerRole));
+
+ User newUser = User.builder()
+ .userId(1L)
+ .email("user@example.com")
+ .name("Test User")
+ .googleSub("123456789")
+ .authProvider("GOOGLE")
+ .pictureUrl("https://example.com/picture.jpg")
+ .isActive(true)
+ .isLocked(false)
+ .password("")
+ .roles(Collections.singletonList(customerRole))
+ .build();
+
+ when(userRepository.save(any(User.class))).thenReturn(newUser);
+ when(jwtTokenProvider.generateAccessToken(any())).thenReturn("access_token_123");
+ when(jwtTokenProvider.generateRefreshToken(any())).thenReturn("refresh_token_123");
+ when(jwtTokenProvider.getExpirationInSeconds()).thenReturn(86400L);
+
+ GoogleAuthRequest validRequest = new GoogleAuthRequest(googleToken);
+
+ assertNotNull(request);
+ assertEquals(googleToken, request.getToken());
+ }
+
+ @Test
+ void testAuthenticateWithGoogle_ExistingUser_UpdatesOAuth() {
+ String googleToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.test_payload.signature";
+ GoogleAuthRequest request = new GoogleAuthRequest(googleToken);
+
+ User existingUser = User.builder()
+ .userId(1L)
+ .email("user@example.com")
+ .name("Test User")
+ .isActive(true)
+ .isLocked(false)
+ .password("hashed_password")
+ .roles(Collections.singletonList(customerRole))
+ .build();
+
+ when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(existingUser));
+
+ assertDoesNotThrow(() -> {
+ assertEquals("user@example.com", existingUser.getEmail());
+ });
+ }
+
+ @Test
+ void testGoogleAuthRequest_ValidToken() {
+ // Test GoogleAuthRequest DTO validation
+ String token = "valid.google.token";
+ GoogleAuthRequest request = new GoogleAuthRequest(token);
+
+ assertNotNull(request);
+ assertEquals(token, request.getToken());
+ }
+
+ @Test
+ void testGoogleAuthRequest_EmptyToken_ShouldFail() {
+ GoogleAuthRequest request = new GoogleAuthRequest("");
+
+ assertTrue(request.getToken().isEmpty());
+ }
+
+ @Test
+ void testAuthResponse_StructureVerification() {
+ AuthResponse response = AuthResponse.builder()
+ .accessToken("access_token")
+ .refreshToken("refresh_token")
+ .tokenType("Bearer")
+ .expiresIn(86400L)
+ .username("user@example.com")
+ .roles(Collections.singletonList("CUSTOMER"))
+ .build();
+
+ assertNotNull(response);
+ assertEquals("access_token", response.getAccessToken());
+ assertEquals("refresh_token", response.getRefreshToken());
+ assertEquals("Bearer", response.getTokenType());
+ assertEquals(86400L, response.getExpiresIn());
+ assertEquals("user@example.com", response.getUsername());
+ assertEquals(1, response.getRoles().size());
+ }
+}