This project is a secure, profile-driven search platform built with Spring Boot, Spring Security, MySQL, and a React + Vite frontend. It combines:
- Real-time web search retrieval and relevance scoring
- Profile-level data isolation for history, top queries, and stored results
- Password-protected profile operations (create/open/delete)
- Admin-only analytics over all user profiles
- Modern SPA UX (Tailwind CSS, Framer Motion, Lucide icons, toast feedback, skeleton loading)
The application uses Spring Security with hybrid authentication:
- Session auth (Spring Security context /
JSESSIONID) for stable page-to-page redirects - Bearer tokens (access + refresh) for API authorization and automatic token refresh
- Role-based authorization (
ROLE_USER,ROLE_ADMIN)
- Profile-first login flow (
/profiles) - Password-hashed profile authentication (BCrypt)
- Spring Security hybrid auth (session + token) login/logout APIs
- Profile-scoped search data access enforced in service layer
- Exact-query page reuse from stored results, with API fallback for uncached pages
- Search history management with delete by id or query text
- Top query analytics per profile
- Stored results retrieval/update/delete with editable rank and relevance score
- Dedicated admin dashboard (
/admin) for cross-profile analytics - Single-page app build pipeline (Vite build output served by Spring Boot)
- Library-based client routing with
react-router-domroute guards
/profiles- create/open/delete profiles/- search page/history- profile query history/top- profile top queries/stored- stored results management/admin-login- admin login page/admin- admin analytics dashboard
- React 18 + Vite 5
- React Router DOM 7 (
BrowserRouter,Routes,Route,Navigate) - Tailwind CSS 3
- Framer Motion (page transitions / micro-interactions)
- Lucide React icons
- Componentized UI structure (
components/ui,pages,hooks,lib)
This project uses library-based SPA routing with react-router-dom:
frontend/src/main.jsxwraps app inBrowserRouter.frontend/src/app.jsxdefines route tree withRoutes/Route:/,/profiles,/history,/top,/stored,/admin-login,/admin
- Route-level protection is handled by wrappers:
ProtectedUserRoutefor user pagesAdminRoutefor admin dashboard- unauthenticated users are redirected using
Navigate
- Spring serves SPA routes through
SpaController:src/main/java/com/example/searchenginebackend/controller/SpaController.java- each app route is forwarded to
/index.html
- Security allows SPA/static paths in
SecurityConfig:/assets/**,/profiles,/admin-login, etc.
POST /api/auth/profile-logincreates server session + returnsaccessTokenandrefreshToken(ROLE_USER)POST /api/auth/admin-logincreates server session + returnsaccessTokenandrefreshToken(ROLE_ADMIN)- Protected endpoints accept authenticated context from Bearer token (filter) and from active session
GET /api/auth/meresolves identity from Spring Security contextPOST /api/auth/logoutclears server session/context; frontend clears local tokens
Authorization rules:
/api/search/**-> authenticated user required/api/admin/**-> authenticated admin required/api/profiles/**-> public (profile management + verify)- SPA routes like
/profiles,/history,/top,/stored,/admin-loginare publicly accessible entry routes.
Notes:
- Search APIs do not use
X-Profile-Idanymore. - Profile id is resolved from authenticated principal in Spring Security context.
- Token signing/parsing:
AuthTokenService - Token auth filter:
BearerTokenAuthenticationFilter - Session persistence on login is handled in
AuthService.persistAuthentication(...)
Admin credential property:
- File:
src/main/resources/application.properties - Key:
security.admin.password - Default fallback:
admin123
JWT properties:
security.jwt.secretsecurity.jwt.expiration-ms
| Method | Endpoint | Auth | Purpose | Success Code |
|---|---|---|---|---|
| POST | /api/auth/profile-login |
No | Login as profile user | 200 |
| POST | /api/auth/admin-login |
No | Login as admin | 200 |
| POST | /api/auth/refresh |
No (refresh token body) | Rotate and issue new access token | 200 |
| GET | /api/auth/me |
Bearer Token | Current token identity | 200 |
| POST | /api/auth/logout |
No | Logout current session | 204 |
| Method | Endpoint | Auth | Purpose | Success Code |
|---|---|---|---|---|
| POST | /api/profiles |
No | Create profile | 200 |
| GET | /api/profiles |
No | List profiles for profile page | 200 |
| POST | /api/profiles/{id}/verify |
No | Verify profile password | 200 |
| DELETE | /api/profiles/{id} |
No | Delete profile (password required) | 204 |
| Method | Endpoint | Auth | Purpose | Success Code |
|---|---|---|---|---|
| POST | /api/search |
Yes | Return stored page for exact query when present, otherwise fetch and persist requested page | 200 |
| GET | /api/search/history |
Yes | Get profile search history | 200 |
| GET | /api/search/top |
Yes | Get profile top queries | 200 |
| GET | /api/search/results?query=... |
Yes | Get latest stored results for query across saved pages | 200 |
| PUT | /api/search/results/{id} |
Yes | Update stored result rank/relevance (owned by profile) | 200 |
| DELETE | /api/search/results/{id} |
Yes | Delete stored result by id | 204 |
| DELETE | /api/search/results?query=... |
Yes | Delete stored results by query text | 200 |
| DELETE | /api/search/history/{id} |
Yes | Delete one history item | 204 |
| DELETE | /api/search/history?query=... |
Yes | Delete history rows by query text | 200 |
| Method | Endpoint | Auth | Purpose | Success Code |
|---|---|---|---|---|
| GET | /api/admin/profiles |
Yes (Admin) | List all profiles | 200 |
| GET | /api/admin/profiles/{id}/data |
Yes (Admin) | Full analytics for one profile | 200 |
All examples use query text: saveetha engineering colge.
Request
{
"profileId": 1,
"password": "user@123"
}Response 200
{
"authenticated": true,
"role": "USER",
"profileId": 1,
"displayName": "Saveetha Student",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Request
{
"password": "admin123"
}Response 200
{
"authenticated": true,
"role": "ADMIN",
"profileId": null,
"displayName": "admin",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Request
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response 200
{
"authenticated": true,
"role": "USER",
"profileId": 1,
"displayName": "Saveetha Student",
"accessToken": "new-access-token",
"refreshToken": "new-refresh-token"
}Request Header
Authorization: Bearer <accessToken>Response 200
{
"authenticated": true,
"role": "USER",
"profileId": 1,
"displayName": "Saveetha Student",
"accessToken": null
}Response 204 (empty body)
Request
{
"displayName": "Saveetha Student",
"password": "user@123"
}Response 200
{
"id": 1,
"displayName": "Saveetha Student",
"createdAt": "2026-02-17T16:45:12"
}Response 200
[
{
"id": 1,
"displayName": "Saveetha Student",
"createdAt": "2026-02-17T16:45:12"
},
{
"id": 2,
"displayName": "Saveetha Faculty",
"createdAt": "2026-02-17T16:50:10"
}
]Request
{
"password": "user@123"
}Response 200
{
"valid": true,
"message": "Profile unlocked"
}Request
{
"password": "user@123"
}Response 204 (empty body)
Header for all Search/Admin endpoints
Authorization: Bearer <accessToken>Request
{
"query": "saveetha engineering colge",
"page": 0,
"size": 10
}Behavior
- If page
0for the exact same query text is already stored for the current profile, the API returns that stored page. - If the requested page is not stored yet, the backend calls the external search API for that page, persists the new results, and returns them.
Response 200
[
{
"resultId": 120,
"pageId": 32,
"url": "https://www.saveetha.ac.in",
"title": "Saveetha Engineering College",
"score": 1.0,
"rank": 1
}
]Response 200
[
{
"id": 201,
"queryText": "saveetha engineering colge",
"profileId": 1,
"searchedAt": "2026-02-17T18:20:11"
}
]Response 200
[
{
"query": "saveetha engineering colge",
"count": 4
}
]Returns the latest stored result per rank for that exact query text, combining all saved pages for the current profile.
Response 200
[
{
"resultId": 120,
"pageId": 32,
"url": "https://www.saveetha.ac.in",
"title": "Saveetha Engineering College",
"score": 1.0,
"rank": 1
}
]Request
{
"rank": 2,
"relevanceScore": 0.91
}Response 200
{
"id": 120,
"queryText": "saveetha engineering colge",
"cosineSimilarity": 0.42,
"relevanceScore": 0.91,
"rank": 2,
"createdAt": "2026-02-17T18:20:12"
}Response 204 (empty body)
Response 200
3Response 204 (empty body)
Response 200
2Response 200
[
{
"id": 1,
"displayName": "Saveetha Student",
"createdAt": "2026-02-17T16:45:12"
},
{
"id": 2,
"displayName": "Saveetha Faculty",
"createdAt": "2026-02-17T16:50:10"
}
]Response 200
{
"profile": {
"id": 1,
"displayName": "Saveetha Student",
"createdAt": "2026-02-17T16:45:12"
},
"totalQueries": 12,
"totalResults": 96,
"history": [
{
"id": 201,
"queryText": "saveetha engineering colge",
"searchedAt": "2026-02-17T18:20:11"
}
],
"topQueries": [
{
"query": "saveetha engineering colge",
"count": 4
}
],
"storedResults": [
{
"resultId": 120,
"pageId": 32,
"url": "https://www.saveetha.ac.in",
"title": "Saveetha Engineering College",
"score": 1.0,
"rank": 1
}
]
}{
"timestamp": "2026-02-11T23:05:34.7832449",
"status": 400,
"error": "Bad Request",
"message": "Query cannot be empty"
}Common cases:
400 Bad Request: invalid input, wrong password, empty query401 Unauthorized: missing/invalid/expired Bearer token403 Forbidden: logged in but role not permitted404 Not Found: profile/result/query id not found500 Internal Server Error: unhandled server error
profilessearch_queriessearch_resultsweb_pageskeywordspage_keywords
classDiagram
class SecurityConfig
class SpaController {
+String spaEntryPoint()
}
class BearerTokenAuthenticationFilter
class AuthTokenService
class SecurityUtil {
+Long currentProfileId()
}
class AuthenticatedProfile {
+Long profileId()
+String displayName()
}
class AuthController {
+AuthMeResponseDTO profileLogin(ProfileLoginRequestDTO)
+AuthMeResponseDTO adminLogin(AdminLoginRequestDTO)
+AuthMeResponseDTO me()
+void logout(HttpServletResponse)
}
class ProfileController {
+ProfileResponseDTO createProfile(CreateProfileRequestDTO)
+List~ProfileResponseDTO~ publicProfiles()
+ProfileVerifyResponseDTO verifyProfile(Long, VerifyProfileRequestDTO)
+void deleteProfile(Long, DeleteProfileRequestDTO)
}
class SearchController {
+List~SearchResponseDTO~ search(SearchRequestDTO)
+List~SearchQuery~ history()
+List~TopQueryDTO~ topQueries()
+List~SearchResponseDTO~ resultsByQuery(String)
+SearchResult updateResult(Long, UpdateSearchResultDTO)
+void deleteResult(Long)
+long deleteResultsByQuery(String)
+void deleteHistoryById(Long)
+long deleteHistoryByQuery(String)
}
class AdminController {
+List~ProfileResponseDTO~ adminProfiles()
+AdminProfileDataDTO adminProfileData(Long)
}
class AuthService {
+AuthMeResponseDTO loginProfile(Long, String)
+AuthMeResponseDTO loginAdmin(String)
+AuthMeResponseDTO me()
+void logout(HttpServletResponse)
}
class ProfileService {
+ProfileResponseDTO createProfile(CreateProfileRequestDTO)
+List~ProfileResponseDTO~ getPublicProfiles()
+List~ProfileResponseDTO~ getAllProfilesForAdmin()
+ProfileVerifyResponseDTO verifyProfilePassword(Long, String)
+void deleteProfile(Long, String)
}
class SearchService {
+List~SearchResponseDTO~ search(String, Pageable, Long)
+List~SearchQuery~ getRecentQueries(Long)
+List~TopQueryDTO~ getTopQueries(Long)
+List~SearchResponseDTO~ getStoredResults(String, Long)
}
class AdminService {
+AdminProfileDataDTO getProfileData(Long)
}
class Profile {
+Long id
+String displayName
+String passwordHash
+LocalDateTime createdAt
}
class SearchQuery {
+Long id
+String queryText
+Long profileId
+LocalDateTime searchedAt
}
class SearchResult {
+Long id
+String queryText
+double cosineSimilarity
+double relevanceScore
+int rank
+LocalDateTime createdAt
}
class WebPage {
+Long id
+String url
+String title
+String content
+LocalDateTime crawlTime
+LocalDateTime lastUpdated
}
AuthController --> AuthService : uses
SecurityConfig --> BearerTokenAuthenticationFilter : adds filter
BearerTokenAuthenticationFilter --> AuthTokenService : validates token
ProfileController --> ProfileService : uses
SearchController --> SearchService : uses
SearchController --> SecurityUtil : current profile
AdminController --> ProfileService : uses
AdminController --> AdminService : uses
SpaController --> SecurityConfig : route + auth alignment
AuthService --> ProfileRepository : validates profile
AuthService --> AuthTokenService : issues token
SearchService --> SearchQueryRepository : query persistence
SearchService --> SearchResultRepository : result persistence
SearchService --> WebPageRepository : page persistence
SearchService --> SerperSearchService : external search
AdminService --> ProfileRepository : reads profiles
AdminService --> SearchQueryRepository : reads history
AdminService --> SearchResultRepository : reads results
SearchResult --> SearchQuery : many-to-one
SearchResult --> WebPage : many-to-one
- Open
/profiles. - Create a profile (or select existing profile).
- Login via
POST /api/auth/profile-loginto establish session and storeaccessToken+refreshToken. - Navigate to
/and run searches (session and/or Bearer token auth applies). - Use
NextandPrevon/to reuse stored pages when available and fetch missing pages only when needed. - Use
/history,/top,/storedfor profile-specific data management. - In
/stored, edit saved rank and relevance score values directly and save changes. - On
401/403, frontend callsPOST /api/auth/refreshwithrefreshTokenand retries request. - Use Switch Profile (sidebar) to clear tokens/session hints and return to
/profiles. - Logout via
POST /api/auth/logoutand clear local tokens. - For admin analytics, login from
/admin-loginusingPOST /api/auth/admin-login. - Fetch
/api/admin/profilesand/api/admin/profiles/{id}/data.
SPRING_DATASOURCE_URLSPRING_DATASOURCE_USERNAMESPRING_DATASOURCE_PASSWORDSPRING_JPA_HIBERNATE_DDL_AUTOSPRING_JPA_SHOW_SQLSERPER_API_BASE_URLSERPER_API_KEYSECURITY_ADMIN_PASSWORDSECURITY_JWT_SECRETSECURITY_JWT_EXPIRATION_MSSECURITY_JWT_REFRESH_EXPIRATION_MSMYSQL_DATABASEMYSQL_ROOT_PASSWORD
If you see redirect loops or page bounce issues after login/logout/switch-profile:
- Clear browser auth state:
localStorage.removeItem("authToken")localStorage.removeItem("refreshToken")sessionStorage.removeItem("activeProfileId")sessionStorage.removeItem("activeProfileName")
- Refresh the page and login again from
/profilesor/admin-login. - Verify backend is running the latest build:
./mvnw -DskipTests package
- If frontend assets are stale, rebuild:
cd frontend npm run build